diff --git a/.gitignore b/.gitignore index 790a44c22f..cb4cfaada1 100644 --- a/.gitignore +++ b/.gitignore @@ -58,6 +58,7 @@ extensions/vp9/src/main/jni/libvpx_android_configs extensions/vp9/src/main/jni/libyuv # AV1 extension +extensions/av1/src/main/jni/cpu_features extensions/av1/src/main/jni/libgav1 # Opus extension diff --git a/.hgignore b/.hgignore index 7819a90ac5..a32dfee85d 100644 --- a/.hgignore +++ b/.hgignore @@ -55,6 +55,7 @@ bazel-testlogs .DS_Store cmake-build-debug dist +jacoco.exec tmp # VP9 extension diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 004f3c1ec5..18a4b2141a 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1,1854 +1,2233 @@ -# Release notes # +# Release notes -### dev-v2 (not yet released) ### +### dev-v2 (not yet released) -* Add Java FLAC extractor - ([#6406](https://github.com/google/ExoPlayer/issues/6406)). - This extractor does not support seeking and live streams. If - `DefaultExtractorsFactory` is used, this extractor is only used if the FLAC - extension is not loaded. -* Require an end time or duration for SubRip (SRT) and SubStation Alpha - (SSA/ASS) subtitles. This applies to both sidecar files & subtitles - [embedded in Matroska streams](https://matroska.org/technical/specs/subtitles/index.html). -* Reconfigure audio sink when PCM encoding changes - ([#6601](https://github.com/google/ExoPlayer/issues/6601)). -* Make `MediaSourceEventListener.LoadEventInfo` and - `MediaSourceEventListener.MediaLoadData` top-level classes. -* Rename `MediaCodecRenderer.onOutputFormatChanged` to - `MediaCodecRenderer.onOutputMediaFormatChanged`, further - clarifying the distinction between `Format` and `MediaFormat`. -* Downloads: Merge downloads in `SegmentDownloader` to improve overall download - speed ([#5978](https://github.com/google/ExoPlayer/issues/5978)). -* Allow `AdtsExtractor` to encounter EoF when calculating average frame size - ([#6700](https://github.com/google/ExoPlayer/issues/6700)). -* Make media session connector dispatch ACTION_SET_CAPTIONING_ENABLED. +* Core library: + * Add opt-in to verify correct thread usage with + `SimpleExoPlayer.setThrowsWhenUsingWrongThread(true)` + ([#4463](https://github.com/google/ExoPlayer/issues/4463)). + * Fix bug where `PlayerMessages` throw an exception after `MediaSources` + are removed from the playlist + ([#7278](https://github.com/google/ExoPlayer/issues/7278)). + * Add playbackPositionUs parameter to 'LoadControl.shouldContinueLoading'. + * The `DefaultLoadControl` default minimum buffer is set to 50 seconds, + equal to the default maximum buffer. `DefaultLoadControl` applies the + same behavior for audio and video. + * Add API in `AnalyticsListener` to report video frame processing offset. + `MediaCodecVideoRenderer` reports the event. + * Add fields `videoFrameProcessingOffsetUsSum` and + `videoFrameProcessingOffsetUsCount` in `DecoderCounters` to compute the + average video frame processing offset. + * Add playlist API + ([#6161](https://github.com/google/ExoPlayer/issues/6161)). + * Add `play` and `pause` methods to `Player`. + * Add `Player.getCurrentLiveOffset` to conveniently return the live + offset. + * Add `Player.onPlayWhenReadyChanged` with reasons. + * Add `Player.onPlaybackStateChanged` and deprecate + `Player.onPlayerStateChanged`. + * Add `Player.setAudioSessionId` to set the session ID attached to the + `AudioTrack`. + * Deprecate and rename `getPlaybackError` to `getPlayerError` for + consistency. + * Deprecate and rename `onLoadingChanged` to `onIsLoadingChanged` for + consistency. + * Deprecate `onSeekProcessed` because all seek changes happen instantly + now and listening to `onPositionDiscontinuity` is sufficient. + * Add `ExoPlayer.setPauseAtEndOfMediaItems` to let the player pause at the + end of each media item + ([#5660](https://github.com/google/ExoPlayer/issues/5660)). + * Split `setPlaybackParameter` into `setPlaybackSpeed` and + `AudioComponent.setSkipSilenceEnabled` with callbacks + `onPlaybackSpeedChanged` and + `AudioListener.onSkipSilenceEnabledChanged`. + * Make `MediaSourceEventListener.LoadEventInfo` and + `MediaSourceEventListener.MediaLoadData` top-level classes. + * Rename `MediaCodecRenderer.onOutputFormatChanged` to + `MediaCodecRenderer.onOutputMediaFormatChanged`, further clarifying the + distinction between `Format` and `MediaFormat`. + * Improve `Format` propagation within the media codec renderer + ([#6646](https://github.com/google/ExoPlayer/issues/6646)). + * Move player message-related constants from `C` to `Renderer`, to avoid + having the constants class depend on player/renderer classes. + * Split out `common` and `extractor` submodules. + * Allow to explicitly send `PlayerMessage`s at the end of a stream. + * Add `DataSpec.Builder` and deprecate most `DataSpec` constructors. + * Add `DataSpec.customData` to allow applications to pass custom data + through `DataSource` chains. + * Add a `Format.Builder` and deprecate all `Format.create*` methods and + most `Format.copyWith*` methods. + * Split `Format.bitrate` into `Format.averageBitrate` and + `Format.peakBitrate` + ([#2863](https://github.com/google/ExoPlayer/issues/2863)). + * Add option to `MergingMediaSource` to adjust the time offsets between + the merged sources + ([#6103](https://github.com/google/ExoPlayer/issues/6103)). + * `SimpleDecoderVideoRenderer` and `SimpleDecoderAudioRenderer` renamed to + `DecoderVideoRenderer` and `DecoderAudioRenderer` respectively, and + generalized to work with `Decoder` rather than `SimpleDecoder`. + * Add media item based playlist API to Player. + * Remove deprecated members in `DefaultTrackSelector`. + * Add `Player.DeviceComponent` and implement it for `SimpleExoPlayer` so + that the device volume can be controlled by player. + * Avoid throwing an exception while parsing fragmented MP4 default sample + values where the most-significant bit is set + ([#7207](https://github.com/google/ExoPlayer/issues/7207)). + * Add `SilenceMediaSource.Factory` to support tags + ([PR #7245](https://github.com/google/ExoPlayer/pull/7245)). + * Fix `AdsMediaSource` child `MediaSource`s not being released. + * Parse track titles from Matroska files + ([#7247](https://github.com/google/ExoPlayer/pull/7247)). + * Replace `CacheDataSinkFactory` and `CacheDataSourceFactory` with + `CacheDataSink.Factory` and `CacheDataSource.Factory` respectively. +* Video: Pass frame rate hint to `Surface.setFrameRate` on Android R devices. +* Text: + * Parse `` and `` tags in WebVTT subtitles (rendering is coming + later). + * Parse `text-combine-upright` CSS property (i.e. tate-chu-yoko) in WebVTT + subtitles (rendering is coming later). + * Parse `tts:combineText` property (i.e. tate-chu-yoko) in TTML subtitles + (rendering is coming later). + * Fix `SubtitlePainter` to render `EDGE_TYPE_OUTLINE` using the correct + color ([#6724](https://github.com/google/ExoPlayer/pull/6724)). + * Add support for WebVTT default + [text](https://www.w3.org/TR/webvtt1/#default-text-color) and + [background](https://www.w3.org/TR/webvtt1/#default-text-background) + colors ([PR #4178](https://github.com/google/ExoPlayer/pull/4178), + [issue #6581](https://github.com/google/ExoPlayer/issues/6581)). + * Parse `tts:ruby` and `tts:rubyPosition` properties in TTML subtitles + (rendering is coming later). + * Update WebVTT position alignment parsing to recognise `line-left`, + `center` and `line-right` as per the + [released spec](https://www.w3.org/TR/webvtt1/#webvtt-position-cue-setting) + (a + [previous draft](https://www.w3.org/TR/2014/WD-webvtt1-20141111/#dfn-webvtt-text-position-cue-setting) + used `start`, `middle` and `end`). + * Use anti-aliasing and bitmap filtering when displaying bitmap subtitles + ([#6950](https://github.com/google/ExoPlayer/pull/6950)). + * Implement timing-out of stuck CEA-608 captions (as permitted by + ANSI/CTA-608-E R-2014 Annex C.9) and set the default timeout to 16 + seconds ([#7181](https://github.com/google/ExoPlayer/issues/7181)). + * Add special-case positioning behaviour for vertical cues being rendered + horizontally. + * Implement steps 4-10 of the + [WebVTT line computation algorithm](https://www.w3.org/TR/webvtt1/#cue-computed-line). +* DRM: + * Add support for attaching DRM sessions to clear content in the demo app. + * Remove `DrmSessionManager` references from all renderers. + `DrmSessionManager` must be injected into the MediaSources using the + MediaSources factories. + * Add option to inject a custom `DefaultDrmSessionManager` into + `OfflineLicenseHelper` + ([#7078](https://github.com/google/ExoPlayer/issues/7078)). + * Remove generics from DRM components. +* Downloads and caching: + * Merge downloads in `SegmentDownloader` to improve overall download speed + ([#5978](https://github.com/google/ExoPlayer/issues/5978)). + * Replace `CacheDataSinkFactory` and `CacheDataSourceFactory` with + `CacheDataSink.Factory` and `CacheDataSource.Factory` respectively. + * Remove `DownloadConstructorHelper` and use `CacheDataSource.Factory` + directly instead. + * Update `CachedContentIndex` to use `SecureRandom` for generating the + initialization vector used to encrypt the cache contents. +* Audio: + * Add a sample count parameter to `MediaCodecRenderer.processOutputBuffer` + and `AudioSink.handleBuffer` to allow batching multiple encoded frames + in one buffer. + * No longer use a `MediaCodec` in audio passthrough mode. +* DASH: + * Merge trick play adaptation sets (i.e., adaptation sets marked with + `http://dashif.org/guidelines/trickmode`) into the same `TrackGroup` as + the main adaptation sets to which they refer. Trick play tracks are + marked with the `C.ROLE_FLAG_TRICK_PLAY` flag. + * Fix assertion failure in `SampleQueue` when playing DASH streams with + EMSG tracks ([#7273](https://github.com/google/ExoPlayer/issues/7273)). +* MP3: Add `IndexSeeker` for accurate seeks in VBR streams + ([#6787](https://github.com/google/ExoPlayer/issues/6787)). This seeker is + enabled by passing `FLAG_ENABLE_INDEX_SEEKING` to the `Mp3Extractor`. It may + require to scan a significant portion of the file for seeking, which may be + costly on large files. +* MP4: Store the Android capture frame rate only in `Format.metadata`. + `Format.frameRate` now stores the calculated frame rate. +* MPEG-TS: Fix issue where SEI NAL units were incorrectly dropped from H.265 + samples ([#7113](https://github.com/google/ExoPlayer/issues/7113)). +* Testing + * Add `TestExoPlayer`, a utility class with APIs to create + `SimpleExoPlayer` instances with fake components for testing. + * Upgrade Truth dependency from 0.44 to 1.0. + * Upgrade to JUnit 4.13-rc-2. +* UI + * Remove deperecated `exo_simple_player_view.xml` and + `exo_playback_control_view.xml` from resource. + * Add `showScrubber` and `hideScrubber` methods to DefaultTimeBar. + * Move logic of prev, next, fast forward and rewind to ControlDispatcher + ([#6926](https://github.com/google/ExoPlayer/issues/6926)). +* Metadata: Add minimal DVB Application Information Table (AIT) support + ([#6922](https://github.com/google/ExoPlayer/pull/6922)). +* Cast extension: Implement playlist API and deprecate the old queue + manipulation API. +* Demo app: Retain previous position in list of samples. +* Change the order of extractors for sniffing to reduce start-up latency in + `DefaultExtractorsFactory` and `DefaultHlsExtractorsFactory` + ([#6410](https://github.com/google/ExoPlayer/issues/6410)). +* Add missing `@Nullable` annotations to `MediaSessionConnector` + ([#7234](https://github.com/google/ExoPlayer/issues/7234)). +* AV1 extension: Add a heuristic to determine the default number of threads + used for AV1 playback using the extension. +* IMA extension: Upgrade to IMA SDK version 3.18.1, and migrate to new + preloading APIs ([#6429](https://github.com/google/ExoPlayer/issues/6429)). -### 2.11.0 (not yet released) ### +### 2.11.4 (2020-04-08) -* Core library: - * Replace `ExoPlayerFactory` by `SimpleExoPlayer.Builder` and - `ExoPlayer.Builder`. - * Add automatic `WakeLock` handling to `SimpleExoPlayer`, which can be enabled - by calling `SimpleExoPlayer.setHandleWakeLock` - ([#5846](https://github.com/google/ExoPlayer/issues/5846)). To use this +* Add `SimpleExoPlayer.setWakeMode` to allow automatic `WifiLock` and + `WakeLock` handling + ([#6914](https://github.com/google/ExoPlayer/issues/6914)). To use this feature, you must add the [WAKE_LOCK](https://developer.android.com/reference/android/Manifest.permission.html#WAKE_LOCK) permission to your application's manifest file. - * Add automatic "audio becoming noisy" handling to `SimpleExoPlayer`, which - can be enabled by calling `SimpleExoPlayer.setHandleAudioBecomingNoisy`. - * Wrap decoder exceptions in a new `DecoderException` class and report them as - renderer errors. - * Add `Timeline.Window.isLive` to indicate that a window is a live stream - ([#2668](https://github.com/google/ExoPlayer/issues/2668) and - [#5973](https://github.com/google/ExoPlayer/issues/5973)). - * Add `Timeline.Window.uid` to uniquely identify window instances. - * Deprecate `setTag` parameter of `Timeline.getWindow`. Tags will always be - set. - * Deprecate passing the manifest directly to - `Player.EventListener.onTimelineChanged`. It can be accessed through - `Timeline.Window.manifest` or `Player.getCurrentManifest()` - * Add `MediaSource.enable` and `MediaSource.disable` to improve resource - management in playlists. - * Add `MediaPeriod.isLoading` to improve `Player.isLoading` state. - * Fix issue where player errors are thrown too early at playlist transitions +* Text: + * Catch and log exceptions in `TextRenderer` rather than re-throwing. This + allows playback to continue even if subtitle decoding fails + ([#6885](https://github.com/google/ExoPlayer/issues/6885)). + * Allow missing hours and milliseconds in SubRip (.srt) timecodes + ([#7122](https://github.com/google/ExoPlayer/issues/7122)). +* Audio: + * Enable playback speed adjustment and silence skipping for floating point + PCM audio, via resampling to 16-bit integer PCM. To output the original + floating point audio without adjustment, pass `enableFloatOutput=true` + to the `DefaultAudioSink` constructor + ([#7134](https://github.com/google/ExoPlayer/issues/7134)). + * Workaround issue that could cause slower than realtime playback of AAC + on Android 10 ([#6671](https://github.com/google/ExoPlayer/issues/6671). + * Fix case where another app spuriously holding transient audio focus + could prevent ExoPlayer from acquiring audio focus for an indefinite + period of time + ([#7182](https://github.com/google/ExoPlayer/issues/7182). + * Fix case where the player volume could be permanently ducked if audio + focus was released whilst ducking. + * Fix playback of WAV files with trailing non-media bytes + ([#7129](https://github.com/google/ExoPlayer/issues/7129)). + * Fix playback of ADTS files with mid-stream ID3 metadata. +* DRM: + * Fix stuck ad playbacks with DRM protected content + ([#7188](https://github.com/google/ExoPlayer/issues/7188)). + * Fix playback of Widevine protected content that only provides V1 PSSH + atoms on API levels 21 and 22. + * Fix playback of PlayReady content on Fire TV Stick (Gen 2). +* DASH: + * Update the manifest URI to avoid repeated HTTP redirects + ([#6907](https://github.com/google/ExoPlayer/issues/6907)). + * Parse period `AssetIdentifier` elements. +* HLS: Recognize IMSC subtitles + ([#7185](https://github.com/google/ExoPlayer/issues/7185)). +* UI: Add an option to set whether to use the orientation sensor for rotation + in spherical playbacks + ([#6761](https://github.com/google/ExoPlayer/issues/6761)). +* Analytics: Fix `PlaybackStatsListener` behavior when not keeping history + ([#7160](https://github.com/google/ExoPlayer/issues/7160)). +* FFmpeg extension: Add support for `x86_64` architecture. +* Opus extension: Fix parsing of negative gain values + ([#7046](https://github.com/google/ExoPlayer/issues/7046)). +* Cast extension: Upgrade `play-services-cast-framework` dependency to 18.1.0. + This fixes an issue where `RemoteServiceException` was thrown due to + `Context.startForegroundService()` not calling `Service.startForeground()` + ([#7191](https://github.com/google/ExoPlayer/issues/7191)). + +### 2.11.3 (2020-02-19) + +* SmoothStreaming: Fix regression that broke playback in 2.11.2 + ([#6981](https://github.com/google/ExoPlayer/issues/6981)). +* DRM: Fix issue switching from protected content that uses a 16-byte + initialization vector to one that uses an 8-byte initialization vector + ([#6982](https://github.com/google/ExoPlayer/issues/6982)). + +### 2.11.2 (2020-02-13) + +* Add Java FLAC extractor + ([#6406](https://github.com/google/ExoPlayer/issues/6406)). +* Startup latency optimization: + * Reduce startup latency for DASH and SmoothStreaming playbacks by + allowing codec initialization to occur before the network connection for + the first media segment has been established. + * Reduce startup latency for on-demand DASH playbacks by allowing codec + initialization to occur before the sidx box has been loaded. +* Downloads: + * Fix download resumption when the requirements for them to continue are + met ([#6733](https://github.com/google/ExoPlayer/issues/6733), + [#6798](https://github.com/google/ExoPlayer/issues/6798)). + * Fix `DownloadHelper.createMediaSource` to use `customCacheKey` when + creating `ProgressiveMediaSource` instances. +* DRM: Fix `NullPointerException` when playing DRM protected content + ([#6951](https://github.com/google/ExoPlayer/issues/6951)). +* Metadata: + * Update `IcyDecoder` to try ISO-8859-1 decoding if UTF-8 decoding fails. + Also change `IcyInfo.rawMetadata` from `String` to `byte[]` to allow + developers to handle data that's neither UTF-8 nor ISO-8859-1 + ([#6753](https://github.com/google/ExoPlayer/issues/6753)). + * Select multiple metadata tracks if multiple metadata renderers are + available ([#6676](https://github.com/google/ExoPlayer/issues/6676)). + * Add support for ID3 genres added in Wimamp 5.6 (2010). +* UI: + * Show ad group markers in `DefaultTimeBar` even if they are after the end + of the current window + ([#6552](https://github.com/google/ExoPlayer/issues/6552)). + * Don't use notification chronometer if playback speed is != 1.0 + ([#6816](https://github.com/google/ExoPlayer/issues/6816)). +* HLS: Fix playback of DRM protected content that uses key rotation + ([#6903](https://github.com/google/ExoPlayer/issues/6903)). +* WAV: + * Support IMA ADPCM encoded data. + * Improve support for G.711 A-law and mu-law encoded data. +* MP4: Support "twos" codec (big endian PCM) + ([#5789](https://github.com/google/ExoPlayer/issues/5789)). +* FMP4: Add support for encrypted AC-4 tracks. +* HLS: Fix slow seeking into long MP3 segments + ([#6155](https://github.com/google/ExoPlayer/issues/6155)). +* Fix handling of E-AC-3 streams that contain AC-3 syncframes + ([#6602](https://github.com/google/ExoPlayer/issues/6602)). +* Fix playback of TrueHD streams in Matroska + ([#6845](https://github.com/google/ExoPlayer/issues/6845)). +* Fix MKV subtitles to disappear when intended instead of lasting until the + next cue ([#6833](https://github.com/google/ExoPlayer/issues/6833)). +* OkHttp extension: Upgrade OkHttp dependency to 3.12.8, which fixes a class + of `SocketTimeoutException` issues when using HTTP/2 + ([#4078](https://github.com/google/ExoPlayer/issues/4078)). +* FLAC extension: Fix handling of bit depths other than 16 in `FLACDecoder`. + This issue caused FLAC streams with other bit depths to sound like white + noise on earlier releases, but only when embedded in a non-FLAC container + such as Matroska or MP4. +* Demo apps: Add + [GL demo app](https://github.com/google/ExoPlayer/tree/dev-v2/demos/gl) to + show how to render video to a `GLSurfaceView` while applying a GL shader. + ([#6920](https://github.com/google/ExoPlayer/issues/6920)). + +### 2.11.1 (2019-12-20) + +* UI: Exclude `DefaultTimeBar` region from system gesture detection + ([#6685](https://github.com/google/ExoPlayer/issues/6685)). +* ProGuard fixes: + * Ensure `Libgav1VideoRenderer` constructor is kept for use by + `DefaultRenderersFactory` + ([#6773](https://github.com/google/ExoPlayer/issues/6773)). + * Ensure `VideoDecoderOutputBuffer` and its members are kept for use by + video decoder extensions. + * Ensure raw resources used with `RawResourceDataSource` are kept. + * Suppress spurious warnings about the `javax.annotation` package, and + restructure use of `IntDef` annotations to remove spurious warnings + about `SsaStyle$SsaAlignment` + ([#6771](https://github.com/google/ExoPlayer/issues/6771)). +* Fix `CacheDataSource` to correctly propagate `DataSpec.httpRequestHeaders`. +* Fix issue with `DefaultDownloadIndex` that could result in an + `IllegalStateException` being thrown from + `DefaultDownloadIndex.getDownloadForCurrentRow` + ([#6785](https://github.com/google/ExoPlayer/issues/6785)). +* Fix `IndexOutOfBoundsException` in `SinglePeriodTimeline.getWindow` + ([#6776](https://github.com/google/ExoPlayer/issues/6776)). +* Add missing `@Nullable` to `MediaCodecAudioRenderer.getMediaClock` and + `SimpleDecoderAudioRenderer.getMediaClock` + ([#6792](https://github.com/google/ExoPlayer/issues/6792)). + +### 2.11.0 (2019-12-11) + +* Core library: + * Replace `ExoPlayerFactory` by `SimpleExoPlayer.Builder` and + `ExoPlayer.Builder`. + * Add automatic `WakeLock` handling to `SimpleExoPlayer`, which can be + enabled by calling `SimpleExoPlayer.setHandleWakeLock` + ([#5846](https://github.com/google/ExoPlayer/issues/5846)). To use this + feature, you must add the + [WAKE_LOCK](https://developer.android.com/reference/android/Manifest.permission.html#WAKE_LOCK) + permission to your application's manifest file. + * Add automatic "audio becoming noisy" handling to `SimpleExoPlayer`, + which can be enabled by calling + `SimpleExoPlayer.setHandleAudioBecomingNoisy`. + * Wrap decoder exceptions in a new `DecoderException` class and report + them as renderer errors. + * Add `Timeline.Window.isLive` to indicate that a window is a live stream + ([#2668](https://github.com/google/ExoPlayer/issues/2668) and + [#5973](https://github.com/google/ExoPlayer/issues/5973)). + * Add `Timeline.Window.uid` to uniquely identify window instances. + * Deprecate `setTag` parameter of `Timeline.getWindow`. Tags will always + be set. + * Deprecate passing the manifest directly to + `Player.EventListener.onTimelineChanged`. It can be accessed through + `Timeline.Window.manifest` or `Player.getCurrentManifest()` + * Add `MediaSource.enable` and `MediaSource.disable` to improve resource + management in playlists. + * Add `MediaPeriod.isLoading` to improve `Player.isLoading` state. + * Fix issue where player errors are thrown too early at playlist + transitions ([#5407](https://github.com/google/ExoPlayer/issues/5407)). + * Add `Format` and renderer support flags to renderer + `ExoPlaybackException`s. + * Where there are multiple platform decoders for a given MIME type, prefer + to use one that advertises support for the profile and level of the + media being played over one that does not, even if it does not come + first in the `MediaCodecList`. +* DRM: + * Inject `DrmSessionManager` into the `MediaSources` instead of + `Renderers`. This allows each `MediaSource` in a + `ConcatenatingMediaSource` to use a different `DrmSessionManager` + ([#5619](https://github.com/google/ExoPlayer/issues/5619)). + * Add `DefaultDrmSessionManager.Builder`, and remove + `DefaultDrmSessionManager` static factory methods that leaked + `ExoMediaDrm` instances + ([#4721](https://github.com/google/ExoPlayer/issues/4721)). + * Add support for the use of secure decoders when playing clear content + ([#4867](https://github.com/google/ExoPlayer/issues/4867)). This can be + enabled using `DefaultDrmSessionManager.Builder`'s + `setUseDrmSessionsForClearContent` method. + * Add support for custom `LoadErrorHandlingPolicies` in key and + provisioning requests + ([#6334](https://github.com/google/ExoPlayer/issues/6334)). Custom + policies can be passed via `DefaultDrmSessionManager.Builder`'s + `setLoadErrorHandlingPolicy` method. + * Use `ExoMediaDrm.Provider` in `OfflineLicenseHelper` to avoid leaking + `ExoMediaDrm` instances + ([#4721](https://github.com/google/ExoPlayer/issues/4721)). +* Track selection: + * Update `DefaultTrackSelector` to set a viewport constraint for the + default display by default. + * Update `DefaultTrackSelector` to set text language and role flag + constraints for the device's accessibility settings by default + ([#5749](https://github.com/google/ExoPlayer/issues/5749)). + * Add option to set preferred text role flags using + `DefaultTrackSelector.ParametersBuilder.setPreferredTextRoleFlags`. +* LoadControl: + * Default `prioritizeTimeOverSizeThresholds` to false to prevent OOM + errors ([#6647](https://github.com/google/ExoPlayer/issues/6647)). +* Android 10: + * Set `compileSdkVersion` to 29 to enable use of Android 10 APIs. + * Expose new `isHardwareAccelerated`, `isSoftwareOnly` and `isVendor` + flags in `MediaCodecInfo` + ([#5839](https://github.com/google/ExoPlayer/issues/5839)). + * Add `allowedCapturePolicy` field to `AudioAttributes` to allow to + configuration of the audio capture policy. +* Video: + * Pass the codec output `MediaFormat` to `VideoFrameMetadataListener`. + * Fix byte order of HDR10+ static metadata to match CTA-861.3. + * Support out-of-band HDR10+ dynamic metadata for VP9 in WebM/Matroska. + * Assume that protected content requires a secure decoder when evaluating + whether `MediaCodecVideoRenderer` supports a given video format + ([#5568](https://github.com/google/ExoPlayer/issues/5568)). + * Fix Dolby Vision fallback to AVC and HEVC. + * Fix early end-of-stream detection when using video tunneling, on API + level 23 and above. + * Fix an issue where a keyframe was rendered rather than skipped when + performing an exact seek to a non-zero position close to the start of + the stream. +* Audio: + * Fix the start of audio getting truncated when transitioning to a new + item in a playlist of Opus streams. + * Workaround broken raw audio decoding on Oppo R9 + ([#5782](https://github.com/google/ExoPlayer/issues/5782)). + * Reconfigure audio sink when PCM encoding changes + ([#6601](https://github.com/google/ExoPlayer/issues/6601)). + * Allow `AdtsExtractor` to encounter EOF when calculating average frame + size ([#6700](https://github.com/google/ExoPlayer/issues/6700)). +* Text: + * Add support for position and overlapping start/end times in SSA/ASS + subtitles ([#6320](https://github.com/google/ExoPlayer/issues/6320)). + * Require an end time or duration for SubRip (SRT) and SubStation Alpha + (SSA/ASS) subtitles. This applies to both sidecar files & subtitles + [embedded in Matroska streams](https://matroska.org/technical/specs/subtitles/index.html). +* UI: + * Make showing and hiding player controls accessible to TalkBack in + `PlayerView`. + * Rename `spherical_view` surface type to `spherical_gl_surface_view`. + * Make it easier to override the shuffle, repeat, fullscreen, VR and small + notification icon assets + ([#6709](https://github.com/google/ExoPlayer/issues/6709)). +* Analytics: + * Remove `AnalyticsCollector.Factory`. Instances should be created + directly, and the `Player` should be set by calling + `AnalyticsCollector.setPlayer`. + * Add `PlaybackStatsListener` to collect `PlaybackStats` for analysis and + analytics reporting. +* DataSource + * Add `DataSpec.httpRequestHeaders` to support setting per-request headers + for HTTP and HTTPS. + * Remove the `DataSpec.FLAG_ALLOW_ICY_METADATA` flag. Use is replaced by + setting the `IcyHeaders.REQUEST_HEADER_ENABLE_METADATA_NAME` header in + `DataSpec.httpRequestHeaders`. + * Fail more explicitly when local file URIs contain invalid parts (e.g. a + fragment) ([#6470](https://github.com/google/ExoPlayer/issues/6470)). +* DASH: Support negative @r values in segment timelines + ([#1787](https://github.com/google/ExoPlayer/issues/1787)). +* HLS: + * Use peak bitrate rather than average bitrate for adaptive track + selection. + * Fix issue where streams could get stuck in an infinite buffering state + after a postroll ad + ([#6314](https://github.com/google/ExoPlayer/issues/6314)). +* Matroska: Support lacing in Blocks + ([#3026](https://github.com/google/ExoPlayer/issues/3026)). +* AV1 extension: + * New in this release. The AV1 extension allows use of the + [libgav1 software decoder](https://chromium.googlesource.com/codecs/libgav1/) + in ExoPlayer. You can read more about playing AV1 videos with ExoPlayer + [here](https://medium.com/google-exoplayer/playing-av1-videos-with-exoplayer-a7cb19bedef9). +* VP9 extension: + * Update to use NDK r20. + * Rename `VpxVideoSurfaceView` to `VideoDecoderSurfaceView` and move it to + the core library. + * Move `LibvpxVideoRenderer.MSG_SET_OUTPUT_BUFFER_RENDERER` to + `C.MSG_SET_OUTPUT_BUFFER_RENDERER`. + * Use `VideoDecoderRenderer` as an implementation of + `VideoDecoderOutputBufferRenderer`, instead of + `VideoDecoderSurfaceView`. +* FLAC extension: Update to use NDK r20. +* Opus extension: Update to use NDK r20. +* FFmpeg extension: + * Update to use NDK r20. + * Update to use FFmpeg version 4.2. It is necessary to rebuild the native + part of the extension after this change, following the instructions in + the extension's readme. +* MediaSession extension: Add `MediaSessionConnector.setCaptionCallback` to + support `ACTION_SET_CAPTIONING_ENABLED` events. +* GVR extension: This extension is now deprecated. +* Demo apps: + * Add + [SurfaceControl demo app](https://github.com/google/ExoPlayer/tree/r2.11.0/demos/surface) + to show how to use the Android 10 `SurfaceControl` API with ExoPlayer + ([#677](https://github.com/google/ExoPlayer/issues/677)). + * Add support for subtitle files to the + [Main demo app](https://github.com/google/ExoPlayer/tree/r2.11.0/demos/main) + ([#5523](https://github.com/google/ExoPlayer/issues/5523)). + * Remove the IMA demo app. IMA functionality is demonstrated by the + [main demo app](https://github.com/google/ExoPlayer/tree/r2.11.0/demos/main). + * Add basic DRM support to the + [Cast demo app](https://github.com/google/ExoPlayer/tree/r2.11.0/demos/cast). +* TestUtils: Publish the `testutils` module to simplify unit testing with + ExoPlayer ([#6267](https://github.com/google/ExoPlayer/issues/6267)). +* IMA extension: Remove `AdsManager` listeners on release to avoid leaking an + `AdEventListener` provided by the app + ([#6687](https://github.com/google/ExoPlayer/issues/6687)). + +### 2.10.8 (2019-11-19) + +* E-AC3 JOC + * Handle new signaling in DASH manifests + ([#6636](https://github.com/google/ExoPlayer/issues/6636)). + * Fix E-AC3 JOC passthrough playback failing to initialize due to + incorrect channel count check. +* FLAC + * Fix sniffing for some FLAC streams. + * Fix FLAC `Format.bitrate` values. +* Parse ALAC channel count and sample rate information from a more robust + source when contained in MP4 + ([#6648](https://github.com/google/ExoPlayer/issues/6648)). +* Fix seeking into multi-period content in the edge case that the period + containing the seek position has just been removed + ([#6641](https://github.com/google/ExoPlayer/issues/6641)). + +### 2.10.7 (2019-11-06) + +* HLS: Fix detection of Dolby Atmos to match the HLS authoring specification. +* MediaSession extension: Update shuffle and repeat modes when playback state + is invalidated ([#6582](https://github.com/google/ExoPlayer/issues/6582)). +* Fix the start of audio getting truncated when transitioning to a new item in + a playlist of Opus streams. + +### 2.10.6 (2019-10-17) + +* Add `Player.onPlaybackSuppressionReasonChanged` to allow listeners to detect + playbacks suppressions (e.g. transient audio focus loss) directly + ([#6203](https://github.com/google/ExoPlayer/issues/6203)). +* DASH: + * Support `Label` elements + ([#6297](https://github.com/google/ExoPlayer/issues/6297)). + * Support legacy audio channel configuration + ([#6523](https://github.com/google/ExoPlayer/issues/6523)). +* HLS: Add support for ID3 in EMSG when using FMP4 streams + ([spec](https://aomediacodec.github.io/av1-id3/)). +* MP3: Add workaround to avoid prematurely ending playback of some SHOUTcast + live streams ([#6537](https://github.com/google/ExoPlayer/issues/6537), + [#6315](https://github.com/google/ExoPlayer/issues/6315) and + [#5658](https://github.com/google/ExoPlayer/issues/5658)). +* Metadata: Expose the raw ICY metadata through `IcyInfo` + ([#6476](https://github.com/google/ExoPlayer/issues/6476)). +* UI: + * Setting `app:played_color` on `PlayerView` and `PlayerControlView` no + longer adjusts the colors of the scrubber handle , buffered and unplayed + parts of the time bar. These can be set separately using + `app:scrubber_color`, `app:buffered_color` and `app_unplayed_color` + respectively. + * Setting `app:ad_marker_color` on `PlayerView` and `PlayerControlView` no + longer adjusts the color of played ad markers. The color of played ad + markers can be set separately using `app:played_ad_marker_color`. + +### 2.10.5 (2019-09-20) + +* Add `Player.isPlaying` and `EventListener.onIsPlayingChanged` to check + whether the playback position is advancing. This helps to determine if + playback is suppressed due to audio focus loss. Also add + `Player.getPlaybackSuppressedReason` to determine the reason of the + suppression ([#6203](https://github.com/google/ExoPlayer/issues/6203)). +* Track selection + * Add `allowAudioMixedChannelCountAdaptiveness` parameter to + `DefaultTrackSelector` to allow adaptive selections of audio tracks with + different channel counts. + * Improve text selection logic to always prefer the better language + matches over other selection parameters. + * Fix audio selection issue where languages are compared by bitrate + ([#6335](https://github.com/google/ExoPlayer/issues/6335)). +* Performance + * Increase maximum video buffer size from 13MB to 32MB. The previous + default was too small for high quality streams. + * Reset `DefaultBandwidthMeter` to initial values on network change. + * Bypass sniffing in `ProgressiveMediaPeriod` in case a single extractor + is provided ([#6325](https://github.com/google/ExoPlayer/issues/6325)). +* Metadata + * Support EMSG V1 boxes in FMP4. + * Support unwrapping of nested metadata (e.g. ID3 and SCTE-35 in EMSG). +* Add `HttpDataSource.getResponseCode` to provide the status code associated + with the most recent HTTP response. +* Fix transitions between packed audio and non-packed audio segments in HLS + ([#6444](https://github.com/google/ExoPlayer/issues/6444)). +* Fix issue where a request would be retried after encountering an error, even + though the `LoadErrorHandlingPolicy` classified the error as fatal. +* Fix initialization data handling for FLAC in MP4 + ([#6396](https://github.com/google/ExoPlayer/issues/6396), + [#6397](https://github.com/google/ExoPlayer/issues/6397)). +* Fix decoder selection for E-AC3 JOC streams + ([#6398](https://github.com/google/ExoPlayer/issues/6398)). +* Fix `PlayerNotificationManager` to show play icon rather than pause icon + when playback is ended + ([#6324](https://github.com/google/ExoPlayer/issues/6324)). +* RTMP extension: Upgrade LibRtmp-Client-for-Android to fix RTMP playback + issues ([#4200](https://github.com/google/ExoPlayer/issues/4200), + [#4249](https://github.com/google/ExoPlayer/issues/4249), + [#4319](https://github.com/google/ExoPlayer/issues/4319), + [#4337](https://github.com/google/ExoPlayer/issues/4337)). +* IMA extension: Fix crash in `ImaAdsLoader.onTimelineChanged` + ([#5831](https://github.com/google/ExoPlayer/issues/5831)). + +### 2.10.4 (2019-07-26) + +* Offline: Add `Scheduler` implementation that uses `WorkManager`. +* Add ability to specify a description when creating notification channels via + ExoPlayer library classes. +* Switch normalized BCP-47 language codes to use 2-letter ISO 639-1 language + tags instead of 3-letter ISO 639-2 language tags. +* Ensure the `SilenceMediaSource` position is in range + ([#6229](https://github.com/google/ExoPlayer/issues/6229)). +* WAV: Calculate correct duration for clipped streams + ([#6241](https://github.com/google/ExoPlayer/issues/6241)). +* MP3: Use CBR header bitrate, not calculated bitrate. This reverts a change + from 2.9.3 ([#6238](https://github.com/google/ExoPlayer/issues/6238)). +* FLAC extension: Parse `VORBIS_COMMENT` and `PICTURE` metadata + ([#5527](https://github.com/google/ExoPlayer/issues/5527)). +* Fix issue where initial seek positions get ignored when playing a preroll ad + ([#6201](https://github.com/google/ExoPlayer/issues/6201)). +* Fix issue where invalid language tags were normalized to "und" instead of + keeping the original + ([#6153](https://github.com/google/ExoPlayer/issues/6153)). +* Fix `DataSchemeDataSource` re-opening and range requests + ([#6192](https://github.com/google/ExoPlayer/issues/6192)). +* Fix FLAC and ALAC playback on some LG devices + ([#5938](https://github.com/google/ExoPlayer/issues/5938)). +* Fix issue when calling `performClick` on `PlayerView` without + `PlayerControlView` + ([#6260](https://github.com/google/ExoPlayer/issues/6260)). +* Fix issue where playback speeds are not used in adaptive track selections + after manual selection changes for other renderers + ([#6256](https://github.com/google/ExoPlayer/issues/6256)). + +### 2.10.3 (2019-07-09) + +* Display last frame when seeking to end of stream + ([#2568](https://github.com/google/ExoPlayer/issues/2568)). +* Audio: + * Fix an issue where not all audio was played out when the configuration + for the underlying track was changing (e.g., at some period + transitions). + * Fix an issue where playback speed was applied inaccurately in playlists + ([#6117](https://github.com/google/ExoPlayer/issues/6117)). +* UI: Fix `PlayerView` incorrectly consuming touch events if no controller is + attached ([#6109](https://github.com/google/ExoPlayer/issues/6109)). +* CEA608: Fix repetition of special North American characters + ([#6133](https://github.com/google/ExoPlayer/issues/6133)). +* FLV: Fix bug that caused playback of some live streams to not start + ([#6111](https://github.com/google/ExoPlayer/issues/6111)). +* SmoothStreaming: Parse text stream `Subtype` into `Format.roleFlags`. +* MediaSession extension: Fix `MediaSessionConnector.play()` not resuming + playback ([#6093](https://github.com/google/ExoPlayer/issues/6093)). + +### 2.10.2 (2019-06-03) + +* Add `ResolvingDataSource` for just-in-time resolution of `DataSpec`s + ([#5779](https://github.com/google/ExoPlayer/issues/5779)). +* Add `SilenceMediaSource` that can be used to play silence of a given + duration ([#5735](https://github.com/google/ExoPlayer/issues/5735)). +* Offline: + * Prevent unexpected `DownloadHelper.Callback.onPrepared` callbacks after + preparation of a `DownloadHelper` fails + ([#5915](https://github.com/google/ExoPlayer/issues/5915)). + * Fix `CacheUtil.cache()` downloading too much data + ([#5927](https://github.com/google/ExoPlayer/issues/5927)). + * Fix misreporting cached bytes when caching is paused + ([#5573](https://github.com/google/ExoPlayer/issues/5573)). +* UI: + * Allow setting `DefaultTimeBar` attributes on `PlayerView` and + `PlayerControlView`. + * Change playback controls toggle from touch down to touch up events + ([#5784](https://github.com/google/ExoPlayer/issues/5784)). + * Fix issue where playback controls were not kept visible on key presses + ([#5963](https://github.com/google/ExoPlayer/issues/5963)). +* Subtitles: + * CEA-608: Handle XDS and TEXT modes + ([#5807](https://github.com/google/ExoPlayer/pull/5807)). + * TTML: Fix bitmap rendering + ([#5633](https://github.com/google/ExoPlayer/pull/5633)). +* IMA: Fix ad pod index offset calculation without preroll + ([#5928](https://github.com/google/ExoPlayer/issues/5928)). +* Add a `playWhenReady` flag to MediaSessionConnector.PlaybackPreparer methods + to indicate whether a controller sent a play or only a prepare command. This + allows to take advantage of decoder reuse with the MediaSessionConnector + ([#5891](https://github.com/google/ExoPlayer/issues/5891)). +* Add `ProgressUpdateListener` to `PlayerControlView` + ([#5834](https://github.com/google/ExoPlayer/issues/5834)). +* Add support for auto-detecting UDP streams in `DefaultDataSource` + ([#6036](https://github.com/google/ExoPlayer/pull/6036)). +* Allow enabling decoder fallback with `DefaultRenderersFactory` + ([#5942](https://github.com/google/ExoPlayer/issues/5942)). +* Gracefully handle revoked `ACCESS_NETWORK_STATE` permission + ([#6019](https://github.com/google/ExoPlayer/issues/6019)). +* Fix decoding problems when seeking back after seeking beyond a mid-roll ad + ([#6009](https://github.com/google/ExoPlayer/issues/6009)). +* Fix application of `maxAudioBitrate` for adaptive audio track groups + ([#6006](https://github.com/google/ExoPlayer/issues/6006)). +* Fix bug caused by parallel adaptive track selection using `Format`s without + bitrate information + ([#5971](https://github.com/google/ExoPlayer/issues/5971)). +* Fix bug in `CastPlayer.getCurrentWindowIndex()` + ([#5955](https://github.com/google/ExoPlayer/issues/5955)). + +### 2.10.1 (2019-05-16) + +* Offline: Add option to remove all downloads. +* HLS: Fix `NullPointerException` when using HLS chunkless preparation + ([#5868](https://github.com/google/ExoPlayer/issues/5868)). +* Fix handling of empty values and line terminators in SHOUTcast ICY metadata + ([#5876](https://github.com/google/ExoPlayer/issues/5876)). +* Fix DVB subtitles for SDK 28 + ([#5862](https://github.com/google/ExoPlayer/issues/5862)). +* Add a workaround for a decoder failure on ZTE Axon7 mini devices when + playing 48kHz audio + ([#5821](https://github.com/google/ExoPlayer/issues/5821)). + +### 2.10.0 (2019-04-15) + +* Core library: + * Improve decoder re-use between playbacks + ([#2826](https://github.com/google/ExoPlayer/issues/2826)). Read + [this blog post](https://medium.com/google-exoplayer/improved-decoder-reuse-in-exoplayer-ef4c6d99591d) + for more details. + * Rename `ExtractorMediaSource` to `ProgressiveMediaSource`. + * Fix issue where using `ProgressiveMediaSource.Factory` would mean that + `DefaultExtractorsFactory` would be kept by proguard. Custom + `ExtractorsFactory` instances must now be passed via the + `ProgressiveMediaSource.Factory` constructor, and `setExtractorsFactory` + is deprecated. + * Make the default minimum buffer size equal the maximum buffer size for + video playbacks + ([#2083](https://github.com/google/ExoPlayer/issues/2083)). + * Move `PriorityTaskManager` from `DefaultLoadControl` to + `SimpleExoPlayer`. + * Add new `ExoPlaybackException` types for remote exceptions and + out-of-memory errors. + * Use full BCP 47 language tags in `Format`. + * Do not retry failed loads whose error is `FileNotFoundException`. + * Fix issue where not resetting the position for a new `MediaSource` in + calls to `ExoPlayer.prepare` causes an `IndexOutOfBoundsException` + ([#5520](https://github.com/google/ExoPlayer/issues/5520)). +* Offline: + * Improve offline support. `DownloadManager` now tracks all offline + content, not just tasks in progress. Read + [this page](https://exoplayer.dev/downloading-media.html) for more + details. +* Caching: + * Improve performance of `SimpleCache` + ([#4253](https://github.com/google/ExoPlayer/issues/4253)). + * Cache data with unknown length by default. The previous flag to opt in + to this behavior (`DataSpec.FLAG_ALLOW_CACHING_UNKNOWN_LENGTH`) has been + replaced with an opt out flag + (`DataSpec.FLAG_DONT_CACHE_IF_LENGTH_UNKNOWN`). +* Extractors: + * MP4/FMP4: Add support for Dolby Vision. + * MP4: Fix issue handling meta atoms in some streams + ([#5698](https://github.com/google/ExoPlayer/issues/5698), + [#5694](https://github.com/google/ExoPlayer/issues/5694)). + * MP3: Add support for SHOUTcast ICY metadata + ([#3735](https://github.com/google/ExoPlayer/issues/3735)). + * MP3: Fix ID3 frame unsychronization + ([#5673](https://github.com/google/ExoPlayer/issues/5673)). + * MP3: Fix playback of badly clipped files + ([#5772](https://github.com/google/ExoPlayer/issues/5772)). + * MPEG-TS: Enable HDMV DTS stream detection only if a flag is set. By + default (i.e. if the flag is not set), the 0x82 elementary stream type + is now treated as an SCTE subtitle track + ([#5330](https://github.com/google/ExoPlayer/issues/5330)). +* Track selection: + * Add options for controlling audio track selections to + `DefaultTrackSelector` + ([#3314](https://github.com/google/ExoPlayer/issues/3314)). + * Update `TrackSelection.Factory` interface to support creating all track + selections together. + * Allow to specify a selection reason for a `SelectionOverride`. + * Select audio track based on system language if no preference is + provided. + * When no text language preference matches, only select forced text tracks + whose language matches the selected audio language. +* UI: + * Update `DefaultTimeBar` based on duration of media and add parameter to + set the minimum update interval to control the smoothness of the updates + ([#5040](https://github.com/google/ExoPlayer/issues/5040)). + * Move creation of dialogs for `TrackSelectionView`s to + `TrackSelectionDialogBuilder` and add option to select multiple + overrides. + * Change signature of `PlayerNotificationManager.NotificationListener` to + better fit service requirements. + * Add option to include navigation actions in the compact mode of + notifications created using `PlayerNotificationManager`. + * Fix issues with flickering notifications on KitKat when using + `PlayerNotificationManager` and `DownloadNotificationUtil`. For the + latter, applications should switch to using + `DownloadNotificationHelper`. + * Fix accuracy of D-pad seeking in `DefaultTimeBar` + ([#5767](https://github.com/google/ExoPlayer/issues/5767)). +* Audio: + * Allow `AudioProcessor`s to be drained of pending output after they are + reconfigured. + * Fix an issue that caused audio to be truncated at the end of a period + when switching to a new period where gapless playback information was + newly present or newly absent. + * Add support for reading AC-4 streams + ([#5303](https://github.com/google/ExoPlayer/pull/5303)). +* Video: + * Remove `MediaCodecSelector.DEFAULT_WITH_FALLBACK`. Apps should instead + signal that fallback should be used by passing `true` as the + `enableDecoderFallback` parameter when instantiating the video renderer. + * Support video tunneling when the decoder is not listed first for the + MIME type ([#3100](https://github.com/google/ExoPlayer/issues/3100)). + * Query `MediaCodecList.ALL_CODECS` when selecting a tunneling decoder + ([#5547](https://github.com/google/ExoPlayer/issues/5547)). +* DRM: + * Fix black flicker when keys rotate in DRM protected content + ([#3561](https://github.com/google/ExoPlayer/issues/3561)). + * Work around lack of LA_URL attribute in PlayReady key request init data. +* CEA-608: Improved conformance to the specification + ([#3860](https://github.com/google/ExoPlayer/issues/3860)). +* DASH: + * Parse role and accessibility descriptors into `Format.roleFlags`. + * Support multiple CEA-608 channels muxed into FMP4 representations + ([#5656](https://github.com/google/ExoPlayer/issues/5656)). +* HLS: + * Prevent unnecessary reloads of initialization segments. + * Form an adaptive track group out of audio renditions with matching name. + * Support encrypted initialization segments + ([#5441](https://github.com/google/ExoPlayer/issues/5441)). + * Parse `EXT-X-MEDIA` `CHARACTERISTICS` attribute into `Format.roleFlags`. + * Add metadata entry for HLS tracks to expose master playlist information. + * Prevent `IndexOutOfBoundsException` in some live HLS scenarios + ([#5816](https://github.com/google/ExoPlayer/issues/5816)). +* Support for playing spherical videos on Daydream. +* Cast extension: Work around Cast framework returning a limited-size queue + items list ([#4964](https://github.com/google/ExoPlayer/issues/4964)). +* VP9 extension: Remove RGB output mode and libyuv dependency, and switch to + surface YUV output as the default. Remove constructor parameters + `scaleToFit` and `useSurfaceYuvOutput`. +* MediaSession extension: + * Let apps intercept media button events + ([#5179](https://github.com/google/ExoPlayer/issues/5179)). + * Fix issue with `TimelineQueueNavigator` not publishing the queue in + shuffled order when in shuffle mode. + * Allow handling of custom commands via `registerCustomCommandReceiver`. + * Add ability to include an extras `Bundle` when reporting a custom error. +* Log warnings when extension native libraries can't be used, to help with + diagnosing playback failures + ([#5788](https://github.com/google/ExoPlayer/issues/5788)). + +### 2.9.6 (2019-02-19) + +* Remove `player` and `isTopLevelSource` parameters from + `MediaSource.prepare`. +* IMA extension: + * Require setting the `Player` on `AdsLoader` instances before playback. + * Remove deprecated `ImaAdsMediaSource`. Create `AdsMediaSource` with an + `ImaAdsLoader` instead. + * Remove deprecated `AdsMediaSource` constructors. Listen for media source + events using `AdsMediaSource.addEventListener`, and ad interaction + events by adding a listener when building `ImaAdsLoader`. + * Allow apps to register playback-related obstructing views that are on + top of their ad display containers via `AdsLoader.AdViewProvider`. + `PlayerView` implements this interface and will register its control + view. This makes it possible for ad loading SDKs to calculate ad + viewability accurately. +* DASH: Fix issue handling large `EventStream` presentation timestamps + ([#5490](https://github.com/google/ExoPlayer/issues/5490)). +* HLS: Fix transition to STATE_ENDED when playing fragmented mp4 in chunkless + preparation ([#5524](https://github.com/google/ExoPlayer/issues/5524)). +* Revert workaround for video quality problems with Amlogic decoders, as this + may cause problems for some devices and/or non-interlaced content + ([#5003](https://github.com/google/ExoPlayer/issues/5003)). + +### 2.9.5 (2019-01-31) + +* HLS: Parse `CHANNELS` attribute from `EXT-X-MEDIA` tag. +* ConcatenatingMediaSource: + * Add `Handler` parameter to methods that take a callback `Runnable`. + * Fix issue with dropped messages when releasing the source + ([#5464](https://github.com/google/ExoPlayer/issues/5464)). +* ExtractorMediaSource: Fix issue that could cause the player to get stuck + buffering at the end of the media. +* PlayerView: Fix issue preventing `OnClickListener` from receiving events + ([#5433](https://github.com/google/ExoPlayer/issues/5433)). +* IMA extension: Upgrade IMA dependency to 3.10.6. +* Cronet extension: Upgrade Cronet dependency to 71.3578.98. +* OkHttp extension: Upgrade OkHttp dependency to 3.12.1. +* MP3: Wider fix for issue where streams would play twice on some Samsung + devices ([#4519](https://github.com/google/ExoPlayer/issues/4519)). + +### 2.9.4 (2019-01-15) + +* IMA extension: Clear ads loader listeners on release + ([#4114](https://github.com/google/ExoPlayer/issues/4114)). +* SmoothStreaming: Fix support for subtitles in DRM protected streams + ([#5378](https://github.com/google/ExoPlayer/issues/5378)). +* FFmpeg extension: Treat invalid data errors as non-fatal to match the + behavior of MediaCodec + ([#5293](https://github.com/google/ExoPlayer/issues/5293)). +* GVR extension: upgrade GVR SDK dependency to 1.190.0. +* Associate fatal player errors of type SOURCE with the loading source in + `AnalyticsListener.EventTime` ([#5407](https://github.com/google/ExoPlayer/issues/5407)). -* DRM: - * Inject `DrmSessionManager` into the `MediaSources` instead of `Renderers`. - This allows each `MediaSource` in a `ConcatenatingMediaSource` to use a - different `DrmSessionManager` - ([#5619](https://github.com/google/ExoPlayer/issues/5619)). - * Add `DefaultDrmSessionManager.Builder`, and remove - `DefaultDrmSessionManager` static factory methods that leaked - `ExoMediaDrm` instances - ([#4721](https://github.com/google/ExoPlayer/issues/4721)). - * Add support for the use of secure decoders when playing clear content - ([#4867](https://github.com/google/ExoPlayer/issues/4867)). This can - be enabled using `DefaultDrmSessionManager.Builder`'s - `setUseDrmSessionsForClearContent` method. - * Add support for custom `LoadErrorHandlingPolicies` in key and provisioning - requests ([#6334](https://github.com/google/ExoPlayer/issues/6334)). Custom - policies can be passed via `DefaultDrmSessionManager.Builder`'s - `setLoadErrorHandlingPolicy` method. - * Use `ExoMediaDrm.Provider` in `OfflineLicenseHelper` to avoid leaking - `ExoMediaDrm` instances - ([#4721](https://github.com/google/ExoPlayer/issues/4721)). -* Track selection: - * Update `DefaultTrackSelector` to set a viewport constraint for the default - display by default. - * Update `DefaultTrackSelector` to set text language and role flag - constraints for the device's accessibility settings by default - ([#5749](https://github.com/google/ExoPlayer/issues/5749)). - * Add option to set preferred text role flags using - `DefaultTrackSelector.ParametersBuilder.setPreferredTextRoleFlags`. -* Android 10: - * Set `compileSdkVersion` to 29 to enable use of Android 10 APIs. - * Expose new `isHardwareAccelerated`, `isSoftwareOnly` and `isVendor` flags - in `MediaCodecInfo` - ([#5839](https://github.com/google/ExoPlayer/issues/5839)). - * Add `allowedCapturePolicy` field to `AudioAttributes` to allow to - configuration of the audio capture policy. -* Video: - * Pass the codec output `MediaFormat` to `VideoFrameMetadataListener`. - * Fix byte order of HDR10+ static metadata to match CTA-861.3. - * Support out-of-band HDR10+ dynamic metadata for VP9 in WebM/Matroska. - * Assume that protected content requires a secure decoder when evaluating - whether `MediaCodecVideoRenderer` supports a given video format - ([#5568](https://github.com/google/ExoPlayer/issues/5568)). - * Fix Dolby Vision fallback to AVC and HEVC. - * Fix early end-of-stream detection when using video tunneling, on API level - 23 and above. -* Audio: - * Fix the start of audio getting truncated when transitioning to a new - item in a playlist of Opus streams. - * Workaround broken raw audio decoding on Oppo R9 - ([#5782](https://github.com/google/ExoPlayer/issues/5782)). - * Reconfigure audio sink when PCM encoding changes - ([#6601](https://github.com/google/ExoPlayer/issues/6601)). -* UI: - * Make showing and hiding player controls accessible to TalkBack in - `PlayerView`. - * Rename `spherical_view` surface type to `spherical_gl_surface_view`. - * Make it easier to override the shuffle, repeat, fullscreen, VR and small - notification icon assets - ([#6709](https://github.com/google/ExoPlayer/issues/6709)). -* Analytics: - * Remove `AnalyticsCollector.Factory`. Instances should be created directly, - and the `Player` should be set by calling `AnalyticsCollector.setPlayer`. - * Add `PlaybackStatsListener` to collect `PlaybackStats` for analysis and - analytics reporting (TODO: link to developer guide page/blog post). -* DataSource - * Add `DataSpec.httpRequestHeaders` to support setting per-request headers for - HTTP and HTTPS. - * Remove the `DataSpec.FLAG_ALLOW_ICY_METADATA` flag. Use is replaced by - setting the `IcyHeaders.REQUEST_HEADER_ENABLE_METADATA_NAME` header in - `DataSpec.httpRequestHeaders`. - * Fail more explicitly when local file URIs contain invalid parts (e.g. a - fragment) ([#6470](https://github.com/google/ExoPlayer/issues/6470)). -* DASH: Support negative @r values in segment timelines - ([#1787](https://github.com/google/ExoPlayer/issues/1787)). -* HLS: - * Use peak bitrate rather than average bitrate for adaptive track selection. - * Fix issue where streams could get stuck in an infinite buffering state - after a postroll ad - ([#6314](https://github.com/google/ExoPlayer/issues/6314)). -* AV1 extension: - * New in this release. The AV1 extension allows use of the - [libgav1 software decoder](https://chromium.googlesource.com/codecs/libgav1/) - in ExoPlayer. You can read more about playing AV1 videos with ExoPlayer - [here](https://medium.com/google-exoplayer/playing-av1-videos-with-exoplayer-a7cb19bedef9). -* VP9 extension: - * Update to use NDK r20. - * Rename `VpxVideoSurfaceView` to `VideoDecoderSurfaceView` and move it to the - core library. - * Move `LibvpxVideoRenderer.MSG_SET_OUTPUT_BUFFER_RENDERER` to - `C.MSG_SET_OUTPUT_BUFFER_RENDERER`. - * Use `VideoDecoderRenderer` as an implementation of - `VideoDecoderOutputBufferRenderer`, instead of `VideoDecoderSurfaceView`. -* Flac extension: - * Update to use NDK r20. - * Fix build - ([#6601](https://github.com/google/ExoPlayer/issues/6601). -* FFmpeg extension: - * Update to use NDK r20. - * Update to use FFmpeg version 4.2. It is necessary to rebuild the native part - of the extension after this change, following the instructions in the - extension's readme. -* Opus extension: Update to use NDK r20. -* GVR extension: This extension is now deprecated. -* Demo apps (TODO: update links to point to r2.11.0 tag): - * Add [SurfaceControl demo app](https://github.com/google/ExoPlayer/tree/dev-v2/demos/surface) - to show how to use the Android 10 `SurfaceControl` API with ExoPlayer - ([#677](https://github.com/google/ExoPlayer/issues/677)). - * Add support for subtitle files to the - [Main demo app](https://github.com/google/ExoPlayer/tree/dev-v2/demos/main) - ([#5523](https://github.com/google/ExoPlayer/issues/5523)). - * Remove the IMA demo app. IMA functionality is demonstrated by the - [main demo app](https://github.com/google/ExoPlayer/tree/dev-v2/demos/main). - * Add basic DRM support to the - [Cast demo app](https://github.com/google/ExoPlayer/tree/dev-v2/demos/cast). -* TestUtils: Publish the `testutils` module to simplify unit testing with - ExoPlayer ([#6267](https://github.com/google/ExoPlayer/issues/6267)). -* IMA extension: Remove `AdsManager` listeners on release to avoid leaking an - `AdEventListener` provided by the app - ([#6687](https://github.com/google/ExoPlayer/issues/6687)). - -### 2.10.8 (2019-11-19) ### - -* E-AC3 JOC - * Handle new signaling in DASH manifests - ([#6636](https://github.com/google/ExoPlayer/issues/6636)). - * Fix E-AC3 JOC passthrough playback failing to initialize due to incorrect - channel count check. -* FLAC - * Fix sniffing for some FLAC streams. - * Fix FLAC `Format.bitrate` values. -* Parse ALAC channel count and sample rate information from a more robust source - when contained in MP4 - ([#6648](https://github.com/google/ExoPlayer/issues/6648)). -* Fix seeking into multi-period content in the edge case that the period - containing the seek position has just been removed - ([#6641](https://github.com/google/ExoPlayer/issues/6641)). - -### 2.10.7 (2019-11-06) ### - -* HLS: Fix detection of Dolby Atmos to match the HLS authoring specification. -* MediaSession extension: Update shuffle and repeat modes when playback state - is invalidated ([#6582](https://github.com/google/ExoPlayer/issues/6582)). -* Fix the start of audio getting truncated when transitioning to a new - item in a playlist of Opus streams. - -### 2.10.6 (2019-10-17) ### - -* Add `Player.onPlaybackSuppressionReasonChanged` to allow listeners to - detect playbacks suppressions (e.g. transient audio focus loss) directly - ([#6203](https://github.com/google/ExoPlayer/issues/6203)). -* DASH: - * Support `Label` elements - ([#6297](https://github.com/google/ExoPlayer/issues/6297)). - * Support legacy audio channel configuration - ([#6523](https://github.com/google/ExoPlayer/issues/6523)). -* HLS: Add support for ID3 in EMSG when using FMP4 streams - ([spec](https://aomediacodec.github.io/av1-id3/)). -* MP3: Add workaround to avoid prematurely ending playback of some SHOUTcast - live streams ([#6537](https://github.com/google/ExoPlayer/issues/6537), - [#6315](https://github.com/google/ExoPlayer/issues/6315) and - [#5658](https://github.com/google/ExoPlayer/issues/5658)). -* Metadata: Expose the raw ICY metadata through `IcyInfo` - ([#6476](https://github.com/google/ExoPlayer/issues/6476)). -* UI: - * Setting `app:played_color` on `PlayerView` and `PlayerControlView` no longer - adjusts the colors of the scrubber handle , buffered and unplayed parts of - the time bar. These can be set separately using `app:scrubber_color`, - `app:buffered_color` and `app_unplayed_color` respectively. - * Setting `app:ad_marker_color` on `PlayerView` and `PlayerControlView` no - longer adjusts the color of played ad markers. The color of played ad - markers can be set separately using `app:played_ad_marker_color`. - -### 2.10.5 (2019-09-20) ### - -* Add `Player.isPlaying` and `EventListener.onIsPlayingChanged` to check whether - the playback position is advancing. This helps to determine if playback is - suppressed due to audio focus loss. Also add - `Player.getPlaybackSuppressedReason` to determine the reason of the - suppression ([#6203](https://github.com/google/ExoPlayer/issues/6203)). -* Track selection - * Add `allowAudioMixedChannelCountAdaptiveness` parameter to - `DefaultTrackSelector` to allow adaptive selections of audio tracks with - different channel counts. - * Improve text selection logic to always prefer the better language matches - over other selection parameters. - * Fix audio selection issue where languages are compared by bitrate - ([#6335](https://github.com/google/ExoPlayer/issues/6335)). -* Performance - * Increase maximum video buffer size from 13MB to 32MB. The previous default - was too small for high quality streams. - * Reset `DefaultBandwidthMeter` to initial values on network change. - * Bypass sniffing in `ProgressiveMediaPeriod` in case a single extractor is - provided ([#6325](https://github.com/google/ExoPlayer/issues/6325)). -* Metadata - * Support EMSG V1 boxes in FMP4. - * Support unwrapping of nested metadata (e.g. ID3 and SCTE-35 in EMSG). -* Add `HttpDataSource.getResponseCode` to provide the status code associated - with the most recent HTTP response. -* Fix transitions between packed audio and non-packed audio segments in HLS - ([#6444](https://github.com/google/ExoPlayer/issues/6444)). -* Fix issue where a request would be retried after encountering an error, even - though the `LoadErrorHandlingPolicy` classified the error as fatal. -* Fix initialization data handling for FLAC in MP4 - ([#6396](https://github.com/google/ExoPlayer/issues/6396), - [#6397](https://github.com/google/ExoPlayer/issues/6397)). -* Fix decoder selection for E-AC3 JOC streams - ([#6398](https://github.com/google/ExoPlayer/issues/6398)). -* Fix `PlayerNotificationManager` to show play icon rather than pause icon when - playback is ended ([#6324](https://github.com/google/ExoPlayer/issues/6324)). -* RTMP extension: Upgrade LibRtmp-Client-for-Android to fix RTMP playback issues - ([#4200](https://github.com/google/ExoPlayer/issues/4200), - [#4249](https://github.com/google/ExoPlayer/issues/4249), - [#4319](https://github.com/google/ExoPlayer/issues/4319), - [#4337](https://github.com/google/ExoPlayer/issues/4337)). -* IMA extension: Fix crash in `ImaAdsLoader.onTimelineChanged` - ([#5831](https://github.com/google/ExoPlayer/issues/5831)). - -### 2.10.4 (2019-07-26) ### - -* Offline: Add `Scheduler` implementation that uses `WorkManager`. -* Add ability to specify a description when creating notification channels via - ExoPlayer library classes. -* Switch normalized BCP-47 language codes to use 2-letter ISO 639-1 language - tags instead of 3-letter ISO 639-2 language tags. -* Ensure the `SilenceMediaSource` position is in range - ([#6229](https://github.com/google/ExoPlayer/issues/6229)). -* WAV: Calculate correct duration for clipped streams - ([#6241](https://github.com/google/ExoPlayer/issues/6241)). -* MP3: Use CBR header bitrate, not calculated bitrate. This reverts a change - from 2.9.3 ([#6238](https://github.com/google/ExoPlayer/issues/6238)). -* Flac extension: Parse `VORBIS_COMMENT` and `PICTURE` metadata - ([#5527](https://github.com/google/ExoPlayer/issues/5527)). -* Fix issue where initial seek positions get ignored when playing a preroll ad - ([#6201](https://github.com/google/ExoPlayer/issues/6201)). -* Fix issue where invalid language tags were normalized to "und" instead of - keeping the original - ([#6153](https://github.com/google/ExoPlayer/issues/6153)). -* Fix `DataSchemeDataSource` re-opening and range requests - ([#6192](https://github.com/google/ExoPlayer/issues/6192)). -* Fix Flac and ALAC playback on some LG devices - ([#5938](https://github.com/google/ExoPlayer/issues/5938)). -* Fix issue when calling `performClick` on `PlayerView` without - `PlayerControlView` - ([#6260](https://github.com/google/ExoPlayer/issues/6260)). -* Fix issue where playback speeds are not used in adaptive track selections - after manual selection changes for other renderers - ([#6256](https://github.com/google/ExoPlayer/issues/6256)). - -### 2.10.3 (2019-07-09) ### - -* Display last frame when seeking to end of stream - ([#2568](https://github.com/google/ExoPlayer/issues/2568)). -* Audio: - * Fix an issue where not all audio was played out when the configuration - for the underlying track was changing (e.g., at some period transitions). - * Fix an issue where playback speed was applied inaccurately in playlists - ([#6117](https://github.com/google/ExoPlayer/issues/6117)). -* UI: Fix `PlayerView` incorrectly consuming touch events if no controller is - attached ([#6109](https://github.com/google/ExoPlayer/issues/6109)). -* CEA608: Fix repetition of special North American characters - ([#6133](https://github.com/google/ExoPlayer/issues/6133)). -* FLV: Fix bug that caused playback of some live streams to not start - ([#6111](https://github.com/google/ExoPlayer/issues/6111)). -* SmoothStreaming: Parse text stream `Subtype` into `Format.roleFlags`. -* MediaSession extension: Fix `MediaSessionConnector.play()` not resuming - playback ([#6093](https://github.com/google/ExoPlayer/issues/6093)). - -### 2.10.2 (2019-06-03) ### - -* Add `ResolvingDataSource` for just-in-time resolution of `DataSpec`s - ([#5779](https://github.com/google/ExoPlayer/issues/5779)). -* Add `SilenceMediaSource` that can be used to play silence of a given - duration ([#5735](https://github.com/google/ExoPlayer/issues/5735)). -* Offline: - * Prevent unexpected `DownloadHelper.Callback.onPrepared` callbacks after - preparation of a `DownloadHelper` fails - ([#5915](https://github.com/google/ExoPlayer/issues/5915)). - * Fix `CacheUtil.cache()` downloading too much data - ([#5927](https://github.com/google/ExoPlayer/issues/5927)). - * Fix misreporting cached bytes when caching is paused - ([#5573](https://github.com/google/ExoPlayer/issues/5573)). -* UI: - * Allow setting `DefaultTimeBar` attributes on `PlayerView` and - `PlayerControlView`. - * Change playback controls toggle from touch down to touch up events - ([#5784](https://github.com/google/ExoPlayer/issues/5784)). - * Fix issue where playback controls were not kept visible on key presses - ([#5963](https://github.com/google/ExoPlayer/issues/5963)). -* Subtitles: - * CEA-608: Handle XDS and TEXT modes - ([#5807](https://github.com/google/ExoPlayer/pull/5807)). - * TTML: Fix bitmap rendering - ([#5633](https://github.com/google/ExoPlayer/pull/5633)). -* IMA: Fix ad pod index offset calculation without preroll - ([#5928](https://github.com/google/ExoPlayer/issues/5928)). -* Add a `playWhenReady` flag to MediaSessionConnector.PlaybackPreparer methods - to indicate whether a controller sent a play or only a prepare command. This - allows to take advantage of decoder reuse with the MediaSessionConnector - ([#5891](https://github.com/google/ExoPlayer/issues/5891)). -* Add `ProgressUpdateListener` to `PlayerControlView` - ([#5834](https://github.com/google/ExoPlayer/issues/5834)). -* Add support for auto-detecting UDP streams in `DefaultDataSource` - ([#6036](https://github.com/google/ExoPlayer/pull/6036)). -* Allow enabling decoder fallback with `DefaultRenderersFactory` - ([#5942](https://github.com/google/ExoPlayer/issues/5942)). -* Gracefully handle revoked `ACCESS_NETWORK_STATE` permission - ([#6019](https://github.com/google/ExoPlayer/issues/6019)). -* Fix decoding problems when seeking back after seeking beyond a mid-roll ad - ([#6009](https://github.com/google/ExoPlayer/issues/6009)). -* Fix application of `maxAudioBitrate` for adaptive audio track groups - ([#6006](https://github.com/google/ExoPlayer/issues/6006)). -* Fix bug caused by parallel adaptive track selection using `Format`s without - bitrate information - ([#5971](https://github.com/google/ExoPlayer/issues/5971)). -* Fix bug in `CastPlayer.getCurrentWindowIndex()` - ([#5955](https://github.com/google/ExoPlayer/issues/5955)). - -### 2.10.1 (2019-05-16) ### - -* Offline: Add option to remove all downloads. -* HLS: Fix `NullPointerException` when using HLS chunkless preparation - ([#5868](https://github.com/google/ExoPlayer/issues/5868)). -* Fix handling of empty values and line terminators in SHOUTcast ICY metadata - ([#5876](https://github.com/google/ExoPlayer/issues/5876)). -* Fix DVB subtitles for SDK 28 - ([#5862](https://github.com/google/ExoPlayer/issues/5862)). -* Add a workaround for a decoder failure on ZTE Axon7 mini devices when playing - 48kHz audio ([#5821](https://github.com/google/ExoPlayer/issues/5821)). - -### 2.10.0 (2019-04-15) ### - -* Core library: - * Improve decoder re-use between playbacks - ([#2826](https://github.com/google/ExoPlayer/issues/2826)). Read - [this blog post](https://medium.com/google-exoplayer/improved-decoder-reuse-in-exoplayer-ef4c6d99591d) - for more details. - * Rename `ExtractorMediaSource` to `ProgressiveMediaSource`. - * Fix issue where using `ProgressiveMediaSource.Factory` would mean that - `DefaultExtractorsFactory` would be kept by proguard. Custom - `ExtractorsFactory` instances must now be passed via the - `ProgressiveMediaSource.Factory` constructor, and `setExtractorsFactory` is - deprecated. - * Make the default minimum buffer size equal the maximum buffer size for video - playbacks ([#2083](https://github.com/google/ExoPlayer/issues/2083)). - * Move `PriorityTaskManager` from `DefaultLoadControl` to `SimpleExoPlayer`. - * Add new `ExoPlaybackException` types for remote exceptions and out-of-memory - errors. - * Use full BCP 47 language tags in `Format`. - * Do not retry failed loads whose error is `FileNotFoundException`. - * Fix issue where not resetting the position for a new `MediaSource` in calls - to `ExoPlayer.prepare` causes an `IndexOutOfBoundsException` - ([#5520](https://github.com/google/ExoPlayer/issues/5520)). -* Offline: - * Improve offline support. `DownloadManager` now tracks all offline content, - not just tasks in progress. Read - [this page](https://exoplayer.dev/downloading-media.html) for more details. -* Caching: - * Improve performance of `SimpleCache` - ([#4253](https://github.com/google/ExoPlayer/issues/4253)). - * Cache data with unknown length by default. The previous flag to opt in to - this behavior (`DataSpec.FLAG_ALLOW_CACHING_UNKNOWN_LENGTH`) has been - replaced with an opt out flag - (`DataSpec.FLAG_DONT_CACHE_IF_LENGTH_UNKNOWN`). -* Extractors: - * MP4/FMP4: Add support for Dolby Vision. - * MP4: Fix issue handling meta atoms in some streams - ([#5698](https://github.com/google/ExoPlayer/issues/5698), - [#5694](https://github.com/google/ExoPlayer/issues/5694)). - * MP3: Add support for SHOUTcast ICY metadata - ([#3735](https://github.com/google/ExoPlayer/issues/3735)). - * MP3: Fix ID3 frame unsychronization - ([#5673](https://github.com/google/ExoPlayer/issues/5673)). - * MP3: Fix playback of badly clipped files - ([#5772](https://github.com/google/ExoPlayer/issues/5772)). - * MPEG-TS: Enable HDMV DTS stream detection only if a flag is set. By default - (i.e. if the flag is not set), the 0x82 elementary stream type is now - treated as an SCTE subtitle track - ([#5330](https://github.com/google/ExoPlayer/issues/5330)). -* Track selection: - * Add options for controlling audio track selections to `DefaultTrackSelector` - ([#3314](https://github.com/google/ExoPlayer/issues/3314)). - * Update `TrackSelection.Factory` interface to support creating all track - selections together. - * Allow to specify a selection reason for a `SelectionOverride`. - * When no text language preference matches, only select forced text tracks - whose language matches the selected audio language. -* UI: - * Update `DefaultTimeBar` based on duration of media and add parameter to set - the minimum update interval to control the smoothness of the updates - ([#5040](https://github.com/google/ExoPlayer/issues/5040)). - * Move creation of dialogs for `TrackSelectionView`s to - `TrackSelectionDialogBuilder` and add option to select multiple overrides. - * Change signature of `PlayerNotificationManager.NotificationListener` to - better fit service requirements. - * Add option to include navigation actions in the compact mode of - notifications created using `PlayerNotificationManager`. - * Fix issues with flickering notifications on KitKat when using - `PlayerNotificationManager` and `DownloadNotificationUtil`. For the latter, - applications should switch to using `DownloadNotificationHelper`. - * Fix accuracy of D-pad seeking in `DefaultTimeBar` - ([#5767](https://github.com/google/ExoPlayer/issues/5767)). -* Audio: - * Allow `AudioProcessor`s to be drained of pending output after they are - reconfigured. - * Fix an issue that caused audio to be truncated at the end of a period - when switching to a new period where gapless playback information was newly - present or newly absent. - * Add support for reading AC-4 streams - ([#5303](https://github.com/google/ExoPlayer/pull/5303)). -* Video: - * Remove `MediaCodecSelector.DEFAULT_WITH_FALLBACK`. Apps should instead - signal that fallback should be used by passing `true` as the - `enableDecoderFallback` parameter when instantiating the video renderer. - * Support video tunneling when the decoder is not listed first for the MIME - type ([#3100](https://github.com/google/ExoPlayer/issues/3100)). - * Query `MediaCodecList.ALL_CODECS` when selecting a tunneling decoder - ([#5547](https://github.com/google/ExoPlayer/issues/5547)). -* DRM: - * Fix black flicker when keys rotate in DRM protected content - ([#3561](https://github.com/google/ExoPlayer/issues/3561)). - * Work around lack of LA_URL attribute in PlayReady key request init data. -* CEA-608: Improved conformance to the specification - ([#3860](https://github.com/google/ExoPlayer/issues/3860)). -* DASH: - * Parse role and accessibility descriptors into `Format.roleFlags`. - * Support multiple CEA-608 channels muxed into FMP4 representations - ([#5656](https://github.com/google/ExoPlayer/issues/5656)). -* HLS: - * Prevent unnecessary reloads of initialization segments. - * Form an adaptive track group out of audio renditions with matching name. - * Support encrypted initialization segments - ([#5441](https://github.com/google/ExoPlayer/issues/5441)). - * Parse `EXT-X-MEDIA` `CHARACTERISTICS` attribute into `Format.roleFlags`. - * Add metadata entry for HLS tracks to expose master playlist information. - * Prevent `IndexOutOfBoundsException` in some live HLS scenarios - ([#5816](https://github.com/google/ExoPlayer/issues/5816)). -* Support for playing spherical videos on Daydream. -* Cast extension: Work around Cast framework returning a limited-size queue - items list ([#4964](https://github.com/google/ExoPlayer/issues/4964)). -* VP9 extension: Remove RGB output mode and libyuv dependency, and switch to - surface YUV output as the default. Remove constructor parameters `scaleToFit` - and `useSurfaceYuvOutput`. -* MediaSession extension: - * Let apps intercept media button events - ([#5179](https://github.com/google/ExoPlayer/issues/5179)). - * Fix issue with `TimelineQueueNavigator` not publishing the queue in shuffled - order when in shuffle mode. - * Allow handling of custom commands via `registerCustomCommandReceiver`. - * Add ability to include an extras `Bundle` when reporting a custom error. -* Log warnings when extension native libraries can't be used, to help with - diagnosing playback failures - ([#5788](https://github.com/google/ExoPlayer/issues/5788)). - -### 2.9.6 (2019-02-19) ### - -* Remove `player` and `isTopLevelSource` parameters from `MediaSource.prepare`. -* IMA extension: - * Require setting the `Player` on `AdsLoader` instances before - playback. - * Remove deprecated `ImaAdsMediaSource`. Create `AdsMediaSource` with an - `ImaAdsLoader` instead. - * Remove deprecated `AdsMediaSource` constructors. Listen for media source - events using `AdsMediaSource.addEventListener`, and ad interaction events by - adding a listener when building `ImaAdsLoader`. - * Allow apps to register playback-related obstructing views that are on top of - their ad display containers via `AdsLoader.AdViewProvider`. `PlayerView` - implements this interface and will register its control view. This makes it - possible for ad loading SDKs to calculate ad viewability accurately. -* DASH: Fix issue handling large `EventStream` presentation timestamps - ([#5490](https://github.com/google/ExoPlayer/issues/5490)). -* HLS: Fix transition to STATE_ENDED when playing fragmented mp4 in chunkless - preparation ([#5524](https://github.com/google/ExoPlayer/issues/5524)). -* Revert workaround for video quality problems with Amlogic decoders, as this - may cause problems for some devices and/or non-interlaced content - ([#5003](https://github.com/google/ExoPlayer/issues/5003)). - -### 2.9.5 (2019-01-31) ### - -* HLS: Parse `CHANNELS` attribute from `EXT-X-MEDIA` tag. -* ConcatenatingMediaSource: - * Add `Handler` parameter to methods that take a callback `Runnable`. - * Fix issue with dropped messages when releasing the source - ([#5464](https://github.com/google/ExoPlayer/issues/5464)). -* ExtractorMediaSource: Fix issue that could cause the player to get stuck - buffering at the end of the media. -* PlayerView: Fix issue preventing `OnClickListener` from receiving events - ([#5433](https://github.com/google/ExoPlayer/issues/5433)). -* IMA extension: Upgrade IMA dependency to 3.10.6. -* Cronet extension: Upgrade Cronet dependency to 71.3578.98. -* OkHttp extension: Upgrade OkHttp dependency to 3.12.1. -* MP3: Wider fix for issue where streams would play twice on some Samsung - devices ([#4519](https://github.com/google/ExoPlayer/issues/4519)). - -### 2.9.4 (2019-01-15) ### - -* IMA extension: Clear ads loader listeners on release - ([#4114](https://github.com/google/ExoPlayer/issues/4114)). -* SmoothStreaming: Fix support for subtitles in DRM protected streams - ([#5378](https://github.com/google/ExoPlayer/issues/5378)). -* FFmpeg extension: Treat invalid data errors as non-fatal to match the behavior - of MediaCodec ([#5293](https://github.com/google/ExoPlayer/issues/5293)). -* GVR extension: upgrade GVR SDK dependency to 1.190.0. -* Associate fatal player errors of type SOURCE with the loading source in - `AnalyticsListener.EventTime` - ([#5407](https://github.com/google/ExoPlayer/issues/5407)). -* Add `startPositionUs` to `MediaSource.createPeriod`. This fixes an issue where - using lazy preparation in `ConcatenatingMediaSource` with an - `ExtractorMediaSource` overrides initial seek positions - ([#5350](https://github.com/google/ExoPlayer/issues/5350)). -* Add subtext to the `MediaDescriptionAdapter` of the - `PlayerNotificationManager`. -* Add workaround for video quality problems with Amlogic decoders - ([#5003](https://github.com/google/ExoPlayer/issues/5003)). -* Fix issue where sending callbacks for playlist changes may cause problems - because of parallel player access - ([#5240](https://github.com/google/ExoPlayer/issues/5240)). -* Fix issue with reusing a `ClippingMediaSource` with an inner - `ExtractorMediaSource` and a non-zero start position - ([#5351](https://github.com/google/ExoPlayer/issues/5351)). -* Fix issue where uneven track durations in MP4 streams can cause OOM problems - ([#3670](https://github.com/google/ExoPlayer/issues/3670)). - -### 2.9.3 (2018-12-20) ### - -* Captions: Support PNG subtitles in SMPTE-TT - ([#1583](https://github.com/google/ExoPlayer/issues/1583)). -* MPEG-TS: Use random access indicators to minimize the need for - `FLAG_ALLOW_NON_IDR_KEYFRAMES`. -* Downloading: Reduce time taken to remove downloads - ([#5136](https://github.com/google/ExoPlayer/issues/5136)). -* MP3: - * Use the true bitrate for constant-bitrate MP3 seeking. - * Fix issue where streams would play twice on some Samsung devices - ([#4519](https://github.com/google/ExoPlayer/issues/4519)). -* Fix regression where some audio formats were incorrectly marked as being - unplayable due to under-reporting of platform decoder capabilities - ([#5145](https://github.com/google/ExoPlayer/issues/5145)). -* Fix decode-only frame skipping on Nvidia Shield TV devices. -* Workaround for MiTV (dangal) issue when swapping output surface - ([#5169](https://github.com/google/ExoPlayer/issues/5169)). - -### 2.9.2 (2018-11-28) ### - -* HLS: - * Fix issue causing unnecessary media playlist requests when playing live - streams ([#5059](https://github.com/google/ExoPlayer/issues/5059)). - * Fix decoder re-instantiation issue for packed audio streams - ([#5063](https://github.com/google/ExoPlayer/issues/5063)). -* MP4: Support Opus and FLAC in the MP4 container, and in DASH - ([#4883](https://github.com/google/ExoPlayer/issues/4883)). -* DASH: Fix detecting the end of live events - ([#4780](https://github.com/google/ExoPlayer/issues/4780)). -* Spherical video: Fall back to `TYPE_ROTATION_VECTOR` if - `TYPE_GAME_ROTATION_VECTOR` is unavailable - ([#5119](https://github.com/google/ExoPlayer/issues/5119)). -* Support seeking for a wider range of MPEG-TS streams - ([#5097](https://github.com/google/ExoPlayer/issues/5097)). -* Include channel count in audio capabilities check - ([#4690](https://github.com/google/ExoPlayer/issues/4690)). -* Fix issue with applying the `show_buffering` attribute in `PlayerView` - ([#5139](https://github.com/google/ExoPlayer/issues/5139)). -* Fix issue where null `Metadata` was output when it failed to decode - ([#5149](https://github.com/google/ExoPlayer/issues/5149)). -* Fix playback of some invalid but playable MP4 streams by replacing assertions - with logged warnings in sample table parsing code - ([#5162](https://github.com/google/ExoPlayer/issues/5162)). -* Fix UUID passed to `MediaCrypto` when using `C.CLEARKEY_UUID` before API 27. - -### 2.9.1 (2018-11-01) ### - -* Add convenience methods `Player.next`, `Player.previous`, `Player.hasNext` - and `Player.hasPrevious` - ([#4863](https://github.com/google/ExoPlayer/issues/4863)). -* Improve initial bandwidth meter estimates using the current country and - network type. -* IMA extension: - * For preroll to live stream transitions, project forward the loading position - to avoid being behind the live window. - * Let apps specify whether to focus the skip button on ATV - ([#5019](https://github.com/google/ExoPlayer/issues/5019)). -* MP3: - * Support seeking based on MLLT metadata - ([#3241](https://github.com/google/ExoPlayer/issues/3241)). - * Fix handling of streams with appended data - ([#4954](https://github.com/google/ExoPlayer/issues/4954)). -* DASH: Parse ProgramInformation element if present in the manifest. -* HLS: - * Add constructor to `DefaultHlsExtractorFactory` for adding TS payload - reader factory flags - ([#4861](https://github.com/google/ExoPlayer/issues/4861)). - * Fix bug in segment sniffing - ([#5039](https://github.com/google/ExoPlayer/issues/5039)). -* SubRip: Add support for alignment tags, and remove tags from the displayed - captions ([#4306](https://github.com/google/ExoPlayer/issues/4306)). -* Fix issue with blind seeking to windows with non-zero offset in a - `ConcatenatingMediaSource` - ([#4873](https://github.com/google/ExoPlayer/issues/4873)). -* Fix logic for enabling next and previous actions in `TimelineQueueNavigator` - ([#5065](https://github.com/google/ExoPlayer/issues/5065)). -* Fix issue where audio focus handling could not be disabled after enabling it - ([#5055](https://github.com/google/ExoPlayer/issues/5055)). -* Fix issue where subtitles were positioned incorrectly if `SubtitleView` had a - non-zero position offset to its parent - ([#4788](https://github.com/google/ExoPlayer/issues/4788)). -* Fix issue where the buffered position was not updated correctly when - transitioning between periods - ([#4899](https://github.com/google/ExoPlayer/issues/4899)). -* Fix issue where a `NullPointerException` is thrown when removing an unprepared - media source from a `ConcatenatingMediaSource` with the `useLazyPreparation` - option enabled ([#4986](https://github.com/google/ExoPlayer/issues/4986)). -* Work around an issue where a non-empty end-of-stream audio buffer would be - output with timestamp zero, causing the player position to jump backwards - ([#5045](https://github.com/google/ExoPlayer/issues/5045)). -* Suppress a spurious assertion failure on some Samsung devices - ([#4532](https://github.com/google/ExoPlayer/issues/4532)). -* Suppress spurious "references unknown class member" shrinking warning - ([#4890](https://github.com/google/ExoPlayer/issues/4890)). -* Swap recommended order for google() and jcenter() in gradle config - ([#4997](https://github.com/google/ExoPlayer/issues/4997)). - -### 2.9.0 (2018-09-06) ### - -* Turn on Java 8 compiler support for the ExoPlayer library. Apps may need to - add `compileOptions { targetCompatibility JavaVersion.VERSION_1_8 }` to their - gradle settings to ensure bytecode compatibility. -* Set `compileSdkVersion` and `targetSdkVersion` to 28. -* Support for automatic audio focus handling via - `SimpleExoPlayer.setAudioAttributes`. -* Add `ExoPlayer.retry` convenience method. -* Add `AudioListener` for listening to changes in audio configuration during - playback ([#3994](https://github.com/google/ExoPlayer/issues/3994)). -* Add `LoadErrorHandlingPolicy` to allow configuration of load error handling - across `MediaSource` implementations - ([#3370](https://github.com/google/ExoPlayer/issues/3370)). -* Allow passing a `Looper`, which specifies the thread that must be used to - access the player, when instantiating player instances using - `ExoPlayerFactory` ([#4278](https://github.com/google/ExoPlayer/issues/4278)). -* Allow setting log level for ExoPlayer logcat output - ([#4665](https://github.com/google/ExoPlayer/issues/4665)). -* Simplify `BandwidthMeter` injection: The `BandwidthMeter` should now be - passed directly to `ExoPlayerFactory`, instead of to `TrackSelection.Factory` - and `DataSource.Factory`. The `BandwidthMeter` is passed to the components - that need it internally. The `BandwidthMeter` may also be omitted, in which - case a default instance will be used. -* Spherical video: - * Support for spherical video by setting `surface_type="spherical_view"` on - `PlayerView`. - * Support for - [VR180](https://github.com/google/spatial-media/blob/master/docs/vr180.md). -* HLS: - * Support PlayReady. - * Add container format sniffing - ([#2025](https://github.com/google/ExoPlayer/issues/2025)). - * Support alternative `EXT-X-KEY` tags. - * Support `EXT-X-INDEPENDENT-SEGMENTS` in the master playlist. - * Support variable substitution - ([#4422](https://github.com/google/ExoPlayer/issues/4422)). - * Fix the bitrate being unset on primary track sample formats - ([#3297](https://github.com/google/ExoPlayer/issues/3297)). - * Make `HlsMediaSource.Factory` take a factory of trackers instead of a - tracker instance ([#4814](https://github.com/google/ExoPlayer/issues/4814)). -* DASH: - * Support `messageData` attribute for in-manifest event streams. - * Clip periods to their specified durations - ([#4185](https://github.com/google/ExoPlayer/issues/4185)). -* Improve seeking support for progressive streams: - * 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)). 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. - 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 on all extractors - that support it. -* Video: - * Add callback to `VideoListener` to notify of surface size changes. - * Improve performance when playing high frame-rate content, and when playing - at greater than 1x speed - ([#2777](https://github.com/google/ExoPlayer/issues/2777)). - * Scale up the initial video decoder maximum input size so playlist - transitions with small increases in maximum sample size do not require - reinitialization ([#4510](https://github.com/google/ExoPlayer/issues/4510)). - * Fix a bug where the player would not transition to the ended state when - playing video in tunneled mode. -* Audio: - * Support attaching auxiliary audio effects to the `AudioTrack` via - `Player.setAuxEffectInfo` and `Player.clearAuxEffectInfo`. - * Support seamless adaptation while playing xHE-AAC streams. - ([#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)). - * WAV: Fix issue where white noise would be output at the end of playback - ([#4724](https://github.com/google/ExoPlayer/issues/4724)). - * MP3: Fix issue where streams would play twice on the SM-T530 - ([#4519](https://github.com/google/ExoPlayer/issues/4519)). -* Analytics: - * Add callbacks to `DefaultDrmSessionEventListener` and `AnalyticsListener` to - be notified of acquired and released DRM sessions. - * Add uri field to `LoadEventInfo` in `MediaSourceEventListener` and - `AnalyticsListener` callbacks. This uri is the redirected uri if redirection - occurred ([#2054](https://github.com/google/ExoPlayer/issues/2054)). - * Add response headers field to `LoadEventInfo` in `MediaSourceEventListener` - and `AnalyticsListener` callbacks - ([#4361](https://github.com/google/ExoPlayer/issues/4361) and - [#4615](https://github.com/google/ExoPlayer/issues/4615)). -* UI: - * Add option to `PlayerView` 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. -* ConcatenatingMediaSource: - * Support lazy preparation of playlist media sources - ([#3972](https://github.com/google/ExoPlayer/issues/3972)). - * Support range removal with `removeMediaSourceRange` methods - ([#4542](https://github.com/google/ExoPlayer/issues/4542)). - * Support setting a new shuffle order with `setShuffleOrder` - ([#4791](https://github.com/google/ExoPlayer/issues/4791)). -* MPEG-TS: Support CEA-608/708 in H262 - ([#2565](https://github.com/google/ExoPlayer/issues/2565)). -* Allow configuration of the back buffer in `DefaultLoadControl.Builder` - ([#4857](https://github.com/google/ExoPlayer/issues/4857)). -* Allow apps to pass a `CacheKeyFactory` for setting custom cache keys when - creating a `CacheDataSource`. -* Provide additional information for adaptive track selection. - `TrackSelection.updateSelectedTrack` has two new parameters for the current - queue of media chunks and iterators for information about upcoming chunks. -* 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)). -* Enable gzip for requests made by `SingleSampleMediaSource` - ([#4771](https://github.com/google/ExoPlayer/issues/4771)). -* Fix bug reporting buffered position for multi-period windows, and add - convenience methods `Player.getTotalBufferedDuration` and - `Player.getContentBufferedDuration` - ([#4023](https://github.com/google/ExoPlayer/issues/4023)). -* Fix bug where transitions to clipped media sources would happen too early - ([#4583](https://github.com/google/ExoPlayer/issues/4583)). -* Fix bugs reporting events for multi-period media sources - ([#4492](https://github.com/google/ExoPlayer/issues/4492) and - [#4634](https://github.com/google/ExoPlayer/issues/4634)). -* Fix issue where removing looping media from a playlist throws an exception - ([#4871](https://github.com/google/ExoPlayer/issues/4871). -* Fix issue where the preferred audio or text track would not be selected if - mapped onto a secondary renderer of the corresponding type - ([#4711](http://github.com/google/ExoPlayer/issues/4711)). -* Fix issue where errors of upcoming playlist items are thrown too early - ([#4661](https://github.com/google/ExoPlayer/issues/4661)). -* Allow edit lists which do not start with a sync sample. - ([#4774](https://github.com/google/ExoPlayer/issues/4774)). -* Fix issue with audio discontinuities at period transitions, e.g. when - looping ([#3829](https://github.com/google/ExoPlayer/issues/3829)). -* Fix issue where `player.getCurrentTag()` throws an `IndexOutOfBoundsException` - ([#4822](https://github.com/google/ExoPlayer/issues/4822)). -* Fix bug preventing use of multiple key session support (`multiSession=true`) - for non-Widevine `DefaultDrmSessionManager` instances - ([#4834](https://github.com/google/ExoPlayer/issues/4834)). -* Fix issue where audio and video would desynchronize when playing - concatenations of gapless content - ([#4559](https://github.com/google/ExoPlayer/issues/4559)). -* IMA extension: - * Refine the previous fix for empty ad groups to avoid discarding ad breaks - unnecessarily ([#4030](https://github.com/google/ExoPlayer/issues/4030) and - [#4280](https://github.com/google/ExoPlayer/issues/4280)). - * Fix handling of empty postrolls - ([#4681](https://github.com/google/ExoPlayer/issues/4681)). - * Fix handling of postrolls with multiple ads - ([#4710](https://github.com/google/ExoPlayer/issues/4710)). -* MediaSession extension: - * Add `MediaSessionConnector.setCustomErrorMessage` to support setting custom - error messages. - * Add `MediaMetadataProvider` to support setting custom metadata - ([#3497](https://github.com/google/ExoPlayer/issues/3497)). -* Cronet extension: Now distributed via jCenter. -* FFmpeg extension: Support mu-law and A-law PCM. - -### 2.8.4 (2018-08-17) ### - -* IMA extension: 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 (2018-07-23) ### - -* IMA extension: - * 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 (2018-06-06) ### - -* IMA extension: 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 (2018-05-22) ### - -* 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. -* UI: - * Add `PlayerView.setKeepContentOnPlayerReset` to keep the currently displayed - video frame or media artwork visible when the player is reset - ([#2843](https://github.com/google/ExoPlayer/issues/2843)). -* Fix crash when switching surface on Moto E(4) - ([#4134](https://github.com/google/ExoPlayer/issues/4134)). -* Fix a bug that could cause event listeners to be called with inconsistent - information if an event listener interacted with the player - ([#4262](https://github.com/google/ExoPlayer/issues/4262)). -* Audio: - * Fix extraction of PCM in MP4/MOV - ([#4228](https://github.com/google/ExoPlayer/issues/4228)). - * FLAC: Supports seeking for FLAC files without SEEKTABLE - ([#1808](https://github.com/google/ExoPlayer/issues/1808)). -* 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. - * Fix an issue when the caption line has no text (empty line or only line - break), and the line's background is still displayed. - * Support TTML font size using % correctly (as percentage of document cell - resolution). - -### 2.8.0 (2018-05-03) ### - -* Downloading: - * Add `DownloadService`, `DownloadManager` and related classes - ([#2643](https://github.com/google/ExoPlayer/issues/2643)). Information on - using these components to download progressive formats can be found - [here](https://medium.com/google-exoplayer/downloading-streams-6d259eec7f95). - To see how to download DASH, HLS and SmoothStreaming media, take a look at - the app. - * Updated main demo app to support downloading DASH, HLS, SmoothStreaming and - progressive media. -* MediaSources: - * Allow reusing media sources after they have been released and - also in parallel to allow adding them multiple times to a concatenation. - ([#3498](https://github.com/google/ExoPlayer/issues/3498)). - * Merged `DynamicConcatenatingMediaSource` into `ConcatenatingMediaSource` and - deprecated `DynamicConcatenatingMediaSource`. - * Allow clipping of child media sources where the period and window have a - non-zero offset with `ClippingMediaSource`. - * Allow adding and removing `MediaSourceEventListener`s to MediaSources after - they have been created. Listening to events is now supported for all - media sources including composite sources. - * Added callbacks to `MediaSourceEventListener` to get notified when media - 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 `Player.getCurrentTag`. -* UI: - * Add support for displaying error messages and a buffering spinner in - `PlayerView`. - * Add support for listening to `AspectRatioFrameLayout`'s aspect ratio update - ([#3736](https://github.com/google/ExoPlayer/issues/3736)). - * Add `PlayerNotificationManager` for displaying notifications reflecting the - player state. - * Add `TrackSelectionView` for selecting tracks with `DefaultTrackSelector`. - * Add `TrackNameProvider` for converting track `Format`s to textual - descriptions, and `DefaultTrackNameProvider` as a default implementation. -* Track selection: - * Reworked `MappingTrackSelector` and `DefaultTrackSelector`. - * `DefaultTrackSelector.Parameters` now implements `Parcelable`. - * Added UI components for track selection (see above). -* Audio: - * Support extracting data from AMR container formats, including both narrow - and wide band ([#2527](https://github.com/google/ExoPlayer/issues/2527)). - * FLAC: - * Sniff FLAC files correctly if they have ID3 headers - ([#4055](https://github.com/google/ExoPlayer/issues/4055)). - * Supports FLAC files with high sample rate (176400 and 192000) - ([#3769](https://github.com/google/ExoPlayer/issues/3769)). - * Factor out `AudioTrack` position tracking from `DefaultAudioSink`. - * Fix an issue where the playback position would pause just after playback - begins, and poll the audio timestamp less frequently once it starts - advancing ([#3841](https://github.com/google/ExoPlayer/issues/3841)). - * Add an option to skip silent audio in `PlaybackParameters` - ([#2635](https://github.com/google/ExoPlayer/issues/2635)). - * Fix an issue where playback of TrueHD streams would get stuck after seeking - due to not finding a syncframe - ([#3845](https://github.com/google/ExoPlayer/issues/3845)). - * Fix an issue with eac3-joc playback where a codec would fail to configure - ([#4165](https://github.com/google/ExoPlayer/issues/4165)). - * Handle non-empty end-of-stream buffers, to fix gapless playback of streams - with encoder padding when the decoder returns a non-empty final buffer. - * Allow trimming more than one sample when applying an elst audio edit via - gapless playback info. - * Allow overriding skipping/scaling with custom `AudioProcessor`s - ([#3142](https://github.com/google/ExoPlayer/issues/3142)). -* Caching: - * Add release method to the `Cache` interface, and prevent multiple instances - of `SimpleCache` using the same folder at the same time. - * Cache redirect URLs - ([#2360](https://github.com/google/ExoPlayer/issues/2360)). -* DRM: - * Allow multiple listeners for `DefaultDrmSessionManager`. - * Pass `DrmSessionManager` to `ExoPlayerFactory` instead of `RendererFactory`. - * Change minimum API requirement for CBC and pattern encryption from 24 to 25 - ([#4022](https://github.com/google/ExoPlayer/issues/4022)). - * Fix handling of 307/308 redirects when making license requests - ([#4108](https://github.com/google/ExoPlayer/issues/4108)). -* HLS: - * Fix playlist loading error propagation when the current selection does - not include all of the playlist's variants. - * Fix SAMPLE-AES-CENC and SAMPLE-AES-CTR EXT-X-KEY methods - ([#4145](https://github.com/google/ExoPlayer/issues/4145)). - * Preeptively declare an ID3 track in chunkless preparation - ([#4016](https://github.com/google/ExoPlayer/issues/4016)). - * Add support for multiple #EXT-X-MAP tags in a media playlist - ([#4164](https://github.com/google/ExoPlayer/issues/4182)). - * Fix seeking in live streams - ([#4187](https://github.com/google/ExoPlayer/issues/4187)). -* IMA extension: - * Allow setting the ad media load timeout - ([#3691](https://github.com/google/ExoPlayer/issues/3691)). - * Expose ad load errors via `MediaSourceEventListener` on `AdsMediaSource`, - and allow setting an ad event listener on `ImaAdsLoader`. Deprecate the - `AdsMediaSource.EventListener`. -* Add `AnalyticsListener` interface which can be registered in - `SimpleExoPlayer` to receive detailed metadata for each ExoPlayer event. -* Optimize seeking in FMP4 by enabling seeking to the nearest sync sample within - a fragment. This benefits standalone FMP4 playbacks, DASH and SmoothStreaming. -* Updated default max buffer length in `DefaultLoadControl`. -* Fix ClearKey decryption error if the key contains a forward slash - ([#4075](https://github.com/google/ExoPlayer/issues/4075)). -* Fix crash when switching surface on Huawei P9 Lite - ([#4084](https://github.com/google/ExoPlayer/issues/4084)), and Philips QM163E - ([#4104](https://github.com/google/ExoPlayer/issues/4104)). -* Support ZLIB compressed PGS subtitles. -* Added `getPlaybackError` to `Player` interface. -* Moved initial bitrate estimate from `AdaptiveTrackSelection` to - `DefaultBandwidthMeter`. -* Removed default renderer time offset of 60000000 from internal player. The - actual renderer timestamp offset can be obtained by listening to - `BaseRenderer.onStreamChanged`. -* Added dependencies on checkerframework annotations for static code analysis. - -### 2.7.3 (2018-04-04) ### - -* Fix ProGuard configuration for Cast, IMA and OkHttp extensions. -* Update OkHttp extension to depend on OkHttp 3.10.0. - -### 2.7.2 (2018-03-29) ### - -* Gradle: Upgrade Gradle version from 4.1 to 4.4 so it can work with Android - Studio 3.1 ([#3708](https://github.com/google/ExoPlayer/issues/3708)). -* Match codecs starting with "mp4a" to different Audio MimeTypes - ([#3779](https://github.com/google/ExoPlayer/issues/3779)). -* Fix ANR issue on Redmi 4X and Redmi Note 4 - ([#4006](https://github.com/google/ExoPlayer/issues/4006)). -* Fix handling of zero padded strings when parsing Matroska streams - ([#4010](https://github.com/google/ExoPlayer/issues/4010)). -* Fix "Decoder input buffer too small" error when playing some FLAC streams. -* MediaSession extension: Omit fast forward and rewind actions when media is not - seekable ([#4001](https://github.com/google/ExoPlayer/issues/4001)). - -### 2.7.1 (2018-03-09) ### - -* Gradle: Replaced 'compile' (deprecated) with 'implementation' and - 'api'. This may lead to build breakage for applications upgrading from - previous version that rely on indirect dependencies of certain modules. In - such cases, application developers need to add the missing dependency to - their gradle file. You can read more about the new dependency configurations - [here](https://developer.android.com/studio/build/gradle-plugin-3-0-0-migration.html#new_configurations). -* HlsMediaSource: Make HLS periods start at zero instead of the epoch. - Applications that rely on HLS timelines having a period starting at - the epoch will need to update their handling of HLS timelines. The program - date time is still available via the informational - `Timeline.Window.windowStartTimeMs` field - ([#3865](https://github.com/google/ExoPlayer/issues/3865), - [#3888](https://github.com/google/ExoPlayer/issues/3888)). -* Enable seeking in MP4 streams where duration is set incorrectly in the track - header ([#3926](https://github.com/google/ExoPlayer/issues/3926)). -* Video: Force rendering a frame periodically in `MediaCodecVideoRenderer` and - `LibvpxVideoRenderer`, even if it is late. - -### 2.7.0 (2018-02-19) ### - -* Player interface: - * Add optional parameter to `stop` to reset the player when stopping. - * Add a reason to `EventListener.onTimelineChanged` to distinguish between - initial preparation, reset and dynamic updates. - * Add `Player.DISCONTINUITY_REASON_AD_INSERTION` to the possible reasons - reported in `Eventlistener.onPositionDiscontinuity` to distinguish - transitions to and from ads within one period from transitions between - periods. - * Replaced `ExoPlayer.sendMessages` with `ExoPlayer.createMessage` to allow - more customization of the message. Now supports setting a message delivery - playback position and/or a delivery handler - ([#2189](https://github.com/google/ExoPlayer/issues/2189)). - * Add `Player.VideoComponent`, `Player.TextComponent` and - `Player.MetadataComponent` interfaces that define optional video, text and - metadata output functionality. New `getVideoComponent`, `getTextComponent` - and `getMetadataComponent` methods provide access to this functionality. -* Add `ExoPlayer.setSeekParameters` for controlling how seek operations are - performed. The `SeekParameters` class contains defaults for exact seeking and - seeking to the closest sync points before, either side or after specified seek - positions. `SeekParameters` are not currently supported when playing HLS - streams. -* DefaultTrackSelector: - * Replace `DefaultTrackSelector.Parameters` copy methods with a builder. - * Support disabling of individual text track selection flags. -* Buffering: - * Allow a back-buffer of media to be retained behind the current playback - position, for fast backward seeking. The back-buffer can be configured by - custom `LoadControl` implementations. - * Add ability for `SequenceableLoader` to re-evaluate its buffer and discard - buffered media so that it can be re-buffered in a different quality. - * Allow more flexible loading strategy when playing media containing multiple - sub-streams, by allowing injection of custom `CompositeSequenceableLoader` - factories through `DashMediaSource.Factory`, `HlsMediaSource.Factory`, - `SsMediaSource.Factory`, and `MergingMediaSource`. - * Play out existing buffer before retrying for progressive live streams - ([#1606](https://github.com/google/ExoPlayer/issues/1606)). -* UI: - * Generalized player and control views to allow them to bind with any - `Player`, and renamed them to `PlayerView` and `PlayerControlView` - respectively. - * Made `PlayerView` automatically apply video rotation when configured to use - `TextureView` ([#91](https://github.com/google/ExoPlayer/issues/91)). - * Made `PlayerView` play button behave correctly when the player is ended - ([#3689](https://github.com/google/ExoPlayer/issues/3689)), and call a - `PlaybackPreparer` when the player is idle. -* DRM: Optimistically attempt playback of DRM protected content that does not - declare scheme specific init data in the manifest. If playback of clear - samples without keys is allowed, delay DRM session error propagation until - keys are actually needed - ([#3630](https://github.com/google/ExoPlayer/issues/3630)). -* DASH: - * Support in-band Emsg events targeting the player with scheme id - `urn:mpeg:dash:event:2012` and scheme values "1", "2" and "3". - * Support EventStream elements in DASH manifests. -* HLS: - * Add opt-in support for chunkless preparation in HLS. This allows an - HLS source to finish preparation without downloading any chunks, which can - significantly reduce initial buffering time - ([#3149](https://github.com/google/ExoPlayer/issues/3149)). More details - can be found - [here](https://medium.com/google-exoplayer/faster-hls-preparation-f6611aa15ea6). - * Fail if unable to sync with the Transport Stream, rather than entering - stuck in an indefinite buffering state. - * Fix mime type propagation - ([#3653](https://github.com/google/ExoPlayer/issues/3653)). - * Fix ID3 context reuse across segment format changes - ([#3622](https://github.com/google/ExoPlayer/issues/3622)). - * Use long for media sequence numbers - ([#3747](https://github.com/google/ExoPlayer/issues/3747)) - * Add initial support for the EXT-X-GAP tag. -* Audio: - * Support TrueHD passthrough for rechunked samples in Matroska files - ([#2147](https://github.com/google/ExoPlayer/issues/2147)). - * Support resampling 24-bit and 32-bit integer to 32-bit float for high - resolution output in `DefaultAudioSink` - ([#3635](https://github.com/google/ExoPlayer/pull/3635)). -* Captions: - * Basic support for PGS subtitles - ([#3008](https://github.com/google/ExoPlayer/issues/3008)). - * Fix handling of CEA-608 captions where multiple buffers have the same - presentation timestamp - ([#3782](https://github.com/google/ExoPlayer/issues/3782)). -* Caching: - * Fix cache corruption issue - ([#3762](https://github.com/google/ExoPlayer/issues/3762)). - * Implement periodic check in `CacheDataSource` to see whether it's possible - to switch to reading/writing the cache having initially bypassed it. -* IMA extension: - * Fix the player getting stuck when an ad group fails to load - ([#3584](https://github.com/google/ExoPlayer/issues/3584)). - * Work around loadAd not being called beore the LOADED AdEvent arrives - ([#3552](https://github.com/google/ExoPlayer/issues/3552)). - * Handle asset mismatch errors - ([#3801](https://github.com/google/ExoPlayer/issues/3801)). - * Add support for playing non-Extractor content MediaSources in - the IMA demo app - ([#3676](https://github.com/google/ExoPlayer/issues/3676)). - * Fix handling of ad tags where ad groups are out of order - ([#3716](https://github.com/google/ExoPlayer/issues/3716)). - * Fix handling of ad tags with only preroll/postroll ad groups - ([#3715](https://github.com/google/ExoPlayer/issues/3715)). - * Propagate ad media preparation errors to IMA so that the ads can be - skipped. - * Handle exceptions in IMA callbacks so that can be logged less verbosely. -* New Cast extension. Simplifies toggling between local and Cast playbacks. -* `EventLogger` moved from the demo app into the core library. -* Fix ANR issue on the Huawei P8 Lite, Huawei Y6II, Moto C+, Meizu M5C, - Lenovo K4 Note and Sony Xperia E5. - ([#3724](https://github.com/google/ExoPlayer/issues/3724), - [#3835](https://github.com/google/ExoPlayer/issues/3835)). -* Fix potential NPE when removing media sources from a - DynamicConcatenatingMediaSource - ([#3796](https://github.com/google/ExoPlayer/issues/3796)). -* Check `sys.display-size` on Philips ATVs - ([#3807](https://github.com/google/ExoPlayer/issues/3807)). -* Release `Extractor`s on the loading thread to avoid potentially leaking - resources when the playback thread has quit by the time the loading task has - completed. -* ID3: Better handle malformed ID3 data - ([#3792](https://github.com/google/ExoPlayer/issues/3792). -* Support 14-bit mode and little endianness in DTS PES packets - ([#3340](https://github.com/google/ExoPlayer/issues/3340)). -* Demo app: Add ability to download not DRM protected content. - -### 2.6.1 (2017-12-15) ### - -* Add factories to `ExtractorMediaSource`, `HlsMediaSource`, `SsMediaSource`, - `DashMediaSource` and `SingleSampleMediaSource`. -* Use the same listener `MediaSourceEventListener` for all MediaSource - implementations. -* IMA extension: - * Support non-ExtractorMediaSource ads - ([#3302](https://github.com/google/ExoPlayer/issues/3302)). - * Skip ads before the ad preceding the player's initial seek position - ([#3527](https://github.com/google/ExoPlayer/issues/3527)). - * Fix ad loading when there is no preroll. - * Add an option to turn off hiding controls during ad playback - ([#3532](https://github.com/google/ExoPlayer/issues/3532)). - * Support specifying an ads response instead of an ad tag - ([#3548](https://github.com/google/ExoPlayer/issues/3548)). - * Support overriding the ad load timeout - ([#3556](https://github.com/google/ExoPlayer/issues/3556)). -* DASH: Support time zone designators in ISO8601 UTCTiming elements - ([#3524](https://github.com/google/ExoPlayer/issues/3524)). -* Audio: - * Support 32-bit PCM float output from `DefaultAudioSink`, and add an option - to use this with `FfmpegAudioRenderer`. - * Add support for extracting 32-bit WAVE files - ([#3379](https://github.com/google/ExoPlayer/issues/3379)). - * Support extraction and decoding of Dolby Atmos - ([#2465](https://github.com/google/ExoPlayer/issues/2465)). - * Fix handling of playback parameter changes while paused when followed by a - seek. -* SimpleExoPlayer: Allow multiple audio and video debug listeners. -* DefaultTrackSelector: Support undefined language text track selection when the - preferred language is not available - ([#2980](https://github.com/google/ExoPlayer/issues/2980)). -* Add options to `DefaultLoadControl` to set maximum buffer size in bytes and - to choose whether size or time constraints are prioritized. -* Use surfaceless context for secure `DummySurface`, if available - ([#3558](https://github.com/google/ExoPlayer/issues/3558)). -* FLV: Fix playback of live streams that do not contain an audio track - ([#3188](https://github.com/google/ExoPlayer/issues/3188)). -* CEA-608: Fix handling of row count changes in roll-up mode - ([#3513](https://github.com/google/ExoPlayer/issues/3513)). -* Prevent period transitions when seeking to the end of a period when paused - ([#2439](https://github.com/google/ExoPlayer/issues/2439)). - -### 2.6.0 (2017-11-03) ### - -* Removed "r" prefix from versions. This release is "2.6.0", not "r2.6.0". -* New `Player.DefaultEventListener` abstract class can be extended to avoid - having to implement all methods defined by `Player.EventListener`. -* Added a reason to `EventListener.onPositionDiscontinuity` - ([#3252](https://github.com/google/ExoPlayer/issues/3252)). -* New `setShuffleModeEnabled` method for enabling shuffled playback. -* SimpleExoPlayer: Support for multiple video, text and metadata outputs. -* Support for `Renderer`s that don't consume any media - ([#3212](https://github.com/google/ExoPlayer/issues/3212)). -* Fix reporting of internal position discontinuities via - `Player.onPositionDiscontinuity`. `DISCONTINUITY_REASON_SEEK_ADJUSTMENT` is - added to disambiguate position adjustments during seeks from other types of - internal position discontinuity. -* Fix potential `IndexOutOfBoundsException` when calling `ExoPlayer.getDuration` - ([#3362](https://github.com/google/ExoPlayer/issues/3362)). -* Fix playbacks involving looping, concatenation and ads getting stuck when - media contains tracks with uneven durations - ([#1874](https://github.com/google/ExoPlayer/issues/1874)). -* Fix issue with `ContentDataSource` when reading from certain `ContentProvider` - implementations ([#3426](https://github.com/google/ExoPlayer/issues/3426)). -* Better playback experience when the video decoder cannot keep up, by skipping - to key-frames. This is particularly relevant for variable speed playbacks. -* Allow `SingleSampleMediaSource` to suppress load errors - ([#3140](https://github.com/google/ExoPlayer/issues/3140)). -* `DynamicConcatenatingMediaSource`: Allow specifying a callback to be invoked - after a dynamic playlist modification has been applied - ([#3407](https://github.com/google/ExoPlayer/issues/3407)). -* Audio: New `AudioSink` interface allows customization of audio output path. -* Offline: Added `Downloader` implementations for DASH, HLS, SmoothStreaming - and progressive streams. -* Track selection: - * Fixed adaptive track selection logic for live playbacks - ([#3017](https://github.com/google/ExoPlayer/issues/3017)). - * Added ability to select the lowest bitrate tracks. -* DASH: - * Don't crash when a malformed or unexpected manifest update occurs - ([#2795](https://github.com/google/ExoPlayer/issues/2795)). -* HLS: - * Support for Widevine protected FMP4 variants. - * Support CEA-608 in FMP4 variants. - * Support extractor injection - ([#2748](https://github.com/google/ExoPlayer/issues/2748)). -* DRM: - * Improved compatibility with ClearKey content - ([#3138](https://github.com/google/ExoPlayer/issues/3138)). - * Support multiple PSSH boxes of the same type. - * Retry initial provisioning and key requests if they fail - * Fix incorrect parsing of non-CENC sinf boxes. -* IMA extension: - * Expose `AdsLoader` via getter - ([#3322](https://github.com/google/ExoPlayer/issues/3322)). - * Handle `setPlayWhenReady` calls during ad playbacks - ([#3303](https://github.com/google/ExoPlayer/issues/3303)). - * Ignore seeks if an ad is playing - ([#3309](https://github.com/google/ExoPlayer/issues/3309)). - * Improve robustness of `ImaAdsLoader` in case content is not paused between - content to ad transitions - ([#3430](https://github.com/google/ExoPlayer/issues/3430)). -* UI: - * Allow specifying a `Drawable` for the `TimeBar` scrubber - ([#3337](https://github.com/google/ExoPlayer/issues/3337)). - * Allow multiple listeners on `TimeBar` - ([#3406](https://github.com/google/ExoPlayer/issues/3406)). -* New Leanback extension: Simplifies binding Exoplayer to Leanback UI - components. -* Unit tests moved to Robolectric. -* Misc bugfixes. - -### r2.5.4 (2017-10-19) ### - -* Remove unnecessary media playlist fetches during playback of live HLS streams. -* Add the ability to inject a HLS playlist parser through `HlsMediaSource`. -* Fix potential `IndexOutOfBoundsException` when using `ImaMediaSource` - ([#3334](https://github.com/google/ExoPlayer/issues/3334)). -* Fix an issue parsing MP4 content containing non-CENC sinf boxes. -* Fix memory leak when seeking with repeated periods. -* Fix playback position when `ExoPlayer.prepare` is called with `resetPosition` - set to false. -* Ignore MP4 edit lists that seem invalid - ([#3351](https://github.com/google/ExoPlayer/issues/3351)). -* Add extractor flag for ignoring all MP4 edit lists - ([#3358](https://github.com/google/ExoPlayer/issues/3358)). -* Improve extensibility by exposing public constructors for - `FrameworkMediaCrypto` and by making `DefaultDashChunkSource.getNextChunk` - non-final. - -### r2.5.3 (2017-09-20) ### - -* IMA extension: Support skipping of skippable ads on AndroidTV and other - non-touch devices ([#3258](https://github.com/google/ExoPlayer/issues/3258)). -* HLS: Fix broken WebVTT captions when PTS wraps around - ([#2928](https://github.com/google/ExoPlayer/issues/2928)). -* Captions: Fix issues rendering CEA-608 captions - ([#3250](https://github.com/google/ExoPlayer/issues/3250)). -* Workaround broken AAC decoders on Galaxy S6 - ([#3249](https://github.com/google/ExoPlayer/issues/3249)). -* Caching: Fix infinite loop when cache eviction fails - ([#3260](https://github.com/google/ExoPlayer/issues/3260)). -* Caching: Force use of BouncyCastle on JellyBean to fix decryption issue - ([#2755](https://github.com/google/ExoPlayer/issues/2755)). - -### r2.5.2 (2017-09-11) ### - -* IMA extension: Fix issue where ad playback could end prematurely for some - content types ([#3180](https://github.com/google/ExoPlayer/issues/3180)). -* RTMP extension: Fix SIGABRT on fast RTMP stream restart - ([#3156](https://github.com/google/ExoPlayer/issues/3156)). -* UI: Allow app to manually specify ad markers - ([#3184](https://github.com/google/ExoPlayer/issues/3184)). -* DASH: Expose segment indices to subclasses of DefaultDashChunkSource - ([#3037](https://github.com/google/ExoPlayer/issues/3037)). -* Captions: Added robustness against malformed WebVTT captions - ([#3228](https://github.com/google/ExoPlayer/issues/3228)). -* DRM: Support forcing a specific license URL. -* Fix playback error when seeking in media loaded through content:// URIs - ([#3216](https://github.com/google/ExoPlayer/issues/3216)). -* Fix issue playing MP4s in which the last atom specifies a size of zero - ([#3191](https://github.com/google/ExoPlayer/issues/3191)). -* Workaround playback failures on some Xiaomi devices - ([#3171](https://github.com/google/ExoPlayer/issues/3171)). -* Workaround SIGSEGV issue on some devices when setting and swapping surface for - secure playbacks ([#3215](https://github.com/google/ExoPlayer/issues/3215)). -* Workaround for Nexus 7 issue when swapping output surface - ([#3236](https://github.com/google/ExoPlayer/issues/3236)). -* Workaround for SimpleExoPlayerView's surface not being hidden properly - ([#3160](https://github.com/google/ExoPlayer/issues/3160)). - -### r2.5.1 (2017-08-08) ### - -* Fix an issue that could cause the reported playback position to stop advancing - in some cases. -* Fix an issue where a Surface could be released whilst still in use by the - player. - -### r2.5.0 (2017-08-07) ### - -* IMA extension: Wraps the Google Interactive Media Ads (IMA) SDK to provide an - easy and seamless way of incorporating display ads into ExoPlayer playbacks. - You can read more about the IMA extension - [here](https://medium.com/google-exoplayer/playing-ads-with-exoplayer-and-ima-868dfd767ea). -* MediaSession extension: Provides an easy way to connect ExoPlayer with - MediaSessionCompat in the Android Support Library. -* RTMP extension: An extension for playing streams over RTMP. -* Build: Made it easier for application developers to depend on a local checkout - of ExoPlayer. You can learn how to do this - [here](https://medium.com/google-exoplayer/howto-2-depend-on-a-local-checkout-of-exoplayer-bcd7f8531720). -* Core playback improvements: - * Eliminated re-buffering when changing audio and text track selections during - playback of progressive streams - ([#2926](https://github.com/google/ExoPlayer/issues/2926)). - * New DynamicConcatenatingMediaSource class to support playback of dynamic - playlists. - * New ExoPlayer.setRepeatMode method for dynamic toggling of repeat mode - during playback. Use of setRepeatMode should be preferred to - LoopingMediaSource for most looping use cases. You can read more about - setRepeatMode - [here](https://medium.com/google-exoplayer/repeat-modes-in-exoplayer-19dd85f036d3). - * Eliminated jank when switching video playback from one Surface to another on - API level 23+ for unencrypted content, and on devices that support the - EGL_EXT_protected_content OpenGL extension for protected content - ([#677](https://github.com/google/ExoPlayer/issues/677)). - * Enabled ExoPlayer instantiation on background threads without Loopers. - Events from such players are delivered on the application's main thread. -* HLS improvements: - * Optimized adaptive switches for playlists that specify the - EXT-X-INDEPENDENT-SEGMENTS tag. - * Optimized in-buffer seeking - ([#551](https://github.com/google/ExoPlayer/issues/551)). - * Eliminated re-buffering when changing audio and text track selections during - playback, provided the new selection does not require switching to different - renditions ([#2718](https://github.com/google/ExoPlayer/issues/2718)). - * Exposed all media playlist tags in ExoPlayer's MediaPlaylist object. -* DASH: Support for seamless switching across streams in different AdaptationSet - elements ([#2431](https://github.com/google/ExoPlayer/issues/2431)). -* DRM: Support for additional crypto schemes (cbc1, cbcs and cens) on - API level 24+ ([#1989](https://github.com/google/ExoPlayer/issues/1989)). -* Captions: Initial support for SSA/ASS subtitles - ([#889](https://github.com/google/ExoPlayer/issues/889)). -* AndroidTV: Fixed issue where tunneled video playback would not start on some - devices ([#2985](https://github.com/google/ExoPlayer/issues/2985)). -* MPEG-TS: Fixed segmentation issue when parsing H262 - ([#2891](https://github.com/google/ExoPlayer/issues/2891)). -* Cronet extension: Support for a user-defined fallback if Cronet library is not - present. -* Fix buffer too small IllegalStateException issue affecting some composite - media playbacks ([#2900](https://github.com/google/ExoPlayer/issues/2900)). -* Misc bugfixes. - -### r2.4.4 (2017-07-19) ### - -* HLS/MPEG-TS: Some initial optimizations of MPEG-TS extractor performance - ([#3040](https://github.com/google/ExoPlayer/issues/3040)). -* HLS: Fix propagation of format identifier for CEA-608 - ([#3033](https://github.com/google/ExoPlayer/issues/3033)). -* HLS: Detect playlist stuck and reset conditions - ([#2872](https://github.com/google/ExoPlayer/issues/2872)). -* Video: Fix video dimension reporting on some devices - ([#3007](https://github.com/google/ExoPlayer/issues/3007)). - -### r2.4.3 (2017-06-30) ### - -* Audio: Workaround custom audio decoders misreporting their maximum supported - channel counts ([#2940](https://github.com/google/ExoPlayer/issues/2940)). -* Audio: Workaround for broken MediaTek raw decoder on some devices - ([#2873](https://github.com/google/ExoPlayer/issues/2873)). -* Captions: Fix TTML captions appearing at the top of the screen - ([#2953](https://github.com/google/ExoPlayer/issues/2953)). -* Captions: Fix handling of some DVB subtitles - ([#2957](https://github.com/google/ExoPlayer/issues/2957)). -* Track selection: Fix setSelectionOverride(index, tracks, null) - ([#2988](https://github.com/google/ExoPlayer/issues/2988)). -* GVR extension: Add support for mono input - ([#2710](https://github.com/google/ExoPlayer/issues/2710)). -* FLAC extension: Fix failing build - ([#2977](https://github.com/google/ExoPlayer/pull/2977)). -* Misc bugfixes. - -### r2.4.2 (2017-06-06) ### - -* Stability: Work around Nexus 10 reboot when playing certain content - ([#2806](https://github.com/google/ExoPlayer/issues/2806)). -* MP3: Correctly treat MP3s with INFO headers as constant bitrate - ([#2895](https://github.com/google/ExoPlayer/issues/2895)). -* HLS: Use average rather than peak bandwidth when available - ([#2863](https://github.com/google/ExoPlayer/issues/2863)). -* SmoothStreaming: Fix timeline for live streams - ([#2760](https://github.com/google/ExoPlayer/issues/2760)). -* UI: Fix DefaultTimeBar invalidation - ([#2871](https://github.com/google/ExoPlayer/issues/2871)). -* Misc bugfixes. - -### r2.4.1 (2017-05-23) ### - -* Stability: Avoid OutOfMemoryError in extractors when parsing malformed media - ([#2780](https://github.com/google/ExoPlayer/issues/2780)). -* Stability: Avoid native crash on Galaxy Nexus. Avoid unnecessarily large codec - input buffer allocations on all devices - ([#2607](https://github.com/google/ExoPlayer/issues/2607)). -* Variable speed playback: Fix interpolation for rate/pitch adjustment - ([#2774](https://github.com/google/ExoPlayer/issues/2774)). -* HLS: Include EXT-X-DATERANGE tags in HlsMediaPlaylist. -* HLS: Don't expose CEA-608 track if CLOSED-CAPTIONS=NONE - ([#2743](https://github.com/google/ExoPlayer/issues/2743)). -* HLS: Correctly propagate errors loading the media playlist - ([#2623](https://github.com/google/ExoPlayer/issues/2623)). -* UI: DefaultTimeBar enhancements and bug fixes - ([#2740](https://github.com/google/ExoPlayer/issues/2740)). -* Ogg: Fix failure to play some Ogg files - ([#2782](https://github.com/google/ExoPlayer/issues/2782)). -* Captions: Don't select text tack with no language by default. -* Captions: TTML positioning fixes - ([#2824](https://github.com/google/ExoPlayer/issues/2824)). -* Misc bugfixes. - -### r2.4.0 (2017-04-25) ### - -* New modular library structure. You can read more about depending on individual - library modules - [here](https://medium.com/google-exoplayer/exoplayers-new-modular-structure-a916c0874907). -* Variable speed playback support on API level 16+. You can read more about - changing the playback speed - [here](https://medium.com/google-exoplayer/variable-speed-playback-with-exoplayer-e6e6a71e0343) - ([#26](https://github.com/google/ExoPlayer/issues/26)). -* New time bar view, including support for displaying ad break markers. -* Support DVB subtitles in MPEG-TS and MKV. -* Support adaptive playback for audio only DASH, HLS and SmoothStreaming - ([#1975](https://github.com/google/ExoPlayer/issues/1975)). -* Support for setting extractor flags on DefaultExtractorsFactory - ([#2657](https://github.com/google/ExoPlayer/issues/2657)). -* Support injecting custom renderers into SimpleExoPlayer using a new - RenderersFactory interface. -* Correctly set ExoPlayer's internal thread priority to `THREAD_PRIORITY_AUDIO`. -* TX3G: Support styling and positioning. -* FLV: - * Support MP3 in FLV. - * Skip unhandled metadata rather than failing - ([#2634](https://github.com/google/ExoPlayer/issues/2634)). - * Fix potential OutOfMemory errors. -* ID3: Better handle malformed ID3 data - ([#2604](https://github.com/google/ExoPlayer/issues/2604), - [#2663](https://github.com/google/ExoPlayer/issues/2663)). -* FFmpeg extension: Fixed build instructions - ([#2561](https://github.com/google/ExoPlayer/issues/2561)). -* VP9 extension: Reduced binary size. -* FLAC extension: Enabled 64 bit targets. -* Misc bugfixes. - -### r2.3.1 (2017-03-23) ### - -* Fix NPE enabling WebVTT subtitles in DASH streams - ([#2596](https://github.com/google/ExoPlayer/issues/2596)). -* Fix skipping to keyframes when MediaCodecVideoRenderer is enabled but without - a Surface ([#2575](https://github.com/google/ExoPlayer/issues/2575)). -* Minor fix for CEA-708 decoder - ([#2595](https://github.com/google/ExoPlayer/issues/2595)). - -### r2.3.0 (2017-03-16) ### - -* GVR extension: Wraps the Google VR Audio SDK to provide spatial audio - rendering. You can read more about the GVR extension - [here](https://medium.com/google-exoplayer/spatial-audio-with-exoplayer-and-gvr-cecb00e9da5f#.xdjebjd7g). -* DASH improvements: - * Support embedded CEA-608 closed captions - ([#2362](https://github.com/google/ExoPlayer/issues/2362)). - * Support embedded EMSG events - ([#2176](https://github.com/google/ExoPlayer/issues/2176)). - * Support mspr:pro manifest element - ([#2386](https://github.com/google/ExoPlayer/issues/2386)). - * Correct handling of empty segment indices at the start of live events - ([#1865](https://github.com/google/ExoPlayer/issues/1865)). -* HLS improvements: - * Respect initial track selection - ([#2353](https://github.com/google/ExoPlayer/issues/2353)). - * Reduced frequency of media playlist requests when playback position is close - to the live edge ([#2548](https://github.com/google/ExoPlayer/issues/2548)). - * Exposed the master playlist through ExoPlayer.getCurrentManifest() - ([#2537](https://github.com/google/ExoPlayer/issues/2537)). - * Support CLOSED-CAPTIONS #EXT-X-MEDIA type - ([#341](https://github.com/google/ExoPlayer/issues/341)). - * Fixed handling of negative values in #EXT-X-SUPPORT - ([#2495](https://github.com/google/ExoPlayer/issues/2495)). - * Fixed potential endless buffering state for streams with WebVTT subtitles - ([#2424](https://github.com/google/ExoPlayer/issues/2424)). -* MPEG-TS improvements: - * Support for multiple programs. - * Support for multiple closed captions and caption service descriptors - ([#2161](https://github.com/google/ExoPlayer/issues/2161)). -* MP3: Add `FLAG_ENABLE_CONSTANT_BITRATE_SEEKING` extractor option to enable - constant bitrate seeking in MP3 files that would otherwise be unseekable - ([#2445](https://github.com/google/ExoPlayer/issues/2445)). -* ID3: Better handle malformed ID3 data - ([#2486](https://github.com/google/ExoPlayer/issues/2486)). -* Track selection: Added maxVideoBitrate parameter to DefaultTrackSelector. -* DRM: Add support for CENC ClearKey on API level 21+ - ([#2361](https://github.com/google/ExoPlayer/issues/2361)). -* DRM: Support dynamic setting of key request headers - ([#1924](https://github.com/google/ExoPlayer/issues/1924)). -* SmoothStreaming: Fixed handling of start_time placeholder - ([#2447](https://github.com/google/ExoPlayer/issues/2447)). -* FLAC extension: Fix proguard configuration - ([#2427](https://github.com/google/ExoPlayer/issues/2427)). -* Misc bugfixes. - -### r2.2.0 (2017-01-30) ### - -* Demo app: Automatic recovery from BehindLiveWindowException, plus improved - handling of pausing and resuming live streams - ([#2344](https://github.com/google/ExoPlayer/issues/2344)). -* AndroidTV: Added Support for tunneled video playback - ([#1688](https://github.com/google/ExoPlayer/issues/1688)). -* DRM: Renamed StreamingDrmSessionManager to DefaultDrmSessionManager and - added support for using offline licenses - ([#876](https://github.com/google/ExoPlayer/issues/876)). -* DRM: Introduce OfflineLicenseHelper to help with offline license acquisition, - renewal and release. -* UI: Updated player control assets. Added vector drawables for use on API level - 21 and above. -* UI: Made player control seek bar work correctly with key events if focusable - ([#2278](https://github.com/google/ExoPlayer/issues/2278)). -* HLS: Improved support for streams that use EXT-X-DISCONTINUITY without - EXT-X-DISCONTINUITY-SEQUENCE - ([#1789](https://github.com/google/ExoPlayer/issues/1789)). -* HLS: Support for EXT-X-START tag - ([#1544](https://github.com/google/ExoPlayer/issues/1544)). -* HLS: Check #EXTM3U header is present when parsing the playlist. Fail - gracefully if not ([#2301](https://github.com/google/ExoPlayer/issues/2301)). -* HLS: Fix memory leak - ([#2319](https://github.com/google/ExoPlayer/issues/2319)). -* HLS: Fix non-seamless first adaptation where master playlist omits resolution - tags ([#2096](https://github.com/google/ExoPlayer/issues/2096)). -* HLS: Fix handling of WebVTT subtitle renditions with non-standard segment file - extensions ([#2025](https://github.com/google/ExoPlayer/issues/2025) and - [#2355](https://github.com/google/ExoPlayer/issues/2355)). -* HLS: Better handle inconsistent HLS playlist update - ([#2249](https://github.com/google/ExoPlayer/issues/2249)). -* DASH: Don't overflow when dealing with large segment numbers - ([#2311](https://github.com/google/ExoPlayer/issues/2311)). -* DASH: Fix propagation of language from the manifest - ([#2335](https://github.com/google/ExoPlayer/issues/2335)). -* SmoothStreaming: Work around "Offset to sample data was negative" failures - ([#2292](https://github.com/google/ExoPlayer/issues/2292), - [#2101](https://github.com/google/ExoPlayer/issues/2101) and - [#1152](https://github.com/google/ExoPlayer/issues/1152)). -* MP3/ID3: Added support for parsing Chapter and URL link frames - ([#2316](https://github.com/google/ExoPlayer/issues/2316)). -* MP3/ID3: Handle ID3 frames that end with empty text field - ([#2309](https://github.com/google/ExoPlayer/issues/2309)). -* Added ClippingMediaSource for playing clipped portions of media - ([#1988](https://github.com/google/ExoPlayer/issues/1988)). -* Added convenience methods to query whether the current window is dynamic and - seekable ([#2320](https://github.com/google/ExoPlayer/issues/2320)). -* Support setting of default headers on HttpDataSource.Factory implementations - ([#2166](https://github.com/google/ExoPlayer/issues/2166)). -* Fixed cache failures when using an encrypted cache content index. -* Fix visual artifacts when switching output surface - ([#2093](https://github.com/google/ExoPlayer/issues/2093)). -* Fix gradle + proguard configurations. -* Fix player position when replacing the MediaSource - ([#2369](https://github.com/google/ExoPlayer/issues/2369)). -* Misc bug fixes, including - [#2330](https://github.com/google/ExoPlayer/issues/2330), - [#2269](https://github.com/google/ExoPlayer/issues/2269), - [#2252](https://github.com/google/ExoPlayer/issues/2252), - [#2264](https://github.com/google/ExoPlayer/issues/2264) and - [#2290](https://github.com/google/ExoPlayer/issues/2290). - -### r2.1.1 (2016-12-20) ### - -* Fix some subtitle types (e.g. WebVTT) being displayed out of sync - ([#2208](https://github.com/google/ExoPlayer/issues/2208)). -* Fix incorrect position reporting for on-demand HLS media that includes - EXT-X-PROGRAM-DATE-TIME tags - ([#2224](https://github.com/google/ExoPlayer/issues/2224)). -* Fix issue where playbacks could get stuck in the initial buffering state if - over 1MB of data needs to be read to initialize the playback. - -### r2.1.0 (2016-12-14) ### - -* HLS: Support for seeking in live streams - ([#87](https://github.com/google/ExoPlayer/issues/87)). -* HLS: Improved support: - * Support for EXT-X-PROGRAM-DATE-TIME - ([#747](https://github.com/google/ExoPlayer/issues/747)). - * Improved handling of sample timestamps and their alignment across variants - and renditions. - * Fix issue that could cause playbacks to get stuck in an endless initial - buffering state. - * Correctly propagate BehindLiveWindowException instead of - IndexOutOfBoundsException exception - ([#1695](https://github.com/google/ExoPlayer/issues/1695)). -* MP3/MP4: Support for ID3 metadata, including embedded album art - ([#979](https://github.com/google/ExoPlayer/issues/979)). -* Improved customization of UI components. You can read about customization of - ExoPlayer's UI components - [here](https://medium.com/google-exoplayer/customizing-exoplayers-ui-components-728cf55ee07a#.9ewjg7avi). -* Robustness improvements when handling MediaSource timeline changes and - MediaPeriod transitions. -* CEA-608: Support for caption styling and positioning. -* MPEG-TS: Improved support: - * Support injection of custom TS payload readers. - * Support injection of custom section payload readers. - * Support SCTE-35 splice information messages. - * Support multiple table sections in a single PSI section. - * Fix NullPointerException when an unsupported stream type is encountered - ([#2149](https://github.com/google/ExoPlayer/issues/2149)). - * Avoid failure when expected ID3 header not found - ([#1966](https://github.com/google/ExoPlayer/issues/1966)). -* Improvements to the upstream cache package. - * Support caching of media segments for DASH, HLS and SmoothStreaming. Note - that caching of manifest and playlist files is still not supported in the - (normal) case where the corresponding responses are compressed. - * Support caching for ExtractorMediaSource based playbacks. -* Improved flexibility of SimpleExoPlayer - ([#2102](https://github.com/google/ExoPlayer/issues/2102)). -* Fix issue where only the audio of a video would play due to capability - detection issues ([#2007](https://github.com/google/ExoPlayer/issues/2007), - [#2034](https://github.com/google/ExoPlayer/issues/2034) and - [#2157](https://github.com/google/ExoPlayer/issues/2157)). -* Fix issues that could cause ExtractorMediaSource based playbacks to get stuck - buffering ([#1962](https://github.com/google/ExoPlayer/issues/1962)). -* Correctly set SimpleExoPlayerView surface aspect ratio when an active player - is attached ([#2077](https://github.com/google/ExoPlayer/issues/2077)). -* OGG: Fix playback of short OGG files - ([#1976](https://github.com/google/ExoPlayer/issues/1976)). -* MP4: Support `.mp3` tracks - ([#2066](https://github.com/google/ExoPlayer/issues/2066)). -* SubRip: Don't fail playbacks if SubRip file contains negative timestamps - ([#2145](https://github.com/google/ExoPlayer/issues/2145)). -* Misc bugfixes. - -### r2.0.4 (2016-10-20) ### - -* Fix crash on Jellybean devices when using playback controls - ([#1965](https://github.com/google/ExoPlayer/issues/1965)). - -### r2.0.3 (2016-10-17) ### - -* Fixed NullPointerException in ExtractorMediaSource - ([#1914](https://github.com/google/ExoPlayer/issues/1914)). -* Fixed NullPointerException in HlsMediaPeriod - ([#1907](https://github.com/google/ExoPlayer/issues/1907)). -* Fixed memory leak in PlaybackControlView - ([#1908](https://github.com/google/ExoPlayer/issues/1908)). -* Fixed strict mode violation when using - SimpleExoPlayer.setVideoPlayerTextureView(). -* Fixed L3 Widevine provisioning - ([#1925](https://github.com/google/ExoPlayer/issues/1925)). -* Fixed hiding of controls with use_controller="false" - ([#1919](https://github.com/google/ExoPlayer/issues/1919)). -* Improvements to Cronet network stack extension. -* Misc bug fixes. - -### r2.0.2 (2016-10-06) ### - -* Fixes for MergingMediaSource and sideloaded subtitles. - ([#1882](https://github.com/google/ExoPlayer/issues/1882), - [#1854](https://github.com/google/ExoPlayer/issues/1854), - [#1900](https://github.com/google/ExoPlayer/issues/1900)). -* Reduced effect of application code leaking player references - ([#1855](https://github.com/google/ExoPlayer/issues/1855)). -* Initial support for fragmented MP4 in HLS. -* Misc bug fixes and minor features. - -### r2.0.1 (2016-09-30) ### - -* Fix playback of short duration content - ([#1837](https://github.com/google/ExoPlayer/issues/1837)). -* Fix MergingMediaSource preparation issue - ([#1853](https://github.com/google/ExoPlayer/issues/1853)). -* Fix live stream buffering (out of memory) issue - ([#1825](https://github.com/google/ExoPlayer/issues/1825)). - -### r2.0.0 (2016-09-14) ### +* Add `startPositionUs` to `MediaSource.createPeriod`. This fixes an issue + where using lazy preparation in `ConcatenatingMediaSource` with an + `ExtractorMediaSource` overrides initial seek positions + ([#5350](https://github.com/google/ExoPlayer/issues/5350)). +* Add subtext to the `MediaDescriptionAdapter` of the + `PlayerNotificationManager`. +* Add workaround for video quality problems with Amlogic decoders + ([#5003](https://github.com/google/ExoPlayer/issues/5003)). +* Fix issue where sending callbacks for playlist changes may cause problems + because of parallel player access + ([#5240](https://github.com/google/ExoPlayer/issues/5240)). +* Fix issue with reusing a `ClippingMediaSource` with an inner + `ExtractorMediaSource` and a non-zero start position + ([#5351](https://github.com/google/ExoPlayer/issues/5351)). +* Fix issue where uneven track durations in MP4 streams can cause OOM problems + ([#3670](https://github.com/google/ExoPlayer/issues/3670)). + +### 2.9.3 (2018-12-20) + +* Captions: Support PNG subtitles in SMPTE-TT + ([#1583](https://github.com/google/ExoPlayer/issues/1583)). +* MPEG-TS: Use random access indicators to minimize the need for + `FLAG_ALLOW_NON_IDR_KEYFRAMES`. +* Downloading: Reduce time taken to remove downloads + ([#5136](https://github.com/google/ExoPlayer/issues/5136)). +* MP3: + * Use the true bitrate for constant-bitrate MP3 seeking. + * Fix issue where streams would play twice on some Samsung devices + ([#4519](https://github.com/google/ExoPlayer/issues/4519)). +* Fix regression where some audio formats were incorrectly marked as being + unplayable due to under-reporting of platform decoder capabilities + ([#5145](https://github.com/google/ExoPlayer/issues/5145)). +* Fix decode-only frame skipping on Nvidia Shield TV devices. +* Workaround for MiTV (dangal) issue when swapping output surface + ([#5169](https://github.com/google/ExoPlayer/issues/5169)). + +### 2.9.2 (2018-11-28) + +* HLS: + * Fix issue causing unnecessary media playlist requests when playing live + streams ([#5059](https://github.com/google/ExoPlayer/issues/5059)). + * Fix decoder re-instantiation issue for packed audio streams + ([#5063](https://github.com/google/ExoPlayer/issues/5063)). +* MP4: Support Opus and FLAC in the MP4 container, and in DASH + ([#4883](https://github.com/google/ExoPlayer/issues/4883)). +* DASH: Fix detecting the end of live events + ([#4780](https://github.com/google/ExoPlayer/issues/4780)). +* Spherical video: Fall back to `TYPE_ROTATION_VECTOR` if + `TYPE_GAME_ROTATION_VECTOR` is unavailable + ([#5119](https://github.com/google/ExoPlayer/issues/5119)). +* Support seeking for a wider range of MPEG-TS streams + ([#5097](https://github.com/google/ExoPlayer/issues/5097)). +* Include channel count in audio capabilities check + ([#4690](https://github.com/google/ExoPlayer/issues/4690)). +* Fix issue with applying the `show_buffering` attribute in `PlayerView` + ([#5139](https://github.com/google/ExoPlayer/issues/5139)). +* Fix issue where null `Metadata` was output when it failed to decode + ([#5149](https://github.com/google/ExoPlayer/issues/5149)). +* Fix playback of some invalid but playable MP4 streams by replacing + assertions with logged warnings in sample table parsing code + ([#5162](https://github.com/google/ExoPlayer/issues/5162)). +* Fix UUID passed to `MediaCrypto` when using `C.CLEARKEY_UUID` before API 27. + +### 2.9.1 (2018-11-01) + +* Add convenience methods `Player.next`, `Player.previous`, `Player.hasNext` + and `Player.hasPrevious` + ([#4863](https://github.com/google/ExoPlayer/issues/4863)). +* Improve initial bandwidth meter estimates using the current country and + network type. +* IMA extension: + * For preroll to live stream transitions, project forward the loading + position to avoid being behind the live window. + * Let apps specify whether to focus the skip button on ATV + ([#5019](https://github.com/google/ExoPlayer/issues/5019)). +* MP3: + * Support seeking based on MLLT metadata + ([#3241](https://github.com/google/ExoPlayer/issues/3241)). + * Fix handling of streams with appended data + ([#4954](https://github.com/google/ExoPlayer/issues/4954)). +* DASH: Parse ProgramInformation element if present in the manifest. +* HLS: + * Add constructor to `DefaultHlsExtractorFactory` for adding TS payload + reader factory flags + ([#4861](https://github.com/google/ExoPlayer/issues/4861)). + * Fix bug in segment sniffing + ([#5039](https://github.com/google/ExoPlayer/issues/5039)). +* SubRip: Add support for alignment tags, and remove tags from the displayed + captions ([#4306](https://github.com/google/ExoPlayer/issues/4306)). +* Fix issue with blind seeking to windows with non-zero offset in a + `ConcatenatingMediaSource` + ([#4873](https://github.com/google/ExoPlayer/issues/4873)). +* Fix logic for enabling next and previous actions in `TimelineQueueNavigator` + ([#5065](https://github.com/google/ExoPlayer/issues/5065)). +* Fix issue where audio focus handling could not be disabled after enabling it + ([#5055](https://github.com/google/ExoPlayer/issues/5055)). +* Fix issue where subtitles were positioned incorrectly if `SubtitleView` had + a non-zero position offset to its parent + ([#4788](https://github.com/google/ExoPlayer/issues/4788)). +* Fix issue where the buffered position was not updated correctly when + transitioning between periods + ([#4899](https://github.com/google/ExoPlayer/issues/4899)). +* Fix issue where a `NullPointerException` is thrown when removing an + unprepared media source from a `ConcatenatingMediaSource` with the + `useLazyPreparation` option enabled + ([#4986](https://github.com/google/ExoPlayer/issues/4986)). +* Work around an issue where a non-empty end-of-stream audio buffer would be + output with timestamp zero, causing the player position to jump backwards + ([#5045](https://github.com/google/ExoPlayer/issues/5045)). +* Suppress a spurious assertion failure on some Samsung devices + ([#4532](https://github.com/google/ExoPlayer/issues/4532)). +* Suppress spurious "references unknown class member" shrinking warning + ([#4890](https://github.com/google/ExoPlayer/issues/4890)). +* Swap recommended order for google() and jcenter() in gradle config + ([#4997](https://github.com/google/ExoPlayer/issues/4997)). + +### 2.9.0 (2018-09-06) + +* Turn on Java 8 compiler support for the ExoPlayer library. Apps may need to + add `compileOptions { targetCompatibility JavaVersion.VERSION_1_8 }` to + their gradle settings to ensure bytecode compatibility. +* Set `compileSdkVersion` and `targetSdkVersion` to 28. +* Support for automatic audio focus handling via + `SimpleExoPlayer.setAudioAttributes`. +* Add `ExoPlayer.retry` convenience method. +* Add `AudioListener` for listening to changes in audio configuration during + playback ([#3994](https://github.com/google/ExoPlayer/issues/3994)). +* Add `LoadErrorHandlingPolicy` to allow configuration of load error handling + across `MediaSource` implementations + ([#3370](https://github.com/google/ExoPlayer/issues/3370)). +* Allow passing a `Looper`, which specifies the thread that must be used to + access the player, when instantiating player instances using + `ExoPlayerFactory` + ([#4278](https://github.com/google/ExoPlayer/issues/4278)). +* Allow setting log level for ExoPlayer logcat output + ([#4665](https://github.com/google/ExoPlayer/issues/4665)). +* Simplify `BandwidthMeter` injection: The `BandwidthMeter` should now be + passed directly to `ExoPlayerFactory`, instead of to + `TrackSelection.Factory` and `DataSource.Factory`. The `BandwidthMeter` is + passed to the components that need it internally. The `BandwidthMeter` may + also be omitted, in which case a default instance will be used. +* Spherical video: + * Support for spherical video by setting `surface_type="spherical_view"` + on `PlayerView`. + * Support for + [VR180](https://github.com/google/spatial-media/blob/master/docs/vr180.md). +* HLS: + * Support PlayReady. + * Add container format sniffing + ([#2025](https://github.com/google/ExoPlayer/issues/2025)). + * Support alternative `EXT-X-KEY` tags. + * Support `EXT-X-INDEPENDENT-SEGMENTS` in the master playlist. + * Support variable substitution + ([#4422](https://github.com/google/ExoPlayer/issues/4422)). + * Fix the bitrate being unset on primary track sample formats + ([#3297](https://github.com/google/ExoPlayer/issues/3297)). + * Make `HlsMediaSource.Factory` take a factory of trackers instead of a + tracker instance + ([#4814](https://github.com/google/ExoPlayer/issues/4814)). +* DASH: + * Support `messageData` attribute for in-manifest event streams. + * Clip periods to their specified durations + ([#4185](https://github.com/google/ExoPlayer/issues/4185)). +* Improve seeking support for progressive streams: + * 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)). 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. + 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 on all + extractors that support it. +* Video: + * Add callback to `VideoListener` to notify of surface size changes. + * Improve performance when playing high frame-rate content, and when + playing at greater than 1x speed + ([#2777](https://github.com/google/ExoPlayer/issues/2777)). + * Scale up the initial video decoder maximum input size so playlist + transitions with small increases in maximum sample size do not require + reinitialization + ([#4510](https://github.com/google/ExoPlayer/issues/4510)). + * Fix a bug where the player would not transition to the ended state when + playing video in tunneled mode. +* Audio: + * Support attaching auxiliary audio effects to the `AudioTrack` via + `Player.setAuxEffectInfo` and `Player.clearAuxEffectInfo`. + * Support seamless adaptation while playing xHE-AAC streams. + ([#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)). + * WAV: Fix issue where white noise would be output at the end of playback + ([#4724](https://github.com/google/ExoPlayer/issues/4724)). + * MP3: Fix issue where streams would play twice on the SM-T530 + ([#4519](https://github.com/google/ExoPlayer/issues/4519)). +* Analytics: + * Add callbacks to `DefaultDrmSessionEventListener` and + `AnalyticsListener` to be notified of acquired and released DRM + sessions. + * Add uri field to `LoadEventInfo` in `MediaSourceEventListener` and + `AnalyticsListener` callbacks. This uri is the redirected uri if + redirection occurred + ([#2054](https://github.com/google/ExoPlayer/issues/2054)). + * Add response headers field to `LoadEventInfo` in + `MediaSourceEventListener` and `AnalyticsListener` callbacks + ([#4361](https://github.com/google/ExoPlayer/issues/4361) and + [#4615](https://github.com/google/ExoPlayer/issues/4615)). +* UI: + * Add option to `PlayerView` 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. +* ConcatenatingMediaSource: + * Support lazy preparation of playlist media sources + ([#3972](https://github.com/google/ExoPlayer/issues/3972)). + * Support range removal with `removeMediaSourceRange` methods + ([#4542](https://github.com/google/ExoPlayer/issues/4542)). + * Support setting a new shuffle order with `setShuffleOrder` + ([#4791](https://github.com/google/ExoPlayer/issues/4791)). +* MPEG-TS: Support CEA-608/708 in H262 + ([#2565](https://github.com/google/ExoPlayer/issues/2565)). +* Allow configuration of the back buffer in `DefaultLoadControl.Builder` + ([#4857](https://github.com/google/ExoPlayer/issues/4857)). +* Allow apps to pass a `CacheKeyFactory` for setting custom cache keys when + creating a `CacheDataSource`. +* Provide additional information for adaptive track selection. + `TrackSelection.updateSelectedTrack` has two new parameters for the current + queue of media chunks and iterators for information about upcoming chunks. +* 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)). +* Enable gzip for requests made by `SingleSampleMediaSource` + ([#4771](https://github.com/google/ExoPlayer/issues/4771)). +* Fix bug reporting buffered position for multi-period windows, and add + convenience methods `Player.getTotalBufferedDuration` and + `Player.getContentBufferedDuration` + ([#4023](https://github.com/google/ExoPlayer/issues/4023)). +* Fix bug where transitions to clipped media sources would happen too early + ([#4583](https://github.com/google/ExoPlayer/issues/4583)). +* Fix bugs reporting events for multi-period media sources + ([#4492](https://github.com/google/ExoPlayer/issues/4492) and + [#4634](https://github.com/google/ExoPlayer/issues/4634)). +* Fix issue where removing looping media from a playlist throws an exception + ([#4871](https://github.com/google/ExoPlayer/issues/4871). +* Fix issue where the preferred audio or text track would not be selected if + mapped onto a secondary renderer of the corresponding type + ([#4711](http://github.com/google/ExoPlayer/issues/4711)). +* Fix issue where errors of upcoming playlist items are thrown too early + ([#4661](https://github.com/google/ExoPlayer/issues/4661)). +* Allow edit lists which do not start with a sync sample. + ([#4774](https://github.com/google/ExoPlayer/issues/4774)). +* Fix issue with audio discontinuities at period transitions, e.g. when + looping ([#3829](https://github.com/google/ExoPlayer/issues/3829)). +* Fix issue where `player.getCurrentTag()` throws an + `IndexOutOfBoundsException` + ([#4822](https://github.com/google/ExoPlayer/issues/4822)). +* Fix bug preventing use of multiple key session support (`multiSession=true`) + for non-Widevine `DefaultDrmSessionManager` instances + ([#4834](https://github.com/google/ExoPlayer/issues/4834)). +* Fix issue where audio and video would desynchronize when playing + concatenations of gapless content + ([#4559](https://github.com/google/ExoPlayer/issues/4559)). +* IMA extension: + * Refine the previous fix for empty ad groups to avoid discarding ad + breaks unnecessarily + ([#4030](https://github.com/google/ExoPlayer/issues/4030) and + [#4280](https://github.com/google/ExoPlayer/issues/4280)). + * Fix handling of empty postrolls + ([#4681](https://github.com/google/ExoPlayer/issues/4681)). + * Fix handling of postrolls with multiple ads + ([#4710](https://github.com/google/ExoPlayer/issues/4710)). +* MediaSession extension: + * Add `MediaSessionConnector.setCustomErrorMessage` to support setting + custom error messages. + * Add `MediaMetadataProvider` to support setting custom metadata + ([#3497](https://github.com/google/ExoPlayer/issues/3497)). +* Cronet extension: Now distributed via jCenter. +* FFmpeg extension: Support mu-law and A-law PCM. + +### 2.8.4 (2018-08-17) + +* IMA extension: 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 (2018-07-23) + +* IMA extension: + * 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 (2018-06-06) + +* IMA extension: 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 (2018-05-22) + +* 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. +* UI: + * Add `PlayerView.setKeepContentOnPlayerReset` to keep the currently + displayed video frame or media artwork visible when the player is reset + ([#2843](https://github.com/google/ExoPlayer/issues/2843)). +* Fix crash when switching surface on Moto E(4) + ([#4134](https://github.com/google/ExoPlayer/issues/4134)). +* Fix a bug that could cause event listeners to be called with inconsistent + information if an event listener interacted with the player + ([#4262](https://github.com/google/ExoPlayer/issues/4262)). +* Audio: + * Fix extraction of PCM in MP4/MOV + ([#4228](https://github.com/google/ExoPlayer/issues/4228)). + * FLAC: Supports seeking for FLAC files without SEEKTABLE + ([#1808](https://github.com/google/ExoPlayer/issues/1808)). +* 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. + * Fix an issue when the caption line has no text (empty line or only line + break), and the line's background is still displayed. + * Support TTML font size using % correctly (as percentage of document cell + resolution). + +### 2.8.0 (2018-05-03) + +* Downloading: + * Add `DownloadService`, `DownloadManager` and related classes + ([#2643](https://github.com/google/ExoPlayer/issues/2643)). Information + on using these components to download progressive formats can be found + [here](https://medium.com/google-exoplayer/downloading-streams-6d259eec7f95). + To see how to download DASH, HLS and SmoothStreaming media, take a look + at the app. + * Updated main demo app to support downloading DASH, HLS, SmoothStreaming + and progressive media. +* MediaSources: + * Allow reusing media sources after they have been released and also in + parallel to allow adding them multiple times to a concatenation. + ([#3498](https://github.com/google/ExoPlayer/issues/3498)). + * Merged `DynamicConcatenatingMediaSource` into `ConcatenatingMediaSource` + and deprecated `DynamicConcatenatingMediaSource`. + * Allow clipping of child media sources where the period and window have a + non-zero offset with `ClippingMediaSource`. + * Allow adding and removing `MediaSourceEventListener`s to MediaSources + after they have been created. Listening to events is now supported for + all media sources including composite sources. + * Added callbacks to `MediaSourceEventListener` to get notified when media + 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 `Player.getCurrentTag`. +* UI: + * Add support for displaying error messages and a buffering spinner in + `PlayerView`. + * Add support for listening to `AspectRatioFrameLayout`'s aspect ratio + update ([#3736](https://github.com/google/ExoPlayer/issues/3736)). + * Add `PlayerNotificationManager` for displaying notifications reflecting + the player state. + * Add `TrackSelectionView` for selecting tracks with + `DefaultTrackSelector`. + * Add `TrackNameProvider` for converting track `Format`s to textual + descriptions, and `DefaultTrackNameProvider` as a default + implementation. +* Track selection: + * Reworked `MappingTrackSelector` and `DefaultTrackSelector`. + * `DefaultTrackSelector.Parameters` now implements `Parcelable`. + * Added UI components for track selection (see above). +* Audio: + * Support extracting data from AMR container formats, including both + narrow and wide band + ([#2527](https://github.com/google/ExoPlayer/issues/2527)). + * FLAC: + * Sniff FLAC files correctly if they have ID3 headers + ([#4055](https://github.com/google/ExoPlayer/issues/4055)). + * Supports FLAC files with high sample rate (176400 and 192000) + ([#3769](https://github.com/google/ExoPlayer/issues/3769)). + * Factor out `AudioTrack` position tracking from `DefaultAudioSink`. + * Fix an issue where the playback position would pause just after playback + begins, and poll the audio timestamp less frequently once it starts + advancing ([#3841](https://github.com/google/ExoPlayer/issues/3841)). + * Add an option to skip silent audio in `PlaybackParameters` + ([#2635](https://github.com/google/ExoPlayer/issues/2635)). + * Fix an issue where playback of TrueHD streams would get stuck after + seeking due to not finding a syncframe + ([#3845](https://github.com/google/ExoPlayer/issues/3845)). + * Fix an issue with eac3-joc playback where a codec would fail to + configure ([#4165](https://github.com/google/ExoPlayer/issues/4165)). + * Handle non-empty end-of-stream buffers, to fix gapless playback of + streams with encoder padding when the decoder returns a non-empty final + buffer. + * Allow trimming more than one sample when applying an elst audio edit via + gapless playback info. + * Allow overriding skipping/scaling with custom `AudioProcessor`s + ([#3142](https://github.com/google/ExoPlayer/issues/3142)). +* Caching: + * Add release method to the `Cache` interface, and prevent multiple + instances of `SimpleCache` using the same folder at the same time. + * Cache redirect URLs + ([#2360](https://github.com/google/ExoPlayer/issues/2360)). +* DRM: + * Allow multiple listeners for `DefaultDrmSessionManager`. + * Pass `DrmSessionManager` to `ExoPlayerFactory` instead of + `RendererFactory`. + * Change minimum API requirement for CBC and pattern encryption from 24 to + 25 ([#4022](https://github.com/google/ExoPlayer/issues/4022)). + * Fix handling of 307/308 redirects when making license requests + ([#4108](https://github.com/google/ExoPlayer/issues/4108)). +* HLS: + * Fix playlist loading error propagation when the current selection does + not include all of the playlist's variants. + * Fix SAMPLE-AES-CENC and SAMPLE-AES-CTR EXT-X-KEY methods + ([#4145](https://github.com/google/ExoPlayer/issues/4145)). + * Preeptively declare an ID3 track in chunkless preparation + ([#4016](https://github.com/google/ExoPlayer/issues/4016)). + * Add support for multiple #EXT-X-MAP tags in a media playlist + ([#4164](https://github.com/google/ExoPlayer/issues/4182)). + * Fix seeking in live streams + ([#4187](https://github.com/google/ExoPlayer/issues/4187)). +* IMA extension: + * Allow setting the ad media load timeout + ([#3691](https://github.com/google/ExoPlayer/issues/3691)). + * Expose ad load errors via `MediaSourceEventListener` on + `AdsMediaSource`, and allow setting an ad event listener on + `ImaAdsLoader`. Deprecate the `AdsMediaSource.EventListener`. +* Add `AnalyticsListener` interface which can be registered in + `SimpleExoPlayer` to receive detailed metadata for each ExoPlayer event. +* Optimize seeking in FMP4 by enabling seeking to the nearest sync sample + within a fragment. This benefits standalone FMP4 playbacks, DASH and + SmoothStreaming. +* Updated default max buffer length in `DefaultLoadControl`. +* Fix ClearKey decryption error if the key contains a forward slash + ([#4075](https://github.com/google/ExoPlayer/issues/4075)). +* Fix crash when switching surface on Huawei P9 Lite + ([#4084](https://github.com/google/ExoPlayer/issues/4084)), and Philips + QM163E ([#4104](https://github.com/google/ExoPlayer/issues/4104)). +* Support ZLIB compressed PGS subtitles. +* Added `getPlaybackError` to `Player` interface. +* Moved initial bitrate estimate from `AdaptiveTrackSelection` to + `DefaultBandwidthMeter`. +* Removed default renderer time offset of 60000000 from internal player. The + actual renderer timestamp offset can be obtained by listening to + `BaseRenderer.onStreamChanged`. +* Added dependencies on checkerframework annotations for static code analysis. + +### 2.7.3 (2018-04-04) + +* Fix ProGuard configuration for Cast, IMA and OkHttp extensions. +* Update OkHttp extension to depend on OkHttp 3.10.0. + +### 2.7.2 (2018-03-29) + +* Gradle: Upgrade Gradle version from 4.1 to 4.4 so it can work with Android + Studio 3.1 ([#3708](https://github.com/google/ExoPlayer/issues/3708)). +* Match codecs starting with "mp4a" to different Audio MimeTypes + ([#3779](https://github.com/google/ExoPlayer/issues/3779)). +* Fix ANR issue on Redmi 4X and Redmi Note 4 + ([#4006](https://github.com/google/ExoPlayer/issues/4006)). +* Fix handling of zero padded strings when parsing Matroska streams + ([#4010](https://github.com/google/ExoPlayer/issues/4010)). +* Fix "Decoder input buffer too small" error when playing some FLAC streams. +* MediaSession extension: Omit fast forward and rewind actions when media is + not seekable ([#4001](https://github.com/google/ExoPlayer/issues/4001)). + +### 2.7.1 (2018-03-09) + +* Gradle: Replaced 'compile' (deprecated) with 'implementation' and 'api'. + This may lead to build breakage for applications upgrading from previous + version that rely on indirect dependencies of certain modules. In such + cases, application developers need to add the missing dependency to their + gradle file. You can read more about the new dependency configurations + [here](https://developer.android.com/studio/build/gradle-plugin-3-0-0-migration.html#new_configurations). +* HlsMediaSource: Make HLS periods start at zero instead of the epoch. + Applications that rely on HLS timelines having a period starting at the + epoch will need to update their handling of HLS timelines. The program date + time is still available via the informational + `Timeline.Window.windowStartTimeMs` field + ([#3865](https://github.com/google/ExoPlayer/issues/3865), + [#3888](https://github.com/google/ExoPlayer/issues/3888)). +* Enable seeking in MP4 streams where duration is set incorrectly in the track + header ([#3926](https://github.com/google/ExoPlayer/issues/3926)). +* Video: Force rendering a frame periodically in `MediaCodecVideoRenderer` and + `LibvpxVideoRenderer`, even if it is late. + +### 2.7.0 (2018-02-19) + +* Player interface: + * Add optional parameter to `stop` to reset the player when stopping. + * Add a reason to `EventListener.onTimelineChanged` to distinguish between + initial preparation, reset and dynamic updates. + * Add `Player.DISCONTINUITY_REASON_AD_INSERTION` to the possible reasons + reported in `Eventlistener.onPositionDiscontinuity` to distinguish + transitions to and from ads within one period from transitions between + periods. + * Replaced `ExoPlayer.sendMessages` with `ExoPlayer.createMessage` to + allow more customization of the message. Now supports setting a message + delivery playback position and/or a delivery handler + ([#2189](https://github.com/google/ExoPlayer/issues/2189)). + * Add `Player.VideoComponent`, `Player.TextComponent` and + `Player.MetadataComponent` interfaces that define optional video, text + and metadata output functionality. New `getVideoComponent`, + `getTextComponent` and `getMetadataComponent` methods provide access to + this functionality. +* Add `ExoPlayer.setSeekParameters` for controlling how seek operations are + performed. The `SeekParameters` class contains defaults for exact seeking + and seeking to the closest sync points before, either side or after + specified seek positions. `SeekParameters` are not currently supported when + playing HLS streams. +* DefaultTrackSelector: + * Replace `DefaultTrackSelector.Parameters` copy methods with a builder. + * Support disabling of individual text track selection flags. +* Buffering: + * Allow a back-buffer of media to be retained behind the current playback + position, for fast backward seeking. The back-buffer can be configured + by custom `LoadControl` implementations. + * Add ability for `SequenceableLoader` to re-evaluate its buffer and + discard buffered media so that it can be re-buffered in a different + quality. + * Allow more flexible loading strategy when playing media containing + multiple sub-streams, by allowing injection of custom + `CompositeSequenceableLoader` factories through + `DashMediaSource.Factory`, `HlsMediaSource.Factory`, + `SsMediaSource.Factory`, and `MergingMediaSource`. + * Play out existing buffer before retrying for progressive live streams + ([#1606](https://github.com/google/ExoPlayer/issues/1606)). +* UI: + * Generalized player and control views to allow them to bind with any + `Player`, and renamed them to `PlayerView` and `PlayerControlView` + respectively. + * Made `PlayerView` automatically apply video rotation when configured to + use `TextureView` + ([#91](https://github.com/google/ExoPlayer/issues/91)). + * Made `PlayerView` play button behave correctly when the player is ended + ([#3689](https://github.com/google/ExoPlayer/issues/3689)), and call a + `PlaybackPreparer` when the player is idle. +* DRM: Optimistically attempt playback of DRM protected content that does not + declare scheme specific init data in the manifest. If playback of clear + samples without keys is allowed, delay DRM session error propagation until + keys are actually needed + ([#3630](https://github.com/google/ExoPlayer/issues/3630)). +* DASH: + * Support in-band Emsg events targeting the player with scheme id + `urn:mpeg:dash:event:2012` and scheme values "1", "2" and "3". + * Support EventStream elements in DASH manifests. +* HLS: + * Add opt-in support for chunkless preparation in HLS. This allows an HLS + source to finish preparation without downloading any chunks, which can + significantly reduce initial buffering time + ([#3149](https://github.com/google/ExoPlayer/issues/3149)). More details + can be found + [here](https://medium.com/google-exoplayer/faster-hls-preparation-f6611aa15ea6). + * Fail if unable to sync with the Transport Stream, rather than entering + stuck in an indefinite buffering state. + * Fix mime type propagation + ([#3653](https://github.com/google/ExoPlayer/issues/3653)). + * Fix ID3 context reuse across segment format changes + ([#3622](https://github.com/google/ExoPlayer/issues/3622)). + * Use long for media sequence numbers + ([#3747](https://github.com/google/ExoPlayer/issues/3747)) + * Add initial support for the EXT-X-GAP tag. +* Audio: + * Support TrueHD passthrough for rechunked samples in Matroska files + ([#2147](https://github.com/google/ExoPlayer/issues/2147)). + * Support resampling 24-bit and 32-bit integer to 32-bit float for high + resolution output in `DefaultAudioSink` + ([#3635](https://github.com/google/ExoPlayer/pull/3635)). +* Captions: + * Basic support for PGS subtitles + ([#3008](https://github.com/google/ExoPlayer/issues/3008)). + * Fix handling of CEA-608 captions where multiple buffers have the same + presentation timestamp + ([#3782](https://github.com/google/ExoPlayer/issues/3782)). +* Caching: + * Fix cache corruption issue + ([#3762](https://github.com/google/ExoPlayer/issues/3762)). + * Implement periodic check in `CacheDataSource` to see whether it's + possible to switch to reading/writing the cache having initially + bypassed it. +* IMA extension: + * Fix the player getting stuck when an ad group fails to load + ([#3584](https://github.com/google/ExoPlayer/issues/3584)). + * Work around loadAd not being called beore the LOADED AdEvent arrives + ([#3552](https://github.com/google/ExoPlayer/issues/3552)). + * Handle asset mismatch errors + ([#3801](https://github.com/google/ExoPlayer/issues/3801)). + * Add support for playing non-Extractor content MediaSources in the IMA + demo app ([#3676](https://github.com/google/ExoPlayer/issues/3676)). + * Fix handling of ad tags where ad groups are out of order + ([#3716](https://github.com/google/ExoPlayer/issues/3716)). + * Fix handling of ad tags with only preroll/postroll ad groups + ([#3715](https://github.com/google/ExoPlayer/issues/3715)). + * Propagate ad media preparation errors to IMA so that the ads can be + skipped. + * Handle exceptions in IMA callbacks so that can be logged less verbosely. +* New Cast extension. Simplifies toggling between local and Cast playbacks. +* `EventLogger` moved from the demo app into the core library. +* Fix ANR issue on the Huawei P8 Lite, Huawei Y6II, Moto C+, Meizu M5C, Lenovo + K4 Note and Sony Xperia E5. + ([#3724](https://github.com/google/ExoPlayer/issues/3724), + [#3835](https://github.com/google/ExoPlayer/issues/3835)). +* Fix potential NPE when removing media sources from a + DynamicConcatenatingMediaSource + ([#3796](https://github.com/google/ExoPlayer/issues/3796)). +* Check `sys.display-size` on Philips ATVs + ([#3807](https://github.com/google/ExoPlayer/issues/3807)). +* Release `Extractor`s on the loading thread to avoid potentially leaking + resources when the playback thread has quit by the time the loading task has + completed. +* ID3: Better handle malformed ID3 data + ([#3792](https://github.com/google/ExoPlayer/issues/3792). +* Support 14-bit mode and little endianness in DTS PES packets + ([#3340](https://github.com/google/ExoPlayer/issues/3340)). +* Demo app: Add ability to download not DRM protected content. + +### 2.6.1 (2017-12-15) + +* Add factories to `ExtractorMediaSource`, `HlsMediaSource`, `SsMediaSource`, + `DashMediaSource` and `SingleSampleMediaSource`. +* Use the same listener `MediaSourceEventListener` for all MediaSource + implementations. +* IMA extension: + * Support non-ExtractorMediaSource ads + ([#3302](https://github.com/google/ExoPlayer/issues/3302)). + * Skip ads before the ad preceding the player's initial seek position + ([#3527](https://github.com/google/ExoPlayer/issues/3527)). + * Fix ad loading when there is no preroll. + * Add an option to turn off hiding controls during ad playback + ([#3532](https://github.com/google/ExoPlayer/issues/3532)). + * Support specifying an ads response instead of an ad tag + ([#3548](https://github.com/google/ExoPlayer/issues/3548)). + * Support overriding the ad load timeout + ([#3556](https://github.com/google/ExoPlayer/issues/3556)). +* DASH: Support time zone designators in ISO8601 UTCTiming elements + ([#3524](https://github.com/google/ExoPlayer/issues/3524)). +* Audio: + * Support 32-bit PCM float output from `DefaultAudioSink`, and add an + option to use this with `FfmpegAudioRenderer`. + * Add support for extracting 32-bit WAVE files + ([#3379](https://github.com/google/ExoPlayer/issues/3379)). + * Support extraction and decoding of Dolby Atmos + ([#2465](https://github.com/google/ExoPlayer/issues/2465)). + * Fix handling of playback parameter changes while paused when followed by + a seek. +* SimpleExoPlayer: Allow multiple audio and video debug listeners. +* DefaultTrackSelector: Support undefined language text track selection when + the preferred language is not available + ([#2980](https://github.com/google/ExoPlayer/issues/2980)). +* Add options to `DefaultLoadControl` to set maximum buffer size in bytes and + to choose whether size or time constraints are prioritized. +* Use surfaceless context for secure `DummySurface`, if available + ([#3558](https://github.com/google/ExoPlayer/issues/3558)). +* FLV: Fix playback of live streams that do not contain an audio track + ([#3188](https://github.com/google/ExoPlayer/issues/3188)). +* CEA-608: Fix handling of row count changes in roll-up mode + ([#3513](https://github.com/google/ExoPlayer/issues/3513)). +* Prevent period transitions when seeking to the end of a period when paused + ([#2439](https://github.com/google/ExoPlayer/issues/2439)). + +### 2.6.0 (2017-11-03) + +* Removed "r" prefix from versions. This release is "2.6.0", not "r2.6.0". +* New `Player.DefaultEventListener` abstract class can be extended to avoid + having to implement all methods defined by `Player.EventListener`. +* Added a reason to `EventListener.onPositionDiscontinuity` + ([#3252](https://github.com/google/ExoPlayer/issues/3252)). +* New `setShuffleModeEnabled` method for enabling shuffled playback. +* SimpleExoPlayer: Support for multiple video, text and metadata outputs. +* Support for `Renderer`s that don't consume any media + ([#3212](https://github.com/google/ExoPlayer/issues/3212)). +* Fix reporting of internal position discontinuities via + `Player.onPositionDiscontinuity`. `DISCONTINUITY_REASON_SEEK_ADJUSTMENT` is + added to disambiguate position adjustments during seeks from other types of + internal position discontinuity. +* Fix potential `IndexOutOfBoundsException` when calling + `ExoPlayer.getDuration` + ([#3362](https://github.com/google/ExoPlayer/issues/3362)). +* Fix playbacks involving looping, concatenation and ads getting stuck when + media contains tracks with uneven durations + ([#1874](https://github.com/google/ExoPlayer/issues/1874)). +* Fix issue with `ContentDataSource` when reading from certain + `ContentProvider` implementations + ([#3426](https://github.com/google/ExoPlayer/issues/3426)). +* Better playback experience when the video decoder cannot keep up, by + skipping to key-frames. This is particularly relevant for variable speed + playbacks. +* Allow `SingleSampleMediaSource` to suppress load errors + ([#3140](https://github.com/google/ExoPlayer/issues/3140)). +* `DynamicConcatenatingMediaSource`: Allow specifying a callback to be invoked + after a dynamic playlist modification has been applied + ([#3407](https://github.com/google/ExoPlayer/issues/3407)). +* Audio: New `AudioSink` interface allows customization of audio output path. +* Offline: Added `Downloader` implementations for DASH, HLS, SmoothStreaming + and progressive streams. +* Track selection: + * Fixed adaptive track selection logic for live playbacks + ([#3017](https://github.com/google/ExoPlayer/issues/3017)). + * Added ability to select the lowest bitrate tracks. +* DASH: + * Don't crash when a malformed or unexpected manifest update occurs + ([#2795](https://github.com/google/ExoPlayer/issues/2795)). +* HLS: + * Support for Widevine protected FMP4 variants. + * Support CEA-608 in FMP4 variants. + * Support extractor injection + ([#2748](https://github.com/google/ExoPlayer/issues/2748)). +* DRM: + * Improved compatibility with ClearKey content + ([#3138](https://github.com/google/ExoPlayer/issues/3138)). + * Support multiple PSSH boxes of the same type. + * Retry initial provisioning and key requests if they fail + * Fix incorrect parsing of non-CENC sinf boxes. +* IMA extension: + * Expose `AdsLoader` via getter + ([#3322](https://github.com/google/ExoPlayer/issues/3322)). + * Handle `setPlayWhenReady` calls during ad playbacks + ([#3303](https://github.com/google/ExoPlayer/issues/3303)). + * Ignore seeks if an ad is playing + ([#3309](https://github.com/google/ExoPlayer/issues/3309)). + * Improve robustness of `ImaAdsLoader` in case content is not paused + between content to ad transitions + ([#3430](https://github.com/google/ExoPlayer/issues/3430)). +* UI: + * Allow specifying a `Drawable` for the `TimeBar` scrubber + ([#3337](https://github.com/google/ExoPlayer/issues/3337)). + * Allow multiple listeners on `TimeBar` + ([#3406](https://github.com/google/ExoPlayer/issues/3406)). +* New Leanback extension: Simplifies binding Exoplayer to Leanback UI + components. +* Unit tests moved to Robolectric. +* Misc bugfixes. + +### r2.5.4 (2017-10-19) + +* Remove unnecessary media playlist fetches during playback of live HLS + streams. +* Add the ability to inject a HLS playlist parser through `HlsMediaSource`. +* Fix potential `IndexOutOfBoundsException` when using `ImaMediaSource` + ([#3334](https://github.com/google/ExoPlayer/issues/3334)). +* Fix an issue parsing MP4 content containing non-CENC sinf boxes. +* Fix memory leak when seeking with repeated periods. +* Fix playback position when `ExoPlayer.prepare` is called with + `resetPosition` set to false. +* Ignore MP4 edit lists that seem invalid + ([#3351](https://github.com/google/ExoPlayer/issues/3351)). +* Add extractor flag for ignoring all MP4 edit lists + ([#3358](https://github.com/google/ExoPlayer/issues/3358)). +* Improve extensibility by exposing public constructors for + `FrameworkMediaCrypto` and by making `DefaultDashChunkSource.getNextChunk` + non-final. + +### r2.5.3 (2017-09-20) + +* IMA extension: Support skipping of skippable ads on AndroidTV and other + non-touch devices + ([#3258](https://github.com/google/ExoPlayer/issues/3258)). +* HLS: Fix broken WebVTT captions when PTS wraps around + ([#2928](https://github.com/google/ExoPlayer/issues/2928)). +* Captions: Fix issues rendering CEA-608 captions + ([#3250](https://github.com/google/ExoPlayer/issues/3250)). +* Workaround broken AAC decoders on Galaxy S6 + ([#3249](https://github.com/google/ExoPlayer/issues/3249)). +* Caching: Fix infinite loop when cache eviction fails + ([#3260](https://github.com/google/ExoPlayer/issues/3260)). +* Caching: Force use of BouncyCastle on JellyBean to fix decryption issue + ([#2755](https://github.com/google/ExoPlayer/issues/2755)). + +### r2.5.2 (2017-09-11) + +* IMA extension: Fix issue where ad playback could end prematurely for some + content types ([#3180](https://github.com/google/ExoPlayer/issues/3180)). +* RTMP extension: Fix SIGABRT on fast RTMP stream restart + ([#3156](https://github.com/google/ExoPlayer/issues/3156)). +* UI: Allow app to manually specify ad markers + ([#3184](https://github.com/google/ExoPlayer/issues/3184)). +* DASH: Expose segment indices to subclasses of DefaultDashChunkSource + ([#3037](https://github.com/google/ExoPlayer/issues/3037)). +* Captions: Added robustness against malformed WebVTT captions + ([#3228](https://github.com/google/ExoPlayer/issues/3228)). +* DRM: Support forcing a specific license URL. +* Fix playback error when seeking in media loaded through content:// URIs + ([#3216](https://github.com/google/ExoPlayer/issues/3216)). +* Fix issue playing MP4s in which the last atom specifies a size of zero + ([#3191](https://github.com/google/ExoPlayer/issues/3191)). +* Workaround playback failures on some Xiaomi devices + ([#3171](https://github.com/google/ExoPlayer/issues/3171)). +* Workaround SIGSEGV issue on some devices when setting and swapping surface + for secure playbacks + ([#3215](https://github.com/google/ExoPlayer/issues/3215)). +* Workaround for Nexus 7 issue when swapping output surface + ([#3236](https://github.com/google/ExoPlayer/issues/3236)). +* Workaround for SimpleExoPlayerView's surface not being hidden properly + ([#3160](https://github.com/google/ExoPlayer/issues/3160)). + +### r2.5.1 (2017-08-08) + +* Fix an issue that could cause the reported playback position to stop + advancing in some cases. +* Fix an issue where a Surface could be released whilst still in use by the + player. + +### r2.5.0 (2017-08-07) + +* IMA extension: Wraps the Google Interactive Media Ads (IMA) SDK to provide + an easy and seamless way of incorporating display ads into ExoPlayer + playbacks. You can read more about the IMA extension + [here](https://medium.com/google-exoplayer/playing-ads-with-exoplayer-and-ima-868dfd767ea). +* MediaSession extension: Provides an easy way to connect ExoPlayer with + MediaSessionCompat in the Android Support Library. +* RTMP extension: An extension for playing streams over RTMP. +* Build: Made it easier for application developers to depend on a local + checkout of ExoPlayer. You can learn how to do this + [here](https://medium.com/google-exoplayer/howto-2-depend-on-a-local-checkout-of-exoplayer-bcd7f8531720). +* Core playback improvements: + * Eliminated re-buffering when changing audio and text track selections + during playback of progressive streams + ([#2926](https://github.com/google/ExoPlayer/issues/2926)). + * New DynamicConcatenatingMediaSource class to support playback of dynamic + playlists. + * New ExoPlayer.setRepeatMode method for dynamic toggling of repeat mode + during playback. Use of setRepeatMode should be preferred to + LoopingMediaSource for most looping use cases. You can read more about + setRepeatMode + [here](https://medium.com/google-exoplayer/repeat-modes-in-exoplayer-19dd85f036d3). + * Eliminated jank when switching video playback from one Surface to + another on API level 23+ for unencrypted content, and on devices that + support the EGL_EXT_protected_content OpenGL extension for protected + content ([#677](https://github.com/google/ExoPlayer/issues/677)). + * Enabled ExoPlayer instantiation on background threads without Loopers. + Events from such players are delivered on the application's main thread. +* HLS improvements: + * Optimized adaptive switches for playlists that specify the + EXT-X-INDEPENDENT-SEGMENTS tag. + * Optimized in-buffer seeking + ([#551](https://github.com/google/ExoPlayer/issues/551)). + * Eliminated re-buffering when changing audio and text track selections + during playback, provided the new selection does not require switching + to different renditions + ([#2718](https://github.com/google/ExoPlayer/issues/2718)). + * Exposed all media playlist tags in ExoPlayer's MediaPlaylist object. +* DASH: Support for seamless switching across streams in different + AdaptationSet elements + ([#2431](https://github.com/google/ExoPlayer/issues/2431)). +* DRM: Support for additional crypto schemes (cbc1, cbcs and cens) on API + level 24+ ([#1989](https://github.com/google/ExoPlayer/issues/1989)). +* Captions: Initial support for SSA/ASS subtitles + ([#889](https://github.com/google/ExoPlayer/issues/889)). +* AndroidTV: Fixed issue where tunneled video playback would not start on some + devices ([#2985](https://github.com/google/ExoPlayer/issues/2985)). +* MPEG-TS: Fixed segmentation issue when parsing H262 + ([#2891](https://github.com/google/ExoPlayer/issues/2891)). +* Cronet extension: Support for a user-defined fallback if Cronet library is + not present. +* Fix buffer too small IllegalStateException issue affecting some composite + media playbacks ([#2900](https://github.com/google/ExoPlayer/issues/2900)). +* Misc bugfixes. + +### r2.4.4 (2017-07-19) + +* HLS/MPEG-TS: Some initial optimizations of MPEG-TS extractor performance + ([#3040](https://github.com/google/ExoPlayer/issues/3040)). +* HLS: Fix propagation of format identifier for CEA-608 + ([#3033](https://github.com/google/ExoPlayer/issues/3033)). +* HLS: Detect playlist stuck and reset conditions + ([#2872](https://github.com/google/ExoPlayer/issues/2872)). +* Video: Fix video dimension reporting on some devices + ([#3007](https://github.com/google/ExoPlayer/issues/3007)). + +### r2.4.3 (2017-06-30) + +* Audio: Workaround custom audio decoders misreporting their maximum supported + channel counts ([#2940](https://github.com/google/ExoPlayer/issues/2940)). +* Audio: Workaround for broken MediaTek raw decoder on some devices + ([#2873](https://github.com/google/ExoPlayer/issues/2873)). +* Captions: Fix TTML captions appearing at the top of the screen + ([#2953](https://github.com/google/ExoPlayer/issues/2953)). +* Captions: Fix handling of some DVB subtitles + ([#2957](https://github.com/google/ExoPlayer/issues/2957)). +* Track selection: Fix setSelectionOverride(index, tracks, null) + ([#2988](https://github.com/google/ExoPlayer/issues/2988)). +* GVR extension: Add support for mono input + ([#2710](https://github.com/google/ExoPlayer/issues/2710)). +* FLAC extension: Fix failing build + ([#2977](https://github.com/google/ExoPlayer/pull/2977)). +* Misc bugfixes. + +### r2.4.2 (2017-06-06) + +* Stability: Work around Nexus 10 reboot when playing certain content + ([#2806](https://github.com/google/ExoPlayer/issues/2806)). +* MP3: Correctly treat MP3s with INFO headers as constant bitrate + ([#2895](https://github.com/google/ExoPlayer/issues/2895)). +* HLS: Use average rather than peak bandwidth when available + ([#2863](https://github.com/google/ExoPlayer/issues/2863)). +* SmoothStreaming: Fix timeline for live streams + ([#2760](https://github.com/google/ExoPlayer/issues/2760)). +* UI: Fix DefaultTimeBar invalidation + ([#2871](https://github.com/google/ExoPlayer/issues/2871)). +* Misc bugfixes. + +### r2.4.1 (2017-05-23) + +* Stability: Avoid OutOfMemoryError in extractors when parsing malformed media + ([#2780](https://github.com/google/ExoPlayer/issues/2780)). +* Stability: Avoid native crash on Galaxy Nexus. Avoid unnecessarily large + codec input buffer allocations on all devices + ([#2607](https://github.com/google/ExoPlayer/issues/2607)). +* Variable speed playback: Fix interpolation for rate/pitch adjustment + ([#2774](https://github.com/google/ExoPlayer/issues/2774)). +* HLS: Include EXT-X-DATERANGE tags in HlsMediaPlaylist. +* HLS: Don't expose CEA-608 track if CLOSED-CAPTIONS=NONE + ([#2743](https://github.com/google/ExoPlayer/issues/2743)). +* HLS: Correctly propagate errors loading the media playlist + ([#2623](https://github.com/google/ExoPlayer/issues/2623)). +* UI: DefaultTimeBar enhancements and bug fixes + ([#2740](https://github.com/google/ExoPlayer/issues/2740)). +* Ogg: Fix failure to play some Ogg files + ([#2782](https://github.com/google/ExoPlayer/issues/2782)). +* Captions: Don't select text tack with no language by default. +* Captions: TTML positioning fixes + ([#2824](https://github.com/google/ExoPlayer/issues/2824)). +* Misc bugfixes. + +### r2.4.0 (2017-04-25) + +* New modular library structure. You can read more about depending on + individual library modules + [here](https://medium.com/google-exoplayer/exoplayers-new-modular-structure-a916c0874907). +* Variable speed playback support on API level 16+. You can read more about + changing the playback speed + [here](https://medium.com/google-exoplayer/variable-speed-playback-with-exoplayer-e6e6a71e0343) + ([#26](https://github.com/google/ExoPlayer/issues/26)). +* New time bar view, including support for displaying ad break markers. +* Support DVB subtitles in MPEG-TS and MKV. +* Support adaptive playback for audio only DASH, HLS and SmoothStreaming + ([#1975](https://github.com/google/ExoPlayer/issues/1975)). +* Support for setting extractor flags on DefaultExtractorsFactory + ([#2657](https://github.com/google/ExoPlayer/issues/2657)). +* Support injecting custom renderers into SimpleExoPlayer using a new + RenderersFactory interface. +* Correctly set ExoPlayer's internal thread priority to + `THREAD_PRIORITY_AUDIO`. +* TX3G: Support styling and positioning. +* FLV: + * Support MP3 in FLV. + * Skip unhandled metadata rather than failing + ([#2634](https://github.com/google/ExoPlayer/issues/2634)). + * Fix potential OutOfMemory errors. +* ID3: Better handle malformed ID3 data + ([#2604](https://github.com/google/ExoPlayer/issues/2604), + [#2663](https://github.com/google/ExoPlayer/issues/2663)). +* FFmpeg extension: Fixed build instructions + ([#2561](https://github.com/google/ExoPlayer/issues/2561)). +* VP9 extension: Reduced binary size. +* FLAC extension: Enabled 64 bit targets. +* Misc bugfixes. + +### r2.3.1 (2017-03-23) + +* Fix NPE enabling WebVTT subtitles in DASH streams + ([#2596](https://github.com/google/ExoPlayer/issues/2596)). +* Fix skipping to keyframes when MediaCodecVideoRenderer is enabled but + without a Surface + ([#2575](https://github.com/google/ExoPlayer/issues/2575)). +* Minor fix for CEA-708 decoder + ([#2595](https://github.com/google/ExoPlayer/issues/2595)). + +### r2.3.0 (2017-03-16) + +* GVR extension: Wraps the Google VR Audio SDK to provide spatial audio + rendering. You can read more about the GVR extension + [here](https://medium.com/google-exoplayer/spatial-audio-with-exoplayer-and-gvr-cecb00e9da5f#.xdjebjd7g). +* DASH improvements: + * Support embedded CEA-608 closed captions + ([#2362](https://github.com/google/ExoPlayer/issues/2362)). + * Support embedded EMSG events + ([#2176](https://github.com/google/ExoPlayer/issues/2176)). + * Support mspr:pro manifest element + ([#2386](https://github.com/google/ExoPlayer/issues/2386)). + * Correct handling of empty segment indices at the start of live events + ([#1865](https://github.com/google/ExoPlayer/issues/1865)). +* HLS improvements: + * Respect initial track selection + ([#2353](https://github.com/google/ExoPlayer/issues/2353)). + * Reduced frequency of media playlist requests when playback position is + close to the live edge + ([#2548](https://github.com/google/ExoPlayer/issues/2548)). + * Exposed the master playlist through ExoPlayer.getCurrentManifest() + ([#2537](https://github.com/google/ExoPlayer/issues/2537)). + * Support CLOSED-CAPTIONS #EXT-X-MEDIA type + ([#341](https://github.com/google/ExoPlayer/issues/341)). + * Fixed handling of negative values in #EXT-X-SUPPORT + ([#2495](https://github.com/google/ExoPlayer/issues/2495)). + * Fixed potential endless buffering state for streams with WebVTT + subtitles ([#2424](https://github.com/google/ExoPlayer/issues/2424)). +* MPEG-TS improvements: + * Support for multiple programs. + * Support for multiple closed captions and caption service descriptors + ([#2161](https://github.com/google/ExoPlayer/issues/2161)). +* MP3: Add `FLAG_ENABLE_CONSTANT_BITRATE_SEEKING` extractor option to enable + constant bitrate seeking in MP3 files that would otherwise be unseekable + ([#2445](https://github.com/google/ExoPlayer/issues/2445)). +* ID3: Better handle malformed ID3 data + ([#2486](https://github.com/google/ExoPlayer/issues/2486)). +* Track selection: Added maxVideoBitrate parameter to DefaultTrackSelector. +* DRM: Add support for CENC ClearKey on API level 21+ + ([#2361](https://github.com/google/ExoPlayer/issues/2361)). +* DRM: Support dynamic setting of key request headers + ([#1924](https://github.com/google/ExoPlayer/issues/1924)). +* SmoothStreaming: Fixed handling of start_time placeholder + ([#2447](https://github.com/google/ExoPlayer/issues/2447)). +* FLAC extension: Fix proguard configuration + ([#2427](https://github.com/google/ExoPlayer/issues/2427)). +* Misc bugfixes. + +### r2.2.0 (2017-01-30) + +* Demo app: Automatic recovery from BehindLiveWindowException, plus improved + handling of pausing and resuming live streams + ([#2344](https://github.com/google/ExoPlayer/issues/2344)). +* AndroidTV: Added Support for tunneled video playback + ([#1688](https://github.com/google/ExoPlayer/issues/1688)). +* DRM: Renamed StreamingDrmSessionManager to DefaultDrmSessionManager and + added support for using offline licenses + ([#876](https://github.com/google/ExoPlayer/issues/876)). +* DRM: Introduce OfflineLicenseHelper to help with offline license + acquisition, renewal and release. +* UI: Updated player control assets. Added vector drawables for use on API + level 21 and above. +* UI: Made player control seek bar work correctly with key events if focusable + ([#2278](https://github.com/google/ExoPlayer/issues/2278)). +* HLS: Improved support for streams that use EXT-X-DISCONTINUITY without + EXT-X-DISCONTINUITY-SEQUENCE + ([#1789](https://github.com/google/ExoPlayer/issues/1789)). +* HLS: Support for EXT-X-START tag + ([#1544](https://github.com/google/ExoPlayer/issues/1544)). +* HLS: Check #EXTM3U header is present when parsing the playlist. Fail + gracefully if not + ([#2301](https://github.com/google/ExoPlayer/issues/2301)). +* HLS: Fix memory leak + ([#2319](https://github.com/google/ExoPlayer/issues/2319)). +* HLS: Fix non-seamless first adaptation where master playlist omits + resolution tags ([#2096](https://github.com/google/ExoPlayer/issues/2096)). +* HLS: Fix handling of WebVTT subtitle renditions with non-standard segment + file extensions ([#2025](https://github.com/google/ExoPlayer/issues/2025) + and [#2355](https://github.com/google/ExoPlayer/issues/2355)). +* HLS: Better handle inconsistent HLS playlist update + ([#2249](https://github.com/google/ExoPlayer/issues/2249)). +* DASH: Don't overflow when dealing with large segment numbers + ([#2311](https://github.com/google/ExoPlayer/issues/2311)). +* DASH: Fix propagation of language from the manifest + ([#2335](https://github.com/google/ExoPlayer/issues/2335)). +* SmoothStreaming: Work around "Offset to sample data was negative" failures + ([#2292](https://github.com/google/ExoPlayer/issues/2292), + [#2101](https://github.com/google/ExoPlayer/issues/2101) and + [#1152](https://github.com/google/ExoPlayer/issues/1152)). +* MP3/ID3: Added support for parsing Chapter and URL link frames + ([#2316](https://github.com/google/ExoPlayer/issues/2316)). +* MP3/ID3: Handle ID3 frames that end with empty text field + ([#2309](https://github.com/google/ExoPlayer/issues/2309)). +* Added ClippingMediaSource for playing clipped portions of media + ([#1988](https://github.com/google/ExoPlayer/issues/1988)). +* Added convenience methods to query whether the current window is dynamic and + seekable ([#2320](https://github.com/google/ExoPlayer/issues/2320)). +* Support setting of default headers on HttpDataSource.Factory implementations + ([#2166](https://github.com/google/ExoPlayer/issues/2166)). +* Fixed cache failures when using an encrypted cache content index. +* Fix visual artifacts when switching output surface + ([#2093](https://github.com/google/ExoPlayer/issues/2093)). +* Fix gradle + proguard configurations. +* Fix player position when replacing the MediaSource + ([#2369](https://github.com/google/ExoPlayer/issues/2369)). +* Misc bug fixes, including + [#2330](https://github.com/google/ExoPlayer/issues/2330), + [#2269](https://github.com/google/ExoPlayer/issues/2269), + [#2252](https://github.com/google/ExoPlayer/issues/2252), + [#2264](https://github.com/google/ExoPlayer/issues/2264) and + [#2290](https://github.com/google/ExoPlayer/issues/2290). + +### r2.1.1 (2016-12-20) + +* Fix some subtitle types (e.g. WebVTT) being displayed out of sync + ([#2208](https://github.com/google/ExoPlayer/issues/2208)). +* Fix incorrect position reporting for on-demand HLS media that includes + EXT-X-PROGRAM-DATE-TIME tags + ([#2224](https://github.com/google/ExoPlayer/issues/2224)). +* Fix issue where playbacks could get stuck in the initial buffering state if + over 1MB of data needs to be read to initialize the playback. + +### r2.1.0 (2016-12-14) + +* HLS: Support for seeking in live streams + ([#87](https://github.com/google/ExoPlayer/issues/87)). +* HLS: Improved support: + * Support for EXT-X-PROGRAM-DATE-TIME + ([#747](https://github.com/google/ExoPlayer/issues/747)). + * Improved handling of sample timestamps and their alignment across + variants and renditions. + * Fix issue that could cause playbacks to get stuck in an endless initial + buffering state. + * Correctly propagate BehindLiveWindowException instead of + IndexOutOfBoundsException exception + ([#1695](https://github.com/google/ExoPlayer/issues/1695)). +* MP3/MP4: Support for ID3 metadata, including embedded album art + ([#979](https://github.com/google/ExoPlayer/issues/979)). +* Improved customization of UI components. You can read about customization of + ExoPlayer's UI components + [here](https://medium.com/google-exoplayer/customizing-exoplayers-ui-components-728cf55ee07a#.9ewjg7avi). +* Robustness improvements when handling MediaSource timeline changes and + MediaPeriod transitions. +* CEA-608: Support for caption styling and positioning. +* MPEG-TS: Improved support: + * Support injection of custom TS payload readers. + * Support injection of custom section payload readers. + * Support SCTE-35 splice information messages. + * Support multiple table sections in a single PSI section. + * Fix NullPointerException when an unsupported stream type is encountered + ([#2149](https://github.com/google/ExoPlayer/issues/2149)). + * Avoid failure when expected ID3 header not found + ([#1966](https://github.com/google/ExoPlayer/issues/1966)). +* Improvements to the upstream cache package. + * Support caching of media segments for DASH, HLS and SmoothStreaming. + Note that caching of manifest and playlist files is still not supported + in the (normal) case where the corresponding responses are compressed. + * Support caching for ExtractorMediaSource based playbacks. +* Improved flexibility of SimpleExoPlayer + ([#2102](https://github.com/google/ExoPlayer/issues/2102)). +* Fix issue where only the audio of a video would play due to capability + detection issues ([#2007](https://github.com/google/ExoPlayer/issues/2007), + [#2034](https://github.com/google/ExoPlayer/issues/2034) and + [#2157](https://github.com/google/ExoPlayer/issues/2157)). +* Fix issues that could cause ExtractorMediaSource based playbacks to get + stuck buffering ([#1962](https://github.com/google/ExoPlayer/issues/1962)). +* Correctly set SimpleExoPlayerView surface aspect ratio when an active player + is attached ([#2077](https://github.com/google/ExoPlayer/issues/2077)). +* OGG: Fix playback of short OGG files + ([#1976](https://github.com/google/ExoPlayer/issues/1976)). +* MP4: Support `.mp3` tracks + ([#2066](https://github.com/google/ExoPlayer/issues/2066)). +* SubRip: Don't fail playbacks if SubRip file contains negative timestamps + ([#2145](https://github.com/google/ExoPlayer/issues/2145)). +* Misc bugfixes. + +### r2.0.4 (2016-10-20) + +* Fix crash on Jellybean devices when using playback controls + ([#1965](https://github.com/google/ExoPlayer/issues/1965)). + +### r2.0.3 (2016-10-17) + +* Fixed NullPointerException in ExtractorMediaSource + ([#1914](https://github.com/google/ExoPlayer/issues/1914)). +* Fixed NullPointerException in HlsMediaPeriod + ([#1907](https://github.com/google/ExoPlayer/issues/1907)). +* Fixed memory leak in PlaybackControlView + ([#1908](https://github.com/google/ExoPlayer/issues/1908)). +* Fixed strict mode violation when using + SimpleExoPlayer.setVideoPlayerTextureView(). +* Fixed L3 Widevine provisioning + ([#1925](https://github.com/google/ExoPlayer/issues/1925)). +* Fixed hiding of controls with use_controller="false" + ([#1919](https://github.com/google/ExoPlayer/issues/1919)). +* Improvements to Cronet network stack extension. +* Misc bug fixes. + +### r2.0.2 (2016-10-06) + +* Fixes for MergingMediaSource and sideloaded subtitles. + ([#1882](https://github.com/google/ExoPlayer/issues/1882), + [#1854](https://github.com/google/ExoPlayer/issues/1854), + [#1900](https://github.com/google/ExoPlayer/issues/1900)). +* Reduced effect of application code leaking player references + ([#1855](https://github.com/google/ExoPlayer/issues/1855)). +* Initial support for fragmented MP4 in HLS. +* Misc bug fixes and minor features. + +### r2.0.1 (2016-09-30) + +* Fix playback of short duration content + ([#1837](https://github.com/google/ExoPlayer/issues/1837)). +* Fix MergingMediaSource preparation issue + ([#1853](https://github.com/google/ExoPlayer/issues/1853)). +* Fix live stream buffering (out of memory) issue + ([#1825](https://github.com/google/ExoPlayer/issues/1825)). + +### r2.0.0 (2016-09-14) ExoPlayer 2.x is a major iteration of the library. It includes significant API and architectural changes, new features and many bug fixes. You can read about some of the motivations behind ExoPlayer 2.x [here](https://medium.com/google-exoplayer/exoplayer-2-x-why-what-and-when-74fd9cb139#.am7h8nytm). -* Root package name changed to `com.google.android.exoplayer2`. The library - structure and class names have also been sanitized. Read more - [here](https://medium.com/google-exoplayer/exoplayer-2-x-new-package-and-class-names-ef8e1d9ba96f#.lv8sd4nez). -* Key architectural changes: - * Late binding between rendering and media source components. Allows the same - rendering components to be re-used from one playback to another. Enables - features such as gapless playback through playlists and DASH multi-period - support. - * Improved track selection design. More details can be found - [here](https://medium.com/google-exoplayer/exoplayer-2-x-track-selection-2b62ff712cc9#.n00zo76b6). - * LoadControl now used to control buffering and loading across all playback - types. - * Media source components given additional structure. A new MediaSource class - has been introduced. MediaSources expose Timelines that describe the media - they expose, and can consist of multiple MediaPeriods. This enables features - such as seeking in live playbacks and DASH multi-period support. - * Responsibility for loading the initial DASH/SmoothStreaming/HLS manifest is - promoted to the corresponding MediaSource components and is no longer the - application's responsibility. - * Higher level abstractions such as SimpleExoPlayer have been added to the - library. These make the library easier to use for common use cases. The demo - app is halved in size as a result, whilst at the same time gaining more - functionality. Read more - [here](https://medium.com/google-exoplayer/exoplayer-2-x-improved-demo-app-d97171aaaaa1). - * Enhanced library support for implementing audio extensions. Read more - [here](https://medium.com/google-exoplayer/exoplayer-2-x-new-audio-features-cfb26c2883a#.ua75vu4s3). - * Format and MediaFormat are replaced by a single Format class. -* Key new features: - * Playlist support. Includes support for gapless playback between playlist - items and consistent application of LoadControl and TrackSelector policies - when transitioning between items - ([#1270](https://github.com/google/ExoPlayer/issues/1270)). - * Seeking in live playbacks for DASH and SmoothStreaming - ([#291](https://github.com/google/ExoPlayer/issues/291)). - * DASH multi-period support - ([#557](https://github.com/google/ExoPlayer/issues/557)). - * MediaSource composition allows MediaSources to be concatenated into a - playlist, merged and looped. Read more - [here](https://medium.com/google-exoplayer/exoplayer-2-x-mediasource-composition-6c285fcbca1f#.zfha8qupz). - * Looping support (see above) - ([#490](https://github.com/google/ExoPlayer/issues/490)). - * Ability to query information about all tracks in a piece of media (including - those not supported by the device) - ([#1121](https://github.com/google/ExoPlayer/issues/1121)). - * Improved player controls. - * Support for PSSH in fMP4 moof atoms - ([#1143](https://github.com/google/ExoPlayer/issues/1143)). - * Support for Opus in Ogg - ([#1447](https://github.com/google/ExoPlayer/issues/1447)). - * CacheDataSource support for standalone media file playbacks (mp3, mp4 etc). - * FFMPEG extension (for audio only). -* Key bug fixes: - * Removed unnecessary secondary requests when playing standalone media files - ([#1041](https://github.com/google/ExoPlayer/issues/1041)). - * Fixed playback of video only (i.e. no audio) live streams - ([#758](https://github.com/google/ExoPlayer/issues/758)). - * Fixed silent failure when media buffer is too small - ([#583](https://github.com/google/ExoPlayer/issues/583)). - * Suppressed "Sending message to a Handler on a dead thread" warnings - ([#426](https://github.com/google/ExoPlayer/issues/426)). +* Root package name changed to `com.google.android.exoplayer2`. The library + structure and class names have also been sanitized. Read more + [here](https://medium.com/google-exoplayer/exoplayer-2-x-new-package-and-class-names-ef8e1d9ba96f#.lv8sd4nez). +* Key architectural changes: + * Late binding between rendering and media source components. Allows the + same rendering components to be re-used from one playback to another. + Enables features such as gapless playback through playlists and DASH + multi-period support. + * Improved track selection design. More details can be found + [here](https://medium.com/google-exoplayer/exoplayer-2-x-track-selection-2b62ff712cc9#.n00zo76b6). + * LoadControl now used to control buffering and loading across all + playback types. + * Media source components given additional structure. A new MediaSource + class has been introduced. MediaSources expose Timelines that describe + the media they expose, and can consist of multiple MediaPeriods. This + enables features such as seeking in live playbacks and DASH multi-period + support. + * Responsibility for loading the initial DASH/SmoothStreaming/HLS manifest + is promoted to the corresponding MediaSource components and is no longer + the application's responsibility. + * Higher level abstractions such as SimpleExoPlayer have been added to the + library. These make the library easier to use for common use cases. The + demo app is halved in size as a result, whilst at the same time gaining + more functionality. Read more + [here](https://medium.com/google-exoplayer/exoplayer-2-x-improved-demo-app-d97171aaaaa1). + * Enhanced library support for implementing audio extensions. Read more + [here](https://medium.com/google-exoplayer/exoplayer-2-x-new-audio-features-cfb26c2883a#.ua75vu4s3). + * Format and MediaFormat are replaced by a single Format class. +* Key new features: + * Playlist support. Includes support for gapless playback between playlist + items and consistent application of LoadControl and TrackSelector + policies when transitioning between items + ([#1270](https://github.com/google/ExoPlayer/issues/1270)). + * Seeking in live playbacks for DASH and SmoothStreaming + ([#291](https://github.com/google/ExoPlayer/issues/291)). + * DASH multi-period support + ([#557](https://github.com/google/ExoPlayer/issues/557)). + * MediaSource composition allows MediaSources to be concatenated into a + playlist, merged and looped. Read more + [here](https://medium.com/google-exoplayer/exoplayer-2-x-mediasource-composition-6c285fcbca1f#.zfha8qupz). + * Looping support (see above) + ([#490](https://github.com/google/ExoPlayer/issues/490)). + * Ability to query information about all tracks in a piece of media + (including those not supported by the device) + ([#1121](https://github.com/google/ExoPlayer/issues/1121)). + * Improved player controls. + * Support for PSSH in fMP4 moof atoms + ([#1143](https://github.com/google/ExoPlayer/issues/1143)). + * Support for Opus in Ogg + ([#1447](https://github.com/google/ExoPlayer/issues/1447)). + * CacheDataSource support for standalone media file playbacks (mp3, mp4 + etc). + * FFMPEG extension (for audio only). +* Key bug fixes: + * Removed unnecessary secondary requests when playing standalone media + files ([#1041](https://github.com/google/ExoPlayer/issues/1041)). + * Fixed playback of video only (i.e. no audio) live streams + ([#758](https://github.com/google/ExoPlayer/issues/758)). + * Fixed silent failure when media buffer is too small + ([#583](https://github.com/google/ExoPlayer/issues/583)). + * Suppressed "Sending message to a Handler on a dead thread" warnings + ([#426](https://github.com/google/ExoPlayer/issues/426)). -# Legacy release notes # +# Legacy release notes Note: Since ExoPlayer V1 is still being maintained alongside V2, there is some overlap between these notes and the notes above. r2.0.0 followed from r1.5.11, @@ -1857,197 +2236,197 @@ in all V2 releases. This cannot be assumed for changes in r1.5.12 and later, however it can be assumed that all such changes are included in the most recent V2 release. -### r1.5.16 ### +### r1.5.16 -* VP9 extension: Reduced binary size. -* FLAC extension: Enabled 64 bit targets and fixed proguard config. -* Misc bugfixes. +* VP9 extension: Reduced binary size. +* FLAC extension: Enabled 64 bit targets and fixed proguard config. +* Misc bugfixes. -### r1.5.15 ### +### r1.5.15 -* SmoothStreaming: Fixed handling of start_time placeholder - ([#2447](https://github.com/google/ExoPlayer/issues/2447)). -* Misc bugfixes. +* SmoothStreaming: Fixed handling of start_time placeholder + ([#2447](https://github.com/google/ExoPlayer/issues/2447)). +* Misc bugfixes. -### r1.5.14 ### +### r1.5.14 -* Fixed cache failures when using an encrypted cache content index. -* SmoothStreaming: Work around "Offset to sample data was negative" failures - ([#2292](https://github.com/google/ExoPlayer/issues/2292), - [#2101](https://github.com/google/ExoPlayer/issues/2101) and - [#1152](https://github.com/google/ExoPlayer/issues/1152)). +* Fixed cache failures when using an encrypted cache content index. +* SmoothStreaming: Work around "Offset to sample data was negative" failures + ([#2292](https://github.com/google/ExoPlayer/issues/2292), + [#2101](https://github.com/google/ExoPlayer/issues/2101) and + [#1152](https://github.com/google/ExoPlayer/issues/1152)). -### r1.5.13 ### +### r1.5.13 -* Improvements to the upstream cache package. -* MP4: Support `.mp3` tracks - ([#2066](https://github.com/google/ExoPlayer/issues/2066)). -* SubRip: Don't fail playbacks if SubRip file contains negative timestamps - ([#2145](https://github.com/google/ExoPlayer/issues/2145)). -* MPEG-TS: Avoid failure when expected ID3 header not found - ([#1966](https://github.com/google/ExoPlayer/issues/1966)). -* Misc bugfixes. +* Improvements to the upstream cache package. +* MP4: Support `.mp3` tracks + ([#2066](https://github.com/google/ExoPlayer/issues/2066)). +* SubRip: Don't fail playbacks if SubRip file contains negative timestamps + ([#2145](https://github.com/google/ExoPlayer/issues/2145)). +* MPEG-TS: Avoid failure when expected ID3 header not found + ([#1966](https://github.com/google/ExoPlayer/issues/1966)). +* Misc bugfixes. -### r1.5.12 ### +### r1.5.12 -* Improvements to Cronet network stack extension. -* Fix bug in demo app introduced in r1.5.11 that caused L3 Widevine - provisioning requests to fail. -* Misc bugfixes. +* Improvements to Cronet network stack extension. +* Fix bug in demo app introduced in r1.5.11 that caused L3 Widevine + provisioning requests to fail. +* Misc bugfixes. -### r1.5.11 ### +### r1.5.11 -* Cronet network stack extension. -* HLS: Fix propagation of language for alternative audio renditions - ([#1784](https://github.com/google/ExoPlayer/issues/1784)). -* WebM: Support for subsample encryption. -* ID3: Fix EOS detection for 2-byte encodings - ([#1774](https://github.com/google/ExoPlayer/issues/1774)). -* MPEG-TS: Support multiple tracks of the same type. -* MPEG-TS: Work toward robust handling of stream corruption. -* Fix ContentDataSource failures triggered by garbage collector - ([#1759](https://github.com/google/ExoPlayer/issues/1759)). +* Cronet network stack extension. +* HLS: Fix propagation of language for alternative audio renditions + ([#1784](https://github.com/google/ExoPlayer/issues/1784)). +* WebM: Support for subsample encryption. +* ID3: Fix EOS detection for 2-byte encodings + ([#1774](https://github.com/google/ExoPlayer/issues/1774)). +* MPEG-TS: Support multiple tracks of the same type. +* MPEG-TS: Work toward robust handling of stream corruption. +* Fix ContentDataSource failures triggered by garbage collector + ([#1759](https://github.com/google/ExoPlayer/issues/1759)). -### r1.5.10 ### +### r1.5.10 -* HLS: Stability fixes. -* MP4: Support for stz2 Atoms. -* Enable 4K format selection on Sony AndroidTV + nVidia SHIELD. -* TX3G caption fixes. +* HLS: Stability fixes. +* MP4: Support for stz2 Atoms. +* Enable 4K format selection on Sony AndroidTV + nVidia SHIELD. +* TX3G caption fixes. -### r1.5.9 ### +### r1.5.9 -* MP4: Fixed incorrect sniffing in some cases (#1523). -* MP4: Improved file compatibility (#1567). -* ID3: Support for TIT2 and APIC frames. -* Fixed querying of platform decoders on some devices. -* Misc bug fixes. +* MP4: Fixed incorrect sniffing in some cases (#1523). +* MP4: Improved file compatibility (#1567). +* ID3: Support for TIT2 and APIC frames. +* Fixed querying of platform decoders on some devices. +* Misc bug fixes. -### r1.5.8 ### +### r1.5.8 -* HLS: Fix handling of HTTP redirects. -* Audio: Minor adjustment to improve A/V sync. -* OGG: Support FLAC in OGG. -* TTML: Support regions. -* WAV/PCM: Support 8, 24 and 32-bit WAV and PCM audio. -* Misc bug fixes and performance optimizations. +* HLS: Fix handling of HTTP redirects. +* Audio: Minor adjustment to improve A/V sync. +* OGG: Support FLAC in OGG. +* TTML: Support regions. +* WAV/PCM: Support 8, 24 and 32-bit WAV and PCM audio. +* Misc bug fixes and performance optimizations. -### r1.5.7 ### +### r1.5.7 -* OGG: Support added for OGG. -* FLAC: Support for FLAC extraction and playback (via an extension). -* HLS: Multiple audio track support (via Renditions). -* FMP4: Support multiple tracks in fragmented MP4 (not applicable to - DASH/SmoothStreaming). -* WAV: Support for 16-bit WAV files. -* MKV: Support non-square pixel formats. -* Misc bug fixes. +* OGG: Support added for OGG. +* FLAC: Support for FLAC extraction and playback (via an extension). +* HLS: Multiple audio track support (via Renditions). +* FMP4: Support multiple tracks in fragmented MP4 (not applicable to + DASH/SmoothStreaming). +* WAV: Support for 16-bit WAV files. +* MKV: Support non-square pixel formats. +* Misc bug fixes. -### r1.5.6 ### +### r1.5.6 -* MP3: Fix mono streams playing at 2x speed on some MediaTek based devices - (#801). -* MP3: Fix playback of some streams when stream length is unknown. -* ID3: Support multiple frames of the same type in a single tag. -* CEA-608: Correctly handle repeated control characters, fixing an issue in - which captions would immediately disappear. -* AVC3: Fix decoder failures on some MediaTek devices in the case where the - first buffer fed to the decoder does not start with SPS/PPS NAL units. -* Misc bug fixes. +* MP3: Fix mono streams playing at 2x speed on some MediaTek based devices + (#801). +* MP3: Fix playback of some streams when stream length is unknown. +* ID3: Support multiple frames of the same type in a single tag. +* CEA-608: Correctly handle repeated control characters, fixing an issue in + which captions would immediately disappear. +* AVC3: Fix decoder failures on some MediaTek devices in the case where the + first buffer fed to the decoder does not start with SPS/PPS NAL units. +* Misc bug fixes. -### r1.5.5 ### +### r1.5.5 -* DASH: Enable MP4 embedded WebVTT playback (#1185) -* HLS: Fix handling of extended ID3 tags in MPEG-TS (#1181) -* MP3: Fix incorrect position calculation in VBRI header (#1197) -* Fix issue seeking backward using SingleSampleSource (#1193) +* DASH: Enable MP4 embedded WebVTT playback (#1185) +* HLS: Fix handling of extended ID3 tags in MPEG-TS (#1181) +* MP3: Fix incorrect position calculation in VBRI header (#1197) +* Fix issue seeking backward using SingleSampleSource (#1193) -### r1.5.4 ### +### r1.5.4 -* HLS: Support for variant selection and WebVtt subtitles. -* MP4: Support for embedded WebVtt. -* Improved device compatibility. -* Fix for resource leak (Issue #1066). -* Misc bug fixes + minor features. +* HLS: Support for variant selection and WebVtt subtitles. +* MP4: Support for embedded WebVtt. +* Improved device compatibility. +* Fix for resource leak (Issue #1066). +* Misc bug fixes + minor features. -### r1.5.3 ### +### r1.5.3 -* Support for FLV (without seeking). -* MP4: Fix for playback of media containing basic edit lists. -* QuickTime: Fix parsing of QuickTime style audio sample entry. -* HLS: Add H262 support for devices that have an H262 decoder. -* Allow AudioTrack PlaybackParams (e.g. speed/pitch) on API level 23+. -* Correctly detect 4K displays on API level 23+. -* Misc bug fixes. +* Support for FLV (without seeking). +* MP4: Fix for playback of media containing basic edit lists. +* QuickTime: Fix parsing of QuickTime style audio sample entry. +* HLS: Add H262 support for devices that have an H262 decoder. +* Allow AudioTrack PlaybackParams (e.g. speed/pitch) on API level 23+. +* Correctly detect 4K displays on API level 23+. +* Misc bug fixes. -### r1.5.2 ### +### r1.5.2 -* MPEG-TS/HLS: Fix frame drops playing H265 video. -* SmoothStreaming: Fix parsing of ProtectionHeader. +* MPEG-TS/HLS: Fix frame drops playing H265 video. +* SmoothStreaming: Fix parsing of ProtectionHeader. -### r1.5.1 ### +### r1.5.1 -* Enable smooth frame release by default. -* Added OkHttpDataSource extension. -* AndroidTV: Correctly detect 4K display size on Bravia devices. -* FMP4: Handle non-sample data in mdat boxes. -* TTML: Fix parsing of some colors on Jellybean. -* SmoothStreaming: Ignore tfdt boxes. -* Misc bug fixes. +* Enable smooth frame release by default. +* Added OkHttpDataSource extension. +* AndroidTV: Correctly detect 4K display size on Bravia devices. +* FMP4: Handle non-sample data in mdat boxes. +* TTML: Fix parsing of some colors on Jellybean. +* SmoothStreaming: Ignore tfdt boxes. +* Misc bug fixes. -### r1.5.0 ### +### r1.5.0 -* Multi-track support. -* DASH: Limited support for multi-period manifests. -* HLS: Smoother format adaptation. -* HLS: Support for MP3 media segments. -* TTML: Support for most embedded TTML styling. -* WebVTT: Enhanced positioning support. -* Initial playback tests. -* Misc bug fixes. +* Multi-track support. +* DASH: Limited support for multi-period manifests. +* HLS: Smoother format adaptation. +* HLS: Support for MP3 media segments. +* TTML: Support for most embedded TTML styling. +* WebVTT: Enhanced positioning support. +* Initial playback tests. +* Misc bug fixes. -### r1.4.2 ### +### r1.4.2 -* Implemented automatic format detection for regular container formats. -* Added UdpDataSource for connecting to multicast streams. -* Improved robustness for MP4 playbacks. -* Misc bug fixes. +* Implemented automatic format detection for regular container formats. +* Added UdpDataSource for connecting to multicast streams. +* Improved robustness for MP4 playbacks. +* Misc bug fixes. -### r1.4.1 ### +### r1.4.1 -* HLS: Fix premature playback failures that could occur in some cases. +* HLS: Fix premature playback failures that could occur in some cases. -### r1.4.0 ### +### r1.4.0 -* Support for extracting Matroska streams (implemented by WebmExtractor). -* Support for tx3g captions in MP4 streams. -* Support for H.265 in MPEG-TS streams on supported devices. -* HLS: Added support for MPEG audio (e.g. MP3) in TS media segments. -* HLS: Improved robustness against missing chunks and variants. -* MP4: Added support for embedded MPEG audio (e.g. MP3). -* TTML: Improved handling of whitespace. -* DASH: Support Mpd.Location element. -* Add option to TsExtractor to allow non-IDR keyframes. -* Added MulticastDataSource for connecting to multicast streams. -* (WorkInProgress) - First steps to supporting seeking in DASH DVR window. -* (WorkInProgress) - First steps to supporting styled + positioned subtitles. -* Misc bug fixes. +* Support for extracting Matroska streams (implemented by WebmExtractor). +* Support for tx3g captions in MP4 streams. +* Support for H.265 in MPEG-TS streams on supported devices. +* HLS: Added support for MPEG audio (e.g. MP3) in TS media segments. +* HLS: Improved robustness against missing chunks and variants. +* MP4: Added support for embedded MPEG audio (e.g. MP3). +* TTML: Improved handling of whitespace. +* DASH: Support Mpd.Location element. +* Add option to TsExtractor to allow non-IDR keyframes. +* Added MulticastDataSource for connecting to multicast streams. +* (WorkInProgress) - First steps to supporting seeking in DASH DVR window. +* (WorkInProgress) - First steps to supporting styled + positioned subtitles. +* Misc bug fixes. -### r1.3.3 ### +### r1.3.3 -* HLS: Fix failure when playing HLS AAC streams. -* Misc bug fixes. +* HLS: Fix failure when playing HLS AAC streams. +* Misc bug fixes. -### r1.3.2 ### +### r1.3.2 -* DataSource improvements: `DefaultUriDataSource` now handles http://, https://, - file://, asset:// and content:// URIs automatically. It also handles - file:///android_asset/* URIs, and file paths like /path/to/media.mp4 where the - scheme is omitted. -* HLS: Fix for some ID3 events being dropped. -* HLS: Correctly handle 0x0 and floating point RESOLUTION tags. -* Mp3Extractor: robustness improvements. +* DataSource improvements: `DefaultUriDataSource` now handles http://, + https://, file://, asset:// and content:// URIs automatically. It also + handles file:///android_asset/* URIs, and file paths like /path/to/media.mp4 + where the scheme is omitted. +* HLS: Fix for some ID3 events being dropped. +* HLS: Correctly handle 0x0 and floating point RESOLUTION tags. +* Mp3Extractor: robustness improvements. -### r1.3.1 ### +### r1.3.1 -* No notes provided. +* No notes provided. diff --git a/build.gradle b/build.gradle index a4823b94ee..d520925fb0 100644 --- a/build.gradle +++ b/build.gradle @@ -17,9 +17,9 @@ buildscript { jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:3.5.1' + classpath 'com.android.tools.build:gradle:3.6.3' classpath 'com.novoda:bintray-release:0.9.1' - classpath 'com.google.android.gms:strict-version-matcher-plugin:1.2.0' + classpath 'com.google.android.gms:strict-version-matcher-plugin:1.2.1' } } allprojects { diff --git a/constants.gradle b/constants.gradle index 65812e4274..1a7840588f 100644 --- a/constants.gradle +++ b/constants.gradle @@ -1,4 +1,4 @@ -// Copyright (C) 2017 The Android Open Source Project +// Copyright 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. @@ -13,20 +13,20 @@ // limitations under the License. project.ext { // ExoPlayer version and version code. - releaseVersion = '2.11.0' - releaseVersionCode = 2011000 + releaseVersion = '2.11.4' + releaseVersionCode = 2011004 minSdkVersion = 16 appTargetSdkVersion = 29 - targetSdkVersion = 28 // TODO: Bump once b/143232359 is resolved + targetSdkVersion = 28 // TODO: Bump once b/143232359 is resolved. Also fix TODOs in UtilTest. compileSdkVersion = 29 dexmakerVersion = '2.21.0' + junitVersion = '4.13-rc-2' + guavaVersion = '28.2-android' mockitoVersion = '2.25.0' - robolectricVersion = '4.3' - autoValueVersion = '1.6' - autoServiceVersion = '1.0-rc4' + robolectricVersion = '4.3.1' checkerframeworkVersion = '2.5.0' jsr305Version = '3.0.2' - kotlinAnnotationsVersion = '1.3.31' + kotlinAnnotationsVersion = '1.3.70' androidxAnnotationVersion = '1.1.0' androidxAppCompatVersion = '1.1.0' androidxCollectionVersion = '1.1.0' @@ -35,7 +35,7 @@ project.ext { androidxTestJUnitVersion = '1.1.1' androidxTestRunnerVersion = '1.2.0' androidxTestRulesVersion = '1.2.0' - truthVersion = '0.44' + truthVersion = '1.0' modulePrefix = ':' if (gradle.ext.has('exoplayerModulePrefix')) { modulePrefix += gradle.ext.exoplayerModulePrefix diff --git a/core_settings.gradle b/core_settings.gradle index 0f9746af96..ac56933155 100644 --- a/core_settings.gradle +++ b/core_settings.gradle @@ -18,12 +18,15 @@ if (gradle.ext.has('exoplayerModulePrefix')) { } include modulePrefix + 'library' +include modulePrefix + 'library-common' include modulePrefix + 'library-core' include modulePrefix + 'library-dash' +include modulePrefix + 'library-extractor' include modulePrefix + 'library-hls' include modulePrefix + 'library-smoothstreaming' include modulePrefix + 'library-ui' include modulePrefix + 'testutils' +include modulePrefix + 'testdata' include modulePrefix + 'extension-av1' include modulePrefix + 'extension-ffmpeg' include modulePrefix + 'extension-flac' @@ -41,12 +44,15 @@ include modulePrefix + 'extension-jobdispatcher' include modulePrefix + 'extension-workmanager' project(modulePrefix + 'library').projectDir = new File(rootDir, 'library/all') +project(modulePrefix + 'library-common').projectDir = new File(rootDir, 'library/common') project(modulePrefix + 'library-core').projectDir = new File(rootDir, 'library/core') project(modulePrefix + 'library-dash').projectDir = new File(rootDir, 'library/dash') +project(modulePrefix + 'library-extractor').projectDir = new File(rootDir, 'library/extractor') project(modulePrefix + 'library-hls').projectDir = new File(rootDir, 'library/hls') project(modulePrefix + 'library-smoothstreaming').projectDir = new File(rootDir, 'library/smoothstreaming') project(modulePrefix + 'library-ui').projectDir = new File(rootDir, 'library/ui') project(modulePrefix + 'testutils').projectDir = new File(rootDir, 'testutils') +project(modulePrefix + 'testdata').projectDir = new File(rootDir, 'testdata') project(modulePrefix + 'extension-av1').projectDir = new File(rootDir, 'extensions/av1') project(modulePrefix + 'extension-ffmpeg').projectDir = new File(rootDir, 'extensions/ffmpeg') project(modulePrefix + 'extension-flac').projectDir = new File(rootDir, 'extensions/flac') diff --git a/demos/cast/build.gradle b/demos/cast/build.gradle index f9228e4b79..c929f09c87 100644 --- a/demos/cast/build.gradle +++ b/demos/cast/build.gradle @@ -57,8 +57,8 @@ dependencies { implementation project(modulePrefix + 'library-ui') implementation project(modulePrefix + 'extension-cast') implementation 'androidx.appcompat:appcompat:' + androidxAppCompatVersion - implementation 'androidx.recyclerview:recyclerview:1.0.0' - implementation 'com.google.android.material:material:1.0.0' + implementation 'androidx.recyclerview:recyclerview:1.1.0' + implementation 'com.google.android.material:material:1.1.0' } apply plugin: 'com.google.android.gms.strict-version-matcher-plugin' diff --git a/demos/cast/src/main/AndroidManifest.xml b/demos/cast/src/main/AndroidManifest.xml index dbfdd833f6..d92d9e2303 100644 --- a/demos/cast/src/main/AndroidManifest.xml +++ b/demos/cast/src/main/AndroidManifest.xml @@ -18,6 +18,7 @@ + diff --git a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/DemoUtil.java b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/DemoUtil.java index dacdbfe616..50343f9205 100644 --- a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/DemoUtil.java +++ b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/DemoUtil.java @@ -17,8 +17,8 @@ package com.google.android.exoplayer2.castdemo; import android.net.Uri; import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.ext.cast.MediaItem; -import com.google.android.exoplayer2.ext.cast.MediaItem.DrmConfiguration; +import com.google.android.exoplayer2.MediaItem; +import com.google.android.exoplayer2.MediaMetadata; import com.google.android.exoplayer2.util.MimeTypes; import java.util.ArrayList; import java.util.Collections; @@ -42,19 +42,19 @@ import java.util.List; samples.add( new MediaItem.Builder() .setUri("https://storage.googleapis.com/wvmedia/clear/h264/tears/tears.mpd") - .setTitle("Clear DASH: Tears") + .setMediaMetadata(new MediaMetadata.Builder().setTitle("Clear DASH: Tears").build()) .setMimeType(MIME_TYPE_DASH) .build()); samples.add( new MediaItem.Builder() .setUri("https://storage.googleapis.com/shaka-demo-assets/angel-one-hls/hls.m3u8") - .setTitle("Clear HLS: Angel one") + .setMediaMetadata(new MediaMetadata.Builder().setTitle("Clear HLS: Angel one").build()) .setMimeType(MIME_TYPE_HLS) .build()); samples.add( new MediaItem.Builder() .setUri("https://html5demos.com/assets/dizzy.mp4") - .setTitle("Clear MP4: Dizzy") + .setMediaMetadata(new MediaMetadata.Builder().setTitle("Clear MP4: Dizzy").build()) .setMimeType(MIME_TYPE_VIDEO_MP4) .build()); @@ -62,39 +62,29 @@ import java.util.List; samples.add( new MediaItem.Builder() .setUri(Uri.parse("https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd")) - .setTitle("Widevine DASH cenc: Tears") + .setMediaMetadata( + new MediaMetadata.Builder().setTitle("Widevine DASH cenc: Tears").build()) .setMimeType(MIME_TYPE_DASH) - .setDrmConfiguration( - new DrmConfiguration( - C.WIDEVINE_UUID, - Uri.parse("https://proxy.uat.widevine.com/proxy?provider=widevine_test"), - Collections.emptyMap())) + .setDrmUuid(C.WIDEVINE_UUID) + .setDrmLicenseUri("https://proxy.uat.widevine.com/proxy?provider=widevine_test") .build()); samples.add( new MediaItem.Builder() - .setUri( - Uri.parse( - "https://storage.googleapis.com/wvmedia/cbc1/h264/tears/tears_aes_cbc1.mpd")) - .setTitle("Widevine DASH cbc1: Tears") + .setUri("https://storage.googleapis.com/wvmedia/cbc1/h264/tears/tears_aes_cbc1.mpd") + .setMediaMetadata( + new MediaMetadata.Builder().setTitle("Widevine DASH cbc1: Tears").build()) .setMimeType(MIME_TYPE_DASH) - .setDrmConfiguration( - new DrmConfiguration( - C.WIDEVINE_UUID, - Uri.parse("https://proxy.uat.widevine.com/proxy?provider=widevine_test"), - Collections.emptyMap())) + .setDrmUuid(C.WIDEVINE_UUID) + .setDrmLicenseUri("https://proxy.uat.widevine.com/proxy?provider=widevine_test") .build()); samples.add( new MediaItem.Builder() - .setUri( - Uri.parse( - "https://storage.googleapis.com/wvmedia/cbcs/h264/tears/tears_aes_cbcs.mpd")) - .setTitle("Widevine DASH cbcs: Tears") + .setUri("https://storage.googleapis.com/wvmedia/cbcs/h264/tears/tears_aes_cbcs.mpd") + .setMediaMetadata( + new MediaMetadata.Builder().setTitle("Widevine DASH cbcs: Tears").build()) .setMimeType(MIME_TYPE_DASH) - .setDrmConfiguration( - new DrmConfiguration( - C.WIDEVINE_UUID, - Uri.parse("https://proxy.uat.widevine.com/proxy?provider=widevine_test"), - Collections.emptyMap())) + .setDrmUuid(C.WIDEVINE_UUID) + .setDrmLicenseUri("https://proxy.uat.widevine.com/proxy?provider=widevine_test") .build()); SAMPLES = Collections.unmodifiableList(samples); diff --git a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/MainActivity.java b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/MainActivity.java index 0c5b5037f5..cf8b02a515 100644 --- a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/MainActivity.java +++ b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/MainActivity.java @@ -37,10 +37,12 @@ import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView.ViewHolder; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.MediaItem; import com.google.android.exoplayer2.SimpleExoPlayer; -import com.google.android.exoplayer2.ext.cast.MediaItem; import com.google.android.exoplayer2.ui.PlayerControlView; import com.google.android.exoplayer2.ui.PlayerView; +import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.Util; import com.google.android.gms.cast.framework.CastButtonFactory; import com.google.android.gms.cast.framework.CastContext; import com.google.android.gms.dynamite.DynamiteModule; @@ -171,8 +173,6 @@ public class MainActivity extends AppCompatActivity showToast(R.string.error_unsupported_audio); } else if (trackType == C.TRACK_TYPE_VIDEO) { showToast(R.string.error_unsupported_video); - } else { - // Do nothing. } } @@ -199,6 +199,7 @@ public class MainActivity extends AppCompatActivity private class MediaQueueListAdapter extends RecyclerView.Adapter { @Override + @NonNull public QueueItemViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { TextView v = (TextView) LayoutInflater.from(parent.getContext()) .inflate(android.R.layout.simple_list_item_1, parent, false); @@ -207,9 +208,10 @@ public class MainActivity extends AppCompatActivity @Override public void onBindViewHolder(QueueItemViewHolder holder, int position) { - holder.item = playerManager.getItem(position); + holder.item = Assertions.checkNotNull(playerManager.getItem(position)); + TextView view = holder.textView; - view.setText(holder.item.title); + view.setText(holder.item.mediaMetadata.title); // TODO: Solve coloring using the theme's ColorStateList. view.setTextColor( ColorUtils.setAlphaComponent( @@ -236,7 +238,9 @@ public class MainActivity extends AppCompatActivity } @Override - public boolean onMove(RecyclerView list, RecyclerView.ViewHolder origin, + public boolean onMove( + @NonNull RecyclerView list, + RecyclerView.ViewHolder origin, RecyclerView.ViewHolder target) { int fromPosition = origin.getAdapterPosition(); int toPosition = target.getAdapterPosition(); @@ -261,7 +265,7 @@ public class MainActivity extends AppCompatActivity } @Override - public void clearView(RecyclerView recyclerView, ViewHolder viewHolder) { + public void clearView(@NonNull RecyclerView recyclerView, @NonNull ViewHolder viewHolder) { super.clearView(recyclerView, viewHolder); if (draggingFromPosition != C.INDEX_UNSET) { QueueItemViewHolder queueItemHolder = (QueueItemViewHolder) viewHolder; @@ -300,11 +304,11 @@ public class MainActivity extends AppCompatActivity super(context, android.R.layout.simple_list_item_1, DemoUtil.SAMPLES); } - @NonNull @Override + @NonNull public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) { View view = super.getView(position, convertView, parent); - ((TextView) view).setText(getItem(position).title); + ((TextView) view).setText(Util.castNonNull(getItem(position)).mediaMetadata.title); return view; } } 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 85104e0d18..c5dfe70d93 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 @@ -16,45 +16,29 @@ package com.google.android.exoplayer2.castdemo; import android.content.Context; -import android.net.Uri; import android.view.KeyEvent; import android.view.View; +import androidx.annotation.NonNull; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.MediaItem; import com.google.android.exoplayer2.Player; 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.SimpleExoPlayer; import com.google.android.exoplayer2.Timeline; -import com.google.android.exoplayer2.Timeline.Period; -import com.google.android.exoplayer2.drm.DefaultDrmSessionManager; -import com.google.android.exoplayer2.drm.DrmSessionManager; -import com.google.android.exoplayer2.drm.ExoMediaCrypto; -import com.google.android.exoplayer2.drm.FrameworkMediaDrm; -import com.google.android.exoplayer2.drm.HttpMediaDrmCallback; import com.google.android.exoplayer2.ext.cast.CastPlayer; -import com.google.android.exoplayer2.ext.cast.DefaultMediaItemConverter; -import com.google.android.exoplayer2.ext.cast.MediaItem; -import com.google.android.exoplayer2.ext.cast.MediaItemConverter; import com.google.android.exoplayer2.ext.cast.SessionAvailabilityListener; -import com.google.android.exoplayer2.source.ConcatenatingMediaSource; -import com.google.android.exoplayer2.source.MediaSource; -import com.google.android.exoplayer2.source.ProgressiveMediaSource; +import com.google.android.exoplayer2.source.DefaultMediaSourceFactory; import com.google.android.exoplayer2.source.TrackGroupArray; -import com.google.android.exoplayer2.source.dash.DashMediaSource; -import com.google.android.exoplayer2.source.hls.HlsMediaSource; -import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import com.google.android.exoplayer2.trackselection.MappingTrackSelector; import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.ui.PlayerControlView; import com.google.android.exoplayer2.ui.PlayerView; import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory; -import com.google.android.exoplayer2.util.Util; -import com.google.android.gms.cast.MediaQueueItem; import com.google.android.gms.cast.framework.CastContext; import java.util.ArrayList; -import java.util.Map; /** Manages players and an internal media queue for the demo app. */ /* package */ class PlayerManager implements EventListener, SessionAvailabilityListener { @@ -77,6 +61,7 @@ import java.util.Map; private static final DefaultHttpDataSourceFactory DATA_SOURCE_FACTORY = new DefaultHttpDataSourceFactory(USER_AGENT); + private final DefaultMediaSourceFactory defaultMediaSourceFactory; private final PlayerView localPlayerView; private final PlayerControlView castControlView; private final DefaultTrackSelector trackSelector; @@ -84,8 +69,6 @@ import java.util.Map; private final CastPlayer castPlayer; private final ArrayList mediaQueue; private final Listener listener; - private final ConcatenatingMediaSource concatenatingMediaSource; - private final MediaItemConverter mediaItemConverter; private TrackGroupArray lastSeenTrackGroupArray; private int currentItemIndex; @@ -111,11 +94,10 @@ import java.util.Map; this.castControlView = castControlView; mediaQueue = new ArrayList<>(); currentItemIndex = C.INDEX_UNSET; - concatenatingMediaSource = new ConcatenatingMediaSource(); - mediaItemConverter = new DefaultMediaItemConverter(); trackSelector = new DefaultTrackSelector(context); exoPlayer = new SimpleExoPlayer.Builder(context).setTrackSelector(trackSelector).build(); + defaultMediaSourceFactory = DefaultMediaSourceFactory.newInstance(context, DATA_SOURCE_FACTORY); exoPlayer.addListener(this); localPlayerView.setPlayer(exoPlayer); @@ -135,7 +117,7 @@ import java.util.Map; * @param itemIndex The index of the item to play. */ public void selectQueueItem(int itemIndex) { - setCurrentItem(itemIndex, C.TIME_UNSET, true); + setCurrentItem(itemIndex); } /** Returns the index of the currently played item. */ @@ -150,10 +132,7 @@ import java.util.Map; */ public void addItem(MediaItem item) { mediaQueue.add(item); - concatenatingMediaSource.addMediaSource(buildMediaSource(item)); - if (currentPlayer == castPlayer) { - castPlayer.addItems(mediaItemConverter.toMediaQueueItem(item)); - } + currentPlayer.addMediaItem(item); } /** Returns the size of the media queue. */ @@ -182,16 +161,7 @@ import java.util.Map; if (itemIndex == -1) { return false; } - concatenatingMediaSource.removeMediaSource(itemIndex); - if (currentPlayer == castPlayer) { - if (castPlayer.getPlaybackState() != Player.STATE_IDLE) { - Timeline castTimeline = castPlayer.getCurrentTimeline(); - if (castTimeline.getPeriodCount() <= itemIndex) { - return false; - } - castPlayer.removeItem((int) castTimeline.getPeriod(itemIndex, new Period()).id); - } - } + currentPlayer.removeMediaItem(itemIndex); mediaQueue.remove(itemIndex); if (itemIndex == currentItemIndex && itemIndex == mediaQueue.size()) { maybeSetCurrentItemAndNotify(C.INDEX_UNSET); @@ -205,34 +175,25 @@ import java.util.Map; * Moves an item within the queue. * * @param item The item to move. - * @param toIndex The target index of the item in the queue. + * @param newIndex The target index of the item in the queue. * @return Whether the item move was successful. */ - public boolean moveItem(MediaItem item, int toIndex) { + public boolean moveItem(MediaItem item, int newIndex) { int fromIndex = mediaQueue.indexOf(item); if (fromIndex == -1) { return false; } - // Player update. - concatenatingMediaSource.moveMediaSource(fromIndex, toIndex); - if (currentPlayer == castPlayer && castPlayer.getPlaybackState() != Player.STATE_IDLE) { - Timeline castTimeline = castPlayer.getCurrentTimeline(); - int periodCount = castTimeline.getPeriodCount(); - if (periodCount <= fromIndex || periodCount <= toIndex) { - return false; - } - int elementId = (int) castTimeline.getPeriod(fromIndex, new Period()).id; - castPlayer.moveItem(elementId, toIndex); - } - mediaQueue.add(toIndex, mediaQueue.remove(fromIndex)); + // Player update. + currentPlayer.moveMediaItem(fromIndex, newIndex); + mediaQueue.add(newIndex, mediaQueue.remove(fromIndex)); // Index update. if (fromIndex == currentItemIndex) { - maybeSetCurrentItemAndNotify(toIndex); - } else if (fromIndex < currentItemIndex && toIndex >= currentItemIndex) { + maybeSetCurrentItemAndNotify(newIndex); + } else if (fromIndex < currentItemIndex && newIndex >= currentItemIndex) { maybeSetCurrentItemAndNotify(currentItemIndex - 1); - } else if (fromIndex > currentItemIndex && toIndex <= currentItemIndex) { + } else if (fromIndex > currentItemIndex && newIndex <= currentItemIndex) { maybeSetCurrentItemAndNotify(currentItemIndex + 1); } @@ -257,7 +218,6 @@ import java.util.Map; public void release() { currentItemIndex = C.INDEX_UNSET; mediaQueue.clear(); - concatenatingMediaSource.clear(); castPlayer.setSessionAvailabilityListener(null); castPlayer.release(); localPlayerView.setPlayer(null); @@ -267,7 +227,7 @@ import java.util.Map; // Player.EventListener implementation. @Override - public void onPlayerStateChanged(boolean playWhenReady, @Player.State int playbackState) { + public void onPlaybackStateChanged(@Player.State int playbackState) { updateCurrentItemIndex(); } @@ -277,12 +237,13 @@ import java.util.Map; } @Override - public void onTimelineChanged(Timeline timeline, @TimelineChangeReason int reason) { + public void onTimelineChanged(@NonNull Timeline timeline, @TimelineChangeReason int reason) { updateCurrentItemIndex(); } @Override - public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) { + public void onTracksChanged( + @NonNull TrackGroupArray trackGroups, @NonNull TrackSelectionArray trackSelections) { if (currentPlayer == exoPlayer && trackGroups != lastSeenTrackGroupArray) { MappingTrackSelector.MappedTrackInfo mappedTrackInfo = trackSelector.getCurrentMappedTrackInfo(); @@ -360,35 +321,26 @@ import java.util.Map; this.currentPlayer = currentPlayer; // Media queue management. - if (currentPlayer == exoPlayer) { - exoPlayer.prepare(concatenatingMediaSource); - } - - // Playback transition. - if (windowIndex != C.INDEX_UNSET) { - setCurrentItem(windowIndex, playbackPositionMs, playWhenReady); - } + currentPlayer.setMediaItems(mediaQueue, windowIndex, playbackPositionMs); + currentPlayer.setPlayWhenReady(playWhenReady); + currentPlayer.prepare(); } /** - * Starts playback of the item at the given position. + * Starts playback of the item at the given index. * * @param itemIndex The index of the item to play. - * @param positionMs The position at which playback should start. - * @param playWhenReady Whether the player should proceed when ready to do so. */ - private void setCurrentItem(int itemIndex, long positionMs, boolean playWhenReady) { + private void setCurrentItem(int itemIndex) { maybeSetCurrentItemAndNotify(itemIndex); - if (currentPlayer == castPlayer && castPlayer.getCurrentTimeline().isEmpty()) { - MediaQueueItem[] items = new MediaQueueItem[mediaQueue.size()]; - for (int i = 0; i < items.length; i++) { - items[i] = mediaItemConverter.toMediaQueueItem(mediaQueue.get(i)); - } - castPlayer.loadItems(items, itemIndex, positionMs, Player.REPEAT_MODE_OFF); + if (currentPlayer.getCurrentTimeline().getWindowCount() != mediaQueue.size()) { + // This only happens with the cast player. The receiver app in the cast device clears the + // timeline when the last item of the timeline has been played to end. + currentPlayer.setMediaItems(mediaQueue, itemIndex, C.TIME_UNSET); } else { - currentPlayer.seekTo(itemIndex, positionMs); - currentPlayer.setPlayWhenReady(playWhenReady); + currentPlayer.seekTo(itemIndex, C.TIME_UNSET); } + currentPlayer.setPlayWhenReady(true); } private void maybeSetCurrentItemAndNotify(int currentItemIndex) { @@ -398,62 +350,4 @@ import java.util.Map; listener.onQueuePositionChanged(oldIndex, currentItemIndex); } } - - private MediaSource buildMediaSource(MediaItem item) { - Uri uri = item.uri; - String mimeType = item.mimeType; - if (mimeType == null) { - throw new IllegalArgumentException("mimeType is required"); - } - - DrmSessionManager drmSessionManager = - DrmSessionManager.getDummyDrmSessionManager(); - MediaItem.DrmConfiguration drmConfiguration = item.drmConfiguration; - if (drmConfiguration != null && Util.SDK_INT >= 18) { - String licenseServerUrl = - drmConfiguration.licenseUri != null ? drmConfiguration.licenseUri.toString() : ""; - HttpMediaDrmCallback drmCallback = - new HttpMediaDrmCallback(licenseServerUrl, DATA_SOURCE_FACTORY); - for (Map.Entry requestHeader : drmConfiguration.requestHeaders.entrySet()) { - drmCallback.setKeyRequestProperty(requestHeader.getKey(), requestHeader.getValue()); - } - drmSessionManager = - new DefaultDrmSessionManager.Builder() - .setMultiSession(/* multiSession= */ true) - .setUuidAndExoMediaDrmProvider( - drmConfiguration.uuid, FrameworkMediaDrm.DEFAULT_PROVIDER) - .build(drmCallback); - } - - MediaSource createdMediaSource; - switch (mimeType) { - case DemoUtil.MIME_TYPE_SS: - createdMediaSource = - new SsMediaSource.Factory(DATA_SOURCE_FACTORY) - .setDrmSessionManager(drmSessionManager) - .createMediaSource(uri); - break; - case DemoUtil.MIME_TYPE_DASH: - createdMediaSource = - new DashMediaSource.Factory(DATA_SOURCE_FACTORY) - .setDrmSessionManager(drmSessionManager) - .createMediaSource(uri); - break; - case DemoUtil.MIME_TYPE_HLS: - createdMediaSource = - new HlsMediaSource.Factory(DATA_SOURCE_FACTORY) - .setDrmSessionManager(drmSessionManager) - .createMediaSource(uri); - break; - case DemoUtil.MIME_TYPE_VIDEO_MP4: - createdMediaSource = - new ProgressiveMediaSource.Factory(DATA_SOURCE_FACTORY) - .setDrmSessionManager(drmSessionManager) - .createMediaSource(uri); - break; - default: - throw new IllegalArgumentException("mimeType is unsupported: " + mimeType); - } - return createdMediaSource; - } } diff --git a/demos/gl/README.md b/demos/gl/README.md new file mode 100644 index 0000000000..12dabe902b --- /dev/null +++ b/demos/gl/README.md @@ -0,0 +1,11 @@ +# ExoPlayer GL demo + +This app demonstrates how to render video to a [GLSurfaceView][] while applying +a GL shader. + +The shader shows an overlap bitmap on top of the video. The overlay bitmap is +drawn using an Android canvas, and includes the current frame's presentation +timestamp, to show how to get the timestamp of the frame currently in the +off-screen surface texture. + +[GLSurfaceView]: https://developer.android.com/reference/android/opengl/GLSurfaceView diff --git a/demos/gl/build.gradle b/demos/gl/build.gradle new file mode 100644 index 0000000000..8fe3e04045 --- /dev/null +++ b/demos/gl/build.gradle @@ -0,0 +1,53 @@ +// Copyright (C) 2020 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. +apply from: '../../constants.gradle' +apply plugin: 'com.android.application' + +android { + compileSdkVersion project.ext.compileSdkVersion + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + defaultConfig { + versionName project.ext.releaseVersion + versionCode project.ext.releaseVersionCode + minSdkVersion project.ext.minSdkVersion + targetSdkVersion project.ext.appTargetSdkVersion + } + + buildTypes { + release { + shrinkResources true + minifyEnabled true + proguardFiles getDefaultProguardFile('proguard-android.txt') + } + } + + lintOptions { + // This demo app does not have translations. + disable 'MissingTranslation' + } +} + +dependencies { + implementation project(modulePrefix + 'library-core') + implementation project(modulePrefix + 'library-ui') + implementation project(modulePrefix + 'library-dash') + implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion + compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion + compileOnly 'org.checkerframework:checker-compat-qual:' + checkerframeworkVersion +} diff --git a/demos/gl/src/main/AndroidManifest.xml b/demos/gl/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..4c95d1ec2f --- /dev/null +++ b/demos/gl/src/main/AndroidManifest.xml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/demos/gl/src/main/assets/bitmap_overlay_video_processor_fragment.glsl b/demos/gl/src/main/assets/bitmap_overlay_video_processor_fragment.glsl new file mode 100644 index 0000000000..e54d0c256d --- /dev/null +++ b/demos/gl/src/main/assets/bitmap_overlay_video_processor_fragment.glsl @@ -0,0 +1,35 @@ +// Copyright 2020 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. + +#extension GL_OES_EGL_image_external : require +precision mediump float; +// External texture containing video decoder output. +uniform samplerExternalOES tex_sampler_0; +// Texture containing the overlap bitmap. +uniform sampler2D tex_sampler_1; +// Horizontal scaling factor for the overlap bitmap. +uniform float scaleX; +// Vertical scaling factory for the overlap bitmap. +uniform float scaleY; +varying vec2 v_texcoord; +void main() { + vec4 videoColor = texture2D(tex_sampler_0, v_texcoord); + vec4 overlayColor = texture2D(tex_sampler_1, + vec2(v_texcoord.x * scaleX, + v_texcoord.y * scaleY)); + // Blend the video decoder output and the overlay bitmap. + gl_FragColor = videoColor * (1.0 - overlayColor.a) + + overlayColor * overlayColor.a; +} + diff --git a/demos/gl/src/main/assets/bitmap_overlay_video_processor_vertex.glsl b/demos/gl/src/main/assets/bitmap_overlay_video_processor_vertex.glsl new file mode 100644 index 0000000000..e333d977b2 --- /dev/null +++ b/demos/gl/src/main/assets/bitmap_overlay_video_processor_vertex.glsl @@ -0,0 +1,21 @@ +// Copyright 2020 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. +attribute vec4 a_position; +attribute vec3 a_texcoord; +varying vec2 v_texcoord; +void main() { + gl_Position = a_position; + v_texcoord = a_texcoord.xy; +} + diff --git a/demos/gl/src/main/java/com/google/android/exoplayer2/gldemo/BitmapOverlayVideoProcessor.java b/demos/gl/src/main/java/com/google/android/exoplayer2/gldemo/BitmapOverlayVideoProcessor.java new file mode 100644 index 0000000000..063b660751 --- /dev/null +++ b/demos/gl/src/main/java/com/google/android/exoplayer2/gldemo/BitmapOverlayVideoProcessor.java @@ -0,0 +1,176 @@ +/* + * Copyright (C) 2020 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.gldemo; + +import android.content.Context; +import android.content.pm.PackageManager; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.drawable.BitmapDrawable; +import android.opengl.GLES20; +import android.opengl.GLUtils; +import androidx.annotation.Nullable; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.GlUtil; +import com.google.android.exoplayer2.util.Util; +import java.io.IOException; +import java.io.InputStream; +import java.util.Locale; +import javax.microedition.khronos.opengles.GL10; + +/** + * Video processor that demonstrates how to overlay a bitmap on video output using a GL shader. The + * bitmap is drawn using an Android {@link Canvas}. + */ +/* package */ final class BitmapOverlayVideoProcessor + implements VideoProcessingGLSurfaceView.VideoProcessor { + + private static final int OVERLAY_WIDTH = 512; + private static final int OVERLAY_HEIGHT = 256; + + private final Context context; + private final Paint paint; + private final int[] textures; + private final Bitmap overlayBitmap; + private final Bitmap logoBitmap; + private final Canvas overlayCanvas; + + private int program; + @Nullable private GlUtil.Attribute[] attributes; + @Nullable private GlUtil.Uniform[] uniforms; + + private float bitmapScaleX; + private float bitmapScaleY; + + public BitmapOverlayVideoProcessor(Context context) { + this.context = context.getApplicationContext(); + paint = new Paint(); + paint.setTextSize(64); + paint.setAntiAlias(true); + paint.setARGB(0xFF, 0xFF, 0xFF, 0xFF); + textures = new int[1]; + overlayBitmap = Bitmap.createBitmap(OVERLAY_WIDTH, OVERLAY_HEIGHT, Bitmap.Config.ARGB_8888); + overlayCanvas = new Canvas(overlayBitmap); + try { + logoBitmap = + ((BitmapDrawable) + context.getPackageManager().getApplicationIcon(context.getPackageName())) + .getBitmap(); + } catch (PackageManager.NameNotFoundException e) { + throw new IllegalStateException(e); + } + } + + @Override + public void initialize() { + String vertexShaderCode = + loadAssetAsString(context, "bitmap_overlay_video_processor_vertex.glsl"); + String fragmentShaderCode = + loadAssetAsString(context, "bitmap_overlay_video_processor_fragment.glsl"); + program = GlUtil.compileProgram(vertexShaderCode, fragmentShaderCode); + GlUtil.Attribute[] attributes = GlUtil.getAttributes(program); + GlUtil.Uniform[] uniforms = GlUtil.getUniforms(program); + for (GlUtil.Attribute attribute : attributes) { + if (attribute.name.equals("a_position")) { + attribute.setBuffer( + new float[] { + -1.0f, -1.0f, 0.0f, 1.0f, 1.0f, -1.0f, 0.0f, 1.0f, -1.0f, 1.0f, 0.0f, 1.0f, 1.0f, + 1.0f, 0.0f, 1.0f, + }, + 4); + } else if (attribute.name.equals("a_texcoord")) { + attribute.setBuffer( + new float[] { + 0.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 1.0f, + }, + 3); + } + } + this.attributes = attributes; + this.uniforms = uniforms; + GLES20.glGenTextures(1, textures, 0); + GLES20.glBindTexture(GL10.GL_TEXTURE_2D, textures[0]); + GLES20.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MIN_FILTER, GL10.GL_NEAREST); + GLES20.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MAG_FILTER, GL10.GL_LINEAR); + GLES20.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_S, GL10.GL_REPEAT); + GLES20.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_T, GL10.GL_REPEAT); + GLUtils.texImage2D(GL10.GL_TEXTURE_2D, /* level= */ 0, overlayBitmap, /* border= */ 0); + } + + @Override + public void setSurfaceSize(int width, int height) { + bitmapScaleX = (float) width / OVERLAY_WIDTH; + bitmapScaleY = (float) height / OVERLAY_HEIGHT; + } + + @Override + public void draw(int frameTexture, long frameTimestampUs) { + // Draw to the canvas and store it in a texture. + String text = String.format(Locale.US, "%.02f", frameTimestampUs / (float) C.MICROS_PER_SECOND); + overlayBitmap.eraseColor(Color.TRANSPARENT); + overlayCanvas.drawBitmap(logoBitmap, /* left= */ 32, /* top= */ 32, paint); + overlayCanvas.drawText(text, /* x= */ 200, /* y= */ 130, paint); + GLES20.glBindTexture(GL10.GL_TEXTURE_2D, textures[0]); + GLUtils.texSubImage2D( + GL10.GL_TEXTURE_2D, /* level= */ 0, /* xoffset= */ 0, /* yoffset= */ 0, overlayBitmap); + GlUtil.checkGlError(); + + // Run the shader program. + GlUtil.Uniform[] uniforms = Assertions.checkNotNull(this.uniforms); + GlUtil.Attribute[] attributes = Assertions.checkNotNull(this.attributes); + GLES20.glUseProgram(program); + for (GlUtil.Uniform uniform : uniforms) { + switch (uniform.name) { + case "tex_sampler_0": + uniform.setSamplerTexId(frameTexture, /* unit= */ 0); + break; + case "tex_sampler_1": + uniform.setSamplerTexId(textures[0], /* unit= */ 1); + break; + case "scaleX": + uniform.setFloat(bitmapScaleX); + break; + case "scaleY": + uniform.setFloat(bitmapScaleY); + break; + } + } + for (GlUtil.Attribute copyExternalAttribute : attributes) { + copyExternalAttribute.bind(); + } + for (GlUtil.Uniform copyExternalUniform : uniforms) { + copyExternalUniform.bind(); + } + GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT); + GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, /* first= */ 0, /* count= */ 4); + GlUtil.checkGlError(); + } + + private static String loadAssetAsString(Context context, String assetFileName) { + @Nullable InputStream inputStream = null; + try { + inputStream = context.getAssets().open(assetFileName); + return Util.fromUtf8Bytes(Util.toByteArray(inputStream)); + } catch (IOException e) { + throw new IllegalStateException(e); + } finally { + Util.closeQuietly(inputStream); + } + } +} diff --git a/demos/gl/src/main/java/com/google/android/exoplayer2/gldemo/MainActivity.java b/demos/gl/src/main/java/com/google/android/exoplayer2/gldemo/MainActivity.java new file mode 100644 index 0000000000..c788f752f7 --- /dev/null +++ b/demos/gl/src/main/java/com/google/android/exoplayer2/gldemo/MainActivity.java @@ -0,0 +1,199 @@ +/* + * Copyright (C) 2020 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.gldemo; + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.widget.FrameLayout; +import android.widget.Toast; +import androidx.annotation.Nullable; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.SimpleExoPlayer; +import com.google.android.exoplayer2.drm.DefaultDrmSessionManager; +import com.google.android.exoplayer2.drm.DrmSessionManager; +import com.google.android.exoplayer2.drm.FrameworkMediaDrm; +import com.google.android.exoplayer2.drm.HttpMediaDrmCallback; +import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.source.ProgressiveMediaSource; +import com.google.android.exoplayer2.source.dash.DashMediaSource; +import com.google.android.exoplayer2.ui.PlayerView; +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.HttpDataSource; +import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.EventLogger; +import com.google.android.exoplayer2.util.GlUtil; +import com.google.android.exoplayer2.util.Util; +import java.util.UUID; + +/** + * Activity that demonstrates playback of video to an {@link android.opengl.GLSurfaceView} with + * postprocessing of the video content using GL. + */ +public final class MainActivity extends Activity { + + private static final String TAG = "MainActivity"; + + private static final String DEFAULT_MEDIA_URI = + "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv"; + + private static final String ACTION_VIEW = "com.google.android.exoplayer.gldemo.action.VIEW"; + private static final String EXTENSION_EXTRA = "extension"; + private static final String DRM_SCHEME_EXTRA = "drm_scheme"; + private static final String DRM_LICENSE_URL_EXTRA = "drm_license_url"; + + @Nullable private PlayerView playerView; + @Nullable private VideoProcessingGLSurfaceView videoProcessingGLSurfaceView; + + @Nullable private SimpleExoPlayer player; + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.main_activity); + playerView = findViewById(R.id.player_view); + + Context context = getApplicationContext(); + boolean requestSecureSurface = getIntent().hasExtra(DRM_SCHEME_EXTRA); + if (requestSecureSurface && !GlUtil.isProtectedContentExtensionSupported(context)) { + Toast.makeText( + context, R.string.error_protected_content_extension_not_supported, Toast.LENGTH_LONG) + .show(); + } + + VideoProcessingGLSurfaceView videoProcessingGLSurfaceView = + new VideoProcessingGLSurfaceView( + context, requestSecureSurface, new BitmapOverlayVideoProcessor(context)); + FrameLayout contentFrame = findViewById(R.id.exo_content_frame); + contentFrame.addView(videoProcessingGLSurfaceView); + this.videoProcessingGLSurfaceView = videoProcessingGLSurfaceView; + } + + @Override + public void onStart() { + super.onStart(); + if (Util.SDK_INT > 23) { + initializePlayer(); + if (playerView != null) { + playerView.onResume(); + } + } + } + + @Override + public void onResume() { + super.onResume(); + if (Util.SDK_INT <= 23 || player == null) { + initializePlayer(); + if (playerView != null) { + playerView.onResume(); + } + } + } + + @Override + public void onPause() { + super.onPause(); + if (Util.SDK_INT <= 23) { + if (playerView != null) { + playerView.onPause(); + } + releasePlayer(); + } + } + + @Override + public void onStop() { + super.onStop(); + if (Util.SDK_INT > 23) { + if (playerView != null) { + playerView.onPause(); + } + releasePlayer(); + } + } + + private void initializePlayer() { + Intent intent = getIntent(); + String action = intent.getAction(); + Uri uri = + ACTION_VIEW.equals(action) + ? Assertions.checkNotNull(intent.getData()) + : Uri.parse(DEFAULT_MEDIA_URI); + String userAgent = Util.getUserAgent(this, getString(R.string.application_name)); + DrmSessionManager drmSessionManager; + if (Util.SDK_INT >= 18 && intent.hasExtra(DRM_SCHEME_EXTRA)) { + String drmScheme = Assertions.checkNotNull(intent.getStringExtra(DRM_SCHEME_EXTRA)); + String drmLicenseUrl = Assertions.checkNotNull(intent.getStringExtra(DRM_LICENSE_URL_EXTRA)); + UUID drmSchemeUuid = Assertions.checkNotNull(Util.getDrmUuid(drmScheme)); + HttpDataSource.Factory licenseDataSourceFactory = new DefaultHttpDataSourceFactory(userAgent); + HttpMediaDrmCallback drmCallback = + new HttpMediaDrmCallback(drmLicenseUrl, licenseDataSourceFactory); + drmSessionManager = + new DefaultDrmSessionManager.Builder() + .setUuidAndExoMediaDrmProvider(drmSchemeUuid, FrameworkMediaDrm.DEFAULT_PROVIDER) + .build(drmCallback); + } else { + drmSessionManager = DrmSessionManager.getDummyDrmSessionManager(); + } + + DataSource.Factory dataSourceFactory = + new DefaultDataSourceFactory( + this, Util.getUserAgent(this, getString(R.string.application_name))); + MediaSource mediaSource; + @C.ContentType int type = Util.inferContentType(uri, intent.getStringExtra(EXTENSION_EXTRA)); + if (type == C.TYPE_DASH) { + mediaSource = + new DashMediaSource.Factory(dataSourceFactory) + .setDrmSessionManager(drmSessionManager) + .createMediaSource(uri); + } else if (type == C.TYPE_OTHER) { + mediaSource = + new ProgressiveMediaSource.Factory(dataSourceFactory) + .setDrmSessionManager(drmSessionManager) + .createMediaSource(uri); + } else { + throw new IllegalStateException(); + } + + SimpleExoPlayer player = new SimpleExoPlayer.Builder(getApplicationContext()).build(); + player.setRepeatMode(Player.REPEAT_MODE_ALL); + player.setMediaSource(mediaSource); + player.prepare(); + player.play(); + VideoProcessingGLSurfaceView videoProcessingGLSurfaceView = + Assertions.checkNotNull(this.videoProcessingGLSurfaceView); + videoProcessingGLSurfaceView.setVideoComponent( + Assertions.checkNotNull(player.getVideoComponent())); + Assertions.checkNotNull(playerView).setPlayer(player); + player.addAnalyticsListener(new EventLogger(/* trackSelector= */ null)); + this.player = player; + } + + private void releasePlayer() { + Assertions.checkNotNull(playerView).setPlayer(null); + if (player != null) { + player.release(); + Assertions.checkNotNull(videoProcessingGLSurfaceView).setVideoComponent(null); + player = null; + } + } +} diff --git a/demos/gl/src/main/java/com/google/android/exoplayer2/gldemo/VideoProcessingGLSurfaceView.java b/demos/gl/src/main/java/com/google/android/exoplayer2/gldemo/VideoProcessingGLSurfaceView.java new file mode 100644 index 0000000000..7aee74801f --- /dev/null +++ b/demos/gl/src/main/java/com/google/android/exoplayer2/gldemo/VideoProcessingGLSurfaceView.java @@ -0,0 +1,292 @@ +/* + * Copyright (C) 2020 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.gldemo; + +import android.content.Context; +import android.graphics.SurfaceTexture; +import android.media.MediaFormat; +import android.opengl.EGL14; +import android.opengl.GLES20; +import android.opengl.GLSurfaceView; +import android.os.Handler; +import android.view.Surface; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.GlUtil; +import com.google.android.exoplayer2.util.TimedValueQueue; +import com.google.android.exoplayer2.video.VideoFrameMetadataListener; +import java.util.concurrent.atomic.AtomicBoolean; +import javax.microedition.khronos.egl.EGL10; +import javax.microedition.khronos.egl.EGLConfig; +import javax.microedition.khronos.egl.EGLContext; +import javax.microedition.khronos.egl.EGLDisplay; +import javax.microedition.khronos.egl.EGLSurface; +import javax.microedition.khronos.opengles.GL10; + +/** + * {@link GLSurfaceView} that creates a GL context (optionally for protected content) and passes + * video frames to a {@link VideoProcessor} for drawing to the view. + * + *

This view must be created programmatically, as it is necessary to specify whether a context + * supporting protected content should be created at construction time. + */ +public final class VideoProcessingGLSurfaceView extends GLSurfaceView { + + /** Processes video frames, provided via a GL texture. */ + public interface VideoProcessor { + /** Performs any required GL initialization. */ + void initialize(); + + /** Sets the size of the output surface in pixels. */ + void setSurfaceSize(int width, int height); + + /** + * Draws using GL operations. + * + * @param frameTexture The ID of a GL texture containing a video frame. + * @param frameTimestampUs The presentation timestamp of the frame, in microseconds. + */ + void draw(int frameTexture, long frameTimestampUs); + } + + private static final int EGL_PROTECTED_CONTENT_EXT = 0x32C0; + + private final VideoRenderer renderer; + private final Handler mainHandler; + + @Nullable private SurfaceTexture surfaceTexture; + @Nullable private Surface surface; + @Nullable private Player.VideoComponent videoComponent; + + /** + * Creates a new instance. Pass {@code true} for {@code requireSecureContext} if the {@link + * GLSurfaceView GLSurfaceView's} associated GL context should handle secure content (if the + * device supports it). + * + * @param context The {@link Context}. + * @param requireSecureContext Whether a GL context supporting protected content should be + * created, if supported by the device. + * @param videoProcessor Processor that draws to the view. + */ + @SuppressWarnings("InlinedApi") + public VideoProcessingGLSurfaceView( + Context context, boolean requireSecureContext, VideoProcessor videoProcessor) { + super(context); + renderer = new VideoRenderer(videoProcessor); + mainHandler = new Handler(); + setEGLContextClientVersion(2); + setEGLConfigChooser( + /* redSize= */ 8, + /* greenSize= */ 8, + /* blueSize= */ 8, + /* alphaSize= */ 8, + /* depthSize= */ 0, + /* stencilSize= */ 0); + setEGLContextFactory( + new EGLContextFactory() { + @Override + public EGLContext createContext(EGL10 egl, EGLDisplay display, EGLConfig eglConfig) { + int[] glAttributes; + if (requireSecureContext) { + glAttributes = + new int[] { + EGL14.EGL_CONTEXT_CLIENT_VERSION, + 2, + EGL_PROTECTED_CONTENT_EXT, + EGL14.EGL_TRUE, + EGL14.EGL_NONE + }; + } else { + glAttributes = new int[] {EGL14.EGL_CONTEXT_CLIENT_VERSION, 2, EGL14.EGL_NONE}; + } + return egl.eglCreateContext( + display, eglConfig, /* share_context= */ EGL10.EGL_NO_CONTEXT, glAttributes); + } + + @Override + public void destroyContext(EGL10 egl, EGLDisplay display, EGLContext context) { + egl.eglDestroyContext(display, context); + } + }); + setEGLWindowSurfaceFactory( + new EGLWindowSurfaceFactory() { + @Override + public EGLSurface createWindowSurface( + EGL10 egl, EGLDisplay display, EGLConfig config, Object nativeWindow) { + int[] attribsList = + requireSecureContext + ? new int[] {EGL_PROTECTED_CONTENT_EXT, EGL14.EGL_TRUE, EGL10.EGL_NONE} + : new int[] {EGL10.EGL_NONE}; + return egl.eglCreateWindowSurface(display, config, nativeWindow, attribsList); + } + + @Override + public void destroySurface(EGL10 egl, EGLDisplay display, EGLSurface surface) { + egl.eglDestroySurface(display, surface); + } + }); + setRenderer(renderer); + setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY); + } + + /** + * Attaches or detaches (if {@code newVideoComponent} is {@code null}) this view from the video + * component of the player. + * + * @param newVideoComponent The new video component, or {@code null} to detach this view. + */ + public void setVideoComponent(@Nullable Player.VideoComponent newVideoComponent) { + if (newVideoComponent == videoComponent) { + return; + } + if (videoComponent != null) { + if (surface != null) { + videoComponent.clearVideoSurface(surface); + } + videoComponent.clearVideoFrameMetadataListener(renderer); + } + videoComponent = newVideoComponent; + if (videoComponent != null) { + videoComponent.setVideoFrameMetadataListener(renderer); + videoComponent.setVideoSurface(surface); + } + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + // Post to make sure we occur in order with any onSurfaceTextureAvailable calls. + mainHandler.post( + () -> { + if (surface != null) { + if (videoComponent != null) { + videoComponent.setVideoSurface(null); + } + releaseSurface(surfaceTexture, surface); + surfaceTexture = null; + surface = null; + } + }); + } + + private void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture) { + mainHandler.post( + () -> { + SurfaceTexture oldSurfaceTexture = this.surfaceTexture; + Surface oldSurface = VideoProcessingGLSurfaceView.this.surface; + this.surfaceTexture = surfaceTexture; + this.surface = new Surface(surfaceTexture); + releaseSurface(oldSurfaceTexture, oldSurface); + if (videoComponent != null) { + videoComponent.setVideoSurface(surface); + } + }); + } + + private static void releaseSurface( + @Nullable SurfaceTexture oldSurfaceTexture, @Nullable Surface oldSurface) { + if (oldSurfaceTexture != null) { + oldSurfaceTexture.release(); + } + if (oldSurface != null) { + oldSurface.release(); + } + } + + private final class VideoRenderer implements GLSurfaceView.Renderer, VideoFrameMetadataListener { + + private final VideoProcessor videoProcessor; + private final AtomicBoolean frameAvailable; + private final TimedValueQueue sampleTimestampQueue; + + private int texture; + @Nullable private SurfaceTexture surfaceTexture; + + private boolean initialized; + private int width; + private int height; + private long frameTimestampUs; + + public VideoRenderer(VideoProcessor videoProcessor) { + this.videoProcessor = videoProcessor; + frameAvailable = new AtomicBoolean(); + sampleTimestampQueue = new TimedValueQueue<>(); + width = -1; + height = -1; + } + + @Override + public synchronized void onSurfaceCreated(GL10 gl, EGLConfig config) { + texture = GlUtil.createExternalTexture(); + surfaceTexture = new SurfaceTexture(texture); + surfaceTexture.setOnFrameAvailableListener( + surfaceTexture -> { + frameAvailable.set(true); + requestRender(); + }); + onSurfaceTextureAvailable(surfaceTexture); + } + + @Override + public void onSurfaceChanged(GL10 gl, int width, int height) { + GLES20.glViewport(0, 0, width, height); + this.width = width; + this.height = height; + } + + @Override + public void onDrawFrame(GL10 gl) { + if (videoProcessor == null) { + return; + } + + if (!initialized) { + videoProcessor.initialize(); + initialized = true; + } + + if (width != -1 && height != -1) { + videoProcessor.setSurfaceSize(width, height); + width = -1; + height = -1; + } + + if (frameAvailable.compareAndSet(true, false)) { + SurfaceTexture surfaceTexture = Assertions.checkNotNull(this.surfaceTexture); + surfaceTexture.updateTexImage(); + long lastFrameTimestampNs = surfaceTexture.getTimestamp(); + Long frameTimestampUs = sampleTimestampQueue.poll(lastFrameTimestampNs); + if (frameTimestampUs != null) { + this.frameTimestampUs = frameTimestampUs; + } + } + + videoProcessor.draw(texture, frameTimestampUs); + } + + @Override + public void onVideoFrameAboutToBeRendered( + long presentationTimeUs, + long releaseTimeNs, + @NonNull Format format, + @Nullable MediaFormat mediaFormat) { + sampleTimestampQueue.add(releaseTimeNs, presentationTimeUs); + } + } +} diff --git a/demos/gl/src/main/res/layout/main_activity.xml b/demos/gl/src/main/res/layout/main_activity.xml new file mode 100644 index 0000000000..ec3868d6a8 --- /dev/null +++ b/demos/gl/src/main/res/layout/main_activity.xml @@ -0,0 +1,30 @@ + + + + + + + + diff --git a/demos/gl/src/main/res/mipmap-hdpi/ic_launcher.png b/demos/gl/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000000..adaa93220e Binary files /dev/null and b/demos/gl/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/demos/gl/src/main/res/mipmap-mdpi/ic_launcher.png b/demos/gl/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000000..9b6f7d5e80 Binary files /dev/null and b/demos/gl/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/demos/gl/src/main/res/mipmap-xhdpi/ic_launcher.png b/demos/gl/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000000..2101026c9f Binary files /dev/null and b/demos/gl/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/demos/gl/src/main/res/mipmap-xxhdpi/ic_launcher.png b/demos/gl/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000000..223ec8bd11 Binary files /dev/null and b/demos/gl/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/demos/gl/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/demos/gl/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000000..698ed68c42 Binary files /dev/null and b/demos/gl/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/demos/gl/src/main/res/values/strings.xml b/demos/gl/src/main/res/values/strings.xml new file mode 100644 index 0000000000..7e9e5d9961 --- /dev/null +++ b/demos/gl/src/main/res/values/strings.xml @@ -0,0 +1,22 @@ + + + + + ExoPlayer GL demo + + The GL protected content extension is not supported. + + diff --git a/demos/main/build.gradle b/demos/main/build.gradle index ab47b6de81..b7a8666fe3 100644 --- a/demos/main/build.gradle +++ b/demos/main/build.gradle @@ -64,7 +64,7 @@ android { dependencies { implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion implementation 'androidx.appcompat:appcompat:' + androidxAppCompatVersion - implementation 'com.google.android.material:material:1.0.0' + implementation 'com.google.android.material:material:1.1.0' implementation project(modulePrefix + 'library-core') implementation project(modulePrefix + 'library-dash') implementation project(modulePrefix + 'library-hls') diff --git a/demos/main/src/main/assets/media.exolist.json b/demos/main/src/main/assets/media.exolist.json index 4375bdf3a7..b9f3d63694 100644 --- a/demos/main/src/main/assets/media.exolist.json +++ b/demos/main/src/main/assets/media.exolist.json @@ -208,6 +208,13 @@ "uri": "https://storage.googleapis.com/wvmedia/cbcs/h264/tears/tears_aes_cbcs_uhd.mpd", "drm_scheme": "widevine", "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test" + }, + { + "name": "WV: Secure and Clear SD & HD (cenc,MP4,H264)", + "uri": "https://storage.googleapis.com/exoplayer-test-media-1/widevine/tears_enc_clear_enc.mpd", + "drm_scheme": "widevine", + "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test", + "drm_session_for_clear_types": ["audio", "video"] } ] }, @@ -609,5 +616,30 @@ "subtitle_language": "en" } ] + }, + { + "name": "60fps", + "samples": [ + { + "name": "Big Buck Bunny (DASH,H264,1080p,Clear)", + "uri": "https://storage.googleapis.com/exoplayer-test-media-1/60fps/bbb-clear-1080/manifest.mpd" + }, + { + "name": "Big Buck Bunny (DASH,H264,4K,Clear)", + "uri": "https://storage.googleapis.com/exoplayer-test-media-1/60fps/bbb-clear-2160/manifest.mpd" + }, + { + "name": "Big Buck Bunny (DASH,H264,1080p,Widevine)", + "uri": "https://storage.googleapis.com/exoplayer-test-media-1/60fps/bbb-wv-1080/manifest.mpd", + "drm_scheme": "widevine", + "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test" + }, + { + "name": "Big Buck Bunny (DASH,H264,4K,Widevine)", + "uri": "https://storage.googleapis.com/exoplayer-test-media-1/60fps/bbb-wv-2160/manifest.mpd", + "drm_scheme": "widevine", + "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test" + } + ] } ] 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 d83d7076c5..c36d370992 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 @@ -22,17 +22,14 @@ import com.google.android.exoplayer2.database.DatabaseProvider; import com.google.android.exoplayer2.database.ExoDatabaseProvider; import com.google.android.exoplayer2.offline.ActionFileUpgradeUtil; import com.google.android.exoplayer2.offline.DefaultDownloadIndex; -import com.google.android.exoplayer2.offline.DefaultDownloaderFactory; import com.google.android.exoplayer2.offline.DownloadManager; -import com.google.android.exoplayer2.offline.DownloaderConstructorHelper; +import com.google.android.exoplayer2.ui.DownloadNotificationHelper; 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.FileDataSource; import com.google.android.exoplayer2.upstream.HttpDataSource; import com.google.android.exoplayer2.upstream.cache.Cache; import com.google.android.exoplayer2.upstream.cache.CacheDataSource; -import com.google.android.exoplayer2.upstream.cache.CacheDataSourceFactory; import com.google.android.exoplayer2.upstream.cache.NoOpCacheEvictor; import com.google.android.exoplayer2.upstream.cache.SimpleCache; import com.google.android.exoplayer2.util.Log; @@ -45,6 +42,8 @@ import java.io.IOException; */ public class DemoApplication extends Application { + public static final String DOWNLOAD_NOTIFICATION_CHANNEL_ID = "download_channel"; + private static final String TAG = "DemoApplication"; private static final String DOWNLOAD_ACTION_FILE = "actions"; private static final String DOWNLOAD_TRACKER_ACTION_FILE = "tracked_actions"; @@ -57,6 +56,7 @@ public class DemoApplication extends Application { private Cache downloadCache; private DownloadManager downloadManager; private DownloadTracker downloadTracker; + private DownloadNotificationHelper downloadNotificationHelper; @Override public void onCreate() { @@ -93,6 +93,14 @@ public class DemoApplication extends Application { .setExtensionRendererMode(extensionRendererMode); } + public DownloadNotificationHelper getDownloadNotificationHelper() { + if (downloadNotificationHelper == null) { + downloadNotificationHelper = + new DownloadNotificationHelper(this, DOWNLOAD_NOTIFICATION_CHANNEL_ID); + } + return downloadNotificationHelper; + } + public DownloadManager getDownloadManager() { initDownloadManager(); return downloadManager; @@ -119,11 +127,9 @@ public class DemoApplication extends Application { DOWNLOAD_ACTION_FILE, downloadIndex, /* addNewDownloadsAsCompleted= */ false); upgradeActionFile( DOWNLOAD_TRACKER_ACTION_FILE, downloadIndex, /* addNewDownloadsAsCompleted= */ true); - DownloaderConstructorHelper downloaderConstructorHelper = - new DownloaderConstructorHelper(getDownloadCache(), buildHttpDataSourceFactory()); downloadManager = new DownloadManager( - this, downloadIndex, new DefaultDownloaderFactory(downloaderConstructorHelper)); + this, getDatabaseProvider(), getDownloadCache(), buildHttpDataSourceFactory()); downloadTracker = new DownloadTracker(/* context= */ this, buildDataSourceFactory(), downloadManager); } @@ -160,14 +166,12 @@ public class DemoApplication extends Application { return downloadDirectory; } - protected static CacheDataSourceFactory buildReadOnlyCacheDataSource( + protected static CacheDataSource.Factory buildReadOnlyCacheDataSource( DataSource.Factory upstreamFactory, Cache cache) { - return new CacheDataSourceFactory( - cache, - upstreamFactory, - new FileDataSource.Factory(), - /* cacheWriteDataSinkFactory= */ null, - CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR, - /* eventListener= */ null); + return new CacheDataSource.Factory() + .setCache(cache) + .setUpstreamDataSourceFactory(upstreamFactory) + .setCacheWriteDataSinkFactory(null) + .setFlags(CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR); } } diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoDownloadService.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoDownloadService.java index c3909dfe46..71b1eda7bf 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoDownloadService.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoDownloadService.java @@ -15,7 +15,11 @@ */ package com.google.android.exoplayer2.demo; +import static com.google.android.exoplayer2.demo.DemoApplication.DOWNLOAD_NOTIFICATION_CHANNEL_ID; + import android.app.Notification; +import android.content.Context; +import androidx.annotation.NonNull; import com.google.android.exoplayer2.offline.Download; import com.google.android.exoplayer2.offline.DownloadManager; import com.google.android.exoplayer2.offline.DownloadService; @@ -28,33 +32,31 @@ import java.util.List; /** A service for downloading media. */ public class DemoDownloadService extends DownloadService { - private static final String CHANNEL_ID = "download_channel"; private static final int JOB_ID = 1; private static final int FOREGROUND_NOTIFICATION_ID = 1; - private static int nextNotificationId = FOREGROUND_NOTIFICATION_ID + 1; - - private DownloadNotificationHelper notificationHelper; - public DemoDownloadService() { super( FOREGROUND_NOTIFICATION_ID, DEFAULT_FOREGROUND_NOTIFICATION_UPDATE_INTERVAL, - CHANNEL_ID, + DOWNLOAD_NOTIFICATION_CHANNEL_ID, R.string.exo_download_notification_channel_name, /* channelDescriptionResourceId= */ 0); - nextNotificationId = FOREGROUND_NOTIFICATION_ID + 1; - } - - @Override - public void onCreate() { - super.onCreate(); - notificationHelper = new DownloadNotificationHelper(this, CHANNEL_ID); } @Override + @NonNull protected DownloadManager getDownloadManager() { - return ((DemoApplication) getApplication()).getDownloadManager(); + // This will only happen once, because getDownloadManager is guaranteed to be called only once + // in the life cycle of the process. + DemoApplication application = (DemoApplication) getApplication(); + DownloadManager downloadManager = application.getDownloadManager(); + DownloadNotificationHelper downloadNotificationHelper = + application.getDownloadNotificationHelper(); + downloadManager.addListener( + new TerminalStateNotificationHelper( + this, downloadNotificationHelper, FOREGROUND_NOTIFICATION_ID + 1)); + return downloadManager; } @Override @@ -63,29 +65,53 @@ public class DemoDownloadService extends DownloadService { } @Override - protected Notification getForegroundNotification(List downloads) { - return notificationHelper.buildProgressNotification( - R.drawable.ic_download, /* contentIntent= */ null, /* message= */ null, downloads); + @NonNull + protected Notification getForegroundNotification(@NonNull List downloads) { + return ((DemoApplication) getApplication()) + .getDownloadNotificationHelper() + .buildProgressNotification( + R.drawable.ic_download, /* contentIntent= */ null, /* message= */ null, downloads); } - @Override - protected void onDownloadChanged(Download download) { - Notification notification; - if (download.state == Download.STATE_COMPLETED) { - notification = - notificationHelper.buildDownloadCompletedNotification( - R.drawable.ic_download_done, - /* contentIntent= */ null, - Util.fromUtf8Bytes(download.request.data)); - } else if (download.state == Download.STATE_FAILED) { - notification = - notificationHelper.buildDownloadFailedNotification( - R.drawable.ic_download_done, - /* contentIntent= */ null, - Util.fromUtf8Bytes(download.request.data)); - } else { - return; + /** + * Creates and displays notifications for downloads when they complete or fail. + * + *

This helper will outlive the lifespan of a single instance of {@link DemoDownloadService}. + * It is static to avoid leaking the first {@link DemoDownloadService} instance. + */ + private static final class TerminalStateNotificationHelper implements DownloadManager.Listener { + + private final Context context; + private final DownloadNotificationHelper notificationHelper; + + private int nextNotificationId; + + public TerminalStateNotificationHelper( + Context context, DownloadNotificationHelper notificationHelper, int firstNotificationId) { + this.context = context.getApplicationContext(); + this.notificationHelper = notificationHelper; + nextNotificationId = firstNotificationId; + } + + @Override + public void onDownloadChanged(@NonNull DownloadManager manager, @NonNull Download download) { + Notification notification; + if (download.state == Download.STATE_COMPLETED) { + notification = + notificationHelper.buildDownloadCompletedNotification( + R.drawable.ic_download_done, + /* contentIntent= */ null, + Util.fromUtf8Bytes(download.request.data)); + } else if (download.state == Download.STATE_FAILED) { + notification = + notificationHelper.buildDownloadFailedNotification( + R.drawable.ic_download_done, + /* contentIntent= */ null, + Util.fromUtf8Bytes(download.request.data)); + } else { + return; + } + NotificationUtil.setNotification(context, nextNotificationId++, notification); } - NotificationUtil.setNotification(this, nextNotificationId++, notification); } } 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 143eda93df..3127ed95e9 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 @@ -15,13 +15,17 @@ */ package com.google.android.exoplayer2.demo; +import static com.google.android.exoplayer2.util.Assertions.checkNotNull; + import android.content.Context; import android.content.DialogInterface; import android.net.Uri; import android.widget.Toast; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.fragment.app.FragmentManager; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.MediaItem; import com.google.android.exoplayer2.RenderersFactory; import com.google.android.exoplayer2.offline.Download; import com.google.android.exoplayer2.offline.DownloadCursor; @@ -80,8 +84,8 @@ public class DownloadTracker { listeners.remove(listener); } - public boolean isDownloaded(Uri uri) { - Download download = downloads.get(uri); + public boolean isDownloaded(MediaItem mediaItem) { + Download download = downloads.get(checkNotNull(mediaItem.playbackProperties).uri); return download != null && download.state != Download.STATE_FAILED; } @@ -91,12 +95,8 @@ public class DownloadTracker { } public void toggleDownload( - FragmentManager fragmentManager, - String name, - Uri uri, - String extension, - RenderersFactory renderersFactory) { - Download download = downloads.get(uri); + FragmentManager fragmentManager, MediaItem mediaItem, RenderersFactory renderersFactory) { + Download download = downloads.get(checkNotNull(mediaItem.playbackProperties).uri); if (download != null) { DownloadService.sendRemoveDownload( context, DemoDownloadService.class, download.request.id, /* foreground= */ false); @@ -106,7 +106,7 @@ public class DownloadTracker { } startDownloadDialogHelper = new StartDownloadDialogHelper( - fragmentManager, getDownloadHelper(uri, extension, renderersFactory), name); + fragmentManager, getDownloadHelper(mediaItem, renderersFactory), mediaItem); } } @@ -121,18 +121,23 @@ public class DownloadTracker { } } - private DownloadHelper getDownloadHelper( - Uri uri, String extension, RenderersFactory renderersFactory) { - int type = Util.inferContentType(uri, extension); + private DownloadHelper getDownloadHelper(MediaItem mediaItem, RenderersFactory renderersFactory) { + MediaItem.PlaybackProperties playbackProperties = checkNotNull(mediaItem.playbackProperties); + @C.ContentType + int type = + Util.inferContentTypeWithMimeType(playbackProperties.uri, playbackProperties.mimeType); switch (type) { case C.TYPE_DASH: - return DownloadHelper.forDash(context, uri, dataSourceFactory, renderersFactory); + return DownloadHelper.forDash( + context, playbackProperties.uri, dataSourceFactory, renderersFactory); case C.TYPE_SS: - return DownloadHelper.forSmoothStreaming(context, uri, dataSourceFactory, renderersFactory); + return DownloadHelper.forSmoothStreaming( + context, playbackProperties.uri, dataSourceFactory, renderersFactory); case C.TYPE_HLS: - return DownloadHelper.forHls(context, uri, dataSourceFactory, renderersFactory); + return DownloadHelper.forHls( + context, playbackProperties.uri, dataSourceFactory, renderersFactory); case C.TYPE_OTHER: - return DownloadHelper.forProgressive(context, uri); + return DownloadHelper.forProgressive(context, playbackProperties.uri); default: throw new IllegalStateException("Unsupported type: " + type); } @@ -141,7 +146,8 @@ public class DownloadTracker { private class DownloadManagerListener implements DownloadManager.Listener { @Override - public void onDownloadChanged(DownloadManager downloadManager, Download download) { + public void onDownloadChanged( + @NonNull DownloadManager downloadManager, @NonNull Download download) { downloads.put(download.request.uri, download); for (Listener listener : listeners) { listener.onDownloadsChanged(); @@ -149,7 +155,8 @@ public class DownloadTracker { } @Override - public void onDownloadRemoved(DownloadManager downloadManager, Download download) { + public void onDownloadRemoved( + @NonNull DownloadManager downloadManager, @NonNull Download download) { downloads.remove(download.request.uri); for (Listener listener : listeners) { listener.onDownloadsChanged(); @@ -164,16 +171,16 @@ public class DownloadTracker { private final FragmentManager fragmentManager; private final DownloadHelper downloadHelper; - private final String name; + private final MediaItem mediaItem; private TrackSelectionDialog trackSelectionDialog; private MappedTrackInfo mappedTrackInfo; public StartDownloadDialogHelper( - FragmentManager fragmentManager, DownloadHelper downloadHelper, String name) { + FragmentManager fragmentManager, DownloadHelper downloadHelper, MediaItem mediaItem) { this.fragmentManager = fragmentManager; this.downloadHelper = downloadHelper; - this.name = name; + this.mediaItem = mediaItem; downloadHelper.prepare(this); } @@ -187,7 +194,7 @@ public class DownloadTracker { // DownloadHelper.Callback implementation. @Override - public void onPrepared(DownloadHelper helper) { + public void onPrepared(@NonNull DownloadHelper helper) { if (helper.getPeriodCount() == 0) { Log.d(TAG, "No periods found. Downloading entire stream."); startDownload(); @@ -214,7 +221,7 @@ public class DownloadTracker { } @Override - public void onPrepareError(DownloadHelper helper, IOException e) { + public void onPrepareError(@NonNull DownloadHelper helper, @NonNull IOException e) { Toast.makeText(context, R.string.download_start_error, Toast.LENGTH_LONG).show(); Log.e( TAG, @@ -268,7 +275,8 @@ public class DownloadTracker { } private DownloadRequest buildDownloadRequest() { - return downloadHelper.getDownloadRequest(Util.getUtf8Bytes(name)); + return downloadHelper.getDownloadRequest( + Util.getUtf8Bytes(checkNotNull(mediaItem.mediaMetadata.title))); } } } diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/IntentUtil.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/IntentUtil.java new file mode 100644 index 0000000000..c043fa9f5d --- /dev/null +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/IntentUtil.java @@ -0,0 +1,294 @@ +/* + * Copyright 2020 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.demo; + +import static com.google.android.exoplayer2.util.Assertions.checkNotNull; +import static com.google.android.exoplayer2.util.Assertions.checkState; + +import android.content.Intent; +import android.net.Uri; +import androidx.annotation.Nullable; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.MediaItem; +import com.google.android.exoplayer2.offline.DownloadRequest; +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.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; + +/** Util to read from and populate an intent. */ +public class IntentUtil { + + /** A tag to hold custom playback configuration attributes. */ + public static class Tag { + + /** Whether the stream is a live stream. */ + public final boolean isLive; + /** The spherical stereo mode or null. */ + @Nullable public final String sphericalStereoMode; + + /** Creates an instance. */ + public Tag(boolean isLive, @Nullable String sphericalStereoMode) { + this.isLive = isLive; + this.sphericalStereoMode = sphericalStereoMode; + } + } + + // Actions. + + public static final String ACTION_VIEW = "com.google.android.exoplayer.demo.action.VIEW"; + public static final String ACTION_VIEW_LIST = + "com.google.android.exoplayer.demo.action.VIEW_LIST"; + + // Activity extras. + + 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"; + + // Player configuration extras. + + public static final String ABR_ALGORITHM_EXTRA = "abr_algorithm"; + public static final String ABR_ALGORITHM_DEFAULT = "default"; + public static final String ABR_ALGORITHM_RANDOM = "random"; + + // Media item configuration extras. + + public static final String URI_EXTRA = "uri"; + public static final String IS_LIVE_EXTRA = "is_live"; + public static final String MIME_TYPE_EXTRA = "mime_type"; + // For backwards compatibility only. + public static final String EXTENSION_EXTRA = "extension"; + + public static final String DRM_SCHEME_EXTRA = "drm_scheme"; + public static final String DRM_LICENSE_URL_EXTRA = "drm_license_url"; + public static final String DRM_KEY_REQUEST_PROPERTIES_EXTRA = "drm_key_request_properties"; + public static final String DRM_SESSION_FOR_CLEAR_TYPES_EXTRA = "drm_session_for_clear_types"; + public static final String DRM_MULTI_SESSION_EXTRA = "drm_multi_session"; + public static final String AD_TAG_URI_EXTRA = "ad_tag_uri"; + public static final String SUBTITLE_URI_EXTRA = "subtitle_uri"; + public static final String SUBTITLE_MIME_TYPE_EXTRA = "subtitle_mime_type"; + public static final String SUBTITLE_LANGUAGE_EXTRA = "subtitle_language"; + // For backwards compatibility only. + public static final String DRM_SCHEME_UUID_EXTRA = "drm_scheme_uuid"; + + public static final String PREFER_EXTENSION_DECODERS_EXTRA = "prefer_extension_decoders"; + public static final String TUNNELING_EXTRA = "tunneling"; + + /** Creates a list of {@link MediaItem media items} from an {@link Intent}. */ + public static List createMediaItemsFromIntent( + Intent intent, DownloadTracker downloadTracker) { + List mediaItems = new ArrayList<>(); + if (ACTION_VIEW_LIST.equals(intent.getAction())) { + int index = 0; + while (intent.hasExtra(URI_EXTRA + "_" + index)) { + Uri uri = Uri.parse(intent.getStringExtra(URI_EXTRA + "_" + index)); + mediaItems.add( + createMediaItemFromIntent( + uri, + intent, + /* extrasKeySuffix= */ "_" + index, + downloadTracker.getDownloadRequest(uri))); + index++; + } + } else { + Uri uri = intent.getData(); + mediaItems.add( + createMediaItemFromIntent( + uri, intent, /* extrasKeySuffix= */ "", downloadTracker.getDownloadRequest(uri))); + } + return mediaItems; + } + + /** Populates the intent with the given list of {@link MediaItem media items}. */ + public static void addToIntent(List mediaItems, Intent intent) { + Assertions.checkArgument(!mediaItems.isEmpty()); + if (mediaItems.size() == 1) { + MediaItem.PlaybackProperties playbackProperties = + checkNotNull(mediaItems.get(0).playbackProperties); + intent.setAction(IntentUtil.ACTION_VIEW).setData(playbackProperties.uri); + addPlaybackPropertiesToIntent(playbackProperties, intent, /* extrasKeySuffix= */ ""); + } else { + intent.setAction(IntentUtil.ACTION_VIEW_LIST); + for (int i = 0; i < mediaItems.size(); i++) { + MediaItem.PlaybackProperties playbackProperties = + checkNotNull(mediaItems.get(i).playbackProperties); + intent.putExtra(IntentUtil.URI_EXTRA + ("_" + i), playbackProperties.uri.toString()); + addPlaybackPropertiesToIntent(playbackProperties, intent, /* extrasKeySuffix= */ "_" + i); + } + } + } + + /** Makes a best guess to infer the MIME type from a {@link Uri} and an optional extension. */ + @Nullable + public static String inferAdaptiveStreamMimeType(Uri uri, @Nullable String extension) { + @C.ContentType int contentType = Util.inferContentType(uri, extension); + switch (contentType) { + case C.TYPE_DASH: + return MimeTypes.APPLICATION_MPD; + case C.TYPE_HLS: + return MimeTypes.APPLICATION_M3U8; + case C.TYPE_SS: + return MimeTypes.APPLICATION_SS; + case C.TYPE_OTHER: + default: + return null; + } + } + + private static MediaItem createMediaItemFromIntent( + Uri uri, Intent intent, String extrasKeySuffix, @Nullable DownloadRequest downloadRequest) { + String mimeType = intent.getStringExtra(MIME_TYPE_EXTRA + extrasKeySuffix); + if (mimeType == null) { + // Try to use extension for backwards compatibility. + String extension = intent.getStringExtra(EXTENSION_EXTRA + extrasKeySuffix); + mimeType = inferAdaptiveStreamMimeType(uri, extension); + } + MediaItem.Builder builder = + new MediaItem.Builder() + .setUri(uri) + .setStreamKeys(downloadRequest != null ? downloadRequest.streamKeys : null) + .setCustomCacheKey(downloadRequest != null ? downloadRequest.customCacheKey : null) + .setMimeType(mimeType) + .setAdTagUri(intent.getStringExtra(AD_TAG_URI_EXTRA + extrasKeySuffix)) + .setSubtitles(createSubtitlesFromIntent(intent, extrasKeySuffix)); + return populateDrmPropertiesFromIntent(builder, intent, extrasKeySuffix).build(); + } + + private static List createSubtitlesFromIntent( + Intent intent, String extrasKeySuffix) { + if (!intent.hasExtra(SUBTITLE_URI_EXTRA + extrasKeySuffix)) { + return Collections.emptyList(); + } + return Collections.singletonList( + new MediaItem.Subtitle( + Uri.parse(intent.getStringExtra(SUBTITLE_URI_EXTRA + extrasKeySuffix)), + checkNotNull(intent.getStringExtra(SUBTITLE_MIME_TYPE_EXTRA + extrasKeySuffix)), + intent.getStringExtra(SUBTITLE_LANGUAGE_EXTRA + extrasKeySuffix), + C.SELECTION_FLAG_DEFAULT)); + } + + private static MediaItem.Builder populateDrmPropertiesFromIntent( + MediaItem.Builder builder, Intent intent, String extrasKeySuffix) { + String schemeKey = DRM_SCHEME_EXTRA + extrasKeySuffix; + String schemeUuidKey = DRM_SCHEME_UUID_EXTRA + extrasKeySuffix; + if (!intent.hasExtra(schemeKey) && !intent.hasExtra(schemeUuidKey)) { + return builder; + } + String drmSchemeExtra = + intent.hasExtra(schemeKey) + ? intent.getStringExtra(schemeKey) + : intent.getStringExtra(schemeUuidKey); + String[] drmSessionForClearTypesExtra = + intent.getStringArrayExtra(DRM_SESSION_FOR_CLEAR_TYPES_EXTRA + extrasKeySuffix); + Map headers = new HashMap<>(); + String[] keyRequestPropertiesArray = + intent.getStringArrayExtra(DRM_KEY_REQUEST_PROPERTIES_EXTRA + extrasKeySuffix); + if (keyRequestPropertiesArray != null) { + for (int i = 0; i < keyRequestPropertiesArray.length; i += 2) { + headers.put(keyRequestPropertiesArray[i], keyRequestPropertiesArray[i + 1]); + } + } + builder + .setDrmUuid(Util.getDrmUuid(Util.castNonNull(drmSchemeExtra))) + .setDrmLicenseUri(intent.getStringExtra(DRM_LICENSE_URL_EXTRA + extrasKeySuffix)) + .setDrmSessionForClearTypes(toTrackTypeList(drmSessionForClearTypesExtra)) + .setDrmMultiSession( + intent.getBooleanExtra(DRM_MULTI_SESSION_EXTRA + extrasKeySuffix, false)) + .setDrmLicenseRequestHeaders(headers); + return builder; + } + + private static List toTrackTypeList(@Nullable String[] trackTypeStringsArray) { + if (trackTypeStringsArray == null) { + return Collections.emptyList(); + } + HashSet trackTypes = new HashSet<>(); + for (String trackTypeString : trackTypeStringsArray) { + switch (Util.toLowerInvariant(trackTypeString)) { + case "audio": + trackTypes.add(C.TRACK_TYPE_AUDIO); + break; + case "video": + trackTypes.add(C.TRACK_TYPE_VIDEO); + break; + default: + throw new IllegalArgumentException("Invalid track type: " + trackTypeString); + } + } + return new ArrayList<>(trackTypes); + } + + private static void addPlaybackPropertiesToIntent( + MediaItem.PlaybackProperties playbackProperties, Intent intent, String extrasKeySuffix) { + boolean isLive = false; + String sphericalStereoMode = null; + if (playbackProperties.tag instanceof Tag) { + Tag tag = (Tag) playbackProperties.tag; + isLive = tag.isLive; + sphericalStereoMode = tag.sphericalStereoMode; + } + intent + .putExtra(MIME_TYPE_EXTRA + extrasKeySuffix, playbackProperties.mimeType) + .putExtra( + AD_TAG_URI_EXTRA + extrasKeySuffix, + playbackProperties.adTagUri != null ? playbackProperties.adTagUri.toString() : null) + .putExtra(IS_LIVE_EXTRA + extrasKeySuffix, isLive) + .putExtra(SPHERICAL_STEREO_MODE_EXTRA, sphericalStereoMode); + if (playbackProperties.drmConfiguration != null) { + addDrmConfigurationToIntent(playbackProperties.drmConfiguration, intent, extrasKeySuffix); + } + if (!playbackProperties.subtitles.isEmpty()) { + checkState(playbackProperties.subtitles.size() == 1); + MediaItem.Subtitle subtitle = playbackProperties.subtitles.get(0); + intent.putExtra(SUBTITLE_URI_EXTRA + extrasKeySuffix, subtitle.uri.toString()); + intent.putExtra(SUBTITLE_MIME_TYPE_EXTRA + extrasKeySuffix, subtitle.mimeType); + intent.putExtra(SUBTITLE_LANGUAGE_EXTRA + extrasKeySuffix, subtitle.language); + } + } + + private static void addDrmConfigurationToIntent( + MediaItem.DrmConfiguration drmConfiguration, Intent intent, String extrasKeySuffix) { + intent.putExtra(DRM_SCHEME_EXTRA + extrasKeySuffix, drmConfiguration.uuid.toString()); + intent.putExtra( + DRM_LICENSE_URL_EXTRA + extrasKeySuffix, + checkNotNull(drmConfiguration.licenseUri).toString()); + intent.putExtra(DRM_MULTI_SESSION_EXTRA + extrasKeySuffix, drmConfiguration.multiSession); + + String[] drmKeyRequestProperties = new String[drmConfiguration.requestHeaders.size() * 2]; + int index = 0; + for (Map.Entry entry : drmConfiguration.requestHeaders.entrySet()) { + drmKeyRequestProperties[index++] = entry.getKey(); + drmKeyRequestProperties[index++] = entry.getValue(); + } + intent.putExtra(DRM_KEY_REQUEST_PROPERTIES_EXTRA + extrasKeySuffix, drmKeyRequestProperties); + + ArrayList typeStrings = new ArrayList<>(); + for (int type : drmConfiguration.sessionForClearTypes) { + // Only audio and video are supported. + Assertions.checkState(type == C.TRACK_TYPE_AUDIO || type == C.TRACK_TYPE_VIDEO); + typeStrings.add(type == C.TRACK_TYPE_AUDIO ? "audio" : "video"); + } + intent.putExtra( + DRM_SESSION_FOR_CLEAR_TYPES_EXTRA + extrasKeySuffix, typeStrings.toArray(new String[0])); + } +} 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 2de117e9d7..47d7966b18 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 @@ -32,37 +32,19 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AppCompatActivity; import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.C.ContentType; import com.google.android.exoplayer2.ExoPlaybackException; -import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.MediaItem; import com.google.android.exoplayer2.PlaybackPreparer; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.RenderersFactory; import com.google.android.exoplayer2.SimpleExoPlayer; -import com.google.android.exoplayer2.demo.Sample.UriSample; -import com.google.android.exoplayer2.drm.DefaultDrmSessionManager; -import com.google.android.exoplayer2.drm.DrmSessionManager; -import com.google.android.exoplayer2.drm.ExoMediaCrypto; -import com.google.android.exoplayer2.drm.FrameworkMediaDrm; -import com.google.android.exoplayer2.drm.HttpMediaDrmCallback; -import com.google.android.exoplayer2.drm.MediaDrmCallback; +import com.google.android.exoplayer2.audio.AudioAttributes; import com.google.android.exoplayer2.mediacodec.MediaCodecRenderer.DecoderInitializationException; import com.google.android.exoplayer2.mediacodec.MediaCodecUtil.DecoderQueryException; -import com.google.android.exoplayer2.offline.DownloadHelper; -import com.google.android.exoplayer2.offline.DownloadRequest; import com.google.android.exoplayer2.source.BehindLiveWindowException; -import com.google.android.exoplayer2.source.ConcatenatingMediaSource; -import com.google.android.exoplayer2.source.MediaSource; -import com.google.android.exoplayer2.source.MediaSourceFactory; -import com.google.android.exoplayer2.source.MergingMediaSource; -import com.google.android.exoplayer2.source.ProgressiveMediaSource; -import com.google.android.exoplayer2.source.SingleSampleMediaSource; +import com.google.android.exoplayer2.source.DefaultMediaSourceFactory; 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.hls.HlsMediaSource; -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.MappingTrackSelector.MappedTrackInfo; @@ -74,7 +56,7 @@ import com.google.android.exoplayer2.ui.PlayerControlView; import com.google.android.exoplayer2.ui.PlayerView; import com.google.android.exoplayer2.ui.spherical.SphericalGLSurfaceView; import com.google.android.exoplayer2.upstream.DataSource; -import com.google.android.exoplayer2.upstream.HttpDataSource; +import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.ErrorMessageProvider; import com.google.android.exoplayer2.util.EventLogger; import com.google.android.exoplayer2.util.Util; @@ -82,49 +64,13 @@ import java.lang.reflect.Constructor; import java.net.CookieHandler; import java.net.CookieManager; import java.net.CookiePolicy; +import java.util.Collections; +import java.util.List; /** An activity that plays media using {@link SimpleExoPlayer}. */ public class PlayerActivity extends AppCompatActivity implements OnClickListener, PlaybackPreparer, PlayerControlView.VisibilityListener { - // Activity extras. - - 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"; - - // Actions. - - public static final String ACTION_VIEW = "com.google.android.exoplayer.demo.action.VIEW"; - public static final String ACTION_VIEW_LIST = - "com.google.android.exoplayer.demo.action.VIEW_LIST"; - - // Player configuration extras. - - public static final String ABR_ALGORITHM_EXTRA = "abr_algorithm"; - public static final String ABR_ALGORITHM_DEFAULT = "default"; - public static final String ABR_ALGORITHM_RANDOM = "random"; - - // Media item configuration extras. - - public static final String URI_EXTRA = "uri"; - public static final String EXTENSION_EXTRA = "extension"; - public static final String IS_LIVE_EXTRA = "is_live"; - - public static final String DRM_SCHEME_EXTRA = "drm_scheme"; - public static final String DRM_LICENSE_URL_EXTRA = "drm_license_url"; - public static final String DRM_KEY_REQUEST_PROPERTIES_EXTRA = "drm_key_request_properties"; - public static final String DRM_MULTI_SESSION_EXTRA = "drm_multi_session"; - public static final String PREFER_EXTENSION_DECODERS_EXTRA = "prefer_extension_decoders"; - public static final String TUNNELING_EXTRA = "tunneling"; - public static final String AD_TAG_URI_EXTRA = "ad_tag_uri"; - public static final String SUBTITLE_URI_EXTRA = "subtitle_uri"; - public static final String SUBTITLE_MIME_TYPE_EXTRA = "subtitle_mime_type"; - public static final String SUBTITLE_LANGUAGE_EXTRA = "subtitle_language"; - // For backwards compatibility only. - public static final String DRM_SCHEME_UUID_EXTRA = "drm_scheme_uuid"; - // Saved instance state keys. private static final String KEY_TRACK_SELECTOR_PARAMETERS = "track_selector_parameters"; @@ -133,6 +79,7 @@ public class PlayerActivity extends AppCompatActivity private static final String KEY_AUTO_PLAY = "auto_play"; private static final CookieManager DEFAULT_COOKIE_MANAGER; + static { DEFAULT_COOKIE_MANAGER = new CookieManager(); DEFAULT_COOKIE_MANAGER.setCookiePolicy(CookiePolicy.ACCEPT_ORIGINAL_SERVER); @@ -146,12 +93,11 @@ public class PlayerActivity extends AppCompatActivity private DataSource.Factory dataSourceFactory; private SimpleExoPlayer player; - private MediaSource mediaSource; + private List mediaItems; private DefaultTrackSelector trackSelector; private DefaultTrackSelector.Parameters trackSelectorParameters; private DebugTextViewHelper debugViewHelper; private TrackGroupArray lastSeenTrackGroupArray; - private boolean startAutoPlay; private int startWindow; private long startPosition; @@ -166,7 +112,7 @@ public class PlayerActivity extends AppCompatActivity @Override public void onCreate(Bundle savedInstanceState) { Intent intent = getIntent(); - String sphericalStereoMode = intent.getStringExtra(SPHERICAL_STEREO_MODE_EXTRA); + String sphericalStereoMode = intent.getStringExtra(IntentUtil.SPHERICAL_STEREO_MODE_EXTRA); if (sphericalStereoMode != null) { setTheme(R.style.PlayerTheme_Spherical); } @@ -188,11 +134,11 @@ public class PlayerActivity extends AppCompatActivity playerView.requestFocus(); if (sphericalStereoMode != null) { int stereoMode; - if (SPHERICAL_STEREO_MODE_MONO.equals(sphericalStereoMode)) { + if (IntentUtil.SPHERICAL_STEREO_MODE_MONO.equals(sphericalStereoMode)) { stereoMode = C.STEREO_MODE_MONO; - } else if (SPHERICAL_STEREO_MODE_TOP_BOTTOM.equals(sphericalStereoMode)) { + } else if (IntentUtil.SPHERICAL_STEREO_MODE_TOP_BOTTOM.equals(sphericalStereoMode)) { stereoMode = C.STEREO_MODE_TOP_BOTTOM; - } else if (SPHERICAL_STEREO_MODE_LEFT_RIGHT.equals(sphericalStereoMode)) { + } else if (IntentUtil.SPHERICAL_STEREO_MODE_LEFT_RIGHT.equals(sphericalStereoMode)) { stereoMode = C.STEREO_MODE_LEFT_RIGHT; } else { showToast(R.string.error_unrecognized_stereo_mode); @@ -210,7 +156,7 @@ public class PlayerActivity extends AppCompatActivity } else { DefaultTrackSelector.ParametersBuilder builder = new DefaultTrackSelector.ParametersBuilder(/* context= */ this); - boolean tunneling = intent.getBooleanExtra(TUNNELING_EXTRA, false); + boolean tunneling = intent.getBooleanExtra(IntentUtil.TUNNELING_EXTRA, false); if (Util.SDK_INT >= 21 && tunneling) { builder.setTunnelingAudioSessionId(C.generateAudioSessionIdV21(/* context= */ this)); } @@ -279,8 +225,9 @@ public class PlayerActivity extends AppCompatActivity } @Override - public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, - @NonNull int[] grantResults) { + public void onRequestPermissionsResult( + int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); if (grantResults.length == 0) { // Empty results are triggered if a permission is requested while another request was already // pending and can be safely ignored in this case. @@ -295,7 +242,7 @@ public class PlayerActivity extends AppCompatActivity } @Override - public void onSaveInstanceState(Bundle outState) { + public void onSaveInstanceState(@NonNull Bundle outState) { super.onSaveInstanceState(outState); updateTrackSelectorParameters(); updateStartPosition(); @@ -333,7 +280,7 @@ public class PlayerActivity extends AppCompatActivity @Override public void preparePlayback() { - player.retry(); + player.prepare(); } // PlaybackControlView.VisibilityListener implementation @@ -349,16 +296,16 @@ public class PlayerActivity extends AppCompatActivity if (player == null) { Intent intent = getIntent(); - mediaSource = createTopLevelMediaSource(intent); - if (mediaSource == null) { + mediaItems = createMediaItems(intent); + if (mediaItems.isEmpty()) { return; } TrackSelection.Factory trackSelectionFactory; - String abrAlgorithm = intent.getStringExtra(ABR_ALGORITHM_EXTRA); - if (abrAlgorithm == null || ABR_ALGORITHM_DEFAULT.equals(abrAlgorithm)) { + String abrAlgorithm = intent.getStringExtra(IntentUtil.ABR_ALGORITHM_EXTRA); + if (abrAlgorithm == null || IntentUtil.ABR_ALGORITHM_DEFAULT.equals(abrAlgorithm)) { trackSelectionFactory = new AdaptiveTrackSelection.Factory(); - } else if (ABR_ALGORITHM_RANDOM.equals(abrAlgorithm)) { + } else if (IntentUtil.ABR_ALGORITHM_RANDOM.equals(abrAlgorithm)) { trackSelectionFactory = new RandomTrackSelection.Factory(); } else { showToast(R.string.error_unrecognized_abr_algorithm); @@ -367,7 +314,7 @@ public class PlayerActivity extends AppCompatActivity } boolean preferExtensionDecoders = - intent.getBooleanExtra(PREFER_EXTENSION_DECODERS_EXTRA, false); + intent.getBooleanExtra(IntentUtil.PREFER_EXTENSION_DECODERS_EXTRA, false); RenderersFactory renderersFactory = ((DemoApplication) getApplication()).buildRenderersFactory(preferExtensionDecoders); @@ -377,174 +324,73 @@ public class PlayerActivity extends AppCompatActivity player = new SimpleExoPlayer.Builder(/* context= */ this, renderersFactory) + .setMediaSourceFactory( + new DefaultMediaSourceFactory( + /* context= */ this, dataSourceFactory, new AdSupportProvider())) .setTrackSelector(trackSelector) .build(); player.addListener(new PlayerEventListener()); + player.setAudioAttributes(AudioAttributes.DEFAULT, /* handleAudioFocus= */ true); player.setPlayWhenReady(startAutoPlay); player.addAnalyticsListener(new EventLogger(trackSelector)); playerView.setPlayer(player); playerView.setPlaybackPreparer(this); debugViewHelper = new DebugTextViewHelper(player, debugTextView); debugViewHelper.start(); - if (adsLoader != null) { - adsLoader.setPlayer(player); - } } boolean haveStartPosition = startWindow != C.INDEX_UNSET; if (haveStartPosition) { player.seekTo(startWindow, startPosition); } - player.setMediaItem(mediaSource); + player.setMediaItems(mediaItems, /* resetPosition= */ !haveStartPosition); player.prepare(); updateButtonVisibility(); } - @Nullable - private MediaSource createTopLevelMediaSource(Intent intent) { + private List createMediaItems(Intent intent) { String action = intent.getAction(); - boolean actionIsListView = ACTION_VIEW_LIST.equals(action); - if (!actionIsListView && !ACTION_VIEW.equals(action)) { + boolean actionIsListView = IntentUtil.ACTION_VIEW_LIST.equals(action); + if (!actionIsListView && !IntentUtil.ACTION_VIEW.equals(action)) { showToast(getString(R.string.unexpected_intent_action, action)); finish(); - return null; + return Collections.emptyList(); } - Sample intentAsSample = Sample.createFromIntent(intent); - UriSample[] samples = - intentAsSample instanceof Sample.PlaylistSample - ? ((Sample.PlaylistSample) intentAsSample).children - : new UriSample[] {(UriSample) intentAsSample}; + List mediaItems = + IntentUtil.createMediaItemsFromIntent( + intent, ((DemoApplication) getApplication()).getDownloadTracker()); + boolean hasAds = false; + for (int i = 0; i < mediaItems.size(); i++) { + MediaItem mediaItem = mediaItems.get(i); - boolean seenAdsTagUri = false; - for (UriSample sample : samples) { - seenAdsTagUri |= sample.adTagUri != null; - if (!Util.checkCleartextTrafficPermitted(sample.uri)) { + if (!Util.checkCleartextTrafficPermitted(mediaItem)) { showToast(R.string.error_cleartext_not_permitted); - return null; + return Collections.emptyList(); } - if (Util.maybeRequestReadExternalStoragePermission(/* activity= */ this, sample.uri)) { + if (Util.maybeRequestReadExternalStoragePermission(/* activity= */ this, mediaItem)) { // The player will be reinitialized if the permission is granted. - return null; + return Collections.emptyList(); } - } - MediaSource[] mediaSources = new MediaSource[samples.length]; - for (int i = 0; i < samples.length; i++) { - mediaSources[i] = createLeafMediaSource(samples[i]); - Sample.SubtitleInfo subtitleInfo = samples[i].subtitleInfo; - if (subtitleInfo != null) { - Format subtitleFormat = - Format.createTextSampleFormat( - /* id= */ null, - subtitleInfo.mimeType, - C.SELECTION_FLAG_DEFAULT, - subtitleInfo.language); - MediaSource subtitleMediaSource = - new SingleSampleMediaSource.Factory(dataSourceFactory) - .createMediaSource(subtitleInfo.uri, subtitleFormat, C.TIME_UNSET); - mediaSources[i] = new MergingMediaSource(mediaSources[i], subtitleMediaSource); - } - } - MediaSource mediaSource = - mediaSources.length == 1 ? mediaSources[0] : new ConcatenatingMediaSource(mediaSources); - - if (seenAdsTagUri) { - Uri adTagUri = samples[0].adTagUri; - if (actionIsListView) { - showToast(R.string.unsupported_ads_in_concatenation); - } else { - if (!adTagUri.equals(loadedAdTagUri)) { - releaseAdsLoader(); - loadedAdTagUri = adTagUri; - } - MediaSource adsMediaSource = createAdsMediaSource(mediaSource, adTagUri); - if (adsMediaSource != null) { - mediaSource = adsMediaSource; - } else { - showToast(R.string.ima_not_loaded); + MediaItem.DrmConfiguration drmConfiguration = + Assertions.checkNotNull(mediaItem.playbackProperties).drmConfiguration; + if (drmConfiguration != null) { + if (Util.SDK_INT < 18) { + showToast(R.string.error_drm_unsupported_before_api_18); + finish(); + return Collections.emptyList(); + } else if (!MediaDrm.isCryptoSchemeSupported(drmConfiguration.uuid)) { + showToast(R.string.error_drm_unsupported_scheme); + finish(); + return Collections.emptyList(); } } - } else { + hasAds |= mediaItem.playbackProperties.adTagUri != null; + } + if (!hasAds) { releaseAdsLoader(); } - - return mediaSource; - } - - private MediaSource createLeafMediaSource(UriSample parameters) { - Sample.DrmInfo drmInfo = parameters.drmInfo; - int errorStringId = R.string.error_drm_unknown; - DrmSessionManager drmSessionManager = null; - if (drmInfo == null) { - drmSessionManager = DrmSessionManager.getDummyDrmSessionManager(); - } else if (Util.SDK_INT < 18) { - errorStringId = R.string.error_drm_unsupported_before_api_18; - } else if (!MediaDrm.isCryptoSchemeSupported(drmInfo.drmScheme)) { - errorStringId = R.string.error_drm_unsupported_scheme; - } else { - MediaDrmCallback mediaDrmCallback = - createMediaDrmCallback(drmInfo.drmLicenseUrl, drmInfo.drmKeyRequestProperties); - drmSessionManager = - new DefaultDrmSessionManager.Builder() - .setUuidAndExoMediaDrmProvider(drmInfo.drmScheme, FrameworkMediaDrm.DEFAULT_PROVIDER) - .setMultiSession(drmInfo.drmMultiSession) - .build(mediaDrmCallback); - } - - if (drmSessionManager == null) { - showToast(errorStringId); - finish(); - return null; - } - - DownloadRequest downloadRequest = - ((DemoApplication) getApplication()) - .getDownloadTracker() - .getDownloadRequest(parameters.uri); - if (downloadRequest != null) { - return DownloadHelper.createMediaSource(downloadRequest, dataSourceFactory); - } - return createLeafMediaSource(parameters.uri, parameters.extension, drmSessionManager); - } - - private MediaSource createLeafMediaSource( - Uri uri, String extension, DrmSessionManager drmSessionManager) { - @ContentType int type = Util.inferContentType(uri, extension); - switch (type) { - case C.TYPE_DASH: - return new DashMediaSource.Factory(dataSourceFactory) - .setDrmSessionManager(drmSessionManager) - .createMediaSource(uri); - case C.TYPE_SS: - return new SsMediaSource.Factory(dataSourceFactory) - .setDrmSessionManager(drmSessionManager) - .createMediaSource(uri); - case C.TYPE_HLS: - return new HlsMediaSource.Factory(dataSourceFactory) - .setDrmSessionManager(drmSessionManager) - .createMediaSource(uri); - case C.TYPE_OTHER: - return new ProgressiveMediaSource.Factory(dataSourceFactory) - .setDrmSessionManager(drmSessionManager) - .createMediaSource(uri); - default: - throw new IllegalStateException("Unsupported type: " + type); - } - } - - private HttpMediaDrmCallback createMediaDrmCallback( - String licenseUrl, String[] keyRequestPropertiesArray) { - HttpDataSource.Factory licenseDataSourceFactory = - ((DemoApplication) getApplication()).buildHttpDataSourceFactory(); - HttpMediaDrmCallback drmCallback = - new HttpMediaDrmCallback(licenseUrl, licenseDataSourceFactory); - if (keyRequestPropertiesArray != null) { - for (int i = 0; i < keyRequestPropertiesArray.length - 1; i += 2) { - drmCallback.setKeyRequestProperty(keyRequestPropertiesArray[i], - keyRequestPropertiesArray[i + 1]); - } - } - return drmCallback; + return mediaItems; } private void releasePlayer() { @@ -555,7 +401,7 @@ public class PlayerActivity extends AppCompatActivity debugViewHelper = null; player.release(); player = null; - mediaSource = null; + mediaItems = Collections.emptyList(); trackSelector = null; } if (adsLoader != null) { @@ -597,37 +443,23 @@ public class PlayerActivity extends AppCompatActivity return ((DemoApplication) getApplication()).buildDataSourceFactory(); } - /** Returns an ads media source, reusing the ads loader if one exists. */ + /** + * Returns an ads loader for the Interactive Media Ads SDK if found in the classpath, or null + * otherwise. + */ @Nullable - private MediaSource createAdsMediaSource(MediaSource mediaSource, Uri adTagUri) { + private AdsLoader maybeCreateAdsLoader(Uri adTagUri) { // Load the extension source using reflection so the demo app doesn't have to depend on it. - // The ads loader is reused for multiple playbacks, so that ad playback can resume. try { Class loaderClass = Class.forName("com.google.android.exoplayer2.ext.ima.ImaAdsLoader"); - if (adsLoader == null) { - // Full class names used so the LINT.IfChange rule triggers should any of the classes move. - // LINT.IfChange - Constructor loaderConstructor = - loaderClass - .asSubclass(AdsLoader.class) - .getConstructor(android.content.Context.class, android.net.Uri.class); - // LINT.ThenChange(../../../../../../../../proguard-rules.txt) - adsLoader = loaderConstructor.newInstance(this, adTagUri); - } - MediaSourceFactory adMediaSourceFactory = - new MediaSourceFactory() { - @Override - public MediaSource createMediaSource(Uri uri) { - return PlayerActivity.this.createLeafMediaSource( - uri, /* extension=*/ null, DrmSessionManager.getDummyDrmSessionManager()); - } - - @Override - public int[] getSupportedTypes() { - return new int[] {C.TYPE_DASH, C.TYPE_SS, C.TYPE_HLS, C.TYPE_OTHER}; - } - }; - return new AdsMediaSource(mediaSource, adMediaSourceFactory, adsLoader, playerView); + // Full class names used so the lint rule triggers should any of the classes move. + // LINT.IfChange + Constructor loaderConstructor = + loaderClass + .asSubclass(AdsLoader.class) + .getConstructor(android.content.Context.class, android.net.Uri.class); + // LINT.ThenChange(../../../../../../../../proguard-rules.txt) + return loaderConstructor.newInstance(this, adTagUri); } catch (ClassNotFoundException e) { // IMA extension not loaded. return null; @@ -672,7 +504,7 @@ public class PlayerActivity extends AppCompatActivity private class PlayerEventListener implements Player.EventListener { @Override - public void onPlayerStateChanged(boolean playWhenReady, @Player.State int playbackState) { + public void onPlaybackStateChanged(@Player.State int playbackState) { if (playbackState == Player.STATE_ENDED) { showControls(); } @@ -680,7 +512,7 @@ public class PlayerActivity extends AppCompatActivity } @Override - public void onPlayerError(ExoPlaybackException e) { + public void onPlayerError(@NonNull ExoPlaybackException e) { if (isBehindLiveWindow(e)) { clearStartPosition(); initializePlayer(); @@ -692,7 +524,8 @@ public class PlayerActivity extends AppCompatActivity @Override @SuppressWarnings("ReferenceEquality") - public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) { + public void onTracksChanged( + @NonNull TrackGroupArray trackGroups, @NonNull TrackSelectionArray trackSelections) { updateButtonVisibility(); if (trackGroups != lastSeenTrackGroupArray) { MappedTrackInfo mappedTrackInfo = trackSelector.getCurrentMappedTrackInfo(); @@ -714,7 +547,8 @@ public class PlayerActivity extends AppCompatActivity private class PlayerErrorMessageProvider implements ErrorMessageProvider { @Override - public Pair getErrorMessage(ExoPlaybackException e) { + @NonNull + public Pair getErrorMessage(@NonNull ExoPlaybackException e) { String errorString = getString(R.string.error_generic); if (e.type == ExoPlaybackException.TYPE_RENDERER) { Exception cause = e.getRendererException(); @@ -744,4 +578,36 @@ public class PlayerActivity extends AppCompatActivity return Pair.create(0, errorString); } } + + private class AdSupportProvider implements DefaultMediaSourceFactory.AdSupportProvider { + + @Nullable + @Override + public AdsLoader getAdsLoader(Uri adTagUri) { + if (mediaItems.size() > 1) { + showToast(R.string.unsupported_ads_in_concatenation); + releaseAdsLoader(); + return null; + } + if (!adTagUri.equals(loadedAdTagUri)) { + releaseAdsLoader(); + loadedAdTagUri = adTagUri; + } + // The ads loader is reused for multiple playbacks, so that ad playback can resume. + if (adsLoader == null) { + adsLoader = maybeCreateAdsLoader(adTagUri); + } + if (adsLoader != null) { + adsLoader.setPlayer(player); + } else { + showToast(R.string.ima_not_loaded); + } + return adsLoader; + } + + @Override + public AdsLoader.AdViewProvider getAdViewProvider() { + return Assertions.checkNotNull(playerView); + } + } } diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/Sample.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/Sample.java deleted file mode 100644 index 85530b993b..0000000000 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/Sample.java +++ /dev/null @@ -1,236 +0,0 @@ -/* - * Copyright (C) 2019 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.demo; - -import static com.google.android.exoplayer2.demo.PlayerActivity.ACTION_VIEW_LIST; -import static com.google.android.exoplayer2.demo.PlayerActivity.AD_TAG_URI_EXTRA; -import static com.google.android.exoplayer2.demo.PlayerActivity.DRM_KEY_REQUEST_PROPERTIES_EXTRA; -import static com.google.android.exoplayer2.demo.PlayerActivity.DRM_LICENSE_URL_EXTRA; -import static com.google.android.exoplayer2.demo.PlayerActivity.DRM_MULTI_SESSION_EXTRA; -import static com.google.android.exoplayer2.demo.PlayerActivity.DRM_SCHEME_EXTRA; -import static com.google.android.exoplayer2.demo.PlayerActivity.DRM_SCHEME_UUID_EXTRA; -import static com.google.android.exoplayer2.demo.PlayerActivity.EXTENSION_EXTRA; -import static com.google.android.exoplayer2.demo.PlayerActivity.IS_LIVE_EXTRA; -import static com.google.android.exoplayer2.demo.PlayerActivity.SUBTITLE_LANGUAGE_EXTRA; -import static com.google.android.exoplayer2.demo.PlayerActivity.SUBTITLE_MIME_TYPE_EXTRA; -import static com.google.android.exoplayer2.demo.PlayerActivity.SUBTITLE_URI_EXTRA; -import static com.google.android.exoplayer2.demo.PlayerActivity.URI_EXTRA; - -import android.content.Intent; -import android.net.Uri; -import androidx.annotation.Nullable; -import com.google.android.exoplayer2.util.Assertions; -import com.google.android.exoplayer2.util.Util; -import java.util.ArrayList; -import java.util.UUID; - -/* package */ abstract class Sample { - - public static final class UriSample extends Sample { - - public static UriSample createFromIntent(Uri uri, Intent intent, String extrasKeySuffix) { - String extension = intent.getStringExtra(EXTENSION_EXTRA + extrasKeySuffix); - String adsTagUriString = intent.getStringExtra(AD_TAG_URI_EXTRA + extrasKeySuffix); - boolean isLive = - intent.getBooleanExtra(IS_LIVE_EXTRA + extrasKeySuffix, /* defaultValue= */ false); - Uri adTagUri = adsTagUriString != null ? Uri.parse(adsTagUriString) : null; - return new UriSample( - /* name= */ null, - uri, - extension, - isLive, - DrmInfo.createFromIntent(intent, extrasKeySuffix), - adTagUri, - /* sphericalStereoMode= */ null, - SubtitleInfo.createFromIntent(intent, extrasKeySuffix)); - } - - public final Uri uri; - public final String extension; - public final boolean isLive; - public final DrmInfo drmInfo; - public final Uri adTagUri; - @Nullable public final String sphericalStereoMode; - @Nullable SubtitleInfo subtitleInfo; - - public UriSample( - String name, - Uri uri, - String extension, - boolean isLive, - DrmInfo drmInfo, - Uri adTagUri, - @Nullable String sphericalStereoMode, - @Nullable SubtitleInfo subtitleInfo) { - super(name); - this.uri = uri; - this.extension = extension; - this.isLive = isLive; - this.drmInfo = drmInfo; - this.adTagUri = adTagUri; - this.sphericalStereoMode = sphericalStereoMode; - this.subtitleInfo = subtitleInfo; - } - - @Override - public void addToIntent(Intent intent) { - intent.setAction(PlayerActivity.ACTION_VIEW).setData(uri); - intent.putExtra(PlayerActivity.IS_LIVE_EXTRA, isLive); - intent.putExtra(PlayerActivity.SPHERICAL_STEREO_MODE_EXTRA, sphericalStereoMode); - addPlayerConfigToIntent(intent, /* extrasKeySuffix= */ ""); - } - - public void addToPlaylistIntent(Intent intent, String extrasKeySuffix) { - intent.putExtra(PlayerActivity.URI_EXTRA + extrasKeySuffix, uri.toString()); - intent.putExtra(PlayerActivity.IS_LIVE_EXTRA + extrasKeySuffix, isLive); - addPlayerConfigToIntent(intent, extrasKeySuffix); - } - - private void addPlayerConfigToIntent(Intent intent, String extrasKeySuffix) { - intent - .putExtra(EXTENSION_EXTRA + extrasKeySuffix, extension) - .putExtra( - AD_TAG_URI_EXTRA + extrasKeySuffix, adTagUri != null ? adTagUri.toString() : null); - if (drmInfo != null) { - drmInfo.addToIntent(intent, extrasKeySuffix); - } - if (subtitleInfo != null) { - subtitleInfo.addToIntent(intent, extrasKeySuffix); - } - } - } - - public static final class PlaylistSample extends Sample { - - public final UriSample[] children; - - public PlaylistSample(String name, UriSample... children) { - super(name); - this.children = children; - } - - @Override - public void addToIntent(Intent intent) { - intent.setAction(PlayerActivity.ACTION_VIEW_LIST); - for (int i = 0; i < children.length; i++) { - children[i].addToPlaylistIntent(intent, /* extrasKeySuffix= */ "_" + i); - } - } - } - - public static final class DrmInfo { - - public static DrmInfo createFromIntent(Intent intent, String extrasKeySuffix) { - String schemeKey = DRM_SCHEME_EXTRA + extrasKeySuffix; - String schemeUuidKey = DRM_SCHEME_UUID_EXTRA + extrasKeySuffix; - if (!intent.hasExtra(schemeKey) && !intent.hasExtra(schemeUuidKey)) { - return null; - } - String drmSchemeExtra = - intent.hasExtra(schemeKey) - ? intent.getStringExtra(schemeKey) - : intent.getStringExtra(schemeUuidKey); - UUID drmScheme = Util.getDrmUuid(drmSchemeExtra); - String drmLicenseUrl = intent.getStringExtra(DRM_LICENSE_URL_EXTRA + extrasKeySuffix); - String[] keyRequestPropertiesArray = - intent.getStringArrayExtra(DRM_KEY_REQUEST_PROPERTIES_EXTRA + extrasKeySuffix); - boolean drmMultiSession = - intent.getBooleanExtra(DRM_MULTI_SESSION_EXTRA + extrasKeySuffix, false); - return new DrmInfo(drmScheme, drmLicenseUrl, keyRequestPropertiesArray, drmMultiSession); - } - - public final UUID drmScheme; - public final String drmLicenseUrl; - public final String[] drmKeyRequestProperties; - public final boolean drmMultiSession; - - public DrmInfo( - UUID drmScheme, - String drmLicenseUrl, - String[] drmKeyRequestProperties, - boolean drmMultiSession) { - this.drmScheme = drmScheme; - this.drmLicenseUrl = drmLicenseUrl; - this.drmKeyRequestProperties = drmKeyRequestProperties; - this.drmMultiSession = drmMultiSession; - } - - public void addToIntent(Intent intent, String extrasKeySuffix) { - Assertions.checkNotNull(intent); - intent.putExtra(DRM_SCHEME_EXTRA + extrasKeySuffix, drmScheme.toString()); - intent.putExtra(DRM_LICENSE_URL_EXTRA + extrasKeySuffix, drmLicenseUrl); - intent.putExtra(DRM_KEY_REQUEST_PROPERTIES_EXTRA + extrasKeySuffix, drmKeyRequestProperties); - intent.putExtra(DRM_MULTI_SESSION_EXTRA + extrasKeySuffix, drmMultiSession); - } - } - - public static final class SubtitleInfo { - - @Nullable - public static SubtitleInfo createFromIntent(Intent intent, String extrasKeySuffix) { - if (!intent.hasExtra(SUBTITLE_URI_EXTRA + extrasKeySuffix)) { - return null; - } - return new SubtitleInfo( - Uri.parse(intent.getStringExtra(SUBTITLE_URI_EXTRA + extrasKeySuffix)), - intent.getStringExtra(SUBTITLE_MIME_TYPE_EXTRA + extrasKeySuffix), - intent.getStringExtra(SUBTITLE_LANGUAGE_EXTRA + extrasKeySuffix)); - } - - public final Uri uri; - public final String mimeType; - @Nullable public final String language; - - public SubtitleInfo(Uri uri, String mimeType, @Nullable String language) { - this.uri = Assertions.checkNotNull(uri); - this.mimeType = Assertions.checkNotNull(mimeType); - this.language = language; - } - - public void addToIntent(Intent intent, String extrasKeySuffix) { - intent.putExtra(SUBTITLE_URI_EXTRA + extrasKeySuffix, uri.toString()); - intent.putExtra(SUBTITLE_MIME_TYPE_EXTRA + extrasKeySuffix, mimeType); - intent.putExtra(SUBTITLE_LANGUAGE_EXTRA + extrasKeySuffix, language); - } - } - - public static Sample createFromIntent(Intent intent) { - if (ACTION_VIEW_LIST.equals(intent.getAction())) { - ArrayList intentUris = new ArrayList<>(); - int index = 0; - while (intent.hasExtra(URI_EXTRA + "_" + index)) { - intentUris.add(intent.getStringExtra(URI_EXTRA + "_" + index)); - index++; - } - UriSample[] children = new UriSample[intentUris.size()]; - for (int i = 0; i < children.length; i++) { - Uri uri = Uri.parse(intentUris.get(i)); - children[i] = UriSample.createFromIntent(uri, intent, /* extrasKeySuffix= */ "_" + i); - } - return new PlaylistSample(/* name= */ null, children); - } else { - return UriSample.createFromIntent(intent.getData(), intent, /* extrasKeySuffix= */ ""); - } - } - - @Nullable public final String name; - - public Sample(String name) { - this.name = name; - } - - public abstract void addToIntent(Intent intent); -} 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 cdce29aa5e..6f598b95a0 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 @@ -15,8 +15,13 @@ */ package com.google.android.exoplayer2.demo; +import static com.google.android.exoplayer2.util.Assertions.checkNotNull; + import android.content.Context; import android.content.Intent; +import android.content.SharedPreferences; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; import android.content.res.AssetManager; import android.net.Uri; import android.os.AsyncTask; @@ -34,13 +39,14 @@ import android.widget.ExpandableListView.OnChildClickListener; import android.widget.ImageButton; import android.widget.TextView; import android.widget.Toast; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AppCompatActivity; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.MediaItem; +import com.google.android.exoplayer2.MediaMetadata; import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.RenderersFactory; -import com.google.android.exoplayer2.demo.Sample.DrmInfo; -import com.google.android.exoplayer2.demo.Sample.PlaylistSample; -import com.google.android.exoplayer2.demo.Sample.UriSample; import com.google.android.exoplayer2.offline.DownloadService; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSourceInputStream; @@ -55,33 +61,40 @@ import java.io.InputStreamReader; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; import java.util.List; +import java.util.Map; /** An activity for selecting from a list of media samples. */ public class SampleChooserActivity extends AppCompatActivity implements DownloadTracker.Listener, OnChildClickListener { private static final String TAG = "SampleChooserActivity"; + private static final String GROUP_POSITION_PREFERENCE_KEY = "SAMPLE_CHOOSER_GROUP_POSITION"; + private static final String CHILD_POSITION_PREFERENCE_KEY = "SAMPLE_CHOOSER_CHILD_POSITION"; + private String[] uris; private boolean useExtensionRenderers; private DownloadTracker downloadTracker; private SampleAdapter sampleAdapter; private MenuItem preferExtensionDecodersMenuItem; private MenuItem randomAbrMenuItem; private MenuItem tunnelingMenuItem; + private ExpandableListView sampleListView; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.sample_chooser_activity); sampleAdapter = new SampleAdapter(); - ExpandableListView sampleListView = findViewById(R.id.sample_list); + sampleListView = findViewById(R.id.sample_list); + sampleListView.setAdapter(sampleAdapter); sampleListView.setOnChildClickListener(this); Intent intent = getIntent(); String dataUri = intent.getDataString(); - String[] uris; if (dataUri != null) { uris = new String[] {dataUri}; } else { @@ -105,8 +118,7 @@ public class SampleChooserActivity extends AppCompatActivity DemoApplication application = (DemoApplication) getApplication(); useExtensionRenderers = application.useExtensionRenderers(); downloadTracker = application.getDownloadTracker(); - SampleListLoader loaderTask = new SampleListLoader(); - loaderTask.execute(uris); + loadSample(); // 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 @@ -157,67 +169,116 @@ public class SampleChooserActivity extends AppCompatActivity sampleAdapter.notifyDataSetChanged(); } - private void onSampleGroups(final List groups, boolean sawError) { + @Override + public void onRequestPermissionsResult( + int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + if (grantResults.length == 0) { + // Empty results are triggered if a permission is requested while another request was already + // pending and can be safely ignored in this case. + return; + } + if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { + loadSample(); + } else { + Toast.makeText(getApplicationContext(), R.string.sample_list_load_error, Toast.LENGTH_LONG) + .show(); + finish(); + } + } + + private void loadSample() { + checkNotNull(uris); + + for (int i = 0; i < uris.length; i++) { + Uri uri = Uri.parse(uris[i]); + if (Util.maybeRequestReadExternalStoragePermission(this, uri)) { + return; + } + } + + SampleListLoader loaderTask = new SampleListLoader(); + loaderTask.execute(uris); + } + + private void onPlaylistGroups(final List groups, boolean sawError) { if (sawError) { Toast.makeText(getApplicationContext(), R.string.sample_list_load_error, Toast.LENGTH_LONG) .show(); } - sampleAdapter.setSampleGroups(groups); + sampleAdapter.setPlaylistGroups(groups); + + SharedPreferences preferences = getPreferences(MODE_PRIVATE); + + int groupPosition = -1; + int childPosition = -1; + try { + groupPosition = preferences.getInt(GROUP_POSITION_PREFERENCE_KEY, /* defValue= */ -1); + childPosition = preferences.getInt(CHILD_POSITION_PREFERENCE_KEY, /* defValue= */ -1); + } catch (ClassCastException e) { + Log.w(TAG, "Saved position is not an int. Will not restore position.", e); + } + if (groupPosition != -1 && childPosition != -1) { + sampleListView.expandGroup(groupPosition); // shouldExpandGroup does not work without this. + sampleListView.setSelectedChild(groupPosition, childPosition, /* shouldExpandGroup= */ true); + } } @Override public boolean onChildClick( ExpandableListView parent, View view, int groupPosition, int childPosition, long id) { - Sample sample = (Sample) view.getTag(); + // Save the selected item first to be able to restore it if the tested code crashes. + SharedPreferences.Editor prefEditor = getPreferences(MODE_PRIVATE).edit(); + prefEditor.putInt(GROUP_POSITION_PREFERENCE_KEY, groupPosition); + prefEditor.putInt(CHILD_POSITION_PREFERENCE_KEY, childPosition); + prefEditor.apply(); + + PlaylistHolder playlistHolder = (PlaylistHolder) view.getTag(); Intent intent = new Intent(this, PlayerActivity.class); intent.putExtra( - PlayerActivity.PREFER_EXTENSION_DECODERS_EXTRA, + IntentUtil.PREFER_EXTENSION_DECODERS_EXTRA, isNonNullAndChecked(preferExtensionDecodersMenuItem)); String abrAlgorithm = isNonNullAndChecked(randomAbrMenuItem) - ? PlayerActivity.ABR_ALGORITHM_RANDOM - : PlayerActivity.ABR_ALGORITHM_DEFAULT; - intent.putExtra(PlayerActivity.ABR_ALGORITHM_EXTRA, abrAlgorithm); - intent.putExtra(PlayerActivity.TUNNELING_EXTRA, isNonNullAndChecked(tunnelingMenuItem)); - sample.addToIntent(intent); + ? IntentUtil.ABR_ALGORITHM_RANDOM + : IntentUtil.ABR_ALGORITHM_DEFAULT; + intent.putExtra(IntentUtil.ABR_ALGORITHM_EXTRA, abrAlgorithm); + intent.putExtra(IntentUtil.TUNNELING_EXTRA, isNonNullAndChecked(tunnelingMenuItem)); + IntentUtil.addToIntent(playlistHolder.mediaItems, intent); startActivity(intent); return true; } - private void onSampleDownloadButtonClicked(Sample sample) { - int downloadUnsupportedStringId = getDownloadUnsupportedStringId(sample); + private void onSampleDownloadButtonClicked(PlaylistHolder playlistHolder) { + int downloadUnsupportedStringId = getDownloadUnsupportedStringId(playlistHolder); if (downloadUnsupportedStringId != 0) { Toast.makeText(getApplicationContext(), downloadUnsupportedStringId, Toast.LENGTH_LONG) .show(); } else { - UriSample uriSample = (UriSample) sample; RenderersFactory renderersFactory = ((DemoApplication) getApplication()) .buildRenderersFactory(isNonNullAndChecked(preferExtensionDecodersMenuItem)); downloadTracker.toggleDownload( - getSupportFragmentManager(), - sample.name, - uriSample.uri, - uriSample.extension, - renderersFactory); + getSupportFragmentManager(), playlistHolder.mediaItems.get(0), renderersFactory); } } - private int getDownloadUnsupportedStringId(Sample sample) { - if (sample instanceof PlaylistSample) { + private int getDownloadUnsupportedStringId(PlaylistHolder playlistHolder) { + if (playlistHolder.mediaItems.size() > 1) { return R.string.download_playlist_unsupported; } - UriSample uriSample = (UriSample) sample; - if (uriSample.drmInfo != null) { + MediaItem.PlaybackProperties playbackProperties = + checkNotNull(playlistHolder.mediaItems.get(0).playbackProperties); + if (playbackProperties.drmConfiguration != null) { return R.string.download_drm_unsupported; } - if (uriSample.isLive) { + if (((IntentUtil.Tag) checkNotNull(playbackProperties.tag)).isLive) { return R.string.download_live_unsupported; } - if (uriSample.adTagUri != null) { + if (playbackProperties.adTagUri != null) { return R.string.download_ads_unsupported; } - String scheme = uriSample.uri.getScheme(); + String scheme = playbackProperties.uri.getScheme(); if (!("http".equals(scheme) || "https".equals(scheme))) { return R.string.download_scheme_unsupported; } @@ -229,13 +290,13 @@ public class SampleChooserActivity extends AppCompatActivity return menuItem != null && menuItem.isChecked(); } - private final class SampleListLoader extends AsyncTask> { + private final class SampleListLoader extends AsyncTask> { private boolean sawError; @Override - protected List doInBackground(String... uris) { - List result = new ArrayList<>(); + protected List doInBackground(String... uris) { + List result = new ArrayList<>(); Context context = getApplicationContext(); String userAgent = Util.getUserAgent(context, "ExoPlayerDemo"); DataSource dataSource = @@ -244,7 +305,7 @@ public class SampleChooserActivity extends AppCompatActivity DataSpec dataSpec = new DataSpec(Uri.parse(uri)); InputStream inputStream = new DataSourceInputStream(dataSource, dataSpec); try { - readSampleGroups(new JsonReader(new InputStreamReader(inputStream, "UTF-8")), result); + readPlaylistGroups(new JsonReader(new InputStreamReader(inputStream, "UTF-8")), result); } catch (Exception e) { Log.e(TAG, "Error loading sample list: " + uri, e); sawError = true; @@ -256,21 +317,23 @@ public class SampleChooserActivity extends AppCompatActivity } @Override - protected void onPostExecute(List result) { - onSampleGroups(result, sawError); + protected void onPostExecute(List result) { + onPlaylistGroups(result, sawError); } - private void readSampleGroups(JsonReader reader, List groups) throws IOException { + private void readPlaylistGroups(JsonReader reader, List groups) + throws IOException { reader.beginArray(); while (reader.hasNext()) { - readSampleGroup(reader, groups); + readPlaylistGroup(reader, groups); } reader.endArray(); } - private void readSampleGroup(JsonReader reader, List groups) throws IOException { + private void readPlaylistGroup(JsonReader reader, List groups) + throws IOException { String groupName = ""; - ArrayList samples = new ArrayList<>(); + ArrayList playlistHolders = new ArrayList<>(); reader.beginObject(); while (reader.hasNext()) { @@ -282,7 +345,7 @@ public class SampleChooserActivity extends AppCompatActivity case "samples": reader.beginArray(); while (reader.hasNext()) { - samples.add(readEntry(reader, false)); + playlistHolders.add(readEntry(reader, false)); } reader.endArray(); break; @@ -295,33 +358,28 @@ public class SampleChooserActivity extends AppCompatActivity } reader.endObject(); - SampleGroup group = getGroup(groupName, groups); - group.samples.addAll(samples); + PlaylistGroup group = getGroup(groupName, groups); + group.playlists.addAll(playlistHolders); } - private Sample readEntry(JsonReader reader, boolean insidePlaylist) throws IOException { - String sampleName = null; + private PlaylistHolder readEntry(JsonReader reader, boolean insidePlaylist) throws IOException { Uri uri = null; String extension = null; + String title = null; boolean isLive = false; - String drmScheme = null; - String drmLicenseUrl = null; - String[] drmKeyRequestProperties = null; - boolean drmMultiSession = false; - ArrayList playlistSamples = null; - String adTagUri = null; String sphericalStereoMode = null; - List subtitleInfos = new ArrayList<>(); + ArrayList children = null; Uri subtitleUri = null; String subtitleMimeType = null; String subtitleLanguage = null; + MediaItem.Builder mediaItem = new MediaItem.Builder(); reader.beginObject(); while (reader.hasNext()) { String name = reader.nextName(); switch (name) { case "name": - sampleName = reader.nextString(); + title = reader.nextString(); break; case "uri": uri = Uri.parse(reader.nextString()); @@ -330,38 +388,46 @@ public class SampleChooserActivity extends AppCompatActivity extension = reader.nextString(); break; case "drm_scheme": - drmScheme = reader.nextString(); + mediaItem.setDrmUuid(Util.getDrmUuid(reader.nextString())); break; case "is_live": isLive = reader.nextBoolean(); break; case "drm_license_url": - drmLicenseUrl = reader.nextString(); + mediaItem.setDrmLicenseUri(reader.nextString()); break; case "drm_key_request_properties": - ArrayList drmKeyRequestPropertiesList = new ArrayList<>(); + Map requestHeaders = new HashMap<>(); reader.beginObject(); while (reader.hasNext()) { - drmKeyRequestPropertiesList.add(reader.nextName()); - drmKeyRequestPropertiesList.add(reader.nextString()); + requestHeaders.put(reader.nextName(), reader.nextString()); } reader.endObject(); - drmKeyRequestProperties = drmKeyRequestPropertiesList.toArray(new String[0]); + mediaItem.setDrmLicenseRequestHeaders(requestHeaders); + break; + case "drm_session_for_clear_types": + HashSet drmSessionForClearTypes = new HashSet<>(); + reader.beginArray(); + while (reader.hasNext()) { + drmSessionForClearTypes.add(toTrackType(reader.nextString())); + } + reader.endArray(); + mediaItem.setDrmSessionForClearTypes(new ArrayList<>(drmSessionForClearTypes)); break; case "drm_multi_session": - drmMultiSession = reader.nextBoolean(); + mediaItem.setDrmMultiSession(reader.nextBoolean()); break; case "playlist": Assertions.checkState(!insidePlaylist, "Invalid nesting of playlists"); - playlistSamples = new ArrayList<>(); + children = new ArrayList<>(); reader.beginArray(); while (reader.hasNext()) { - playlistSamples.add((UriSample) readEntry(reader, /* insidePlaylist= */ true)); + children.add(readEntry(reader, /* insidePlaylist= */ true)); } reader.endArray(); break; case "ad_tag_uri": - adTagUri = reader.nextString(); + mediaItem.setAdTagUri(reader.nextString()); break; case "spherical_stereo_mode": Assertions.checkState( @@ -382,67 +448,71 @@ public class SampleChooserActivity extends AppCompatActivity } } reader.endObject(); - DrmInfo drmInfo = - drmScheme == null - ? null - : new DrmInfo( - Util.getDrmUuid(drmScheme), - drmLicenseUrl, - drmKeyRequestProperties, - drmMultiSession); - Sample.SubtitleInfo subtitleInfo = - subtitleUri == null - ? null - : new Sample.SubtitleInfo( + + if (children != null) { + List mediaItems = new ArrayList<>(); + for (int i = 0; i < children.size(); i++) { + mediaItems.addAll(children.get(i).mediaItems); + } + return new PlaylistHolder(title, mediaItems); + } else { + mediaItem + .setUri(uri) + .setMediaMetadata(new MediaMetadata.Builder().setTitle(title).build()) + .setMimeType(IntentUtil.inferAdaptiveStreamMimeType(uri, extension)) + .setTag(new IntentUtil.Tag(isLive, sphericalStereoMode)); + if (subtitleUri != null) { + MediaItem.Subtitle subtitle = + new MediaItem.Subtitle( subtitleUri, - Assertions.checkNotNull( + checkNotNull( subtitleMimeType, "subtitle_mime_type is required if subtitle_uri is set."), subtitleLanguage); - if (playlistSamples != null) { - UriSample[] playlistSamplesArray = playlistSamples.toArray(new UriSample[0]); - return new PlaylistSample(sampleName, playlistSamplesArray); - } else { - return new UriSample( - sampleName, - uri, - extension, - isLive, - drmInfo, - adTagUri != null ? Uri.parse(adTagUri) : null, - sphericalStereoMode, - subtitleInfo); + mediaItem.setSubtitles(Collections.singletonList(subtitle)); + } + return new PlaylistHolder(title, Collections.singletonList(mediaItem.build())); } } - private SampleGroup getGroup(String groupName, List groups) { + private PlaylistGroup getGroup(String groupName, List groups) { for (int i = 0; i < groups.size(); i++) { if (Util.areEqual(groupName, groups.get(i).title)) { return groups.get(i); } } - SampleGroup group = new SampleGroup(groupName); + PlaylistGroup group = new PlaylistGroup(groupName); groups.add(group); return group; } + private int toTrackType(String trackTypeString) { + switch (Util.toLowerInvariant(trackTypeString)) { + case "audio": + return C.TRACK_TYPE_AUDIO; + case "video": + return C.TRACK_TYPE_VIDEO; + default: + throw new IllegalArgumentException("Invalid track type: " + trackTypeString); + } + } } private final class SampleAdapter extends BaseExpandableListAdapter implements OnClickListener { - private List sampleGroups; + private List playlistGroups; public SampleAdapter() { - sampleGroups = Collections.emptyList(); + playlistGroups = Collections.emptyList(); } - public void setSampleGroups(List sampleGroups) { - this.sampleGroups = sampleGroups; + public void setPlaylistGroups(List playlistGroups) { + this.playlistGroups = playlistGroups; notifyDataSetChanged(); } @Override - public Sample getChild(int groupPosition, int childPosition) { - return getGroup(groupPosition).samples.get(childPosition); + public PlaylistHolder getChild(int groupPosition, int childPosition) { + return getGroup(groupPosition).playlists.get(childPosition); } @Override @@ -451,8 +521,12 @@ public class SampleChooserActivity extends AppCompatActivity } @Override - public View getChildView(int groupPosition, int childPosition, boolean isLastChild, - View convertView, ViewGroup parent) { + public View getChildView( + int groupPosition, + int childPosition, + boolean isLastChild, + View convertView, + ViewGroup parent) { View view = convertView; if (view == null) { view = getLayoutInflater().inflate(R.layout.sample_list_item, parent, false); @@ -466,12 +540,12 @@ public class SampleChooserActivity extends AppCompatActivity @Override public int getChildrenCount(int groupPosition) { - return getGroup(groupPosition).samples.size(); + return getGroup(groupPosition).playlists.size(); } @Override - public SampleGroup getGroup(int groupPosition) { - return sampleGroups.get(groupPosition); + public PlaylistGroup getGroup(int groupPosition) { + return playlistGroups.get(groupPosition); } @Override @@ -480,8 +554,8 @@ public class SampleChooserActivity extends AppCompatActivity } @Override - public View getGroupView(int groupPosition, boolean isExpanded, View convertView, - ViewGroup parent) { + public View getGroupView( + int groupPosition, boolean isExpanded, View convertView, ViewGroup parent) { View view = convertView; if (view == null) { view = @@ -494,7 +568,7 @@ public class SampleChooserActivity extends AppCompatActivity @Override public int getGroupCount() { - return sampleGroups.size(); + return playlistGroups.size(); } @Override @@ -509,18 +583,19 @@ public class SampleChooserActivity extends AppCompatActivity @Override public void onClick(View view) { - onSampleDownloadButtonClicked((Sample) view.getTag()); + onSampleDownloadButtonClicked((PlaylistHolder) view.getTag()); } - private void initializeChildView(View view, Sample sample) { - view.setTag(sample); + private void initializeChildView(View view, PlaylistHolder playlistHolder) { + view.setTag(playlistHolder); TextView sampleTitle = view.findViewById(R.id.sample_title); - sampleTitle.setText(sample.name); + sampleTitle.setText(playlistHolder.title); - boolean canDownload = getDownloadUnsupportedStringId(sample) == 0; - boolean isDownloaded = canDownload && downloadTracker.isDownloaded(((UriSample) sample).uri); + boolean canDownload = getDownloadUnsupportedStringId(playlistHolder) == 0; + boolean isDownloaded = + canDownload && downloadTracker.isDownloaded(playlistHolder.mediaItems.get(0)); ImageButton downloadButton = view.findViewById(R.id.download_button); - downloadButton.setTag(sample); + downloadButton.setTag(playlistHolder); downloadButton.setColorFilter( canDownload ? (isDownloaded ? 0xFF42A5F5 : 0xFFBDBDBD) : 0xFF666666); downloadButton.setImageResource( @@ -528,15 +603,26 @@ public class SampleChooserActivity extends AppCompatActivity } } - private static final class SampleGroup { + private static final class PlaylistHolder { public final String title; - public final List samples; + public final List mediaItems; - public SampleGroup(String title) { + private PlaylistHolder(String title, List mediaItems) { + Assertions.checkArgument(!mediaItems.isEmpty()); this.title = title; - this.samples = new ArrayList<>(); + this.mediaItems = Collections.unmodifiableList(new ArrayList<>(mediaItems)); } + } + private static final class PlaylistGroup { + + public final String title; + public final List playlists; + + public PlaylistGroup(String title) { + this.title = title; + this.playlists = new ArrayList<>(); + } } } diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/TrackSelectionDialog.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/TrackSelectionDialog.java index 9e8009388e..b1db44110d 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/TrackSelectionDialog.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/TrackSelectionDialog.java @@ -24,6 +24,7 @@ import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.Button; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AppCompatDialog; import androidx.fragment.app.DialogFragment; @@ -212,6 +213,7 @@ public final class TrackSelectionDialog extends DialogFragment { } @Override + @NonNull public Dialog onCreateDialog(Bundle savedInstanceState) { // We need to own the view to let tab layout work correctly on all API levels. We can't use // AlertDialog because it owns the view itself, so we use AppCompatDialog instead, themed using @@ -223,16 +225,14 @@ public final class TrackSelectionDialog extends DialogFragment { } @Override - public void onDismiss(DialogInterface dialog) { + public void onDismiss(@NonNull DialogInterface dialog) { super.onDismiss(dialog); onDismissListener.onDismiss(dialog); } - @Nullable @Override public View onCreateView( LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { - View dialogView = inflater.inflate(R.layout.track_selection_dialog, container, false); TabLayout tabLayout = dialogView.findViewById(R.id.track_selection_dialog_tab_layout); ViewPager viewPager = dialogView.findViewById(R.id.track_selection_dialog_view_pager); @@ -290,6 +290,7 @@ public final class TrackSelectionDialog extends DialogFragment { } @Override + @NonNull public Fragment getItem(int position) { return tabFragments.valueAt(position); } @@ -299,7 +300,6 @@ public final class TrackSelectionDialog extends DialogFragment { return tabFragments.size(); } - @Nullable @Override public CharSequence getPageTitle(int position) { return getTrackTypeString(getResources(), tabTrackTypes.get(position)); @@ -341,7 +341,6 @@ public final class TrackSelectionDialog extends DialogFragment { this.allowMultipleOverrides = allowMultipleOverrides; } - @Nullable @Override public View onCreateView( LayoutInflater inflater, @@ -360,7 +359,8 @@ public final class TrackSelectionDialog extends DialogFragment { } @Override - public void onTrackSelectionChanged(boolean isDisabled, List overrides) { + public void onTrackSelectionChanged( + boolean isDisabled, @NonNull List overrides) { this.isDisabled = isDisabled; this.overrides = overrides; } diff --git a/demos/surface/src/main/java/com/google/android/exoplayer2/surfacedemo/MainActivity.java b/demos/surface/src/main/java/com/google/android/exoplayer2/surfacedemo/MainActivity.java index 99bc0d7abc..67419edf3b 100644 --- a/demos/surface/src/main/java/com/google/android/exoplayer2/surfacedemo/MainActivity.java +++ b/demos/surface/src/main/java/com/google/android/exoplayer2/surfacedemo/MainActivity.java @@ -32,7 +32,6 @@ import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.drm.DefaultDrmSessionManager; import com.google.android.exoplayer2.drm.DrmSessionManager; -import com.google.android.exoplayer2.drm.ExoMediaCrypto; import com.google.android.exoplayer2.drm.FrameworkMediaDrm; import com.google.android.exoplayer2.drm.HttpMediaDrmCallback; import com.google.android.exoplayer2.source.MediaSource; @@ -185,7 +184,7 @@ public final class MainActivity extends Activity { ? Assertions.checkNotNull(intent.getData()) : Uri.parse(DEFAULT_MEDIA_URI); String userAgent = Util.getUserAgent(this, getString(R.string.application_name)); - DrmSessionManager drmSessionManager; + DrmSessionManager drmSessionManager; if (intent.hasExtra(DRM_SCHEME_EXTRA)) { String drmScheme = Assertions.checkNotNull(intent.getStringExtra(DRM_SCHEME_EXTRA)); String drmLicenseUrl = Assertions.checkNotNull(intent.getStringExtra(DRM_LICENSE_URL_EXTRA)); @@ -220,8 +219,9 @@ public final class MainActivity extends Activity { throw new IllegalStateException(); } SimpleExoPlayer player = new SimpleExoPlayer.Builder(getApplicationContext()).build(); - player.prepare(mediaSource); - player.setPlayWhenReady(true); + player.setMediaSource(mediaSource); + player.prepare(); + player.play(); player.setRepeatMode(Player.REPEAT_MODE_ALL); surfaceControl = diff --git a/extensions/av1/README.md b/extensions/av1/README.md index 276daae4e2..54e27a3b87 100644 --- a/extensions/av1/README.md +++ b/extensions/av1/README.md @@ -96,6 +96,14 @@ a custom track selector the choice of `Renderer` is up to your implementation. You need to make sure you are passing a `Libgav1VideoRenderer` to the player and then you need to implement your own logic to use the renderer for a given track. +## Using the extension in the demo application ## + +To try out playback using the extension in the [demo application][], see +[enabling extension decoders][]. + +[demo application]: https://exoplayer.dev/demo-application.html +[enabling extension decoders]: https://exoplayer.dev/demo-application.html#enabling-extension-decoders + ## Rendering options ## There are two possibilities for rendering the output `Libgav1VideoRenderer` diff --git a/extensions/av1/build.gradle b/extensions/av1/build.gradle index 0b539d551b..d61a3a97f8 100644 --- a/extensions/av1/build.gradle +++ b/extensions/av1/build.gradle @@ -65,6 +65,7 @@ if (project.file('src/main/jni/libgav1').exists()) { dependencies { implementation project(modulePrefix + 'library-core') implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion + compileOnly 'org.jetbrains.kotlin:kotlin-annotations-jvm:' + kotlinAnnotationsVersion } ext { diff --git a/extensions/av1/src/main/java/com/google/android/exoplayer2/ext/av1/Gav1Decoder.java b/extensions/av1/src/main/java/com/google/android/exoplayer2/ext/av1/Gav1Decoder.java index 687ac47f2a..840cd158f9 100644 --- a/extensions/av1/src/main/java/com/google/android/exoplayer2/ext/av1/Gav1Decoder.java +++ b/extensions/av1/src/main/java/com/google/android/exoplayer2/ext/av1/Gav1Decoder.java @@ -15,6 +15,8 @@ */ package com.google.android.exoplayer2.ext.av1; +import static java.lang.Runtime.getRuntime; + import android.view.Surface; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; @@ -44,7 +46,9 @@ import java.nio.ByteBuffer; * @param numInputBuffers Number of input buffers. * @param numOutputBuffers Number of output buffers. * @param initialInputBufferSize The initial size of each input buffer, in bytes. - * @param threads Number of threads libgav1 will use to decode. + * @param threads Number of threads libgav1 will use to decode. If {@link + * Libgav1VideoRenderer#THREAD_COUNT_AUTODETECT} is passed, then this class will auto detect + * the number of threads to be used. * @throws Gav1DecoderException Thrown if an exception occurs when initializing the decoder. */ public Gav1Decoder( @@ -56,6 +60,16 @@ import java.nio.ByteBuffer; if (!Gav1Library.isAvailable()) { throw new Gav1DecoderException("Failed to load decoder native library."); } + + if (threads == Libgav1VideoRenderer.THREAD_COUNT_AUTODETECT) { + // Try to get the optimal number of threads from the AV1 heuristic. + threads = gav1GetThreads(); + if (threads <= 0) { + // If that is not available, default to the number of available processors. + threads = getRuntime().availableProcessors(); + } + } + gav1DecoderContext = gav1Init(threads); if (gav1DecoderContext == GAV1_ERROR || gav1CheckError(gav1DecoderContext) == GAV1_ERROR) { throw new Gav1DecoderException( @@ -88,8 +102,8 @@ import java.nio.ByteBuffer; return new VideoDecoderOutputBuffer(this::releaseOutputBuffer); } - @Nullable @Override + @Nullable protected Gav1DecoderException decode( VideoDecoderInputBuffer inputBuffer, VideoDecoderOutputBuffer outputBuffer, boolean reset) { ByteBuffer inputData = Util.castNonNull(inputBuffer.data); @@ -203,7 +217,7 @@ import java.nio.ByteBuffer; * @param context Decoder context. * @param surface Output surface. * @param outputBuffer Output buffer with the decoded frame. - * @return {@link #GAV1_OK} if successful, {@link #GAV1_ERROR} if an error occured. + * @return {@link #GAV1_OK} if successful, {@link #GAV1_ERROR} if an error occurred. */ private native int gav1RenderFrame( long context, Surface surface, VideoDecoderOutputBuffer outputBuffer); @@ -225,10 +239,17 @@ import java.nio.ByteBuffer; private native String gav1GetErrorMessage(long context); /** - * Returns whether an error occured. + * Returns whether an error occurred. * * @param context Decoder context. - * @return {@link #GAV1_OK} if there was no error, {@link #GAV1_ERROR} if an error occured. + * @return {@link #GAV1_OK} if there was no error, {@link #GAV1_ERROR} if an error occurred. */ private native int gav1CheckError(long context); + + /** + * Returns the optimal number of threads to be used for AV1 decoding. + * + * @return Optimal number of threads if there was no error, 0 if an error occurred. + */ + private native int gav1GetThreads(); } diff --git a/extensions/av1/src/main/java/com/google/android/exoplayer2/ext/av1/Gav1DecoderException.java b/extensions/av1/src/main/java/com/google/android/exoplayer2/ext/av1/Gav1DecoderException.java index 9d8692c581..13839f0ceb 100644 --- a/extensions/av1/src/main/java/com/google/android/exoplayer2/ext/av1/Gav1DecoderException.java +++ b/extensions/av1/src/main/java/com/google/android/exoplayer2/ext/av1/Gav1DecoderException.java @@ -15,10 +15,10 @@ */ package com.google.android.exoplayer2.ext.av1; -import com.google.android.exoplayer2.video.VideoDecoderException; +import com.google.android.exoplayer2.decoder.DecoderException; /** Thrown when a libgav1 decoder error occurs. */ -public final class Gav1DecoderException extends VideoDecoderException { +public final class Gav1DecoderException extends DecoderException { /* package */ Gav1DecoderException(String message) { super(message); diff --git a/extensions/av1/src/main/java/com/google/android/exoplayer2/ext/av1/Libgav1VideoRenderer.java b/extensions/av1/src/main/java/com/google/android/exoplayer2/ext/av1/Libgav1VideoRenderer.java index 81cfec29fd..c07a590c68 100644 --- a/extensions/av1/src/main/java/com/google/android/exoplayer2/ext/av1/Libgav1VideoRenderer.java +++ b/extensions/av1/src/main/java/com/google/android/exoplayer2/ext/av1/Libgav1VideoRenderer.java @@ -15,45 +15,31 @@ */ package com.google.android.exoplayer2.ext.av1; -import static java.lang.Runtime.getRuntime; - import android.os.Handler; import android.view.Surface; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.ExoPlaybackException; -import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.Format; -import com.google.android.exoplayer2.PlayerMessage.Target; -import com.google.android.exoplayer2.decoder.SimpleDecoder; -import com.google.android.exoplayer2.drm.DrmSessionManager; +import com.google.android.exoplayer2.RendererCapabilities; import com.google.android.exoplayer2.drm.ExoMediaCrypto; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.TraceUtil; import com.google.android.exoplayer2.util.Util; -import com.google.android.exoplayer2.video.SimpleDecoderVideoRenderer; -import com.google.android.exoplayer2.video.VideoDecoderException; -import com.google.android.exoplayer2.video.VideoDecoderInputBuffer; +import com.google.android.exoplayer2.video.DecoderVideoRenderer; import com.google.android.exoplayer2.video.VideoDecoderOutputBuffer; -import com.google.android.exoplayer2.video.VideoDecoderOutputBufferRenderer; import com.google.android.exoplayer2.video.VideoRendererEventListener; -/** - * Decodes and renders video using libgav1 decoder. - * - *

This renderer accepts the following messages sent via {@link ExoPlayer#createMessage(Target)} - * on the playback thread: - * - *

    - *
  • Message with type {@link C#MSG_SET_SURFACE} to set the output surface. The message payload - * should be the target {@link Surface}, or null. - *
  • Message with type {@link C#MSG_SET_VIDEO_DECODER_OUTPUT_BUFFER_RENDERER} to set the output - * buffer renderer. The message payload should be the target {@link - * VideoDecoderOutputBufferRenderer}, or null. - *
- */ -public class Libgav1VideoRenderer extends SimpleDecoderVideoRenderer { +/** Decodes and renders video using libgav1 decoder. */ +public class Libgav1VideoRenderer extends DecoderVideoRenderer { + /** + * Attempts to use as many threads as performance processors available on the device. If the + * number of performance processors cannot be detected, the number of available processors is + * used. + */ + public static final int THREAD_COUNT_AUTODETECT = 0; + + private static final String TAG = "Libgav1VideoRenderer"; private static final int DEFAULT_NUM_OF_INPUT_BUFFERS = 4; private static final int DEFAULT_NUM_OF_OUTPUT_BUFFERS = 4; /* Default size based on 720p resolution video compressed by a factor of two. */ @@ -73,7 +59,7 @@ public class Libgav1VideoRenderer extends SimpleDecoderVideoRenderer { @Nullable private Gav1Decoder decoder; /** - * Creates a Libgav1VideoRenderer. + * Creates a new instance. * * @param allowedJoiningTimeMs The maximum duration in milliseconds for which this video renderer * can attempt to seamlessly join an ongoing playback. @@ -93,13 +79,13 @@ public class Libgav1VideoRenderer extends SimpleDecoderVideoRenderer { eventHandler, eventListener, maxDroppedFramesToNotify, - /* threads= */ getRuntime().availableProcessors(), + THREAD_COUNT_AUTODETECT, DEFAULT_NUM_OF_INPUT_BUFFERS, DEFAULT_NUM_OF_OUTPUT_BUFFERS); } /** - * Creates a Libgav1VideoRenderer. + * Creates a new instance. * * @param allowedJoiningTimeMs The maximum duration in milliseconds for which this video renderer * can attempt to seamlessly join an ongoing playback. @@ -108,7 +94,9 @@ public class Libgav1VideoRenderer extends SimpleDecoderVideoRenderer { * @param eventListener A listener of events. May be null if delivery of events is not required. * @param maxDroppedFramesToNotify The maximum number of frames that can be dropped between * invocations of {@link VideoRendererEventListener#onDroppedFrames(int, long)}. - * @param threads Number of threads libgav1 will use to decode. + * @param threads Number of threads libgav1 will use to decode. If {@link + * #THREAD_COUNT_AUTODETECT} is passed, then the number of threads to use is autodetected + * based on CPU capabilities. * @param numInputBuffers Number of input buffers. * @param numOutputBuffers Number of output buffers. */ @@ -120,38 +108,33 @@ public class Libgav1VideoRenderer extends SimpleDecoderVideoRenderer { int threads, int numInputBuffers, int numOutputBuffers) { - super( - allowedJoiningTimeMs, - eventHandler, - eventListener, - maxDroppedFramesToNotify, - /* drmSessionManager= */ null, - /* playClearSamplesWithoutKeys= */ false); + super(allowedJoiningTimeMs, eventHandler, eventListener, maxDroppedFramesToNotify); this.threads = threads; this.numInputBuffers = numInputBuffers; this.numOutputBuffers = numOutputBuffers; } @Override - protected int supportsFormatInternal( - @Nullable DrmSessionManager drmSessionManager, Format format) { - if (!MimeTypes.VIDEO_AV1.equalsIgnoreCase(format.sampleMimeType) - || !Gav1Library.isAvailable()) { - return FORMAT_UNSUPPORTED_TYPE; - } - if (!supportsFormatDrm(drmSessionManager, format.drmInitData)) { - return FORMAT_UNSUPPORTED_DRM; - } - return FORMAT_HANDLED | ADAPTIVE_SEAMLESS; + public String getName() { + return TAG; } @Override - protected SimpleDecoder< - VideoDecoderInputBuffer, - ? extends VideoDecoderOutputBuffer, - ? extends VideoDecoderException> - createDecoder(Format format, @Nullable ExoMediaCrypto mediaCrypto) - throws VideoDecoderException { + @Capabilities + public final int supportsFormat(Format format) { + if (!MimeTypes.VIDEO_AV1.equalsIgnoreCase(format.sampleMimeType) + || !Gav1Library.isAvailable()) { + return RendererCapabilities.create(FORMAT_UNSUPPORTED_TYPE); + } + if (format.drmInitData != null && format.exoMediaCryptoType == null) { + return RendererCapabilities.create(FORMAT_UNSUPPORTED_DRM); + } + return RendererCapabilities.create(FORMAT_HANDLED, ADAPTIVE_SEAMLESS, TUNNELING_NOT_SUPPORTED); + } + + @Override + protected Gav1Decoder createDecoder(Format format, @Nullable ExoMediaCrypto mediaCrypto) + throws Gav1DecoderException { TraceUtil.beginSection("createGav1Decoder"); int initialInputBufferSize = format.maxInputSize != Format.NO_VALUE ? format.maxInputSize : DEFAULT_INPUT_BUFFER_SIZE; @@ -180,16 +163,8 @@ public class Libgav1VideoRenderer extends SimpleDecoderVideoRenderer { } } - // PlayerMessage.Target implementation. - @Override - public void handleMessage(int messageType, @Nullable Object message) throws ExoPlaybackException { - if (messageType == C.MSG_SET_SURFACE) { - setOutputSurface((Surface) message); - } else if (messageType == C.MSG_SET_VIDEO_DECODER_OUTPUT_BUFFER_RENDERER) { - setOutputBufferRenderer((VideoDecoderOutputBufferRenderer) message); - } else { - super.handleMessage(messageType, message); - } + protected boolean canKeepCodec(Format oldFormat, Format newFormat) { + return true; } } diff --git a/extensions/av1/src/main/jni/CMakeLists.txt b/extensions/av1/src/main/jni/CMakeLists.txt index abd8764e0f..075773a70e 100644 --- a/extensions/av1/src/main/jni/CMakeLists.txt +++ b/extensions/av1/src/main/jni/CMakeLists.txt @@ -11,9 +11,15 @@ project(libgav1JNI C CXX) # armeabi-v7a build. This flag enables it. if(${ANDROID_ABI} MATCHES "armeabi-v7a") add_compile_options("-mfpu=neon") + add_compile_options("-marm") add_compile_options("-fPIC") endif() +string(TOLOWER "${CMAKE_BUILD_TYPE}" build_type) +if(build_type MATCHES "^rel") + add_compile_options("-O2") +endif() + set(libgav1_jni_root "${CMAKE_CURRENT_SOURCE_DIR}") set(libgav1_jni_build "${CMAKE_BINARY_DIR}") set(libgav1_jni_output_directory @@ -38,7 +44,9 @@ add_subdirectory("${libgav1_root}" # Build libgav1JNI. add_library(gav1JNI SHARED - gav1_jni.cc) + gav1_jni.cc + cpu_info.cc + cpu_info.h) # Locate NDK log library. find_library(android_log_lib log) diff --git a/extensions/av1/src/main/jni/cpu_info.cc b/extensions/av1/src/main/jni/cpu_info.cc new file mode 100644 index 0000000000..8f4a405f4f --- /dev/null +++ b/extensions/av1/src/main/jni/cpu_info.cc @@ -0,0 +1,153 @@ +#include "cpu_info.h" // NOLINT + +#include + +#include +#include +#include +#include +#include + +namespace gav1_jni { +namespace { + +// Note: The code in this file needs to use the 'long' type because it is the +// return type of the Standard C Library function strtol(). The linter warnings +// are suppressed with NOLINT comments since they are integers at runtime. + +// Returns the number of online processor cores. +int GetNumberOfProcessorsOnline() { + // See https://developer.android.com/ndk/guides/cpu-features. + long num_cpus = sysconf(_SC_NPROCESSORS_ONLN); // NOLINT + if (num_cpus < 0) { + return 0; + } + // It is safe to cast num_cpus to int. sysconf(_SC_NPROCESSORS_ONLN) returns + // the return value of get_nprocs(), which is an int. + return static_cast(num_cpus); +} + +} // namespace + +// These CPUs support heterogeneous multiprocessing. +#if defined(__arm__) || defined(__aarch64__) + +// A helper function used by GetNumberOfPerformanceCoresOnline(). +// +// Returns the cpuinfo_max_freq value (in kHz) of the given CPU. Returns 0 on +// failure. +long GetCpuinfoMaxFreq(int cpu_index) { // NOLINT + char buffer[128]; + const int rv = snprintf( + buffer, sizeof(buffer), + "/sys/devices/system/cpu/cpu%d/cpufreq/cpuinfo_max_freq", cpu_index); + if (rv < 0 || rv >= sizeof(buffer)) { + return 0; + } + FILE* file = fopen(buffer, "r"); + if (file == nullptr) { + return 0; + } + char* const str = fgets(buffer, sizeof(buffer), file); + fclose(file); + if (str == nullptr) { + return 0; + } + const long freq = strtol(str, nullptr, 10); // NOLINT + if (freq <= 0 || freq == LONG_MAX) { + return 0; + } + return freq; +} + +// Returns the number of performance CPU cores that are online. The number of +// efficiency CPU cores is subtracted from the total number of CPU cores. Uses +// cpuinfo_max_freq to determine whether a CPU is a performance core or an +// efficiency core. +// +// This function is not perfect. For example, the Snapdragon 632 SoC used in +// Motorola Moto G7 has performance and efficiency cores with the same +// cpuinfo_max_freq but different cpuinfo_min_freq. This function fails to +// differentiate the two kinds of cores and reports all the cores as +// performance cores. +int GetNumberOfPerformanceCoresOnline() { + // Get the online CPU list. Some examples of the online CPU list are: + // "0-7" + // "0" + // "0-1,2,3,4-7" + FILE* file = fopen("/sys/devices/system/cpu/online", "r"); + if (file == nullptr) { + return 0; + } + char online[512]; + char* const str = fgets(online, sizeof(online), file); + fclose(file); + file = nullptr; + if (str == nullptr) { + return 0; + } + + // Count the number of the slowest CPUs. Some SoCs such as Snapdragon 855 + // have performance cores with different max frequencies, so only the slowest + // CPUs are efficiency cores. If we count the number of the fastest CPUs, we + // will fail to count the second fastest performance cores. + long slowest_cpu_freq = LONG_MAX; // NOLINT + int num_slowest_cpus = 0; + int num_cpus = 0; + const char* cp = online; + int range_begin = -1; + while (true) { + char* str_end; + const int cpu = static_cast(strtol(cp, &str_end, 10)); // NOLINT + if (str_end == cp) { + break; + } + cp = str_end; + if (*cp == '-') { + range_begin = cpu; + } else { + if (range_begin == -1) { + range_begin = cpu; + } + + num_cpus += cpu - range_begin + 1; + for (int i = range_begin; i <= cpu; ++i) { + const long freq = GetCpuinfoMaxFreq(i); // NOLINT + if (freq <= 0) { + return 0; + } + if (freq < slowest_cpu_freq) { + slowest_cpu_freq = freq; + num_slowest_cpus = 0; + } + if (freq == slowest_cpu_freq) { + ++num_slowest_cpus; + } + } + + range_begin = -1; + } + if (*cp == '\0') { + break; + } + ++cp; + } + + // If there are faster CPU cores than the slowest CPU cores, exclude the + // slowest CPU cores. + if (num_slowest_cpus < num_cpus) { + num_cpus -= num_slowest_cpus; + } + return num_cpus; +} + +#else + +// Assume symmetric multiprocessing. +int GetNumberOfPerformanceCoresOnline() { + return GetNumberOfProcessorsOnline(); +} + +#endif + +} // namespace gav1_jni diff --git a/extensions/av1/src/main/jni/cpu_info.h b/extensions/av1/src/main/jni/cpu_info.h new file mode 100644 index 0000000000..77f869a93e --- /dev/null +++ b/extensions/av1/src/main/jni/cpu_info.h @@ -0,0 +1,13 @@ +#ifndef EXOPLAYER_V2_EXTENSIONS_AV1_SRC_MAIN_JNI_CPU_INFO_H_ +#define EXOPLAYER_V2_EXTENSIONS_AV1_SRC_MAIN_JNI_CPU_INFO_H_ + +namespace gav1_jni { + +// Returns the number of performance cores that are available for AV1 decoding. +// This is a heuristic that works on most common android devices. Returns 0 on +// error or if the number of performance cores cannot be determined. +int GetNumberOfPerformanceCoresOnline(); + +} // namespace gav1_jni + +#endif // EXOPLAYER_V2_EXTENSIONS_AV1_SRC_MAIN_JNI_CPU_INFO_H_ diff --git a/extensions/av1/src/main/jni/gav1_jni.cc b/extensions/av1/src/main/jni/gav1_jni.cc index 9ac3ea5cd2..6b25798e3f 100644 --- a/extensions/av1/src/main/jni/gav1_jni.cc +++ b/extensions/av1/src/main/jni/gav1_jni.cc @@ -27,10 +27,12 @@ #endif // CPU_FEATURES_COMPILED_ANY_ARM_NEON #include +#include #include #include // NOLINT #include +#include "cpu_info.h" // NOLINT #include "gav1/decoder.h" #define LOG_TAG "gav1_jni" @@ -71,7 +73,7 @@ const int kImageFormatYV12 = 0x32315659; // Output modes. const int kOutputModeYuv = 0; const int kOutputModeSurfaceYuv = 1; -// LINT.ThenChange(../../../../../library/core/src/main/java/com/google/android/exoplayer2/C.java) +// LINT.ThenChange(../../../../../library/common/src/main/java/com/google/android/exoplayer2/C.java) // LINT.IfChange const int kColorSpaceUnknown = 0; @@ -121,18 +123,22 @@ const char* GetJniErrorMessage(JniStatusCode error_code) { } } -// Manages Libgav1FrameBuffer and reference information. +// Manages frame buffer and reference information. class JniFrameBuffer { public: - explicit JniFrameBuffer(int id) : id_(id), reference_count_(0) { - gav1_frame_buffer_.private_data = &id_; - } + explicit JniFrameBuffer(int id) : id_(id), reference_count_(0) {} ~JniFrameBuffer() { for (int plane_index = kPlaneY; plane_index < kMaxPlanes; plane_index++) { - delete[] gav1_frame_buffer_.data[plane_index]; + delete[] raw_buffer_[plane_index]; } } + // Not copyable or movable. + JniFrameBuffer(const JniFrameBuffer&) = delete; + JniFrameBuffer(JniFrameBuffer&&) = delete; + JniFrameBuffer& operator=(const JniFrameBuffer&) = delete; + JniFrameBuffer& operator=(JniFrameBuffer&&) = delete; + void SetFrameData(const libgav1::DecoderBuffer& decoder_buffer) { for (int plane_index = kPlaneY; plane_index < decoder_buffer.NumPlanes(); plane_index++) { @@ -160,9 +166,8 @@ class JniFrameBuffer { void RemoveReference() { reference_count_--; } bool InUse() const { return reference_count_ != 0; } - const Libgav1FrameBuffer& GetGav1FrameBuffer() const { - return gav1_frame_buffer_; - } + uint8_t* RawBuffer(int plane_index) const { return raw_buffer_[plane_index]; } + void* BufferPrivateData() const { return const_cast(&id_); } // Attempts to reallocate data planes if the existing ones don't have enough // capacity. Returns true if the allocation was successful or wasn't needed, @@ -172,15 +177,14 @@ class JniFrameBuffer { for (int plane_index = kPlaneY; plane_index < kMaxPlanes; plane_index++) { const int min_size = (plane_index == kPlaneY) ? y_plane_min_size : uv_plane_min_size; - if (gav1_frame_buffer_.size[plane_index] >= min_size) continue; - delete[] gav1_frame_buffer_.data[plane_index]; - gav1_frame_buffer_.data[plane_index] = - new (std::nothrow) uint8_t[min_size]; - if (!gav1_frame_buffer_.data[plane_index]) { - gav1_frame_buffer_.size[plane_index] = 0; + if (raw_buffer_size_[plane_index] >= min_size) continue; + delete[] raw_buffer_[plane_index]; + raw_buffer_[plane_index] = new (std::nothrow) uint8_t[min_size]; + if (!raw_buffer_[plane_index]) { + raw_buffer_size_[plane_index] = 0; return false; } - gav1_frame_buffer_.size[plane_index] = min_size; + raw_buffer_size_[plane_index] = min_size; } return true; } @@ -190,9 +194,12 @@ class JniFrameBuffer { uint8_t* plane_[kMaxPlanes]; int displayed_width_[kMaxPlanes]; int displayed_height_[kMaxPlanes]; - int id_; + const int id_; int reference_count_; - Libgav1FrameBuffer gav1_frame_buffer_ = {}; + // Pointers to the raw buffers allocated for the data planes. + uint8_t* raw_buffer_[kMaxPlanes] = {}; + // Sizes of the raw buffers in bytes. + size_t raw_buffer_size_[kMaxPlanes] = {}; }; // Manages frame buffers used by libgav1 decoder and ExoPlayer. @@ -210,7 +217,7 @@ class JniBufferManager { } JniStatusCode GetBuffer(size_t y_plane_min_size, size_t uv_plane_min_size, - Libgav1FrameBuffer* frame_buffer) { + JniFrameBuffer** jni_buffer) { std::lock_guard lock(mutex_); JniFrameBuffer* output_buffer; @@ -230,7 +237,7 @@ class JniBufferManager { } output_buffer->AddReference(); - *frame_buffer = output_buffer->GetGav1FrameBuffer(); + *jni_buffer = output_buffer; return kJniStatusOk; } @@ -316,29 +323,46 @@ struct JniContext { JniStatusCode jni_status_code = kJniStatusOk; }; -int Libgav1GetFrameBuffer(void* private_data, size_t y_plane_min_size, - size_t uv_plane_min_size, - Libgav1FrameBuffer* frame_buffer) { - JniContext* const context = reinterpret_cast(private_data); +Libgav1StatusCode Libgav1GetFrameBuffer(void* callback_private_data, + int bitdepth, + libgav1::ImageFormat image_format, + int width, int height, int left_border, + int right_border, int top_border, + int bottom_border, int stride_alignment, + libgav1::FrameBuffer* frame_buffer) { + libgav1::FrameBufferInfo info; + Libgav1StatusCode status = libgav1::ComputeFrameBufferInfo( + bitdepth, image_format, width, height, left_border, right_border, + top_border, bottom_border, stride_alignment, &info); + if (status != kLibgav1StatusOk) return status; + + JniContext* const context = static_cast(callback_private_data); + JniFrameBuffer* jni_buffer; context->jni_status_code = context->buffer_manager.GetBuffer( - y_plane_min_size, uv_plane_min_size, frame_buffer); + info.y_buffer_size, info.uv_buffer_size, &jni_buffer); if (context->jni_status_code != kJniStatusOk) { LOGE("%s", GetJniErrorMessage(context->jni_status_code)); - return -1; + return kLibgav1StatusOutOfMemory; } - return 0; + + uint8_t* const y_buffer = jni_buffer->RawBuffer(0); + uint8_t* const u_buffer = + (info.uv_buffer_size != 0) ? jni_buffer->RawBuffer(1) : nullptr; + uint8_t* const v_buffer = + (info.uv_buffer_size != 0) ? jni_buffer->RawBuffer(2) : nullptr; + + return libgav1::SetFrameBuffer(&info, y_buffer, u_buffer, v_buffer, + jni_buffer->BufferPrivateData(), frame_buffer); } -int Libgav1ReleaseFrameBuffer(void* private_data, - Libgav1FrameBuffer* frame_buffer) { - JniContext* const context = reinterpret_cast(private_data); - const int buffer_id = *reinterpret_cast(frame_buffer->private_data); +void Libgav1ReleaseFrameBuffer(void* callback_private_data, + void* buffer_private_data) { + JniContext* const context = static_cast(callback_private_data); + const int buffer_id = *static_cast(buffer_private_data); context->jni_status_code = context->buffer_manager.ReleaseBuffer(buffer_id); if (context->jni_status_code != kJniStatusOk) { LOGE("%s", GetJniErrorMessage(context->jni_status_code)); - return -1; } - return 0; } constexpr int AlignTo16(int value) { return (value + 15) & (~15); } @@ -508,8 +532,8 @@ DECODER_FUNC(jlong, gav1Init, jint threads) { libgav1::DecoderSettings settings; settings.threads = threads; - settings.get = Libgav1GetFrameBuffer; - settings.release = Libgav1ReleaseFrameBuffer; + settings.get_frame_buffer = Libgav1GetFrameBuffer; + settings.release_frame_buffer = Libgav1ReleaseFrameBuffer; settings.callback_private_data = context; context->libgav1_status_code = context->decoder.Init(&settings); @@ -544,7 +568,8 @@ DECODER_FUNC(jint, gav1Decode, jlong jContext, jobject encodedData, const uint8_t* const buffer = reinterpret_cast( env->GetDirectBufferAddress(encodedData)); context->libgav1_status_code = - context->decoder.EnqueueFrame(buffer, length, /*user_private_data=*/0); + context->decoder.EnqueueFrame(buffer, length, /*user_private_data=*/0, + /*buffer_private_data=*/nullptr); if (context->libgav1_status_code != kLibgav1StatusOk) { return kStatusError; } @@ -619,7 +644,7 @@ DECODER_FUNC(jint, gav1GetFrame, jlong jContext, jobject jOutputBuffer, } const int buffer_id = - *reinterpret_cast(decoder_buffer->buffer_private_data); + *static_cast(decoder_buffer->buffer_private_data); context->buffer_manager.AddBufferReference(buffer_id); JniFrameBuffer* const jni_buffer = context->buffer_manager.GetBuffer(buffer_id); @@ -750,5 +775,9 @@ DECODER_FUNC(jint, gav1CheckError, jlong jContext) { return kStatusOk; } +DECODER_FUNC(jint, gav1GetThreads) { + return gav1_jni::GetNumberOfPerformanceCoresOnline(); +} + // TODO(b/139902005): Add functions for getting libgav1 version and build // configuration once libgav1 ABI provides this information. diff --git a/extensions/cast/build.gradle b/extensions/cast/build.gradle index 0d7d96db4c..853861e4ad 100644 --- a/extensions/cast/build.gradle +++ b/extensions/cast/build.gradle @@ -31,12 +31,13 @@ android { } dependencies { - api 'com.google.android.gms:play-services-cast-framework:17.0.0' + api 'com.google.android.gms:play-services-cast-framework:18.1.0' implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion implementation project(modulePrefix + 'library-core') implementation project(modulePrefix + 'library-ui') compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion compileOnly 'org.checkerframework:checker-compat-qual:' + checkerframeworkVersion + compileOnly 'org.jetbrains.kotlin:kotlin-annotations-jvm:' + kotlinAnnotationsVersion testImplementation project(modulePrefix + 'testutils') testImplementation 'org.robolectric:robolectric:' + robolectricVersion } 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 c198b49777..835d6a33fc 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 @@ -21,6 +21,7 @@ import com.google.android.exoplayer2.BasePlayer; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.ExoPlayerLibraryInfo; +import com.google.android.exoplayer2.MediaItem; import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Timeline; @@ -83,6 +84,7 @@ public final class CastPlayer extends BasePlayer { private static final long[] EMPTY_TRACK_ID_ARRAY = new long[0]; private final CastContext castContext; + private final MediaItemConverter mediaItemConverter; // TODO: Allow custom implementations of CastTimelineTracker. private final CastTimelineTracker timelineTracker; private final Timeline.Period period; @@ -110,13 +112,25 @@ public final class CastPlayer extends BasePlayer { private int pendingSeekCount; private int pendingSeekWindowIndex; private long pendingSeekPositionMs; - private boolean waitingForInitialTimeline; /** + * Creates a new cast player that uses a {@link DefaultMediaItemConverter}. + * * @param castContext The context from which the cast session is obtained. */ public CastPlayer(CastContext castContext) { + this(castContext, new DefaultMediaItemConverter()); + } + + /** + * Creates a new cast player. + * + * @param castContext The context from which the cast session is obtained. + * @param mediaItemConverter The {@link MediaItemConverter} to use. + */ + public CastPlayer(CastContext castContext, MediaItemConverter mediaItemConverter) { this.castContext = castContext; + this.mediaItemConverter = mediaItemConverter; timelineTracker = new CastTimelineTracker(); period = new Timeline.Period(); statusListener = new StatusListener(); @@ -143,106 +157,61 @@ public final class CastPlayer extends BasePlayer { // Media Queue manipulation methods. - /** - * Loads a single item media queue. If no session is available, does nothing. - * - * @param item The item to load. - * @param positionMs The position at which the playback should start in milliseconds relative to - * the start of the item at {@code startIndex}. If {@link C#TIME_UNSET} is passed, playback - * starts at position 0. - * @return The Cast {@code PendingResult}, or null if no session is available. - */ + /** @deprecated Use {@link #setMediaItems(List, int, long)} instead. */ + @Deprecated @Nullable public PendingResult loadItem(MediaQueueItem item, long positionMs) { - return loadItems(new MediaQueueItem[] {item}, 0, positionMs, REPEAT_MODE_OFF); + return setMediaItemsInternal( + new MediaQueueItem[] {item}, /* startWindowIndex= */ 0, positionMs, repeatMode.value); } /** - * Loads a media queue. If no session is available, does nothing. - * - * @param items The items to load. - * @param startIndex The index of the item at which playback should start. - * @param positionMs The position at which the playback should start in milliseconds relative to - * the start of the item at {@code startIndex}. If {@link C#TIME_UNSET} is passed, playback - * starts at position 0. - * @param repeatMode The repeat mode for the created media queue. - * @return The Cast {@code PendingResult}, or null if no session is available. + * @deprecated Use {@link #setMediaItems(List, int, long)} and {@link #setRepeatMode(int)} + * instead. */ + @Deprecated @Nullable public PendingResult loadItems( MediaQueueItem[] items, int startIndex, long positionMs, @RepeatMode int repeatMode) { - if (remoteMediaClient != null) { - positionMs = positionMs != C.TIME_UNSET ? positionMs : 0; - waitingForInitialTimeline = true; - return remoteMediaClient.queueLoad(items, startIndex, getCastRepeatMode(repeatMode), - positionMs, null); - } - return null; + return setMediaItemsInternal(items, startIndex, positionMs, repeatMode); } - /** - * Appends a sequence of items to the media queue. If no media queue exists, does nothing. - * - * @param items The items to append. - * @return The Cast {@code PendingResult}, or null if no media queue exists. - */ + /** @deprecated Use {@link #addMediaItems(List)} instead. */ + @Deprecated @Nullable public PendingResult addItems(MediaQueueItem... items) { - return addItems(MediaQueueItem.INVALID_ITEM_ID, items); + return addMediaItemsInternal(items, MediaQueueItem.INVALID_ITEM_ID); } - /** - * Inserts a sequence of items into the media queue. If no media queue or period with id {@code - * periodId} exist, does nothing. - * - * @param periodId The id of the period ({@link #getCurrentTimeline}) that corresponds to the item - * that will follow immediately after the inserted items. - * @param items The items to insert. - * @return The Cast {@code PendingResult}, or null if no media queue or no period with id {@code - * periodId} exist. - */ + /** @deprecated Use {@link #addMediaItems(int, List)} instead. */ + @Deprecated @Nullable public PendingResult addItems(int periodId, MediaQueueItem... items) { - if (getMediaStatus() != null && (periodId == MediaQueueItem.INVALID_ITEM_ID - || currentTimeline.getIndexOfPeriod(periodId) != C.INDEX_UNSET)) { - return remoteMediaClient.queueInsertItems(items, periodId, null); + if (periodId == MediaQueueItem.INVALID_ITEM_ID + || currentTimeline.getIndexOfPeriod(periodId) != C.INDEX_UNSET) { + return addMediaItemsInternal(items, periodId); } return null; } - /** - * Removes an item from the media queue. If no media queue or period with id {@code periodId} - * exist, does nothing. - * - * @param periodId The id of the period ({@link #getCurrentTimeline}) that corresponds to the item - * to remove. - * @return The Cast {@code PendingResult}, or null if no media queue or no period with id {@code - * periodId} exist. - */ + /** @deprecated Use {@link #removeMediaItem(int)} instead. */ + @Deprecated @Nullable public PendingResult removeItem(int periodId) { - if (getMediaStatus() != null && currentTimeline.getIndexOfPeriod(periodId) != C.INDEX_UNSET) { - return remoteMediaClient.queueRemoveItem(periodId, null); + if (currentTimeline.getIndexOfPeriod(periodId) != C.INDEX_UNSET) { + return removeMediaItemsInternal(new int[] {periodId}); } return null; } - /** - * Moves an existing item within the media queue. If no media queue or period with id {@code - * periodId} exist, does nothing. - * - * @param periodId The id of the period ({@link #getCurrentTimeline}) that corresponds to the item - * to move. - * @param newIndex The target index of the item in the media queue. Must be in the range 0 <= - * index < {@link Timeline#getPeriodCount()}, as provided by {@link #getCurrentTimeline()}. - * @return The Cast {@code PendingResult}, or null if no media queue or no period with id {@code - * periodId} exist. - */ + /** @deprecated Use {@link #moveMediaItem(int, int)} instead. */ + @Deprecated @Nullable public PendingResult moveItem(int periodId, int newIndex) { - Assertions.checkArgument(newIndex >= 0 && newIndex < currentTimeline.getPeriodCount()); - if (getMediaStatus() != null && currentTimeline.getIndexOfPeriod(periodId) != C.INDEX_UNSET) { - return remoteMediaClient.queueMoveItemToNewIndex(periodId, newIndex, null); + Assertions.checkArgument(newIndex >= 0 && newIndex < currentTimeline.getWindowCount()); + int fromIndex = currentTimeline.getIndexOfPeriod(periodId); + if (fromIndex != C.INDEX_UNSET && fromIndex != newIndex) { + return moveMediaItemsInternal(new int[] {periodId}, fromIndex, newIndex); } return null; } @@ -307,6 +276,13 @@ public final class CastPlayer extends BasePlayer { return null; } + @Override + @Nullable + public DeviceComponent getDeviceComponent() { + // TODO(b/151792305): Implement the component. + return null; + } + @Override public Looper getApplicationLooper() { return Looper.getMainLooper(); @@ -327,6 +303,73 @@ public final class CastPlayer extends BasePlayer { } } + @Override + public void setMediaItems( + List mediaItems, int startWindowIndex, long startPositionMs) { + setMediaItemsInternal( + toMediaQueueItems(mediaItems), startWindowIndex, startPositionMs, repeatMode.value); + } + + @Override + public void addMediaItems(List mediaItems) { + addMediaItemsInternal(toMediaQueueItems(mediaItems), MediaQueueItem.INVALID_ITEM_ID); + } + + @Override + public void addMediaItems(int index, List mediaItems) { + Assertions.checkArgument(index >= 0); + int uid = MediaQueueItem.INVALID_ITEM_ID; + if (index < currentTimeline.getWindowCount()) { + uid = (int) currentTimeline.getWindow(/* windowIndex= */ index, window).uid; + } + addMediaItemsInternal(toMediaQueueItems(mediaItems), uid); + } + + @Override + public void moveMediaItems(int fromIndex, int toIndex, int newIndex) { + Assertions.checkArgument( + fromIndex >= 0 + && fromIndex <= toIndex + && toIndex <= currentTimeline.getWindowCount() + && newIndex >= 0 + && newIndex < currentTimeline.getWindowCount()); + newIndex = Math.min(newIndex, currentTimeline.getWindowCount() - (toIndex - fromIndex)); + if (fromIndex == toIndex || fromIndex == newIndex) { + // Do nothing. + return; + } + int[] uids = new int[toIndex - fromIndex]; + for (int i = 0; i < uids.length; i++) { + uids[i] = (int) currentTimeline.getWindow(/* windowIndex= */ i + fromIndex, window).uid; + } + moveMediaItemsInternal(uids, fromIndex, newIndex); + } + + @Override + public void removeMediaItems(int fromIndex, int toIndex) { + Assertions.checkArgument( + fromIndex >= 0 && toIndex >= fromIndex && toIndex <= currentTimeline.getWindowCount()); + if (fromIndex == toIndex) { + // Do nothing. + return; + } + int[] uids = new int[toIndex - fromIndex]; + for (int i = 0; i < uids.length; i++) { + uids[i] = (int) currentTimeline.getWindow(/* windowIndex= */ i + fromIndex, window).uid; + } + removeMediaItemsInternal(uids); + } + + @Override + public void clearMediaItems() { + removeMediaItems(/* fromIndex= */ 0, /* toIndex= */ currentTimeline.getWindowCount()); + } + + @Override + public void prepare() { + // Do nothing. + } + @Override @Player.State public int getPlaybackState() { @@ -339,9 +382,16 @@ public final class CastPlayer extends BasePlayer { return Player.PLAYBACK_SUPPRESSION_REASON_NONE; } + @Deprecated @Override @Nullable public ExoPlaybackException getPlaybackError() { + return getPlayerError(); + } + + @Override + @Nullable + public ExoPlaybackException getPlayerError() { return null; } @@ -353,7 +403,8 @@ public final class CastPlayer extends BasePlayer { // We update the local state and send the message to the receiver app, which will cause the // operation to be perceived as synchronous by the user. When the operation reports a result, // the local state will be updated to reflect the state reported by the Cast SDK. - setPlayerStateAndNotifyIfChanged(playWhenReady, playbackState); + setPlayerStateAndNotifyIfChanged( + playWhenReady, PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST, playbackState); flushNotifications(); PendingResult pendingResult = playWhenReady ? remoteMediaClient.play() : remoteMediaClient.pause(); @@ -400,16 +451,32 @@ public final class CastPlayer extends BasePlayer { flushNotifications(); } + /** @deprecated Use {@link #setPlaybackSpeed(float)} instead. */ + @SuppressWarnings("deprecation") + @Deprecated @Override public void setPlaybackParameters(@Nullable PlaybackParameters playbackParameters) { // Unsupported by the RemoteMediaClient API. Do nothing. } + /** @deprecated Use {@link #getPlaybackSpeed()} instead. */ + @SuppressWarnings("deprecation") + @Deprecated @Override public PlaybackParameters getPlaybackParameters() { return PlaybackParameters.DEFAULT; } + @Override + public void setPlaybackSpeed(float playbackSpeed) { + // Unsupported by the RemoteMediaClient API. Do nothing. + } + + @Override + public float getPlaybackSpeed() { + return Player.DEFAULT_PLAYBACK_SPEED; + } + @Override public void stop(boolean reset) { playbackState = STATE_IDLE; @@ -627,8 +694,14 @@ public final class CastPlayer extends BasePlayer { newPlayWhenReadyValue = !remoteMediaClient.isPaused(); playWhenReady.clearPendingResultCallback(); } + @PlayWhenReadyChangeReason + int playWhenReadyChangeReason = + newPlayWhenReadyValue != playWhenReady.value + ? PLAY_WHEN_READY_CHANGE_REASON_REMOTE + : PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST; // We do not mask the playback state, so try setting it regardless of the playWhenReady masking. - setPlayerStateAndNotifyIfChanged(newPlayWhenReadyValue, fetchPlaybackState(remoteMediaClient)); + setPlayerStateAndNotifyIfChanged( + newPlayWhenReadyValue, playWhenReadyChangeReason, fetchPlaybackState(remoteMediaClient)); } @RequiresNonNull("remoteMediaClient") @@ -641,15 +714,13 @@ public final class CastPlayer extends BasePlayer { private void updateTimelineAndNotifyIfChanged() { if (updateTimeline()) { - @Player.TimelineChangeReason - int reason = - waitingForInitialTimeline - ? Player.TIMELINE_CHANGE_REASON_PREPARED - : Player.TIMELINE_CHANGE_REASON_DYNAMIC; - waitingForInitialTimeline = false; + // TODO: Differentiate TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED and + // TIMELINE_CHANGE_REASON_SOURCE_UPDATE [see internal: b/65152553]. notificationsBatch.add( new ListenerNotificationTask( - listener -> listener.onTimelineChanged(currentTimeline, reason))); + listener -> + listener.onTimelineChanged( + currentTimeline, Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE))); } } @@ -713,6 +784,58 @@ public final class CastPlayer extends BasePlayer { return false; } + @Nullable + private PendingResult setMediaItemsInternal( + MediaQueueItem[] mediaQueueItems, + int startWindowIndex, + long startPositionMs, + @RepeatMode int repeatMode) { + if (remoteMediaClient == null || mediaQueueItems.length == 0) { + return null; + } + startPositionMs = startPositionMs == C.TIME_UNSET ? 0 : startPositionMs; + if (startWindowIndex == C.INDEX_UNSET) { + startWindowIndex = getCurrentWindowIndex(); + startPositionMs = getCurrentPosition(); + } + return remoteMediaClient.queueLoad( + mediaQueueItems, + Math.min(startWindowIndex, mediaQueueItems.length - 1), + getCastRepeatMode(repeatMode), + startPositionMs, + /* customData= */ null); + } + + @Nullable + private PendingResult addMediaItemsInternal(MediaQueueItem[] items, int uid) { + if (remoteMediaClient == null || getMediaStatus() == null) { + return null; + } + return remoteMediaClient.queueInsertItems(items, uid, /* customData= */ null); + } + + @Nullable + private PendingResult moveMediaItemsInternal( + int[] uids, int fromIndex, int newIndex) { + if (remoteMediaClient == null || getMediaStatus() == null) { + return null; + } + int insertBeforeIndex = fromIndex < newIndex ? newIndex + uids.length : newIndex; + int insertBeforeItemId = MediaQueueItem.INVALID_ITEM_ID; + if (insertBeforeIndex < currentTimeline.getWindowCount()) { + insertBeforeItemId = (int) currentTimeline.getWindow(insertBeforeIndex, window).uid; + } + return remoteMediaClient.queueReorderItems(uids, insertBeforeItemId, /* customData= */ null); + } + + @Nullable + private PendingResult removeMediaItemsInternal(int[] uids) { + if (remoteMediaClient == null || getMediaStatus() == null) { + return null; + } + return remoteMediaClient.queueRemoveItems(uids, /* customData= */ null); + } + private void setRepeatModeAndNotifyIfChanged(@Player.RepeatMode int repeatMode) { if (this.repeatMode.value != repeatMode) { this.repeatMode.value = repeatMode; @@ -721,14 +844,27 @@ public final class CastPlayer extends BasePlayer { } } + @SuppressWarnings("deprecation") private void setPlayerStateAndNotifyIfChanged( - boolean playWhenReady, @Player.State int playbackState) { - if (this.playWhenReady.value != playWhenReady || this.playbackState != playbackState) { - this.playWhenReady.value = playWhenReady; + boolean playWhenReady, + @Player.PlayWhenReadyChangeReason int playWhenReadyChangeReason, + @Player.State int playbackState) { + boolean playWhenReadyChanged = this.playWhenReady.value != playWhenReady; + boolean playbackStateChanged = this.playbackState != playbackState; + if (playWhenReadyChanged || playbackStateChanged) { this.playbackState = playbackState; + this.playWhenReady.value = playWhenReady; notificationsBatch.add( new ListenerNotificationTask( - listener -> listener.onPlayerStateChanged(playWhenReady, playbackState))); + listener -> { + listener.onPlayerStateChanged(playWhenReady, playbackState); + if (playbackStateChanged) { + listener.onPlaybackStateChanged(playbackState); + } + if (playWhenReadyChanged) { + listener.onPlayWhenReadyChanged(playWhenReady, playWhenReadyChangeReason); + } + })); } } @@ -750,6 +886,7 @@ public final class CastPlayer extends BasePlayer { remoteMediaClient.addProgressListener(statusListener, PROGRESS_REPORT_PERIOD_MS); updateInternalStateAndNotifyIfChanged(); } else { + updateTimelineAndNotifyIfChanged(); if (sessionAvailabilityListener != null) { sessionAvailabilityListener.onCastSessionUnavailable(); } @@ -849,6 +986,14 @@ public final class CastPlayer extends BasePlayer { } } + private MediaQueueItem[] toMediaQueueItems(List mediaItems) { + MediaQueueItem[] mediaQueueItems = new MediaQueueItem[mediaItems.size()]; + for (int i = 0; i < mediaItems.size(); i++) { + mediaQueueItems[i] = mediaItemConverter.toMediaQueueItem(mediaItems.get(i)); + } + return mediaQueueItems; + } + // Internal classes. private final class StatusListener 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 a3bdc5e415..38a7a692b2 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 @@ -130,6 +130,7 @@ import java.util.Arrays; /* manifest= */ null, /* presentationStartTimeMs= */ C.TIME_UNSET, /* windowStartTimeMs= */ C.TIME_UNSET, + /* elapsedRealtimeEpochOffsetMs= */ C.TIME_UNSET, /* isSeekable= */ !isDynamic, isDynamic, isLive[windowIndex], 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 1dc25576a0..182afb0468 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 @@ -104,16 +104,11 @@ import com.google.android.gms.cast.MediaTrack; * @return The equivalent {@link Format}. */ public static Format mediaTrackToFormat(MediaTrack mediaTrack) { - return Format.createContainerFormat( - mediaTrack.getContentId(), - /* label= */ null, - mediaTrack.getContentType(), - /* sampleMimeType= */ null, - /* codecs= */ null, - /* bitrate= */ Format.NO_VALUE, - /* selectionFlags= */ 0, - /* roleFlags= */ 0, - mediaTrack.getLanguage()); + return new Format.Builder() + .setId(mediaTrack.getContentId()) + .setContainerMimeType(mediaTrack.getContentType()) + .setLanguage(mediaTrack.getLanguage()) + .build(); } private CastUtils() {} diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/DefaultMediaItemConverter.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/DefaultMediaItemConverter.java index 098803a512..705f2c2508 100644 --- a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/DefaultMediaItemConverter.java +++ b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/DefaultMediaItemConverter.java @@ -18,7 +18,8 @@ package com.google.android.exoplayer2.ext.cast; import android.net.Uri; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.ext.cast.MediaItem.DrmConfiguration; +import com.google.android.exoplayer2.MediaItem; +import com.google.android.exoplayer2.util.Assertions; import com.google.android.gms.cast.MediaInfo; import com.google.android.gms.cast.MediaMetadata; import com.google.android.gms.cast.MediaQueueItem; @@ -43,22 +44,24 @@ public final class DefaultMediaItemConverter implements MediaItemConverter { @Override public MediaItem toMediaItem(MediaQueueItem item) { - return getMediaItem(item.getMedia().getCustomData()); + // `item` came from `toMediaQueueItem()` so the custom JSON data must be set. + return getMediaItem(Assertions.checkNotNull(item.getMedia().getCustomData())); } @Override public MediaQueueItem toMediaQueueItem(MediaItem item) { - if (item.mimeType == null) { + Assertions.checkNotNull(item.playbackProperties); + if (item.playbackProperties.mimeType == null) { throw new IllegalArgumentException("The item must specify its mimeType"); } MediaMetadata metadata = new MediaMetadata(MediaMetadata.MEDIA_TYPE_MOVIE); - if (item.title != null) { - metadata.putString(MediaMetadata.KEY_TITLE, item.title); + if (item.mediaMetadata.title != null) { + metadata.putString(MediaMetadata.KEY_TITLE, item.mediaMetadata.title); } MediaInfo mediaInfo = - new MediaInfo.Builder(item.uri.toString()) + new MediaInfo.Builder(item.playbackProperties.uri.toString()) .setStreamType(MediaInfo.STREAM_TYPE_BUFFERED) - .setContentType(item.mimeType) + .setContentType(item.playbackProperties.mimeType) .setMetadata(metadata) .setCustomData(getCustomData(item)) .build(); @@ -73,14 +76,17 @@ public final class DefaultMediaItemConverter implements MediaItemConverter { MediaItem.Builder builder = new MediaItem.Builder(); builder.setUri(Uri.parse(mediaItemJson.getString(KEY_URI))); if (mediaItemJson.has(KEY_TITLE)) { - builder.setTitle(mediaItemJson.getString(KEY_TITLE)); + com.google.android.exoplayer2.MediaMetadata mediaMetadata = + new com.google.android.exoplayer2.MediaMetadata.Builder() + .setTitle(mediaItemJson.getString(KEY_TITLE)) + .build(); + builder.setMediaMetadata(mediaMetadata); } if (mediaItemJson.has(KEY_MIME_TYPE)) { builder.setMimeType(mediaItemJson.getString(KEY_MIME_TYPE)); } if (mediaItemJson.has(KEY_DRM_CONFIGURATION)) { - builder.setDrmConfiguration( - getDrmConfiguration(mediaItemJson.getJSONObject(KEY_DRM_CONFIGURATION))); + populateDrmConfiguration(mediaItemJson.getJSONObject(KEY_DRM_CONFIGURATION), builder); } return builder.build(); } catch (JSONException e) { @@ -88,25 +94,26 @@ public final class DefaultMediaItemConverter implements MediaItemConverter { } } - private static DrmConfiguration getDrmConfiguration(JSONObject json) throws JSONException { - UUID uuid = UUID.fromString(json.getString(KEY_UUID)); - Uri licenseUri = Uri.parse(json.getString(KEY_LICENSE_URI)); + private static void populateDrmConfiguration(JSONObject json, MediaItem.Builder builder) + throws JSONException { + builder.setDrmUuid(UUID.fromString(json.getString(KEY_UUID))); + builder.setDrmLicenseUri(json.getString(KEY_LICENSE_URI)); JSONObject requestHeadersJson = json.getJSONObject(KEY_REQUEST_HEADERS); HashMap requestHeaders = new HashMap<>(); for (Iterator iterator = requestHeadersJson.keys(); iterator.hasNext(); ) { String key = iterator.next(); requestHeaders.put(key, requestHeadersJson.getString(key)); } - return new DrmConfiguration(uuid, licenseUri, requestHeaders); + builder.setDrmLicenseRequestHeaders(requestHeaders); } // Serialization. - private static JSONObject getCustomData(MediaItem item) { + private static JSONObject getCustomData(MediaItem mediaItem) { JSONObject json = new JSONObject(); try { - json.put(KEY_MEDIA_ITEM, getMediaItemJson(item)); - JSONObject playerConfigJson = getPlayerConfigJson(item); + json.put(KEY_MEDIA_ITEM, getMediaItemJson(mediaItem)); + @Nullable JSONObject playerConfigJson = getPlayerConfigJson(mediaItem); if (playerConfigJson != null) { json.put(KEY_PLAYER_CONFIG, playerConfigJson); } @@ -116,18 +123,21 @@ public final class DefaultMediaItemConverter implements MediaItemConverter { return json; } - private static JSONObject getMediaItemJson(MediaItem item) throws JSONException { + private static JSONObject getMediaItemJson(MediaItem mediaItem) throws JSONException { + Assertions.checkNotNull(mediaItem.playbackProperties); JSONObject json = new JSONObject(); - json.put(KEY_URI, item.uri.toString()); - json.put(KEY_TITLE, item.title); - json.put(KEY_MIME_TYPE, item.mimeType); - if (item.drmConfiguration != null) { - json.put(KEY_DRM_CONFIGURATION, getDrmConfigurationJson(item.drmConfiguration)); + json.put(KEY_TITLE, mediaItem.mediaMetadata.title); + json.put(KEY_URI, mediaItem.playbackProperties.uri.toString()); + json.put(KEY_MIME_TYPE, mediaItem.playbackProperties.mimeType); + if (mediaItem.playbackProperties.drmConfiguration != null) { + json.put( + KEY_DRM_CONFIGURATION, + getDrmConfigurationJson(mediaItem.playbackProperties.drmConfiguration)); } return json; } - private static JSONObject getDrmConfigurationJson(DrmConfiguration drmConfiguration) + private static JSONObject getDrmConfigurationJson(MediaItem.DrmConfiguration drmConfiguration) throws JSONException { JSONObject json = new JSONObject(); json.put(KEY_UUID, drmConfiguration.uuid); @@ -137,11 +147,12 @@ public final class DefaultMediaItemConverter implements MediaItemConverter { } @Nullable - private static JSONObject getPlayerConfigJson(MediaItem item) throws JSONException { - DrmConfiguration drmConfiguration = item.drmConfiguration; - if (drmConfiguration == null) { + private static JSONObject getPlayerConfigJson(MediaItem mediaItem) throws JSONException { + if (mediaItem.playbackProperties == null + || mediaItem.playbackProperties.drmConfiguration == null) { return null; } + MediaItem.DrmConfiguration drmConfiguration = mediaItem.playbackProperties.drmConfiguration; String drmScheme; if (C.WIDEVINE_UUID.equals(drmConfiguration.uuid)) { diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/MediaItem.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/MediaItem.java deleted file mode 100644 index 7ac0da7078..0000000000 --- a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/MediaItem.java +++ /dev/null @@ -1,175 +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.ext.cast; - -import android.net.Uri; -import androidx.annotation.Nullable; -import com.google.android.exoplayer2.util.Assertions; -import com.google.android.exoplayer2.util.Util; -import java.util.Collections; -import java.util.Map; -import java.util.UUID; - -/** Representation of a media item. */ -public final class MediaItem { - - /** A builder for {@link MediaItem} instances. */ - public static final class Builder { - - @Nullable private Uri uri; - @Nullable private String title; - @Nullable private String mimeType; - @Nullable private DrmConfiguration drmConfiguration; - - /** See {@link MediaItem#uri}. */ - public Builder setUri(String uri) { - return setUri(Uri.parse(uri)); - } - - /** See {@link MediaItem#uri}. */ - public Builder setUri(Uri uri) { - this.uri = uri; - return this; - } - - /** See {@link MediaItem#title}. */ - public Builder setTitle(String title) { - this.title = title; - return this; - } - - /** See {@link MediaItem#mimeType}. */ - public Builder setMimeType(String mimeType) { - this.mimeType = mimeType; - return this; - } - - /** See {@link MediaItem#drmConfiguration}. */ - public Builder setDrmConfiguration(DrmConfiguration drmConfiguration) { - this.drmConfiguration = drmConfiguration; - return this; - } - - /** Returns a new {@link MediaItem} instance with the current builder values. */ - public MediaItem build() { - Assertions.checkNotNull(uri); - return new MediaItem(uri, title, mimeType, drmConfiguration); - } - } - - /** DRM configuration for a media item. */ - public static final class DrmConfiguration { - - /** The UUID of the protection scheme. */ - public final UUID uuid; - - /** - * Optional license server {@link Uri}. If {@code null} then the license server must be - * specified by the media. - */ - @Nullable public final Uri licenseUri; - - /** Headers that should be attached to any license requests. */ - public final Map requestHeaders; - - /** - * Creates an instance. - * - * @param uuid See {@link #uuid}. - * @param licenseUri See {@link #licenseUri}. - * @param requestHeaders See {@link #requestHeaders}. - */ - public DrmConfiguration( - UUID uuid, @Nullable Uri licenseUri, @Nullable Map requestHeaders) { - this.uuid = uuid; - this.licenseUri = licenseUri; - this.requestHeaders = - requestHeaders == null - ? Collections.emptyMap() - : Collections.unmodifiableMap(requestHeaders); - } - - @Override - public boolean equals(@Nullable Object obj) { - if (this == obj) { - return true; - } - if (obj == null || getClass() != obj.getClass()) { - return false; - } - - DrmConfiguration other = (DrmConfiguration) obj; - return uuid.equals(other.uuid) - && Util.areEqual(licenseUri, other.licenseUri) - && requestHeaders.equals(other.requestHeaders); - } - - @Override - public int hashCode() { - int result = uuid.hashCode(); - result = 31 * result + (licenseUri != null ? licenseUri.hashCode() : 0); - result = 31 * result + requestHeaders.hashCode(); - return result; - } - } - - /** The media {@link Uri}. */ - public final Uri uri; - - /** The title of the item, or {@code null} if unspecified. */ - @Nullable public final String title; - - /** The mime type for the media, or {@code null} if unspecified. */ - @Nullable public final String mimeType; - - /** Optional {@link DrmConfiguration} for the media. */ - @Nullable public final DrmConfiguration drmConfiguration; - - private MediaItem( - Uri uri, - @Nullable String title, - @Nullable String mimeType, - @Nullable DrmConfiguration drmConfiguration) { - this.uri = uri; - this.title = title; - this.mimeType = mimeType; - this.drmConfiguration = drmConfiguration; - } - - @Override - public boolean equals(@Nullable Object obj) { - if (this == obj) { - return true; - } - if (obj == null || getClass() != obj.getClass()) { - return false; - } - MediaItem other = (MediaItem) obj; - return uri.equals(other.uri) - && Util.areEqual(title, other.title) - && Util.areEqual(mimeType, other.mimeType) - && Util.areEqual(drmConfiguration, other.drmConfiguration); - } - - @Override - public int hashCode() { - int result = uri.hashCode(); - result = 31 * result + (title == null ? 0 : title.hashCode()); - result = 31 * result + (drmConfiguration == null ? 0 : drmConfiguration.hashCode()); - result = 31 * result + (mimeType == null ? 0 : mimeType.hashCode()); - return result; - } -} diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/MediaItemConverter.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/MediaItemConverter.java index 23633aa4d2..c4a5184632 100644 --- a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/MediaItemConverter.java +++ b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/MediaItemConverter.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.ext.cast; +import com.google.android.exoplayer2.MediaItem; import com.google.android.gms.cast.MediaQueueItem; /** Converts between {@link MediaItem} and the Cast SDK's {@link MediaQueueItem}. */ diff --git a/extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/CastPlayerTest.java b/extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/CastPlayerTest.java index ae081b1248..d1ab67b5ad 100644 --- a/extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/CastPlayerTest.java +++ b/extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/CastPlayerTest.java @@ -18,13 +18,22 @@ package com.google.android.exoplayer2.ext.cast; import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; import static org.mockito.MockitoAnnotations.initMocks; import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.MediaItem; import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.MimeTypes; +import com.google.android.gms.cast.MediaInfo; +import com.google.android.gms.cast.MediaQueueItem; import com.google.android.gms.cast.MediaStatus; import com.google.android.gms.cast.framework.CastContext; import com.google.android.gms.cast.framework.CastSession; @@ -33,6 +42,9 @@ import com.google.android.gms.cast.framework.media.MediaQueue; import com.google.android.gms.cast.framework.media.RemoteMediaClient; import com.google.android.gms.common.api.PendingResult; import com.google.android.gms.common.api.ResultCallback; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -46,9 +58,13 @@ import org.mockito.Mockito; public class CastPlayerTest { private CastPlayer castPlayer; + + @SuppressWarnings("deprecation") private RemoteMediaClient.Listener remoteMediaClientListener; + @Mock private RemoteMediaClient mockRemoteMediaClient; @Mock private MediaStatus mockMediaStatus; + @Mock private MediaInfo mockMediaInfo; @Mock private MediaQueue mockMediaQueue; @Mock private CastContext mockCastContext; @Mock private SessionManager mockSessionManager; @@ -62,6 +78,9 @@ public class CastPlayerTest { @Captor private ArgumentCaptor listenerArgumentCaptor; + @Captor private ArgumentCaptor queueItemsArgumentCaptor; + + @SuppressWarnings("deprecation") @Before public void setUp() { initMocks(this); @@ -80,15 +99,18 @@ public class CastPlayerTest { remoteMediaClientListener = listenerArgumentCaptor.getValue(); } + @SuppressWarnings("deprecation") @Test - public void testSetPlayWhenReady_masksRemoteState() { + public void setPlayWhenReady_masksRemoteState() { when(mockRemoteMediaClient.play()).thenReturn(mockPendingResult); assertThat(castPlayer.getPlayWhenReady()).isFalse(); - castPlayer.setPlayWhenReady(true); + castPlayer.play(); verify(mockPendingResult).setResultCallback(setResultCallbackArgumentCaptor.capture()); assertThat(castPlayer.getPlayWhenReady()).isTrue(); verify(mockListener).onPlayerStateChanged(true, Player.STATE_IDLE); + verify(mockListener) + .onPlayWhenReadyChanged(true, Player.PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST); // There is a status update in the middle, which should be hidden by masking. remoteMediaClientListener.onStatusUpdated(); @@ -102,35 +124,59 @@ public class CastPlayerTest { verifyNoMoreInteractions(mockListener); } + @SuppressWarnings("deprecation") @Test - public void testSetPlayWhenReadyMasking_updatesUponResultChange() { + public void setPlayWhenReadyMasking_updatesUponResultChange() { when(mockRemoteMediaClient.play()).thenReturn(mockPendingResult); assertThat(castPlayer.getPlayWhenReady()).isFalse(); - castPlayer.setPlayWhenReady(true); + castPlayer.play(); verify(mockPendingResult).setResultCallback(setResultCallbackArgumentCaptor.capture()); assertThat(castPlayer.getPlayWhenReady()).isTrue(); verify(mockListener).onPlayerStateChanged(true, Player.STATE_IDLE); + verify(mockListener) + .onPlayWhenReadyChanged(true, Player.PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST); // Upon result, the remote media client is still paused. The state should reflect that. setResultCallbackArgumentCaptor .getValue() .onResult(Mockito.mock(RemoteMediaClient.MediaChannelResult.class)); verify(mockListener).onPlayerStateChanged(false, Player.STATE_IDLE); + verify(mockListener).onPlayWhenReadyChanged(false, Player.PLAY_WHEN_READY_CHANGE_REASON_REMOTE); assertThat(castPlayer.getPlayWhenReady()).isFalse(); } + @SuppressWarnings("deprecation") @Test - public void testPlayWhenReady_changesOnStatusUpdates() { + public void setPlayWhenReady_correctChangeReasonOnPause() { + when(mockRemoteMediaClient.play()).thenReturn(mockPendingResult); + when(mockRemoteMediaClient.pause()).thenReturn(mockPendingResult); + castPlayer.play(); + assertThat(castPlayer.getPlayWhenReady()).isTrue(); + verify(mockListener).onPlayerStateChanged(true, Player.STATE_IDLE); + verify(mockListener) + .onPlayWhenReadyChanged(true, Player.PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST); + + castPlayer.pause(); + assertThat(castPlayer.getPlayWhenReady()).isFalse(); + verify(mockListener).onPlayerStateChanged(false, Player.STATE_IDLE); + verify(mockListener) + .onPlayWhenReadyChanged(false, Player.PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST); + } + + @SuppressWarnings("deprecation") + @Test + public void playWhenReady_changesOnStatusUpdates() { assertThat(castPlayer.getPlayWhenReady()).isFalse(); when(mockRemoteMediaClient.isPaused()).thenReturn(false); remoteMediaClientListener.onStatusUpdated(); verify(mockListener).onPlayerStateChanged(true, Player.STATE_IDLE); + verify(mockListener).onPlayWhenReadyChanged(true, Player.PLAY_WHEN_READY_CHANGE_REASON_REMOTE); assertThat(castPlayer.getPlayWhenReady()).isTrue(); } @Test - public void testSetRepeatMode_masksRemoteState() { + public void setRepeatMode_masksRemoteState() { when(mockRemoteMediaClient.queueSetRepeatMode(anyInt(), any())).thenReturn(mockPendingResult); assertThat(castPlayer.getRepeatMode()).isEqualTo(Player.REPEAT_MODE_OFF); @@ -153,7 +199,7 @@ public class CastPlayerTest { } @Test - public void testSetRepeatMode_updatesUponResultChange() { + public void setRepeatMode_updatesUponResultChange() { when(mockRemoteMediaClient.queueSetRepeatMode(anyInt(), any())).thenReturn(mockPendingResult); castPlayer.setRepeatMode(Player.REPEAT_MODE_ONE); @@ -175,11 +221,279 @@ public class CastPlayerTest { } @Test - public void testRepeatMode_changesOnStatusUpdates() { + public void repeatMode_changesOnStatusUpdates() { assertThat(castPlayer.getRepeatMode()).isEqualTo(Player.REPEAT_MODE_OFF); when(mockMediaStatus.getQueueRepeatMode()).thenReturn(MediaStatus.REPEAT_MODE_REPEAT_SINGLE); remoteMediaClientListener.onStatusUpdated(); verify(mockListener).onRepeatModeChanged(Player.REPEAT_MODE_ONE); assertThat(castPlayer.getRepeatMode()).isEqualTo(Player.REPEAT_MODE_ONE); } + + @Test + public void setMediaItems_callsRemoteMediaClient() { + List mediaItems = new ArrayList<>(); + String uri1 = "http://www.google.com/video1"; + String uri2 = "http://www.google.com/video2"; + mediaItems.add( + new MediaItem.Builder().setUri(uri1).setMimeType(MimeTypes.APPLICATION_MPD).build()); + mediaItems.add( + new MediaItem.Builder().setUri(uri2).setMimeType(MimeTypes.APPLICATION_MP4).build()); + + castPlayer.setMediaItems(mediaItems, /* startWindowIndex= */ 1, /* startPositionMs= */ 2000L); + + verify(mockRemoteMediaClient) + .queueLoad(queueItemsArgumentCaptor.capture(), eq(1), anyInt(), eq(2000L), any()); + MediaQueueItem[] mediaQueueItems = queueItemsArgumentCaptor.getValue(); + assertThat(mediaQueueItems[0].getMedia().getContentId()).isEqualTo(uri1); + assertThat(mediaQueueItems[1].getMedia().getContentId()).isEqualTo(uri2); + } + + @Test + public void setMediaItems_doNotReset_callsRemoteMediaClient() { + MediaItem.Builder builder = new MediaItem.Builder(); + List mediaItems = new ArrayList<>(); + String uri1 = "http://www.google.com/video1"; + String uri2 = "http://www.google.com/video2"; + mediaItems.add(builder.setUri(uri1).setMimeType(MimeTypes.APPLICATION_MPD).build()); + mediaItems.add(builder.setUri(uri2).setMimeType(MimeTypes.APPLICATION_MP4).build()); + int startWindowIndex = C.INDEX_UNSET; + long startPositionMs = 2000L; + + castPlayer.setMediaItems(mediaItems, startWindowIndex, startPositionMs); + + verify(mockRemoteMediaClient) + .queueLoad(queueItemsArgumentCaptor.capture(), eq(0), anyInt(), eq(0L), any()); + + MediaQueueItem[] mediaQueueItems = queueItemsArgumentCaptor.getValue(); + assertThat(mediaQueueItems[0].getMedia().getContentId()).isEqualTo(uri1); + assertThat(mediaQueueItems[1].getMedia().getContentId()).isEqualTo(uri2); + } + + @Test + public void addMediaItems_callsRemoteMediaClient() { + MediaItem.Builder builder = new MediaItem.Builder(); + List mediaItems = new ArrayList<>(); + String uri1 = "http://www.google.com/video1"; + String uri2 = "http://www.google.com/video2"; + mediaItems.add(builder.setUri(uri1).setMimeType(MimeTypes.APPLICATION_MPD).build()); + mediaItems.add(builder.setUri(uri2).setMimeType(MimeTypes.APPLICATION_MP4).build()); + + castPlayer.addMediaItems(mediaItems); + + verify(mockRemoteMediaClient) + .queueInsertItems( + queueItemsArgumentCaptor.capture(), eq(MediaQueueItem.INVALID_ITEM_ID), any()); + + MediaQueueItem[] mediaQueueItems = queueItemsArgumentCaptor.getValue(); + assertThat(mediaQueueItems[0].getMedia().getContentId()).isEqualTo(uri1); + assertThat(mediaQueueItems[1].getMedia().getContentId()).isEqualTo(uri2); + } + + @SuppressWarnings("ConstantConditions") + @Test + public void addMediaItems_insertAtIndex_callsRemoteMediaClient() { + int[] mediaQueueItemIds = createMediaQueueItemIds(/* numberOfIds= */ 2); + List mediaItems = createMediaItems(mediaQueueItemIds); + fillTimeline(mediaItems, mediaQueueItemIds); + String uri = "http://www.google.com/video3"; + MediaItem anotherMediaItem = + new MediaItem.Builder().setUri(uri).setMimeType(MimeTypes.APPLICATION_MPD).build(); + + // Add another on position 1 + int index = 1; + castPlayer.addMediaItems(index, Collections.singletonList(anotherMediaItem)); + + verify(mockRemoteMediaClient) + .queueInsertItems( + queueItemsArgumentCaptor.capture(), + eq((int) mediaItems.get(index).playbackProperties.tag), + any()); + + MediaQueueItem[] mediaQueueItems = queueItemsArgumentCaptor.getValue(); + assertThat(mediaQueueItems[0].getMedia().getContentId()).isEqualTo(uri); + } + + @Test + public void moveMediaItem_callsRemoteMediaClient() { + int[] mediaQueueItemIds = createMediaQueueItemIds(/* numberOfIds= */ 5); + List mediaItems = createMediaItems(mediaQueueItemIds); + fillTimeline(mediaItems, mediaQueueItemIds); + + castPlayer.moveMediaItem(/* currentIndex= */ 1, /* newIndex= */ 2); + + verify(mockRemoteMediaClient) + .queueReorderItems(new int[] {2}, /* insertBeforeItemId= */ 4, /* customData= */ null); + } + + @Test + public void moveMediaItem_toBegin_callsRemoteMediaClient() { + int[] mediaQueueItemIds = createMediaQueueItemIds(/* numberOfIds= */ 5); + List mediaItems = createMediaItems(mediaQueueItemIds); + fillTimeline(mediaItems, mediaQueueItemIds); + + castPlayer.moveMediaItem(/* currentIndex= */ 1, /* newIndex= */ 0); + + verify(mockRemoteMediaClient) + .queueReorderItems(new int[] {2}, /* insertBeforeItemId= */ 1, /* customData= */ null); + } + + @Test + public void moveMediaItem_toEnd_callsRemoteMediaClient() { + int[] mediaQueueItemIds = createMediaQueueItemIds(/* numberOfIds= */ 5); + List mediaItems = createMediaItems(mediaQueueItemIds); + fillTimeline(mediaItems, mediaQueueItemIds); + + castPlayer.moveMediaItem(/* currentIndex= */ 1, /* newIndex= */ 4); + + verify(mockRemoteMediaClient) + .queueReorderItems( + new int[] {2}, + /* insertBeforeItemId= */ MediaQueueItem.INVALID_ITEM_ID, + /* customData= */ null); + } + + @Test + public void moveMediaItems_callsRemoteMediaClient() { + int[] mediaQueueItemIds = createMediaQueueItemIds(/* numberOfIds= */ 5); + List mediaItems = createMediaItems(mediaQueueItemIds); + fillTimeline(mediaItems, mediaQueueItemIds); + + castPlayer.moveMediaItems(/* fromIndex= */ 0, /* toIndex= */ 3, /* newIndex= */ 1); + + verify(mockRemoteMediaClient) + .queueReorderItems( + new int[] {1, 2, 3}, /* insertBeforeItemId= */ 5, /* customData= */ null); + } + + @Test + public void moveMediaItems_toBeginning_callsRemoteMediaClient() { + int[] mediaQueueItemIds = createMediaQueueItemIds(/* numberOfIds= */ 5); + List mediaItems = createMediaItems(mediaQueueItemIds); + fillTimeline(mediaItems, mediaQueueItemIds); + + castPlayer.moveMediaItems(/* fromIndex= */ 1, /* toIndex= */ 4, /* newIndex= */ 0); + + verify(mockRemoteMediaClient) + .queueReorderItems( + new int[] {2, 3, 4}, /* insertBeforeItemId= */ 1, /* customData= */ null); + } + + @Test + public void moveMediaItems_toEnd_callsRemoteMediaClient() { + int[] mediaQueueItemIds = createMediaQueueItemIds(/* numberOfIds= */ 5); + List mediaItems = createMediaItems(mediaQueueItemIds); + fillTimeline(mediaItems, mediaQueueItemIds); + + castPlayer.moveMediaItems(/* fromIndex= */ 0, /* toIndex= */ 2, /* newIndex= */ 3); + + verify(mockRemoteMediaClient) + .queueReorderItems( + new int[] {1, 2}, + /* insertBeforeItemId= */ MediaQueueItem.INVALID_ITEM_ID, + /* customData= */ null); + } + + @Test + public void moveMediaItems_noItems_doesNotCallRemoteMediaClient() { + int[] mediaQueueItemIds = createMediaQueueItemIds(/* numberOfIds= */ 5); + List mediaItems = createMediaItems(mediaQueueItemIds); + fillTimeline(mediaItems, mediaQueueItemIds); + + castPlayer.moveMediaItems(/* fromIndex= */ 1, /* toIndex= */ 1, /* newIndex= */ 0); + + verify(mockRemoteMediaClient, never()).queueReorderItems(any(), anyInt(), any()); + } + + @Test + public void moveMediaItems_noMove_doesNotCallRemoteMediaClient() { + int[] mediaQueueItemIds = createMediaQueueItemIds(/* numberOfIds= */ 5); + List mediaItems = createMediaItems(mediaQueueItemIds); + fillTimeline(mediaItems, mediaQueueItemIds); + + castPlayer.moveMediaItems(/* fromIndex= */ 1, /* toIndex= */ 3, /* newIndex= */ 1); + + verify(mockRemoteMediaClient, never()).queueReorderItems(any(), anyInt(), any()); + } + + @Test + public void removeMediaItems_callsRemoteMediaClient() { + int[] mediaQueueItemIds = createMediaQueueItemIds(/* numberOfIds= */ 5); + List mediaItems = createMediaItems(mediaQueueItemIds); + fillTimeline(mediaItems, mediaQueueItemIds); + + castPlayer.removeMediaItems(/* fromIndex= */ 1, /* toIndex= */ 4); + + verify(mockRemoteMediaClient).queueRemoveItems(new int[] {2, 3, 4}, /* customData= */ null); + } + + @Test + public void clearMediaItems_callsRemoteMediaClient() { + int[] mediaQueueItemIds = createMediaQueueItemIds(/* numberOfIds= */ 5); + List mediaItems = createMediaItems(mediaQueueItemIds); + fillTimeline(mediaItems, mediaQueueItemIds); + + castPlayer.clearMediaItems(); + + verify(mockRemoteMediaClient) + .queueRemoveItems(new int[] {1, 2, 3, 4, 5}, /* customData= */ null); + } + + @SuppressWarnings("ConstantConditions") + @Test + public void addMediaItems_fillsTimeline() { + Timeline.Window window = new Timeline.Window(); + int[] mediaQueueItemIds = createMediaQueueItemIds(/* numberOfIds= */ 5); + List mediaItems = createMediaItems(mediaQueueItemIds); + + fillTimeline(mediaItems, mediaQueueItemIds); + + Timeline currentTimeline = castPlayer.getCurrentTimeline(); + for (int i = 0; i < mediaItems.size(); i++) { + assertThat(currentTimeline.getWindow(/* windowIndex= */ i, window).uid) + .isEqualTo(mediaItems.get(i).playbackProperties.tag); + } + } + + private int[] createMediaQueueItemIds(int numberOfIds) { + int[] mediaQueueItemIds = new int[numberOfIds]; + for (int i = 0; i < numberOfIds; i++) { + mediaQueueItemIds[i] = i + 1; + } + return mediaQueueItemIds; + } + + private List createMediaItems(int[] mediaQueueItemIds) { + MediaItem.Builder builder = new MediaItem.Builder(); + List mediaItems = new ArrayList<>(); + for (int mediaQueueItemId : mediaQueueItemIds) { + MediaItem mediaItem = + builder + .setUri("http://www.google.com/video" + mediaQueueItemId) + .setMimeType(MimeTypes.APPLICATION_MPD) + .setTag(mediaQueueItemId) + .build(); + mediaItems.add(mediaItem); + } + return mediaItems; + } + + private void fillTimeline(List mediaItems, int[] mediaQueueItemIds) { + Assertions.checkState(mediaItems.size() == mediaQueueItemIds.length); + List queueItems = new ArrayList<>(); + DefaultMediaItemConverter converter = new DefaultMediaItemConverter(); + for (MediaItem mediaItem : mediaItems) { + queueItems.add(converter.toMediaQueueItem(mediaItem)); + } + + // Set up mocks to allow the player to update the timeline. + when(mockMediaQueue.getItemIds()).thenReturn(mediaQueueItemIds); + when(mockMediaStatus.getCurrentItemId()).thenReturn(1); + when(mockMediaStatus.getMediaInfo()).thenReturn(mockMediaInfo); + when(mockMediaInfo.getStreamType()).thenReturn(MediaInfo.STREAM_TYPE_NONE); + when(mockMediaStatus.getQueueItems()).thenReturn(queueItems); + + castPlayer.addMediaItems(mediaItems); + // Call listener to update the timeline of the player. + remoteMediaClientListener.onQueueStatusUpdated(); + } } diff --git a/extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/CastTimelineTrackerTest.java b/extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/CastTimelineTrackerTest.java index 69b25e4456..cb852eb1d6 100644 --- a/extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/CastTimelineTrackerTest.java +++ b/extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/CastTimelineTrackerTest.java @@ -39,7 +39,7 @@ public class CastTimelineTrackerTest { /** Tests that duration of the current media info is correctly propagated to the timeline. */ @Test - public void testGetCastTimelinePersistsDuration() { + public void getCastTimelinePersistsDuration() { CastTimelineTracker tracker = new CastTimelineTracker(); RemoteMediaClient remoteMediaClient = diff --git a/extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/DefaultMediaItemConverterTest.java b/extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/DefaultMediaItemConverterTest.java index cf9b9d3496..9d65bada16 100644 --- a/extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/DefaultMediaItemConverterTest.java +++ b/extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/DefaultMediaItemConverterTest.java @@ -20,7 +20,9 @@ import static com.google.common.truth.Truth.assertThat; import android.net.Uri; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.ext.cast.MediaItem.DrmConfiguration; +import com.google.android.exoplayer2.MediaItem; +import com.google.android.exoplayer2.MediaMetadata; +import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.gms.cast.MediaQueueItem; import java.util.Collections; import org.junit.Test; @@ -33,7 +35,8 @@ public class DefaultMediaItemConverterTest { @Test public void serialize_deserialize_minimal() { MediaItem.Builder builder = new MediaItem.Builder(); - MediaItem item = builder.setUri(Uri.parse("http://example.com")).setMimeType("mime").build(); + MediaItem item = + builder.setUri("http://example.com").setMimeType(MimeTypes.APPLICATION_MPD).build(); DefaultMediaItemConverter converter = new DefaultMediaItemConverter(); MediaQueueItem queueItem = converter.toMediaQueueItem(item); @@ -48,13 +51,11 @@ public class DefaultMediaItemConverterTest { MediaItem item = builder .setUri(Uri.parse("http://example.com")) - .setTitle("title") - .setMimeType("mime") - .setDrmConfiguration( - new DrmConfiguration( - C.WIDEVINE_UUID, - Uri.parse("http://license.com"), - Collections.singletonMap("key", "value"))) + .setMediaMetadata(new MediaMetadata.Builder().build()) + .setMimeType(MimeTypes.APPLICATION_MPD) + .setDrmUuid(C.WIDEVINE_UUID) + .setDrmLicenseUri("http://license.com") + .setDrmLicenseRequestHeaders(Collections.singletonMap("key", "value")) .build(); DefaultMediaItemConverter converter = new DefaultMediaItemConverter(); diff --git a/extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/MediaItemTest.java b/extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/MediaItemTest.java deleted file mode 100644 index 7b410a8fbc..0000000000 --- a/extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/MediaItemTest.java +++ /dev/null @@ -1,86 +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.ext.cast; - -import static com.google.common.truth.Truth.assertThat; - -import android.net.Uri; -import androidx.test.ext.junit.runners.AndroidJUnit4; -import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.util.MimeTypes; -import java.util.HashMap; -import org.junit.Test; -import org.junit.runner.RunWith; - -/** Test for {@link MediaItem}. */ -@RunWith(AndroidJUnit4.class) -public class MediaItemTest { - - @Test - public void buildMediaItem_doesNotChangeState() { - MediaItem.Builder builder = new MediaItem.Builder(); - MediaItem item1 = - builder - .setUri(Uri.parse("http://example.com")) - .setTitle("title") - .setMimeType(MimeTypes.AUDIO_MP4) - .build(); - MediaItem item2 = builder.build(); - assertThat(item1).isEqualTo(item2); - } - - @Test - public void equals_withEqualDrmSchemes_returnsTrue() { - MediaItem.Builder builder1 = new MediaItem.Builder(); - MediaItem mediaItem1 = - builder1 - .setUri(Uri.parse("www.google.com")) - .setDrmConfiguration(buildDrmConfiguration(1)) - .build(); - MediaItem.Builder builder2 = new MediaItem.Builder(); - MediaItem mediaItem2 = - builder2 - .setUri(Uri.parse("www.google.com")) - .setDrmConfiguration(buildDrmConfiguration(1)) - .build(); - assertThat(mediaItem1).isEqualTo(mediaItem2); - } - - @Test - public void equals_withDifferentDrmRequestHeaders_returnsFalse() { - MediaItem.Builder builder1 = new MediaItem.Builder(); - MediaItem mediaItem1 = - builder1 - .setUri(Uri.parse("www.google.com")) - .setDrmConfiguration(buildDrmConfiguration(1)) - .build(); - MediaItem.Builder builder2 = new MediaItem.Builder(); - MediaItem mediaItem2 = - builder2 - .setUri(Uri.parse("www.google.com")) - .setDrmConfiguration(buildDrmConfiguration(2)) - .build(); - assertThat(mediaItem1).isNotEqualTo(mediaItem2); - } - - private static MediaItem.DrmConfiguration buildDrmConfiguration(int seed) { - HashMap requestHeaders = new HashMap<>(); - requestHeaders.put("key1", "value1"); - requestHeaders.put("key2", "value2" + seed); - return new MediaItem.DrmConfiguration( - C.WIDEVINE_UUID, Uri.parse("www.uri1.com"), requestHeaders); - } -} diff --git a/extensions/cronet/build.gradle b/extensions/cronet/build.gradle index d5b7a99f96..742b163ebf 100644 --- a/extensions/cronet/build.gradle +++ b/extensions/cronet/build.gradle @@ -35,6 +35,7 @@ dependencies { implementation project(modulePrefix + 'library-core') implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion + compileOnly 'org.jetbrains.kotlin:kotlin-annotations-jvm:' + kotlinAnnotationsVersion testImplementation project(modulePrefix + 'library') testImplementation project(modulePrefix + 'testutils') testImplementation 'org.robolectric:robolectric:' + robolectricVersion 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 1903e33995..457401f5df 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 @@ -32,6 +32,7 @@ import com.google.android.exoplayer2.util.ConditionVariable; import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.Predicate; import java.io.IOException; +import java.io.InterruptedIOException; import java.net.SocketTimeoutException; import java.net.UnknownHostException; import java.nio.ByteBuffer; @@ -83,14 +84,6 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource { } - /** Thrown on catching an InterruptedException. */ - public static final class InterruptedIOException extends IOException { - - public InterruptedIOException(InterruptedException e) { - super(e); - } - } - static { ExoPlayerLibraryInfo.registerModule("goog.exo.cronet"); } @@ -440,7 +433,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource { } } catch (InterruptedException e) { Thread.currentThread().interrupt(); - throw new OpenException(new InterruptedIOException(e), dataSpec, Status.INVALID); + throw new OpenException(new InterruptedIOException(), dataSpec, Status.INVALID); } // Check for a valid response code. @@ -705,7 +698,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource { if (dataSpec.httpBody != null && !requestHeaders.containsKey(CONTENT_TYPE)) { 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) { StringBuilder rangeValue = new StringBuilder(); @@ -769,7 +762,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource { } Thread.currentThread().interrupt(); throw new HttpDataSourceException( - new InterruptedIOException(e), + new InterruptedIOException(), castNonNull(currentDataSpec), HttpDataSourceException.TYPE_READ); } catch (SocketTimeoutException e) { @@ -819,7 +812,9 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource { if (matcher.find()) { try { long contentLengthFromRange = - Long.parseLong(matcher.group(2)) - Long.parseLong(matcher.group(1)) + 1; + Long.parseLong(Assertions.checkNotNull(matcher.group(2))) + - Long.parseLong(Assertions.checkNotNull(matcher.group(1))) + + 1; if (contentLength < 0) { // Some proxy servers strip the Content-Length header. Fall back to the length // calculated here in this case. @@ -924,16 +919,12 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource { // For POST redirects that aren't 307 or 308, the redirect is followed but request is // transformed into a GET. redirectUrlDataSpec = - new DataSpec( - Uri.parse(newLocationUrl), - DataSpec.HTTP_METHOD_GET, - /* httpBody= */ null, - dataSpec.absoluteStreamPosition, - dataSpec.position, - dataSpec.length, - dataSpec.key, - dataSpec.flags, - dataSpec.httpRequestHeaders); + dataSpec + .buildUpon() + .setUri(newLocationUrl) + .setHttpMethod(DataSpec.HTTP_METHOD_GET) + .setHttpBody(null) + .build(); } else { redirectUrlDataSpec = dataSpec.withUri(Uri.parse(newLocationUrl)); } diff --git a/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetEngineWrapper.java b/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetEngineWrapper.java index 7d549be7cb..a05dda1983 100644 --- a/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetEngineWrapper.java +++ b/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetEngineWrapper.java @@ -166,7 +166,8 @@ public final class CronetEngineWrapper { private final boolean preferGMSCoreCronet; // Multi-catch can only be used for API 19+ in this case. - @SuppressWarnings("UseMultiCatch") + // Field#get(null) is blocked by the null-checker, but is safe because the field is static. + @SuppressWarnings({"UseMultiCatch", "nullness:argument.type.incompatible"}) public CronetProviderComparator(boolean preferGMSCoreCronet) { // GMSCore CronetProvider classes are only available in some configurations. // Thus, we use reflection to copy static name. diff --git a/extensions/cronet/src/test/java/com/google/android/exoplayer2/ext/cronet/ByteArrayUploadDataProviderTest.java b/extensions/cronet/src/test/java/com/google/android/exoplayer2/ext/cronet/ByteArrayUploadDataProviderTest.java index 244ba9083b..355b4ed2a9 100644 --- a/extensions/cronet/src/test/java/com/google/android/exoplayer2/ext/cronet/ByteArrayUploadDataProviderTest.java +++ b/extensions/cronet/src/test/java/com/google/android/exoplayer2/ext/cronet/ByteArrayUploadDataProviderTest.java @@ -48,18 +48,18 @@ public final class ByteArrayUploadDataProviderTest { } @Test - public void testGetLength() { + public void getLength() { assertThat(byteArrayUploadDataProvider.getLength()).isEqualTo(TEST_DATA.length); } @Test - public void testReadFullBuffer() throws IOException { + public void readFullBuffer() throws IOException { byteArrayUploadDataProvider.read(mockUploadDataSink, byteBuffer); assertThat(byteBuffer.array()).isEqualTo(TEST_DATA); } @Test - public void testReadPartialBuffer() throws IOException { + public void readPartialBuffer() throws IOException { byte[] firstHalf = Arrays.copyOf(TEST_DATA, TEST_DATA.length / 2); byte[] secondHalf = Arrays.copyOfRange(TEST_DATA, TEST_DATA.length / 2, TEST_DATA.length); byteBuffer = ByteBuffer.allocate(TEST_DATA.length / 2); @@ -75,7 +75,7 @@ public final class ByteArrayUploadDataProviderTest { } @Test - public void testRewind() throws IOException { + public void rewind() throws IOException { // Read all the data. byteArrayUploadDataProvider.read(mockUploadDataSink, byteBuffer); assertThat(byteBuffer.array()).isEqualTo(TEST_DATA); 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 47f6fa7d2f..efcdd3c440 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 @@ -19,7 +19,7 @@ import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.fail; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.Matchers.eq; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; @@ -40,6 +40,7 @@ import com.google.android.exoplayer2.upstream.TransferListener; import com.google.android.exoplayer2.util.Clock; import com.google.android.exoplayer2.util.Util; import java.io.IOException; +import java.io.InterruptedIOException; import java.net.SocketTimeoutException; import java.net.UnknownHostException; import java.nio.ByteBuffer; @@ -63,9 +64,13 @@ import org.junit.runner.RunWith; import org.mockito.ArgumentMatchers; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import org.robolectric.annotation.LooperMode; +import org.robolectric.annotation.LooperMode.Mode; +import org.robolectric.shadows.ShadowLooper; /** Tests for {@link CronetDataSource}. */ @RunWith(AndroidJUnit4.class) +@LooperMode(Mode.PAUSED) public final class CronetDataSourceTest { private static final int TEST_CONNECT_TIMEOUT_MS = 100; @@ -120,12 +125,15 @@ public final class CronetDataSourceTest { when(mockUrlRequestBuilder.build()).thenReturn(mockUrlRequest); mockStatusResponse(); - testDataSpec = new DataSpec(Uri.parse(TEST_URL), 0, C.LENGTH_UNSET, null); + testDataSpec = new DataSpec(Uri.parse(TEST_URL)); testPostDataSpec = - new DataSpec(Uri.parse(TEST_URL), TEST_POST_BODY, 0, 0, C.LENGTH_UNSET, null, 0); + new DataSpec.Builder() + .setUri(TEST_URL) + .setHttpMethod(DataSpec.HTTP_METHOD_POST) + .setHttpBody(TEST_POST_BODY) + .build(); testHeadDataSpec = - new DataSpec( - Uri.parse(TEST_URL), DataSpec.HTTP_METHOD_HEAD, null, 0, 0, C.LENGTH_UNSET, null, 0); + new DataSpec.Builder().setUri(TEST_URL).setHttpMethod(DataSpec.HTTP_METHOD_HEAD).build(); testResponseHeader = new HashMap<>(); testResponseHeader.put("Content-Type", TEST_CONTENT_TYPE); // This value can be anything since the DataSpec is unset. @@ -151,7 +159,7 @@ public final class CronetDataSourceTest { } @Test - public void testOpeningTwiceThrows() throws HttpDataSourceException { + public void openingTwiceThrows() throws HttpDataSourceException { mockResponseStartSuccess(); dataSourceUnderTest.open(testDataSpec); try { @@ -163,7 +171,7 @@ public final class CronetDataSourceTest { } @Test - public void testCallbackFromPreviousRequest() throws HttpDataSourceException { + public void callbackFromPreviousRequest() throws HttpDataSourceException { mockResponseStartSuccess(); dataSourceUnderTest.open(testDataSpec); @@ -186,7 +194,7 @@ public final class CronetDataSourceTest { } @Test - public void testRequestStartCalled() throws HttpDataSourceException { + public void requestStartCalled() throws HttpDataSourceException { mockResponseStartSuccess(); dataSourceUnderTest.open(testDataSpec); @@ -196,8 +204,8 @@ public final class CronetDataSourceTest { } @Test - public void testRequestSetsRangeHeader() throws HttpDataSourceException { - testDataSpec = new DataSpec(Uri.parse(TEST_URL), 1000, 5000, null); + public void requestSetsRangeHeader() throws HttpDataSourceException { + testDataSpec = new DataSpec(Uri.parse(TEST_URL), 1000, 5000); mockResponseStartSuccess(); dataSourceUnderTest.open(testDataSpec); @@ -206,8 +214,7 @@ public final class CronetDataSourceTest { } @Test - public void testRequestHeadersSet() throws HttpDataSourceException { - + public void requestHeadersSet() throws HttpDataSourceException { Map headersSet = new HashMap<>(); doAnswer( (invocation) -> { @@ -227,17 +234,14 @@ public final class CronetDataSourceTest { dataSpecRequestProperties.put("defaultHeader3", "dataSpecOverridesAll"); dataSpecRequestProperties.put("dataSourceHeader2", "dataSpecOverridesDataSource"); dataSpecRequestProperties.put("dataSpecHeader1", "dataSpecValue1"); + testDataSpec = - new DataSpec( - /* uri= */ Uri.parse(TEST_URL), - /* httpMethod= */ DataSpec.HTTP_METHOD_GET, - /* httpBody= */ null, - /* absoluteStreamPosition= */ 1000, - /* position= */ 1000, - /* length= */ 5000, - /* key= */ null, - /* flags= */ 0, - dataSpecRequestProperties); + new DataSpec.Builder() + .setUri(TEST_URL) + .setPosition(1000) + .setLength(5000) + .setHttpRequestHeaders(dataSpecRequestProperties) + .build(); mockResponseStartSuccess(); dataSourceUnderTest.open(testDataSpec); @@ -253,7 +257,7 @@ public final class CronetDataSourceTest { } @Test - public void testRequestOpen() throws HttpDataSourceException { + public void requestOpen() throws HttpDataSourceException { mockResponseStartSuccess(); assertThat(dataSourceUnderTest.open(testDataSpec)).isEqualTo(TEST_CONTENT_LENGTH); verify(mockTransferListener) @@ -261,9 +265,8 @@ public final class CronetDataSourceTest { } @Test - public void testRequestOpenGzippedCompressedReturnsDataSpecLength() - throws HttpDataSourceException { - testDataSpec = new DataSpec(Uri.parse(TEST_URL), 0, 5000, null); + public void requestOpenGzippedCompressedReturnsDataSpecLength() throws HttpDataSourceException { + testDataSpec = new DataSpec(Uri.parse(TEST_URL), 0, 5000); testResponseHeader.put("Content-Encoding", "gzip"); testResponseHeader.put("Content-Length", Long.toString(50L)); mockResponseStartSuccess(); @@ -274,7 +277,7 @@ public final class CronetDataSourceTest { } @Test - public void testRequestOpenFail() { + public void requestOpenFail() { mockResponseStartFailure(); try { @@ -282,7 +285,7 @@ public final class CronetDataSourceTest { fail("HttpDataSource.HttpDataSourceException expected"); } catch (HttpDataSourceException e) { // Check for connection not automatically closed. - assertThat(e.getCause() instanceof UnknownHostException).isFalse(); + assertThat(e).hasCauseThat().isNotInstanceOf(UnknownHostException.class); verify(mockUrlRequest, never()).cancel(); verify(mockTransferListener, never()) .onTransferStart(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true); @@ -292,14 +295,14 @@ public final class CronetDataSourceTest { @Test public void open_ifBodyIsSetWithoutContentTypeHeader_fails() { testDataSpec = - new DataSpec( - /* uri= */ Uri.parse(TEST_URL), - /* postBody= */ new byte[1024], - /* absoluteStreamPosition= */ 200, - /* position= */ 200, - /* length= */ 1024, - /* key= */ "key", - /* flags= */ 0); + new DataSpec.Builder() + .setUri(TEST_URL) + .setHttpMethod(DataSpec.HTTP_METHOD_POST) + .setHttpBody(new byte[1024]) + .setPosition(200) + .setLength(1024) + .setKey("key") + .build(); try { dataSourceUnderTest.open(testDataSpec); @@ -310,7 +313,7 @@ public final class CronetDataSourceTest { } @Test - public void testRequestOpenFailDueToDnsFailure() { + public void requestOpenFailDueToDnsFailure() { mockResponseStartFailure(); when(mockNetworkException.getErrorCode()) .thenReturn(NetworkException.ERROR_HOSTNAME_NOT_RESOLVED); @@ -320,7 +323,7 @@ public final class CronetDataSourceTest { fail("HttpDataSource.HttpDataSourceException expected"); } catch (HttpDataSourceException e) { // Check for connection not automatically closed. - assertThat(e.getCause() instanceof UnknownHostException).isTrue(); + assertThat(e).hasCauseThat().isInstanceOf(UnknownHostException.class); verify(mockUrlRequest, never()).cancel(); verify(mockTransferListener, never()) .onTransferStart(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true); @@ -328,7 +331,7 @@ public final class CronetDataSourceTest { } @Test - public void testRequestOpenValidatesStatusCode() { + public void requestOpenValidatesStatusCode() { mockResponseStartSuccess(); testUrlResponseInfo = createUrlResponseInfo(500); // statusCode @@ -336,7 +339,7 @@ public final class CronetDataSourceTest { dataSourceUnderTest.open(testDataSpec); fail("HttpDataSource.HttpDataSourceException expected"); } catch (HttpDataSourceException e) { - assertThat(e instanceof HttpDataSource.InvalidResponseCodeException).isTrue(); + assertThat(e).isInstanceOf(HttpDataSource.InvalidResponseCodeException.class); // Check for connection not automatically closed. verify(mockUrlRequest, never()).cancel(); verify(mockTransferListener, never()) @@ -345,7 +348,7 @@ public final class CronetDataSourceTest { } @Test - public void testRequestOpenValidatesContentTypePredicate() { + public void requestOpenValidatesContentTypePredicate() { mockResponseStartSuccess(); ArrayList testedContentTypes = new ArrayList<>(); @@ -359,7 +362,7 @@ public final class CronetDataSourceTest { dataSourceUnderTest.open(testDataSpec); fail("HttpDataSource.HttpDataSourceException expected"); } catch (HttpDataSourceException e) { - assertThat(e instanceof HttpDataSource.InvalidContentTypeException).isTrue(); + assertThat(e).isInstanceOf(HttpDataSource.InvalidContentTypeException.class); // Check for connection not automatically closed. verify(mockUrlRequest, never()).cancel(); assertThat(testedContentTypes).hasSize(1); @@ -368,7 +371,7 @@ public final class CronetDataSourceTest { } @Test - public void testPostRequestOpen() throws HttpDataSourceException { + public void postRequestOpen() throws HttpDataSourceException { mockResponseStartSuccess(); dataSourceUnderTest.setRequestProperty("Content-Type", TEST_CONTENT_TYPE); @@ -378,7 +381,7 @@ public final class CronetDataSourceTest { } @Test - public void testPostRequestOpenValidatesContentType() { + public void postRequestOpenValidatesContentType() { mockResponseStartSuccess(); try { @@ -390,7 +393,7 @@ public final class CronetDataSourceTest { } @Test - public void testPostRequestOpenRejects307Redirects() { + public void postRequestOpenRejects307Redirects() { mockResponseStartSuccess(); mockResponseStartRedirect(); @@ -404,7 +407,7 @@ public final class CronetDataSourceTest { } @Test - public void testHeadRequestOpen() throws HttpDataSourceException { + public void headRequestOpen() throws HttpDataSourceException { mockResponseStartSuccess(); dataSourceUnderTest.open(testHeadDataSpec); verify(mockTransferListener) @@ -413,7 +416,7 @@ public final class CronetDataSourceTest { } @Test - public void testRequestReadTwice() throws HttpDataSourceException { + public void requestReadTwice() throws HttpDataSourceException { mockResponseStartSuccess(); mockReadSuccess(0, 16); @@ -436,7 +439,7 @@ public final class CronetDataSourceTest { } @Test - public void testSecondRequestNoContentLength() throws HttpDataSourceException { + public void secondRequestNoContentLength() throws HttpDataSourceException { mockResponseStartSuccess(); testResponseHeader.put("Content-Length", Long.toString(1L)); mockReadSuccess(0, 16); @@ -462,7 +465,7 @@ public final class CronetDataSourceTest { } @Test - public void testReadWithOffset() throws HttpDataSourceException { + public void readWithOffset() throws HttpDataSourceException { mockResponseStartSuccess(); mockReadSuccess(0, 16); @@ -477,11 +480,11 @@ public final class CronetDataSourceTest { } @Test - public void testRangeRequestWith206Response() throws HttpDataSourceException { + public void rangeRequestWith206Response() throws HttpDataSourceException { mockResponseStartSuccess(); mockReadSuccess(1000, 5000); testUrlResponseInfo = createUrlResponseInfo(206); // Server supports range requests. - testDataSpec = new DataSpec(Uri.parse(TEST_URL), 1000, 5000, null); + testDataSpec = new DataSpec(Uri.parse(TEST_URL), 1000, 5000); dataSourceUnderTest.open(testDataSpec); @@ -494,11 +497,11 @@ public final class CronetDataSourceTest { } @Test - public void testRangeRequestWith200Response() throws HttpDataSourceException { + public void rangeRequestWith200Response() throws HttpDataSourceException { mockResponseStartSuccess(); mockReadSuccess(0, 7000); testUrlResponseInfo = createUrlResponseInfo(200); // Server does not support range requests. - testDataSpec = new DataSpec(Uri.parse(TEST_URL), 1000, 5000, null); + testDataSpec = new DataSpec(Uri.parse(TEST_URL), 1000, 5000); dataSourceUnderTest.open(testDataSpec); @@ -511,7 +514,7 @@ public final class CronetDataSourceTest { } @Test - public void testReadWithUnsetLength() throws HttpDataSourceException { + public void readWithUnsetLength() throws HttpDataSourceException { testResponseHeader.remove("Content-Length"); mockResponseStartSuccess(); mockReadSuccess(0, 16); @@ -527,7 +530,7 @@ public final class CronetDataSourceTest { } @Test - public void testReadReturnsWhatItCan() throws HttpDataSourceException { + public void readReturnsWhatItCan() throws HttpDataSourceException { mockResponseStartSuccess(); mockReadSuccess(0, 16); @@ -542,7 +545,7 @@ public final class CronetDataSourceTest { } @Test - public void testClosedMeansClosed() throws HttpDataSourceException { + public void closedMeansClosed() throws HttpDataSourceException { mockResponseStartSuccess(); mockReadSuccess(0, 16); @@ -570,8 +573,8 @@ public final class CronetDataSourceTest { } @Test - public void testOverread() throws HttpDataSourceException { - testDataSpec = new DataSpec(Uri.parse(TEST_URL), 0, 16, null); + public void overread() throws HttpDataSourceException { + testDataSpec = new DataSpec(Uri.parse(TEST_URL), 0, 16); testResponseHeader.put("Content-Length", Long.toString(16L)); mockResponseStartSuccess(); mockReadSuccess(0, 16); @@ -623,7 +626,7 @@ public final class CronetDataSourceTest { } @Test - public void testRequestReadByteBufferTwice() throws HttpDataSourceException { + public void requestReadByteBufferTwice() throws HttpDataSourceException { mockResponseStartSuccess(); mockReadSuccess(0, 16); @@ -649,7 +652,7 @@ public final class CronetDataSourceTest { } @Test - public void testRequestIntermixRead() throws HttpDataSourceException { + public void requestIntermixRead() throws HttpDataSourceException { mockResponseStartSuccess(); // Chunking reads into parts 6, 7, 8, 9. mockReadSuccess(0, 30); @@ -691,7 +694,7 @@ public final class CronetDataSourceTest { } @Test - public void testSecondRequestNoContentLengthReadByteBuffer() throws HttpDataSourceException { + public void secondRequestNoContentLengthReadByteBuffer() throws HttpDataSourceException { mockResponseStartSuccess(); testResponseHeader.put("Content-Length", Long.toString(1L)); mockReadSuccess(0, 16); @@ -720,11 +723,11 @@ public final class CronetDataSourceTest { } @Test - public void testRangeRequestWith206ResponseReadByteBuffer() throws HttpDataSourceException { + public void rangeRequestWith206ResponseReadByteBuffer() throws HttpDataSourceException { mockResponseStartSuccess(); mockReadSuccess(1000, 5000); testUrlResponseInfo = createUrlResponseInfo(206); // Server supports range requests. - testDataSpec = new DataSpec(Uri.parse(TEST_URL), 1000, 5000, null); + testDataSpec = new DataSpec(Uri.parse(TEST_URL), 1000, 5000); dataSourceUnderTest.open(testDataSpec); @@ -738,12 +741,12 @@ public final class CronetDataSourceTest { } @Test - public void testRangeRequestWith200ResponseReadByteBuffer() throws HttpDataSourceException { + public void rangeRequestWith200ResponseReadByteBuffer() throws HttpDataSourceException { // Tests for skipping bytes. mockResponseStartSuccess(); mockReadSuccess(0, 7000); testUrlResponseInfo = createUrlResponseInfo(200); // Server does not support range requests. - testDataSpec = new DataSpec(Uri.parse(TEST_URL), 1000, 5000, null); + testDataSpec = new DataSpec(Uri.parse(TEST_URL), 1000, 5000); dataSourceUnderTest.open(testDataSpec); @@ -757,7 +760,7 @@ public final class CronetDataSourceTest { } @Test - public void testReadByteBufferWithUnsetLength() throws HttpDataSourceException { + public void readByteBufferWithUnsetLength() throws HttpDataSourceException { testResponseHeader.remove("Content-Length"); mockResponseStartSuccess(); mockReadSuccess(0, 16); @@ -775,7 +778,7 @@ public final class CronetDataSourceTest { } @Test - public void testReadByteBufferReturnsWhatItCan() throws HttpDataSourceException { + public void readByteBufferReturnsWhatItCan() throws HttpDataSourceException { mockResponseStartSuccess(); mockReadSuccess(0, 16); @@ -791,8 +794,8 @@ public final class CronetDataSourceTest { } @Test - public void testOverreadByteBuffer() throws HttpDataSourceException { - testDataSpec = new DataSpec(Uri.parse(TEST_URL), 0, 16, null); + public void overreadByteBuffer() throws HttpDataSourceException { + testDataSpec = new DataSpec(Uri.parse(TEST_URL), 0, 16); testResponseHeader.put("Content-Length", Long.toString(16L)); mockResponseStartSuccess(); mockReadSuccess(0, 16); @@ -847,7 +850,7 @@ public final class CronetDataSourceTest { } @Test - public void testClosedMeansClosedReadByteBuffer() throws HttpDataSourceException { + public void closedMeansClosedReadByteBuffer() throws HttpDataSourceException { mockResponseStartSuccess(); mockReadSuccess(0, 16); @@ -877,7 +880,7 @@ public final class CronetDataSourceTest { } @Test - public void testConnectTimeout() throws InterruptedException { + public void connectTimeout() throws InterruptedException { long startTimeMs = SystemClock.elapsedRealtime(); final ConditionVariable startCondition = buildUrlRequestStartedCondition(); final CountDownLatch timedOutLatch = new CountDownLatch(1); @@ -890,8 +893,8 @@ public final class CronetDataSourceTest { fail(); } catch (HttpDataSourceException e) { // Expected. - assertThat(e instanceof CronetDataSource.OpenException).isTrue(); - assertThat(e.getCause() instanceof SocketTimeoutException).isTrue(); + assertThat(e).isInstanceOf(CronetDataSource.OpenException.class); + assertThat(e).hasCauseThat().isInstanceOf(SocketTimeoutException.class); assertThat(((CronetDataSource.OpenException) e).cronetConnectionStatus) .isEqualTo(TEST_CONNECTION_STATUS); timedOutLatch.countDown(); @@ -903,10 +906,12 @@ public final class CronetDataSourceTest { // We should still be trying to open. assertNotCountedDown(timedOutLatch); // We should still be trying to open as we approach the timeout. - SystemClock.setCurrentTimeMillis(startTimeMs + TEST_CONNECT_TIMEOUT_MS - 1); + setSystemClockInMsAndTriggerPendingMessages( + /* nowMs= */ startTimeMs + TEST_CONNECT_TIMEOUT_MS - 1); assertNotCountedDown(timedOutLatch); // Now we timeout. - SystemClock.setCurrentTimeMillis(startTimeMs + TEST_CONNECT_TIMEOUT_MS + 10); + setSystemClockInMsAndTriggerPendingMessages( + /* nowMs= */ startTimeMs + TEST_CONNECT_TIMEOUT_MS + 10); timedOutLatch.await(); verify(mockTransferListener, never()) @@ -914,7 +919,7 @@ public final class CronetDataSourceTest { } @Test - public void testConnectInterrupted() throws InterruptedException { + public void connectInterrupted() throws InterruptedException { long startTimeMs = SystemClock.elapsedRealtime(); final ConditionVariable startCondition = buildUrlRequestStartedCondition(); final CountDownLatch timedOutLatch = new CountDownLatch(1); @@ -928,8 +933,8 @@ public final class CronetDataSourceTest { fail(); } catch (HttpDataSourceException e) { // Expected. - assertThat(e instanceof CronetDataSource.OpenException).isTrue(); - assertThat(e.getCause() instanceof CronetDataSource.InterruptedIOException).isTrue(); + assertThat(e).isInstanceOf(CronetDataSource.OpenException.class); + assertThat(e).hasCauseThat().isInstanceOf(InterruptedIOException.class); assertThat(((CronetDataSource.OpenException) e).cronetConnectionStatus) .isEqualTo(TEST_INVALID_CONNECTION_STATUS); timedOutLatch.countDown(); @@ -942,7 +947,8 @@ public final class CronetDataSourceTest { // We should still be trying to open. assertNotCountedDown(timedOutLatch); // We should still be trying to open as we approach the timeout. - SystemClock.setCurrentTimeMillis(startTimeMs + TEST_CONNECT_TIMEOUT_MS - 1); + setSystemClockInMsAndTriggerPendingMessages( + /* nowMs= */ startTimeMs + TEST_CONNECT_TIMEOUT_MS - 1); assertNotCountedDown(timedOutLatch); // Now we interrupt. thread.interrupt(); @@ -953,7 +959,7 @@ public final class CronetDataSourceTest { } @Test - public void testConnectResponseBeforeTimeout() throws Exception { + public void connectResponseBeforeTimeout() throws Exception { long startTimeMs = SystemClock.elapsedRealtime(); final ConditionVariable startCondition = buildUrlRequestStartedCondition(); final CountDownLatch openLatch = new CountDownLatch(1); @@ -976,7 +982,8 @@ public final class CronetDataSourceTest { // We should still be trying to open. assertNotCountedDown(openLatch); // We should still be trying to open as we approach the timeout. - SystemClock.setCurrentTimeMillis(startTimeMs + TEST_CONNECT_TIMEOUT_MS - 1); + setSystemClockInMsAndTriggerPendingMessages( + /* nowMs= */ startTimeMs + TEST_CONNECT_TIMEOUT_MS - 1); assertNotCountedDown(openLatch); // The response arrives just in time. dataSourceUnderTest.urlRequestCallback.onResponseStarted(mockUrlRequest, testUrlResponseInfo); @@ -985,7 +992,7 @@ public final class CronetDataSourceTest { } @Test - public void testRedirectIncreasesConnectionTimeout() throws Exception { + public void redirectIncreasesConnectionTimeout() throws Exception { long startTimeMs = SystemClock.elapsedRealtime(); final ConditionVariable startCondition = buildUrlRequestStartedCondition(); final CountDownLatch timedOutLatch = new CountDownLatch(1); @@ -999,8 +1006,8 @@ public final class CronetDataSourceTest { fail(); } catch (HttpDataSourceException e) { // Expected. - assertThat(e instanceof CronetDataSource.OpenException).isTrue(); - assertThat(e.getCause() instanceof SocketTimeoutException).isTrue(); + assertThat(e).isInstanceOf(CronetDataSource.OpenException.class); + assertThat(e).hasCauseThat().isInstanceOf(SocketTimeoutException.class); openExceptions.getAndIncrement(); timedOutLatch.countDown(); } @@ -1011,14 +1018,15 @@ public final class CronetDataSourceTest { // We should still be trying to open. assertNotCountedDown(timedOutLatch); // We should still be trying to open as we approach the timeout. - SystemClock.setCurrentTimeMillis(startTimeMs + TEST_CONNECT_TIMEOUT_MS - 1); + setSystemClockInMsAndTriggerPendingMessages( + /* nowMs= */ startTimeMs + TEST_CONNECT_TIMEOUT_MS - 1); assertNotCountedDown(timedOutLatch); // A redirect arrives just in time. dataSourceUnderTest.urlRequestCallback.onRedirectReceived( mockUrlRequest, testUrlResponseInfo, "RandomRedirectedUrl1"); long newTimeoutMs = 2 * TEST_CONNECT_TIMEOUT_MS - 1; - SystemClock.setCurrentTimeMillis(startTimeMs + newTimeoutMs - 1); + setSystemClockInMsAndTriggerPendingMessages(/* nowMs= */ startTimeMs + newTimeoutMs - 1); // We should still be trying to open as we approach the new timeout. assertNotCountedDown(timedOutLatch); // A redirect arrives just in time. @@ -1026,11 +1034,11 @@ public final class CronetDataSourceTest { mockUrlRequest, testUrlResponseInfo, "RandomRedirectedUrl2"); newTimeoutMs = 3 * TEST_CONNECT_TIMEOUT_MS - 2; - SystemClock.setCurrentTimeMillis(startTimeMs + newTimeoutMs - 1); + setSystemClockInMsAndTriggerPendingMessages(/* nowMs= */ startTimeMs + newTimeoutMs - 1); // We should still be trying to open as we approach the new timeout. assertNotCountedDown(timedOutLatch); // Now we timeout. - SystemClock.setCurrentTimeMillis(startTimeMs + newTimeoutMs + 10); + setSystemClockInMsAndTriggerPendingMessages(/* nowMs= */ startTimeMs + newTimeoutMs + 10); timedOutLatch.await(); verify(mockTransferListener, never()) @@ -1039,7 +1047,7 @@ public final class CronetDataSourceTest { } @Test - public void testRedirectParseAndAttachCookie_dataSourceDoesNotHandleSetCookie_followsRedirect() + public void redirectParseAndAttachCookie_dataSourceDoesNotHandleSetCookie_followsRedirect() throws HttpDataSourceException { mockSingleRedirectSuccess(); mockFollowRedirectSuccess(); @@ -1084,7 +1092,7 @@ public final class CronetDataSourceTest { public void testRedirectParseAndAttachCookie_dataSourceHandlesSetCookie_andPreservesOriginalRequestHeadersIncludingByteRangeHeader() throws HttpDataSourceException { - testDataSpec = new DataSpec(Uri.parse(TEST_URL), 1000, 5000, null); + testDataSpec = new DataSpec(Uri.parse(TEST_URL), 1000, 5000); dataSourceUnderTest = new CronetDataSource( mockCronetEngine, @@ -1111,7 +1119,7 @@ public final class CronetDataSourceTest { } @Test - public void testRedirectNoSetCookieFollowsRedirect() throws HttpDataSourceException { + public void redirectNoSetCookieFollowsRedirect() throws HttpDataSourceException { mockSingleRedirectSuccess(); mockFollowRedirectSuccess(); @@ -1121,7 +1129,7 @@ public final class CronetDataSourceTest { } @Test - public void testRedirectNoSetCookieFollowsRedirect_dataSourceHandlesSetCookie() + public void redirectNoSetCookieFollowsRedirect_dataSourceHandlesSetCookie() throws HttpDataSourceException { dataSourceUnderTest = new CronetDataSource( @@ -1143,7 +1151,7 @@ public final class CronetDataSourceTest { } @Test - public void testExceptionFromTransferListener() throws HttpDataSourceException { + public void exceptionFromTransferListener() throws HttpDataSourceException { mockResponseStartSuccess(); // Make mockTransferListener throw an exception in CronetDataSource.close(). Ensure that @@ -1163,7 +1171,7 @@ public final class CronetDataSourceTest { } @Test - public void testReadFailure() throws HttpDataSourceException { + public void readFailure() throws HttpDataSourceException { mockResponseStartSuccess(); mockReadFailure(); @@ -1178,7 +1186,7 @@ public final class CronetDataSourceTest { } @Test - public void testReadByteBufferFailure() throws HttpDataSourceException { + public void readByteBufferFailure() throws HttpDataSourceException { mockResponseStartSuccess(); mockReadFailure(); @@ -1193,7 +1201,7 @@ public final class CronetDataSourceTest { } @Test - public void testReadNonDirectedByteBufferFailure() throws HttpDataSourceException { + public void readNonDirectedByteBufferFailure() throws HttpDataSourceException { mockResponseStartSuccess(); mockReadFailure(); @@ -1208,7 +1216,7 @@ public final class CronetDataSourceTest { } @Test - public void testReadInterrupted() throws HttpDataSourceException, InterruptedException { + public void readInterrupted() throws HttpDataSourceException, InterruptedException { mockResponseStartSuccess(); dataSourceUnderTest.open(testDataSpec); @@ -1224,7 +1232,7 @@ public final class CronetDataSourceTest { fail(); } catch (HttpDataSourceException e) { // Expected. - assertThat(e.getCause() instanceof CronetDataSource.InterruptedIOException).isTrue(); + assertThat(e).hasCauseThat().isInstanceOf(InterruptedIOException.class); timedOutLatch.countDown(); } } @@ -1239,7 +1247,7 @@ public final class CronetDataSourceTest { } @Test - public void testReadByteBufferInterrupted() throws HttpDataSourceException, InterruptedException { + public void readByteBufferInterrupted() throws HttpDataSourceException, InterruptedException { mockResponseStartSuccess(); dataSourceUnderTest.open(testDataSpec); @@ -1255,7 +1263,7 @@ public final class CronetDataSourceTest { fail(); } catch (HttpDataSourceException e) { // Expected. - assertThat(e.getCause() instanceof CronetDataSource.InterruptedIOException).isTrue(); + assertThat(e).hasCauseThat().isInstanceOf(InterruptedIOException.class); timedOutLatch.countDown(); } } @@ -1270,8 +1278,8 @@ public final class CronetDataSourceTest { } @Test - public void testAllowDirectExecutor() throws HttpDataSourceException { - testDataSpec = new DataSpec(Uri.parse(TEST_URL), 1000, 5000, null); + public void allowDirectExecutor() throws HttpDataSourceException { + testDataSpec = new DataSpec(Uri.parse(TEST_URL), 1000, 5000); mockResponseStartSuccess(); dataSourceUnderTest.open(testDataSpec); @@ -1460,4 +1468,9 @@ public final class CronetDataSourceTest { } return copy; } + + private static void setSystemClockInMsAndTriggerPendingMessages(long nowMs) { + SystemClock.setCurrentTimeMillis(nowMs); + ShadowLooper.idleMainLooper(); + } } diff --git a/extensions/ffmpeg/README.md b/extensions/ffmpeg/README.md index fe4aca772a..f6e3944572 100644 --- a/extensions/ffmpeg/README.md +++ b/extensions/ffmpeg/README.md @@ -35,22 +35,22 @@ FFMPEG_EXT_PATH="$(pwd)/extensions/ffmpeg/src/main/jni" NDK_PATH="" ``` -* Set up host platform ("darwin-x86_64" for Mac OS X): +* Set the host platform (use "darwin-x86_64" for Mac OS X): ``` HOST_PLATFORM="linux-x86_64" ``` -* Configure the formats supported by adapting the following variable if needed - and by setting it. See the [Supported formats][] page for more details of the - formats. +* Configure the decoders to include. See the [Supported formats][] page for + details of the available decoders, and which formats they support. ``` ENABLED_DECODERS=(vorbis opus flac) ``` -* Fetch and build FFmpeg. For example, executing script `build_ffmpeg.sh` will - fetch and build FFmpeg release 4.2 for armeabi-v7a, arm64-v8a and x86: +* Fetch and build FFmpeg. Executing `build_ffmpeg.sh` will fetch and build + FFmpeg 4.2 for `armeabi-v7a`, `arm64-v8a`, `x86` and `x86_64`. The script can + be edited if you need to build for different architectures. ``` cd "${FFMPEG_EXT_PATH}" && \ @@ -63,7 +63,7 @@ cd "${FFMPEG_EXT_PATH}" && \ ``` cd "${FFMPEG_EXT_PATH}" && \ -${NDK_PATH}/ndk-build APP_ABI="armeabi-v7a arm64-v8a x86" -j4 +${NDK_PATH}/ndk-build APP_ABI="armeabi-v7a arm64-v8a x86 x86_64" -j4 ``` ## Build instructions (Windows) ## @@ -106,9 +106,19 @@ then implement your own logic to use the renderer for a given track. [#2781]: https://github.com/google/ExoPlayer/issues/2781 [Supported formats]: https://exoplayer.dev/supported-formats.html#ffmpeg-extension +## Using the extension in the demo application ## + +To try out playback using the extension in the [demo application][], see +[enabling extension decoders][]. + +[demo application]: https://exoplayer.dev/demo-application.html +[enabling extension decoders]: https://exoplayer.dev/demo-application.html#enabling-extension-decoders + ## Links ## +* [Troubleshooting using extensions][] * [Javadoc][]: Classes matching `com.google.android.exoplayer2.ext.ffmpeg.*` belong to this module. +[Troubleshooting using extensions]: https://exoplayer.dev/troubleshooting.html#how-can-i-get-a-decoding-extension-to-load-and-be-used-for-playback [Javadoc]: https://exoplayer.dev/doc/reference/index.html diff --git a/extensions/ffmpeg/build.gradle b/extensions/ffmpeg/build.gradle index 657fa75c24..26a72ae335 100644 --- a/extensions/ffmpeg/build.gradle +++ b/extensions/ffmpeg/build.gradle @@ -40,6 +40,7 @@ dependencies { implementation project(modulePrefix + 'library-core') implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion + compileOnly 'org.jetbrains.kotlin:kotlin-annotations-jvm:' + kotlinAnnotationsVersion testImplementation project(modulePrefix + 'testutils') testImplementation 'org.robolectric:robolectric:' + robolectricVersion } 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/FfmpegAudioDecoder.java similarity index 88% rename from extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegDecoder.java rename to extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegAudioDecoder.java index 5314835d1e..c5072a3398 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/FfmpegAudioDecoder.java @@ -28,19 +28,18 @@ import com.google.android.exoplayer2.util.Util; import java.nio.ByteBuffer; import java.util.List; -/** - * FFmpeg audio decoder. - */ -/* package */ final class FfmpegDecoder extends - SimpleDecoder { +/** FFmpeg audio decoder. */ +/* package */ final class FfmpegAudioDecoder + extends SimpleDecoder { // 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; - // Error codes matching ffmpeg_jni.cc. - private static final int DECODER_ERROR_INVALID_DATA = -1; - private static final int DECODER_ERROR_OTHER = -2; + // LINT.IfChange + private static final int AUDIO_DECODER_ERROR_INVALID_DATA = -1; + private static final int AUDIO_DECODER_ERROR_OTHER = -2; + // LINT.ThenChange(../../../../../../../jni/ffmpeg_jni.cc) private final String codecName; @Nullable private final byte[] extraData; @@ -52,7 +51,7 @@ import java.util.List; private volatile int channelCount; private volatile int sampleRate; - public FfmpegDecoder( + public FfmpegAudioDecoder( int numInputBuffers, int numOutputBuffers, int initialInputBufferSize, @@ -64,9 +63,7 @@ import java.util.List; throw new FfmpegDecoderException("Failed to load decoder native libraries."); } Assertions.checkNotNull(format.sampleMimeType); - codecName = - Assertions.checkNotNull( - FfmpegLibrary.getCodecName(format.sampleMimeType, format.pcmEncoding)); + codecName = Assertions.checkNotNull(FfmpegLibrary.getCodecName(format.sampleMimeType)); 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; @@ -90,7 +87,7 @@ import java.util.List; @Override protected SimpleOutputBuffer createOutputBuffer() { - return new SimpleOutputBuffer(this); + return new SimpleOutputBuffer(this::releaseOutputBuffer); } @Override @@ -111,13 +108,13 @@ import java.util.List; int inputSize = inputData.limit(); ByteBuffer outputData = outputBuffer.init(inputBuffer.timeUs, outputBufferSize); int result = ffmpegDecode(nativeContext, inputData, inputSize, outputData, outputBufferSize); - if (result == DECODER_ERROR_INVALID_DATA) { + if (result == AUDIO_DECODER_ERROR_INVALID_DATA) { // Treat invalid data errors as non-fatal to match the behavior of MediaCodec. No output will // be produced for this buffer, so mark it as decode-only to ensure that the audio sink's // position is reset when more audio is produced. outputBuffer.setFlags(C.BUFFER_FLAG_DECODE_ONLY); return null; - } else if (result == DECODER_ERROR_OTHER) { + } else if (result == AUDIO_DECODER_ERROR_OTHER) { return new FfmpegDecoderException("Error decoding (see logcat)."); } if (!hasOutputFormat) { @@ -125,8 +122,8 @@ import java.util.List; 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 + // 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); parsableExtraData.setPosition(extraData.length - 4); sampleRate = parsableExtraData.readUnsignedIntToInt(); @@ -145,23 +142,17 @@ import java.util.List; nativeContext = 0; } - /** - * Returns the channel count of output audio. May only be called after {@link #decode}. - */ + /** Returns the channel count of output audio. */ public int getChannelCount() { return channelCount; } - /** - * Returns the sample rate of output audio. May only be called after {@link #decode}. - */ + /** Returns the sample rate of output audio. */ public int getSampleRate() { return sampleRate; } - /** - * Returns the encoding of output audio. - */ + /** Returns the encoding of output audio. */ public @C.Encoding int getEncoding() { return encoding; } @@ -223,13 +214,14 @@ import java.util.List; int rawSampleRate, int rawChannelCount); - private native int ffmpegDecode(long context, ByteBuffer inputData, int inputSize, - ByteBuffer outputData, int outputSize); + 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, @Nullable byte[] extraData); private native void ffmpegRelease(long context); - } 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 39d1ee4094..4bb64cbaa0 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 @@ -18,24 +18,22 @@ package com.google.android.exoplayer2.ext.ffmpeg; import android.os.Handler; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.audio.AudioProcessor; import com.google.android.exoplayer2.audio.AudioRendererEventListener; import com.google.android.exoplayer2.audio.AudioSink; +import com.google.android.exoplayer2.audio.DecoderAudioRenderer; 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 com.google.android.exoplayer2.util.TraceUtil; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; -/** - * Decodes and renders audio using FFmpeg. - */ -public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer { +/** Decodes and renders audio using FFmpeg. */ +public final class FfmpegAudioRenderer extends DecoderAudioRenderer { + + private static final String TAG = "FfmpegAudioRenderer"; /** The number of input and output buffers. */ private static final int NUM_BUFFERS = 16; @@ -44,13 +42,15 @@ public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer { private final boolean enableFloatOutput; - private @MonotonicNonNull FfmpegDecoder decoder; + private @MonotonicNonNull FfmpegAudioDecoder decoder; public FfmpegAudioRenderer() { this(/* eventHandler= */ null, /* eventListener= */ null); } /** + * Creates a new instance. + * * @param eventHandler A handler to use when delivering events to {@code eventListener}. 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. @@ -68,6 +68,8 @@ public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer { } /** + * Creates a new instance. + * * @param eventHandler A handler to use when delivering events to {@code eventListener}. 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. @@ -85,22 +87,24 @@ public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer { super( eventHandler, eventListener, - /* drmSessionManager= */ null, - /* playClearSamplesWithoutKeys= */ false, audioSink); this.enableFloatOutput = enableFloatOutput; } @Override - protected int supportsFormatInternal( - @Nullable DrmSessionManager drmSessionManager, Format format) { - Assertions.checkNotNull(format.sampleMimeType); - if (!FfmpegLibrary.isAvailable()) { + public String getName() { + return TAG; + } + + @Override + @FormatSupport + protected int supportsFormatInternal(Format format) { + String mimeType = Assertions.checkNotNull(format.sampleMimeType); + if (!FfmpegLibrary.isAvailable() || !MimeTypes.isAudio(mimeType)) { return FORMAT_UNSUPPORTED_TYPE; - } else if (!FfmpegLibrary.supportsFormat(format.sampleMimeType, format.pcmEncoding) - || !isOutputSupported(format)) { + } else if (!FfmpegLibrary.supportsFormat(mimeType) || !isOutputSupported(format)) { return FORMAT_UNSUPPORTED_SUBTYPE; - } else if (!supportsFormatDrm(drmSessionManager, format.drmInitData)) { + } else if (format.drmInitData != null && format.exoMediaCryptoType == null) { return FORMAT_UNSUPPORTED_DRM; } else { return FORMAT_HANDLED; @@ -108,40 +112,33 @@ public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer { } @Override - public final int supportsMixedMimeTypeAdaptation() throws ExoPlaybackException { + @AdaptiveSupport + public final int supportsMixedMimeTypeAdaptation() { return ADAPTIVE_NOT_SEAMLESS; } @Override - protected FfmpegDecoder createDecoder(Format format, @Nullable ExoMediaCrypto mediaCrypto) + protected FfmpegAudioDecoder createDecoder(Format format, @Nullable ExoMediaCrypto mediaCrypto) throws FfmpegDecoderException { + TraceUtil.beginSection("createFfmpegAudioDecoder"); int initialInputBufferSize = format.maxInputSize != Format.NO_VALUE ? format.maxInputSize : DEFAULT_INPUT_BUFFER_SIZE; decoder = - new FfmpegDecoder( + new FfmpegAudioDecoder( NUM_BUFFERS, NUM_BUFFERS, initialInputBufferSize, format, shouldUseFloatOutput(format)); + TraceUtil.endSection(); 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( - /* id= */ null, - MimeTypes.AUDIO_RAW, - /* codecs= */ null, - Format.NO_VALUE, - Format.NO_VALUE, - channelCount, - sampleRate, - encoding, - Collections.emptyList(), - /* drmInitData= */ null, - /* selectionFlags= */ 0, - /* language= */ null); + return new Format.Builder() + .setSampleMimeType(MimeTypes.AUDIO_RAW) + .setChannelCount(decoder.getChannelCount()) + .setSampleRate(decoder.getSampleRate()) + .setPcmEncoding(decoder.getEncoding()) + .build(); } private boolean isOutputSupported(Format inputFormat) { diff --git a/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegDecoderException.java b/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegDecoderException.java index d6b5a62450..47d5017350 100644 --- a/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegDecoderException.java +++ b/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegDecoderException.java @@ -15,12 +15,10 @@ */ package com.google.android.exoplayer2.ext.ffmpeg; -import com.google.android.exoplayer2.audio.AudioDecoderException; +import com.google.android.exoplayer2.decoder.DecoderException; -/** - * Thrown when an FFmpeg decoder error occurs. - */ -public final class FfmpegDecoderException extends AudioDecoderException { +/** Thrown when an FFmpeg decoder error occurs. */ +public final class FfmpegDecoderException extends DecoderException { /* package */ FfmpegDecoderException(String message) { super(message); 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 5b816b8c20..cc2a78ae86 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 @@ -16,7 +16,6 @@ package com.google.android.exoplayer2.ext.ffmpeg; import androidx.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.Log; @@ -34,14 +33,14 @@ public final class FfmpegLibrary { private static final String TAG = "FfmpegLibrary"; private static final LibraryLoader LOADER = - new LibraryLoader("avutil", "avresample", "swresample", "avcodec", "ffmpeg"); + new LibraryLoader("avutil", "swresample", "avcodec", "ffmpeg"); private FfmpegLibrary() {} /** * Override the names of the FFmpeg native libraries. If an application wishes to call this * method, it must do so before calling any other method defined by this class, and before - * instantiating a {@link FfmpegAudioRenderer} instance. + * instantiating a {@link FfmpegAudioRenderer} or {@link FfmpegVideoRenderer} instance. * * @param libraries The names of the FFmpeg native libraries. */ @@ -57,7 +56,8 @@ public final class FfmpegLibrary { } /** Returns the version of the underlying library if available, or null otherwise. */ - public static @Nullable String getVersion() { + @Nullable + public static String getVersion() { return isAvailable() ? ffmpegGetVersion() : null; } @@ -65,13 +65,12 @@ 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, @C.PcmEncoding int encoding) { + public static boolean supportsFormat(String mimeType) { if (!isAvailable()) { return false; } - String codecName = getCodecName(mimeType, encoding); + @Nullable String codecName = getCodecName(mimeType); if (codecName == null) { return false; } @@ -86,7 +85,8 @@ public final class FfmpegLibrary { * Returns the name of the FFmpeg decoder that could be used to decode the format, or {@code null} * if it's unsupported. */ - /* package */ static @Nullable String getCodecName(String mimeType, @C.PcmEncoding int encoding) { + @Nullable + /* package */ static String getCodecName(String mimeType) { switch (mimeType) { case MimeTypes.AUDIO_AAC: return "aac"; @@ -116,14 +116,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; - } + case MimeTypes.AUDIO_MLAW: + return "pcm_mulaw"; + case MimeTypes.AUDIO_ALAW: + return "pcm_alaw"; + case MimeTypes.VIDEO_H264: + return "h264"; + case MimeTypes.VIDEO_H265: + return "hevc"; default: return null; } diff --git a/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegVideoRenderer.java b/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegVideoRenderer.java new file mode 100644 index 0000000000..6f3b8b1fc7 --- /dev/null +++ b/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegVideoRenderer.java @@ -0,0 +1,122 @@ +/* + * Copyright (C) 2020 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.ffmpeg; + +import android.os.Handler; +import android.view.Surface; +import androidx.annotation.Nullable; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.RendererCapabilities; +import com.google.android.exoplayer2.decoder.Decoder; +import com.google.android.exoplayer2.drm.ExoMediaCrypto; +import com.google.android.exoplayer2.util.TraceUtil; +import com.google.android.exoplayer2.util.Util; +import com.google.android.exoplayer2.video.DecoderVideoRenderer; +import com.google.android.exoplayer2.video.VideoDecoderInputBuffer; +import com.google.android.exoplayer2.video.VideoDecoderOutputBuffer; +import com.google.android.exoplayer2.video.VideoRendererEventListener; + +// TODO: Remove the NOTE below. +/** + * NOTE: This class if under development and is not yet functional. + * + *

Decodes and renders video using FFmpeg. + */ +public final class FfmpegVideoRenderer extends DecoderVideoRenderer { + + private static final String TAG = "FfmpegAudioRenderer"; + + /** + * Creates a new instance. + * + * @param allowedJoiningTimeMs The maximum duration in milliseconds for which this video renderer + * can attempt to seamlessly join an ongoing playback. + * @param eventHandler A handler to use when delivering events to {@code eventListener}. 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 maxDroppedFramesToNotify The maximum number of frames that can be dropped between + * invocations of {@link VideoRendererEventListener#onDroppedFrames(int, long)}. + */ + public FfmpegVideoRenderer( + long allowedJoiningTimeMs, + @Nullable Handler eventHandler, + @Nullable VideoRendererEventListener eventListener, + int maxDroppedFramesToNotify) { + super(allowedJoiningTimeMs, eventHandler, eventListener, maxDroppedFramesToNotify); + // TODO: Implement. + } + + @Override + public String getName() { + return TAG; + } + + @Override + @RendererCapabilities.Capabilities + public final int supportsFormat(Format format) { + // TODO: Remove this line and uncomment the implementation below. + return FORMAT_UNSUPPORTED_TYPE; + /* + String mimeType = Assertions.checkNotNull(format.sampleMimeType); + if (!FfmpegLibrary.isAvailable() || !MimeTypes.isVideo(mimeType)) { + return FORMAT_UNSUPPORTED_TYPE; + } else if (!FfmpegLibrary.supportsFormat(format.sampleMimeType)) { + return RendererCapabilities.create(FORMAT_UNSUPPORTED_SUBTYPE); + } else if (format.drmInitData != null && format.exoMediaCryptoType == null) { + return RendererCapabilities.create(FORMAT_UNSUPPORTED_DRM); + } else { + return RendererCapabilities.create( + FORMAT_HANDLED, + ADAPTIVE_SEAMLESS, + TUNNELING_NOT_SUPPORTED); + } + */ + } + + @SuppressWarnings("return.type.incompatible") + @Override + protected Decoder + createDecoder(Format format, @Nullable ExoMediaCrypto mediaCrypto) + throws FfmpegDecoderException { + TraceUtil.beginSection("createFfmpegVideoDecoder"); + // TODO: Implement, remove the SuppressWarnings annotation, and update the return type to use + // the concrete type of the decoder (probably FfmepgVideoDecoder). + TraceUtil.endSection(); + return null; + } + + @Override + protected void renderOutputBufferToSurface(VideoDecoderOutputBuffer outputBuffer, Surface surface) + throws FfmpegDecoderException { + // TODO: Implement. + } + + @Override + protected void setDecoderOutputMode(@C.VideoOutputMode int outputMode) { + // TODO: Uncomment the implementation below. + /* + if (decoder != null) { + decoder.setOutputMode(outputMode); + } + */ + } + + @Override + protected boolean canKeepCodec(Format oldFormat, Format newFormat) { + return Util.areEqual(oldFormat.sampleMimeType, newFormat.sampleMimeType); + } +} diff --git a/extensions/ffmpeg/src/main/jni/Android.mk b/extensions/ffmpeg/src/main/jni/Android.mk index 22a4edcdae..bcaf12cd11 100644 --- a/extensions/ffmpeg/src/main/jni/Android.mk +++ b/extensions/ffmpeg/src/main/jni/Android.mk @@ -21,11 +21,6 @@ LOCAL_MODULE := libavcodec LOCAL_SRC_FILES := ffmpeg/android-libs/$(TARGET_ARCH_ABI)/$(LOCAL_MODULE).so include $(PREBUILT_SHARED_LIBRARY) -include $(CLEAR_VARS) -LOCAL_MODULE := libavresample -LOCAL_SRC_FILES := ffmpeg/android-libs/$(TARGET_ARCH_ABI)/$(LOCAL_MODULE).so -include $(PREBUILT_SHARED_LIBRARY) - include $(CLEAR_VARS) LOCAL_MODULE := libswresample LOCAL_SRC_FILES := ffmpeg/android-libs/$(TARGET_ARCH_ABI)/$(LOCAL_MODULE).so @@ -40,6 +35,6 @@ include $(CLEAR_VARS) LOCAL_MODULE := ffmpeg LOCAL_SRC_FILES := ffmpeg_jni.cc LOCAL_C_INCLUDES := ffmpeg -LOCAL_SHARED_LIBRARIES := libavcodec libavresample libswresample libavutil +LOCAL_SHARED_LIBRARIES := libavcodec libswresample libavutil LOCAL_LDLIBS := -Lffmpeg/android-libs/$(TARGET_ARCH_ABI) -llog include $(BUILD_SHARED_LIBRARY) diff --git a/extensions/ffmpeg/src/main/jni/build_ffmpeg.sh b/extensions/ffmpeg/src/main/jni/build_ffmpeg.sh index a76fa0e589..833ea189b2 100755 --- a/extensions/ffmpeg/src/main/jni/build_ffmpeg.sh +++ b/extensions/ffmpeg/src/main/jni/build_ffmpeg.sh @@ -32,8 +32,9 @@ COMMON_OPTIONS=" --disable-postproc --disable-avfilter --disable-symver - --enable-avresample + --disable-avresample --enable-swresample + --extra-ldexeflags=-pie " TOOLCHAIN_PREFIX="${NDK_PATH}/toolchains/llvm/prebuilt/${HOST_PLATFORM}/bin" for decoder in "${ENABLED_DECODERS[@]}" @@ -53,7 +54,6 @@ git checkout release/4.2 --strip="${TOOLCHAIN_PREFIX}/arm-linux-androideabi-strip" \ --extra-cflags="-march=armv7-a -mfloat-abi=softfp" \ --extra-ldflags="-Wl,--fix-cortex-a8" \ - --extra-ldexeflags=-pie \ ${COMMON_OPTIONS} make -j4 make install-libs @@ -65,7 +65,6 @@ make clean --cross-prefix="${TOOLCHAIN_PREFIX}/aarch64-linux-android21-" \ --nm="${TOOLCHAIN_PREFIX}/aarch64-linux-android-nm" \ --strip="${TOOLCHAIN_PREFIX}/aarch64-linux-android-strip" \ - --extra-ldexeflags=-pie \ ${COMMON_OPTIONS} make -j4 make install-libs @@ -77,7 +76,18 @@ make clean --cross-prefix="${TOOLCHAIN_PREFIX}/i686-linux-android16-" \ --nm="${TOOLCHAIN_PREFIX}/i686-linux-android-nm" \ --strip="${TOOLCHAIN_PREFIX}/i686-linux-android-strip" \ - --extra-ldexeflags=-pie \ + --disable-asm \ + ${COMMON_OPTIONS} +make -j4 +make install-libs +make clean +./configure \ + --libdir=android-libs/x86_64 \ + --arch=x86_64 \ + --cpu=x86_64 \ + --cross-prefix="${TOOLCHAIN_PREFIX}/x86_64-linux-android21-" \ + --nm="${TOOLCHAIN_PREFIX}/x86_64-linux-android-nm" \ + --strip="${TOOLCHAIN_PREFIX}/x86_64-linux-android-strip" \ --disable-asm \ ${COMMON_OPTIONS} make -j4 diff --git a/extensions/ffmpeg/src/main/jni/ffmpeg_jni.cc b/extensions/ffmpeg/src/main/jni/ffmpeg_jni.cc index dcd4560e4a..adbf515f9b 100644 --- a/extensions/ffmpeg/src/main/jni/ffmpeg_jni.cc +++ b/extensions/ffmpeg/src/main/jni/ffmpeg_jni.cc @@ -26,35 +26,35 @@ extern "C" { #include #endif #include -#include #include #include #include +#include } #define LOG_TAG "ffmpeg_jni" #define LOGE(...) ((void)__android_log_print(ANDROID_LOG_ERROR, LOG_TAG, \ __VA_ARGS__)) -#define DECODER_FUNC(RETURN_TYPE, NAME, ...) \ - extern "C" { \ - JNIEXPORT RETURN_TYPE \ - Java_com_google_android_exoplayer2_ext_ffmpeg_FfmpegDecoder_ ## NAME \ - (JNIEnv* env, jobject thiz, ##__VA_ARGS__);\ - } \ - JNIEXPORT RETURN_TYPE \ - Java_com_google_android_exoplayer2_ext_ffmpeg_FfmpegDecoder_ ## NAME \ - (JNIEnv* env, jobject thiz, ##__VA_ARGS__)\ +#define LIBRARY_FUNC(RETURN_TYPE, NAME, ...) \ + extern "C" { \ + JNIEXPORT RETURN_TYPE \ + Java_com_google_android_exoplayer2_ext_ffmpeg_FfmpegLibrary_##NAME( \ + JNIEnv *env, jobject thiz, ##__VA_ARGS__); \ + } \ + JNIEXPORT RETURN_TYPE \ + Java_com_google_android_exoplayer2_ext_ffmpeg_FfmpegLibrary_##NAME( \ + JNIEnv *env, jobject thiz, ##__VA_ARGS__) -#define LIBRARY_FUNC(RETURN_TYPE, NAME, ...) \ - extern "C" { \ - JNIEXPORT RETURN_TYPE \ - Java_com_google_android_exoplayer2_ext_ffmpeg_FfmpegLibrary_ ## NAME \ - (JNIEnv* env, jobject thiz, ##__VA_ARGS__);\ - } \ - JNIEXPORT RETURN_TYPE \ - Java_com_google_android_exoplayer2_ext_ffmpeg_FfmpegLibrary_ ## NAME \ - (JNIEnv* env, jobject thiz, ##__VA_ARGS__)\ +#define AUDIO_DECODER_FUNC(RETURN_TYPE, NAME, ...) \ + extern "C" { \ + JNIEXPORT RETURN_TYPE \ + Java_com_google_android_exoplayer2_ext_ffmpeg_FfmpegAudioDecoder_##NAME( \ + JNIEnv *env, jobject thiz, ##__VA_ARGS__); \ + } \ + JNIEXPORT RETURN_TYPE \ + Java_com_google_android_exoplayer2_ext_ffmpeg_FfmpegAudioDecoder_##NAME( \ + JNIEnv *env, jobject thiz, ##__VA_ARGS__) #define ERROR_STRING_BUFFER_LENGTH 256 @@ -63,9 +63,10 @@ static const AVSampleFormat OUTPUT_FORMAT_PCM_16BIT = AV_SAMPLE_FMT_S16; // Output format corresponding to AudioFormat.ENCODING_PCM_FLOAT. static const AVSampleFormat OUTPUT_FORMAT_PCM_FLOAT = AV_SAMPLE_FMT_FLT; -// Error codes matching FfmpegDecoder.java. -static const int DECODER_ERROR_INVALID_DATA = -1; -static const int DECODER_ERROR_OTHER = -2; +// LINT.IfChange +static const int AUDIO_DECODER_ERROR_INVALID_DATA = -1; +static const int AUDIO_DECODER_ERROR_OTHER = -2; +// LINT.ThenChange(../java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegAudioDecoder.java) /** * Returns the AVCodec with the specified name, or NULL if it is not available. @@ -83,7 +84,8 @@ AVCodecContext *createContext(JNIEnv *env, AVCodec *codec, jbyteArray extraData, /** * Decodes the packet into the output buffer, returning the number of bytes - * written, or a negative DECODER_ERROR constant value in the case of an error. + * written, or a negative AUDIO_DECODER_ERROR constant value in the case of an + * error. */ int decodePacket(AVCodecContext *context, AVPacket *packet, uint8_t *outputBuffer, int outputSize); @@ -115,8 +117,9 @@ LIBRARY_FUNC(jboolean, ffmpegHasDecoder, jstring codecName) { return getCodecByName(env, codecName) != NULL; } -DECODER_FUNC(jlong, ffmpegInitialize, jstring codecName, jbyteArray extraData, - jboolean outputFloat, jint rawSampleRate, jint rawChannelCount) { +AUDIO_DECODER_FUNC(jlong, ffmpegInitialize, jstring codecName, + jbyteArray extraData, jboolean outputFloat, + jint rawSampleRate, jint rawChannelCount) { AVCodec *codec = getCodecByName(env, codecName); if (!codec) { LOGE("Codec not found."); @@ -126,8 +129,8 @@ DECODER_FUNC(jlong, ffmpegInitialize, jstring codecName, jbyteArray extraData, rawChannelCount); } -DECODER_FUNC(jint, ffmpegDecode, jlong context, jobject inputData, - jint inputSize, jobject outputData, jint outputSize) { +AUDIO_DECODER_FUNC(jint, ffmpegDecode, jlong context, jobject inputData, + jint inputSize, jobject outputData, jint outputSize) { if (!context) { LOGE("Context must be non-NULL."); return -1; @@ -154,7 +157,7 @@ DECODER_FUNC(jint, ffmpegDecode, jlong context, jobject inputData, outputSize); } -DECODER_FUNC(jint, ffmpegGetChannelCount, jlong context) { +AUDIO_DECODER_FUNC(jint, ffmpegGetChannelCount, jlong context) { if (!context) { LOGE("Context must be non-NULL."); return -1; @@ -162,7 +165,7 @@ DECODER_FUNC(jint, ffmpegGetChannelCount, jlong context) { return ((AVCodecContext *) context)->channels; } -DECODER_FUNC(jint, ffmpegGetSampleRate, jlong context) { +AUDIO_DECODER_FUNC(jint, ffmpegGetSampleRate, jlong context) { if (!context) { LOGE("Context must be non-NULL."); return -1; @@ -170,7 +173,7 @@ DECODER_FUNC(jint, ffmpegGetSampleRate, jlong context) { return ((AVCodecContext *) context)->sample_rate; } -DECODER_FUNC(jlong, ffmpegReset, jlong jContext, jbyteArray extraData) { +AUDIO_DECODER_FUNC(jlong, ffmpegReset, jlong jContext, jbyteArray extraData) { AVCodecContext *context = (AVCodecContext *) jContext; if (!context) { LOGE("Tried to reset without a context."); @@ -198,7 +201,7 @@ DECODER_FUNC(jlong, ffmpegReset, jlong jContext, jbyteArray extraData) { return (jlong) context; } -DECODER_FUNC(void, ffmpegRelease, jlong context) { +AUDIO_DECODER_FUNC(void, ffmpegRelease, jlong context) { if (context) { releaseContext((AVCodecContext *) context); } @@ -259,8 +262,8 @@ int decodePacket(AVCodecContext *context, AVPacket *packet, result = avcodec_send_packet(context, packet); if (result) { logError("avcodec_send_packet", result); - return result == AVERROR_INVALIDDATA ? DECODER_ERROR_INVALID_DATA - : DECODER_ERROR_OTHER; + return result == AVERROR_INVALIDDATA ? AUDIO_DECODER_ERROR_INVALID_DATA + : AUDIO_DECODER_ERROR_OTHER; } // Dequeue output data until it runs out. @@ -289,11 +292,11 @@ int decodePacket(AVCodecContext *context, AVPacket *packet, int sampleCount = frame->nb_samples; int dataSize = av_samples_get_buffer_size(NULL, channelCount, sampleCount, sampleFormat, 1); - AVAudioResampleContext *resampleContext; + SwrContext *resampleContext; if (context->opaque) { - resampleContext = (AVAudioResampleContext *) context->opaque; + resampleContext = (SwrContext *)context->opaque; } else { - resampleContext = avresample_alloc_context(); + resampleContext = swr_alloc(); av_opt_set_int(resampleContext, "in_channel_layout", channelLayout, 0); av_opt_set_int(resampleContext, "out_channel_layout", channelLayout, 0); av_opt_set_int(resampleContext, "in_sample_rate", sampleRate, 0); @@ -302,9 +305,9 @@ int decodePacket(AVCodecContext *context, AVPacket *packet, // The output format is always the requested format. av_opt_set_int(resampleContext, "out_sample_fmt", context->request_sample_fmt, 0); - result = avresample_open(resampleContext); + result = swr_init(resampleContext); if (result < 0) { - logError("avresample_open", result); + logError("swr_init", result); av_frame_free(&frame); return -1; } @@ -312,7 +315,7 @@ int decodePacket(AVCodecContext *context, AVPacket *packet, } int inSampleSize = av_get_bytes_per_sample(sampleFormat); int outSampleSize = av_get_bytes_per_sample(context->request_sample_fmt); - int outSamples = avresample_get_out_samples(resampleContext, sampleCount); + int outSamples = swr_get_out_samples(resampleContext, sampleCount); int bufferOutSize = outSampleSize * channelCount * outSamples; if (outSize + bufferOutSize > outputSize) { LOGE("Output buffer size (%d) too small for output data (%d).", @@ -320,15 +323,14 @@ int decodePacket(AVCodecContext *context, AVPacket *packet, av_frame_free(&frame); return -1; } - result = avresample_convert(resampleContext, &outputBuffer, bufferOutSize, - outSamples, frame->data, frame->linesize[0], - sampleCount); + result = swr_convert(resampleContext, &outputBuffer, bufferOutSize, + (const uint8_t **)frame->data, frame->nb_samples); av_frame_free(&frame); if (result < 0) { - logError("avresample_convert", result); + logError("swr_convert", result); return result; } - int available = avresample_available(resampleContext); + int available = swr_get_out_samples(resampleContext, 0); if (available != 0) { LOGE("Expected no samples remaining after resampling, but found %d.", available); @@ -351,9 +353,9 @@ void releaseContext(AVCodecContext *context) { if (!context) { return; } - AVAudioResampleContext *resampleContext; - if ((resampleContext = (AVAudioResampleContext *) context->opaque)) { - avresample_free(&resampleContext); + SwrContext *swrContext; + if ((swrContext = (SwrContext *)context->opaque)) { + swr_free(&swrContext); context->opaque = NULL; } avcodec_free_context(&context); diff --git a/extensions/flac/README.md b/extensions/flac/README.md index 84a92f9586..a9d4c3094e 100644 --- a/extensions/flac/README.md +++ b/extensions/flac/README.md @@ -97,6 +97,14 @@ a custom track selector the choice of `Renderer` is up to your implementation, so you need to make sure you are passing an `LibflacAudioRenderer` to the player, then implement your own logic to use the renderer for a given track. +## Using the extension in the demo application ## + +To try out playback using the extension in the [demo application][], see +[enabling extension decoders][]. + +[demo application]: https://exoplayer.dev/demo-application.html +[enabling extension decoders]: https://exoplayer.dev/demo-application.html#enabling-extension-decoders + ## Links ## * [Javadoc][]: Classes matching `com.google.android.exoplayer2.ext.flac.*` diff --git a/extensions/flac/build.gradle b/extensions/flac/build.gradle index 4a326ac646..f220d21106 100644 --- a/extensions/flac/build.gradle +++ b/extensions/flac/build.gradle @@ -29,9 +29,12 @@ android { testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner' } - sourceSets.main { - jniLibs.srcDir 'src/main/libs' - jni.srcDirs = [] // Disable the automatic ndk-build call by Android Studio. + sourceSets { + main { + jniLibs.srcDir 'src/main/libs' + jni.srcDirs = [] // Disable the automatic ndk-build call by Android Studio. + } + androidTest.assets.srcDir '../../testdata/src/test/assets/' } testOptions.unitTests.includeAndroidResources = true @@ -41,11 +44,13 @@ dependencies { implementation project(modulePrefix + 'library-core') implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion + compileOnly 'org.jetbrains.kotlin:kotlin-annotations-jvm:' + kotlinAnnotationsVersion androidTestImplementation project(modulePrefix + 'testutils') androidTestImplementation 'androidx.test:runner:' + androidxTestRunnerVersion testImplementation 'androidx.test:core:' + androidxTestCoreVersion testImplementation 'androidx.test.ext:junit:' + androidxTestJUnitVersion testImplementation project(modulePrefix + 'testutils') + testImplementation project(modulePrefix + 'testdata') testImplementation 'org.robolectric:robolectric:' + robolectricVersion } diff --git a/extensions/flac/proguard-rules.txt b/extensions/flac/proguard-rules.txt index 3e52f643e7..436ac9b0c7 100644 --- a/extensions/flac/proguard-rules.txt +++ b/extensions/flac/proguard-rules.txt @@ -9,7 +9,7 @@ -keep class com.google.android.exoplayer2.ext.flac.FlacDecoderJni { *; } --keep class com.google.android.exoplayer2.util.FlacStreamMetadata { +-keep class com.google.android.exoplayer2.extractor.FlacStreamMetadata { *; } -keep class com.google.android.exoplayer2.metadata.flac.PictureFrame { diff --git a/extensions/flac/src/androidTest/AndroidManifest.xml b/extensions/flac/src/androidTest/AndroidManifest.xml index 6736ab4b16..9e1133b34d 100644 --- a/extensions/flac/src/androidTest/AndroidManifest.xml +++ b/extensions/flac/src/androidTest/AndroidManifest.xml @@ -23,9 +23,7 @@ - - + tools:ignore="MissingApplicationIcon,HardcodedDebugMode"/> numSampleBeforeSeek) { - // First index after seek = num sample before seek. - return numSampleBeforeSeek; - } - } - } - - @Nullable - private SeekMap extractSeekMap(FlacExtractor extractor, FakeExtractorOutput output) - throws IOException, InterruptedException { - try { - ExtractorInput input = getExtractorInputFromPosition(0); - extractor.init(output); - while (output.seekMap == null) { - extractor.read(input, positionHolder); - } - } finally { - Util.closeQuietly(dataSource); - } - return output.seekMap; - } - - 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)); + expectedTrackOutput.getSampleData(expectedFrameIndex), + expectedTrackOutput.getSampleTimeUs(expectedFrameIndex), + expectedTrackOutput.getSampleFlags(expectedFrameIndex), + expectedTrackOutput.getSampleCryptoData(expectedFrameIndex)); } - 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; + private static void assertFirstFrameAfterSeekPrecedesTargetSeekTime( + String fileName, + FakeTrackOutput trackOutput, + long targetSeekTimeUs, + int firstFrameIndexAfterSeek) + throws IOException { + FakeTrackOutput expectedTrackOutput = getExpectedTrackOutput(fileName); + int maxFrameIndex = getFrameIndex(expectedTrackOutput, targetSeekTimeUs); + + long firstFrameAfterSeekTimeUs = trackOutput.getSampleTimeUs(firstFrameIndexAfterSeek); + assertThat(firstFrameAfterSeekTimeUs).isAtMost(targetSeekTimeUs); + + boolean frameFound = false; + for (int i = maxFrameIndex; i >= 0; i--) { + if (firstFrameAfterSeekTimeUs == expectedTrackOutput.getSampleTimeUs(i)) { + trackOutput.assertSample( + firstFrameIndexAfterSeek, + expectedTrackOutput.getSampleData(i), + expectedTrackOutput.getSampleTimeUs(i), + expectedTrackOutput.getSampleFlags(i), + expectedTrackOutput.getSampleCryptoData(i)); + frameFound = true; + break; } } - return sampleTimes.size() - 1; + + assertThat(frameFound).isTrue(); } - private ExtractorInput getExtractorInputFromPosition(long position) throws IOException { - DataSpec dataSpec = new DataSpec(FILE_URI, position, totalInputLength, /* key= */ null); - dataSource.open(dataSpec); - return new DefaultExtractorInput(dataSource, position, totalInputLength); + private static FakeTrackOutput getExpectedTrackOutput(String fileName) throws IOException { + return TestUtil.extractAllSamplesFromFile( + new FlacExtractor(), ApplicationProvider.getApplicationContext(), fileName) + .trackOutputs + .get(0); } - private void extractAllSamplesFromFileToExpectedOutput(Context context, String fileName) - throws IOException, InterruptedException { - byte[] data = TestUtil.getByteArray(context, fileName); - - FlacExtractor extractor = new FlacExtractor(); - extractor.init(expectedOutput); - FakeExtractorInput input = new FakeExtractorInput.Builder().setData(data).build(); - - while (extractor.read(input, new PositionHolder()) != Extractor.RESULT_END_OF_INPUT) {} + private static int getFrameIndex(FakeTrackOutput expectedTrackOutput, long targetSeekTimeUs) { + List frameTimes = expectedTrackOutput.getSampleTimesUs(); + return Util.binarySearchFloor( + frameTimes, targetSeekTimeUs, /* inclusive= */ true, /* stayInBounds= */ false); } } diff --git a/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacExtractorTest.java b/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacExtractorTest.java index c8033e04d3..ed28a2286a 100644 --- a/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacExtractorTest.java +++ b/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacExtractorTest.java @@ -17,7 +17,6 @@ package com.google.android.exoplayer2.ext.flac; import static org.junit.Assert.fail; -import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.testutil.ExtractorAsserts; import org.junit.Before; @@ -25,6 +24,8 @@ import org.junit.Test; import org.junit.runner.RunWith; /** Unit test for {@link FlacExtractor}. */ +// TODO(internal: b/26110951): Use org.junit.runners.Parameterized (and corresponding methods on +// ExtractorAsserts) when it's supported by our testing infrastructure. @RunWith(AndroidJUnit4.class) public class FlacExtractorTest { @@ -36,14 +37,80 @@ public class FlacExtractorTest { } @Test - public void testExtractFlacSample() throws Exception { - ExtractorAsserts.assertBehavior( - FlacExtractor::new, "bear.flac", ApplicationProvider.getApplicationContext()); + public void sample() throws Exception { + ExtractorAsserts.assertAllBehaviors( + FlacExtractor::new, /* file= */ "flac/bear.flac", /* dumpFilesPrefix= */ "flac/bear_raw"); } @Test - public void testExtractFlacSampleWithId3Header() throws Exception { - ExtractorAsserts.assertBehavior( - FlacExtractor::new, "bear_with_id3.flac", ApplicationProvider.getApplicationContext()); + public void sampleWithId3HeaderAndId3Enabled() throws Exception { + ExtractorAsserts.assertAllBehaviors( + FlacExtractor::new, + /* file= */ "flac/bear_with_id3.flac", + /* dumpFilesPrefix= */ "flac/bear_with_id3_enabled_raw"); + } + + @Test + public void sampleWithId3HeaderAndId3Disabled() throws Exception { + ExtractorAsserts.assertAllBehaviors( + () -> new FlacExtractor(FlacExtractor.FLAG_DISABLE_ID3_METADATA), + /* file= */ "flac/bear_with_id3.flac", + /* dumpFilesPrefix= */ "flac/bear_with_id3_disabled_raw"); + } + + @Test + public void sampleUnseekable() throws Exception { + ExtractorAsserts.assertAllBehaviors( + FlacExtractor::new, + /* file= */ "flac/bear_no_seek_table_no_num_samples.flac", + /* dumpFilesPrefix= */ "flac/bear_no_seek_table_no_num_samples_raw"); + } + + @Test + public void sampleWithVorbisComments() throws Exception { + ExtractorAsserts.assertAllBehaviors( + FlacExtractor::new, + /* file= */ "flac/bear_with_vorbis_comments.flac", + /* dumpFilesPrefix= */ "flac/bear_with_vorbis_comments_raw"); + } + + @Test + public void sampleWithPicture() throws Exception { + ExtractorAsserts.assertAllBehaviors( + FlacExtractor::new, + /* file= */ "flac/bear_with_picture.flac", + /* dumpFilesPrefix= */ "flac/bear_with_picture_raw"); + } + + @Test + public void oneMetadataBlock() throws Exception { + ExtractorAsserts.assertAllBehaviors( + FlacExtractor::new, + /* file= */ "flac/bear_one_metadata_block.flac", + /* dumpFilesPrefix= */ "flac/bear_one_metadata_block_raw"); + } + + @Test + public void noMinMaxFrameSize() throws Exception { + ExtractorAsserts.assertAllBehaviors( + FlacExtractor::new, + /* file= */ "flac/bear_no_min_max_frame_size.flac", + /* dumpFilesPrefix= */ "flac/bear_no_min_max_frame_size_raw"); + } + + @Test + public void noNumSamples() throws Exception { + ExtractorAsserts.assertAllBehaviors( + FlacExtractor::new, + /* file= */ "flac/bear_no_num_samples.flac", + /* dumpFilesPrefix= */ "flac/bear_no_num_samples_raw"); + } + + @Test + public void uncommonSampleRate() throws Exception { + ExtractorAsserts.assertAllBehaviors( + FlacExtractor::new, + /* file= */ "flac/bear_uncommon_sample_rate.flac", + /* dumpFilesPrefix= */ "flac/bear_uncommon_sample_rate_raw"); } } 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 bf96442f61..e9b1fd1019 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 @@ -20,14 +20,19 @@ import static org.junit.Assert.fail; import android.content.Context; import android.net.Uri; import android.os.Looper; +import androidx.annotation.Nullable; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.audio.AudioProcessor; +import com.google.android.exoplayer2.audio.AudioSink; +import com.google.android.exoplayer2.audio.DefaultAudioSink; import com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.ProgressiveMediaSource; +import com.google.android.exoplayer2.testutil.CapturingAudioSink; import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; import org.junit.Before; import org.junit.Test; @@ -37,7 +42,8 @@ import org.junit.runner.RunWith; @RunWith(AndroidJUnit4.class) public class FlacPlaybackTest { - private static final String BEAR_FLAC_URI = "asset:///bear-flac.mka"; + private static final String BEAR_FLAC_16BIT = "mka/bear-flac-16bit.mka"; + private static final String BEAR_FLAC_24BIT = "mka/bear-flac-24bit.mka"; @Before public void setUp() { @@ -47,38 +53,56 @@ public class FlacPlaybackTest { } @Test - public void testBasicPlayback() throws Exception { - playUri(BEAR_FLAC_URI); + public void test16BitPlayback() throws Exception { + playAndAssertAudioSinkInput(BEAR_FLAC_16BIT); } - private void playUri(String uri) throws Exception { + @Test + public void test24BitPlayback() throws Exception { + playAndAssertAudioSinkInput(BEAR_FLAC_24BIT); + } + + private static void playAndAssertAudioSinkInput(String fileName) throws Exception { + CapturingAudioSink audioSink = + new CapturingAudioSink( + new DefaultAudioSink(/* audioCapabilities= */ null, new AudioProcessor[0])); + TestPlaybackRunnable testPlaybackRunnable = - new TestPlaybackRunnable(Uri.parse(uri), ApplicationProvider.getApplicationContext()); + new TestPlaybackRunnable( + Uri.parse("asset:///" + fileName), + ApplicationProvider.getApplicationContext(), + audioSink); Thread thread = new Thread(testPlaybackRunnable); thread.start(); thread.join(); if (testPlaybackRunnable.playbackException != null) { throw testPlaybackRunnable.playbackException; } + + audioSink.assertOutput( + ApplicationProvider.getApplicationContext(), fileName + ".audiosink.dump"); } private static class TestPlaybackRunnable implements Player.EventListener, Runnable { private final Context context; private final Uri uri; + private final AudioSink audioSink; - private ExoPlayer player; - private ExoPlaybackException playbackException; + @Nullable private ExoPlayer player; + @Nullable private ExoPlaybackException playbackException; - public TestPlaybackRunnable(Uri uri, Context context) { + public TestPlaybackRunnable(Uri uri, Context context, AudioSink audioSink) { this.uri = uri; this.context = context; + this.audioSink = audioSink; } @Override public void run() { Looper.prepare(); - LibflacAudioRenderer audioRenderer = new LibflacAudioRenderer(); + LibflacAudioRenderer audioRenderer = + new LibflacAudioRenderer(/* eventHandler= */ null, /* eventListener= */ null, audioSink); player = new ExoPlayer.Builder(context, audioRenderer).build(); player.addListener(this); MediaSource mediaSource = @@ -86,8 +110,9 @@ public class FlacPlaybackTest { new DefaultDataSourceFactory(context, "ExoPlayerExtFlacTest"), MatroskaExtractor.FACTORY) .createMediaSource(uri); - player.prepare(mediaSource); - player.setPlayWhenReady(true); + player.setMediaSource(mediaSource); + player.prepare(); + player.play(); Looper.loop(); } @@ -97,7 +122,7 @@ public class FlacPlaybackTest { } @Override - public void onPlayerStateChanged(boolean playWhenReady, @Player.State int playbackState) { + public void onPlaybackStateChanged(@Player.State int playbackState) { if (playbackState == Player.STATE_ENDED || (playbackState == Player.STATE_IDLE && playbackException != null)) { player.release(); @@ -105,5 +130,4 @@ public class FlacPlaybackTest { } } } - } 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 74c3e73791..742ade214d 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 @@ -17,9 +17,10 @@ package com.google.android.exoplayer2.ext.flac; import com.google.android.exoplayer2.extractor.BinarySearchSeeker; import com.google.android.exoplayer2.extractor.ExtractorInput; +import com.google.android.exoplayer2.extractor.FlacStreamMetadata; import com.google.android.exoplayer2.extractor.SeekMap; import com.google.android.exoplayer2.util.Assertions; -import com.google.android.exoplayer2.util.FlacStreamMetadata; +import com.google.android.exoplayer2.util.FlacConstants; import java.io.IOException; import java.nio.ByteBuffer; @@ -49,6 +50,15 @@ import java.nio.ByteBuffer; private final FlacDecoderJni decoderJni; + /** + * Creates a {@link FlacBinarySearchSeeker}. + * + * @param streamMetadata The stream metadata. + * @param firstFramePosition The byte offset of the first frame in the stream. + * @param inputLength The length of the stream in bytes. + * @param decoderJni The FLAC JNI decoder. + * @param outputFrameHolder A holder used to retrieve the frame found by a seeking operation. + */ public FlacBinarySearchSeeker( FlacStreamMetadata streamMetadata, long firstFramePosition, @@ -56,7 +66,7 @@ import java.nio.ByteBuffer; FlacDecoderJni decoderJni, OutputFrameHolder outputFrameHolder) { super( - new FlacSeekTimestampConverter(streamMetadata), + /* seekTimestampConverter= */ streamMetadata::getSampleNumber, new FlacTimestampSeeker(decoderJni, outputFrameHolder), streamMetadata.getDurationUs(), /* floorTimePosition= */ 0, @@ -64,7 +74,8 @@ import java.nio.ByteBuffer; /* floorBytePosition= */ firstFramePosition, /* ceilingBytePosition= */ inputLength, /* approxBytesPerFrame= */ streamMetadata.getApproxBytesPerFrame(), - /* minimumSearchRange= */ Math.max(1, streamMetadata.minFrameSize)); + /* minimumSearchRange= */ Math.max( + FlacConstants.MIN_FRAME_HEADER_SIZE, streamMetadata.minFrameSize)); this.decoderJni = Assertions.checkNotNull(decoderJni); } @@ -89,7 +100,7 @@ import java.nio.ByteBuffer; @Override public TimestampSearchResult searchForTimestamp(ExtractorInput input, long targetSampleIndex) - throws IOException, InterruptedException { + throws IOException { ByteBuffer outputBuffer = outputFrameHolder.byteBuffer; long searchPosition = input.getPosition(); decoderJni.reset(searchPosition); @@ -115,6 +126,8 @@ import java.nio.ByteBuffer; if (targetSampleInLastFrame) { // We are holding the target frame in outputFrameHolder. Set its presentation time now. outputFrameHolder.timeUs = decoderJni.getLastFrameTimestamp(); + // The input position is passed even though it does not indicate the frame containing the + // target sample because the extractor must continue to read from this position. return TimestampSearchResult.targetFoundResult(input.getPosition()); } else if (nextFrameSampleIndex <= targetSampleIndex) { return TimestampSearchResult.underestimatedResult( @@ -124,21 +137,4 @@ import java.nio.ByteBuffer; } } } - - /** - * A {@link SeekTimestampConverter} implementation that returns the frame index (sample index) as - * the timestamp for a stream seek time position. - */ - private static final class FlacSeekTimestampConverter implements SeekTimestampConverter { - private final FlacStreamMetadata streamMetadata; - - public FlacSeekTimestampConverter(FlacStreamMetadata streamMetadata) { - this.streamMetadata = streamMetadata; - } - - @Override - public long timeUsToTargetTime(long timeUs) { - return Assertions.checkNotNull(streamMetadata).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 e1f6112319..84cb081b29 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 @@ -21,7 +21,7 @@ import com.google.android.exoplayer2.ParserException; 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.FlacStreamMetadata; +import com.google.android.exoplayer2.extractor.FlacStreamMetadata; import com.google.android.exoplayer2.util.Util; import java.io.IOException; import java.nio.ByteBuffer; @@ -33,7 +33,7 @@ import java.util.List; /* package */ final class FlacDecoder extends SimpleDecoder { - private final int maxOutputBufferSize; + private final FlacStreamMetadata streamMetadata; private final FlacDecoderJni decoderJni; /** @@ -59,12 +59,11 @@ import java.util.List; } decoderJni = new FlacDecoderJni(); decoderJni.setData(ByteBuffer.wrap(initializationData.get(0))); - FlacStreamMetadata streamMetadata; try { streamMetadata = decoderJni.decodeStreamMetadata(); } catch (ParserException e) { throw new FlacDecoderException("Failed to decode StreamInfo", e); - } catch (IOException | InterruptedException e) { + } catch (IOException e) { // Never happens. throw new IllegalStateException(e); } @@ -72,7 +71,6 @@ import java.util.List; int initialInputBufferSize = maxInputBufferSize != Format.NO_VALUE ? maxInputBufferSize : streamMetadata.maxFrameSize; setInitialInputBufferSize(initialInputBufferSize); - maxOutputBufferSize = streamMetadata.getMaxDecodedFrameSize(); } @Override @@ -87,7 +85,7 @@ import java.util.List; @Override protected SimpleOutputBuffer createOutputBuffer() { - return new SimpleOutputBuffer(this); + return new SimpleOutputBuffer(this::releaseOutputBuffer); } @Override @@ -103,12 +101,13 @@ import java.util.List; decoderJni.flush(); } decoderJni.setData(Util.castNonNull(inputBuffer.data)); - ByteBuffer outputData = outputBuffer.init(inputBuffer.timeUs, maxOutputBufferSize); + ByteBuffer outputData = + outputBuffer.init(inputBuffer.timeUs, streamMetadata.getMaxDecodedFrameSize()); try { decoderJni.decodeSample(outputData); } catch (FlacDecoderJni.FlacFrameDecodeException e) { return new FlacDecoderException("Frame decoding failed", e); - } catch (IOException | InterruptedException e) { + } catch (IOException e) { // Never happens. throw new IllegalStateException(e); } @@ -121,4 +120,8 @@ import java.util.List; decoderJni.release(); } + /** Returns the {@link FlacStreamMetadata} decoded from the initialization data. */ + public FlacStreamMetadata getStreamMetadata() { + return streamMetadata; + } } diff --git a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoderException.java b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoderException.java index 95d7f87c05..2c2f56e06b 100644 --- a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoderException.java +++ b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoderException.java @@ -15,12 +15,10 @@ */ package com.google.android.exoplayer2.ext.flac; -import com.google.android.exoplayer2.audio.AudioDecoderException; +import com.google.android.exoplayer2.decoder.DecoderException; -/** - * Thrown when an Flac decoder error occurs. - */ -public final class FlacDecoderException extends AudioDecoderException { +/** Thrown when an Flac decoder error occurs. */ +public final class FlacDecoderException extends DecoderException { /* package */ FlacDecoderException(String message) { super(message); diff --git a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoderJni.java b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoderJni.java index 60f1d32a79..daf4584948 100644 --- a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoderJni.java +++ b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoderJni.java @@ -19,9 +19,9 @@ import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.extractor.ExtractorInput; +import com.google.android.exoplayer2.extractor.FlacStreamMetadata; import com.google.android.exoplayer2.extractor.SeekMap; import com.google.android.exoplayer2.extractor.SeekPoint; -import com.google.android.exoplayer2.util.FlacStreamMetadata; import com.google.android.exoplayer2.util.Util; import java.io.IOException; import java.nio.ByteBuffer; @@ -51,12 +51,6 @@ import java.nio.ByteBuffer; @Nullable private byte[] tempBuffer; private boolean endOfExtractorInput; - // the constructor does not initialize fields: tempBuffer - // call to flacInit() not allowed on the given receiver. - @SuppressWarnings({ - "nullness:initialization.fields.uninitialized", - "nullness:method.invocation.invalid" - }) public FlacDecoderJni() throws FlacDecoderException { if (!FlacLibrary.isAvailable()) { throw new FlacDecoderException("Failed to load decoder native libraries."); @@ -121,7 +115,7 @@ import java.nio.ByteBuffer; * read from the source, then 0 is returned. */ @SuppressWarnings("unused") // Called from native code. - public int read(ByteBuffer target) throws IOException, InterruptedException { + public int read(ByteBuffer target) throws IOException { int byteCount = target.remaining(); if (byteBufferData != null) { byteCount = Math.min(byteCount, byteBufferData.remaining()); @@ -151,7 +145,7 @@ import java.nio.ByteBuffer; } /** Decodes and consumes the metadata from the FLAC stream. */ - public FlacStreamMetadata decodeStreamMetadata() throws IOException, InterruptedException { + public FlacStreamMetadata decodeStreamMetadata() throws IOException { FlacStreamMetadata streamMetadata = flacDecodeMetadata(nativeDecoderContext); if (streamMetadata == null) { throw new ParserException("Failed to decode stream metadata"); @@ -167,7 +161,7 @@ import java.nio.ByteBuffer; * @param retryPosition If any error happens, the input will be rewound to {@code retryPosition}. */ public void decodeSampleWithBacktrackPosition(ByteBuffer output, long retryPosition) - throws InterruptedException, IOException, FlacFrameDecodeException { + throws IOException, FlacFrameDecodeException { try { decodeSample(output); } catch (IOException e) { @@ -183,8 +177,7 @@ import java.nio.ByteBuffer; /** Decodes and consumes the next sample from the FLAC stream into the given byte buffer. */ @SuppressWarnings("ByteBufferBackingArray") - public void decodeSample(ByteBuffer output) - throws IOException, InterruptedException, FlacFrameDecodeException { + public void decodeSample(ByteBuffer output) throws IOException, FlacFrameDecodeException { output.clear(); int frameSize = output.isDirect() @@ -272,8 +265,7 @@ import java.nio.ByteBuffer; } private int readFromExtractorInput( - ExtractorInput extractorInput, byte[] tempBuffer, int offset, int length) - throws IOException, InterruptedException { + ExtractorInput extractorInput, byte[] tempBuffer, int offset, int length) throws IOException { int read = extractorInput.read(tempBuffer, offset, length); if (read == C.RESULT_END_OF_INPUT) { endOfExtractorInput = true; @@ -284,14 +276,11 @@ import java.nio.ByteBuffer; private native long flacInit(); - private native FlacStreamMetadata flacDecodeMetadata(long context) - throws IOException, InterruptedException; + private native FlacStreamMetadata flacDecodeMetadata(long context) throws IOException; - private native int flacDecodeToBuffer(long context, ByteBuffer outputBuffer) - throws IOException, InterruptedException; + private native int flacDecodeToBuffer(long context, ByteBuffer outputBuffer) throws IOException; - private native int flacDecodeToArray(long context, byte[] outputArray) - throws IOException, InterruptedException; + private native int flacDecodeToArray(long context, byte[] outputArray) throws IOException; private native long flacGetDecodePosition(long context); 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 7c69a93fc9..364cf80ef8 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 @@ -27,13 +27,13 @@ import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.ExtractorsFactory; import com.google.android.exoplayer2.extractor.FlacMetadataReader; +import com.google.android.exoplayer2.extractor.FlacStreamMetadata; 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.extractor.TrackOutput; import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.util.Assertions; -import com.google.android.exoplayer2.util.FlacStreamMetadata; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.ParsableByteArray; import java.io.IOException; @@ -113,14 +113,13 @@ public final class FlacExtractor implements Extractor { } @Override - public boolean sniff(ExtractorInput input) throws IOException, InterruptedException { + public boolean sniff(ExtractorInput input) throws IOException { id3Metadata = FlacMetadataReader.peekId3Metadata(input, /* parseData= */ !id3MetadataDisabled); return FlacMetadataReader.checkAndPeekStreamMarker(input); } @Override - public int read(final ExtractorInput input, PositionHolder seekPosition) - throws IOException, InterruptedException { + public int read(final ExtractorInput input, PositionHolder seekPosition) throws IOException { if (input.getPosition() == 0 && !id3MetadataDisabled && id3Metadata == null) { id3Metadata = FlacMetadataReader.peekId3Metadata(input, /* parseData= */ true); } @@ -185,7 +184,7 @@ public final class FlacExtractor implements Extractor { @RequiresNonNull({"decoderJni", "extractorOutput", "trackOutput"}) // Requires initialized. @EnsuresNonNull({"streamMetadata", "outputFrameHolder"}) // Ensures stream metadata decoded. @SuppressWarnings({"contracts.postcondition.not.satisfied"}) - private void decodeStreamMetadata(ExtractorInput input) throws InterruptedException, IOException { + private void decodeStreamMetadata(ExtractorInput input) throws IOException { if (streamMetadataDecoded) { return; } @@ -212,6 +211,7 @@ public final class FlacExtractor implements Extractor { input.getLength(), extractorOutput, outputFrameHolder); + @Nullable Metadata metadata = streamMetadata.getMetadataCopyWithAppendedEntriesFrom(id3Metadata); outputFormat(streamMetadata, metadata, trackOutput); } @@ -224,7 +224,7 @@ public final class FlacExtractor implements Extractor { ParsableByteArray outputBuffer, OutputFrameHolder outputFrameHolder, TrackOutput trackOutput) - throws InterruptedException, IOException { + throws IOException { int seekResult = binarySearchSeeker.handlePendingSeek(input, seekPosition); ByteBuffer outputByteBuffer = outputFrameHolder.byteBuffer; if (seekResult == RESULT_CONTINUE && outputByteBuffer.limit() > 0) { @@ -249,7 +249,7 @@ public final class FlacExtractor implements Extractor { SeekMap seekMap; if (haveSeekTable) { seekMap = new FlacSeekMap(streamMetadata.getDurationUs(), decoderJni); - } else if (streamLength != C.LENGTH_UNSET) { + } else if (streamLength != C.LENGTH_UNSET && streamMetadata.totalSamples > 0) { long firstFramePosition = decoderJni.getDecodePosition(); binarySearchSeeker = new FlacBinarySearchSeeker( @@ -265,22 +265,16 @@ public final class FlacExtractor implements Extractor { private static void outputFormat( FlacStreamMetadata streamMetadata, @Nullable Metadata metadata, TrackOutput output) { Format mediaFormat = - Format.createAudioSampleFormat( - /* id= */ null, - MimeTypes.AUDIO_RAW, - /* codecs= */ null, - streamMetadata.getBitRate(), - streamMetadata.getMaxDecodedFrameSize(), - streamMetadata.channels, - streamMetadata.sampleRate, - getPcmEncoding(streamMetadata.bitsPerSample), - /* encoderDelay= */ 0, - /* encoderPadding= */ 0, - /* initializationData= */ null, - /* drmInitData= */ null, - /* selectionFlags= */ 0, - /* language= */ null, - metadata); + new Format.Builder() + .setSampleMimeType(MimeTypes.AUDIO_RAW) + .setAverageBitrate(streamMetadata.getDecodedBitrate()) + .setPeakBitrate(streamMetadata.getDecodedBitrate()) + .setMaxInputSize(streamMetadata.getMaxDecodedFrameSize()) + .setChannelCount(streamMetadata.channels) + .setSampleRate(streamMetadata.sampleRate) + .setPcmEncoding(getPcmEncoding(streamMetadata.bitsPerSample)) + .setMetadata(metadata) + .build(); output.format(mediaFormat); } 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 d833c47d14..cbdf42dbaf 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 @@ -21,18 +21,25 @@ import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.audio.AudioProcessor; import com.google.android.exoplayer2.audio.AudioRendererEventListener; -import com.google.android.exoplayer2.audio.SimpleDecoderAudioRenderer; -import com.google.android.exoplayer2.drm.DrmSessionManager; +import com.google.android.exoplayer2.audio.AudioSink; +import com.google.android.exoplayer2.audio.DecoderAudioRenderer; import com.google.android.exoplayer2.drm.ExoMediaCrypto; +import com.google.android.exoplayer2.extractor.FlacStreamMetadata; +import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.FlacConstants; import com.google.android.exoplayer2.util.MimeTypes; +import com.google.android.exoplayer2.util.TraceUtil; +import com.google.android.exoplayer2.util.Util; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; -/** - * Decodes and renders audio using the native Flac decoder. - */ -public class LibflacAudioRenderer extends SimpleDecoderAudioRenderer { +/** Decodes and renders audio using the native Flac decoder. */ +public final class LibflacAudioRenderer extends DecoderAudioRenderer { + private static final String TAG = "LibflacAudioRenderer"; private static final int NUM_BUFFERS = 16; + private @MonotonicNonNull FlacStreamMetadata streamMetadata; + public LibflacAudioRenderer() { this(/* eventHandler= */ null, /* eventListener= */ null); } @@ -50,15 +57,52 @@ public class LibflacAudioRenderer extends SimpleDecoderAudioRenderer { super(eventHandler, eventListener, audioProcessors); } + /** + * @param eventHandler A handler to use when delivering events to {@code eventListener}. 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 audioSink The sink to which audio will be output. + */ + public LibflacAudioRenderer( + @Nullable Handler eventHandler, + @Nullable AudioRendererEventListener eventListener, + AudioSink audioSink) { + super( + eventHandler, + eventListener, + audioSink); + } + @Override - protected int supportsFormatInternal( - @Nullable DrmSessionManager drmSessionManager, Format format) { + public String getName() { + return TAG; + } + + @Override + @FormatSupport + protected int supportsFormatInternal(Format format) { if (!FlacLibrary.isAvailable() || !MimeTypes.AUDIO_FLAC.equalsIgnoreCase(format.sampleMimeType)) { return FORMAT_UNSUPPORTED_TYPE; - } else if (!supportsOutput(format.channelCount, C.ENCODING_PCM_16BIT)) { + } + // Compute the PCM encoding that the FLAC decoder will output. + @C.PcmEncoding int pcmEncoding; + if (format.initializationData.isEmpty()) { + // The initialization data might not be set if the format was obtained from a manifest (e.g. + // for DASH playbacks) rather than directly from the media. In this case we assume + // ENCODING_PCM_16BIT. If the actual encoding is different then playback will still succeed as + // long as the AudioSink supports it, which will always be true when using DefaultAudioSink. + pcmEncoding = C.ENCODING_PCM_16BIT; + } else { + int streamMetadataOffset = + FlacConstants.STREAM_MARKER_SIZE + FlacConstants.METADATA_BLOCK_HEADER_SIZE; + FlacStreamMetadata streamMetadata = + new FlacStreamMetadata(format.initializationData.get(0), streamMetadataOffset); + pcmEncoding = Util.getPcmEncoding(streamMetadata.bitsPerSample); + } + if (!supportsOutput(format.channelCount, pcmEncoding)) { return FORMAT_UNSUPPORTED_SUBTYPE; - } else if (!supportsFormatDrm(drmSessionManager, format.drmInitData)) { + } else if (format.drmInitData != null && format.exoMediaCryptoType == null) { return FORMAT_UNSUPPORTED_DRM; } else { return FORMAT_HANDLED; @@ -68,8 +112,22 @@ public class LibflacAudioRenderer extends SimpleDecoderAudioRenderer { @Override protected FlacDecoder createDecoder(Format format, @Nullable ExoMediaCrypto mediaCrypto) throws FlacDecoderException { - return new FlacDecoder( - NUM_BUFFERS, NUM_BUFFERS, format.maxInputSize, format.initializationData); + TraceUtil.beginSection("createFlacDecoder"); + FlacDecoder decoder = + new FlacDecoder(NUM_BUFFERS, NUM_BUFFERS, format.maxInputSize, format.initializationData); + streamMetadata = decoder.getStreamMetadata(); + TraceUtil.endSection(); + return decoder; } + @Override + protected Format getOutputFormat() { + Assertions.checkNotNull(streamMetadata); + return new Format.Builder() + .setSampleMimeType(MimeTypes.AUDIO_RAW) + .setChannelCount(streamMetadata.channels) + .setSampleRate(streamMetadata.sampleRate) + .setPcmEncoding(Util.getPcmEncoding(streamMetadata.bitsPerSample)) + .build(); + } } diff --git a/extensions/flac/src/main/jni/flac_jni.cc b/extensions/flac/src/main/jni/flac_jni.cc index 4fc28ce887..850f6883bf 100644 --- a/extensions/flac/src/main/jni/flac_jni.cc +++ b/extensions/flac/src/main/jni/flac_jni.cc @@ -147,7 +147,7 @@ DECODER_FUNC(jobject, flacDecodeMetadata, jlong jContext) { context->parser->getStreamInfo(); jclass flacStreamMetadataClass = env->FindClass( - "com/google/android/exoplayer2/util/" + "com/google/android/exoplayer2/extractor/" "FlacStreamMetadata"); jmethodID flacStreamMetadataConstructor = env->GetMethodID(flacStreamMetadataClass, "", diff --git a/extensions/flac/src/main/jni/flac_parser.cc b/extensions/flac/src/main/jni/flac_parser.cc index b920560f3a..c368647420 100644 --- a/extensions/flac/src/main/jni/flac_parser.cc +++ b/extensions/flac/src/main/jni/flac_parser.cc @@ -349,26 +349,6 @@ bool FLACParser::decodeMetadata() { ALOGE("unsupported bits per sample %u", getBitsPerSample()); return false; } - // check sample rate - switch (getSampleRate()) { - case 8000: - case 11025: - case 12000: - case 16000: - case 22050: - case 24000: - case 32000: - case 44100: - case 48000: - case 88200: - case 96000: - case 176400: - case 192000: - break; - default: - ALOGE("unsupported sample rate %u", getSampleRate()); - return false; - } // configure the appropriate copy function based on device endianness. if (isBigEndian()) { mCopy = copyToByteArrayBigEndian; @@ -462,8 +442,9 @@ bool FLACParser::getSeekPositions(int64_t timeUs, if (sampleNumber <= targetSampleNumber) { result[0] = (sampleNumber * 1000000LL) / sampleRate; result[1] = firstFrameOffset + points[i - 1].stream_offset; - if (sampleNumber == targetSampleNumber || i >= length) { - // exact seek, or no following seek point. + if (sampleNumber == targetSampleNumber || i >= length || + points[i].sample_number == -1) { // placeholder + // exact seek, or no following non-placeholder seek point result[2] = result[0]; result[3] = result[1]; } else { 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 deleted file mode 100644 index 611197bbe5..0000000000 --- a/extensions/flac/src/test/java/com/google/android/exoplayer2/ext/flac/DefaultExtractorsFactoryTest.java +++ /dev/null @@ -1,76 +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.ext.flac; - -import static com.google.common.truth.Truth.assertThat; - -import androidx.test.ext.junit.runners.AndroidJUnit4; -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.Ac4Extractor; -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; - -/** Unit test for {@link DefaultExtractorsFactory}. */ -@RunWith(AndroidJUnit4.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, - Ac4Extractor.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/gvr/build.gradle b/extensions/gvr/build.gradle index f8992616a2..4e6bd76cb4 100644 --- a/extensions/gvr/build.gradle +++ b/extensions/gvr/build.gradle @@ -36,6 +36,7 @@ dependencies { implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion api 'com.google.vr:sdk-base:1.190.0' compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion + compileOnly 'org.jetbrains.kotlin:kotlin-annotations-jvm:' + kotlinAnnotationsVersion } ext { 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 8ba33290ea..14570759c3 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 @@ -32,7 +32,7 @@ import java.nio.ByteOrder; * href="https://github.com/google/ExoPlayer/issues">issue tracker. */ @Deprecated -public final class GvrAudioProcessor implements AudioProcessor { +public class GvrAudioProcessor implements AudioProcessor { static { ExoPlayerLibraryInfo.registerModule("goog.exo.gvr"); diff --git a/extensions/gvr/src/main/java/com/google/android/exoplayer2/ext/gvr/package-info.java b/extensions/gvr/src/main/java/com/google/android/exoplayer2/ext/gvr/package-info.java new file mode 100644 index 0000000000..155317fc29 --- /dev/null +++ b/extensions/gvr/src/main/java/com/google/android/exoplayer2/ext/gvr/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2020 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. + */ +@NonNullApi +package com.google.android.exoplayer2.ext.gvr; + +import com.google.android.exoplayer2.util.NonNullApi; diff --git a/extensions/ima/README.md b/extensions/ima/README.md index 4ed6a5428a..f28ba2977e 100644 --- a/extensions/ima/README.md +++ b/extensions/ima/README.md @@ -58,7 +58,9 @@ playback. ## Links ## +* [ExoPlayer documentation on ad insertion][] * [Javadoc][]: Classes matching `com.google.android.exoplayer2.ext.ima.*` belong to this module. +[ExoPlayer documentation on ad insertion]: https://exoplayer.dev/ad-insertion.html [Javadoc]: https://exoplayer.dev/doc/reference/index.html diff --git a/extensions/ima/build.gradle b/extensions/ima/build.gradle index e2292aed8f..f5d29efb97 100644 --- a/extensions/ima/build.gradle +++ b/extensions/ima/build.gradle @@ -26,16 +26,29 @@ android { minSdkVersion project.ext.minSdkVersion targetSdkVersion project.ext.targetSdkVersion consumerProguardFiles 'proguard-rules.txt' + testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner' + // Enable multidex for androidTests. + multiDexEnabled true + } + + sourceSets { + androidTest.assets.srcDir '../../testdata/src/test/assets/' } testOptions.unitTests.includeAndroidResources = true } dependencies { - api 'com.google.ads.interactivemedia.v3:interactivemedia:3.11.3' + api 'com.google.ads.interactivemedia.v3:interactivemedia:3.18.1' implementation project(modulePrefix + 'library-core') implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion implementation 'com.google.android.gms:play-services-ads-identifier:17.0.0' + compileOnly 'org.jetbrains.kotlin:kotlin-annotations-jvm:' + kotlinAnnotationsVersion + androidTestImplementation project(modulePrefix + 'testutils') + androidTestImplementation 'androidx.test:rules:' + androidxTestRulesVersion + androidTestImplementation 'androidx.test:runner:' + androidxTestRunnerVersion + androidTestImplementation 'com.android.support:multidex:1.0.3' + androidTestCompileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion testImplementation project(modulePrefix + 'testutils') testImplementation 'org.robolectric:robolectric:' + robolectricVersion } diff --git a/extensions/ima/src/androidTest/AndroidManifest.xml b/extensions/ima/src/androidTest/AndroidManifest.xml new file mode 100644 index 0000000000..c8bd575f60 --- /dev/null +++ b/extensions/ima/src/androidTest/AndroidManifest.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + diff --git a/extensions/ima/src/androidTest/java/com/google/android/exoplayer2/ext/ima/ImaPlaybackTest.java b/extensions/ima/src/androidTest/java/com/google/android/exoplayer2/ext/ima/ImaPlaybackTest.java new file mode 100644 index 0000000000..0e685e55ea --- /dev/null +++ b/extensions/ima/src/androidTest/java/com/google/android/exoplayer2/ext/ima/ImaPlaybackTest.java @@ -0,0 +1,283 @@ +/* + * Copyright 2020 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 android.content.Context; +import android.net.Uri; +import android.view.Surface; +import android.view.View; +import android.view.ViewGroup; +import android.widget.FrameLayout; +import androidx.annotation.Nullable; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.rule.ActivityTestRule; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.MediaItem; +import com.google.android.exoplayer2.Player; +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.SimpleExoPlayer; +import com.google.android.exoplayer2.Timeline.Window; +import com.google.android.exoplayer2.analytics.AnalyticsListener; +import com.google.android.exoplayer2.decoder.DecoderCounters; +import com.google.android.exoplayer2.drm.DrmSessionManager; +import com.google.android.exoplayer2.source.DefaultMediaSourceFactory; +import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.source.ads.AdsLoader.AdViewProvider; +import com.google.android.exoplayer2.source.ads.AdsMediaSource; +import com.google.android.exoplayer2.testutil.ActionSchedule; +import com.google.android.exoplayer2.testutil.ExoHostedTest; +import com.google.android.exoplayer2.testutil.HostActivity; +import com.google.android.exoplayer2.testutil.TestUtil; +import com.google.android.exoplayer2.trackselection.MappingTrackSelector; +import com.google.android.exoplayer2.upstream.DataSource; +import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; +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.List; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.junit.Ignore; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Playback tests using {@link ImaAdsLoader}. */ +@RunWith(AndroidJUnit4.class) +public final class ImaPlaybackTest { + + private static final String TAG = "ImaPlaybackTest"; + + private static final long TIMEOUT_MS = 5 * 60 * C.MILLIS_PER_SECOND; + + private static final String CONTENT_URI_SHORT = + "https://storage.googleapis.com/exoplayer-test-media-1/mp4/android-screens-10s.mp4"; + private static final String CONTENT_URI_LONG = + "https://storage.googleapis.com/exoplayer-test-media-1/mp4/android-screens-25s.mp4"; + private static final AdId CONTENT = new AdId(C.INDEX_UNSET, C.INDEX_UNSET); + + @Rule public ActivityTestRule testRule = new ActivityTestRule<>(HostActivity.class); + + @Test + public void playbackWithPrerollAdTag_playsAdAndContent() throws Exception { + String adsResponse = + TestUtil.getString(/* context= */ testRule.getActivity(), "ad-responses/preroll.xml"); + AdId[] expectedAdIds = new AdId[] {ad(0), CONTENT}; + ImaHostedTest hostedTest = + new ImaHostedTest(Uri.parse(CONTENT_URI_SHORT), adsResponse, expectedAdIds); + + testRule.getActivity().runTest(hostedTest, TIMEOUT_MS); + } + + @Test + public void playbackWithMidrolls_playsAdAndContent() throws Exception { + String adsResponse = + TestUtil.getString( + /* context= */ testRule.getActivity(), "ad-responses/preroll_midroll6s_postroll.xml"); + AdId[] expectedAdIds = new AdId[] {ad(0), CONTENT, ad(1), CONTENT, ad(2), CONTENT}; + ImaHostedTest hostedTest = + new ImaHostedTest(Uri.parse(CONTENT_URI_SHORT), adsResponse, expectedAdIds); + + testRule.getActivity().runTest(hostedTest, TIMEOUT_MS); + } + + @Test + public void playbackWithMidrolls1And7_playsAdsAndContent() throws Exception { + String adsResponse = + TestUtil.getString( + /* context= */ testRule.getActivity(), "ad-responses/midroll1s_midroll7s.xml"); + AdId[] expectedAdIds = new AdId[] {CONTENT, ad(0), CONTENT, ad(1), CONTENT}; + ImaHostedTest hostedTest = + new ImaHostedTest(Uri.parse(CONTENT_URI_SHORT), adsResponse, expectedAdIds); + + testRule.getActivity().runTest(hostedTest, TIMEOUT_MS); + } + + @Test + public void playbackWithMidrolls10And20WithSeekTo12_playsAdsAndContent() throws Exception { + String adsResponse = + TestUtil.getString( + /* context= */ testRule.getActivity(), "ad-responses/midroll10s_midroll20s.xml"); + AdId[] expectedAdIds = new AdId[] {CONTENT, ad(0), CONTENT, ad(1), CONTENT}; + ImaHostedTest hostedTest = + new ImaHostedTest(Uri.parse(CONTENT_URI_LONG), adsResponse, expectedAdIds); + hostedTest.setSchedule( + new ActionSchedule.Builder(TAG) + .waitForPlaybackState(Player.STATE_READY) + .seek(12 * C.MILLIS_PER_SECOND) + .build()); + testRule.getActivity().runTest(hostedTest, TIMEOUT_MS); + } + + @Ignore("The second ad doesn't preload so playback gets stuck. See [internal: b/155615925].") + @Test + public void playbackWithMidrolls10And20WithSeekTo18_playsAdsAndContent() throws Exception { + String adsResponse = + TestUtil.getString( + /* context= */ testRule.getActivity(), "ad-responses/midroll10s_midroll20s.xml"); + AdId[] expectedAdIds = new AdId[] {CONTENT, ad(0), CONTENT, ad(1), CONTENT}; + ImaHostedTest hostedTest = + new ImaHostedTest(Uri.parse(CONTENT_URI_LONG), adsResponse, expectedAdIds); + hostedTest.setSchedule( + new ActionSchedule.Builder(TAG) + .waitForPlaybackState(Player.STATE_READY) + .seek(18 * C.MILLIS_PER_SECOND) + .build()); + testRule.getActivity().runTest(hostedTest, TIMEOUT_MS); + } + + private static AdId ad(int groupIndex) { + return new AdId(groupIndex, /* indexInGroup= */ 0); + } + + private static final class AdId { + + public final int groupIndex; + public final int indexInGroup; + + public AdId(int groupIndex, int indexInGroup) { + this.groupIndex = groupIndex; + this.indexInGroup = indexInGroup; + } + + @Override + public String toString() { + return "(" + groupIndex + ", " + indexInGroup + ')'; + } + + @Override + public boolean equals(@Nullable Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + AdId that = (AdId) o; + + if (groupIndex != that.groupIndex) { + return false; + } + return indexInGroup == that.indexInGroup; + } + + @Override + public int hashCode() { + int result = groupIndex; + result = 31 * result + indexInGroup; + return result; + } + } + + private static final class ImaHostedTest extends ExoHostedTest implements EventListener { + + private final Uri contentUri; + private final String adsResponse; + private final List expectedAdIds; + private final List seenAdIds; + private @MonotonicNonNull ImaAdsLoader imaAdsLoader; + private @MonotonicNonNull SimpleExoPlayer player; + + private ImaHostedTest(Uri contentUri, String adsResponse, AdId... expectedAdIds) { + // fullPlaybackNoSeeking is false as the playback lasts longer than the content source + // duration due to ad playback, so the hosted test shouldn't assert the playing duration. + super(ImaPlaybackTest.class.getSimpleName(), /* fullPlaybackNoSeeking= */ false); + this.contentUri = contentUri; + this.adsResponse = adsResponse; + this.expectedAdIds = Arrays.asList(expectedAdIds); + seenAdIds = new ArrayList<>(); + } + + @Override + protected SimpleExoPlayer buildExoPlayer( + HostActivity host, Surface surface, MappingTrackSelector trackSelector) { + player = super.buildExoPlayer(host, surface, trackSelector); + player.addAnalyticsListener( + new AnalyticsListener() { + @Override + public void onTimelineChanged(EventTime eventTime, @TimelineChangeReason int reason) { + maybeUpdateSeenAdIdentifiers(); + } + + @Override + public void onPositionDiscontinuity( + EventTime eventTime, @DiscontinuityReason int reason) { + if (reason != Player.DISCONTINUITY_REASON_SEEK) { + maybeUpdateSeenAdIdentifiers(); + } + } + }); + Context context = host.getApplicationContext(); + imaAdsLoader = new ImaAdsLoader.Builder(context).buildForAdsResponse(adsResponse); + imaAdsLoader.setPlayer(player); + return player; + } + + @Override + protected MediaSource buildSource( + HostActivity host, + String userAgent, + DrmSessionManager drmSessionManager, + FrameLayout overlayFrameLayout) { + Context context = host.getApplicationContext(); + DataSource.Factory dataSourceFactory = + new DefaultDataSourceFactory( + context, Util.getUserAgent(context, ImaPlaybackTest.class.getSimpleName())); + MediaSource contentMediaSource = + DefaultMediaSourceFactory.newInstance(context) + .createMediaSource(MediaItem.fromUri(contentUri)); + return new AdsMediaSource( + contentMediaSource, + dataSourceFactory, + Assertions.checkNotNull(imaAdsLoader), + new AdViewProvider() { + @Override + public ViewGroup getAdViewGroup() { + return overlayFrameLayout; + } + + @Override + public View[] getAdOverlayViews() { + return new View[0]; + } + }); + } + + @Override + protected void assertPassed(DecoderCounters audioCounters, DecoderCounters videoCounters) { + assertThat(seenAdIds).isEqualTo(expectedAdIds); + } + + private void maybeUpdateSeenAdIdentifiers() { + if (Assertions.checkNotNull(player) + .getCurrentTimeline() + .getWindow(/* windowIndex= */ 0, new Window()) + .isPlaceholder) { + // The window is still an initial placeholder so do nothing. + return; + } + AdId adId = new AdId(player.getCurrentAdGroupIndex(), player.getCurrentAdIndexInAdGroup()); + if (seenAdIds.isEmpty() || !seenAdIds.get(seenAdIds.size() - 1).equals(adId)) { + seenAdIds.add(adId); + } + } + } +} 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 98dbef7c6c..77e0f0f7e8 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 @@ -15,8 +15,11 @@ */ package com.google.android.exoplayer2.ext.ima; +import static com.google.android.exoplayer2.util.Util.castNonNull; + import android.content.Context; import android.net.Uri; +import android.os.Handler; import android.os.Looper; import android.os.SystemClock; import android.view.View; @@ -24,7 +27,6 @@ import android.view.ViewGroup; import androidx.annotation.IntDef; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; -import com.google.ads.interactivemedia.v3.api.Ad; import com.google.ads.interactivemedia.v3.api.AdDisplayContainer; import com.google.ads.interactivemedia.v3.api.AdError; import com.google.ads.interactivemedia.v3.api.AdError.AdErrorCode; @@ -43,6 +45,7 @@ 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.UiElement; +import com.google.ads.interactivemedia.v3.api.player.AdMediaInfo; import com.google.ads.interactivemedia.v3.api.player.ContentProgressProvider; import com.google.ads.interactivemedia.v3.api.player.VideoAdPlayer; import com.google.ads.interactivemedia.v3.api.player.VideoProgressUpdate; @@ -52,7 +55,6 @@ import com.google.android.exoplayer2.ExoPlayerLibraryInfo; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Timeline; 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; @@ -69,6 +71,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; +import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -272,14 +275,17 @@ public final class ImaAdsLoader private static final boolean DEBUG = false; private static final String TAG = "ImaAdsLoader"; - /** - * Whether to enable preloading of ads in {@link AdsRenderingSettings}. - */ - private static final boolean ENABLE_PRELOADING = true; - private static final String IMA_SDK_SETTINGS_PLAYER_TYPE = "google/exo.ext.ima"; private static final String IMA_SDK_SETTINGS_PLAYER_VERSION = ExoPlayerLibraryInfo.VERSION; + /** + * Interval at which ad progress updates are provided to the IMA SDK, in milliseconds. 100 ms is + * the interval recommended by the IMA documentation. + * + * @see com.google.ads.interactivemedia.v3.api.player.VideoAdPlayer.VideoAdPlayerCallback + */ + private static final int AD_PROGRESS_UPDATE_INTERVAL_MS = 100; + /** The value used in {@link VideoProgressUpdate}s to indicate an unset duration. */ private static final long IMA_DURATION_UNSET = -1L; @@ -287,10 +293,7 @@ public final class ImaAdsLoader * Threshold before the end of content at which IMA is notified that content is complete if the * player buffers, in milliseconds. */ - private static final long END_OF_CONTENT_POSITION_THRESHOLD_MS = 5000; - - /** The maximum duration before an ad break that IMA may start preloading the next ad. */ - private static final long MAXIMUM_PRELOAD_DURATION_MS = 8000; + private static final long END_OF_CONTENT_THRESHOLD_MS = 5000; private static final int TIMEOUT_UNSET = -1; private static final int BITRATE_UNSET = -1; @@ -305,11 +308,12 @@ public final class ImaAdsLoader */ private static final int IMA_AD_STATE_NONE = 0; /** - * The ad playback state when IMA has called {@link #playAd()} and not {@link #pauseAd()}. + * The ad playback state when IMA has called {@link #playAd(AdMediaInfo)} and not {@link + * #pauseAd(AdMediaInfo)}. */ private static final int IMA_AD_STATE_PLAYING = 1; /** - * The ad playback state when IMA has called {@link #pauseAd()} while playing an ad. + * The ad playback state when IMA has called {@link #pauseAd(AdMediaInfo)} while playing an ad. */ private static final int IMA_AD_STATE_PAUSED = 2; @@ -323,13 +327,16 @@ public final class ImaAdsLoader @Nullable private final AdEventListener adEventListener; private final ImaFactory imaFactory; private final Timeline.Period period; + private final Handler handler; private final List adCallbacks; private final AdDisplayContainer adDisplayContainer; private final com.google.ads.interactivemedia.v3.api.AdsLoader adsLoader; + private final Runnable updateAdProgressRunnable; + private final Map adInfoByAdMediaInfo; private boolean wasSetPlayerCalled; @Nullable private Player nextPlayer; - private Object pendingAdRequestContext; + @Nullable private Object pendingAdRequestContext; private List supportedMimeTypes; @Nullable private EventListener eventListener; @Nullable private Player player; @@ -337,24 +344,24 @@ public final class ImaAdsLoader private VideoProgressUpdate lastAdProgress; private int lastVolumePercentage; - private AdsManager adsManager; + @Nullable private AdsManager adsManager; private boolean initializedAdsManager; - private AdLoadException pendingAdLoadError; + private boolean hasAdPlaybackState; + @Nullable private AdLoadException pendingAdLoadError; private Timeline timeline; private long contentDurationMs; - private int podIndexOffset; private AdPlaybackState adPlaybackState; // Fields tracking IMA's state. - /** The expected ad group index that IMA should load next. */ - private int expectedAdGroupIndex; - /** 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. */ private boolean imaPausedContent; /** The current ad playback state. */ private @ImaAdState int imaAdState; + /** The current ad media info, or {@code null} if in state {@link #IMA_AD_STATE_NONE}. */ + @Nullable private AdMediaInfo imaAdMediaInfo; + /** The current ad info, or {@code null} if in state {@link #IMA_AD_STATE_NONE}. */ + @Nullable private AdInfo imaAdInfo; /** * Whether {@link com.google.ads.interactivemedia.v3.api.AdsLoader#contentComplete()} has been * called since starting ad playback. @@ -365,20 +372,23 @@ public final class ImaAdsLoader /** Whether the player is playing an ad. */ private boolean playingAd; + /** Whether the player is buffering an ad. */ + private boolean bufferingAd; /** * If the player is playing an ad, stores the ad index in its ad group. {@link C#INDEX_UNSET} * otherwise. */ private int playingAdIndexInAdGroup; /** - * Whether there's a pending ad preparation error which IMA needs to be notified of when it - * transitions from playing content to playing the ad. + * The ad info for a pending ad for which the media failed preparation, or {@code null} if no + * pending ads have failed to prepare. */ - private boolean shouldNotifyAdPrepareError; + @Nullable private AdInfo pendingAdPrepareErrorAdInfo; /** - * If a content period has finished but IMA has not yet called {@link #playAd()}, stores the value - * of {@link SystemClock#elapsedRealtime()} when the content stopped playing. This can be used to - * determine a fake, increasing content position. {@link C#TIME_UNSET} otherwise. + * If a content period has finished but IMA has not yet called {@link #playAd(AdMediaInfo)}, + * stores the value of {@link SystemClock#elapsedRealtime()} when the content stopped playing. + * This can be used to determine a fake, increasing content position. {@link C#TIME_UNSET} + * otherwise. */ private long fakeContentProgressElapsedRealtimeMs; /** @@ -443,6 +453,7 @@ public final class ImaAdsLoader /* imaFactory= */ new DefaultImaFactory()); } + @SuppressWarnings({"nullness:argument.type.incompatible", "methodref.receiver.bound.invalid"}) private ImaAdsLoader( Context context, @Nullable Uri adTagUri, @@ -474,18 +485,26 @@ public final class ImaAdsLoader imaSdkSettings.setPlayerType(IMA_SDK_SETTINGS_PLAYER_TYPE); imaSdkSettings.setPlayerVersion(IMA_SDK_SETTINGS_PLAYER_VERSION); period = new Timeline.Period(); + handler = Util.createHandler(getImaLooper(), /* callback= */ null); adCallbacks = new ArrayList<>(/* initialCapacity= */ 1); adDisplayContainer = imaFactory.createAdDisplayContainer(); adDisplayContainer.setPlayer(/* videoAdPlayer= */ this); - adsLoader = imaFactory.createAdsLoader(context, imaSdkSettings, adDisplayContainer); + adsLoader = + imaFactory.createAdsLoader( + context.getApplicationContext(), imaSdkSettings, adDisplayContainer); adsLoader.addAdErrorListener(/* adErrorListener= */ this); adsLoader.addAdsLoadedListener(/* adsLoadedListener= */ this); + updateAdProgressRunnable = this::updateAdProgress; + adInfoByAdMediaInfo = new HashMap<>(); + supportedMimeTypes = Collections.emptyList(); + lastContentProgress = VideoProgressUpdate.VIDEO_TIME_NOT_READY; + lastAdProgress = VideoProgressUpdate.VIDEO_TIME_NOT_READY; fakeContentProgressElapsedRealtimeMs = C.TIME_UNSET; fakeContentProgressOffsetMs = C.TIME_UNSET; pendingContentPositionMs = C.TIME_UNSET; - adGroupIndex = C.INDEX_UNSET; contentDurationMs = C.TIME_UNSET; timeline = Timeline.EMPTY; + adPlaybackState = AdPlaybackState.NONE; } /** @@ -533,22 +552,22 @@ public final class ImaAdsLoader * @param adViewGroup A {@link ViewGroup} on top of the player that will show any ad UI. */ public void requestAds(ViewGroup adViewGroup) { - if (adPlaybackState != null || adsManager != null || pendingAdRequestContext != null) { + if (hasAdPlaybackState || adsManager != null || pendingAdRequestContext != null) { // Ads have already been requested. return; } adDisplayContainer.setAdContainer(adViewGroup); - pendingAdRequestContext = new Object(); AdsRequest request = imaFactory.createAdsRequest(); if (adTagUri != null) { request.setAdTagUrl(adTagUri.toString()); - } else /* adsResponse != null */ { - request.setAdsResponse(adsResponse); + } else { + request.setAdsResponse(castNonNull(adsResponse)); } if (vastLoadTimeoutMs != TIMEOUT_UNSET) { request.setVastLoadTimeout(vastLoadTimeoutMs); } request.setContentProgressProvider(this); + pendingAdRequestContext = new Object(); request.setUserRequestContext(pendingAdRequestContext); adsLoader.requestAds(request); } @@ -557,9 +576,8 @@ public final class ImaAdsLoader @Override public void setPlayer(@Nullable Player player) { - Assertions.checkState(Looper.getMainLooper() == Looper.myLooper()); - Assertions.checkState( - player == null || player.getApplicationLooper() == Looper.getMainLooper()); + Assertions.checkState(Looper.myLooper() == getImaLooper()); + Assertions.checkState(player == null || player.getApplicationLooper() == getImaLooper()); nextPlayer = player; wasSetPlayerCalled = true; } @@ -568,6 +586,7 @@ public final class ImaAdsLoader public void setSupportedContentTypes(@C.ContentType int... contentTypes) { List supportedMimeTypes = new ArrayList<>(); for (@C.ContentType int contentType : contentTypes) { + // IMA does not support Smooth Streaming ad media. if (contentType == C.TYPE_DASH) { supportedMimeTypes.add(MimeTypes.APPLICATION_MPD); } else if (contentType == C.TYPE_HLS) { @@ -580,8 +599,6 @@ public final class ImaAdsLoader MimeTypes.VIDEO_H263, MimeTypes.AUDIO_MP4, MimeTypes.AUDIO_MPEG)); - } else if (contentType == C.TYPE_SS) { - // IMA does not support Smooth Streaming ad media. } } this.supportedMimeTypes = Collections.unmodifiableList(supportedMimeTypes); @@ -595,22 +612,23 @@ public final class ImaAdsLoader if (player == null) { return; } + player.addListener(this); + boolean playWhenReady = player.getPlayWhenReady(); this.eventListener = eventListener; lastVolumePercentage = 0; - lastAdProgress = null; - lastContentProgress = null; + lastAdProgress = VideoProgressUpdate.VIDEO_TIME_NOT_READY; + lastContentProgress = VideoProgressUpdate.VIDEO_TIME_NOT_READY; ViewGroup adViewGroup = adViewProvider.getAdViewGroup(); adDisplayContainer.setAdContainer(adViewGroup); View[] adOverlayViews = adViewProvider.getAdOverlayViews(); for (View view : adOverlayViews) { adDisplayContainer.registerVideoControlsOverlay(view); } - player.addListener(this); maybeNotifyPendingAdLoadError(); - if (adPlaybackState != null) { + if (hasAdPlaybackState) { // Pass the ad playback state to the player, and resume ads if necessary. eventListener.onAdPlaybackState(adPlaybackState); - if (imaPausedContent && player.getPlayWhenReady()) { + if (adsManager != null && imaPausedContent && playWhenReady) { adsManager.resume(); } } else if (adsManager != null) { @@ -624,21 +642,22 @@ public final class ImaAdsLoader @Override public void stop() { + @Nullable Player player = this.player; if (player == null) { return; } if (adsManager != null && imaPausedContent) { + adsManager.pause(); adPlaybackState = adPlaybackState.withAdResumePositionUs( playingAd ? C.msToUs(player.getCurrentPosition()) : 0); - adsManager.pause(); } lastVolumePercentage = getVolume(); - lastAdProgress = getAdProgress(); + lastAdProgress = getAdVideoProgressUpdate(); lastContentProgress = getContentProgress(); adDisplayContainer.unregisterAllVideoControlsOverlays(); player.removeListener(this); - player = null; + this.player = null; eventListener = null; } @@ -658,8 +677,11 @@ public final class ImaAdsLoader adsLoader.removeAdErrorListener(/* adErrorListener= */ this); imaPausedContent = false; imaAdState = IMA_AD_STATE_NONE; + imaAdMediaInfo = null; + imaAdInfo = null; pendingAdLoadError = null; adPlaybackState = AdPlaybackState.NONE; + hasAdPlaybackState = false; updateAdPlaybackState(); } @@ -695,6 +717,7 @@ public final class ImaAdsLoader // If a player is attached already, start playback immediately. try { adPlaybackState = new AdPlaybackState(getAdGroupTimesUs(adsManager.getAdCuePoints())); + hasAdPlaybackState = true; updateAdPlaybackState(); } catch (Exception e) { maybeNotifyInternalError("onAdsManagerLoaded", e); @@ -711,7 +734,7 @@ public final class ImaAdsLoader Log.d(TAG, "onAdEvent: " + adEventType); } if (adsManager == null) { - Log.w(TAG, "Ignoring AdEvent after release: " + adEvent); + // Drop events after release. return; } try { @@ -732,7 +755,8 @@ public final class ImaAdsLoader if (adsManager == null) { // No ads were loaded, so allow playback to start without any ads. pendingAdRequestContext = null; - adPlaybackState = new AdPlaybackState(); + adPlaybackState = AdPlaybackState.NONE; + hasAdPlaybackState = true; updateAdPlaybackState(); } else if (isAdGroupLoadError(error)) { try { @@ -759,30 +783,11 @@ public final class ImaAdsLoader if (pendingContentPositionMs != C.TIME_UNSET) { sentPendingContentPositionMs = true; contentPositionMs = pendingContentPositionMs; - expectedAdGroupIndex = - adPlaybackState.getAdGroupIndexForPositionUs(C.msToUs(contentPositionMs)); } else if (fakeContentProgressElapsedRealtimeMs != C.TIME_UNSET) { long elapsedSinceEndMs = SystemClock.elapsedRealtime() - fakeContentProgressElapsedRealtimeMs; contentPositionMs = fakeContentProgressOffsetMs + elapsedSinceEndMs; - expectedAdGroupIndex = - adPlaybackState.getAdGroupIndexForPositionUs(C.msToUs(contentPositionMs)); } 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 - // until MAXIMUM_PRELOAD_DURATION_MS before the ad so that an ad group load error delivered - // just after an ad group isn't incorrectly attributed to the next ad group. - int nextAdGroupIndex = - adPlaybackState.getAdGroupIndexAfterPositionUs( - C.msToUs(contentPositionMs), C.msToUs(contentDurationMs)); - if (nextAdGroupIndex != expectedAdGroupIndex && nextAdGroupIndex != C.INDEX_UNSET) { - long nextAdGroupTimeMs = C.usToMs(adPlaybackState.adGroupTimesUs[nextAdGroupIndex]); - if (nextAdGroupTimeMs == C.TIME_END_OF_SOURCE) { - nextAdGroupTimeMs = contentDurationMs; - } - if (nextAdGroupTimeMs - contentPositionMs < MAXIMUM_PRELOAD_DURATION_MS) { - expectedAdGroupIndex = nextAdGroupIndex; - } - } + contentPositionMs = getContentPeriodPositionMs(player, timeline, period); } else { return VideoProgressUpdate.VIDEO_TIME_NOT_READY; } @@ -794,24 +799,17 @@ public final class ImaAdsLoader @Override public VideoProgressUpdate getAdProgress() { - if (player == null) { - return lastAdProgress; - } else if (imaAdState != IMA_AD_STATE_NONE && playingAd) { - long adDuration = player.getDuration(); - return adDuration == C.TIME_UNSET ? VideoProgressUpdate.VIDEO_TIME_NOT_READY - : new VideoProgressUpdate(player.getCurrentPosition(), adDuration); - } else { - return VideoProgressUpdate.VIDEO_TIME_NOT_READY; - } + throw new IllegalStateException("Unexpected call to getAdProgress when using preloading"); } @Override public int getVolume() { + @Nullable Player player = this.player; if (player == null) { return lastVolumePercentage; } - Player.AudioComponent audioComponent = player.getAudioComponent(); + @Nullable Player.AudioComponent audioComponent = player.getAudioComponent(); if (audioComponent != null) { return (int) (audioComponent.getVolume() * 100); } @@ -827,30 +825,37 @@ public final class ImaAdsLoader } @Override - public void loadAd(String adUriString) { + public void loadAd(AdMediaInfo adMediaInfo, AdPodInfo adPodInfo) { try { if (DEBUG) { - Log.d(TAG, "loadAd in ad group " + adGroupIndex); + Log.d(TAG, "loadAd " + getAdMediaInfoString(adMediaInfo) + ", ad pod " + adPodInfo); } if (adsManager == null) { - Log.w(TAG, "Ignoring loadAd after release"); + // Drop events after release. return; } - if (adGroupIndex == C.INDEX_UNSET) { - Log.w( - TAG, - "Unexpected loadAd without LOADED event; assuming ad group index is actually " - + expectedAdGroupIndex); - adGroupIndex = expectedAdGroupIndex; - adsManager.start(); + int adGroupIndex = getAdGroupIndex(adPodInfo); + int adIndexInAdGroup = adPodInfo.getAdPosition() - 1; + AdInfo adInfo = new AdInfo(adGroupIndex, adIndexInAdGroup); + adInfoByAdMediaInfo.put(adMediaInfo, adInfo); + AdPlaybackState.AdGroup adGroup = adPlaybackState.adGroups[adInfo.adGroupIndex]; + if (adGroup.count == C.LENGTH_UNSET) { + adPlaybackState = + adPlaybackState.withAdCount( + adInfo.adGroupIndex, Math.max(adPodInfo.getTotalAds(), adGroup.states.length)); + adGroup = adPlaybackState.adGroups[adInfo.adGroupIndex]; } - int adIndexInAdGroup = getAdIndexInAdGroupToLoad(adGroupIndex); - if (adIndexInAdGroup == C.INDEX_UNSET) { - Log.w(TAG, "Unexpected loadAd in an ad group with no remaining unavailable ads"); - return; + for (int i = 0; i < adIndexInAdGroup; i++) { + // Any preceding ads that haven't loaded are not going to load. + if (adGroup.states[i] == AdPlaybackState.AD_STATE_UNAVAILABLE) { + adPlaybackState = + adPlaybackState.withAdLoadError( + /* adGroupIndex= */ adGroupIndex, /* adIndexInAdGroup= */ i); + } } + Uri adUri = Uri.parse(adMediaInfo.getUrl()); adPlaybackState = - adPlaybackState.withAdUri(adGroupIndex, adIndexInAdGroup, Uri.parse(adUriString)); + adPlaybackState.withAdUri(adInfo.adGroupIndex, adInfo.adIndexInAdGroup, adUri); updateAdPlaybackState(); } catch (Exception e) { maybeNotifyInternalError("loadAd", e); @@ -868,69 +873,62 @@ public final class ImaAdsLoader } @Override - public void playAd() { + public void playAd(AdMediaInfo adMediaInfo) { if (DEBUG) { - Log.d(TAG, "playAd"); + Log.d(TAG, "playAd " + getAdMediaInfoString(adMediaInfo)); } if (adsManager == null) { - Log.w(TAG, "Ignoring playAd after release"); + // Drop events after release. return; } - switch (imaAdState) { - case IMA_AD_STATE_PLAYING: - // IMA does not always call stopAd before resuming content. - // See [Internal: b/38354028, b/63320878]. - Log.w(TAG, "Unexpected playAd without stopAd"); - break; - case IMA_AD_STATE_NONE: - // IMA is requesting to play the ad, so stop faking the content position. - fakeContentProgressElapsedRealtimeMs = C.TIME_UNSET; - fakeContentProgressOffsetMs = C.TIME_UNSET; - imaAdState = IMA_AD_STATE_PLAYING; - for (int i = 0; i < adCallbacks.size(); i++) { - adCallbacks.get(i).onPlay(); - } - if (shouldNotifyAdPrepareError) { - shouldNotifyAdPrepareError = false; - for (int i = 0; i < adCallbacks.size(); i++) { - adCallbacks.get(i).onError(); - } - } - break; - case IMA_AD_STATE_PAUSED: - imaAdState = IMA_AD_STATE_PLAYING; - for (int i = 0; i < adCallbacks.size(); i++) { - adCallbacks.get(i).onResume(); - } - break; - default: - throw new IllegalStateException(); + + if (imaAdState == IMA_AD_STATE_PLAYING) { + // IMA does not always call stopAd before resuming content. + // See [Internal: b/38354028]. + Log.w(TAG, "Unexpected playAd without stopAd"); } - if (player == null) { - // Sometimes messages from IMA arrive after detaching the player. See [Internal: b/63801642]. - Log.w(TAG, "Unexpected playAd while detached"); - } else if (!player.getPlayWhenReady()) { - adsManager.pause(); + + if (imaAdState == IMA_AD_STATE_NONE) { + // IMA is requesting to play the ad, so stop faking the content position. + fakeContentProgressElapsedRealtimeMs = C.TIME_UNSET; + fakeContentProgressOffsetMs = C.TIME_UNSET; + imaAdState = IMA_AD_STATE_PLAYING; + imaAdMediaInfo = adMediaInfo; + imaAdInfo = Assertions.checkNotNull(adInfoByAdMediaInfo.get(adMediaInfo)); + for (int i = 0; i < adCallbacks.size(); i++) { + adCallbacks.get(i).onPlay(adMediaInfo); + } + if (pendingAdPrepareErrorAdInfo != null && pendingAdPrepareErrorAdInfo.equals(imaAdInfo)) { + pendingAdPrepareErrorAdInfo = null; + for (int i = 0; i < adCallbacks.size(); i++) { + adCallbacks.get(i).onError(adMediaInfo); + } + } + updateAdProgress(); + } else { + imaAdState = IMA_AD_STATE_PLAYING; + Assertions.checkState(adMediaInfo.equals(imaAdMediaInfo)); + for (int i = 0; i < adCallbacks.size(); i++) { + adCallbacks.get(i).onResume(adMediaInfo); + } + } + if (!Assertions.checkNotNull(player).getPlayWhenReady()) { + Assertions.checkNotNull(adsManager).pause(); } } @Override - public void stopAd() { + public void stopAd(AdMediaInfo adMediaInfo) { if (DEBUG) { - Log.d(TAG, "stopAd"); + Log.d(TAG, "stopAd " + getAdMediaInfoString(adMediaInfo)); } 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"); - } - if (imaAdState == IMA_AD_STATE_NONE) { - Log.w(TAG, "Unexpected stopAd"); + // Drop event after release. return; } + + Assertions.checkNotNull(player); + Assertions.checkState(imaAdState != IMA_AD_STATE_NONE); try { stopAdInternal(); } catch (Exception e) { @@ -939,26 +937,21 @@ public final class ImaAdsLoader } @Override - public void pauseAd() { + public void pauseAd(AdMediaInfo adMediaInfo) { if (DEBUG) { - Log.d(TAG, "pauseAd"); + Log.d(TAG, "pauseAd " + getAdMediaInfoString(adMediaInfo)); } if (imaAdState == IMA_AD_STATE_NONE) { // This method is called after content is resumed. return; } + Assertions.checkState(adMediaInfo.equals(imaAdMediaInfo)); imaAdState = IMA_AD_STATE_PAUSED; for (int i = 0; i < adCallbacks.size(); i++) { - adCallbacks.get(i).onPause(); + adCallbacks.get(i).onPause(adMediaInfo); } } - @Override - public void resumeAd() { - // This method is never called. See [Internal: b/18931719]. - maybeNotifyInternalError("resumeAd", new IllegalStateException("Unexpected call to resumeAd")); - } - // Player.EventListener implementation. @Override @@ -969,21 +962,35 @@ public final class ImaAdsLoader } Assertions.checkArgument(timeline.getPeriodCount() == 1); this.timeline = timeline; - long contentDurationUs = timeline.getPeriod(0, period).durationUs; + long contentDurationUs = timeline.getPeriod(/* periodIndex= */ 0, period).durationUs; contentDurationMs = C.usToMs(contentDurationUs); if (contentDurationUs != C.TIME_UNSET) { adPlaybackState = adPlaybackState.withContentDurationUs(contentDurationUs); } if (!initializedAdsManager && adsManager != null) { initializedAdsManager = true; - initializeAdsManager(); + initializeAdsManager(adsManager); } - onPositionDiscontinuity(Player.DISCONTINUITY_REASON_INTERNAL); + handleTimelineOrPositionChanged(); } @Override - public void onPlayerStateChanged(boolean playWhenReady, @Player.State int playbackState) { - if (adsManager == null) { + public void onPositionDiscontinuity(@Player.DiscontinuityReason int reason) { + handleTimelineOrPositionChanged(); + } + + @Override + public void onPlaybackStateChanged(@Player.State int playbackState) { + if (adsManager == null || player == null) { + return; + } + handlePlayerStateChanged(player.getPlayWhenReady(), playbackState); + } + + @Override + public void onPlayWhenReadyChanged( + boolean playWhenReady, @Player.PlayWhenReadyChangeReason int reason) { + if (adsManager == null || player == null) { return; } @@ -996,64 +1003,24 @@ public final class ImaAdsLoader adsManager.resume(); return; } - - if (imaAdState == IMA_AD_STATE_NONE && playbackState == Player.STATE_BUFFERING - && playWhenReady) { - checkForContentComplete(); - } else if (imaAdState != IMA_AD_STATE_NONE && playbackState == Player.STATE_ENDED) { - for (int i = 0; i < adCallbacks.size(); i++) { - adCallbacks.get(i).onEnded(); - } - if (DEBUG) { - Log.d(TAG, "VideoAdPlayerCallback.onEnded in onPlayerStateChanged"); - } - } + handlePlayerStateChanged(playWhenReady, player.getPlaybackState()); } @Override public void onPlayerError(ExoPlaybackException error) { if (imaAdState != IMA_AD_STATE_NONE) { + AdMediaInfo adMediaInfo = Assertions.checkNotNull(imaAdMediaInfo); for (int i = 0; i < adCallbacks.size(); i++) { - adCallbacks.get(i).onError(); + adCallbacks.get(i).onError(adMediaInfo); } } } - @Override - public void onPositionDiscontinuity(@Player.DiscontinuityReason int reason) { - if (adsManager == null) { - return; - } - if (!playingAd && !player.isPlayingAd()) { - checkForContentComplete(); - if (sentContentComplete) { - for (int i = 0; i < adPlaybackState.adGroupCount; i++) { - if (adPlaybackState.adGroupTimesUs[i] != C.TIME_END_OF_SOURCE) { - adPlaybackState = adPlaybackState.withSkippedAdGroup(i); - } - } - updateAdPlaybackState(); - } else if (!timeline.isEmpty()) { - long positionMs = player.getCurrentPosition(); - timeline.getPeriod(0, period); - int newAdGroupIndex = period.getAdGroupIndexForPositionUs(C.msToUs(positionMs)); - if (newAdGroupIndex != C.INDEX_UNSET) { - sentPendingContentPositionMs = false; - pendingContentPositionMs = positionMs; - if (newAdGroupIndex != adGroupIndex) { - shouldNotifyAdPrepareError = false; - } - } - } - } - updateImaStateForPlayerState(); - } - // Internal methods. - private void initializeAdsManager() { + private void initializeAdsManager(AdsManager adsManager) { AdsRenderingSettings adsRenderingSettings = imaFactory.createAdsRenderingSettings(); - adsRenderingSettings.setEnablePreloading(ENABLE_PRELOADING); + adsRenderingSettings.setEnablePreloading(true); adsRenderingSettings.setMimeTypes(supportedMimeTypes); if (mediaLoadTimeoutMs != TIMEOUT_UNSET) { adsRenderingSettings.setLoadVideoTimeout(mediaLoadTimeoutMs); @@ -1068,9 +1035,11 @@ public final class ImaAdsLoader // Skip ads based on the start position as required. long[] adGroupTimesUs = getAdGroupTimesUs(adsManager.getAdCuePoints()); - long contentPositionMs = player.getContentPosition(); + long contentPositionMs = + getContentPeriodPositionMs(Assertions.checkNotNull(player), timeline, period); int adGroupIndexForPosition = - adPlaybackState.getAdGroupIndexForPositionUs(C.msToUs(contentPositionMs)); + adPlaybackState.getAdGroupIndexForPositionUs( + C.msToUs(contentPositionMs), C.msToUs(contentDurationMs)); if (adGroupIndexForPosition > 0 && adGroupIndexForPosition != C.INDEX_UNSET) { // Skip any ad groups before the one at or immediately before the playback position. for (int i = 0; i < adGroupIndexForPosition; i++) { @@ -1084,25 +1053,13 @@ public final class ImaAdsLoader adsRenderingSettings.setPlayAdsAfterTime(midpointTimeUs / C.MICROS_PER_SECOND); } - // IMA indexes any remaining midroll ad pods from 1. A preroll (if present) has index 0. - // Store an index offset as we want to index all ads (including skipped ones) from 0. - if (adGroupIndexForPosition == 0 && adGroupTimesUs[0] == 0) { - // We are playing a preroll. - podIndexOffset = 0; - } else if (adGroupIndexForPosition == C.INDEX_UNSET) { - // There's no ad to play which means there's no preroll. - podIndexOffset = -1; - } else { - // We are playing a midroll and any ads before it were skipped. - podIndexOffset = adGroupIndexForPosition - 1; - } - if (adGroupIndexForPosition != C.INDEX_UNSET && hasMidrollAdGroups(adGroupTimesUs)) { // Provide the player's initial position to trigger loading and playing the ad. pendingContentPositionMs = contentPositionMs; } adsManager.init(adsRenderingSettings); + adsManager.start(); updateAdPlaybackState(); if (DEBUG) { Log.d(TAG, "Initialized with ads rendering settings: " + adsRenderingSettings); @@ -1110,39 +1067,32 @@ public final class ImaAdsLoader } private void handleAdEvent(AdEvent adEvent) { - Ad ad = adEvent.getAd(); switch (adEvent.getType()) { - case LOADED: - // The ad position is not always accurate when using preloading. See [Internal: b/62613240]. - AdPodInfo adPodInfo = ad.getAdPodInfo(); - int podIndex = adPodInfo.getPodIndex(); - adGroupIndex = - podIndex == -1 ? (adPlaybackState.adGroupCount - 1) : (podIndex + podIndexOffset); - int adPosition = adPodInfo.getAdPosition(); - int adCount = adPodInfo.getTotalAds(); - adsManager.start(); + case AD_BREAK_FETCH_ERROR: + String adGroupTimeSecondsString = + Assertions.checkNotNull(adEvent.getAdData().get("adBreakTime")); if (DEBUG) { - Log.d(TAG, "Loaded ad " + adPosition + " of " + adCount + " in group " + adGroupIndex); + Log.d(TAG, "Fetch error for ad at " + adGroupTimeSecondsString + " seconds"); } - int oldAdCount = adPlaybackState.adGroups[adGroupIndex].count; - if (adCount != oldAdCount) { - if (oldAdCount == C.LENGTH_UNSET) { - adPlaybackState = adPlaybackState.withAdCount(adGroupIndex, adCount); - updateAdPlaybackState(); - } else { - // IMA sometimes unexpectedly decreases the ad count in an ad group. - Log.w(TAG, "Unexpected ad count in LOADED, " + adCount + ", expected " + oldAdCount); + int adGroupTimeSeconds = Integer.parseInt(adGroupTimeSecondsString); + int adGroupIndex = + Arrays.binarySearch( + adPlaybackState.adGroupTimesUs, C.MICROS_PER_SECOND * adGroupTimeSeconds); + AdPlaybackState.AdGroup adGroup = adPlaybackState.adGroups[adGroupIndex]; + if (adGroup.count == C.LENGTH_UNSET) { + adPlaybackState = + adPlaybackState.withAdCount(adGroupIndex, Math.max(1, adGroup.states.length)); + adGroup = adPlaybackState.adGroups[adGroupIndex]; + } + for (int i = 0; i < adGroup.count; i++) { + if (adGroup.states[i] == AdPlaybackState.AD_STATE_UNAVAILABLE) { + if (DEBUG) { + Log.d(TAG, "Removing ad " + i + " in ad group " + adGroupIndex); + } + adPlaybackState = adPlaybackState.withAdLoadError(adGroupIndex, i); } } - if (adGroupIndex != expectedAdGroupIndex) { - Log.w( - TAG, - "Expected ad group index " - + expectedAdGroupIndex - + ", actual ad group index " - + adGroupIndex); - expectedAdGroupIndex = adGroupIndex; - } + updateAdPlaybackState(); break; case CONTENT_PAUSE_REQUESTED: // After CONTENT_PAUSE_REQUESTED, IMA will playAd/pauseAd/stopAd to show one or more ads @@ -1168,18 +1118,97 @@ public final class ImaAdsLoader Map adData = adEvent.getAdData(); String message = "AdEvent: " + adData; Log.i(TAG, message); - if ("adLoadError".equals(adData.get("type"))) { - handleAdGroupLoadError(new IOException(message)); - } break; - case STARTED: - case ALL_ADS_COMPLETED: default: break; } } - private void updateImaStateForPlayerState() { + private VideoProgressUpdate getAdVideoProgressUpdate() { + if (player == null) { + return lastAdProgress; + } else if (imaAdState != IMA_AD_STATE_NONE && playingAd) { + long adDuration = player.getDuration(); + return adDuration == C.TIME_UNSET + ? VideoProgressUpdate.VIDEO_TIME_NOT_READY + : new VideoProgressUpdate(player.getCurrentPosition(), adDuration); + } else { + return VideoProgressUpdate.VIDEO_TIME_NOT_READY; + } + } + + private void updateAdProgress() { + VideoProgressUpdate videoProgressUpdate = getAdVideoProgressUpdate(); + AdMediaInfo adMediaInfo = Assertions.checkNotNull(imaAdMediaInfo); + for (int i = 0; i < adCallbacks.size(); i++) { + adCallbacks.get(i).onAdProgress(adMediaInfo, videoProgressUpdate); + } + handler.removeCallbacks(updateAdProgressRunnable); + handler.postDelayed(updateAdProgressRunnable, AD_PROGRESS_UPDATE_INTERVAL_MS); + } + + private void stopUpdatingAdProgress() { + handler.removeCallbacks(updateAdProgressRunnable); + } + + private void handlePlayerStateChanged(boolean playWhenReady, @Player.State int playbackState) { + if (playingAd && imaAdState == IMA_AD_STATE_PLAYING) { + if (!bufferingAd && playbackState == Player.STATE_BUFFERING) { + AdMediaInfo adMediaInfo = Assertions.checkNotNull(imaAdMediaInfo); + for (int i = 0; i < adCallbacks.size(); i++) { + adCallbacks.get(i).onBuffering(adMediaInfo); + } + stopUpdatingAdProgress(); + } else if (bufferingAd && playbackState == Player.STATE_READY) { + bufferingAd = false; + updateAdProgress(); + } + } + + if (imaAdState == IMA_AD_STATE_NONE + && playbackState == Player.STATE_BUFFERING + && playWhenReady) { + checkForContentComplete(); + } else if (imaAdState != IMA_AD_STATE_NONE && playbackState == Player.STATE_ENDED) { + AdMediaInfo adMediaInfo = Assertions.checkNotNull(imaAdMediaInfo); + if (adMediaInfo == null) { + Log.w(TAG, "onEnded without ad media info"); + } else { + for (int i = 0; i < adCallbacks.size(); i++) { + adCallbacks.get(i).onEnded(adMediaInfo); + } + } + if (DEBUG) { + Log.d(TAG, "VideoAdPlayerCallback.onEnded in onPlaybackStateChanged"); + } + } + } + + private void handleTimelineOrPositionChanged() { + @Nullable Player player = this.player; + if (adsManager == null || player == null) { + return; + } + if (!playingAd && !player.isPlayingAd()) { + checkForContentComplete(); + if (sentContentComplete) { + for (int i = 0; i < adPlaybackState.adGroupCount; i++) { + if (adPlaybackState.adGroupTimesUs[i] != C.TIME_END_OF_SOURCE) { + adPlaybackState = adPlaybackState.withSkippedAdGroup(/* adGroupIndex= */ i); + } + } + updateAdPlaybackState(); + } else if (!timeline.isEmpty()) { + long positionMs = getContentPeriodPositionMs(player, timeline, period); + timeline.getPeriod(/* periodIndex= */ 0, period); + int newAdGroupIndex = period.getAdGroupIndexForPositionUs(C.msToUs(positionMs)); + if (newAdGroupIndex != C.INDEX_UNSET) { + sentPendingContentPositionMs = false; + pendingContentPositionMs = positionMs; + } + } + } + boolean wasPlayingAd = playingAd; int oldPlayingAdIndexInAdGroup = playingAdIndexInAdGroup; playingAd = player.isPlayingAd(); @@ -1188,8 +1217,13 @@ public final class ImaAdsLoader if (adFinished) { // IMA is waiting for the ad playback to finish so invoke the callback now. // Either CONTENT_RESUME_REQUESTED will be passed next, or playAd will be called again. - for (int i = 0; i < adCallbacks.size(); i++) { - adCallbacks.get(i).onEnded(); + @Nullable AdMediaInfo adMediaInfo = imaAdMediaInfo; + if (adMediaInfo == null) { + Log.w(TAG, "onEnded without ad media info"); + } else { + for (int i = 0; i < adCallbacks.size(); i++) { + adCallbacks.get(i).onEnded(adMediaInfo); + } } if (DEBUG) { Log.d(TAG, "VideoAdPlayerCallback.onEnded in onTimelineChanged/onPositionDiscontinuity"); @@ -1207,15 +1241,8 @@ public final class ImaAdsLoader } private void resumeContentInternal() { - if (imaAdState != IMA_AD_STATE_NONE) { - imaAdState = IMA_AD_STATE_NONE; - if (DEBUG) { - Log.d(TAG, "Unexpected CONTENT_RESUME_REQUESTED without stopAd"); - } - } - if (adGroupIndex != C.INDEX_UNSET) { - adPlaybackState = adPlaybackState.withSkippedAdGroup(adGroupIndex); - adGroupIndex = C.INDEX_UNSET; + if (imaAdInfo != null) { + adPlaybackState = adPlaybackState.withSkippedAdGroup(imaAdInfo.adGroupIndex); updateAdPlaybackState(); } } @@ -1230,23 +1257,40 @@ public final class ImaAdsLoader private void stopAdInternal() { imaAdState = IMA_AD_STATE_NONE; - int adIndexInAdGroup = adPlaybackState.adGroups[adGroupIndex].getFirstAdIndexToPlay(); + stopUpdatingAdProgress(); // TODO: Handle the skipped event so the ad can be marked as skipped rather than played. + Assertions.checkNotNull(imaAdInfo); + int adGroupIndex = imaAdInfo.adGroupIndex; + int adIndexInAdGroup = imaAdInfo.adIndexInAdGroup; adPlaybackState = adPlaybackState.withPlayedAd(adGroupIndex, adIndexInAdGroup).withAdResumePositionUs(0); updateAdPlaybackState(); if (!playingAd) { - adGroupIndex = C.INDEX_UNSET; + imaAdMediaInfo = null; + imaAdInfo = null; } } private void handleAdGroupLoadError(Exception error) { - int adGroupIndex = - this.adGroupIndex == C.INDEX_UNSET ? expectedAdGroupIndex : this.adGroupIndex; - if (adGroupIndex == C.INDEX_UNSET) { - // Drop the error, as we don't know which ad group it relates to. + if (player == null) { return; } + + // TODO: Once IMA signals which ad group failed to load, clean up this code. + long playerPositionMs = player.getContentPosition(); + int adGroupIndex = + adPlaybackState.getAdGroupIndexForPositionUs( + C.msToUs(playerPositionMs), C.msToUs(contentDurationMs)); + if (adGroupIndex == C.INDEX_UNSET) { + adGroupIndex = + adPlaybackState.getAdGroupIndexAfterPositionUs( + C.msToUs(playerPositionMs), C.msToUs(contentDurationMs)); + if (adGroupIndex == C.INDEX_UNSET) { + // The error doesn't seem to relate to any ad group so give up handling it. + return; + } + } + AdPlaybackState.AdGroup adGroup = adPlaybackState.adGroups[adGroupIndex]; if (adGroup.count == C.LENGTH_UNSET) { adPlaybackState = @@ -1286,19 +1330,20 @@ public final class ImaAdsLoader if (fakeContentProgressOffsetMs == C.TIME_END_OF_SOURCE) { fakeContentProgressOffsetMs = contentDurationMs; } - shouldNotifyAdPrepareError = true; + pendingAdPrepareErrorAdInfo = new AdInfo(adGroupIndex, adIndexInAdGroup); } else { + AdMediaInfo adMediaInfo = Assertions.checkNotNull(imaAdMediaInfo); // We're already playing an ad. if (adIndexInAdGroup > playingAdIndexInAdGroup) { // Mark the playing ad as ended so we can notify the error on the next ad and remove it, // which means that the ad after will load (if any). for (int i = 0; i < adCallbacks.size(); i++) { - adCallbacks.get(i).onEnded(); + adCallbacks.get(i).onEnded(adMediaInfo); } } playingAdIndexInAdGroup = adPlaybackState.adGroups[adGroupIndex].getFirstAdIndexToPlay(); for (int i = 0; i < adCallbacks.size(); i++) { - adCallbacks.get(i).onError(); + adCallbacks.get(i).onError(Assertions.checkNotNull(adMediaInfo)); } } adPlaybackState = adPlaybackState.withAdLoadError(adGroupIndex, adIndexInAdGroup); @@ -1306,18 +1351,16 @@ public final class ImaAdsLoader } private void checkForContentComplete() { - if (contentDurationMs != C.TIME_UNSET && pendingContentPositionMs == C.TIME_UNSET - && player.getContentPosition() + END_OF_CONTENT_POSITION_THRESHOLD_MS >= contentDurationMs - && !sentContentComplete) { + long positionMs = getContentPeriodPositionMs(Assertions.checkNotNull(player), timeline, period); + if (!sentContentComplete + && contentDurationMs != C.TIME_UNSET + && pendingContentPositionMs == C.TIME_UNSET + && positionMs + END_OF_CONTENT_THRESHOLD_MS >= contentDurationMs) { adsLoader.contentComplete(); if (DEBUG) { Log.d(TAG, "adsLoader.contentComplete"); } sentContentComplete = true; - // After sending content complete IMA will not poll the content position, so set the expected - // ad group index. - expectedAdGroupIndex = - adPlaybackState.getAdGroupIndexForPositionUs(C.msToUs(contentDurationMs)); } } @@ -1328,24 +1371,9 @@ public final class ImaAdsLoader } } - /** - * 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. - */ - private int getAdIndexInAdGroupToLoad(int adGroupIndex) { - @AdState int[] states = adPlaybackState.adGroups[adGroupIndex].states; - int adIndexInAdGroup = 0; - // IMA loads ads in order. - while (adIndexInAdGroup < states.length - && states[adIndexInAdGroup] != AdPlaybackState.AD_STATE_UNAVAILABLE) { - adIndexInAdGroup++; - } - return adIndexInAdGroup == states.length ? C.INDEX_UNSET : adIndexInAdGroup; - } - private void maybeNotifyPendingAdLoadError() { if (pendingAdLoadError != null && eventListener != null) { - eventListener.onAdLoadError(pendingAdLoadError, new DataSpec(adTagUri)); + eventListener.onAdLoadError(pendingAdLoadError, getAdsDataSpec(adTagUri)); pendingAdLoadError = null; } } @@ -1354,21 +1382,44 @@ public final class ImaAdsLoader String message = "Internal error in " + name; Log.e(TAG, message, cause); // We can't recover from an unexpected error in general, so skip all remaining ads. - if (adPlaybackState == null) { - adPlaybackState = AdPlaybackState.NONE; - } else { - for (int i = 0; i < adPlaybackState.adGroupCount; i++) { - adPlaybackState = adPlaybackState.withSkippedAdGroup(i); - } + for (int i = 0; i < adPlaybackState.adGroupCount; i++) { + adPlaybackState = adPlaybackState.withSkippedAdGroup(i); } updateAdPlaybackState(); if (eventListener != null) { eventListener.onAdLoadError( AdLoadException.createForUnexpected(new RuntimeException(message, cause)), - new DataSpec(adTagUri)); + getAdsDataSpec(adTagUri)); } } + private static DataSpec getAdsDataSpec(@Nullable Uri adTagUri) { + return new DataSpec(adTagUri != null ? adTagUri : Uri.EMPTY); + } + + private static long getContentPeriodPositionMs( + Player player, Timeline timeline, Timeline.Period period) { + long contentWindowPositionMs = player.getContentPosition(); + return contentWindowPositionMs + - timeline.getPeriod(/* periodIndex= */ 0, period).getPositionInWindowMs(); + } + + private int getAdGroupIndex(AdPodInfo adPodInfo) { + if (adPodInfo.getPodIndex() == -1) { + // This is a postroll ad. + return adPlaybackState.adGroupCount - 1; + } + + // adPodInfo.podIndex may be 0-based or 1-based, so for now look up the cue point instead. + long adGroupTimeUs = (long) (((float) adPodInfo.getTimeOffset()) * C.MICROS_PER_SECOND); + for (int adGroupIndex = 0; adGroupIndex < adPlaybackState.adGroupCount; adGroupIndex++) { + if (adPlaybackState.adGroupTimesUs[adGroupIndex] == adGroupTimeUs) { + return adGroupIndex; + } + } + throw new IllegalStateException("Failed to find cue point"); + } + private static long[] getAdGroupTimesUs(List cuePoints) { if (cuePoints.isEmpty()) { // If no cue points are specified, there is a preroll ad. @@ -1398,6 +1449,12 @@ public final class ImaAdsLoader || adError.getErrorCode() == AdErrorCode.UNKNOWN_ERROR; } + private static Looper getImaLooper() { + // IMA SDK callbacks occur on the main thread. This method can be used to check that the player + // is using the same looper, to ensure all interaction with this class is on the main thread. + return Looper.getMainLooper(); + } + private static boolean hasMidrollAdGroups(long[] adGroupTimesUs) { int count = adGroupTimesUs.length; if (count == 1) { @@ -1426,6 +1483,49 @@ public final class ImaAdsLoader Context context, ImaSdkSettings imaSdkSettings, AdDisplayContainer adDisplayContainer); } + private String getAdMediaInfoString(AdMediaInfo adMediaInfo) { + @Nullable AdInfo adInfo = adInfoByAdMediaInfo.get(adMediaInfo); + return "AdMediaInfo[" + adMediaInfo.getUrl() + (adInfo != null ? ", " + adInfo : "") + "]"; + } + + // TODO: Consider moving this into AdPlaybackState. + private static final class AdInfo { + public final int adGroupIndex; + public final int adIndexInAdGroup; + + public AdInfo(int adGroupIndex, int adIndexInAdGroup) { + this.adGroupIndex = adGroupIndex; + this.adIndexInAdGroup = adIndexInAdGroup; + } + + @Override + public boolean equals(@Nullable Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + AdInfo adInfo = (AdInfo) o; + if (adGroupIndex != adInfo.adGroupIndex) { + return false; + } + return adIndexInAdGroup == adInfo.adIndexInAdGroup; + } + + @Override + public int hashCode() { + int result = adGroupIndex; + result = 31 * result + adIndexInAdGroup; + return result; + } + + @Override + public String toString() { + return "(" + adGroupIndex + ", " + adIndexInAdGroup + ')'; + } + } + /** Default {@link ImaFactory} for non-test usage, which delegates to {@link ImaSdkFactory}. */ private static final class DefaultImaFactory implements ImaFactory { @Override 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 deleted file mode 100644 index 59dfc6473c..0000000000 --- a/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/FakeAd.java +++ /dev/null @@ -1,211 +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.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 int getVastMediaWidth() { - throw new UnsupportedOperationException(); - } - - @Override - public int getVastMediaHeight() { - throw new UnsupportedOperationException(); - } - - @Override - public int getVastMediaBitrate() { - 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 deleted file mode 100644 index a8f3daae33..0000000000 --- a/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/FakeAdsLoader.java +++ /dev/null @@ -1,100 +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.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 deleted file mode 100644 index 7c2c8a6e0b..0000000000 --- a/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/FakeAdsRequest.java +++ /dev/null @@ -1,132 +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.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 index a9572b7a8d..4c98233acb 100644 --- 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 @@ -30,7 +30,6 @@ import java.util.ArrayList; private final Timeline.Period period; private final Timeline timeline; - private boolean prepared; @Player.State private int state; private boolean playWhenReady; private long position; @@ -48,12 +47,10 @@ import java.util.ArrayList; } /** Sets the timeline on this fake player, which notifies listeners with the changed timeline. */ - public void updateTimeline(Timeline timeline) { + public void updateTimeline(Timeline timeline, @TimelineChangeReason int reason) { for (Player.EventListener listener : listeners) { - listener.onTimelineChanged( - timeline, prepared ? TIMELINE_CHANGE_REASON_DYNAMIC : TIMELINE_CHANGE_REASON_PREPARED); + listener.onTimelineChanged(timeline, reason); } - prepared = true; } /** @@ -95,13 +92,22 @@ import java.util.ArrayList; } /** Sets the {@link Player.State} of this player. */ + @SuppressWarnings("deprecation") public void setState(@Player.State int state, boolean playWhenReady) { - boolean notify = this.state != state || this.playWhenReady != playWhenReady; + boolean playWhenReadyChanged = this.playWhenReady != playWhenReady; + boolean playbackStateChanged = this.state != state; this.state = state; this.playWhenReady = playWhenReady; - if (notify) { + if (playbackStateChanged || playWhenReadyChanged) { for (Player.EventListener listener : listeners) { listener.onPlayerStateChanged(playWhenReady, state); + if (playbackStateChanged) { + listener.onPlaybackStateChanged(state); + } + if (playWhenReadyChanged) { + listener.onPlayWhenReadyChanged( + playWhenReady, PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST); + } } } } 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 index edaa4cde29..6405583bf1 100644 --- 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 @@ -16,7 +16,10 @@ package com.google.android.exoplayer2.ext.ima; import static com.google.common.truth.Truth.assertThat; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -32,51 +35,75 @@ 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.AdPodInfo; 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.ImaSdkSettings; +import com.google.ads.interactivemedia.v3.api.player.AdMediaInfo; 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.Timeline.Period; +import com.google.android.exoplayer2.ext.ima.ImaAdsLoader.ImaFactory; +import com.google.android.exoplayer2.source.MaskingMediaSource.DummyTimeline; 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.testutil.FakeTimeline; +import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition; import com.google.android.exoplayer2.upstream.DataSpec; import java.io.IOException; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.List; import java.util.Map; import org.junit.After; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; import org.mockito.InOrder; import org.mockito.Mock; -import org.mockito.MockitoAnnotations; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; +import org.mockito.stubbing.Answer; -/** Test for {@link ImaAdsLoader}. */ +/** Tests for {@link ImaAdsLoader}. */ @RunWith(AndroidJUnit4.class) -public class ImaAdsLoaderTest { +public final 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, /* isLive= */ false); + new FakeTimeline( + new TimelineWindowDefinition( + /* isSeekable= */ true, /* isDynamic= */ false, CONTENT_DURATION_US)); + private static final long CONTENT_PERIOD_DURATION_US = + CONTENT_TIMELINE.getPeriod(/* periodIndex= */ 0, new Period()).durationUs; private static final Uri TEST_URI = Uri.EMPTY; + private static final AdMediaInfo TEST_AD_MEDIA_INFO = new AdMediaInfo(TEST_URI.toString()); 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; + @Rule public final MockitoRule mockito = MockitoJUnit.rule(); + + @Mock private ImaSdkSettings mockImaSdkSettings; + @Mock private AdsRenderingSettings mockAdsRenderingSettings; + @Mock private AdDisplayContainer mockAdDisplayContainer; + @Mock private AdsManager mockAdsManager; + @Mock private AdsRequest mockAdsRequest; + @Mock private AdsManagerLoadedEvent mockAdsManagerLoadedEvent; + @Mock private com.google.ads.interactivemedia.v3.api.AdsLoader mockAdsLoader; + @Mock private ImaFactory mockImaFactory; + @Mock private AdPodInfo mockAdPodInfo; + @Mock private Ad mockPrerollSingleAd; + private ViewGroup adViewGroup; private View adOverlayView; private AdsLoader.AdViewProvider adViewProvider; @@ -86,16 +113,7 @@ public class ImaAdsLoaderTest { @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); + setupMocks(); adViewGroup = new FrameLayout(ApplicationProvider.getApplicationContext()); adOverlayView = new View(ApplicationProvider.getApplicationContext()); adViewProvider = @@ -120,43 +138,53 @@ public class ImaAdsLoaderTest { } @Test - public void testBuilder_overridesPlayerType() { - when(imaSdkSettings.getPlayerType()).thenReturn("test player type"); + public void builder_overridesPlayerType() { + when(mockImaSdkSettings.getPlayerType()).thenReturn("test player type"); setupPlayback(CONTENT_TIMELINE, PREROLL_ADS_DURATIONS_US, PREROLL_CUE_POINTS_SECONDS); - verify(imaSdkSettings).setPlayerType("google/exo.ext.ima"); + verify(mockImaSdkSettings).setPlayerType("google/exo.ext.ima"); } @Test - public void testStart_setsAdUiViewGroup() { + public void start_setsAdUiViewGroup() { setupPlayback(CONTENT_TIMELINE, PREROLL_ADS_DURATIONS_US, PREROLL_CUE_POINTS_SECONDS); imaAdsLoader.start(adsLoaderListener, adViewProvider); - verify(adDisplayContainer, atLeastOnce()).setAdContainer(adViewGroup); - verify(adDisplayContainer, atLeastOnce()).registerVideoControlsOverlay(adOverlayView); + verify(mockAdDisplayContainer, atLeastOnce()).setAdContainer(adViewGroup); + verify(mockAdDisplayContainer, atLeastOnce()).registerVideoControlsOverlay(adOverlayView); } @Test - public void testStart_updatesAdPlaybackState() { + public void start_withPlaceholderContent_initializedAdsLoader() { + Timeline placeholderTimeline = new DummyTimeline(/* tag= */ null); + setupPlayback(placeholderTimeline, PREROLL_ADS_DURATIONS_US, PREROLL_CUE_POINTS_SECONDS); + imaAdsLoader.start(adsLoaderListener, adViewProvider); + + // We'll only create the rendering settings when initializing the ads loader. + verify(mockImaFactory).createAdsRenderingSettings(); + } + + @Test + public void start_updatesAdPlaybackState() { setupPlayback(CONTENT_TIMELINE, PREROLL_ADS_DURATIONS_US, PREROLL_CUE_POINTS_SECONDS); imaAdsLoader.start(adsLoaderListener, adViewProvider); assertThat(adsLoaderListener.adPlaybackState) .isEqualTo( - new AdPlaybackState(/* adGroupTimesUs= */ 0) + new AdPlaybackState(/* adGroupTimesUs...= */ 0) .withAdDurationsUs(PREROLL_ADS_DURATIONS_US) - .withContentDurationUs(CONTENT_DURATION_US)); + .withContentDurationUs(CONTENT_PERIOD_DURATION_US)); } @Test - public void testStartAfterRelease() { + public void startAfterRelease() { setupPlayback(CONTENT_TIMELINE, PREROLL_ADS_DURATIONS_US, PREROLL_CUE_POINTS_SECONDS); imaAdsLoader.release(); imaAdsLoader.start(adsLoaderListener, adViewProvider); } @Test - public void testStartAndCallbacksAfterRelease() { + public void startAndCallbacksAfterRelease() { setupPlayback(CONTENT_TIMELINE, PREROLL_ADS_DURATIONS_US, PREROLL_CUE_POINTS_SECONDS); imaAdsLoader.release(); imaAdsLoader.start(adsLoaderListener, adViewProvider); @@ -168,13 +196,13 @@ public class ImaAdsLoaderTest { // when using Robolectric and accessing VideoProgressUpdate.VIDEO_TIME_NOT_READY, due to the IMA // SDK being proguarded. imaAdsLoader.requestAds(adViewGroup); - 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.onAdEvent(getAdEvent(AdEventType.LOADED, mockPrerollSingleAd)); + imaAdsLoader.loadAd(TEST_AD_MEDIA_INFO, mockAdPodInfo); + imaAdsLoader.onAdEvent(getAdEvent(AdEventType.CONTENT_PAUSE_REQUESTED, mockPrerollSingleAd)); + imaAdsLoader.playAd(TEST_AD_MEDIA_INFO); + imaAdsLoader.onAdEvent(getAdEvent(AdEventType.STARTED, mockPrerollSingleAd)); + imaAdsLoader.pauseAd(TEST_AD_MEDIA_INFO); + imaAdsLoader.stopAd(TEST_AD_MEDIA_INFO); imaAdsLoader.onPlayerError(ExoPlaybackException.createForSource(new IOException())); imaAdsLoader.onPositionDiscontinuity(Player.DISCONTINUITY_REASON_SEEK); imaAdsLoader.onAdEvent(getAdEvent(AdEventType.CONTENT_RESUME_REQUESTED, /* ad= */ null)); @@ -183,38 +211,38 @@ public class ImaAdsLoaderTest { } @Test - public void testPlayback_withPrerollAd_marksAdAsPlayed() { + public void playback_withPrerollAd_marksAdAsPlayed() { setupPlayback(CONTENT_TIMELINE, PREROLL_ADS_DURATIONS_US, PREROLL_CUE_POINTS_SECONDS); // Load the preroll ad. imaAdsLoader.start(adsLoaderListener, adViewProvider); - imaAdsLoader.onAdEvent(getAdEvent(AdEventType.LOADED, UNSKIPPABLE_AD)); - imaAdsLoader.loadAd(TEST_URI.toString()); - imaAdsLoader.onAdEvent(getAdEvent(AdEventType.CONTENT_PAUSE_REQUESTED, UNSKIPPABLE_AD)); + imaAdsLoader.onAdEvent(getAdEvent(AdEventType.LOADED, mockPrerollSingleAd)); + imaAdsLoader.loadAd(TEST_AD_MEDIA_INFO, mockAdPodInfo); + imaAdsLoader.onAdEvent(getAdEvent(AdEventType.CONTENT_PAUSE_REQUESTED, mockPrerollSingleAd)); // Play the preroll ad. - imaAdsLoader.playAd(); + imaAdsLoader.playAd(TEST_AD_MEDIA_INFO); 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)); + imaAdsLoader.onAdEvent(getAdEvent(AdEventType.STARTED, mockPrerollSingleAd)); + imaAdsLoader.onAdEvent(getAdEvent(AdEventType.FIRST_QUARTILE, mockPrerollSingleAd)); + imaAdsLoader.onAdEvent(getAdEvent(AdEventType.MIDPOINT, mockPrerollSingleAd)); + imaAdsLoader.onAdEvent(getAdEvent(AdEventType.THIRD_QUARTILE, mockPrerollSingleAd)); // Play the content. fakeExoPlayer.setPlayingContentPosition(0); - imaAdsLoader.stopAd(); + imaAdsLoader.stopAd(TEST_AD_MEDIA_INFO); 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) + new AdPlaybackState(/* adGroupTimesUs...= */ 0) + .withContentDurationUs(CONTENT_PERIOD_DURATION_US) .withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 1) .withAdUri(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0, /* uri= */ TEST_URI) .withAdDurationsUs(PREROLL_ADS_DURATIONS_US) @@ -223,29 +251,77 @@ public class ImaAdsLoaderTest { } @Test - public void testStop_unregistersAllVideoControlOverlays() { + public void stop_unregistersAllVideoControlOverlays() { setupPlayback(CONTENT_TIMELINE, PREROLL_ADS_DURATIONS_US, PREROLL_CUE_POINTS_SECONDS); imaAdsLoader.start(adsLoaderListener, adViewProvider); imaAdsLoader.requestAds(adViewGroup); imaAdsLoader.stop(); - InOrder inOrder = inOrder(adDisplayContainer); - inOrder.verify(adDisplayContainer).registerVideoControlsOverlay(adOverlayView); - inOrder.verify(adDisplayContainer).unregisterAllVideoControlsOverlays(); + InOrder inOrder = inOrder(mockAdDisplayContainer); + inOrder.verify(mockAdDisplayContainer).registerVideoControlsOverlay(adOverlayView); + inOrder.verify(mockAdDisplayContainer).unregisterAllVideoControlsOverlays(); } 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)); + when(mockAdsManager.getAdCuePoints()).thenReturn(Arrays.asList(cuePoints)); imaAdsLoader = new ImaAdsLoader.Builder(ApplicationProvider.getApplicationContext()) - .setImaFactory(testImaFactory) - .setImaSdkSettings(imaSdkSettings) + .setImaFactory(mockImaFactory) + .setImaSdkSettings(mockImaSdkSettings) .buildForAdTag(TEST_URI); imaAdsLoader.setPlayer(fakeExoPlayer); } + private void setupMocks() { + ArgumentCaptor userRequestContextCaptor = ArgumentCaptor.forClass(Object.class); + doNothing().when(mockAdsRequest).setUserRequestContext(userRequestContextCaptor.capture()); + when(mockAdsRequest.getUserRequestContext()) + .thenAnswer((Answer) invocation -> userRequestContextCaptor.getValue()); + List adsLoadedListeners = + new ArrayList<>(); + doAnswer( + invocation -> { + adsLoadedListeners.add(invocation.getArgument(0)); + return null; + }) + .when(mockAdsLoader) + .addAdsLoadedListener(any()); + doAnswer( + invocation -> { + adsLoadedListeners.remove(invocation.getArgument(0)); + return null; + }) + .when(mockAdsLoader) + .removeAdsLoadedListener(any()); + when(mockAdsManagerLoadedEvent.getAdsManager()).thenReturn(mockAdsManager); + when(mockAdsManagerLoadedEvent.getUserRequestContext()) + .thenAnswer(invocation -> mockAdsRequest.getUserRequestContext()); + doAnswer( + (Answer) + invocation -> { + for (com.google.ads.interactivemedia.v3.api.AdsLoader.AdsLoadedListener listener : + adsLoadedListeners) { + listener.onAdsManagerLoaded(mockAdsManagerLoadedEvent); + } + return null; + }) + .when(mockAdsLoader) + .requestAds(mockAdsRequest); + + when(mockImaFactory.createAdDisplayContainer()).thenReturn(mockAdDisplayContainer); + when(mockImaFactory.createAdsRenderingSettings()).thenReturn(mockAdsRenderingSettings); + when(mockImaFactory.createAdsRequest()).thenReturn(mockAdsRequest); + when(mockImaFactory.createAdsLoader(any(), any(), any())).thenReturn(mockAdsLoader); + + when(mockAdPodInfo.getPodIndex()).thenReturn(0); + when(mockAdPodInfo.getTotalAds()).thenReturn(1); + when(mockAdPodInfo.getAdPosition()).thenReturn(1); + + when(mockPrerollSingleAd.getAdPodInfo()).thenReturn(mockAdPodInfo); + } + private static AdEvent getAdEvent(AdEventType adEventType, @Nullable Ad ad) { return new AdEvent() { @Override @@ -286,7 +362,9 @@ public class ImaAdsLoaderTest { public void onAdPlaybackState(AdPlaybackState adPlaybackState) { adPlaybackState = adPlaybackState.withAdDurationsUs(adDurationsUs); this.adPlaybackState = adPlaybackState; - fakeExoPlayer.updateTimeline(new SinglePeriodAdTimeline(contentTimeline, adPlaybackState)); + fakeExoPlayer.updateTimeline( + new SinglePeriodAdTimeline(contentTimeline, adPlaybackState), + Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE); } @Override 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 deleted file mode 100644 index 4efd8cf38c..0000000000 --- a/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/SingletonImaFactory.java +++ /dev/null @@ -1,72 +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.ext.ima; - -import android.content.Context; -import com.google.ads.interactivemedia.v3.api.AdDisplayContainer; -import com.google.ads.interactivemedia.v3.api.AdsLoader; -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 AdsLoader createAdsLoader( - Context context, ImaSdkSettings imaSdkSettings, AdDisplayContainer adDisplayContainer) { - return adsLoader; - } -} diff --git a/extensions/jobdispatcher/build.gradle b/extensions/jobdispatcher/build.gradle index d7f19d2545..05ac82ba08 100644 --- a/extensions/jobdispatcher/build.gradle +++ b/extensions/jobdispatcher/build.gradle @@ -35,6 +35,7 @@ android { dependencies { implementation project(modulePrefix + 'library-core') implementation 'com.firebase:firebase-jobdispatcher:0.8.5' + compileOnly 'org.jetbrains.kotlin:kotlin-annotations-jvm:' + kotlinAnnotationsVersion } ext { 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 c8975275f1..8841f8355f 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 @@ -76,8 +76,8 @@ public final class JobDispatcherScheduler implements Scheduler { * {@link #schedule(Requirements, String, String)} or {@link #cancel()} are called. */ public JobDispatcherScheduler(Context context, String jobTag) { - this.jobDispatcher = - new FirebaseJobDispatcher(new GooglePlayDriver(context.getApplicationContext())); + context = context.getApplicationContext(); + this.jobDispatcher = new FirebaseJobDispatcher(new GooglePlayDriver(context)); this.jobTag = jobTag; } diff --git a/extensions/leanback/build.gradle b/extensions/leanback/build.gradle index f0be172c90..19b4cde3bf 100644 --- a/extensions/leanback/build.gradle +++ b/extensions/leanback/build.gradle @@ -34,6 +34,7 @@ dependencies { implementation project(modulePrefix + 'library-core') implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion implementation 'androidx.leanback:leanback:1.0.0' + compileOnly 'org.jetbrains.kotlin:kotlin-annotations-jvm:' + kotlinAnnotationsVersion } ext { 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 7c2285c57e..e385cd52e9 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 @@ -272,7 +272,7 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter implements Runnab // Player.EventListener implementation. @Override - public void onPlayerStateChanged(boolean playWhenReady, @Player.State int playbackState) { + public void onPlaybackStateChanged(@Player.State int playbackState) { notifyStateChanged(); } diff --git a/extensions/mediasession/build.gradle b/extensions/mediasession/build.gradle index 537c5ba534..f32ef263e0 100644 --- a/extensions/mediasession/build.gradle +++ b/extensions/mediasession/build.gradle @@ -34,6 +34,7 @@ dependencies { implementation project(modulePrefix + 'library-core') api 'androidx.media:media:' + androidxMediaVersion compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion + compileOnly 'org.jetbrains.kotlin:kotlin-annotations-jvm:' + kotlinAnnotationsVersion } ext { 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 5382e286a1..fc75d4f549 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 @@ -38,7 +38,6 @@ import com.google.android.exoplayer2.ControlDispatcher; import com.google.android.exoplayer2.DefaultControlDispatcher; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.ExoPlayerLibraryInfo; -import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.util.Assertions; @@ -127,21 +126,11 @@ public final class MediaSessionConnector { /** The default playback actions. */ @PlaybackActions public static final long DEFAULT_PLAYBACK_ACTIONS = ALL_PLAYBACK_ACTIONS; - /** The default fast forward increment, in milliseconds. */ - public static final int DEFAULT_FAST_FORWARD_MS = 15000; - /** The default rewind increment, in milliseconds. */ - public static final int DEFAULT_REWIND_MS = 5000; - /** * The name of the {@link PlaybackStateCompat} float extra with the value of {@link - * PlaybackParameters#speed}. + * Player#getPlaybackSpeed()}. */ public static final String EXTRAS_SPEED = "EXO_SPEED"; - /** - * The name of the {@link PlaybackStateCompat} float extra with the value of {@link - * PlaybackParameters#pitch}. - */ - public static final String EXTRAS_PITCH = "EXO_PITCH"; private static final long BASE_PLAYBACK_ACTIONS = PlaybackStateCompat.ACTION_PLAY_PAUSE @@ -178,8 +167,8 @@ public final class MediaSessionConnector { Player player, ControlDispatcher controlDispatcher, String command, - Bundle extras, - ResultReceiver cb); + @Nullable Bundle extras, + @Nullable ResultReceiver cb); } /** Interface to which playback preparation and play actions are delegated. */ @@ -218,25 +207,25 @@ public final class MediaSessionConnector { * * @param mediaId The media id of the media item to be prepared. * @param playWhenReady Whether playback should be started after preparation. - * @param extras A {@link Bundle} of extras passed by the media controller. + * @param extras A {@link Bundle} of extras passed by the media controller, may be null. */ - void onPrepareFromMediaId(String mediaId, boolean playWhenReady, Bundle extras); + void onPrepareFromMediaId(String mediaId, boolean playWhenReady, @Nullable Bundle extras); /** * See {@link MediaSessionCompat.Callback#onPrepareFromSearch(String, Bundle)}. * * @param query The search query. * @param playWhenReady Whether playback should be started after preparation. - * @param extras A {@link Bundle} of extras passed by the media controller. + * @param extras A {@link Bundle} of extras passed by the media controller, may be null. */ - void onPrepareFromSearch(String query, boolean playWhenReady, Bundle extras); + void onPrepareFromSearch(String query, boolean playWhenReady, @Nullable Bundle extras); /** * See {@link MediaSessionCompat.Callback#onPrepareFromUri(Uri, Bundle)}. * * @param uri The {@link Uri} of the media item to be prepared. * @param playWhenReady Whether playback should be started after preparation. - * @param extras A {@link Bundle} of extras passed by the media controller. + * @param extras A {@link Bundle} of extras passed by the media controller, may be null. */ - void onPrepareFromUri(Uri uri, boolean playWhenReady, Bundle extras); + void onPrepareFromUri(Uri uri, boolean playWhenReady, @Nullable Bundle extras); } /** @@ -336,7 +325,7 @@ public final class MediaSessionConnector { void onSetRating(Player player, RatingCompat rating); /** See {@link MediaSessionCompat.Callback#onSetRating(RatingCompat, Bundle)}. */ - void onSetRating(Player player, RatingCompat rating, Bundle extras); + void onSetRating(Player player, RatingCompat rating, @Nullable Bundle extras); } /** Handles requests for enabling or disabling captions. */ @@ -381,7 +370,7 @@ public final class MediaSessionConnector { * @param controlDispatcher A {@link ControlDispatcher} that should be used for dispatching * changes to the player. * @param action The name of the action which was sent by a media controller. - * @param extras Optional extras sent by a media controller. + * @param extras Optional extras sent by a media controller, may be null. */ void onCustomAction( Player player, ControlDispatcher controlDispatcher, String action, @Nullable Bundle extras); @@ -394,6 +383,7 @@ public final class MediaSessionConnector { * @param player The player connected to the media session. * @return The custom action to be included in the session playback state or {@code null}. */ + @Nullable PlaybackStateCompat.CustomAction getCustomAction(Player player); } @@ -439,8 +429,6 @@ public final class MediaSessionConnector { @Nullable private MediaButtonEventHandler mediaButtonEventHandler; private long enabledPlaybackActions; - private int rewindMs; - private int fastForwardMs; /** * Creates an instance. @@ -460,8 +448,6 @@ public final class MediaSessionConnector { new DefaultMediaMetadataProvider( mediaSession.getController(), /* metadataExtrasPrefix= */ null); enabledPlaybackActions = DEFAULT_PLAYBACK_ACTIONS; - rewindMs = DEFAULT_REWIND_MS; - fastForwardMs = DEFAULT_FAST_FORWARD_MS; mediaSession.setFlags(BASE_MEDIA_SESSION_FLAGS); mediaSession.setCallback(componentListener, new Handler(looper)); } @@ -503,13 +489,12 @@ public final class MediaSessionConnector { /** * Sets the {@link ControlDispatcher}. * - * @param controlDispatcher The {@link ControlDispatcher}, or null to use {@link - * DefaultControlDispatcher}. + * @param controlDispatcher The {@link ControlDispatcher}. */ - public void setControlDispatcher(@Nullable ControlDispatcher controlDispatcher) { + public void setControlDispatcher(ControlDispatcher controlDispatcher) { if (this.controlDispatcher != controlDispatcher) { - this.controlDispatcher = - controlDispatcher == null ? new DefaultControlDispatcher() : controlDispatcher; + this.controlDispatcher = controlDispatcher; + invalidateMediaSessionPlaybackState(); } } @@ -550,27 +535,27 @@ public final class MediaSessionConnector { } /** - * Sets the rewind increment in milliseconds. - * - * @param rewindMs The rewind increment in milliseconds. A non-positive value will cause the - * rewind button to be disabled. + * @deprecated Use {@link #setControlDispatcher(ControlDispatcher)} with {@link + * DefaultControlDispatcher#DefaultControlDispatcher(long, long)} instead. */ + @SuppressWarnings("deprecation") + @Deprecated public void setRewindIncrementMs(int rewindMs) { - if (this.rewindMs != rewindMs) { - this.rewindMs = rewindMs; + if (controlDispatcher instanceof DefaultControlDispatcher) { + ((DefaultControlDispatcher) controlDispatcher).setRewindIncrementMs(rewindMs); invalidateMediaSessionPlaybackState(); } } /** - * Sets the fast forward increment in milliseconds. - * - * @param fastForwardMs The fast forward increment in milliseconds. A non-positive value will - * cause the fast forward button to be disabled. + * @deprecated Use {@link #setControlDispatcher(ControlDispatcher)} with {@link + * DefaultControlDispatcher#DefaultControlDispatcher(long, long)} instead. */ + @SuppressWarnings("deprecation") + @Deprecated public void setFastForwardIncrementMs(int fastForwardMs) { - if (this.fastForwardMs != fastForwardMs) { - this.fastForwardMs = fastForwardMs; + if (controlDispatcher instanceof DefaultControlDispatcher) { + ((DefaultControlDispatcher) controlDispatcher).setFastForwardIncrementMs(fastForwardMs); invalidateMediaSessionPlaybackState(); } } @@ -688,8 +673,6 @@ public final class MediaSessionConnector { * @param customActionProviders The custom action providers, or null to remove all existing custom * action providers. */ - // incompatible types in assignment. - @SuppressWarnings("nullness:assignment.type.incompatible") public void setCustomActionProviders(@Nullable CustomActionProvider... customActionProviders) { this.customActionProviders = customActionProviders == null ? new CustomActionProvider[0] : customActionProviders; @@ -763,7 +746,7 @@ public final class MediaSessionConnector { customActionMap = Collections.unmodifiableMap(currentActions); Bundle extras = new Bundle(); - @Nullable ExoPlaybackException playbackError = player.getPlaybackError(); + @Nullable ExoPlaybackException playbackError = player.getPlayerError(); boolean reportError = playbackError != null || customError != null; int sessionPlaybackState = reportError @@ -782,10 +765,9 @@ public final class MediaSessionConnector { queueNavigator != null ? queueNavigator.getActiveQueueItemId(player) : MediaSessionCompat.QueueItem.UNKNOWN_ID; - PlaybackParameters playbackParameters = player.getPlaybackParameters(); - extras.putFloat(EXTRAS_SPEED, playbackParameters.speed); - extras.putFloat(EXTRAS_PITCH, playbackParameters.pitch); - float sessionPlaybackSpeed = player.isPlaying() ? playbackParameters.speed : 0f; + float playbackSpeed = player.getPlaybackSpeed(); + extras.putFloat(EXTRAS_SPEED, playbackSpeed); + float sessionPlaybackSpeed = player.isPlaying() ? playbackSpeed : 0f; builder .setActions(buildPrepareActions() | buildPlaybackActions(player)) .setActiveQueueItemId(activeQueueItemId) @@ -876,8 +858,8 @@ public final class MediaSessionConnector { Timeline timeline = player.getCurrentTimeline(); if (!timeline.isEmpty() && !player.isPlayingAd()) { enableSeeking = player.isCurrentWindowSeekable(); - enableRewind = enableSeeking && rewindMs > 0; - enableFastForward = enableSeeking && fastForwardMs > 0; + enableRewind = enableSeeking && controlDispatcher.isRewindEnabled(); + enableFastForward = enableSeeking && controlDispatcher.isFastForwardEnabled(); enableSetRating = ratingCallback != null; enableSetCaptioningEnabled = captionCallback != null && captionCallback.hasCaptions(player); } @@ -956,28 +938,6 @@ public final class MediaSessionConnector { return player != null && mediaButtonEventHandler != null; } - private void rewind(Player player) { - if (player.isCurrentWindowSeekable() && rewindMs > 0) { - seekToOffset(player, /* offsetMs= */ -rewindMs); - } - } - - private void fastForward(Player player) { - if (player.isCurrentWindowSeekable() && fastForwardMs > 0) { - seekToOffset(player, /* offsetMs= */ fastForwardMs); - } - } - - private void seekToOffset(Player player, long offsetMs) { - long positionMs = player.getCurrentPosition() + offsetMs; - long durationMs = player.getDuration(); - if (durationMs != C.TIME_UNSET) { - positionMs = Math.min(positionMs, durationMs); - } - positionMs = Math.max(positionMs, 0); - seekTo(player, player.getCurrentWindowIndex(), positionMs); - } - private void seekTo(Player player, int windowIndex, long positionMs) { controlDispatcher.dispatchSeekTo(player, windowIndex, positionMs); } @@ -1128,7 +1088,13 @@ public final class MediaSessionConnector { } @Override - public void onPlayerStateChanged(boolean playWhenReady, @Player.State int playbackState) { + public void onPlaybackStateChanged(@Player.State int playbackState) { + invalidateMediaSessionPlaybackState(); + } + + @Override + public void onPlayWhenReadyChanged( + boolean playWhenReady, @Player.PlayWhenReadyChangeReason int reason) { invalidateMediaSessionPlaybackState(); } @@ -1166,7 +1132,7 @@ public final class MediaSessionConnector { } @Override - public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) { + public void onPlaybackSpeedChanged(float playbackSpeed) { invalidateMediaSessionPlaybackState(); } @@ -1204,14 +1170,14 @@ public final class MediaSessionConnector { @Override public void onFastForward() { if (canDispatchPlaybackAction(PlaybackStateCompat.ACTION_FAST_FORWARD)) { - fastForward(player); + controlDispatcher.dispatchFastForward(player); } } @Override public void onRewind() { if (canDispatchPlaybackAction(PlaybackStateCompat.ACTION_REWIND)) { - rewind(player); + controlDispatcher.dispatchRewind(player); } } @@ -1293,7 +1259,7 @@ public final class MediaSessionConnector { } @Override - public void onCommand(String command, Bundle extras, ResultReceiver cb) { + public void onCommand(String command, @Nullable Bundle extras, @Nullable ResultReceiver cb) { if (player != null) { for (int i = 0; i < commandReceivers.size(); i++) { if (commandReceivers.get(i).onCommand(player, controlDispatcher, command, extras, cb)) { @@ -1318,42 +1284,42 @@ public final class MediaSessionConnector { } @Override - public void onPrepareFromMediaId(String mediaId, Bundle extras) { + public void onPrepareFromMediaId(String mediaId, @Nullable Bundle extras) { if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PREPARE_FROM_MEDIA_ID)) { playbackPreparer.onPrepareFromMediaId(mediaId, /* playWhenReady= */ false, extras); } } @Override - public void onPrepareFromSearch(String query, Bundle extras) { + public void onPrepareFromSearch(String query, @Nullable Bundle extras) { if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PREPARE_FROM_SEARCH)) { playbackPreparer.onPrepareFromSearch(query, /* playWhenReady= */ false, extras); } } @Override - public void onPrepareFromUri(Uri uri, Bundle extras) { + public void onPrepareFromUri(Uri uri, @Nullable Bundle extras) { if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PREPARE_FROM_URI)) { playbackPreparer.onPrepareFromUri(uri, /* playWhenReady= */ false, extras); } } @Override - public void onPlayFromMediaId(String mediaId, Bundle extras) { + public void onPlayFromMediaId(String mediaId, @Nullable Bundle extras) { if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PLAY_FROM_MEDIA_ID)) { playbackPreparer.onPrepareFromMediaId(mediaId, /* playWhenReady= */ true, extras); } } @Override - public void onPlayFromSearch(String query, Bundle extras) { + public void onPlayFromSearch(String query, @Nullable Bundle extras) { if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PLAY_FROM_SEARCH)) { playbackPreparer.onPrepareFromSearch(query, /* playWhenReady= */ true, extras); } } @Override - public void onPlayFromUri(Uri uri, Bundle extras) { + public void onPlayFromUri(Uri uri, @Nullable Bundle extras) { if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PLAY_FROM_URI)) { playbackPreparer.onPrepareFromUri(uri, /* playWhenReady= */ true, extras); } @@ -1367,7 +1333,7 @@ public final class MediaSessionConnector { } @Override - public void onSetRating(RatingCompat rating, Bundle extras) { + public void onSetRating(RatingCompat rating, @Nullable Bundle extras) { if (canDispatchSetRating()) { ratingCallback.onSetRating(player, rating, extras); } 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 41bda3bf44..7f60d5e715 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 @@ -191,9 +191,9 @@ public final class TimelineQueueEditor Player player, ControlDispatcher controlDispatcher, String command, - Bundle extras, - ResultReceiver cb) { - if (!COMMAND_MOVE_QUEUE_ITEM.equals(command)) { + @Nullable Bundle extras, + @Nullable ResultReceiver cb) { + if (!COMMAND_MOVE_QUEUE_ITEM.equals(command) || extras == null) { return false; } int from = extras.getInt(EXTRA_FROM_INDEX, C.INDEX_UNSET); 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 fc4cc11b58..024faea209 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 @@ -36,7 +36,6 @@ import java.util.Collections; */ public abstract class TimelineQueueNavigator implements MediaSessionConnector.QueueNavigator { - public static final long MAX_POSITION_FOR_SEEK_TO_PREVIOUS = 3000; public static final int DEFAULT_MAX_QUEUE_SIZE = 10; private final MediaSessionCompat mediaSession; @@ -136,20 +135,7 @@ public abstract class TimelineQueueNavigator implements MediaSessionConnector.Qu @Override public void onSkipToPrevious(Player player, ControlDispatcher controlDispatcher) { - Timeline timeline = player.getCurrentTimeline(); - if (timeline.isEmpty() || player.isPlayingAd()) { - return; - } - int windowIndex = player.getCurrentWindowIndex(); - timeline.getWindow(windowIndex, window); - int previousWindowIndex = player.getPreviousWindowIndex(); - if (previousWindowIndex != C.INDEX_UNSET - && (player.getCurrentPosition() <= MAX_POSITION_FOR_SEEK_TO_PREVIOUS - || (window.isDynamic && !window.isSeekable))) { - controlDispatcher.dispatchSeekTo(player, previousWindowIndex, C.TIME_UNSET); - } else { - controlDispatcher.dispatchSeekTo(player, windowIndex, 0); - } + controlDispatcher.dispatchPrevious(player); } @Override @@ -166,17 +152,7 @@ public abstract class TimelineQueueNavigator implements MediaSessionConnector.Qu @Override public void onSkipToNext(Player player, ControlDispatcher controlDispatcher) { - Timeline timeline = player.getCurrentTimeline(); - if (timeline.isEmpty() || player.isPlayingAd()) { - return; - } - int windowIndex = player.getCurrentWindowIndex(); - int nextWindowIndex = player.getNextWindowIndex(); - if (nextWindowIndex != C.INDEX_UNSET) { - controlDispatcher.dispatchSeekTo(player, nextWindowIndex, C.TIME_UNSET); - } else if (timeline.getWindow(windowIndex, window).isDynamic) { - controlDispatcher.dispatchSeekTo(player, windowIndex, C.TIME_UNSET); - } + controlDispatcher.dispatchNext(player); } // CommandReceiver implementation. @@ -186,8 +162,8 @@ public abstract class TimelineQueueNavigator implements MediaSessionConnector.Qu Player player, ControlDispatcher controlDispatcher, String command, - Bundle extras, - ResultReceiver cb) { + @Nullable Bundle extras, + @Nullable ResultReceiver cb) { return false; } diff --git a/extensions/okhttp/build.gradle b/extensions/okhttp/build.gradle index 3af38397a8..b03abac670 100644 --- a/extensions/okhttp/build.gradle +++ b/extensions/okhttp/build.gradle @@ -35,13 +35,14 @@ dependencies { implementation project(modulePrefix + 'library-core') implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion + compileOnly 'org.jetbrains.kotlin:kotlin-annotations-jvm:' + kotlinAnnotationsVersion testImplementation project(modulePrefix + 'testutils') testImplementation 'org.robolectric:robolectric:' + robolectricVersion // Do not update to 3.13.X or later until minSdkVersion is increased to 21: // https://cashapp.github.io/2019-02-05/okhttp-3-13-requires-android-5 - // Since OkHttp is distributed as a jar rather than an aar, Gradle wont stop - // us from making this mistake! - api 'com.squareup.okhttp3:okhttp:3.12.5' + // Since OkHttp is distributed as a jar rather than an aar, Gradle won't + // stop us from making this mistake! + api 'com.squareup.okhttp3:okhttp:3.12.8' } 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 3053961f49..fe2bdd672b 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 @@ -223,7 +223,7 @@ public class OkHttpDataSource extends BaseDataSource implements HttpDataSource { responseByteStream = responseBody.byteStream(); } catch (IOException e) { throw new HttpDataSourceException( - "Unable to connect to " + dataSpec.uri, e, dataSpec, HttpDataSourceException.TYPE_OPEN); + "Unable to connect", e, dataSpec, HttpDataSourceException.TYPE_OPEN); } int responseCode = response.code(); diff --git a/extensions/okhttp/src/test/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSourceTest.java b/extensions/okhttp/src/test/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSourceTest.java index dab62b06e8..393c048eec 100644 --- a/extensions/okhttp/src/test/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSourceTest.java +++ b/extensions/okhttp/src/test/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSourceTest.java @@ -18,7 +18,6 @@ package com.google.android.exoplayer2.ext.okhttp; import static com.google.common.truth.Truth.assertThat; -import android.net.Uri; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.upstream.HttpDataSource; @@ -86,16 +85,12 @@ public class OkHttpDataSourceTest { dataSpecRequestProperties.put("5", dataSpecValue); DataSpec dataSpec = - new DataSpec( - /* uri= */ Uri.parse("http://www.google.com"), - /* httpMethod= */ 1, - /* httpBody= */ null, - /* absoluteStreamPosition= */ 1000, - /* position= */ 1000, - /* length= */ 5000, - /* key= */ null, - /* flags= */ 0, - dataSpecRequestProperties); + new DataSpec.Builder() + .setUri("http://www.google.com") + .setPosition(1000) + .setLength(5000) + .setHttpRequestHeaders(dataSpecRequestProperties) + .build(); Mockito.doAnswer( invocation -> { diff --git a/extensions/opus/README.md b/extensions/opus/README.md index 05448f2073..d3691b07bd 100644 --- a/extensions/opus/README.md +++ b/extensions/opus/README.md @@ -101,6 +101,14 @@ a custom track selector the choice of `Renderer` is up to your implementation, so you need to make sure you are passing an `LibopusAudioRenderer` to the player, then implement your own logic to use the renderer for a given track. +## Using the extension in the demo application ## + +To try out playback using the extension in the [demo application][], see +[enabling extension decoders][]. + +[demo application]: https://exoplayer.dev/demo-application.html +[enabling extension decoders]: https://exoplayer.dev/demo-application.html#enabling-extension-decoders + ## Links ## * [Javadoc][]: Classes matching `com.google.android.exoplayer2.ext.opus.*` diff --git a/extensions/opus/build.gradle b/extensions/opus/build.gradle index 28cf8f138f..545b5a7af8 100644 --- a/extensions/opus/build.gradle +++ b/extensions/opus/build.gradle @@ -29,9 +29,12 @@ android { testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner' } - sourceSets.main { - jniLibs.srcDir 'src/main/libs' - jni.srcDirs = [] // Disable the automatic ndk-build call by Android Studio. + sourceSets { + main { + jniLibs.srcDir 'src/main/libs' + jni.srcDirs = [] // Disable the automatic ndk-build call by Android Studio. + } + androidTest.assets.srcDir '../../testdata/src/test/assets/' } testOptions.unitTests.includeAndroidResources = true @@ -40,6 +43,7 @@ android { dependencies { implementation project(modulePrefix + 'library-core') implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion + compileOnly 'org.jetbrains.kotlin:kotlin-annotations-jvm:' + kotlinAnnotationsVersion testImplementation project(modulePrefix + 'testutils') testImplementation 'org.robolectric:robolectric:' + robolectricVersion androidTestImplementation 'androidx.test:runner:' + androidxTestRunnerVersion diff --git a/extensions/opus/src/androidTest/AndroidManifest.xml b/extensions/opus/src/androidTest/AndroidManifest.xml index 7f75cbccea..53ee20a584 100644 --- a/extensions/opus/src/androidTest/AndroidManifest.xml +++ b/extensions/opus/src/androidTest/AndroidManifest.xml @@ -23,9 +23,7 @@ - - + tools:ignore="MissingApplicationIcon,HardcodedDebugMode"/> drmSessionManager, - boolean playClearSamplesWithoutKeys, - AudioProcessor... audioProcessors) { - super(eventHandler, eventListener, null, drmSessionManager, playClearSamplesWithoutKeys, - audioProcessors); + @Override + public String getName() { + return TAG; } @Override - protected int supportsFormatInternal( - @Nullable DrmSessionManager drmSessionManager, Format format) { + @FormatSupport + protected int supportsFormatInternal(Format format) { boolean drmIsSupported = format.drmInitData == null - || OpusLibrary.matchesExpectedExoMediaCryptoType(format.exoMediaCryptoType) - || (format.exoMediaCryptoType == null - && supportsFormatDrm(drmSessionManager, format.drmInitData)); + || OpusLibrary.matchesExpectedExoMediaCryptoType(format.exoMediaCryptoType); if (!OpusLibrary.isAvailable() || !MimeTypes.AUDIO_OPUS.equalsIgnoreCase(format.sampleMimeType)) { return FORMAT_UNSUPPORTED_TYPE; @@ -105,6 +81,7 @@ public class LibopusAudioRenderer extends SimpleDecoderAudioRenderer { @Override protected OpusDecoder createDecoder(Format format, @Nullable ExoMediaCrypto mediaCrypto) throws OpusDecoderException { + TraceUtil.beginSection("createOpusDecoder"); int initialInputBufferSize = format.maxInputSize != Format.NO_VALUE ? format.maxInputSize : DEFAULT_INPUT_BUFFER_SIZE; OpusDecoder decoder = @@ -116,23 +93,17 @@ public class LibopusAudioRenderer extends SimpleDecoderAudioRenderer { mediaCrypto); channelCount = decoder.getChannelCount(); sampleRate = decoder.getSampleRate(); + TraceUtil.endSection(); return decoder; } @Override protected Format getOutputFormat() { - return Format.createAudioSampleFormat( - /* id= */ null, - MimeTypes.AUDIO_RAW, - /* codecs= */ null, - Format.NO_VALUE, - Format.NO_VALUE, - channelCount, - sampleRate, - C.ENCODING_PCM_16BIT, - /* initializationData= */ null, - /* drmInitData= */ null, - /* selectionFlags= */ 0, - /* language= */ null); + return new Format.Builder() + .setSampleMimeType(MimeTypes.AUDIO_RAW) + .setChannelCount(channelCount) + .setSampleRate(sampleRate) + .setPcmEncoding(C.ENCODING_PCM_16BIT) + .build(); } } diff --git a/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/OpusDecoder.java b/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/OpusDecoder.java index f0e993e3b9..8795950671 100644 --- a/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/OpusDecoder.java +++ b/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/OpusDecoder.java @@ -90,8 +90,8 @@ import java.util.List; if (channelCount > 8) { throw new OpusDecoderException("Invalid channel count: " + channelCount); } - int preskip = readLittleEndian16(headerBytes, 10); - int gain = readLittleEndian16(headerBytes, 16); + int preskip = readUnsignedLittleEndian16(headerBytes, 10); + int gain = readSignedLittleEndian16(headerBytes, 16); byte[] streamMap = new byte[8]; int numStreams; @@ -148,7 +148,7 @@ import java.util.List; @Override protected SimpleOutputBuffer createOutputBuffer() { - return new SimpleOutputBuffer(this); + return new SimpleOutputBuffer(this::releaseOutputBuffer); } @Override @@ -228,12 +228,16 @@ import java.util.List; return (int) (ns * SAMPLE_RATE / 1000000000); } - private static int readLittleEndian16(byte[] input, int offset) { + private static int readUnsignedLittleEndian16(byte[] input, int offset) { int value = input[offset] & 0xFF; value |= (input[offset + 1] & 0xFF) << 8; return value; } + private static int readSignedLittleEndian16(byte[] input, int offset) { + return (short) readUnsignedLittleEndian16(input, offset); + } + private native long opusInit(int sampleRate, int channelCount, int numStreams, int numCoupled, int gain, byte[] streamMap); private native int opusDecode(long decoder, long timeUs, ByteBuffer inputBuffer, int inputSize, diff --git a/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/OpusDecoderException.java b/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/OpusDecoderException.java index 6645086838..8d9cfea763 100644 --- a/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/OpusDecoderException.java +++ b/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/OpusDecoderException.java @@ -15,12 +15,10 @@ */ package com.google.android.exoplayer2.ext.opus; -import com.google.android.exoplayer2.audio.AudioDecoderException; +import com.google.android.exoplayer2.decoder.DecoderException; -/** - * Thrown when an Opus decoder error occurs. - */ -public final class OpusDecoderException extends AudioDecoderException { +/** Thrown when an Opus decoder error occurs. */ +public final class OpusDecoderException extends DecoderException { /* package */ OpusDecoderException(String message) { super(message); diff --git a/extensions/rtmp/build.gradle b/extensions/rtmp/build.gradle index 88d3524d72..621f8b2998 100644 --- a/extensions/rtmp/build.gradle +++ b/extensions/rtmp/build.gradle @@ -34,6 +34,7 @@ dependencies { implementation project(modulePrefix + 'library-core') implementation 'net.butterflytv.utils:rtmp-client:3.1.0' implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion + compileOnly 'org.jetbrains.kotlin:kotlin-annotations-jvm:' + kotlinAnnotationsVersion testImplementation project(modulePrefix + 'testutils') testImplementation 'org.robolectric:robolectric:' + robolectricVersion } diff --git a/extensions/vp9/README.md b/extensions/vp9/README.md index 71241d9a4f..fd0836648a 100644 --- a/extensions/vp9/README.md +++ b/extensions/vp9/README.md @@ -114,6 +114,14 @@ a custom track selector the choice of `Renderer` is up to your implementation, so you need to make sure you are passing an `LibvpxVideoRenderer` to the player, then implement your own logic to use the renderer for a given track. +## Using the extension in the demo application ## + +To try out playback using the extension in the [demo application][], see +[enabling extension decoders][]. + +[demo application]: https://exoplayer.dev/demo-application.html +[enabling extension decoders]: https://exoplayer.dev/demo-application.html#enabling-extension-decoders + ## Rendering options ## There are two possibilities for rendering the output `LibvpxVideoRenderer` diff --git a/extensions/vp9/build.gradle b/extensions/vp9/build.gradle index 80239beb22..ffd76d6e2f 100644 --- a/extensions/vp9/build.gradle +++ b/extensions/vp9/build.gradle @@ -29,9 +29,12 @@ android { testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner' } - sourceSets.main { - jniLibs.srcDir 'src/main/libs' - jni.srcDirs = [] // Disable the automatic ndk-build call by Android Studio. + sourceSets { + main { + jniLibs.srcDir 'src/main/libs' + jni.srcDirs = [] // Disable the automatic ndk-build call by Android Studio. + } + androidTest.assets.srcDir '../../testdata/src/test/assets/' } testOptions.unitTests.includeAndroidResources = true @@ -40,6 +43,7 @@ android { dependencies { implementation project(modulePrefix + 'library-core') implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion + compileOnly 'org.jetbrains.kotlin:kotlin-annotations-jvm:' + kotlinAnnotationsVersion testImplementation project(modulePrefix + 'testutils') testImplementation 'org.robolectric:robolectric:' + robolectricVersion androidTestImplementation 'androidx.test:runner:' + androidxTestRunnerVersion diff --git a/extensions/vp9/src/androidTest/AndroidManifest.xml b/extensions/vp9/src/androidTest/AndroidManifest.xml index 4d0832d198..b0d1a88c19 100644 --- a/extensions/vp9/src/androidTest/AndroidManifest.xml +++ b/extensions/vp9/src/androidTest/AndroidManifest.xml @@ -23,9 +23,7 @@ - - + tools:ignore="MissingApplicationIcon,HardcodedDebugMode"/> This renderer accepts the following messages sent via {@link ExoPlayer#createMessage(Target)} - * on the playback thread: - * - *
    - *
  • Message with type {@link C#MSG_SET_SURFACE} to set the output surface. The message payload - * should be the target {@link Surface}, or null. - *
  • Message with type {@link C#MSG_SET_VIDEO_DECODER_OUTPUT_BUFFER_RENDERER} to set the output - * buffer renderer. The message payload should be the target {@link - * VideoDecoderOutputBufferRenderer}, or null. - *
- */ -public class LibvpxVideoRenderer extends SimpleDecoderVideoRenderer { +/** Decodes and renders video using the native VP9 decoder. */ +public class LibvpxVideoRenderer extends DecoderVideoRenderer { + + private static final String TAG = "LibvpxVideoRenderer"; /** The number of input buffers. */ private final int numInputBuffers; @@ -71,9 +51,10 @@ public class LibvpxVideoRenderer extends SimpleDecoderVideoRenderer { private final int threads; @Nullable private VpxDecoder decoder; - @Nullable private VideoFrameMetadataListener frameMetadataListener; /** + * Creates a new instance. + * * @param allowedJoiningTimeMs The maximum duration in milliseconds for which this video renderer * can attempt to seamlessly join an ongoing playback. */ @@ -82,6 +63,8 @@ public class LibvpxVideoRenderer extends SimpleDecoderVideoRenderer { } /** + * Creates a new instance. + * * @param allowedJoiningTimeMs The maximum duration in milliseconds for which this video renderer * can attempt to seamlessly join an ongoing playback. * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be @@ -100,51 +83,14 @@ public class LibvpxVideoRenderer extends SimpleDecoderVideoRenderer { eventHandler, eventListener, maxDroppedFramesToNotify, - /* drmSessionManager= */ null, - /* playClearSamplesWithoutKeys= */ false); - } - - /** - * @param allowedJoiningTimeMs The maximum duration in milliseconds for which this video renderer - * can attempt to seamlessly join an ongoing playback. - * @param eventHandler A handler to use when delivering events to {@code eventListener}. 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 maxDroppedFramesToNotify The maximum number of frames that can be dropped between - * invocations of {@link VideoRendererEventListener#onDroppedFrames(int, long)}. - * @param drmSessionManager For use with encrypted media. May be null if support for encrypted - * media is not required. - * @param playClearSamplesWithoutKeys Encrypted media may contain clear (un-encrypted) regions. - * For example a media file may start with a short clear region so as to allow playback to - * 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. - * @deprecated Use {@link #LibvpxVideoRenderer(long, Handler, VideoRendererEventListener, int, - * int, int, int)}} instead, and pass DRM-related parameters to the {@link MediaSource} - * factories. - */ - @Deprecated - @SuppressWarnings("deprecation") - public LibvpxVideoRenderer( - long allowedJoiningTimeMs, - @Nullable Handler eventHandler, - @Nullable VideoRendererEventListener eventListener, - int maxDroppedFramesToNotify, - @Nullable DrmSessionManager drmSessionManager, - boolean playClearSamplesWithoutKeys) { - this( - allowedJoiningTimeMs, - eventHandler, - eventListener, - maxDroppedFramesToNotify, - drmSessionManager, - playClearSamplesWithoutKeys, getRuntime().availableProcessors(), /* numInputBuffers= */ 4, /* numOutputBuffers= */ 4); } /** + * Creates a new instance. + * * @param allowedJoiningTimeMs The maximum duration in milliseconds for which this video renderer * can attempt to seamlessly join an ongoing playback. * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be @@ -165,87 +111,35 @@ public class LibvpxVideoRenderer extends SimpleDecoderVideoRenderer { int threads, int numInputBuffers, int numOutputBuffers) { - this( - allowedJoiningTimeMs, - eventHandler, - eventListener, - maxDroppedFramesToNotify, - /* drmSessionManager= */ null, - /* playClearSamplesWithoutKeys= */ false, - threads, - numInputBuffers, - numOutputBuffers); - } - - /** - * @param allowedJoiningTimeMs The maximum duration in milliseconds for which this video renderer - * can attempt to seamlessly join an ongoing playback. - * @param eventHandler A handler to use when delivering events to {@code eventListener}. 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 maxDroppedFramesToNotify The maximum number of frames that can be dropped between - * invocations of {@link VideoRendererEventListener#onDroppedFrames(int, long)}. - * @param drmSessionManager For use with encrypted media. May be null if support for encrypted - * media is not required. - * @param playClearSamplesWithoutKeys Encrypted media may contain clear (un-encrypted) regions. - * For example a media file may start with a short clear region so as to allow playback to - * 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 threads Number of threads libvpx will use to decode. - * @param numInputBuffers Number of input buffers. - * @param numOutputBuffers Number of output buffers. - * @deprecated Use {@link #LibvpxVideoRenderer(long, Handler, VideoRendererEventListener, int, - * int, int, int)}} instead, and pass DRM-related parameters to the {@link MediaSource} - * factories. - */ - @Deprecated - public LibvpxVideoRenderer( - long allowedJoiningTimeMs, - @Nullable Handler eventHandler, - @Nullable VideoRendererEventListener eventListener, - int maxDroppedFramesToNotify, - @Nullable DrmSessionManager drmSessionManager, - boolean playClearSamplesWithoutKeys, - int threads, - int numInputBuffers, - int numOutputBuffers) { - super( - allowedJoiningTimeMs, - eventHandler, - eventListener, - maxDroppedFramesToNotify, - drmSessionManager, - playClearSamplesWithoutKeys); + super(allowedJoiningTimeMs, eventHandler, eventListener, maxDroppedFramesToNotify); this.threads = threads; this.numInputBuffers = numInputBuffers; this.numOutputBuffers = numOutputBuffers; } @Override - protected int supportsFormatInternal( - @Nullable DrmSessionManager drmSessionManager, Format format) { - if (!VpxLibrary.isAvailable() || !MimeTypes.VIDEO_VP9.equalsIgnoreCase(format.sampleMimeType)) { - return FORMAT_UNSUPPORTED_TYPE; - } - boolean drmIsSupported = - format.drmInitData == null - || VpxLibrary.matchesExpectedExoMediaCryptoType(format.exoMediaCryptoType) - || (format.exoMediaCryptoType == null - && supportsFormatDrm(drmSessionManager, format.drmInitData)); - if (!drmIsSupported) { - return FORMAT_UNSUPPORTED_DRM; - } - return FORMAT_HANDLED | ADAPTIVE_SEAMLESS; + public String getName() { + return TAG; } @Override - protected SimpleDecoder< - VideoDecoderInputBuffer, - ? extends VideoDecoderOutputBuffer, - ? extends VideoDecoderException> - createDecoder(Format format, @Nullable ExoMediaCrypto mediaCrypto) - throws VideoDecoderException { + @Capabilities + public final int supportsFormat(Format format) { + if (!VpxLibrary.isAvailable() || !MimeTypes.VIDEO_VP9.equalsIgnoreCase(format.sampleMimeType)) { + return RendererCapabilities.create(FORMAT_UNSUPPORTED_TYPE); + } + boolean drmIsSupported = + format.drmInitData == null + || VpxLibrary.matchesExpectedExoMediaCryptoType(format.exoMediaCryptoType); + if (!drmIsSupported) { + return RendererCapabilities.create(FORMAT_UNSUPPORTED_DRM); + } + return RendererCapabilities.create(FORMAT_HANDLED, ADAPTIVE_SEAMLESS, TUNNELING_NOT_SUPPORTED); + } + + @Override + protected VpxDecoder createDecoder(Format format, @Nullable ExoMediaCrypto mediaCrypto) + throws VpxDecoderException { TraceUtil.beginSection("createVpxDecoder"); int initialInputBufferSize = format.maxInputSize != Format.NO_VALUE ? format.maxInputSize : DEFAULT_INPUT_BUFFER_SIZE; @@ -257,17 +151,6 @@ public class LibvpxVideoRenderer extends SimpleDecoderVideoRenderer { return decoder; } - @Override - protected void renderOutputBuffer( - VideoDecoderOutputBuffer outputBuffer, long presentationTimeUs, Format outputFormat) - throws VideoDecoderException { - if (frameMetadataListener != null) { - frameMetadataListener.onVideoFrameAboutToBeRendered( - presentationTimeUs, System.nanoTime(), outputFormat, /* mediaFormat= */ null); - } - super.renderOutputBuffer(outputBuffer, presentationTimeUs, outputFormat); - } - @Override protected void renderOutputBufferToSurface(VideoDecoderOutputBuffer outputBuffer, Surface surface) throws VpxDecoderException { @@ -286,18 +169,8 @@ public class LibvpxVideoRenderer extends SimpleDecoderVideoRenderer { } } - // PlayerMessage.Target implementation. - @Override - public void handleMessage(int messageType, @Nullable Object message) throws ExoPlaybackException { - if (messageType == C.MSG_SET_SURFACE) { - setOutputSurface((Surface) message); - } else if (messageType == C.MSG_SET_VIDEO_DECODER_OUTPUT_BUFFER_RENDERER) { - setOutputBufferRenderer((VideoDecoderOutputBufferRenderer) message); - } else if (messageType == C.MSG_SET_VIDEO_FRAME_METADATA_LISTENER) { - frameMetadataListener = (VideoFrameMetadataListener) message; - } else { - super.handleMessage(messageType, message); - } + protected boolean canKeepCodec(Format oldFormat, Format newFormat) { + return true; } } diff --git a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxDecoderException.java b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxDecoderException.java index b2da9a7ff8..686790bc2c 100644 --- a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxDecoderException.java +++ b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxDecoderException.java @@ -15,10 +15,10 @@ */ package com.google.android.exoplayer2.ext.vp9; -import com.google.android.exoplayer2.video.VideoDecoderException; +import com.google.android.exoplayer2.decoder.DecoderException; /** Thrown when a libvpx decoder error occurs. */ -public final class VpxDecoderException extends VideoDecoderException { +public final class VpxDecoderException extends DecoderException { /* package */ VpxDecoderException(String message) { super(message); 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 1c434032d0..99f35217fc 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 @@ -32,7 +32,7 @@ public final class VpxOutputBuffer extends VideoDecoderOutputBuffer { * * @param owner Buffer owner. */ - public VpxOutputBuffer(VideoDecoderOutputBuffer.Owner owner) { + public VpxOutputBuffer(Owner owner) { super(owner); } } diff --git a/extensions/vp9/src/main/jni/vpx_jni.cc b/extensions/vp9/src/main/jni/vpx_jni.cc index 823f9b8cab..9996848047 100644 --- a/extensions/vp9/src/main/jni/vpx_jni.cc +++ b/extensions/vp9/src/main/jni/vpx_jni.cc @@ -535,7 +535,7 @@ DECODER_FUNC(jint, vpxGetFrame, jlong jContext, jobject jOutputBuffer) { // LINT.IfChange const int kOutputModeYuv = 0; const int kOutputModeSurfaceYuv = 1; - // LINT.ThenChange(../../../../../library/core/src/main/java/com/google/android/exoplayer2/C.java) + // LINT.ThenChange(../../../../../library/common/src/main/java/com/google/android/exoplayer2/C.java) int outputMode = env->GetIntField(jOutputBuffer, outputModeField); if (outputMode == kOutputModeYuv) { diff --git a/extensions/workmanager/build.gradle b/extensions/workmanager/build.gradle index 6a7aa10722..6025ecfcd0 100644 --- a/extensions/workmanager/build.gradle +++ b/extensions/workmanager/build.gradle @@ -34,7 +34,8 @@ android { dependencies { implementation project(modulePrefix + 'library-core') - implementation 'androidx.work:work-runtime:2.2.0' + implementation 'androidx.work:work-runtime:2.3.4' + compileOnly 'org.jetbrains.kotlin:kotlin-annotations-jvm:' + kotlinAnnotationsVersion } ext { diff --git a/extensions/workmanager/src/main/java/com/google/android/exoplayer2/ext/workmanager/WorkManagerScheduler.java b/extensions/workmanager/src/main/java/com/google/android/exoplayer2/ext/workmanager/WorkManagerScheduler.java index 01801c9897..97b132980d 100644 --- a/extensions/workmanager/src/main/java/com/google/android/exoplayer2/ext/workmanager/WorkManagerScheduler.java +++ b/extensions/workmanager/src/main/java/com/google/android/exoplayer2/ext/workmanager/WorkManagerScheduler.java @@ -15,9 +15,9 @@ */ package com.google.android.exoplayer2.ext.workmanager; -import android.annotation.TargetApi; import android.content.Context; import android.content.Intent; +import androidx.annotation.RequiresApi; import androidx.work.Constraints; import androidx.work.Data; import androidx.work.ExistingWorkPolicy; @@ -92,7 +92,7 @@ public final class WorkManagerScheduler implements Scheduler { return builder.build(); } - @TargetApi(23) + @RequiresApi(23) private static void setRequiresDeviceIdle(Constraints.Builder builder) { builder.setRequiresDeviceIdle(true); } diff --git a/gradle.properties b/gradle.properties index 31ff0ad6b6..297016aec1 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,6 +1,5 @@ ## Project-wide Gradle settings. android.useAndroidX=true android.enableJetifier=true -android.enableUnitTestBinaryResources=true buildDir=buildout org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=512m diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 7fefd1c665..eefcdc910f 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Mon Oct 07 17:24:00 BST 2019 +#Wed Mar 04 12:41:50 GMT 2020 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-all.zip diff --git a/javadoc_combined.gradle b/javadoc_combined.gradle index d2fa241a81..3b482910ae 100644 --- a/javadoc_combined.gradle +++ b/javadoc_combined.gradle @@ -29,8 +29,7 @@ class CombinedJavadocPlugin implements Plugin { classpath = project.files([]) destinationDir = project.file("$project.buildDir/docs/javadoc") options { - links "https://docs.oracle.com/javase/7/docs/api/", - "https://developer.android.com/reference" + links "https://developer.android.com/reference" encoding = "UTF-8" } exclude "**/BuildConfig.java" diff --git a/javadoc_library.gradle b/javadoc_library.gradle index 74fcc3dd6c..dd508a1781 100644 --- a/javadoc_library.gradle +++ b/javadoc_library.gradle @@ -26,9 +26,7 @@ android.libraryVariants.all { variant -> title = "ExoPlayer ${javadocTitle}" source = allSourceDirs options { - links "http://docs.oracle.com/javase/7/docs/api/" - linksOffline "https://developer.android.com/reference", - "${android.sdkDirectory}/docs/reference" + links "https://developer.android.com/reference" encoding = "UTF-8" } exclude "**/BuildConfig.java" diff --git a/javadoc_util.gradle b/javadoc_util.gradle index cff5f29392..b9962d33a9 100644 --- a/javadoc_util.gradle +++ b/javadoc_util.gradle @@ -15,16 +15,26 @@ ext.fixJavadoc = { def javadocPath = "${project.buildDir}/docs/javadoc" // Fix external Android links to target the top frame. def androidRoot = "https://developer.android.com/reference/" - def androidLink = "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + ant.replaceregexp(match:headTag, replace:headTagWithFavicon, flags:'g') { fileset(dir: "${javadocPath}", includes: "**/*.html") } // Remove date metadata that changes every time Javadoc is generated. diff --git a/library/common/README.md b/library/common/README.md new file mode 100644 index 0000000000..af7264bcad --- /dev/null +++ b/library/common/README.md @@ -0,0 +1,10 @@ +# ExoPlayer common library module # + +Common code used by other ExoPlayer modules. + +## Links ## + +* [Javadoc][]: Note that this Javadoc is combined with that of other modules. + +[Javadoc]: https://exoplayer.dev/doc/reference/index.html + diff --git a/library/common/build.gradle b/library/common/build.gradle new file mode 100644 index 0000000000..9dc3aabac3 --- /dev/null +++ b/library/common/build.gradle @@ -0,0 +1,58 @@ +// Copyright (C) 2020 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. +apply from: '../../constants.gradle' +apply plugin: 'com.android.library' + +android { + compileSdkVersion project.ext.compileSdkVersion + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + defaultConfig { + minSdkVersion project.ext.minSdkVersion + targetSdkVersion project.ext.targetSdkVersion + } + + buildTypes { + debug { + testCoverageEnabled = true + } + } + + testOptions.unitTests.includeAndroidResources = true +} + +dependencies { + implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion + compileOnly 'com.google.code.findbugs:jsr305:' + jsr305Version + compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion + compileOnly 'org.checkerframework:checker-compat-qual:' + checkerframeworkVersion + compileOnly 'org.jetbrains.kotlin:kotlin-annotations-jvm:' + kotlinAnnotationsVersion + testImplementation project(modulePrefix + 'testutils') + testImplementation 'org.robolectric:robolectric:' + robolectricVersion +} + +ext { + javadocTitle = 'Common module' +} +apply from: '../../javadoc_library.gradle' + +ext { + releaseArtifact = 'exoplayer-common' + releaseDescription = 'The ExoPlayer library common module.' +} +apply from: '../../publish.gradle' diff --git a/library/common/proguard-rules.txt b/library/common/proguard-rules.txt new file mode 100644 index 0000000000..c83dbaee2d --- /dev/null +++ b/library/common/proguard-rules.txt @@ -0,0 +1,6 @@ +# Proguard rules specific to the common module. + +# Don't warn about checkerframework and Kotlin annotations +-dontwarn org.checkerframework.** +-dontwarn kotlin.annotations.jvm.** +-dontwarn javax.annotation.** diff --git a/library/common/src/main/AndroidManifest.xml b/library/common/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..83cb0cbde6 --- /dev/null +++ b/library/common/src/main/AndroidManifest.xml @@ -0,0 +1,20 @@ + + + + + + diff --git a/library/core/src/main/java/com/google/android/exoplayer2/C.java b/library/common/src/main/java/com/google/android/exoplayer2/C.java similarity index 83% rename from library/core/src/main/java/com/google/android/exoplayer2/C.java rename to library/common/src/main/java/com/google/android/exoplayer2/C.java index 567ce98b1a..9f4e8beb1c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/C.java +++ b/library/common/src/main/java/com/google/android/exoplayer2/C.java @@ -15,22 +15,15 @@ */ package com.google.android.exoplayer2; -import android.annotation.TargetApi; import android.content.Context; import android.media.AudioAttributes; import android.media.AudioFormat; import android.media.AudioManager; import android.media.MediaCodec; import android.media.MediaFormat; -import android.view.Surface; import androidx.annotation.IntDef; -import com.google.android.exoplayer2.PlayerMessage.Target; -import com.google.android.exoplayer2.audio.AuxEffectInfo; +import androidx.annotation.RequiresApi; import com.google.android.exoplayer2.util.Util; -import com.google.android.exoplayer2.video.SimpleDecoderVideoRenderer; -import com.google.android.exoplayer2.video.VideoDecoderOutputBufferRenderer; -import com.google.android.exoplayer2.video.VideoFrameMetadataListener; -import com.google.android.exoplayer2.video.spherical.CameraMotionListener; import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -95,14 +88,16 @@ public final class C { * The name of the ASCII charset. */ public static final String ASCII_NAME = "US-ASCII"; + /** * The name of the UTF-8 charset. */ public static final String UTF8_NAME = "UTF-8"; - /** - * The name of the UTF-16 charset. - */ + /** The name of the ISO-8859-1 charset. */ + public static final String ISO88591_NAME = "ISO-8859-1"; + + /** The name of the UTF-16 charset. */ public static final String UTF16_NAME = "UTF-16"; /** The name of the UTF-16 little-endian charset. */ @@ -148,8 +143,8 @@ public final class C { /** * Represents an audio encoding, or an invalid or unset value. One of {@link Format#NO_VALUE}, * {@link #ENCODING_INVALID}, {@link #ENCODING_PCM_8BIT}, {@link #ENCODING_PCM_16BIT}, {@link - * #ENCODING_PCM_24BIT}, {@link #ENCODING_PCM_32BIT}, {@link #ENCODING_PCM_FLOAT}, {@link - * #ENCODING_PCM_MU_LAW}, {@link #ENCODING_PCM_A_LAW}, {@link #ENCODING_AC3}, {@link + * #ENCODING_PCM_16BIT_BIG_ENDIAN}, {@link #ENCODING_PCM_24BIT}, {@link #ENCODING_PCM_32BIT}, + * {@link #ENCODING_PCM_FLOAT}, {@link #ENCODING_MP3}, {@link #ENCODING_AC3}, {@link * #ENCODING_E_AC3}, {@link #ENCODING_E_AC3_JOC}, {@link #ENCODING_AC4}, {@link #ENCODING_DTS}, * {@link #ENCODING_DTS_HD} or {@link #ENCODING_DOLBY_TRUEHD}. */ @@ -160,26 +155,31 @@ public final class C { ENCODING_INVALID, ENCODING_PCM_8BIT, ENCODING_PCM_16BIT, + ENCODING_PCM_16BIT_BIG_ENDIAN, ENCODING_PCM_24BIT, ENCODING_PCM_32BIT, ENCODING_PCM_FLOAT, - ENCODING_PCM_MU_LAW, - ENCODING_PCM_A_LAW, + ENCODING_MP3, + ENCODING_AAC_LC, + ENCODING_AAC_HE_V1, + ENCODING_AAC_HE_V2, + ENCODING_AAC_XHE, + ENCODING_AAC_ELD, ENCODING_AC3, ENCODING_E_AC3, ENCODING_E_AC3_JOC, ENCODING_AC4, ENCODING_DTS, ENCODING_DTS_HD, - ENCODING_DOLBY_TRUEHD, + ENCODING_DOLBY_TRUEHD }) public @interface Encoding {} /** * Represents a PCM audio encoding, or an invalid or unset value. One of {@link Format#NO_VALUE}, * {@link #ENCODING_INVALID}, {@link #ENCODING_PCM_8BIT}, {@link #ENCODING_PCM_16BIT}, {@link - * #ENCODING_PCM_24BIT}, {@link #ENCODING_PCM_32BIT}, {@link #ENCODING_PCM_FLOAT}, {@link - * #ENCODING_PCM_MU_LAW} or {@link #ENCODING_PCM_A_LAW}. + * #ENCODING_PCM_16BIT_BIG_ENDIAN}, {@link #ENCODING_PCM_24BIT}, {@link #ENCODING_PCM_32BIT}, + * {@link #ENCODING_PCM_FLOAT}. */ @Documented @Retention(RetentionPolicy.SOURCE) @@ -188,11 +188,10 @@ public final class C { ENCODING_INVALID, ENCODING_PCM_8BIT, ENCODING_PCM_16BIT, + ENCODING_PCM_16BIT_BIG_ENDIAN, ENCODING_PCM_24BIT, ENCODING_PCM_32BIT, - ENCODING_PCM_FLOAT, - ENCODING_PCM_MU_LAW, - ENCODING_PCM_A_LAW + ENCODING_PCM_FLOAT }) public @interface PcmEncoding {} /** @see AudioFormat#ENCODING_INVALID */ @@ -201,16 +200,26 @@ public final class C { public static final int ENCODING_PCM_8BIT = AudioFormat.ENCODING_PCM_8BIT; /** @see AudioFormat#ENCODING_PCM_16BIT */ public static final int ENCODING_PCM_16BIT = AudioFormat.ENCODING_PCM_16BIT; + /** Like {@link #ENCODING_PCM_16BIT}, but with the bytes in big endian order. */ + public static final int ENCODING_PCM_16BIT_BIG_ENDIAN = 0x10000000; /** PCM encoding with 24 bits per sample. */ - public static final int ENCODING_PCM_24BIT = 0x80000000; + public static final int ENCODING_PCM_24BIT = 0x20000000; /** PCM encoding with 32 bits per sample. */ - public static final int ENCODING_PCM_32BIT = 0x40000000; + public static final int ENCODING_PCM_32BIT = 0x30000000; /** @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_MP3 */ + public static final int ENCODING_MP3 = AudioFormat.ENCODING_MP3; + /** @see AudioFormat#ENCODING_AAC_LC */ + public static final int ENCODING_AAC_LC = AudioFormat.ENCODING_AAC_LC; + /** @see AudioFormat#ENCODING_AAC_HE_V1 */ + public static final int ENCODING_AAC_HE_V1 = AudioFormat.ENCODING_AAC_HE_V1; + /** @see AudioFormat#ENCODING_AAC_HE_V2 */ + public static final int ENCODING_AAC_HE_V2 = AudioFormat.ENCODING_AAC_HE_V2; + /** @see AudioFormat#ENCODING_AAC_XHE */ + public static final int ENCODING_AAC_XHE = AudioFormat.ENCODING_AAC_XHE; + /** @see AudioFormat#ENCODING_AAC_ELD */ + public static final int ENCODING_AAC_ELD = AudioFormat.ENCODING_AAC_ELD; /** @see AudioFormat#ENCODING_AC3 */ public static final int ENCODING_AC3 = AudioFormat.ENCODING_AC3; /** @see AudioFormat#ENCODING_E_AC3 */ @@ -283,9 +292,9 @@ public final class C { public static final int STREAM_TYPE_DEFAULT = STREAM_TYPE_MUSIC; /** - * Content types for {@link com.google.android.exoplayer2.audio.AudioAttributes}. One of {@link - * #CONTENT_TYPE_MOVIE}, {@link #CONTENT_TYPE_MUSIC}, {@link #CONTENT_TYPE_SONIFICATION}, {@link - * #CONTENT_TYPE_SPEECH} or {@link #CONTENT_TYPE_UNKNOWN}. + * Content types for audio attributes. One of {@link #CONTENT_TYPE_MOVIE}, {@link + * #CONTENT_TYPE_MUSIC}, {@link #CONTENT_TYPE_SONIFICATION}, {@link #CONTENT_TYPE_SPEECH} or + * {@link #CONTENT_TYPE_UNKNOWN}. */ @Documented @Retention(RetentionPolicy.SOURCE) @@ -322,8 +331,7 @@ public final class C { android.media.AudioAttributes.CONTENT_TYPE_UNKNOWN; /** - * Flags for {@link com.google.android.exoplayer2.audio.AudioAttributes}. Possible flag value is - * {@link #FLAG_AUDIBILITY_ENFORCED}. + * Flags for audio attributes. Possible flag value is {@link #FLAG_AUDIBILITY_ENFORCED}. * *

Note that {@code FLAG_HW_AV_SYNC} is not available because the player takes care of setting * the flag when tunneling is enabled via a track selector. @@ -341,15 +349,14 @@ public final class C { android.media.AudioAttributes.FLAG_AUDIBILITY_ENFORCED; /** - * Usage types for {@link com.google.android.exoplayer2.audio.AudioAttributes}. One of {@link - * #USAGE_ALARM}, {@link #USAGE_ASSISTANCE_ACCESSIBILITY}, {@link - * #USAGE_ASSISTANCE_NAVIGATION_GUIDANCE}, {@link #USAGE_ASSISTANCE_SONIFICATION}, {@link - * #USAGE_ASSISTANT}, {@link #USAGE_GAME}, {@link #USAGE_MEDIA}, {@link #USAGE_NOTIFICATION}, - * {@link #USAGE_NOTIFICATION_COMMUNICATION_DELAYED}, {@link - * #USAGE_NOTIFICATION_COMMUNICATION_INSTANT}, {@link #USAGE_NOTIFICATION_COMMUNICATION_REQUEST}, - * {@link #USAGE_NOTIFICATION_EVENT}, {@link #USAGE_NOTIFICATION_RINGTONE}, {@link - * #USAGE_UNKNOWN}, {@link #USAGE_VOICE_COMMUNICATION} or {@link - * #USAGE_VOICE_COMMUNICATION_SIGNALLING}. + * Usage types for audio attributes. One of {@link #USAGE_ALARM}, {@link + * #USAGE_ASSISTANCE_ACCESSIBILITY}, {@link #USAGE_ASSISTANCE_NAVIGATION_GUIDANCE}, {@link + * #USAGE_ASSISTANCE_SONIFICATION}, {@link #USAGE_ASSISTANT}, {@link #USAGE_GAME}, {@link + * #USAGE_MEDIA}, {@link #USAGE_NOTIFICATION}, {@link #USAGE_NOTIFICATION_COMMUNICATION_DELAYED}, + * {@link #USAGE_NOTIFICATION_COMMUNICATION_INSTANT}, {@link + * #USAGE_NOTIFICATION_COMMUNICATION_REQUEST}, {@link #USAGE_NOTIFICATION_EVENT}, {@link + * #USAGE_NOTIFICATION_RINGTONE}, {@link #USAGE_UNKNOWN}, {@link #USAGE_VOICE_COMMUNICATION} or + * {@link #USAGE_VOICE_COMMUNICATION_SIGNALLING}. */ @Documented @Retention(RetentionPolicy.SOURCE) @@ -444,8 +451,8 @@ public final class C { android.media.AudioAttributes.USAGE_VOICE_COMMUNICATION_SIGNALLING; /** - * Capture policies for {@link com.google.android.exoplayer2.audio.AudioAttributes}. One of {@link - * #ALLOW_CAPTURE_BY_ALL}, {@link #ALLOW_CAPTURE_BY_NONE} or {@link #ALLOW_CAPTURE_BY_SYSTEM}. + * Capture policies for audio attributes. One of {@link #ALLOW_CAPTURE_BY_ALL}, {@link + * #ALLOW_CAPTURE_BY_NONE} or {@link #ALLOW_CAPTURE_BY_SYSTEM}. */ @Documented @Retention(RetentionPolicy.SOURCE) @@ -541,28 +548,22 @@ public final class C { // ../../../../../../../../../extensions/vp9/src/main/jni/vpx_jni.cc // ) - /** - * Video scaling modes for {@link MediaCodec}-based {@link Renderer}s. One of {@link - * #VIDEO_SCALING_MODE_SCALE_TO_FIT} or {@link #VIDEO_SCALING_MODE_SCALE_TO_FIT_WITH_CROPPING}. - */ + /** @deprecated Use {@code Renderer.VideoScalingMode}. */ @Documented @Retention(RetentionPolicy.SOURCE) @IntDef(value = {VIDEO_SCALING_MODE_SCALE_TO_FIT, VIDEO_SCALING_MODE_SCALE_TO_FIT_WITH_CROPPING}) + @Deprecated public @interface VideoScalingMode {} - /** - * @see MediaCodec#VIDEO_SCALING_MODE_SCALE_TO_FIT - */ + /** @deprecated Use {@code Renderer.VIDEO_SCALING_MODE_SCALE_TO_FIT}. */ + @Deprecated public static final int VIDEO_SCALING_MODE_SCALE_TO_FIT = MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT; - /** - * @see MediaCodec#VIDEO_SCALING_MODE_SCALE_TO_FIT - */ + /** @deprecated Use {@code Renderer.VIDEO_SCALING_MODE_SCALE_TO_FIT_WITH_CROPPING}. */ + @Deprecated public static final int VIDEO_SCALING_MODE_SCALE_TO_FIT_WITH_CROPPING = MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT_WITH_CROPPING; - /** - * A default video scaling mode for {@link MediaCodec}-based {@link Renderer}s. - */ - public static final int VIDEO_SCALING_MODE_DEFAULT = VIDEO_SCALING_MODE_SCALE_TO_FIT; + /** @deprecated Use {@code Renderer.VIDEO_SCALING_MODE_DEFAULT}. */ + @Deprecated public static final int VIDEO_SCALING_MODE_DEFAULT = VIDEO_SCALING_MODE_SCALE_TO_FIT; /** * Track selection flags. Possible flag values are {@link #SELECTION_FLAG_DEFAULT}, {@link @@ -762,90 +763,32 @@ public final class C { */ public static final UUID PLAYREADY_UUID = new UUID(0x9A04F07998404286L, 0xAB92E65BE0885F95L); - /** - * The type of a message that can be passed to a video {@link Renderer} via {@link - * ExoPlayer#createMessage(Target)}. The message payload should be the target {@link Surface}, or - * null. - */ - public static final int MSG_SET_SURFACE = 1; + /** @deprecated Use {@code Renderer.MSG_SET_SURFACE}. */ + @Deprecated public static final int MSG_SET_SURFACE = 1; - /** - * A type of a message that can be passed to an audio {@link Renderer} via {@link - * ExoPlayer#createMessage(Target)}. The message payload should be a {@link Float} with 0 being - * silence and 1 being unity gain. - */ - public static final int MSG_SET_VOLUME = 2; + /** @deprecated Use {@code Renderer.MSG_SET_VOLUME}. */ + @Deprecated public static final int MSG_SET_VOLUME = 2; - /** - * 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 - * com.google.android.exoplayer2.audio.AudioAttributes} instance that will configure 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)}. - * - *

To get audio attributes that are equivalent to a legacy stream type, pass the stream type to - * {@link Util#getAudioUsageForStreamType(int)} and use the returned {@link C.AudioUsage} to build - * an audio attributes instance. - */ - public static final int MSG_SET_AUDIO_ATTRIBUTES = 3; + /** @deprecated Use {@code Renderer.MSG_SET_AUDIO_ATTRIBUTES}. */ + @Deprecated public static final int MSG_SET_AUDIO_ATTRIBUTES = 3; - /** - * The type of a message that can be passed to a {@link MediaCodec}-based video {@link Renderer} - * via {@link ExoPlayer#createMessage(Target)}. The message payload should be one of the integer - * scaling modes in {@link C.VideoScalingMode}. - * - *

Note that the scaling mode only applies if the {@link Surface} targeted by the renderer is - * owned by a {@link android.view.SurfaceView}. - */ - public static final int MSG_SET_SCALING_MODE = 4; + /** @deprecated Use {@code Renderer.MSG_SET_SCALING_MODE}. */ + @Deprecated 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; + /** @deprecated Use {@code Renderer.MSG_SET_AUX_EFFECT_INFO}. */ + @Deprecated public static final int MSG_SET_AUX_EFFECT_INFO = 5; - /** - * The type of a message that can be passed to a video {@link Renderer} via {@link - * ExoPlayer#createMessage(Target)}. The message payload should be a {@link - * VideoFrameMetadataListener} instance, or null. - */ - public static final int MSG_SET_VIDEO_FRAME_METADATA_LISTENER = 6; + /** @deprecated Use {@code Renderer.MSG_SET_VIDEO_FRAME_METADATA_LISTENER}. */ + @Deprecated public static final int MSG_SET_VIDEO_FRAME_METADATA_LISTENER = 6; - /** - * The type of a message that can be passed to a camera motion {@link Renderer} via {@link - * ExoPlayer#createMessage(Target)}. The message payload should be a {@link CameraMotionListener} - * instance, or null. - */ - public static final int MSG_SET_CAMERA_MOTION_LISTENER = 7; + /** @deprecated Use {@code Renderer.MSG_SET_CAMERA_MOTION_LISTENER}. */ + @Deprecated public static final int MSG_SET_CAMERA_MOTION_LISTENER = 7; - /** - * The type of a message that can be passed to a {@link SimpleDecoderVideoRenderer} via {@link - * ExoPlayer#createMessage(Target)}. The message payload should be the target {@link - * VideoDecoderOutputBufferRenderer}, or null. - * - *

This message is intended only for use with extension renderers that expect a {@link - * VideoDecoderOutputBufferRenderer}. For other use cases, an output surface should be passed via - * {@link #MSG_SET_SURFACE} instead. - */ - public static final int MSG_SET_VIDEO_DECODER_OUTPUT_BUFFER_RENDERER = 8; + /** @deprecated Use {@code Renderer.MSG_SET_VIDEO_DECODER_OUTPUT_BUFFER_RENDERER}. */ + @Deprecated public static final int MSG_SET_VIDEO_DECODER_OUTPUT_BUFFER_RENDERER = 8; - /** - * 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. - */ - public static final int MSG_CUSTOM_BASE = 10000; + /** @deprecated Use {@code Renderer.MSG_CUSTOM_BASE}. */ + @Deprecated public static final int MSG_CUSTOM_BASE = 10000; /** * The stereo mode for 360/3D/VR videos. One of {@link Format#NO_VALUE}, {@link @@ -976,8 +919,8 @@ public final class C { /** * Network connection type. One of {@link #NETWORK_TYPE_UNKNOWN}, {@link #NETWORK_TYPE_OFFLINE}, * {@link #NETWORK_TYPE_WIFI}, {@link #NETWORK_TYPE_2G}, {@link #NETWORK_TYPE_3G}, {@link - * #NETWORK_TYPE_4G}, {@link #NETWORK_TYPE_CELLULAR_UNKNOWN}, {@link #NETWORK_TYPE_ETHERNET} or - * {@link #NETWORK_TYPE_OTHER}. + * #NETWORK_TYPE_4G}, {@link #NETWORK_TYPE_5G}, {@link #NETWORK_TYPE_CELLULAR_UNKNOWN}, {@link + * #NETWORK_TYPE_ETHERNET} or {@link #NETWORK_TYPE_OTHER}. */ @Documented @Retention(RetentionPolicy.SOURCE) @@ -988,6 +931,7 @@ public final class C { NETWORK_TYPE_2G, NETWORK_TYPE_3G, NETWORK_TYPE_4G, + NETWORK_TYPE_5G, NETWORK_TYPE_CELLULAR_UNKNOWN, NETWORK_TYPE_ETHERNET, NETWORK_TYPE_OTHER @@ -1005,6 +949,8 @@ public final class C { 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 a 5G cellular connection. */ + public static final int NETWORK_TYPE_5G = 9; /** * 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}. @@ -1012,19 +958,48 @@ public final class C { 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). - */ + /** Network type for other connections which are not Wifi or cellular (e.g. VPN, Bluetooth). */ public static final int NETWORK_TYPE_OTHER = 8; + /** + * Mode specifying whether the player should hold a WakeLock and a WifiLock. One of {@link + * #WAKE_MODE_NONE}, {@link #WAKE_MODE_LOCAL} and {@link #WAKE_MODE_NETWORK}. + */ + @Documented + @Retention(RetentionPolicy.SOURCE) + @IntDef({WAKE_MODE_NONE, WAKE_MODE_LOCAL, WAKE_MODE_NETWORK}) + public @interface WakeMode {} + /** + * A wake mode that will not cause the player to hold any locks. + * + *

This is suitable for applications that do not play media with the screen off. + */ + public static final int WAKE_MODE_NONE = 0; + /** + * A wake mode that will cause the player to hold a {@link android.os.PowerManager.WakeLock} + * during playback. + * + *

This is suitable for applications that play media with the screen off and do not load media + * over wifi. + */ + public static final int WAKE_MODE_LOCAL = 1; + /** + * A wake mode that will cause the player to hold a {@link android.os.PowerManager.WakeLock} and a + * {@link android.net.wifi.WifiManager.WifiLock} during playback. + * + *

This is suitable for applications that play media with the screen off and may load media + * over wifi. + */ + public static final int WAKE_MODE_NETWORK = 2; + /** * Track role flags. Possible flag values are {@link #ROLE_FLAG_MAIN}, {@link * #ROLE_FLAG_ALTERNATE}, {@link #ROLE_FLAG_SUPPLEMENTARY}, {@link #ROLE_FLAG_COMMENTARY}, {@link * #ROLE_FLAG_DUB}, {@link #ROLE_FLAG_EMERGENCY}, {@link #ROLE_FLAG_CAPTION}, {@link * #ROLE_FLAG_SUBTITLE}, {@link #ROLE_FLAG_SIGN}, {@link #ROLE_FLAG_DESCRIBES_VIDEO}, {@link * #ROLE_FLAG_DESCRIBES_MUSIC_AND_SOUND}, {@link #ROLE_FLAG_ENHANCED_DIALOG_INTELLIGIBILITY}, - * {@link #ROLE_FLAG_TRANSCRIBES_DIALOG} and {@link #ROLE_FLAG_EASY_TO_READ}. + * {@link #ROLE_FLAG_TRANSCRIBES_DIALOG}, {@link #ROLE_FLAG_EASY_TO_READ} and {@link + * #ROLE_FLAG_TRICK_PLAY}. */ @Documented @Retention(RetentionPolicy.SOURCE) @@ -1044,7 +1019,8 @@ public final class C { ROLE_FLAG_DESCRIBES_MUSIC_AND_SOUND, ROLE_FLAG_ENHANCED_DIALOG_INTELLIGIBILITY, ROLE_FLAG_TRANSCRIBES_DIALOG, - ROLE_FLAG_EASY_TO_READ + ROLE_FLAG_EASY_TO_READ, + ROLE_FLAG_TRICK_PLAY }) public @interface RoleFlags {} /** Indicates a main track. */ @@ -1090,6 +1066,8 @@ public final class C { public static final int ROLE_FLAG_TRANSCRIBES_DIALOG = 1 << 12; /** Indicates the track contains a text that has been edited for ease of reading. */ public static final int ROLE_FLAG_EASY_TO_READ = 1 << 13; + /** Indicates the track is intended for trick play. */ + public static final int ROLE_FLAG_TRICK_PLAY = 1 << 14; /** * Converts a time in microseconds to the corresponding time in milliseconds, preserving @@ -1119,7 +1097,7 @@ public final class C { * * @see AudioManager#generateAudioSessionId() */ - @TargetApi(21) + @RequiresApi(21) public static int generateAudioSessionIdV21(Context context) { return ((AudioManager) context.getSystemService(Context.AUDIO_SERVICE)) .generateAudioSessionId(); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java b/library/common/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java similarity index 96% rename from library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java rename to library/common/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java index 249ef7e44e..06743732e7 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java +++ b/library/common/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.11.0"; + public static final String VERSION = "2.11.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.11.0"; + public static final String VERSION_SLASHY = "ExoPlayerLib/2.11.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 = 2011000; + public static final int VERSION_INT = 2011004; /** * Whether the library was compiled with {@link com.google.android.exoplayer2.util.Assertions} diff --git a/library/common/src/main/java/com/google/android/exoplayer2/Format.java b/library/common/src/main/java/com/google/android/exoplayer2/Format.java new file mode 100644 index 0000000000..e7db47d535 --- /dev/null +++ b/library/common/src/main/java/com/google/android/exoplayer2/Format.java @@ -0,0 +1,1732 @@ +/* + * 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; + +import android.os.Parcel; +import android.os.Parcelable; +import androidx.annotation.Nullable; +import com.google.android.exoplayer2.drm.DrmInitData; +import com.google.android.exoplayer2.drm.ExoMediaCrypto; +import com.google.android.exoplayer2.metadata.Metadata; +import com.google.android.exoplayer2.util.MimeTypes; +import com.google.android.exoplayer2.util.Util; +import com.google.android.exoplayer2.video.ColorInfo; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +/** + * Represents a media format. + * + *

When building formats, populate all fields whose values are known and relevant to the type of + * format being constructed. For information about different types of format, see ExoPlayer's Supported formats page. + * + *

Fields commonly relevant to all formats

+ * + *
    + *
  • {@link #id} + *
  • {@link #label} + *
  • {@link #language} + *
  • {@link #selectionFlags} + *
  • {@link #roleFlags} + *
  • {@link #averageBitrate} + *
  • {@link #peakBitrate} + *
  • {@link #codecs} + *
  • {@link #metadata} + *
+ * + *

Fields relevant to container formats

+ * + *
    + *
  • {@link #containerMimeType} + *
  • If the container only contains a single media track, fields + * relevant to sample formats can are also be relevant and can be set to describe the + * sample format of that track. + *
  • If the container only contains one track of a given type (possibly alongside tracks of + * other types), then fields relevant to that track type can be set to describe the properties + * of the track. See the sections below for video, audio and text formats. + *
+ * + *

Fields relevant to sample formats

+ * + *
    + *
  • {@link #sampleMimeType} + *
  • {@link #maxInputSize} + *
  • {@link #initializationData} + *
  • {@link #drmInitData} + *
  • {@link #subsampleOffsetUs} + *
  • Fields relevant to the sample format's track type are also relevant. See the sections below + * for video, audio and text formats. + *
+ * + *

Fields relevant to video formats

+ * + *
    + *
  • {@link #width} + *
  • {@link #height} + *
  • {@link #frameRate} + *
  • {@link #rotationDegrees} + *
  • {@link #pixelWidthHeightRatio} + *
  • {@link #projectionData} + *
  • {@link #stereoMode} + *
  • {@link #colorInfo} + *
+ * + *

Fields relevant to audio formats

+ * + *
    + *
  • {@link #channelCount} + *
  • {@link #sampleRate} + *
  • {@link #pcmEncoding} + *
  • {@link #encoderDelay} + *
  • {@link #encoderPadding} + *
+ * + *

Fields relevant to text formats

+ * + *
    + *
  • {@link #accessibilityChannel} + *
+ */ +public final class Format implements Parcelable { + + /** + * Builds {@link Format} instances. + * + *

Use Format#buildUpon() to obtain a builder representing an existing {@link Format}. + * + *

When building formats, populate all fields whose values are known and relevant to the type + * of format being constructed. See the {@link Format} Javadoc for information about which fields + * should be set for different types of format. + */ + public static final class Builder { + + @Nullable private String id; + @Nullable private String label; + @Nullable private String language; + @C.SelectionFlags private int selectionFlags; + @C.RoleFlags private int roleFlags; + private int averageBitrate; + private int peakBitrate; + @Nullable private String codecs; + @Nullable private Metadata metadata; + + // Container specific. + + @Nullable private String containerMimeType; + + // Sample specific. + + @Nullable private String sampleMimeType; + private int maxInputSize; + @Nullable private List initializationData; + @Nullable private DrmInitData drmInitData; + private long subsampleOffsetUs; + + // Video specific. + + private int width; + private int height; + private float frameRate; + private int rotationDegrees; + private float pixelWidthHeightRatio; + @Nullable private byte[] projectionData; + @C.StereoMode private int stereoMode; + @Nullable private ColorInfo colorInfo; + + // Audio specific. + + private int channelCount; + private int sampleRate; + @C.PcmEncoding private int pcmEncoding; + private int encoderDelay; + private int encoderPadding; + + // Text specific. + + private int accessibilityChannel; + + // Provided by source. + + @Nullable private Class exoMediaCryptoType; + + /** Creates a new instance with default values. */ + public Builder() { + averageBitrate = NO_VALUE; + peakBitrate = NO_VALUE; + // Sample specific. + maxInputSize = NO_VALUE; + subsampleOffsetUs = OFFSET_SAMPLE_RELATIVE; + // Video specific. + width = NO_VALUE; + height = NO_VALUE; + frameRate = NO_VALUE; + pixelWidthHeightRatio = 1.0f; + stereoMode = NO_VALUE; + // Audio specific. + channelCount = NO_VALUE; + sampleRate = NO_VALUE; + pcmEncoding = NO_VALUE; + // Text specific. + accessibilityChannel = NO_VALUE; + } + + /** + * Creates a new instance to build upon the provided {@link Format}. + * + * @param format The {@link Format} to build upon. + */ + private Builder(Format format) { + this.id = format.id; + this.label = format.label; + this.language = format.language; + this.selectionFlags = format.selectionFlags; + this.roleFlags = format.roleFlags; + this.averageBitrate = format.averageBitrate; + this.peakBitrate = format.peakBitrate; + this.codecs = format.codecs; + this.metadata = format.metadata; + // Container specific. + this.containerMimeType = format.containerMimeType; + // Sample specific. + this.sampleMimeType = format.sampleMimeType; + this.maxInputSize = format.maxInputSize; + this.initializationData = format.initializationData; + this.drmInitData = format.drmInitData; + this.subsampleOffsetUs = format.subsampleOffsetUs; + // Video specific. + this.width = format.width; + this.height = format.height; + this.frameRate = format.frameRate; + this.rotationDegrees = format.rotationDegrees; + this.pixelWidthHeightRatio = format.pixelWidthHeightRatio; + this.projectionData = format.projectionData; + this.stereoMode = format.stereoMode; + this.colorInfo = format.colorInfo; + // Audio specific. + this.channelCount = format.channelCount; + this.sampleRate = format.sampleRate; + this.pcmEncoding = format.pcmEncoding; + this.encoderDelay = format.encoderDelay; + this.encoderPadding = format.encoderPadding; + // Text specific. + this.accessibilityChannel = format.accessibilityChannel; + // Provided by source. + this.exoMediaCryptoType = format.exoMediaCryptoType; + } + + /** + * Sets {@link Format#id}. The default value is {@code null}. + * + * @param id The {@link Format#id}. + * @return The builder. + */ + public Builder setId(@Nullable String id) { + this.id = id; + return this; + } + + /** + * Sets {@link Format#id} to {@link Integer#toString() Integer.toString(id)}. The default value + * is {@code null}. + * + * @param id The {@link Format#id}. + * @return The builder. + */ + public Builder setId(int id) { + this.id = Integer.toString(id); + return this; + } + + /** + * Sets {@link Format#label}. The default value is {@code null}. + * + * @param label The {@link Format#label}. + * @return The builder. + */ + public Builder setLabel(@Nullable String label) { + this.label = label; + return this; + } + + /** + * Sets {@link Format#language}. The default value is {@code null}. + * + * @param language The {@link Format#language}. + * @return The builder. + */ + public Builder setLanguage(@Nullable String language) { + this.language = language; + return this; + } + + /** + * Sets {@link Format#selectionFlags}. The default value is 0. + * + * @param selectionFlags The {@link Format#selectionFlags}. + * @return The builder. + */ + public Builder setSelectionFlags(@C.SelectionFlags int selectionFlags) { + this.selectionFlags = selectionFlags; + return this; + } + + /** + * Sets {@link Format#roleFlags}. The default value is 0. + * + * @param roleFlags The {@link Format#roleFlags}. + * @return The builder. + */ + public Builder setRoleFlags(@C.RoleFlags int roleFlags) { + this.roleFlags = roleFlags; + return this; + } + + /** + * Sets {@link Format#averageBitrate}. The default value is {@link #NO_VALUE}. + * + * @param averageBitrate The {@link Format#averageBitrate}. + * @return The builder. + */ + public Builder setAverageBitrate(int averageBitrate) { + this.averageBitrate = averageBitrate; + return this; + } + + /** + * Sets {@link Format#peakBitrate}. The default value is {@link #NO_VALUE}. + * + * @param peakBitrate The {@link Format#peakBitrate}. + * @return The builder. + */ + public Builder setPeakBitrate(int peakBitrate) { + this.peakBitrate = peakBitrate; + return this; + } + + /** + * Sets {@link Format#codecs}. The default value is {@code null}. + * + * @param codecs The {@link Format#codecs}. + * @return The builder. + */ + public Builder setCodecs(@Nullable String codecs) { + this.codecs = codecs; + return this; + } + + /** + * Sets {@link Format#metadata}. The default value is {@code null}. + * + * @param metadata The {@link Format#metadata}. + * @return The builder. + */ + public Builder setMetadata(@Nullable Metadata metadata) { + this.metadata = metadata; + return this; + } + + // Container specific. + + /** + * Sets {@link Format#containerMimeType}. The default value is {@code null}. + * + * @param containerMimeType The {@link Format#containerMimeType}. + * @return The builder. + */ + public Builder setContainerMimeType(@Nullable String containerMimeType) { + this.containerMimeType = containerMimeType; + return this; + } + + // Sample specific. + + /** + * Sets {@link Format#sampleMimeType}. The default value is {@code null}. + * + * @param sampleMimeType {@link Format#sampleMimeType}. + * @return The builder. + */ + public Builder setSampleMimeType(@Nullable String sampleMimeType) { + this.sampleMimeType = sampleMimeType; + return this; + } + + /** + * Sets {@link Format#maxInputSize}. The default value is {@link #NO_VALUE}. + * + * @param maxInputSize The {@link Format#maxInputSize}. + * @return The builder. + */ + public Builder setMaxInputSize(int maxInputSize) { + this.maxInputSize = maxInputSize; + return this; + } + + /** + * Sets {@link Format#initializationData}. The default value is {@code null}. + * + * @param initializationData The {@link Format#initializationData}. + * @return The builder. + */ + public Builder setInitializationData(@Nullable List initializationData) { + this.initializationData = initializationData; + return this; + } + + /** + * Sets {@link Format#drmInitData}. The default value is {@code null}. + * + * @param drmInitData The {@link Format#drmInitData}. + * @return The builder. + */ + public Builder setDrmInitData(@Nullable DrmInitData drmInitData) { + this.drmInitData = drmInitData; + return this; + } + + /** + * Sets {@link Format#subsampleOffsetUs}. The default value is {@link #OFFSET_SAMPLE_RELATIVE}. + * + * @param subsampleOffsetUs The {@link Format#subsampleOffsetUs}. + * @return The builder. + */ + public Builder setSubsampleOffsetUs(long subsampleOffsetUs) { + this.subsampleOffsetUs = subsampleOffsetUs; + return this; + } + + // Video specific. + + /** + * Sets {@link Format#width}. The default value is {@link #NO_VALUE}. + * + * @param width The {@link Format#width}. + * @return The builder. + */ + public Builder setWidth(int width) { + this.width = width; + return this; + } + + /** + * Sets {@link Format#height}. The default value is {@link #NO_VALUE}. + * + * @param height The {@link Format#height}. + * @return The builder. + */ + public Builder setHeight(int height) { + this.height = height; + return this; + } + + /** + * Sets {@link Format#frameRate}. The default value is {@link #NO_VALUE}. + * + * @param frameRate The {@link Format#frameRate}. + * @return The builder. + */ + public Builder setFrameRate(float frameRate) { + this.frameRate = frameRate; + return this; + } + + /** + * Sets {@link Format#rotationDegrees}. The default value is 0. + * + * @param rotationDegrees The {@link Format#rotationDegrees}. + * @return The builder. + */ + public Builder setRotationDegrees(int rotationDegrees) { + this.rotationDegrees = rotationDegrees; + return this; + } + + /** + * Sets {@link Format#pixelWidthHeightRatio}. The default value is 1.0f. + * + * @param pixelWidthHeightRatio The {@link Format#pixelWidthHeightRatio}. + * @return The builder. + */ + public Builder setPixelWidthHeightRatio(float pixelWidthHeightRatio) { + this.pixelWidthHeightRatio = pixelWidthHeightRatio; + return this; + } + + /** + * Sets {@link Format#projectionData}. The default value is {@code null}. + * + * @param projectionData The {@link Format#projectionData}. + * @return The builder. + */ + public Builder setProjectionData(@Nullable byte[] projectionData) { + this.projectionData = projectionData; + return this; + } + + /** + * Sets {@link Format#stereoMode}. The default value is {@link #NO_VALUE}. + * + * @param stereoMode The {@link Format#stereoMode}. + * @return The builder. + */ + public Builder setStereoMode(@C.StereoMode int stereoMode) { + this.stereoMode = stereoMode; + return this; + } + + /** + * Sets {@link Format#colorInfo}. The default value is {@code null}. + * + * @param colorInfo The {@link Format#colorInfo}. + * @return The builder. + */ + public Builder setColorInfo(@Nullable ColorInfo colorInfo) { + this.colorInfo = colorInfo; + return this; + } + + // Audio specific. + + /** + * Sets {@link Format#channelCount}. The default value is {@link #NO_VALUE}. + * + * @param channelCount The {@link Format#channelCount}. + * @return The builder. + */ + public Builder setChannelCount(int channelCount) { + this.channelCount = channelCount; + return this; + } + + /** + * Sets {@link Format#sampleRate}. The default value is {@link #NO_VALUE}. + * + * @param sampleRate The {@link Format#sampleRate}. + * @return The builder. + */ + public Builder setSampleRate(int sampleRate) { + this.sampleRate = sampleRate; + return this; + } + + /** + * Sets {@link Format#pcmEncoding}. The default value is {@link #NO_VALUE}. + * + * @param pcmEncoding The {@link Format#pcmEncoding}. + * @return The builder. + */ + public Builder setPcmEncoding(@C.PcmEncoding int pcmEncoding) { + this.pcmEncoding = pcmEncoding; + return this; + } + + /** + * Sets {@link Format#encoderDelay}. The default value is 0. + * + * @param encoderDelay The {@link Format#encoderDelay}. + * @return The builder. + */ + public Builder setEncoderDelay(int encoderDelay) { + this.encoderDelay = encoderDelay; + return this; + } + + /** + * Sets {@link Format#encoderPadding}. The default value is 0. + * + * @param encoderPadding The {@link Format#encoderPadding}. + * @return The builder. + */ + public Builder setEncoderPadding(int encoderPadding) { + this.encoderPadding = encoderPadding; + return this; + } + + // Text specific. + + /** + * Sets {@link Format#accessibilityChannel}. The default value is {@link #NO_VALUE}. + * + * @param accessibilityChannel The {@link Format#accessibilityChannel}. + * @return The builder. + */ + public Builder setAccessibilityChannel(int accessibilityChannel) { + this.accessibilityChannel = accessibilityChannel; + return this; + } + + // Provided by source. + + /** + * Sets {@link Format#exoMediaCryptoType}. The default value is {@code null}. + * + * @param exoMediaCryptoType The {@link Format#exoMediaCryptoType}. + * @return The builder. + */ + public Builder setExoMediaCryptoType( + @Nullable Class exoMediaCryptoType) { + this.exoMediaCryptoType = exoMediaCryptoType; + return this; + } + + // Build. + + public Format build() { + return new Format( + id, + label, + language, + selectionFlags, + roleFlags, + averageBitrate, + peakBitrate, + codecs, + metadata, + containerMimeType, + sampleMimeType, + maxInputSize, + initializationData, + drmInitData, + subsampleOffsetUs, + width, + height, + frameRate, + rotationDegrees, + pixelWidthHeightRatio, + projectionData, + stereoMode, + colorInfo, + channelCount, + sampleRate, + pcmEncoding, + encoderDelay, + encoderPadding, + accessibilityChannel, + exoMediaCryptoType); + } + } + + /** A value for various fields to indicate that the field's value is unknown or not applicable. */ + public static final int NO_VALUE = -1; + + /** + * A value for {@link #subsampleOffsetUs} to indicate that subsample timestamps are relative to + * the timestamps of their parent samples. + */ + public static final long OFFSET_SAMPLE_RELATIVE = Long.MAX_VALUE; + + /** An identifier for the format, or null if unknown or not applicable. */ + @Nullable public final String id; + /** The human readable label, or null if unknown or not applicable. */ + @Nullable public final String label; + /** The language as an IETF BCP 47 conformant tag, or null if unknown or not applicable. */ + @Nullable public final String language; + /** Track selection flags. */ + @C.SelectionFlags public final int selectionFlags; + /** Track role flags. */ + @C.RoleFlags public final int roleFlags; + /** + * The average bitrate in bits per second, or {@link #NO_VALUE} if unknown or not applicable. The + * way in which this field is populated depends on the type of media to which the format + * corresponds: + * + *

    + *
  • DASH representations: Always {@link Format#NO_VALUE}. + *
  • HLS variants: The {@code AVERAGE-BANDWIDTH} attribute defined on the corresponding {@code + * EXT-X-STREAM-INF} tag in the master playlist, or {@link Format#NO_VALUE} if not present. + *
  • SmoothStreaming track elements: The {@code Bitrate} attribute defined on the + * corresponding {@code TrackElement} in the manifest, or {@link Format#NO_VALUE} if not + * present. + *
  • Progressive container formats: Often {@link Format#NO_VALUE}, but may be populated with + * the average bitrate of the container if known. + *
  • Sample formats: Often {@link Format#NO_VALUE}, but may be populated with the average + * bitrate of the stream of samples with type {@link #sampleMimeType} if known. Note that if + * {@link #sampleMimeType} is a compressed format (e.g., {@link MimeTypes#AUDIO_AAC}), then + * this bitrate is for the stream of still compressed samples. + *
+ */ + public final int averageBitrate; + /** + * The peak bitrate in bits per second, or {@link #NO_VALUE} if unknown or not applicable. The way + * in which this field is populated depends on the type of media to which the format corresponds: + * + *
    + *
  • DASH representations: The {@code @bandwidth} attribute of the corresponding {@code + * Representation} element in the manifest. + *
  • HLS variants: The {@code BANDWIDTH} attribute defined on the corresponding {@code + * EXT-X-STREAM-INF} tag. + *
  • SmoothStreaming track elements: Always {@link Format#NO_VALUE}. + *
  • Progressive container formats: Often {@link Format#NO_VALUE}, but may be populated with + * the peak bitrate of the container if known. + *
  • Sample formats: Often {@link Format#NO_VALUE}, but may be populated with the peak bitrate + * of the stream of samples with type {@link #sampleMimeType} if known. Note that if {@link + * #sampleMimeType} is a compressed format (e.g., {@link MimeTypes#AUDIO_AAC}), then this + * bitrate is for the stream of still compressed samples. + *
+ */ + public final int peakBitrate; + /** + * The bitrate in bits per second. This is the peak bitrate if known, or else the average bitrate + * if known, or else {@link Format#NO_VALUE}. Equivalent to: {@code peakBitrate != NO_VALUE ? + * peakBitrate : averageBitrate}. + */ + public final int bitrate; + /** Codecs of the format as described in RFC 6381, or null if unknown or not applicable. */ + @Nullable public final String codecs; + /** Metadata, or null if unknown or not applicable. */ + @Nullable public final Metadata metadata; + + // Container specific. + + /** The mime type of the container, or null if unknown or not applicable. */ + @Nullable public final String containerMimeType; + + // Sample specific. + + /** The sample mime type, or null if unknown or not applicable. */ + @Nullable public final String sampleMimeType; + /** + * The maximum size of a buffer of data (typically one sample), or {@link #NO_VALUE} if unknown or + * not applicable. + */ + public final int maxInputSize; + /** + * Initialization data that must be provided to the decoder. Will not be null, but may be empty + * if initialization data is not required. + */ + public final List initializationData; + /** DRM initialization data if the stream is protected, or null otherwise. */ + @Nullable public final 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. + + /** + * The width of the video in pixels, or {@link #NO_VALUE} if unknown or not applicable. + */ + public final int width; + /** + * The height of the video in pixels, or {@link #NO_VALUE} if unknown or not applicable. + */ + public final int height; + /** + * The frame rate in frames per second, or {@link #NO_VALUE} if unknown or not applicable. + */ + public final float frameRate; + /** + * The clockwise rotation that should be applied to the video for it to be rendered in the correct + * orientation, or 0 if unknown or not applicable. Only 0, 90, 180 and 270 are supported. + */ + public final int rotationDegrees; + /** The width to height ratio of pixels in the video, or 1.0 if unknown or not applicable. */ + public final float pixelWidthHeightRatio; + /** The projection data for 360/VR video, or null if not applicable. */ + @Nullable public final byte[] projectionData; + /** + * The stereo layout for 360/3D/VR video, or {@link #NO_VALUE} if not applicable. Valid stereo + * modes are {@link C#STEREO_MODE_MONO}, {@link C#STEREO_MODE_TOP_BOTTOM}, {@link + * C#STEREO_MODE_LEFT_RIGHT}, {@link C#STEREO_MODE_STEREO_MESH}. + */ + @C.StereoMode public final int stereoMode; + /** The color metadata associated with the video, or null if not applicable. */ + @Nullable public final ColorInfo colorInfo; + + // Audio specific. + + /** + * The number of audio channels, or {@link #NO_VALUE} if unknown or not applicable. + */ + public final int channelCount; + /** + * The audio sampling rate in Hz, or {@link #NO_VALUE} if unknown or not applicable. + */ + public final int sampleRate; + /** The {@link C.PcmEncoding} for PCM audio. Set to {@link #NO_VALUE} for other media types. */ + @C.PcmEncoding public final int pcmEncoding; + /** + * The number of frames to trim from the start of the decoded audio stream, or 0 if not + * applicable. + */ + public final int encoderDelay; + /** + * The number of frames to trim from the end of the decoded audio stream, or 0 if not applicable. + */ + public final int encoderPadding; + + // Text specific. + + /** The Accessibility channel, or {@link #NO_VALUE} if not known or applicable. */ + public final int accessibilityChannel; + + // Provided by source. + + /** + * The type of the {@link ExoMediaCrypto} provided by the media source, if the media source can + * acquire a DRM session for {@link #drmInitData}. Null if the media source cannot acquire a + * session for {@link #drmInitData}, or if not applicable. + */ + @Nullable public final Class exoMediaCryptoType; + + // Lazily initialized hashcode. + private int hashCode; + + // Video. + + /** @deprecated Use {@link Format.Builder}. */ + @Deprecated + public static Format createVideoContainerFormat( + @Nullable String id, + @Nullable String label, + @Nullable String containerMimeType, + @Nullable String sampleMimeType, + @Nullable String codecs, + @Nullable Metadata metadata, + int bitrate, + int width, + int height, + float frameRate, + @Nullable List initializationData, + @C.SelectionFlags int selectionFlags, + @C.RoleFlags int roleFlags) { + return new Builder() + .setId(id) + .setLabel(label) + .setSelectionFlags(selectionFlags) + .setRoleFlags(roleFlags) + .setAverageBitrate(bitrate) + .setPeakBitrate(bitrate) + .setCodecs(codecs) + .setMetadata(metadata) + .setContainerMimeType(containerMimeType) + .setSampleMimeType(sampleMimeType) + .setInitializationData(initializationData) + .setWidth(width) + .setHeight(height) + .setFrameRate(frameRate) + .build(); + } + + /** @deprecated Use {@link Format.Builder}. */ + @Deprecated + public static Format createVideoSampleFormat( + @Nullable String id, + @Nullable String sampleMimeType, + @Nullable String codecs, + int bitrate, + int maxInputSize, + int width, + int height, + float frameRate, + @Nullable List initializationData, + @Nullable DrmInitData drmInitData) { + return new Builder() + .setId(id) + .setAverageBitrate(bitrate) + .setPeakBitrate(bitrate) + .setCodecs(codecs) + .setSampleMimeType(sampleMimeType) + .setMaxInputSize(maxInputSize) + .setInitializationData(initializationData) + .setDrmInitData(drmInitData) + .setWidth(width) + .setHeight(height) + .setFrameRate(frameRate) + .build(); + } + + /** @deprecated Use {@link Format.Builder}. */ + @Deprecated + public static Format createVideoSampleFormat( + @Nullable String id, + @Nullable String sampleMimeType, + @Nullable String codecs, + int bitrate, + int maxInputSize, + int width, + int height, + float frameRate, + @Nullable List initializationData, + int rotationDegrees, + float pixelWidthHeightRatio, + @Nullable DrmInitData drmInitData) { + return new Builder() + .setId(id) + .setAverageBitrate(bitrate) + .setPeakBitrate(bitrate) + .setCodecs(codecs) + .setSampleMimeType(sampleMimeType) + .setMaxInputSize(maxInputSize) + .setInitializationData(initializationData) + .setDrmInitData(drmInitData) + .setWidth(width) + .setHeight(height) + .setFrameRate(frameRate) + .setRotationDegrees(rotationDegrees) + .setPixelWidthHeightRatio(pixelWidthHeightRatio) + .build(); + } + + /** @deprecated Use {@link Format.Builder}. */ + @Deprecated + public static Format createVideoSampleFormat( + @Nullable String id, + @Nullable String sampleMimeType, + @Nullable String codecs, + int bitrate, + int maxInputSize, + int width, + int height, + float frameRate, + @Nullable List initializationData, + int rotationDegrees, + float pixelWidthHeightRatio, + @Nullable byte[] projectionData, + @C.StereoMode int stereoMode, + @Nullable ColorInfo colorInfo, + @Nullable DrmInitData drmInitData) { + return new Builder() + .setId(id) + .setAverageBitrate(bitrate) + .setPeakBitrate(bitrate) + .setCodecs(codecs) + .setSampleMimeType(sampleMimeType) + .setMaxInputSize(maxInputSize) + .setInitializationData(initializationData) + .setDrmInitData(drmInitData) + .setWidth(width) + .setHeight(height) + .setFrameRate(frameRate) + .setRotationDegrees(rotationDegrees) + .setPixelWidthHeightRatio(pixelWidthHeightRatio) + .setProjectionData(projectionData) + .setStereoMode(stereoMode) + .setColorInfo(colorInfo) + .build(); + } + + // Audio. + + /** @deprecated Use {@link Format.Builder}. */ + @Deprecated + public static Format createAudioContainerFormat( + @Nullable String id, + @Nullable String label, + @Nullable String containerMimeType, + @Nullable String sampleMimeType, + @Nullable String codecs, + @Nullable Metadata metadata, + int bitrate, + int channelCount, + int sampleRate, + @Nullable List initializationData, + @C.SelectionFlags int selectionFlags, + @C.RoleFlags int roleFlags, + @Nullable String language) { + return new Builder() + .setId(id) + .setLabel(label) + .setLanguage(language) + .setSelectionFlags(selectionFlags) + .setRoleFlags(roleFlags) + .setAverageBitrate(bitrate) + .setPeakBitrate(bitrate) + .setCodecs(codecs) + .setMetadata(metadata) + .setContainerMimeType(containerMimeType) + .setSampleMimeType(sampleMimeType) + .setInitializationData(initializationData) + .setChannelCount(channelCount) + .setSampleRate(sampleRate) + .build(); + } + + /** @deprecated Use {@link Format.Builder}. */ + @Deprecated + public static Format createAudioSampleFormat( + @Nullable String id, + @Nullable String sampleMimeType, + @Nullable String codecs, + int bitrate, + int maxInputSize, + int channelCount, + int sampleRate, + @Nullable List initializationData, + @Nullable DrmInitData drmInitData, + @C.SelectionFlags int selectionFlags, + @Nullable String language) { + return new Builder() + .setId(id) + .setLanguage(language) + .setSelectionFlags(selectionFlags) + .setAverageBitrate(bitrate) + .setPeakBitrate(bitrate) + .setCodecs(codecs) + .setSampleMimeType(sampleMimeType) + .setMaxInputSize(maxInputSize) + .setInitializationData(initializationData) + .setDrmInitData(drmInitData) + .setChannelCount(channelCount) + .setSampleRate(sampleRate) + .build(); + } + + /** @deprecated Use {@link Format.Builder}. */ + @Deprecated + public static Format createAudioSampleFormat( + @Nullable String id, + @Nullable String sampleMimeType, + @Nullable String codecs, + int bitrate, + int maxInputSize, + int channelCount, + int sampleRate, + @C.PcmEncoding int pcmEncoding, + @Nullable List initializationData, + @Nullable DrmInitData drmInitData, + @C.SelectionFlags int selectionFlags, + @Nullable String language) { + return new Builder() + .setId(id) + .setLanguage(language) + .setSelectionFlags(selectionFlags) + .setAverageBitrate(bitrate) + .setPeakBitrate(bitrate) + .setCodecs(codecs) + .setSampleMimeType(sampleMimeType) + .setMaxInputSize(maxInputSize) + .setInitializationData(initializationData) + .setDrmInitData(drmInitData) + .setChannelCount(channelCount) + .setSampleRate(sampleRate) + .setPcmEncoding(pcmEncoding) + .build(); + } + + /** @deprecated Use {@link Format.Builder}. */ + @Deprecated + public static Format createAudioSampleFormat( + @Nullable String id, + @Nullable String sampleMimeType, + @Nullable String codecs, + int bitrate, + int maxInputSize, + int channelCount, + int sampleRate, + @C.PcmEncoding int pcmEncoding, + int encoderDelay, + int encoderPadding, + @Nullable List initializationData, + @Nullable DrmInitData drmInitData, + @C.SelectionFlags int selectionFlags, + @Nullable String language, + @Nullable Metadata metadata) { + return new Builder() + .setId(id) + .setLanguage(language) + .setSelectionFlags(selectionFlags) + .setAverageBitrate(bitrate) + .setPeakBitrate(bitrate) + .setCodecs(codecs) + .setMetadata(metadata) + .setSampleMimeType(sampleMimeType) + .setMaxInputSize(maxInputSize) + .setInitializationData(initializationData) + .setDrmInitData(drmInitData) + .setChannelCount(channelCount) + .setSampleRate(sampleRate) + .setPcmEncoding(pcmEncoding) + .setEncoderDelay(encoderDelay) + .setEncoderPadding(encoderPadding) + .build(); + } + + // Text. + + /** @deprecated Use {@link Format.Builder}. */ + @Deprecated + 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, + @C.RoleFlags int roleFlags, + @Nullable String language) { + return new Builder() + .setId(id) + .setLabel(label) + .setLanguage(language) + .setSelectionFlags(selectionFlags) + .setRoleFlags(roleFlags) + .setAverageBitrate(bitrate) + .setPeakBitrate(bitrate) + .setCodecs(codecs) + .setContainerMimeType(containerMimeType) + .setSampleMimeType(sampleMimeType) + .build(); + } + + /** @deprecated Use {@link Format.Builder}. */ + @Deprecated + 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, + @C.RoleFlags int roleFlags, + @Nullable String language, + int accessibilityChannel) { + return new Builder() + .setId(id) + .setLabel(label) + .setLanguage(language) + .setSelectionFlags(selectionFlags) + .setRoleFlags(roleFlags) + .setAverageBitrate(bitrate) + .setPeakBitrate(bitrate) + .setCodecs(codecs) + .setContainerMimeType(containerMimeType) + .setSampleMimeType(sampleMimeType) + .setAccessibilityChannel(accessibilityChannel) + .build(); + } + + /** @deprecated Use {@link Format.Builder}. */ + @Deprecated + public static Format createTextSampleFormat( + @Nullable String id, + @Nullable String sampleMimeType, + @C.SelectionFlags int selectionFlags, + @Nullable String language) { + return new Builder() + .setId(id) + .setLanguage(language) + .setSelectionFlags(selectionFlags) + .setSampleMimeType(sampleMimeType) + .build(); + } + + /** @deprecated Use {@link Format.Builder}. */ + @Deprecated + public static Format createTextSampleFormat( + @Nullable String id, + @Nullable String sampleMimeType, + @C.SelectionFlags int selectionFlags, + @Nullable String language, + int accessibilityChannel, + long subsampleOffsetUs, + @Nullable List initializationData) { + return new Builder() + .setId(id) + .setLanguage(language) + .setSelectionFlags(selectionFlags) + .setSampleMimeType(sampleMimeType) + .setInitializationData(initializationData) + .setSubsampleOffsetUs(subsampleOffsetUs) + .setAccessibilityChannel(accessibilityChannel) + .build(); + } + + // Image. + + /** @deprecated Use {@link Format.Builder}. */ + @Deprecated + public static Format createImageSampleFormat( + @Nullable String id, + @Nullable String sampleMimeType, + @C.SelectionFlags int selectionFlags, + @Nullable List initializationData, + @Nullable String language) { + return new Builder() + .setId(id) + .setLanguage(language) + .setSelectionFlags(selectionFlags) + .setSampleMimeType(sampleMimeType) + .setInitializationData(initializationData) + .build(); + } + + // Generic. + + /** @deprecated Use {@link Format.Builder}. */ + @Deprecated + 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, + @C.RoleFlags int roleFlags, + @Nullable String language) { + return new Builder() + .setId(id) + .setLabel(label) + .setLanguage(language) + .setSelectionFlags(selectionFlags) + .setRoleFlags(roleFlags) + .setAverageBitrate(bitrate) + .setPeakBitrate(bitrate) + .setCodecs(codecs) + .setContainerMimeType(containerMimeType) + .setSampleMimeType(sampleMimeType) + .build(); + } + + /** @deprecated Use {@link Format.Builder}. */ + @Deprecated + public static Format createSampleFormat(@Nullable String id, @Nullable String sampleMimeType) { + return new Builder().setId(id).setSampleMimeType(sampleMimeType).build(); + } + + /* package */ Format( + @Nullable String id, + @Nullable String label, + @Nullable String language, + @C.SelectionFlags int selectionFlags, + @C.RoleFlags int roleFlags, + int averageBitrate, + int peakBitrate, + @Nullable String codecs, + @Nullable Metadata metadata, + // Container specific. + @Nullable String containerMimeType, + // Sample specific. + @Nullable String sampleMimeType, + int maxInputSize, + @Nullable List initializationData, + @Nullable DrmInitData drmInitData, + long subsampleOffsetUs, + // Video specific. + int width, + int height, + float frameRate, + int rotationDegrees, + float pixelWidthHeightRatio, + @Nullable byte[] projectionData, + @C.StereoMode int stereoMode, + @Nullable ColorInfo colorInfo, + // Audio specific. + int channelCount, + int sampleRate, + @C.PcmEncoding int pcmEncoding, + int encoderDelay, + int encoderPadding, + // Text specific. + int accessibilityChannel, + // Provided by source. + @Nullable Class exoMediaCryptoType) { + this.id = id; + this.label = label; + this.language = Util.normalizeLanguageCode(language); + this.selectionFlags = selectionFlags; + this.roleFlags = roleFlags; + this.averageBitrate = averageBitrate; + this.peakBitrate = peakBitrate; + this.bitrate = peakBitrate != NO_VALUE ? peakBitrate : averageBitrate; + this.codecs = codecs; + this.metadata = metadata; + // Container specific. + this.containerMimeType = containerMimeType; + // Sample specific. + this.sampleMimeType = sampleMimeType; + this.maxInputSize = maxInputSize; + this.initializationData = + initializationData == null ? Collections.emptyList() : initializationData; + this.drmInitData = drmInitData; + this.subsampleOffsetUs = subsampleOffsetUs; + // Video specific. + this.width = width; + this.height = height; + this.frameRate = frameRate; + this.rotationDegrees = rotationDegrees == NO_VALUE ? 0 : rotationDegrees; + this.pixelWidthHeightRatio = pixelWidthHeightRatio == NO_VALUE ? 1 : pixelWidthHeightRatio; + this.projectionData = projectionData; + this.stereoMode = stereoMode; + this.colorInfo = colorInfo; + // Audio specific. + this.channelCount = channelCount; + this.sampleRate = sampleRate; + this.pcmEncoding = pcmEncoding; + this.encoderDelay = encoderDelay == NO_VALUE ? 0 : encoderDelay; + this.encoderPadding = encoderPadding == NO_VALUE ? 0 : encoderPadding; + // Text specific. + this.accessibilityChannel = accessibilityChannel; + // Provided by source. + this.exoMediaCryptoType = exoMediaCryptoType; + } + + @SuppressWarnings("ResourceType") + /* package */ Format(Parcel in) { + id = in.readString(); + label = in.readString(); + language = in.readString(); + selectionFlags = in.readInt(); + roleFlags = in.readInt(); + averageBitrate = in.readInt(); + peakBitrate = in.readInt(); + bitrate = peakBitrate != NO_VALUE ? peakBitrate : averageBitrate; + codecs = in.readString(); + metadata = in.readParcelable(Metadata.class.getClassLoader()); + // Container specific. + containerMimeType = in.readString(); + // Sample specific. + sampleMimeType = in.readString(); + maxInputSize = in.readInt(); + int initializationDataSize = in.readInt(); + initializationData = new ArrayList<>(initializationDataSize); + for (int i = 0; i < initializationDataSize; i++) { + initializationData.add(in.createByteArray()); + } + drmInitData = in.readParcelable(DrmInitData.class.getClassLoader()); + subsampleOffsetUs = in.readLong(); + // Video specific. + width = in.readInt(); + height = in.readInt(); + frameRate = in.readFloat(); + rotationDegrees = in.readInt(); + pixelWidthHeightRatio = in.readFloat(); + boolean hasProjectionData = Util.readBoolean(in); + projectionData = hasProjectionData ? in.createByteArray() : null; + stereoMode = in.readInt(); + colorInfo = in.readParcelable(ColorInfo.class.getClassLoader()); + // Audio specific. + channelCount = in.readInt(); + sampleRate = in.readInt(); + pcmEncoding = in.readInt(); + encoderDelay = in.readInt(); + encoderPadding = in.readInt(); + // Text specific. + accessibilityChannel = in.readInt(); + // Provided by source. + exoMediaCryptoType = null; + } + + /** Returns a {@link Format.Builder} initialized with the values of this instance. */ + public Builder buildUpon() { + return new Builder(this); + } + + /** @deprecated Use {@link #buildUpon()} and {@link Builder#setMaxInputSize(int)}. */ + @Deprecated + public Format copyWithMaxInputSize(int maxInputSize) { + return buildUpon().setMaxInputSize(maxInputSize).build(); + } + + /** @deprecated Use {@link #buildUpon()} and {@link Builder#setSubsampleOffsetUs(long)}. */ + @Deprecated + public Format copyWithSubsampleOffsetUs(long subsampleOffsetUs) { + return buildUpon().setSubsampleOffsetUs(subsampleOffsetUs).build(); + } + + /** @deprecated Use {@link #buildUpon()} and {@link Builder#setLabel(String)} . */ + @Deprecated + public Format copyWithLabel(@Nullable String label) { + return buildUpon().setLabel(label).build(); + } + + /** @deprecated Use {@link #withManifestFormatInfo(Format)}. */ + @Deprecated + public Format copyWithManifestFormatInfo(Format manifestFormat) { + return withManifestFormatInfo(manifestFormat); + } + + @SuppressWarnings("ReferenceEquality") + public Format withManifestFormatInfo(Format manifestFormat) { + if (this == manifestFormat) { + // No need to copy from ourselves. + return this; + } + + int trackType = MimeTypes.getTrackType(sampleMimeType); + + // Use manifest value only. + @Nullable String id = manifestFormat.id; + + // Prefer manifest values, but fill in from sample format if missing. + @Nullable String label = manifestFormat.label != null ? manifestFormat.label : this.label; + @Nullable 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 averageBitrate = + this.averageBitrate == NO_VALUE ? manifestFormat.averageBitrate : this.averageBitrate; + int peakBitrate = this.peakBitrate == NO_VALUE ? manifestFormat.peakBitrate : this.peakBitrate; + @Nullable 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. + @Nullable String codecsOfType = Util.getCodecsOfType(manifestFormat.codecs, trackType); + if (Util.splitCodecs(codecsOfType).length == 1) { + codecs = codecsOfType; + } + } + + @Nullable + Metadata metadata = + this.metadata == null + ? manifestFormat.metadata + : this.metadata.copyWithAppendedEntriesFrom(manifestFormat.metadata); + + 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; + @C.RoleFlags int roleFlags = this.roleFlags | manifestFormat.roleFlags; + @Nullable + DrmInitData drmInitData = + DrmInitData.createSessionCreationData(manifestFormat.drmInitData, this.drmInitData); + + return buildUpon() + .setId(id) + .setLabel(label) + .setLanguage(language) + .setSelectionFlags(selectionFlags) + .setRoleFlags(roleFlags) + .setAverageBitrate(averageBitrate) + .setPeakBitrate(peakBitrate) + .setCodecs(codecs) + .setMetadata(metadata) + .setDrmInitData(drmInitData) + .setFrameRate(frameRate) + .build(); + } + + /** + * @deprecated Use {@link #buildUpon()}, {@link Builder#setEncoderDelay(int)} and {@link + * Builder#setEncoderPadding(int)}. + */ + @Deprecated + public Format copyWithGaplessInfo(int encoderDelay, int encoderPadding) { + return buildUpon().setEncoderDelay(encoderDelay).setEncoderPadding(encoderPadding).build(); + } + + /** @deprecated Use {@link #buildUpon()} and {@link Builder#setFrameRate(float)}. */ + @Deprecated + public Format copyWithFrameRate(float frameRate) { + return buildUpon().setFrameRate(frameRate).build(); + } + + /** @deprecated Use {@link #buildUpon()} and {@link Builder#setDrmInitData(DrmInitData)}. */ + @Deprecated + public Format copyWithDrmInitData(@Nullable DrmInitData drmInitData) { + return buildUpon().setDrmInitData(drmInitData).build(); + } + + /** @deprecated Use {@link #buildUpon()} and {@link Builder#setMetadata(Metadata)}. */ + @Deprecated + public Format copyWithMetadata(@Nullable Metadata metadata) { + return buildUpon().setMetadata(metadata).build(); + } + + /** + * @deprecated Use {@link #buildUpon()} and {@link Builder#setAverageBitrate(int)} and {@link + * Builder#setPeakBitrate(int)}. + */ + @Deprecated + public Format copyWithBitrate(int bitrate) { + return buildUpon().setAverageBitrate(bitrate).setPeakBitrate(bitrate).build(); + } + + /** + * @deprecated Use {@link #buildUpon()}, {@link Builder#setWidth(int)} and {@link + * Builder#setHeight(int)}. + */ + @Deprecated + public Format copyWithVideoSize(int width, int height) { + return buildUpon().setWidth(width).setHeight(height).build(); + } + + /** Returns a copy of this format with the specified {@link #exoMediaCryptoType}. */ + public Format copyWithExoMediaCryptoType( + @Nullable Class exoMediaCryptoType) { + return buildUpon().setExoMediaCryptoType(exoMediaCryptoType).build(); + } + + /** + * Returns the number of pixels if this is a video format whose {@link #width} and {@link #height} + * are known, or {@link #NO_VALUE} otherwise + */ + public int getPixelCount() { + return width == NO_VALUE || height == NO_VALUE ? NO_VALUE : (width * height); + } + + @Override + public String toString() { + return "Format(" + + id + + ", " + + label + + ", " + + containerMimeType + + ", " + + sampleMimeType + + ", " + + codecs + + ", " + + bitrate + + ", " + + language + + ", [" + + width + + ", " + + height + + ", " + + frameRate + + "]" + + ", [" + + channelCount + + ", " + + sampleRate + + "])"; + } + + @Override + public int hashCode() { + if (hashCode == 0) { + // Some fields for which hashing is expensive are deliberately omitted. + int result = 17; + result = 31 * result + (id == null ? 0 : id.hashCode()); + result = 31 * result + (label != null ? label.hashCode() : 0); + result = 31 * result + (language == null ? 0 : language.hashCode()); + result = 31 * result + selectionFlags; + result = 31 * result + roleFlags; + result = 31 * result + averageBitrate; + result = 31 * result + peakBitrate; + result = 31 * result + (codecs == null ? 0 : codecs.hashCode()); + result = 31 * result + (metadata == null ? 0 : metadata.hashCode()); + // Container specific. + result = 31 * result + (containerMimeType == null ? 0 : containerMimeType.hashCode()); + // Sample specific. + result = 31 * result + (sampleMimeType == null ? 0 : sampleMimeType.hashCode()); + result = 31 * result + maxInputSize; + // [Omitted] initializationData. + // [Omitted] drmInitData. + result = 31 * result + (int) subsampleOffsetUs; + // Video specific. + result = 31 * result + width; + result = 31 * result + height; + result = 31 * result + Float.floatToIntBits(frameRate); + result = 31 * result + rotationDegrees; + result = 31 * result + Float.floatToIntBits(pixelWidthHeightRatio); + // [Omitted] projectionData. + result = 31 * result + stereoMode; + // [Omitted] colorInfo. + // Audio specific. + result = 31 * result + channelCount; + result = 31 * result + sampleRate; + result = 31 * result + pcmEncoding; + result = 31 * result + encoderDelay; + result = 31 * result + encoderPadding; + // Text specific. + result = 31 * result + accessibilityChannel; + // Provided by source. + result = 31 * result + (exoMediaCryptoType == null ? 0 : exoMediaCryptoType.hashCode()); + hashCode = result; + } + return hashCode; + } + + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + Format other = (Format) obj; + if (hashCode != 0 && other.hashCode != 0 && hashCode != other.hashCode) { + return false; + } + // Field equality checks ordered by type, with the cheapest checks first. + return selectionFlags == other.selectionFlags + && roleFlags == other.roleFlags + && averageBitrate == other.averageBitrate + && peakBitrate == other.peakBitrate + && maxInputSize == other.maxInputSize + && subsampleOffsetUs == other.subsampleOffsetUs + && width == other.width + && height == other.height + && rotationDegrees == other.rotationDegrees + && stereoMode == other.stereoMode + && channelCount == other.channelCount + && sampleRate == other.sampleRate + && pcmEncoding == other.pcmEncoding + && encoderDelay == other.encoderDelay + && encoderPadding == other.encoderPadding + && accessibilityChannel == other.accessibilityChannel + && Float.compare(frameRate, other.frameRate) == 0 + && Float.compare(pixelWidthHeightRatio, other.pixelWidthHeightRatio) == 0 + && Util.areEqual(exoMediaCryptoType, other.exoMediaCryptoType) + && Util.areEqual(id, other.id) + && Util.areEqual(label, other.label) + && Util.areEqual(codecs, other.codecs) + && Util.areEqual(containerMimeType, other.containerMimeType) + && Util.areEqual(sampleMimeType, other.sampleMimeType) + && Util.areEqual(language, other.language) + && Arrays.equals(projectionData, other.projectionData) + && Util.areEqual(metadata, other.metadata) + && Util.areEqual(colorInfo, other.colorInfo) + && Util.areEqual(drmInitData, other.drmInitData) + && initializationDataEquals(other); + } + + /** + * Returns whether the {@link #initializationData}s belonging to this format and {@code other} are + * equal. + * + * @param other The other format whose {@link #initializationData} is being compared. + * @return Whether the {@link #initializationData}s belonging to this format and {@code other} are + * equal. + */ + public boolean initializationDataEquals(Format other) { + if (initializationData.size() != other.initializationData.size()) { + return false; + } + for (int i = 0; i < initializationData.size(); i++) { + if (!Arrays.equals(initializationData.get(i), other.initializationData.get(i))) { + return false; + } + } + return true; + } + + // Utility methods + + /** Returns a prettier {@link String} than {@link #toString()}, intended for logging. */ + public static String toLogString(@Nullable Format format) { + if (format == null) { + return "null"; + } + StringBuilder builder = new StringBuilder(); + builder.append("id=").append(format.id).append(", mimeType=").append(format.sampleMimeType); + if (format.bitrate != NO_VALUE) { + builder.append(", bitrate=").append(format.bitrate); + } + if (format.codecs != null) { + builder.append(", codecs=").append(format.codecs); + } + if (format.width != NO_VALUE && format.height != NO_VALUE) { + builder.append(", res=").append(format.width).append("x").append(format.height); + } + if (format.frameRate != NO_VALUE) { + builder.append(", fps=").append(format.frameRate); + } + if (format.channelCount != NO_VALUE) { + builder.append(", channels=").append(format.channelCount); + } + if (format.sampleRate != NO_VALUE) { + builder.append(", sample_rate=").append(format.sampleRate); + } + if (format.language != null) { + builder.append(", language=").append(format.language); + } + if (format.label != null) { + builder.append(", label=").append(format.label); + } + return builder.toString(); + } + + // Parcelable implementation. + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(id); + dest.writeString(label); + dest.writeString(language); + dest.writeInt(selectionFlags); + dest.writeInt(roleFlags); + dest.writeInt(averageBitrate); + dest.writeInt(peakBitrate); + dest.writeString(codecs); + dest.writeParcelable(metadata, 0); + // Container specific. + dest.writeString(containerMimeType); + // Sample specific. + dest.writeString(sampleMimeType); + dest.writeInt(maxInputSize); + int initializationDataSize = initializationData.size(); + dest.writeInt(initializationDataSize); + for (int i = 0; i < initializationDataSize; i++) { + dest.writeByteArray(initializationData.get(i)); + } + dest.writeParcelable(drmInitData, 0); + dest.writeLong(subsampleOffsetUs); + // Video specific. + dest.writeInt(width); + dest.writeInt(height); + dest.writeFloat(frameRate); + dest.writeInt(rotationDegrees); + dest.writeFloat(pixelWidthHeightRatio); + Util.writeBoolean(dest, projectionData != null); + if (projectionData != null) { + dest.writeByteArray(projectionData); + } + dest.writeInt(stereoMode); + dest.writeParcelable(colorInfo, flags); + // Audio specific. + dest.writeInt(channelCount); + dest.writeInt(sampleRate); + dest.writeInt(pcmEncoding); + dest.writeInt(encoderDelay); + dest.writeInt(encoderPadding); + // Text specific. + dest.writeInt(accessibilityChannel); + } + + public static final Creator CREATOR = new Creator() { + + @Override + public Format createFromParcel(Parcel in) { + return new Format(in); + } + + @Override + public Format[] newArray(int size) { + return new Format[size]; + } + + }; +} diff --git a/library/common/src/main/java/com/google/android/exoplayer2/MediaItem.java b/library/common/src/main/java/com/google/android/exoplayer2/MediaItem.java new file mode 100644 index 0000000000..15ec1bb227 --- /dev/null +++ b/library/common/src/main/java/com/google/android/exoplayer2/MediaItem.java @@ -0,0 +1,814 @@ +/* + * Copyright 2020 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; + +import android.net.Uri; +import androidx.annotation.Nullable; +import com.google.android.exoplayer2.offline.StreamKey; +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.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +/** Representation of a media item. */ +public final class MediaItem { + + /** + * Creates a {@link MediaItem} for the given uri. + * + * @param uri The uri. + * @return An {@link MediaItem} for the given uri. + */ + public static MediaItem fromUri(String uri) { + return new MediaItem.Builder().setUri(uri).build(); + } + + /** + * Creates a {@link MediaItem} for the given {@link Uri uri}. + * + * @param uri The {@link Uri uri}. + * @return An {@link MediaItem} for the given uri. + */ + public static MediaItem fromUri(Uri uri) { + return new MediaItem.Builder().setUri(uri).build(); + } + + /** A builder for {@link MediaItem} instances. */ + public static final class Builder { + + @Nullable private String mediaId; + @Nullable private Uri uri; + @Nullable private String mimeType; + private long clipStartPositionMs; + private long clipEndPositionMs; + private boolean clipRelativeToLiveWindow; + private boolean clipRelativeToDefaultPosition; + private boolean clipStartsAtKeyFrame; + @Nullable private Uri drmLicenseUri; + private Map drmLicenseRequestHeaders; + @Nullable private UUID drmUuid; + private boolean drmMultiSession; + private boolean drmPlayClearContentWithoutKey; + private List drmSessionForClearTypes; + @Nullable private byte[] drmKeySetId; + private List streamKeys; + @Nullable private String customCacheKey; + private List subtitles; + @Nullable private Uri adTagUri; + @Nullable private Object tag; + @Nullable private MediaMetadata mediaMetadata; + + /** Creates a builder. */ + public Builder() { + clipEndPositionMs = C.TIME_END_OF_SOURCE; + drmSessionForClearTypes = Collections.emptyList(); + drmLicenseRequestHeaders = Collections.emptyMap(); + streamKeys = Collections.emptyList(); + subtitles = Collections.emptyList(); + } + + private Builder(MediaItem mediaItem) { + this(); + clipEndPositionMs = mediaItem.clippingProperties.endPositionMs; + clipRelativeToLiveWindow = mediaItem.clippingProperties.relativeToLiveWindow; + clipRelativeToDefaultPosition = mediaItem.clippingProperties.relativeToDefaultPosition; + clipStartPositionMs = mediaItem.clippingProperties.startPositionMs; + clipStartsAtKeyFrame = mediaItem.clippingProperties.startsAtKeyFrame; + mediaId = mediaItem.mediaId; + mediaMetadata = mediaItem.mediaMetadata; + @Nullable PlaybackProperties playbackProperties = mediaItem.playbackProperties; + if (playbackProperties != null) { + adTagUri = playbackProperties.adTagUri; + customCacheKey = playbackProperties.customCacheKey; + mimeType = playbackProperties.mimeType; + uri = playbackProperties.uri; + streamKeys = playbackProperties.streamKeys; + subtitles = playbackProperties.subtitles; + tag = playbackProperties.tag; + @Nullable DrmConfiguration drmConfiguration = playbackProperties.drmConfiguration; + if (drmConfiguration != null) { + drmLicenseUri = drmConfiguration.licenseUri; + drmLicenseRequestHeaders = drmConfiguration.requestHeaders; + drmMultiSession = drmConfiguration.multiSession; + drmPlayClearContentWithoutKey = drmConfiguration.playClearContentWithoutKey; + drmSessionForClearTypes = drmConfiguration.sessionForClearTypes; + drmUuid = drmConfiguration.uuid; + drmKeySetId = drmConfiguration.getKeySetId(); + } + } + } + + /** + * Sets the optional media id which identifies the media item. If not specified, {@link #setUri} + * must be called and the string representation of {@link PlaybackProperties#uri} is used as the + * media id. + */ + public Builder setMediaId(@Nullable String mediaId) { + this.mediaId = mediaId; + return this; + } + + /** Sets the optional uri. If not specified, {@link #setMediaId(String)} must be called. */ + public Builder setUri(@Nullable String uri) { + return setUri(uri == null ? null : Uri.parse(uri)); + } + + /** + * Sets the optional {@link Uri}. If not specified, {@link #setMediaId(String)} must be called. + */ + public Builder setUri(@Nullable Uri uri) { + this.uri = uri; + return this; + } + + /** + * Sets the optional mime type. + * + *

The mime type may be used as a hint for inferring the type of the media item. + * + *

If a {@link PlaybackProperties#uri} is set, the mime type is used to create a {@link + * PlaybackProperties} object. Otherwise it will be ignored. + * + * @param mimeType The mime type. + */ + public Builder setMimeType(@Nullable String mimeType) { + this.mimeType = mimeType; + return this; + } + + /** + * Sets the optional start position in milliseconds which must be a value larger than or equal + * to zero (Default: 0). + */ + public Builder setClipStartPositionMs(long startPositionMs) { + Assertions.checkArgument(startPositionMs >= 0); + this.clipStartPositionMs = startPositionMs; + return this; + } + + /** + * Sets the optional end position in milliseconds which must be a value larger than or equal to + * zero, or {@link C#TIME_END_OF_SOURCE} to end when playback reaches the end of media (Default: + * {@link C#TIME_END_OF_SOURCE}). + */ + public Builder setClipEndPositionMs(long endPositionMs) { + Assertions.checkArgument(endPositionMs == C.TIME_END_OF_SOURCE || endPositionMs >= 0); + this.clipEndPositionMs = endPositionMs; + return this; + } + + /** + * Sets whether the start/end positions should move with the live window for live streams. If + * {@code false}, live streams end when playback reaches the end position in live window seen + * when the media is first loaded (Default: {@code false}). + */ + public Builder setClipRelativeToLiveWindow(boolean relativeToLiveWindow) { + this.clipRelativeToLiveWindow = relativeToLiveWindow; + return this; + } + + /** + * Sets whether the start position and the end position are relative to the default position in + * the window (Default: {@code false}). + */ + public Builder setClipRelativeToDefaultPosition(boolean relativeToDefaultPosition) { + this.clipRelativeToDefaultPosition = relativeToDefaultPosition; + return this; + } + + /** + * Sets whether the start point is guaranteed to be a key frame. If {@code false}, the playback + * transition into the clip may not be seamless (Default: {@code false}). + */ + public Builder setClipStartsAtKeyFrame(boolean startsAtKeyFrame) { + this.clipStartsAtKeyFrame = startsAtKeyFrame; + return this; + } + + /** + * Sets the optional license server {@link Uri}. If a license uri is set, the {@link + * DrmConfiguration#uuid} needs to be specified as well. + * + *

If a {@link PlaybackProperties#uri} is set, the drm license uri is used to create a {@link + * PlaybackProperties} object. Otherwise it will be ignored. + */ + public Builder setDrmLicenseUri(@Nullable Uri licenseUri) { + drmLicenseUri = licenseUri; + return this; + } + + /** + * Sets the optional license server uri as a {@link String}. If a license uri is set, the {@link + * DrmConfiguration#uuid} needs to be specified as well. + * + *

If a {@link PlaybackProperties#uri} is set, the drm license uri is used to create a {@link + * PlaybackProperties} object. Otherwise it will be ignored. + */ + public Builder setDrmLicenseUri(@Nullable String licenseUri) { + drmLicenseUri = licenseUri == null ? null : Uri.parse(licenseUri); + return this; + } + + /** + * Sets the optional request headers attached to the drm license request. + * + *

{@code null} or an empty {@link Map} can be used for a reset. + * + *

If no valid drm configuration is specified, the drm license request headers are ignored. + */ + public Builder setDrmLicenseRequestHeaders( + @Nullable Map licenseRequestHeaders) { + this.drmLicenseRequestHeaders = + licenseRequestHeaders != null && !licenseRequestHeaders.isEmpty() + ? Collections.unmodifiableMap(new HashMap<>(licenseRequestHeaders)) + : Collections.emptyMap(); + return this; + } + + /** + * Sets the {@link UUID} of the protection scheme. If a drm system uuid is set, the {@link + * DrmConfiguration#licenseUri} needs to be set as well. + * + *

If a {@link PlaybackProperties#uri} is set, the drm system uuid is used to create a {@link + * PlaybackProperties} object. Otherwise it will be ignored. + */ + public Builder setDrmUuid(@Nullable UUID uuid) { + drmUuid = uuid; + return this; + } + + /** + * Sets whether the drm configuration is multi session enabled. + * + *

If a {@link PlaybackProperties#uri} is set, the drm multi session flag is used to create a + * {@link PlaybackProperties} object. Otherwise it will be ignored. + */ + public Builder setDrmMultiSession(boolean multiSession) { + drmMultiSession = multiSession; + return this; + } + + /** + * Sets whether clear samples within protected content should be played when keys for the + * encrypted part of the content have yet to be loaded. + */ + public Builder setDrmPlayClearContentWithoutKey(boolean playClearContentWithoutKey) { + this.drmPlayClearContentWithoutKey = playClearContentWithoutKey; + return this; + } + + /** + * Sets whether a drm session should be used for clear tracks of type {@link C#TRACK_TYPE_VIDEO} + * and {@link C#TRACK_TYPE_AUDIO}. + * + *

This method overrides what has been set by previously calling {@link + * #setDrmSessionForClearTypes(List)}. + */ + public Builder setDrmSessionForClearPeriods(boolean sessionForClearPeriods) { + this.setDrmSessionForClearTypes( + sessionForClearPeriods + ? Arrays.asList(C.TRACK_TYPE_VIDEO, C.TRACK_TYPE_AUDIO) + : Collections.emptyList()); + return this; + } + + /** + * Sets a list of {@link C}{@code .TRACK_TYPE_*} constants for which to use a drm session even + * when the tracks are in the clear. + * + *

For the common case of using a drm session for {@link C#TRACK_TYPE_VIDEO} and {@link + * C#TRACK_TYPE_AUDIO} the {@link #setDrmSessionForClearPeriods(boolean)} can be used. + * + *

This method overrides what has been set by previously calling {@link + * #setDrmSessionForClearPeriods(boolean)}. + * + *

{@code null} or an empty {@link List} can be used for a reset. + */ + public Builder setDrmSessionForClearTypes(@Nullable List sessionForClearTypes) { + this.drmSessionForClearTypes = + sessionForClearTypes != null && !sessionForClearTypes.isEmpty() + ? Collections.unmodifiableList(new ArrayList<>(sessionForClearTypes)) + : Collections.emptyList(); + return this; + } + + /** + * Sets the key set ID of the offline license. + * + *

The key set ID identifies an offline license. The ID is required to query, renew or + * release an existing offline license (see {@code DefaultDrmSessionManager#setMode(int + * mode,byte[] offlineLicenseKeySetId)}). + * + *

If no valid DRM configuration is specified, the key set ID is ignored. + */ + public Builder setDrmKeySetId(@Nullable byte[] keySetId) { + this.drmKeySetId = keySetId != null ? Arrays.copyOf(keySetId, keySetId.length) : null; + return this; + } + + /** + * Sets the optional stream keys by which the manifest is filtered (only used for adaptive + * streams). + * + *

{@code null} or an empty {@link List} can be used for a reset. + * + *

If a {@link PlaybackProperties#uri} is set, the stream keys are used to create a {@link + * PlaybackProperties} object. Otherwise it will be ignored. + */ + public Builder setStreamKeys(@Nullable List streamKeys) { + this.streamKeys = + streamKeys != null && !streamKeys.isEmpty() + ? Collections.unmodifiableList(new ArrayList<>(streamKeys)) + : Collections.emptyList(); + return this; + } + + /** + * Sets the optional custom cache key (only used for progressive streams). + * + *

If a {@link PlaybackProperties#uri} is set, the custom cache key is used to create a + * {@link PlaybackProperties} object. Otherwise it will be ignored. + */ + public Builder setCustomCacheKey(@Nullable String customCacheKey) { + this.customCacheKey = customCacheKey; + return this; + } + + /** + * Sets the optional subtitles. + * + *

{@code null} or an empty {@link List} can be used for a reset. + * + *

If a {@link PlaybackProperties#uri} is set, the subtitles are used to create a {@link + * PlaybackProperties} object. Otherwise it will be ignored. + */ + public Builder setSubtitles(@Nullable List subtitles) { + this.subtitles = + subtitles != null && !subtitles.isEmpty() + ? Collections.unmodifiableList(new ArrayList<>(subtitles)) + : Collections.emptyList(); + return this; + } + + /** + * Sets the optional ad tag URI. + * + *

If a {@link PlaybackProperties#uri} is set, the ad tag URI is used to create a {@link + * PlaybackProperties} object. Otherwise it will be ignored. + */ + public Builder setAdTagUri(@Nullable String adTagUri) { + this.adTagUri = adTagUri != null ? Uri.parse(adTagUri) : null; + return this; + } + + /** + * Sets the optional ad tag {@link Uri}. + * + *

If a {@link PlaybackProperties#uri} is set, the ad tag URI is used to create a {@link + * PlaybackProperties} object. Otherwise it will be ignored. + */ + public Builder setAdTagUri(@Nullable Uri adTagUri) { + this.adTagUri = adTagUri; + return this; + } + + /** + * Sets the optional tag for custom attributes. The tag for the media source which will be + * published in the {@code com.google.android.exoplayer2.Timeline} of the source as {@code + * com.google.android.exoplayer2.Timeline.Window#tag}. + * + *

If a {@link PlaybackProperties#uri} is set, the tag is used to create a {@link + * PlaybackProperties} object. Otherwise it will be ignored. + */ + public Builder setTag(@Nullable Object tag) { + this.tag = tag; + return this; + } + + /** Sets the media metadata. */ + public Builder setMediaMetadata(MediaMetadata mediaMetadata) { + this.mediaMetadata = mediaMetadata; + return this; + } + + /** + * Returns a new {@link MediaItem} instance with the current builder values. + */ + public MediaItem build() { + Assertions.checkState(drmLicenseUri == null || drmUuid != null); + @Nullable PlaybackProperties playbackProperties = null; + if (uri != null) { + playbackProperties = + new PlaybackProperties( + uri, + mimeType, + drmUuid != null + ? new DrmConfiguration( + drmUuid, + drmLicenseUri, + drmLicenseRequestHeaders, + drmMultiSession, + drmPlayClearContentWithoutKey, + drmSessionForClearTypes, + drmKeySetId) + : null, + streamKeys, + customCacheKey, + subtitles, + adTagUri, + tag); + mediaId = mediaId != null ? mediaId : uri.toString(); + } + return new MediaItem( + Assertions.checkNotNull(mediaId), + new ClippingProperties( + clipStartPositionMs, + clipEndPositionMs, + clipRelativeToLiveWindow, + clipRelativeToDefaultPosition, + clipStartsAtKeyFrame), + playbackProperties, + mediaMetadata != null ? mediaMetadata : new MediaMetadata.Builder().build()); + } + } + + /** DRM configuration for a media item. */ + public static final class DrmConfiguration { + + /** The UUID of the protection scheme. */ + public final UUID uuid; + + /** + * Optional license server {@link Uri}. If {@code null} then the license server must be + * specified by the media. + */ + @Nullable public final Uri licenseUri; + + /** The headers to attach to the request for the license uri. */ + public final Map requestHeaders; + + /** Whether the drm configuration is multi session enabled. */ + public final boolean multiSession; + + /** + * Whether clear samples within protected content should be played when keys for the encrypted + * part of the content have yet to be loaded. + */ + public final boolean playClearContentWithoutKey; + + /** The types of clear tracks for which to use a drm session. */ + public final List sessionForClearTypes; + + @Nullable private final byte[] keySetId; + + private DrmConfiguration( + UUID uuid, + @Nullable Uri licenseUri, + Map requestHeaders, + boolean multiSession, + boolean playClearContentWithoutKey, + List drmSessionForClearTypes, + @Nullable byte[] keySetId) { + this.uuid = uuid; + this.licenseUri = licenseUri; + this.requestHeaders = requestHeaders; + this.multiSession = multiSession; + this.playClearContentWithoutKey = playClearContentWithoutKey; + this.sessionForClearTypes = drmSessionForClearTypes; + this.keySetId = keySetId != null ? Arrays.copyOf(keySetId, keySetId.length) : null; + } + + /** Returns the key set ID of the offline license. */ + @Nullable + public byte[] getKeySetId() { + return keySetId != null ? Arrays.copyOf(keySetId, keySetId.length) : null; + } + + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof DrmConfiguration)) { + return false; + } + + DrmConfiguration other = (DrmConfiguration) obj; + return uuid.equals(other.uuid) + && Util.areEqual(licenseUri, other.licenseUri) + && Util.areEqual(requestHeaders, other.requestHeaders) + && multiSession == other.multiSession + && playClearContentWithoutKey == other.playClearContentWithoutKey + && sessionForClearTypes.equals(other.sessionForClearTypes) + && Arrays.equals(keySetId, other.keySetId); + } + + @Override + public int hashCode() { + int result = uuid.hashCode(); + result = 31 * result + (licenseUri != null ? licenseUri.hashCode() : 0); + result = 31 * result + requestHeaders.hashCode(); + result = 31 * result + (multiSession ? 1 : 0); + result = 31 * result + (playClearContentWithoutKey ? 1 : 0); + result = 31 * result + sessionForClearTypes.hashCode(); + result = 31 * result + Arrays.hashCode(keySetId); + return result; + } + } + + /** Properties for local playback. */ + public static final class PlaybackProperties { + + /** The {@link Uri}. */ + public final Uri uri; + + /** + * The optional mime type of the item, or {@code null} if unspecified. + * + *

The mime type can be used to disambiguate media items that have a uri which does not allow + * to infer the actual media type. + */ + @Nullable public final String mimeType; + + /** Optional {@link DrmConfiguration} for the media. */ + @Nullable public final DrmConfiguration drmConfiguration; + + /** Optional stream keys by which the manifest is filtered. */ + public final List streamKeys; + + /** Optional custom cache key (only used for progressive streams). */ + @Nullable public final String customCacheKey; + + /** Optional subtitles to be sideloaded. */ + public final List subtitles; + + /** Optional ad tag {@link Uri}. */ + @Nullable public final Uri adTagUri; + + /** + * Optional tag for custom attributes. The tag for the media source which will be published in + * the {@code com.google.android.exoplayer2.Timeline} of the source as {@code + * com.google.android.exoplayer2.Timeline.Window#tag}. + */ + @Nullable public final Object tag; + + private PlaybackProperties( + Uri uri, + @Nullable String mimeType, + @Nullable DrmConfiguration drmConfiguration, + List streamKeys, + @Nullable String customCacheKey, + List subtitles, + @Nullable Uri adTagUri, + @Nullable Object tag) { + this.uri = uri; + this.mimeType = mimeType; + this.drmConfiguration = drmConfiguration; + this.streamKeys = streamKeys; + this.customCacheKey = customCacheKey; + this.subtitles = subtitles; + this.adTagUri = adTagUri; + this.tag = tag; + } + + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof PlaybackProperties)) { + return false; + } + PlaybackProperties other = (PlaybackProperties) obj; + + return uri.equals(other.uri) + && Util.areEqual(mimeType, other.mimeType) + && Util.areEqual(drmConfiguration, other.drmConfiguration) + && streamKeys.equals(other.streamKeys) + && Util.areEqual(customCacheKey, other.customCacheKey) + && subtitles.equals(other.subtitles) + && Util.areEqual(adTagUri, other.adTagUri) + && Util.areEqual(tag, other.tag); + } + + @Override + public int hashCode() { + int result = uri.hashCode(); + result = 31 * result + (mimeType == null ? 0 : mimeType.hashCode()); + result = 31 * result + (drmConfiguration == null ? 0 : drmConfiguration.hashCode()); + result = 31 * result + streamKeys.hashCode(); + result = 31 * result + (customCacheKey == null ? 0 : customCacheKey.hashCode()); + result = 31 * result + subtitles.hashCode(); + result = 31 * result + (adTagUri == null ? 0 : adTagUri.hashCode()); + result = 31 * result + (tag == null ? 0 : tag.hashCode()); + return result; + } + } + + /** Properties for a text track. */ + public static final class Subtitle { + + /** The {@link Uri} to the subtitle file. */ + public final Uri uri; + /** The MIME type. */ + public final String mimeType; + /** The language. */ + @Nullable public final String language; + /** The selection flags. */ + @C.SelectionFlags public final int selectionFlags; + + /** + * Creates an instance. + * + * @param uri The {@link Uri uri} to the subtitle file. + * @param mimeType The mime type. + * @param language The optional language. + */ + public Subtitle(Uri uri, String mimeType, @Nullable String language) { + this(uri, mimeType, language, /* selectionFlags= */ 0); + } + + /** + * Creates an instance with the given selection flags. + * + * @param uri The {@link Uri uri} to the subtitle file. + * @param mimeType The mime type. + * @param language The optional language. + * @param selectionFlags The selection flags. + */ + public Subtitle( + Uri uri, String mimeType, @Nullable String language, @C.SelectionFlags int selectionFlags) { + this.uri = uri; + this.mimeType = mimeType; + this.language = language; + this.selectionFlags = selectionFlags; + } + + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof Subtitle)) { + return false; + } + + Subtitle other = (Subtitle) obj; + + return uri.equals(other.uri) + && mimeType.equals(other.mimeType) + && Util.areEqual(language, other.language) + && selectionFlags == other.selectionFlags; + } + + @Override + public int hashCode() { + int result = uri.hashCode(); + result = 31 * result + mimeType.hashCode(); + result = 31 * result + (language == null ? 0 : language.hashCode()); + result = 31 * result + selectionFlags; + return result; + } + } + + /** Optionally clips the media item to a custom start and end position. */ + public static final class ClippingProperties { + + /** The start position in milliseconds. This is a value larger than or equal to zero. */ + public final long startPositionMs; + + /** + * The end position in milliseconds. This is a value larger than or equal to zero or {@link + * C#TIME_END_OF_SOURCE} to play to the end of the stream. + */ + public final long endPositionMs; + + /** + * Whether the clipping of active media periods moves with a live window. If {@code false}, + * playback ends when it reaches {@link #endPositionMs}. + */ + public final boolean relativeToLiveWindow; + + /** + * Whether {@link #startPositionMs} and {@link #endPositionMs} are relative to the default + * position. + */ + public final boolean relativeToDefaultPosition; + + /** Sets whether the start point is guaranteed to be a key frame. */ + public final boolean startsAtKeyFrame; + + private ClippingProperties( + long startPositionMs, + long endPositionMs, + boolean relativeToLiveWindow, + boolean relativeToDefaultPosition, + boolean startsAtKeyFrame) { + this.startPositionMs = startPositionMs; + this.endPositionMs = endPositionMs; + this.relativeToLiveWindow = relativeToLiveWindow; + this.relativeToDefaultPosition = relativeToDefaultPosition; + this.startsAtKeyFrame = startsAtKeyFrame; + } + + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof ClippingProperties)) { + return false; + } + + ClippingProperties other = (ClippingProperties) obj; + + return startPositionMs == other.startPositionMs + && endPositionMs == other.endPositionMs + && relativeToLiveWindow == other.relativeToLiveWindow + && relativeToDefaultPosition == other.relativeToDefaultPosition + && startsAtKeyFrame == other.startsAtKeyFrame; + } + + @Override + public int hashCode() { + int result = Long.valueOf(startPositionMs).hashCode(); + result = 31 * result + Long.valueOf(endPositionMs).hashCode(); + result = 31 * result + (relativeToLiveWindow ? 1 : 0); + result = 31 * result + (relativeToDefaultPosition ? 1 : 0); + result = 31 * result + (startsAtKeyFrame ? 1 : 0); + return result; + } + } + + /** Identifies the media item. */ + public final String mediaId; + + /** Optional playback properties. Maybe be {@code null} if shared over process boundaries. */ + @Nullable public final PlaybackProperties playbackProperties; + + /** The media metadata. */ + public final MediaMetadata mediaMetadata; + + /** The clipping properties. */ + public final ClippingProperties clippingProperties; + + private MediaItem( + String mediaId, + ClippingProperties clippingProperties, + @Nullable PlaybackProperties playbackProperties, + MediaMetadata mediaMetadata) { + this.mediaId = mediaId; + this.playbackProperties = playbackProperties; + this.mediaMetadata = mediaMetadata; + this.clippingProperties = clippingProperties; + } + + /** Returns a {@link Builder} initialized with the values of this instance. */ + public Builder buildUpon() { + return new Builder(this); + } + + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof MediaItem)) { + return false; + } + + MediaItem other = (MediaItem) obj; + + return Util.areEqual(mediaId, other.mediaId) + && clippingProperties.equals(other.clippingProperties) + && Util.areEqual(playbackProperties, other.playbackProperties) + && Util.areEqual(mediaMetadata, other.mediaMetadata); + } + + @Override + public int hashCode() { + int result = mediaId.hashCode(); + result = 31 * result + (playbackProperties != null ? playbackProperties.hashCode() : 0); + result = 31 * result + clippingProperties.hashCode(); + result = 31 * result + mediaMetadata.hashCode(); + return result; + } +} diff --git a/library/common/src/main/java/com/google/android/exoplayer2/MediaMetadata.java b/library/common/src/main/java/com/google/android/exoplayer2/MediaMetadata.java new file mode 100644 index 0000000000..37fb8fcb0d --- /dev/null +++ b/library/common/src/main/java/com/google/android/exoplayer2/MediaMetadata.java @@ -0,0 +1,65 @@ +/* + * Copyright 2020 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; + +import androidx.annotation.Nullable; +import com.google.android.exoplayer2.util.Util; + +/** Metadata of the {@link MediaItem}. */ +public final class MediaMetadata { + + /** A builder for {@link MediaMetadata} instances. */ + public static final class Builder { + + @Nullable private String title; + + /** Sets the optional title. */ + public Builder setTitle(@Nullable String title) { + this.title = title; + return this; + } + + /** Returns a new {@link MediaMetadata} instance with the current builder values. */ + public MediaMetadata build() { + return new MediaMetadata(title); + } + } + + /** Optional title. */ + @Nullable public final String title; + + private MediaMetadata(@Nullable String title) { + this.title = title; + } + + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + MediaMetadata other = (MediaMetadata) obj; + + return Util.areEqual(title, other.title); + } + + @Override + public int hashCode() { + return title == null ? 0 : title.hashCode(); + } +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ParserException.java b/library/common/src/main/java/com/google/android/exoplayer2/ParserException.java similarity index 100% rename from library/core/src/main/java/com/google/android/exoplayer2/ParserException.java rename to library/common/src/main/java/com/google/android/exoplayer2/ParserException.java diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/CodecSpecificDataUtil.java b/library/common/src/main/java/com/google/android/exoplayer2/audio/AacUtil.java similarity index 52% rename from library/core/src/main/java/com/google/android/exoplayer2/util/CodecSpecificDataUtil.java rename to library/common/src/main/java/com/google/android/exoplayer2/audio/AacUtil.java index 3372f23971..8de423042f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/CodecSpecificDataUtil.java +++ b/library/common/src/main/java/com/google/android/exoplayer2/audio/AacUtil.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2016 The Android Open Source Project + * Copyright 2020 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,41 +13,101 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.google.android.exoplayer2.util; +package com.google.android.exoplayer2.audio; -import android.util.Pair; -import androidx.annotation.Nullable; +import androidx.annotation.IntDef; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ParserException; -import java.util.ArrayList; -import java.util.List; +import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.Log; +import com.google.android.exoplayer2.util.ParsableBitArray; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; -/** - * Provides static utility methods for manipulating various types of codec specific data. - */ -public final class CodecSpecificDataUtil { +/** Utility methods for handling AAC audio streams. */ +public final class AacUtil { - private static final byte[] NAL_START_CODE = new byte[] {0, 0, 0, 1}; + private static final String TAG = "AacUtil"; + + /** Holds sample format information for AAC audio. */ + public static final class Config { + + /** The sample rate in Hertz. */ + public final int sampleRateHz; + /** The number of channels. */ + public final int channelCount; + /** The RFC 6381 codecs string. */ + public final String codecs; + + private Config(int sampleRateHz, int channelCount, String codecs) { + this.sampleRateHz = sampleRateHz; + this.channelCount = channelCount; + this.codecs = codecs; + } + } + + // Audio sample count constants assume the frameLengthFlag in the access unit is 0. + /** + * Number of raw audio samples that are produced per channel when decoding an AAC LC access unit. + */ + public static final int AAC_LC_AUDIO_SAMPLE_COUNT = 1024; + /** + * Number of raw audio samples that are produced per channel when decoding an AAC XHE access unit. + */ + public static final int AAC_XHE_AUDIO_SAMPLE_COUNT = AAC_LC_AUDIO_SAMPLE_COUNT; + /** + * Number of raw audio samples that are produced per channel when decoding an AAC HE access unit. + */ + public static final int AAC_HE_AUDIO_SAMPLE_COUNT = 2048; + /** + * Number of raw audio samples that are produced per channel when decoding an AAC LD access unit. + */ + public static final int AAC_LD_AUDIO_SAMPLE_COUNT = 512; + + // Maximum bitrates for AAC profiles from the Fraunhofer FDK AAC encoder documentation: + // https://cs.android.com/android/platform/superproject/+/android-9.0.0_r8:external/aac/libAACenc/include/aacenc_lib.h;l=718 + /** Maximum rate for an AAC LC audio stream, in bytes per second. */ + public static final int AAC_LC_MAX_RATE_BYTES_PER_SECOND = 800 * 1000 / 8; + /** Maximum rate for an AAC HE V1 audio stream, in bytes per second. */ + public static final int AAC_HE_V1_MAX_RATE_BYTES_PER_SECOND = 128 * 1000 / 8; + /** Maximum rate for an AAC HE V2 audio stream, in bytes per second. */ + public static final int AAC_HE_V2_MAX_RATE_BYTES_PER_SECOND = 56 * 1000 / 8; + /** + * Maximum rate for an AAC XHE audio stream, in bytes per second. + * + *

Fraunhofer documentation says "500 kbit/s and above" for stereo, so we use a rate generously + * above the 500 kbit/s level. + */ + public static final int AAC_XHE_MAX_RATE_BYTES_PER_SECOND = 2048 * 1000 / 8; + /** + * Maximum rate for an AAC ELD audio stream, in bytes per second. + * + *

Fraunhofer documentation shows AAC-ELD as useful for up to ~ 64 kbit/s so we use this value. + */ + public static final int AAC_ELD_MAX_RATE_BYTES_PER_SECOND = 64 * 1000 / 8; private static final int AUDIO_SPECIFIC_CONFIG_FREQUENCY_INDEX_ARBITRARY = 0xF; - - private static final int[] AUDIO_SPECIFIC_CONFIG_SAMPLING_RATE_TABLE = new int[] { - 96000, 88200, 64000, 48000, 44100, 32000, 24000, 22050, 16000, 12000, 11025, 8000, 7350 - }; - + private static final int[] AUDIO_SPECIFIC_CONFIG_SAMPLING_RATE_TABLE = + new int[] { + 96000, 88200, 64000, 48000, 44100, 32000, 24000, 22050, 16000, 12000, 11025, 8000, 7350 + }; private static final int AUDIO_SPECIFIC_CONFIG_CHANNEL_CONFIGURATION_INVALID = -1; /** - * In the channel configurations below, indicates a single channel element; (A, B) indicates a - * channel pair element; and [A] indicates a low-frequency effects element. - * The speaker mapping short forms used are: - * - FC: front center - * - BC: back center - * - FL/FR: front left/right - * - FCL/FCR: front center left/right - * - FTL/FTR: front top left/right - * - SL/SR: back surround left/right - * - BL/BR: back left/right - * - LFE: low frequency effects + * In the channel configurations below, <A> indicates a single channel element; (A, B) + * indicates a channel pair element; and [A] indicates a low-frequency effects element. The + * speaker mapping short forms used are: + * + *

*/ private static final int[] AUDIO_SPECIFIC_CONFIG_CHANNEL_COUNT_TABLE = new int[] { @@ -69,29 +129,55 @@ public final class CodecSpecificDataUtil { AUDIO_SPECIFIC_CONFIG_CHANNEL_CONFIGURATION_INVALID }; + /** + * Prefix for the RFC 6381 codecs string for AAC formats. To form a full codecs string, suffix the + * decimal AudioObjectType. + */ + private static final String CODECS_STRING_PREFIX = "mp4a.40."; + // Advanced Audio Coding Low-Complexity profile. - private static final int AUDIO_OBJECT_TYPE_AAC_LC = 2; + public static final int AUDIO_OBJECT_TYPE_AAC_LC = 2; // Spectral Band Replication. - private static final int AUDIO_OBJECT_TYPE_SBR = 5; + public static final int AUDIO_OBJECT_TYPE_AAC_SBR = 5; // Error Resilient Bit-Sliced Arithmetic Coding. - private static final int AUDIO_OBJECT_TYPE_ER_BSAC = 22; + public static final int AUDIO_OBJECT_TYPE_AAC_ER_BSAC = 22; + // Enhanced low delay. + public static final int AUDIO_OBJECT_TYPE_AAC_ELD = 23; // Parametric Stereo. - private static final int AUDIO_OBJECT_TYPE_PS = 29; + public static final int AUDIO_OBJECT_TYPE_AAC_PS = 29; // Escape code for extended audio object types. private static final int AUDIO_OBJECT_TYPE_ESCAPE = 31; + // Extended high efficiency. + public static final int AUDIO_OBJECT_TYPE_AAC_XHE = 42; - private CodecSpecificDataUtil() {} + /** + * Valid AAC Audio object types. One of {@link #AUDIO_OBJECT_TYPE_AAC_LC}, {@link + * #AUDIO_OBJECT_TYPE_AAC_SBR}, {@link #AUDIO_OBJECT_TYPE_AAC_ER_BSAC}, {@link + * #AUDIO_OBJECT_TYPE_AAC_ELD}, {@link #AUDIO_OBJECT_TYPE_AAC_PS} or {@link + * #AUDIO_OBJECT_TYPE_AAC_XHE}. + */ + @Documented + @Retention(RetentionPolicy.SOURCE) + @IntDef({ + AUDIO_OBJECT_TYPE_AAC_LC, + AUDIO_OBJECT_TYPE_AAC_SBR, + AUDIO_OBJECT_TYPE_AAC_ER_BSAC, + AUDIO_OBJECT_TYPE_AAC_ELD, + AUDIO_OBJECT_TYPE_AAC_PS, + AUDIO_OBJECT_TYPE_AAC_XHE + }) + public @interface AacAudioObjectType {} /** * Parses an AAC AudioSpecificConfig, as defined in ISO 14496-3 1.6.2.1 * * @param audioSpecificConfig A byte array containing the AudioSpecificConfig to parse. - * @return A pair consisting of the sample rate in Hz and the channel count. + * @return The parsed configuration. * @throws ParserException If the AudioSpecificConfig cannot be parsed as it's not supported. */ - public static Pair parseAacAudioSpecificConfig(byte[] audioSpecificConfig) - throws ParserException { - return parseAacAudioSpecificConfig(new ParsableBitArray(audioSpecificConfig), false); + public static Config parseAudioSpecificConfig(byte[] audioSpecificConfig) throws ParserException { + return parseAudioSpecificConfig( + new ParsableBitArray(audioSpecificConfig), /* forceReadToEnd= */ false); } /** @@ -101,23 +187,25 @@ public final class CodecSpecificDataUtil { * position is advanced to the end of the AudioSpecificConfig. * @param forceReadToEnd Whether the entire AudioSpecificConfig should be read. Required for * knowing the length of the configuration payload. - * @return A pair consisting of the sample rate in Hz and the channel count. + * @return The parsed configuration. * @throws ParserException If the AudioSpecificConfig cannot be parsed as it's not supported. */ - public static Pair parseAacAudioSpecificConfig( - ParsableBitArray bitArray, boolean forceReadToEnd) throws ParserException { - int audioObjectType = getAacAudioObjectType(bitArray); - int sampleRate = getAacSamplingFrequency(bitArray); + public static Config parseAudioSpecificConfig(ParsableBitArray bitArray, boolean forceReadToEnd) + throws ParserException { + int audioObjectType = getAudioObjectType(bitArray); + int sampleRateHz = getSamplingFrequency(bitArray); int channelConfiguration = bitArray.readBits(4); - if (audioObjectType == AUDIO_OBJECT_TYPE_SBR || audioObjectType == AUDIO_OBJECT_TYPE_PS) { + String codecs = CODECS_STRING_PREFIX + audioObjectType; + if (audioObjectType == AUDIO_OBJECT_TYPE_AAC_SBR + || audioObjectType == AUDIO_OBJECT_TYPE_AAC_PS) { // For an AAC bitstream using spectral band replication (SBR) or parametric stereo (PS) with // explicit signaling, we return the extension sampling frequency as the sample rate of the // content; this is identical to the sample rate of the decoded output but may differ from // the sample rate set above. // Use the extensionSamplingFrequencyIndex. - sampleRate = getAacSamplingFrequency(bitArray); - audioObjectType = getAacAudioObjectType(bitArray); - if (audioObjectType == AUDIO_OBJECT_TYPE_ER_BSAC) { + sampleRateHz = getSamplingFrequency(bitArray); + audioObjectType = getAudioObjectType(bitArray); + if (audioObjectType == AUDIO_OBJECT_TYPE_AAC_ER_BSAC) { // Use the extensionChannelConfiguration. channelConfiguration = bitArray.readBits(4); } @@ -154,16 +242,18 @@ public final class CodecSpecificDataUtil { throw new ParserException("Unsupported epConfig: " + epConfig); } break; + default: + break; } } // For supported containers, bits_to_decode() is always 0. int channelCount = AUDIO_SPECIFIC_CONFIG_CHANNEL_COUNT_TABLE[channelConfiguration]; Assertions.checkArgument(channelCount != AUDIO_SPECIFIC_CONFIG_CHANNEL_CONFIGURATION_INVALID); - return Pair.create(sampleRate, channelCount); + return new Config(sampleRateHz, channelCount, codecs); } /** - * Builds a simple HE-AAC LC AudioSpecificConfig, as defined in ISO 14496-3 1.6.2.1 + * Builds a simple AAC LC AudioSpecificConfig, as defined in ISO 14496-3 1.6.2.1 * * @param sampleRate The sample rate in Hz. * @param channelCount The channel count. @@ -186,7 +276,7 @@ public final class CodecSpecificDataUtil { throw new IllegalArgumentException( "Invalid sample rate or number of channels: " + sampleRate + ", " + channelCount); } - return buildAacAudioSpecificConfig(AUDIO_OBJECT_TYPE_AAC_LC, sampleRateIndex, channelConfig); + return buildAudioSpecificConfig(AUDIO_OBJECT_TYPE_AAC_LC, sampleRateIndex, channelConfig); } /** @@ -197,127 +287,31 @@ public final class CodecSpecificDataUtil { * @param channelConfig The channel configuration. * @return The AudioSpecificConfig. */ - public static byte[] buildAacAudioSpecificConfig(int audioObjectType, int sampleRateIndex, - int channelConfig) { + public static byte[] buildAudioSpecificConfig( + int audioObjectType, int sampleRateIndex, int channelConfig) { byte[] specificConfig = new byte[2]; specificConfig[0] = (byte) (((audioObjectType << 3) & 0xF8) | ((sampleRateIndex >> 1) & 0x07)); specificConfig[1] = (byte) (((sampleRateIndex << 7) & 0x80) | ((channelConfig << 3) & 0x78)); return specificConfig; } - /** - * Parses an ALAC AudioSpecificConfig (i.e. an ALACSpecificConfig). - * - * @param audioSpecificConfig A byte array containing the AudioSpecificConfig to parse. - * @return A pair consisting of the sample rate in Hz and the channel count. - */ - public static Pair parseAlacAudioSpecificConfig(byte[] audioSpecificConfig) { - ParsableByteArray byteArray = new ParsableByteArray(audioSpecificConfig); - byteArray.setPosition(9); - int channelCount = byteArray.readUnsignedByte(); - byteArray.setPosition(20); - int sampleRate = byteArray.readUnsignedIntToInt(); - return Pair.create(sampleRate, channelCount); - } - - /** - * 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. - * - * @param data An array containing the data that should follow the NAL start code. - * @param offset The start offset into {@code data}. - * @param length The number of bytes to copy from {@code data} - * @return The constructed NAL unit. - */ - public static byte[] buildNalUnit(byte[] data, int offset, int length) { - byte[] nalUnit = new byte[length + NAL_START_CODE.length]; - System.arraycopy(NAL_START_CODE, 0, nalUnit, 0, NAL_START_CODE.length); - System.arraycopy(data, offset, nalUnit, NAL_START_CODE.length, length); - return nalUnit; - } - - /** - * Splits an array of NAL units. - * - *

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. - * - * @param data An array of data. - * @return The individual NAL units, or null if the input did not consist of NAL start code - * delimited units. - */ - public static @Nullable byte[][] splitNalUnits(byte[] data) { - if (!isNalStartCode(data, 0)) { - // data does not consist of NAL start code delimited units. - return null; + /** Returns the encoding for a given AAC audio object type. */ + @C.Encoding + public static int getEncodingForAudioObjectType(@AacAudioObjectType int audioObjectType) { + switch (audioObjectType) { + case AUDIO_OBJECT_TYPE_AAC_LC: + return C.ENCODING_AAC_LC; + case AUDIO_OBJECT_TYPE_AAC_SBR: + return C.ENCODING_AAC_HE_V1; + case AUDIO_OBJECT_TYPE_AAC_PS: + return C.ENCODING_AAC_HE_V2; + case AUDIO_OBJECT_TYPE_AAC_XHE: + return C.ENCODING_AAC_XHE; + case AUDIO_OBJECT_TYPE_AAC_ELD: + return C.ENCODING_AAC_ELD; + default: + return C.ENCODING_INVALID; } - List starts = new ArrayList<>(); - int nalUnitIndex = 0; - do { - starts.add(nalUnitIndex); - nalUnitIndex = findNalStartCode(data, nalUnitIndex + NAL_START_CODE.length); - } while (nalUnitIndex != C.INDEX_UNSET); - byte[][] split = new byte[starts.size()][]; - for (int i = 0; i < starts.size(); i++) { - int startIndex = starts.get(i); - int endIndex = i < starts.size() - 1 ? starts.get(i + 1) : data.length; - byte[] nal = new byte[endIndex - startIndex]; - System.arraycopy(data, startIndex, nal, 0, nal.length); - split[i] = nal; - } - return split; - } - - /** - * Finds the next occurrence of the NAL start code from a given index. - * - * @param data The data in which to search. - * @param index The first index to test. - * @return The index of the first byte of the found start code, or {@link C#INDEX_UNSET}. - */ - private static int findNalStartCode(byte[] data, int index) { - int endIndex = data.length - NAL_START_CODE.length; - for (int i = index; i <= endIndex; i++) { - if (isNalStartCode(data, i)) { - return i; - } - } - return C.INDEX_UNSET; - } - - /** - * Tests whether there exists a NAL start code at a given index. - * - * @param data The data. - * @param index The index to test. - * @return Whether there exists a start code that begins at {@code index}. - */ - private static boolean isNalStartCode(byte[] data, int index) { - if (data.length - index <= NAL_START_CODE.length) { - return false; - } - for (int j = 0; j < NAL_START_CODE.length; j++) { - if (data[index + j] != NAL_START_CODE[j]) { - return false; - } - } - return true; } /** @@ -326,7 +320,7 @@ public final class CodecSpecificDataUtil { * @param bitArray The bit array containing the audio specific configuration. * @return The audio object type. */ - private static int getAacAudioObjectType(ParsableBitArray bitArray) { + private static int getAudioObjectType(ParsableBitArray bitArray) { int audioObjectType = bitArray.readBits(5); if (audioObjectType == AUDIO_OBJECT_TYPE_ESCAPE) { audioObjectType = 32 + bitArray.readBits(6); @@ -341,7 +335,7 @@ public final class CodecSpecificDataUtil { * @param bitArray The bit array containing the audio specific configuration. * @return The sampling frequency. */ - private static int getAacSamplingFrequency(ParsableBitArray bitArray) { + private static int getSamplingFrequency(ParsableBitArray bitArray) { int samplingFrequency; int frequencyIndex = bitArray.readBits(4); if (frequencyIndex == AUDIO_SPECIFIC_CONFIG_FREQUENCY_INDEX_ARBITRARY) { @@ -353,9 +347,12 @@ public final class CodecSpecificDataUtil { return samplingFrequency; } - private static void parseGaSpecificConfig(ParsableBitArray bitArray, int audioObjectType, - int channelConfiguration) { - bitArray.skipBits(1); // frameLengthFlag. + private static void parseGaSpecificConfig( + ParsableBitArray bitArray, int audioObjectType, int channelConfiguration) { + boolean frameLengthFlag = bitArray.readBit(); + if (frameLengthFlag) { + Log.w(TAG, "Unexpected frameLengthFlag = 1"); + } boolean dependsOnCoreDecoder = bitArray.readBit(); if (dependsOnCoreDecoder) { bitArray.skipBits(14); // coreCoderDelay. @@ -371,7 +368,9 @@ public final class CodecSpecificDataUtil { if (audioObjectType == 22) { bitArray.skipBits(16); // numOfSubFrame (5), layer_length(11). } - if (audioObjectType == 17 || audioObjectType == 19 || audioObjectType == 20 + if (audioObjectType == 17 + || audioObjectType == 19 + || audioObjectType == 20 || audioObjectType == 23) { // aacSectionDataResilienceFlag, aacScalefactorDataResilienceFlag, // aacSpectralDataResilienceFlag. @@ -381,4 +380,5 @@ public final class CodecSpecificDataUtil { } } + private AacUtil() {} } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/Ac3Util.java b/library/common/src/main/java/com/google/android/exoplayer2/audio/Ac3Util.java similarity index 85% rename from library/core/src/main/java/com/google/android/exoplayer2/audio/Ac3Util.java rename to library/common/src/main/java/com/google/android/exoplayer2/audio/Ac3Util.java index 05c20939ff..f9a97d961f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/Ac3Util.java +++ b/library/common/src/main/java/com/google/android/exoplayer2/audio/Ac3Util.java @@ -24,6 +24,7 @@ import com.google.android.exoplayer2.drm.DrmInitData; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.ParsableBitArray; import com.google.android.exoplayer2.util.ParsableByteArray; +import com.google.android.exoplayer2.util.Util; import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -31,7 +32,7 @@ import java.nio.ByteBuffer; /** * Utility methods for parsing Dolby TrueHD and (E-)AC-3 syncframes. (E-)AC-3 parsing follows the - * definition in ETSI TS 102 366 V1.2.1. + * definition in ETSI TS 102 366 V1.4.1. */ public final class Ac3Util { @@ -39,8 +40,8 @@ public final class Ac3Util { public static final class SyncFrameInfo { /** - * AC3 stream types. See also ETSI TS 102 366 E.1.3.1.1. One of {@link #STREAM_TYPE_UNDEFINED}, - * {@link #STREAM_TYPE_TYPE0}, {@link #STREAM_TYPE_TYPE1} or {@link #STREAM_TYPE_TYPE2}. + * AC3 stream types. See also E.1.3.1.1. One of {@link #STREAM_TYPE_UNDEFINED}, {@link + * #STREAM_TYPE_TYPE0}, {@link #STREAM_TYPE_TYPE1} or {@link #STREAM_TYPE_TYPE2}. */ @Documented @Retention(RetentionPolicy.SOURCE) @@ -99,6 +100,13 @@ public final class Ac3Util { } + /** Maximum rate for an AC-3 audio stream, in bytes per second. */ + public static final int AC3_MAX_RATE_BYTES_PER_SECOND = 640 * 1000 / 8; + /** Maximum rate for an E-AC-3 audio stream, in bytes per second. */ + public static final int E_AC3_MAX_RATE_BYTES_PER_SECOND = 6144 * 1000 / 8; + /** Maximum rate for a TrueHD audio stream, in bytes per second. */ + public static final int TRUEHD_MAX_RATE_BYTES_PER_SECOND = 24500 * 1000 / 8; + /** * The number of samples to store in each output chunk when rechunking TrueHD streams. The number * of samples extracted from the container corresponding to one syncframe must be an integer @@ -114,9 +122,7 @@ public final class Ac3Util { * The number of new samples per (E-)AC-3 audio block. */ private static final int AUDIO_SAMPLES_PER_AUDIO_BLOCK = 256; - /** - * Each syncframe has 6 blocks that provide 256 new audio samples. See ETSI TS 102 366 4.1. - */ + /** Each syncframe has 6 blocks that provide 256 new audio samples. See subsection 4.1. */ private static final int AC3_SYNCFRAME_AUDIO_SAMPLE_COUNT = 6 * AUDIO_SAMPLES_PER_AUDIO_BLOCK; /** * Number of audio blocks per E-AC-3 syncframe, indexed by numblkscod. @@ -134,20 +140,21 @@ public final class Ac3Util { * Channel counts, indexed by acmod. */ private static final int[] CHANNEL_COUNT_BY_ACMOD = new int[] {2, 1, 2, 3, 3, 4, 4, 5}; - /** - * Nominal bitrates in kbps, indexed by frmsizecod / 2. (See ETSI TS 102 366 table 4.13.) - */ - private static final int[] BITRATE_BY_HALF_FRMSIZECOD = new int[] {32, 40, 48, 56, 64, 80, 96, - 112, 128, 160, 192, 224, 256, 320, 384, 448, 512, 576, 640}; - /** - * 16-bit words per syncframe, indexed by frmsizecod / 2. (See ETSI TS 102 366 table 4.13.) - */ - private static final int[] SYNCFRAME_SIZE_WORDS_BY_HALF_FRMSIZECOD_44_1 = new int[] {69, 87, 104, - 121, 139, 174, 208, 243, 278, 348, 417, 487, 557, 696, 835, 975, 1114, 1253, 1393}; + /** Nominal bitrates in kbps, indexed by frmsizecod / 2. (See table 4.13.) */ + private static final int[] BITRATE_BY_HALF_FRMSIZECOD = + new int[] { + 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 384, 448, 512, 576, 640 + }; + /** 16-bit words per syncframe, indexed by frmsizecod / 2. (See table 4.13.) */ + private static final int[] SYNCFRAME_SIZE_WORDS_BY_HALF_FRMSIZECOD_44_1 = + new int[] { + 69, 87, 104, 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 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. @@ -164,23 +171,19 @@ public final class Ac3Util { if ((nextByte & 0x04) != 0) { // lfeon channelCount++; } - return Format.createAudioSampleFormat( - trackId, - MimeTypes.AUDIO_AC3, - /* codecs= */ null, - Format.NO_VALUE, - Format.NO_VALUE, - channelCount, - sampleRate, - /* initializationData= */ null, - drmInitData, - /* selectionFlags= */ 0, - language); + return new Format.Builder() + .setId(trackId) + .setSampleMimeType(MimeTypes.AUDIO_AC3) + .setChannelCount(channelCount) + .setSampleRate(sampleRate) + .setDrmInitData(drmInitData) + .setLanguage(language) + .build(); } /** - * 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 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. @@ -219,18 +222,14 @@ public final class Ac3Util { mimeType = MimeTypes.AUDIO_E_AC3_JOC; } } - return Format.createAudioSampleFormat( - trackId, - mimeType, - /* codecs= */ null, - Format.NO_VALUE, - Format.NO_VALUE, - channelCount, - sampleRate, - /* initializationData= */ null, - drmInitData, - /* selectionFlags= */ 0, - language); + return new Format.Builder() + .setId(trackId) + .setSampleMimeType(mimeType) + .setChannelCount(channelCount) + .setSampleRate(sampleRate) + .setDrmInitData(drmInitData) + .setLanguage(language) + .build(); } /** @@ -243,9 +242,10 @@ public final class Ac3Util { public static SyncFrameInfo parseAc3SyncframeInfo(ParsableBitArray data) { int initialPosition = data.getPosition(); data.skipBits(40); - boolean isEac3 = data.readBits(5) == 16; // See bsid in subsection E.1.3.1.6. + // Parse the bitstream ID for AC-3 and E-AC-3 (see subsections 4.3, E.1.2 and E.1.3.1.6). + boolean isEac3 = data.readBits(5) > 10; data.setPosition(initialPosition); - String mimeType; + @Nullable String mimeType; @StreamType int streamType = SyncFrameInfo.STREAM_TYPE_UNDEFINED; int sampleRate; int acmod; @@ -254,7 +254,7 @@ public final class Ac3Util { boolean lfeon; int channelCount; if (isEac3) { - // Syntax from ETSI TS 102 366 V1.2.1 subsections E.1.2.1 and E.1.2.2. + // Subsection E.1.2. data.skipBits(16); // syncword switch (data.readBits(2)) { // strmtyp case 0: @@ -472,7 +472,8 @@ public final class Ac3Util { if (data.length < 6) { return C.LENGTH_UNSET; } - boolean isEac3 = ((data[5] & 0xFF) >> 3) == 16; // See bsid in subsection E.1.3.1.6. + // Parse the bitstream ID for AC-3 and E-AC-3 (see subsections 4.3, E.1.2 and E.1.3.1.6). + boolean isEac3 = ((data[5] & 0xF8) >> 3) > 10; if (isEac3) { int frmsiz = (data[2] & 0x07) << 8; // Most significant 3 bits. frmsiz |= data[3] & 0xFF; // Least significant 8 bits. @@ -485,24 +486,22 @@ public final class Ac3Util { } /** - * Returns the number of audio samples in an AC-3 syncframe. - */ - public static int getAc3SyncframeAudioSampleCount() { - return AC3_SYNCFRAME_AUDIO_SAMPLE_COUNT; - } - - /** - * Reads the number of audio samples represented by the given E-AC-3 syncframe. The buffer's + * Reads the number of audio samples represented by the given (E-)AC-3 syncframe. The buffer's * position is not modified. * * @param buffer The {@link ByteBuffer} from which to read the syncframe. * @return The number of audio samples represented by the syncframe. */ - public static int parseEAc3SyncframeAudioSampleCount(ByteBuffer buffer) { - // See ETSI TS 102 366 subsection E.1.2.2. - int fscod = (buffer.get(buffer.position() + 4) & 0xC0) >> 6; - return AUDIO_SAMPLES_PER_AUDIO_BLOCK * (fscod == 0x03 ? 6 - : BLOCKS_PER_SYNCFRAME_BY_NUMBLKSCOD[(buffer.get(buffer.position() + 4) & 0x30) >> 4]); + public static int parseAc3SyncframeAudioSampleCount(ByteBuffer buffer) { + // Parse the bitstream ID for AC-3 and E-AC-3 (see subsections 4.3, E.1.2 and E.1.3.1.6). + boolean isEac3 = ((buffer.get(buffer.position() + 5) & 0xF8) >> 3) > 10; + if (isEac3) { + int fscod = (buffer.get(buffer.position() + 4) & 0xC0) >> 6; + int numblkscod = fscod == 0x03 ? 3 : (buffer.get(buffer.position() + 4) & 0x30) >> 4; + return BLOCKS_PER_SYNCFRAME_BY_NUMBLKSCOD[numblkscod] * AUDIO_SAMPLES_PER_AUDIO_BLOCK; + } else { + return AC3_SYNCFRAME_AUDIO_SAMPLE_COUNT; + } } /** @@ -518,7 +517,7 @@ public final class Ac3Util { int endIndex = buffer.limit() - TRUEHD_SYNCFRAME_PREFIX_LENGTH; for (int i = startIndex; i <= endIndex; i++) { // The syncword ends 0xBA for TrueHD or 0xBB for MLP. - if ((buffer.getInt(i + 4) & 0xFEFFFFFF) == 0xBA6F72F8) { + if ((Util.getBigEndianInt(buffer, i + 4) & 0xFFFFFFFE) == 0xF8726FBA) { return i - startIndex; } } @@ -535,8 +534,8 @@ public final class Ac3Util { * contain the start of a syncframe. */ public static int parseTrueHdSyncframeAudioSampleCount(byte[] syncframe) { - // TODO: Link to specification if available. - // The syncword ends 0xBA for TrueHD or 0xBB for MLP. + // See "Dolby TrueHD (MLP) high-level bitstream description" on the Dolby developer site, + // subsections 2.2 and 4.2.1. The syncword ends 0xBA for TrueHD or 0xBB for MLP. if (syncframe[4] != (byte) 0xF8 || syncframe[5] != (byte) 0x72 || syncframe[6] != (byte) 0x6F diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/Ac4Util.java b/library/common/src/main/java/com/google/android/exoplayer2/audio/Ac4Util.java similarity index 92% rename from library/core/src/main/java/com/google/android/exoplayer2/audio/Ac4Util.java rename to library/common/src/main/java/com/google/android/exoplayer2/audio/Ac4Util.java index c54e3844a3..2e4367f4e2 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/Ac4Util.java +++ b/library/common/src/main/java/com/google/android/exoplayer2/audio/Ac4Util.java @@ -54,9 +54,17 @@ public final class Ac4Util { public static final int AC40_SYNCWORD = 0xAC40; public static final int AC41_SYNCWORD = 0xAC41; + /** Maximum rate for an AC-4 audio stream, in bytes per second. */ + public static final int MAX_RATE_BYTES_PER_SECOND = 2688 * 1000 / 8; + /** The channel count of AC-4 stream. */ // TODO: Parse AC-4 stream channel count. private static final int CHANNEL_COUNT_2 = 2; + /** + * The AC-4 sync frame header size for extractor. The seven bytes are 0xAC, 0x40, 0xFF, 0xFF, + * sizeByte1, sizeByte2, sizeByte3. See ETSI TS 103 190-1 V1.3.1, Annex G + */ + public static final int SAMPLE_HEADER_SIZE = 7; /** * The header size for AC-4 parser. Only needs to be as big as we need to read, not the full * header size. @@ -99,18 +107,14 @@ public final class Ac4Util { ParsableByteArray data, String trackId, String language, @Nullable DrmInitData drmInitData) { data.skipBytes(1); // ac4_dsi_version, bitstream_version[0:5] int sampleRate = ((data.readUnsignedByte() & 0x20) >> 5 == 1) ? 48000 : 44100; - return Format.createAudioSampleFormat( - trackId, - MimeTypes.AUDIO_AC4, - /* codecs= */ null, - /* bitrate= */ Format.NO_VALUE, - /* maxInputSize= */ Format.NO_VALUE, - CHANNEL_COUNT_2, - sampleRate, - /* initializationData= */ null, - drmInitData, - /* selectionFlags= */ 0, - language); + return new Format.Builder() + .setId(trackId) + .setSampleMimeType(MimeTypes.AUDIO_AC4) + .setChannelCount(CHANNEL_COUNT_2) + .setSampleRate(sampleRate) + .setDrmInitData(drmInitData) + .setLanguage(language) + .build(); } /** @@ -218,7 +222,7 @@ public final class Ac4Util { /** Populates {@code buffer} with an AC-4 sample header for a sample of the specified size. */ public static void getAc4SampleHeader(int size, ParsableByteArray buffer) { // See ETSI TS 103 190-1 V1.3.1, Annex G. - buffer.reset(/* limit= */ 7); + buffer.reset(SAMPLE_HEADER_SIZE); buffer.data[0] = (byte) 0xAC; buffer.data[1] = 0x40; buffer.data[2] = (byte) 0xFF; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/DtsUtil.java b/library/common/src/main/java/com/google/android/exoplayer2/audio/DtsUtil.java similarity index 91% rename from library/core/src/main/java/com/google/android/exoplayer2/audio/DtsUtil.java rename to library/common/src/main/java/com/google/android/exoplayer2/audio/DtsUtil.java index 7af9d9f074..8640c46e1a 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/DtsUtil.java +++ b/library/common/src/main/java/com/google/android/exoplayer2/audio/DtsUtil.java @@ -28,6 +28,15 @@ import java.util.Arrays; */ public final class DtsUtil { + /** + * Maximum rate for a DTS audio stream, in bytes per second. + * + *

DTS allows an 'open' bitrate, but we assume the maximum listed value: 1536 kbit/s. + */ + public static final int DTS_MAX_RATE_BYTES_PER_SECOND = 1536 * 1000 / 8; + /** Maximum rate for a DTS-HD audio stream, in bytes per second. */ + public static final int DTS_HD_MAX_RATE_BYTES_PER_SECOND = 18000 * 1000 / 8; + private static final int SYNC_VALUE_BE = 0x7FFE8001; private static final int SYNC_VALUE_14B_BE = 0x1FFFE800; private static final int SYNC_VALUE_LE = 0xFE7F0180; @@ -81,7 +90,10 @@ public final class DtsUtil { * @return The DTS format parsed from data in the header. */ public static Format parseDtsFormat( - byte[] frame, String trackId, @Nullable String language, @Nullable DrmInitData drmInitData) { + byte[] frame, + @Nullable String trackId, + @Nullable String language, + @Nullable 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); @@ -93,8 +105,15 @@ public final class DtsUtil { : TWICE_BITRATE_KBPS_BY_RATE[rate] * 1000 / 2; frameBits.skipBits(10); // MIX, DYNF, TIMEF, AUXF, HDCD, EXT_AUDIO_ID, EXT_AUDIO, ASPF channelCount += frameBits.readBits(2) > 0 ? 1 : 0; // LFF - return Format.createAudioSampleFormat(trackId, MimeTypes.AUDIO_DTS, null, bitrate, - Format.NO_VALUE, channelCount, sampleRate, null, drmInitData, 0, language); + return new Format.Builder() + .setId(trackId) + .setSampleMimeType(MimeTypes.AUDIO_DTS) + .setAverageBitrate(bitrate) + .setChannelCount(channelCount) + .setSampleRate(sampleRate) + .setDrmInitData(drmInitData) + .setLanguage(language) + .build(); } /** diff --git a/library/common/src/main/java/com/google/android/exoplayer2/audio/MpegAudioUtil.java b/library/common/src/main/java/com/google/android/exoplayer2/audio/MpegAudioUtil.java new file mode 100644 index 0000000000..d09443daf0 --- /dev/null +++ b/library/common/src/main/java/com/google/android/exoplayer2/audio/MpegAudioUtil.java @@ -0,0 +1,265 @@ +/* + * Copyright (C) 2020 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 androidx.annotation.Nullable; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.util.MimeTypes; + +/** Utility methods for handling MPEG audio streams. */ +public final class MpegAudioUtil { + + /** Stores the metadata for an MPEG audio frame. */ + public static final class Header { + + /** MPEG audio header version. */ + public int version; + /** The mime type. */ + @Nullable public String mimeType; + /** Size of the frame associated with this header, in bytes. */ + public int frameSize; + /** Sample rate in samples per second. */ + public int sampleRate; + /** Number of audio channels in the frame. */ + public int channels; + /** Bitrate of the frame in bit/s. */ + public int bitrate; + /** Number of samples stored in the frame. */ + public int samplesPerFrame; + + /** + * Populates the fields in this instance to reflect the MPEG audio header in {@code headerData}, + * returning whether the header was valid. If false, the values of the fields in this instance + * will not be updated. + * + * @param headerData Header data to parse. + * @return True if the fields were populated. False otherwise, indicating that {@code + * headerData} is not a valid MPEG audio header. + */ + public boolean setForHeaderData(int headerData) { + if (!isMagicPresent(headerData)) { + return false; + } + + int version = (headerData >>> 19) & 3; + if (version == 1) { + return false; + } + + int layer = (headerData >>> 17) & 3; + if (layer == 0) { + return false; + } + + int bitrateIndex = (headerData >>> 12) & 15; + if (bitrateIndex == 0 || bitrateIndex == 0xF) { + // Disallow "free" bitrate. + return false; + } + + int samplingRateIndex = (headerData >>> 10) & 3; + if (samplingRateIndex == 3) { + return false; + } + + this.version = version; + mimeType = MIME_TYPE_BY_LAYER[3 - layer]; + sampleRate = SAMPLING_RATE_V1[samplingRateIndex]; + if (version == 2) { + // Version 2 + sampleRate /= 2; + } else if (version == 0) { + // Version 2.5 + sampleRate /= 4; + } + int padding = (headerData >>> 9) & 1; + samplesPerFrame = getFrameSizeInSamples(version, layer); + if (layer == 3) { + // Layer I (layer == 3) + bitrate = version == 3 ? BITRATE_V1_L1[bitrateIndex - 1] : BITRATE_V2_L1[bitrateIndex - 1]; + frameSize = (12 * bitrate / sampleRate + padding) * 4; + } else { + // Layer II (layer == 2) or III (layer == 1) + if (version == 3) { + // Version 1 + bitrate = layer == 2 ? BITRATE_V1_L2[bitrateIndex - 1] : BITRATE_V1_L3[bitrateIndex - 1]; + frameSize = 144 * bitrate / sampleRate + padding; + } else { + // Version 2 or 2.5. + bitrate = BITRATE_V2[bitrateIndex - 1]; + frameSize = (layer == 1 ? 72 : 144) * bitrate / sampleRate + padding; + } + } + channels = ((headerData >> 6) & 3) == 3 ? 1 : 2; + return true; + } + } + + /** + * Returns the size of the frame associated with {@code header}, or {@link C#LENGTH_UNSET} if it + * is invalid. + */ + public static int getFrameSize(int headerData) { + if (!isMagicPresent(headerData)) { + return C.LENGTH_UNSET; + } + + int version = (headerData >>> 19) & 3; + if (version == 1) { + return C.LENGTH_UNSET; + } + + int layer = (headerData >>> 17) & 3; + if (layer == 0) { + return C.LENGTH_UNSET; + } + + int bitrateIndex = (headerData >>> 12) & 15; + if (bitrateIndex == 0 || bitrateIndex == 0xF) { + // Disallow "free" bitrate. + return C.LENGTH_UNSET; + } + + int samplingRateIndex = (headerData >>> 10) & 3; + if (samplingRateIndex == 3) { + return C.LENGTH_UNSET; + } + + int samplingRate = SAMPLING_RATE_V1[samplingRateIndex]; + if (version == 2) { + // Version 2 + samplingRate /= 2; + } else if (version == 0) { + // Version 2.5 + samplingRate /= 4; + } + + int bitrate; + int padding = (headerData >>> 9) & 1; + if (layer == 3) { + // Layer I (layer == 3) + bitrate = version == 3 ? BITRATE_V1_L1[bitrateIndex - 1] : BITRATE_V2_L1[bitrateIndex - 1]; + return (12 * bitrate / samplingRate + padding) * 4; + } else { + // Layer II (layer == 2) or III (layer == 1) + if (version == 3) { + bitrate = layer == 2 ? BITRATE_V1_L2[bitrateIndex - 1] : BITRATE_V1_L3[bitrateIndex - 1]; + } else { + // Version 2 or 2.5. + bitrate = BITRATE_V2[bitrateIndex - 1]; + } + } + + if (version == 3) { + // Version 1 + return 144 * bitrate / samplingRate + padding; + } else { + // Version 2 or 2.5 + return (layer == 1 ? 72 : 144) * bitrate / samplingRate + padding; + } + } + + /** + * Returns the number of samples per frame associated with {@code headerData}, or {@link + * C#LENGTH_UNSET} if it is invalid. + */ + public static int parseMpegAudioFrameSampleCount(int headerData) { + if (!isMagicPresent(headerData)) { + return C.LENGTH_UNSET; + } + + int version = (headerData >>> 19) & 3; + if (version == 1) { + return C.LENGTH_UNSET; + } + + int layer = (headerData >>> 17) & 3; + if (layer == 0) { + return C.LENGTH_UNSET; + } + + // Those header values are not used but are checked for consistency with the other methods + int bitrateIndex = (headerData >>> 12) & 15; + int samplingRateIndex = (headerData >>> 10) & 3; + if (bitrateIndex == 0 || bitrateIndex == 0xF || samplingRateIndex == 3) { + return C.LENGTH_UNSET; + } + + return getFrameSizeInSamples(version, layer); + } + + /** + * Theoretical maximum frame size for an MPEG audio stream, which occurs when playing a Layer 2 + * MPEG 2.5 audio stream at 16 kb/s (with padding). The size is 1152 sample/frame * 160000 bit/s / + * (8000 sample/s * 8 bit/byte) + 1 padding byte/frame = 2881 byte/frame. The next power of two + * size is 4 KiB. + */ + public static final int MAX_FRAME_SIZE_BYTES = 4096; + + /** + * Maximum rate for an MPEG audio stream corresponding to MPEG-1 layer III (320 kbit/s), in bytes + * per second. + */ + public static final int MAX_RATE_BYTES_PER_SECOND = 320 * 1000 / 8; + + private static final String[] MIME_TYPE_BY_LAYER = + new String[] {MimeTypes.AUDIO_MPEG_L1, MimeTypes.AUDIO_MPEG_L2, MimeTypes.AUDIO_MPEG}; + private static final int[] SAMPLING_RATE_V1 = {44100, 48000, 32000}; + private static final int[] BITRATE_V1_L1 = { + 32000, 64000, 96000, 128000, 160000, 192000, 224000, 256000, 288000, 320000, 352000, 384000, + 416000, 448000 + }; + private static final int[] BITRATE_V2_L1 = { + 32000, 48000, 56000, 64000, 80000, 96000, 112000, 128000, 144000, 160000, 176000, 192000, + 224000, 256000 + }; + private static final int[] BITRATE_V1_L2 = { + 32000, 48000, 56000, 64000, 80000, 96000, 112000, 128000, 160000, 192000, 224000, 256000, + 320000, 384000 + }; + private static final int[] BITRATE_V1_L3 = { + 32000, 40000, 48000, 56000, 64000, 80000, 96000, 112000, 128000, 160000, 192000, 224000, 256000, + 320000 + }; + private static final int[] BITRATE_V2 = { + 8000, 16000, 24000, 32000, 40000, 48000, 56000, 64000, 80000, 96000, 112000, 128000, 144000, + 160000 + }; + + private static final int SAMPLES_PER_FRAME_L1 = 384; + private static final int SAMPLES_PER_FRAME_L2 = 1152; + private static final int SAMPLES_PER_FRAME_L3_V1 = 1152; + private static final int SAMPLES_PER_FRAME_L3_V2 = 576; + + private MpegAudioUtil() {} + + private static boolean isMagicPresent(int headerData) { + return (headerData & 0xFFE00000) == 0xFFE00000; + } + + private static int getFrameSizeInSamples(int version, int layer) { + switch (layer) { + case 1: + return version == 3 ? SAMPLES_PER_FRAME_L3_V1 : SAMPLES_PER_FRAME_L3_V2; // Layer III + case 2: + return SAMPLES_PER_FRAME_L2; // Layer II + case 3: + return SAMPLES_PER_FRAME_L1; // Layer I + default: + throw new IllegalArgumentException(); + } + } +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/WavUtil.java b/library/common/src/main/java/com/google/android/exoplayer2/audio/WavUtil.java similarity index 65% rename from library/core/src/main/java/com/google/android/exoplayer2/audio/WavUtil.java rename to library/common/src/main/java/com/google/android/exoplayer2/audio/WavUtil.java index f5cabf7c30..208989124a 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/WavUtil.java +++ b/library/common/src/main/java/com/google/android/exoplayer2/audio/WavUtil.java @@ -32,30 +32,36 @@ public final class WavUtil { public static final int DATA_FOURCC = 0x64617461; /** WAVE type value for integer PCM audio data. */ - private static final int TYPE_PCM = 0x0001; + public static final int TYPE_PCM = 0x0001; /** WAVE type value for float PCM audio data. */ - private static final int TYPE_FLOAT = 0x0003; + public 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; + public static final int TYPE_ALAW = 0x0006; /** WAVE type value for 8-bit ITU-T G.711 mu-law audio data. */ - private static final int TYPE_MU_LAW = 0x0007; + public static final int TYPE_MLAW = 0x0007; + /** WAVE type value for IMA ADPCM audio data. */ + public static final int TYPE_IMA_ADPCM = 0x0011; /** WAVE type value for extended WAVE format. */ - private static final int TYPE_WAVE_FORMAT_EXTENSIBLE = 0xFFFE; + public 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) { + /** + * Returns the WAVE format type value for the given {@link C.PcmEncoding}. + * + * @param pcmEncoding The {@link C.PcmEncoding} value. + * @return The corresponding WAVE format type. + * @throws IllegalArgumentException If {@code pcmEncoding} is not a {@link C.PcmEncoding}, or if + * it's {@link C#ENCODING_INVALID} or {@link Format#NO_VALUE}. + */ + public static int getTypeForPcmEncoding(@C.PcmEncoding int pcmEncoding) { + switch (pcmEncoding) { 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_PCM_16BIT_BIG_ENDIAN: // Not TYPE_PCM, because TYPE_PCM is little endian. case C.ENCODING_INVALID: case Format.NO_VALUE: default: @@ -63,18 +69,17 @@ public final class WavUtil { } } - /** Returns the PCM encoding for the given WAVE {@code type} value. */ - public static @C.PcmEncoding int getEncodingForType(int type, int bitsPerSample) { + /** + * Returns the {@link C.PcmEncoding} for the given WAVE format type value, or {@link + * C#ENCODING_INVALID} if the type is not a known PCM type. + */ + public static @C.PcmEncoding int getPcmEncodingForType(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; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/package-info.java b/library/common/src/main/java/com/google/android/exoplayer2/audio/package-info.java similarity index 100% rename from library/core/src/main/java/com/google/android/exoplayer2/audio/package-info.java rename to library/common/src/main/java/com/google/android/exoplayer2/audio/package-info.java diff --git a/library/core/src/main/java/com/google/android/exoplayer2/decoder/Buffer.java b/library/common/src/main/java/com/google/android/exoplayer2/decoder/Buffer.java similarity index 100% rename from library/core/src/main/java/com/google/android/exoplayer2/decoder/Buffer.java rename to library/common/src/main/java/com/google/android/exoplayer2/decoder/Buffer.java diff --git a/library/core/src/main/java/com/google/android/exoplayer2/decoder/CryptoInfo.java b/library/common/src/main/java/com/google/android/exoplayer2/decoder/CryptoInfo.java similarity index 72% rename from library/core/src/main/java/com/google/android/exoplayer2/decoder/CryptoInfo.java rename to library/common/src/main/java/com/google/android/exoplayer2/decoder/CryptoInfo.java index 379ca971b5..1c52abc476 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/decoder/CryptoInfo.java +++ b/library/common/src/main/java/com/google/android/exoplayer2/decoder/CryptoInfo.java @@ -15,7 +15,7 @@ */ package com.google.android.exoplayer2.decoder; -import android.annotation.TargetApi; +import androidx.annotation.RequiresApi; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.util.Util; @@ -25,27 +25,41 @@ import com.google.android.exoplayer2.util.Util; public final class CryptoInfo { /** + * The 16 byte initialization vector. If the initialization vector of the content is shorter than + * 16 bytes, 0 byte padding is appended to extend the vector to the required 16 byte length. + * * @see android.media.MediaCodec.CryptoInfo#iv */ public byte[] iv; /** + * The 16 byte key id. + * * @see android.media.MediaCodec.CryptoInfo#key */ public byte[] key; /** + * The type of encryption that has been applied. Must be one of the {@link C.CryptoMode} values. + * * @see android.media.MediaCodec.CryptoInfo#mode */ - @C.CryptoMode - public int mode; + @C.CryptoMode public int mode; /** + * The number of leading unencrypted bytes in each sub-sample. If null, all bytes are treated as + * encrypted and {@link #numBytesOfEncryptedData} must be specified. + * * @see android.media.MediaCodec.CryptoInfo#numBytesOfClearData */ public int[] numBytesOfClearData; /** + * The number of trailing encrypted bytes in each sub-sample. If null, all bytes are treated as + * clear and {@link #numBytesOfClearData} must be specified. + * * @see android.media.MediaCodec.CryptoInfo#numBytesOfEncryptedData */ public int[] numBytesOfEncryptedData; /** + * The number of subSamples that make up the buffer's contents. + * * @see android.media.MediaCodec.CryptoInfo#numSubSamples */ public int numSubSamples; @@ -111,7 +125,30 @@ public final class CryptoInfo { return getFrameworkCryptoInfo(); } - @TargetApi(24) + /** + * Increases the number of clear data for the first sub sample by {@code count}. + * + *

If {@code count} is 0, this method is a no-op. Otherwise, it adds {@code count} to {@link + * #numBytesOfClearData}[0]. + * + *

If {@link #numBytesOfClearData} is null (which is permitted), this method will instantiate + * it to a new {@code int[1]}. + * + * @param count The number of bytes to be added to the first subSample of {@link + * #numBytesOfClearData}. + */ + public void increaseClearDataFirstSubSampleBy(int count) { + if (count == 0) { + return; + } + if (numBytesOfClearData == null) { + numBytesOfClearData = new int[1]; + frameworkCryptoInfo.numBytesOfClearData = numBytesOfClearData; + } + numBytesOfClearData[0] += count; + } + + @RequiresApi(24) private static final class PatternHolderV24 { private final android.media.MediaCodec.CryptoInfo frameworkCryptoInfo; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/decoder/DecoderInputBuffer.java b/library/common/src/main/java/com/google/android/exoplayer2/decoder/DecoderInputBuffer.java similarity index 94% rename from library/core/src/main/java/com/google/android/exoplayer2/decoder/DecoderInputBuffer.java rename to library/common/src/main/java/com/google/android/exoplayer2/decoder/DecoderInputBuffer.java index 302ae0609f..bd5df4c8b1 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/decoder/DecoderInputBuffer.java +++ b/library/common/src/main/java/com/google/android/exoplayer2/decoder/DecoderInputBuffer.java @@ -63,6 +63,14 @@ public class DecoderInputBuffer extends Buffer { /** The buffer's data, or {@code null} if no data has been set. */ @Nullable public ByteBuffer data; + // TODO: Remove this temporary signaling once end-of-stream propagation for clips using content + // protection is fixed. See [Internal: b/153326944] for details. + /** + * Whether the last attempt to read a sample into this buffer failed due to not yet having the DRM + * keys associated with the next sample. + */ + public boolean waitingForKeys; + /** * The time at which the sample should be presented. */ @@ -137,6 +145,7 @@ public class DecoderInputBuffer extends Buffer { } // Instantiate a new buffer if possible. ByteBuffer newData = createReplacementByteBuffer(requiredCapacity); + newData.order(data.order()); // Copy data up to the current position from the old buffer to the new one. if (position > 0) { data.flip(); @@ -182,6 +191,7 @@ public class DecoderInputBuffer extends Buffer { if (supplementalData != null) { supplementalData.clear(); } + waitingForKeys = false; } private ByteBuffer createReplacementByteBuffer(int requiredCapacity) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/decoder/package-info.java b/library/common/src/main/java/com/google/android/exoplayer2/decoder/package-info.java similarity index 100% rename from library/core/src/main/java/com/google/android/exoplayer2/decoder/package-info.java rename to library/common/src/main/java/com/google/android/exoplayer2/decoder/package-info.java diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmInitData.java b/library/common/src/main/java/com/google/android/exoplayer2/drm/DrmInitData.java similarity index 99% rename from library/core/src/main/java/com/google/android/exoplayer2/drm/DrmInitData.java rename to library/common/src/main/java/com/google/android/exoplayer2/drm/DrmInitData.java index 2f0246ba64..ee09838b0a 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmInitData.java +++ b/library/common/src/main/java/com/google/android/exoplayer2/drm/DrmInitData.java @@ -285,7 +285,7 @@ public final class DrmInitData implements Comparator, Parcelable { * The {@link UUID} of the DRM scheme, or {@link C#UUID_NIL} if the data is universal (i.e. * applies to all schemes). */ - private final UUID uuid; + public final UUID uuid; /** The URL of the server to which license requests should be made. May be null if unknown. */ @Nullable public final String licenseServerUrl; /** The mimeType of {@link #data}. */ diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/ExoMediaCrypto.java b/library/common/src/main/java/com/google/android/exoplayer2/drm/ExoMediaCrypto.java similarity index 100% rename from library/core/src/main/java/com/google/android/exoplayer2/drm/ExoMediaCrypto.java rename to library/common/src/main/java/com/google/android/exoplayer2/drm/ExoMediaCrypto.java diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/package-info.java b/library/common/src/main/java/com/google/android/exoplayer2/drm/package-info.java similarity index 100% rename from library/core/src/main/java/com/google/android/exoplayer2/drm/package-info.java rename to library/common/src/main/java/com/google/android/exoplayer2/drm/package-info.java diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/Metadata.java b/library/common/src/main/java/com/google/android/exoplayer2/metadata/Metadata.java similarity index 88% rename from library/core/src/main/java/com/google/android/exoplayer2/metadata/Metadata.java rename to library/common/src/main/java/com/google/android/exoplayer2/metadata/Metadata.java index 35702da576..046c1fef55 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/Metadata.java +++ b/library/common/src/main/java/com/google/android/exoplayer2/metadata/Metadata.java @@ -22,7 +22,6 @@ import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.util.Util; import java.util.Arrays; import java.util.List; -import org.checkerframework.checker.nullness.compatqual.NullableType; /** * A collection of metadata entries. @@ -57,19 +56,15 @@ public final class Metadata implements Parcelable { * @param entries The metadata entries. */ public Metadata(Entry... entries) { - this.entries = entries == null ? new Entry[0] : entries; + this.entries = entries; } /** * @param entries The metadata entries. */ public Metadata(List entries) { - if (entries != null) { - this.entries = new Entry[entries.size()]; - entries.toArray(this.entries); - } else { - this.entries = new Entry[0]; - } + this.entries = new Entry[entries.size()]; + entries.toArray(this.entries); } /* package */ Metadata(Parcel in) { @@ -118,9 +113,10 @@ public final class Metadata implements Parcelable { * @return The metadata instance with the appended entries. */ public Metadata copyWithAppendedEntries(Entry... entriesToAppend) { - @NullableType Entry[] merged = Arrays.copyOf(entries, entries.length + entriesToAppend.length); - System.arraycopy(entriesToAppend, 0, merged, entries.length, entriesToAppend.length); - return new Metadata(Util.castNonNullTypeArray(merged)); + if (entriesToAppend.length == 0) { + return this; + } + return new Metadata(Util.nullSafeArrayConcatenation(entries, entriesToAppend)); } @Override diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/MetadataDecoder.java b/library/common/src/main/java/com/google/android/exoplayer2/metadata/MetadataDecoder.java similarity index 81% rename from library/core/src/main/java/com/google/android/exoplayer2/metadata/MetadataDecoder.java rename to library/common/src/main/java/com/google/android/exoplayer2/metadata/MetadataDecoder.java index 1d95d32290..dee0db5a8e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/MetadataDecoder.java +++ b/library/common/src/main/java/com/google/android/exoplayer2/metadata/MetadataDecoder.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.metadata; import androidx.annotation.Nullable; +import java.nio.ByteBuffer; /** * Decodes metadata from binary data. @@ -25,6 +26,10 @@ public interface MetadataDecoder { /** * Decodes a {@link Metadata} element from the provided input buffer. * + *

Respects {@link ByteBuffer#limit()} of {@code inputBuffer.data}, but assumes {@link + * ByteBuffer#position()} and {@link ByteBuffer#arrayOffset()} are both zero and {@link + * ByteBuffer#hasArray()} is true. + * * @param inputBuffer The input buffer to decode. * @return The decoded metadata object, or null if the metadata could not be decoded. */ diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/MetadataInputBuffer.java b/library/common/src/main/java/com/google/android/exoplayer2/metadata/MetadataInputBuffer.java similarity index 100% rename from library/core/src/main/java/com/google/android/exoplayer2/metadata/MetadataInputBuffer.java rename to library/common/src/main/java/com/google/android/exoplayer2/metadata/MetadataInputBuffer.java diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessage.java b/library/common/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessage.java similarity index 95% rename from library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessage.java rename to library/common/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessage.java index 7e3862ca31..0dd46bae22 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessage.java +++ b/library/common/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessage.java @@ -50,11 +50,9 @@ public final class EventMessage implements Metadata.Entry { @VisibleForTesting public static final String SCTE35_SCHEME_ID = "urn:scte:scte35:2014:bin"; private static final Format ID3_FORMAT = - Format.createSampleFormat( - /* id= */ null, MimeTypes.APPLICATION_ID3, Format.OFFSET_SAMPLE_RELATIVE); + new Format.Builder().setSampleMimeType(MimeTypes.APPLICATION_ID3).build(); private static final Format SCTE35_FORMAT = - Format.createSampleFormat( - /* id= */ null, MimeTypes.APPLICATION_SCTE35, Format.OFFSET_SAMPLE_RELATIVE); + new Format.Builder().setSampleMimeType(MimeTypes.APPLICATION_SCTE35).build(); /** The message scheme. */ public final String schemeIdUri; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessageDecoder.java b/library/common/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessageDecoder.java similarity index 90% rename from library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessageDecoder.java rename to library/common/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessageDecoder.java index d87376feb0..c03a5cb038 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessageDecoder.java +++ b/library/common/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessageDecoder.java @@ -26,13 +26,12 @@ import java.util.Arrays; /** Decodes data encoded by {@link EventMessageEncoder}. */ public final class EventMessageDecoder implements MetadataDecoder { - @SuppressWarnings("ByteBufferBackingArray") @Override public Metadata decode(MetadataInputBuffer inputBuffer) { ByteBuffer buffer = Assertions.checkNotNull(inputBuffer.data); - byte[] data = buffer.array(); - int size = buffer.limit(); - return new Metadata(decode(new ParsableByteArray(data, size))); + Assertions.checkArgument( + buffer.position() == 0 && buffer.hasArray() && buffer.arrayOffset() == 0); + return new Metadata(decode(new ParsableByteArray(buffer.array(), buffer.limit()))); } public EventMessage decode(ParsableByteArray emsgData) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessageEncoder.java b/library/common/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessageEncoder.java similarity index 100% rename from library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessageEncoder.java rename to library/common/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessageEncoder.java diff --git a/library/common/src/main/java/com/google/android/exoplayer2/metadata/emsg/package-info.java b/library/common/src/main/java/com/google/android/exoplayer2/metadata/emsg/package-info.java new file mode 100644 index 0000000000..2b03ce8df3 --- /dev/null +++ b/library/common/src/main/java/com/google/android/exoplayer2/metadata/emsg/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2019 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. + */ +@NonNullApi +package com.google.android.exoplayer2.metadata.emsg; + +import com.google.android.exoplayer2.util.NonNullApi; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/flac/PictureFrame.java b/library/common/src/main/java/com/google/android/exoplayer2/metadata/flac/PictureFrame.java similarity index 100% rename from library/core/src/main/java/com/google/android/exoplayer2/metadata/flac/PictureFrame.java rename to library/common/src/main/java/com/google/android/exoplayer2/metadata/flac/PictureFrame.java diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/flac/VorbisComment.java b/library/common/src/main/java/com/google/android/exoplayer2/metadata/flac/VorbisComment.java similarity index 100% rename from library/core/src/main/java/com/google/android/exoplayer2/metadata/flac/VorbisComment.java rename to library/common/src/main/java/com/google/android/exoplayer2/metadata/flac/VorbisComment.java diff --git a/library/common/src/main/java/com/google/android/exoplayer2/metadata/flac/package-info.java b/library/common/src/main/java/com/google/android/exoplayer2/metadata/flac/package-info.java new file mode 100644 index 0000000000..343ab232e0 --- /dev/null +++ b/library/common/src/main/java/com/google/android/exoplayer2/metadata/flac/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2019 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. + */ +@NonNullApi +package com.google.android.exoplayer2.metadata.flac; + +import com.google.android.exoplayer2.util.NonNullApi; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/ApicFrame.java b/library/common/src/main/java/com/google/android/exoplayer2/metadata/id3/ApicFrame.java similarity index 98% rename from library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/ApicFrame.java rename to library/common/src/main/java/com/google/android/exoplayer2/metadata/id3/ApicFrame.java index d4bedc63cc..3f4a400677 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/ApicFrame.java +++ b/library/common/src/main/java/com/google/android/exoplayer2/metadata/id3/ApicFrame.java @@ -47,7 +47,7 @@ public final class ApicFrame extends Id3Frame { /* package */ ApicFrame(Parcel in) { super(ID); mimeType = castNonNull(in.readString()); - description = castNonNull(in.readString()); + description = in.readString(); pictureType = in.readInt(); pictureData = castNonNull(in.createByteArray()); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/BinaryFrame.java b/library/common/src/main/java/com/google/android/exoplayer2/metadata/id3/BinaryFrame.java similarity index 100% rename from library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/BinaryFrame.java rename to library/common/src/main/java/com/google/android/exoplayer2/metadata/id3/BinaryFrame.java diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/ChapterFrame.java b/library/common/src/main/java/com/google/android/exoplayer2/metadata/id3/ChapterFrame.java similarity index 100% rename from library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/ChapterFrame.java rename to library/common/src/main/java/com/google/android/exoplayer2/metadata/id3/ChapterFrame.java diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/ChapterTocFrame.java b/library/common/src/main/java/com/google/android/exoplayer2/metadata/id3/ChapterTocFrame.java similarity index 100% rename from library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/ChapterTocFrame.java rename to library/common/src/main/java/com/google/android/exoplayer2/metadata/id3/ChapterTocFrame.java diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/CommentFrame.java b/library/common/src/main/java/com/google/android/exoplayer2/metadata/id3/CommentFrame.java similarity index 100% rename from library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/CommentFrame.java rename to library/common/src/main/java/com/google/android/exoplayer2/metadata/id3/CommentFrame.java diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/GeobFrame.java b/library/common/src/main/java/com/google/android/exoplayer2/metadata/id3/GeobFrame.java similarity index 100% rename from library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/GeobFrame.java rename to library/common/src/main/java/com/google/android/exoplayer2/metadata/id3/GeobFrame.java diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Decoder.java b/library/common/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Decoder.java similarity index 97% rename from library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Decoder.java rename to library/common/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Decoder.java index ba0968cbd4..84a316e848 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Decoder.java +++ b/library/common/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Decoder.java @@ -96,11 +96,12 @@ public final class Id3Decoder implements MetadataDecoder { this.framePredicate = framePredicate; } - @SuppressWarnings("ByteBufferBackingArray") @Override @Nullable public Metadata decode(MetadataInputBuffer inputBuffer) { ByteBuffer buffer = Assertions.checkNotNull(inputBuffer.data); + Assertions.checkArgument( + buffer.position() == 0 && buffer.hasArray() && buffer.arrayOffset() == 0); return decode(buffer.array(), buffer.limit()); } @@ -155,7 +156,8 @@ 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 @Nullable Id3Header decodeHeader(ParsableByteArray data) { + @Nullable + private static Id3Header decodeHeader(ParsableByteArray data) { if (data.bytesLeft() < ID3_HEADER_LENGTH) { Log.w(TAG, "Data too short to be an ID3 tag"); return null; @@ -269,7 +271,8 @@ public final class Id3Decoder implements MetadataDecoder { } } - private static @Nullable Id3Frame decodeFrame( + @Nullable + private static Id3Frame decodeFrame( int majorVersion, ParsableByteArray id3Data, boolean unsignedIntFrameSizeHack, @@ -404,8 +407,9 @@ public final class Id3Decoder implements MetadataDecoder { } } - private static @Nullable TextInformationFrame decodeTxxxFrame( - ParsableByteArray id3Data, int frameSize) throws UnsupportedEncodingException { + @Nullable + private static TextInformationFrame decodeTxxxFrame(ParsableByteArray id3Data, int frameSize) + throws UnsupportedEncodingException { if (frameSize < 1) { // Frame is malformed. return null; @@ -427,7 +431,8 @@ public final class Id3Decoder implements MetadataDecoder { return new TextInformationFrame("TXXX", description, value); } - private static @Nullable TextInformationFrame decodeTextInformationFrame( + @Nullable + private static TextInformationFrame decodeTextInformationFrame( ParsableByteArray id3Data, int frameSize, String id) throws UnsupportedEncodingException { if (frameSize < 1) { // Frame is malformed. @@ -446,7 +451,8 @@ public final class Id3Decoder implements MetadataDecoder { return new TextInformationFrame(id, null, value); } - private static @Nullable UrlLinkFrame decodeWxxxFrame(ParsableByteArray id3Data, int frameSize) + @Nullable + private static UrlLinkFrame decodeWxxxFrame(ParsableByteArray id3Data, int frameSize) throws UnsupportedEncodingException { if (frameSize < 1) { // Frame is malformed. @@ -557,7 +563,8 @@ public final class Id3Decoder implements MetadataDecoder { return new ApicFrame(mimeType, description, pictureType, pictureData); } - private static @Nullable CommentFrame decodeCommentFrame(ParsableByteArray id3Data, int frameSize) + @Nullable + private static CommentFrame decodeCommentFrame(ParsableByteArray id3Data, int frameSize) throws UnsupportedEncodingException { if (frameSize < 4) { // Frame is malformed. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Frame.java b/library/common/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Frame.java similarity index 100% rename from library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Frame.java rename to library/common/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Frame.java diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/InternalFrame.java b/library/common/src/main/java/com/google/android/exoplayer2/metadata/id3/InternalFrame.java similarity index 100% rename from library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/InternalFrame.java rename to library/common/src/main/java/com/google/android/exoplayer2/metadata/id3/InternalFrame.java diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/MlltFrame.java b/library/common/src/main/java/com/google/android/exoplayer2/metadata/id3/MlltFrame.java similarity index 100% rename from library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/MlltFrame.java rename to library/common/src/main/java/com/google/android/exoplayer2/metadata/id3/MlltFrame.java diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/PrivFrame.java b/library/common/src/main/java/com/google/android/exoplayer2/metadata/id3/PrivFrame.java similarity index 100% rename from library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/PrivFrame.java rename to library/common/src/main/java/com/google/android/exoplayer2/metadata/id3/PrivFrame.java diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/TextInformationFrame.java b/library/common/src/main/java/com/google/android/exoplayer2/metadata/id3/TextInformationFrame.java similarity index 100% rename from library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/TextInformationFrame.java rename to library/common/src/main/java/com/google/android/exoplayer2/metadata/id3/TextInformationFrame.java diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/UrlLinkFrame.java b/library/common/src/main/java/com/google/android/exoplayer2/metadata/id3/UrlLinkFrame.java similarity index 100% rename from library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/UrlLinkFrame.java rename to library/common/src/main/java/com/google/android/exoplayer2/metadata/id3/UrlLinkFrame.java diff --git a/library/common/src/main/java/com/google/android/exoplayer2/metadata/id3/package-info.java b/library/common/src/main/java/com/google/android/exoplayer2/metadata/id3/package-info.java new file mode 100644 index 0000000000..8422071842 --- /dev/null +++ b/library/common/src/main/java/com/google/android/exoplayer2/metadata/id3/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2019 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. + */ +@NonNullApi +package com.google.android.exoplayer2.metadata.id3; + +import com.google.android.exoplayer2.util.NonNullApi; diff --git a/library/common/src/main/java/com/google/android/exoplayer2/metadata/package-info.java b/library/common/src/main/java/com/google/android/exoplayer2/metadata/package-info.java new file mode 100644 index 0000000000..a55cc1b6b3 --- /dev/null +++ b/library/common/src/main/java/com/google/android/exoplayer2/metadata/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2019 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. + */ +@NonNullApi +package com.google.android.exoplayer2.metadata; + +import com.google.android.exoplayer2.util.NonNullApi; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/StreamKey.java b/library/common/src/main/java/com/google/android/exoplayer2/offline/StreamKey.java similarity index 100% rename from library/core/src/main/java/com/google/android/exoplayer2/offline/StreamKey.java rename to library/common/src/main/java/com/google/android/exoplayer2/offline/StreamKey.java diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/package-info.java b/library/common/src/main/java/com/google/android/exoplayer2/offline/package-info.java similarity index 100% rename from library/core/src/main/java/com/google/android/exoplayer2/offline/package-info.java rename to library/common/src/main/java/com/google/android/exoplayer2/offline/package-info.java diff --git a/library/core/src/main/java/com/google/android/exoplayer2/package-info.java b/library/common/src/main/java/com/google/android/exoplayer2/package-info.java similarity index 100% rename from library/core/src/main/java/com/google/android/exoplayer2/package-info.java rename to library/common/src/main/java/com/google/android/exoplayer2/package-info.java diff --git a/library/common/src/main/java/com/google/android/exoplayer2/upstream/DataReader.java b/library/common/src/main/java/com/google/android/exoplayer2/upstream/DataReader.java new file mode 100644 index 0000000000..eeddc9984e --- /dev/null +++ b/library/common/src/main/java/com/google/android/exoplayer2/upstream/DataReader.java @@ -0,0 +1,40 @@ +/* + * 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.upstream; + +import com.google.android.exoplayer2.C; +import java.io.IOException; + +/** Reads bytes from a data stream. */ +public interface DataReader { + /** + * Reads up to {@code length} bytes of data from the input. + * + *

If {@code readLength} is zero then 0 is returned. Otherwise, if no data is available because + * the end of the opened range has been reached, then {@link C#RESULT_END_OF_INPUT} is returned. + * Otherwise, the call will block until at least one byte of data has been read and the number of + * bytes read is returned. + * + * @param target A target array into which data should be written. + * @param offset The offset into the target array at which to write. + * @param length The maximum number of bytes to read from the input. + * @return The number of bytes read, or {@link C#RESULT_END_OF_INPUT} if the input has ended. This + * may be less than {@code length} because the end of the input (or available data) was + * reached, the method was interrupted, or the operation was aborted early for another reason. + * @throws IOException If an error occurs reading from the input. + */ + int read(byte[] target, int offset, int length) throws IOException; +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSource.java b/library/common/src/main/java/com/google/android/exoplayer2/upstream/DataSource.java similarity index 75% rename from library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSource.java rename to library/common/src/main/java/com/google/android/exoplayer2/upstream/DataSource.java index 204b9d4d66..9a321fbdd8 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSource.java +++ b/library/common/src/main/java/com/google/android/exoplayer2/upstream/DataSource.java @@ -23,10 +23,8 @@ import java.util.Collections; import java.util.List; import java.util.Map; -/** - * A component from which streams of data can be read. - */ -public interface DataSource { +/** Reads data from URI-identified resources. */ +public interface DataSource extends DataReader { /** * A factory for {@link DataSource} instances. @@ -63,24 +61,6 @@ public interface DataSource { */ long open(DataSpec dataSpec) throws IOException; - /** - * Reads up to {@code readLength} bytes of data and stores them into {@code buffer}, starting at - * index {@code offset}. - * - *

If {@code readLength} is zero then 0 is returned. Otherwise, if no data is available because - * the end of the opened range has been reached, then {@link C#RESULT_END_OF_INPUT} is returned. - * Otherwise, the call will block until at least one byte of data has been read and the number of - * bytes read is returned. - * - * @param buffer The buffer into which the read data should be stored. - * @param offset The start offset into {@code buffer} at which data should be written. - * @param readLength The maximum number of bytes to read. - * @return The number of bytes read, or {@link C#RESULT_END_OF_INPUT} if no data is available - * because the end of the opened range has been reached. - * @throws IOException If an error occurs reading from the source. - */ - int read(byte[] buffer, int offset, int readLength) throws IOException; - /** * When the source is open, returns the {@link Uri} from which data is being read. The returned * {@link Uri} will be identical to the one passed {@link #open(DataSpec)} in the {@link DataSpec} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSourceException.java b/library/common/src/main/java/com/google/android/exoplayer2/upstream/DataSourceException.java similarity index 100% rename from library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSourceException.java rename to library/common/src/main/java/com/google/android/exoplayer2/upstream/DataSourceException.java diff --git a/library/common/src/main/java/com/google/android/exoplayer2/upstream/DataSpec.java b/library/common/src/main/java/com/google/android/exoplayer2/upstream/DataSpec.java new file mode 100644 index 0000000000..cdbf3fee7d --- /dev/null +++ b/library/common/src/main/java/com/google/android/exoplayer2/upstream/DataSpec.java @@ -0,0 +1,798 @@ +/* + * 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.upstream; + +import android.net.Uri; +import androidx.annotation.IntDef; +import androidx.annotation.Nullable; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.util.Assertions; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +/** + * Defines a region of data. + */ +public final class DataSpec { + + /** + * Builds {@link DataSpec} instances. + * + *

Use DataSpec#buildUpon() to obtain a builder representing an existing {@link DataSpec}. + */ + public static final class Builder { + + @Nullable private Uri uri; + private long uriPositionOffset; + @HttpMethod private int httpMethod; + @Nullable private byte[] httpBody; + private Map httpRequestHeaders; + private long position; + private long length; + @Nullable private String key; + @Flags private int flags; + @Nullable private Object customData; + + /** Creates a new instance with default values. */ + public Builder() { + httpMethod = HTTP_METHOD_GET; + httpRequestHeaders = Collections.emptyMap(); + length = C.LENGTH_UNSET; + } + + /** + * Creates a new instance to build upon the provided {@link DataSpec}. + * + * @param dataSpec The {@link DataSpec} to build upon. + */ + private Builder(DataSpec dataSpec) { + uri = dataSpec.uri; + uriPositionOffset = dataSpec.uriPositionOffset; + httpMethod = dataSpec.httpMethod; + httpBody = dataSpec.httpBody; + httpRequestHeaders = dataSpec.httpRequestHeaders; + position = dataSpec.position; + length = dataSpec.length; + key = dataSpec.key; + flags = dataSpec.flags; + customData = dataSpec.customData; + } + + /** + * Sets {@link DataSpec#uri}. + * + * @param uriString The {@link DataSpec#uri}. + * @return The builder. + */ + public Builder setUri(String uriString) { + this.uri = Uri.parse(uriString); + return this; + } + + /** + * Sets {@link DataSpec#uri}. + * + * @param uri The {@link DataSpec#uri}. + * @return The builder. + */ + public Builder setUri(Uri uri) { + this.uri = uri; + return this; + } + + /** + * Sets the {@link DataSpec#uriPositionOffset}. The default value is 0. + * + * @param uriPositionOffset The {@link DataSpec#uriPositionOffset}. + * @return The builder. + */ + public Builder setUriPositionOffset(long uriPositionOffset) { + this.uriPositionOffset = uriPositionOffset; + return this; + } + + /** + * Sets {@link DataSpec#httpMethod}. The default value is {@link #HTTP_METHOD_GET}. + * + * @param httpMethod The {@link DataSpec#httpMethod}. + * @return The builder. + */ + public Builder setHttpMethod(@HttpMethod int httpMethod) { + this.httpMethod = httpMethod; + return this; + } + + /** + * Sets {@link DataSpec#httpBody}. The default value is {@code null}. + * + * @param httpBody The {@link DataSpec#httpBody}. + * @return The builder. + */ + public Builder setHttpBody(@Nullable byte[] httpBody) { + this.httpBody = httpBody; + return this; + } + + /** + * Sets the {@link DataSpec#httpRequestHeaders}. The default value is an empty map. + * + *

Note: {@code Range}, {@code Accept-Encoding} and {@code User-Agent} should not be set with + * this method, since they are set directly by {@link HttpDataSource} implementations. See + * {@link DataSpec#httpRequestHeaders} for more details. + * + * @param httpRequestHeaders The {@link DataSpec#httpRequestHeaders}. + * @return The builder. + */ + public Builder setHttpRequestHeaders(Map httpRequestHeaders) { + this.httpRequestHeaders = httpRequestHeaders; + return this; + } + + /** + * Sets the {@link DataSpec#position}. The default value is 0. + * + * @param position The {@link DataSpec#position}. + * @return The builder. + */ + public Builder setPosition(long position) { + this.position = position; + return this; + } + + /** + * Sets the {@link DataSpec#length}. The default value is {@link C#LENGTH_UNSET}. + * + * @param length The {@link DataSpec#length}. + * @return The builder. + */ + public Builder setLength(long length) { + this.length = length; + return this; + } + + /** + * Sets the {@link DataSpec#key}. The default value is {@code null}. + * + * @param key The {@link DataSpec#key}. + * @return The builder. + */ + public Builder setKey(@Nullable String key) { + this.key = key; + return this; + } + + /** + * Sets the {@link DataSpec#flags}. The default value is 0. + * + * @param flags The {@link DataSpec#flags}. + * @return The builder. + */ + public Builder setFlags(@Flags int flags) { + this.flags = flags; + return this; + } + + /** + * Sets the {@link DataSpec#customData}. The default value is {@code null}. + * + * @param customData The {@link DataSpec#customData}. + * @return The builder. + */ + public Builder setCustomData(@Nullable Object customData) { + this.customData = customData; + return this; + } + + /** + * Builds a {@link DataSpec} with the builder's current values. + * + * @return The build {@link DataSpec}. + * @throws IllegalStateException If {@link #setUri} has not been called. + */ + public DataSpec build() { + Assertions.checkStateNotNull(uri, "The uri must be set."); + return new DataSpec( + uri, + uriPositionOffset, + httpMethod, + httpBody, + httpRequestHeaders, + position, + length, + key, + flags, + customData); + } + } + + /** + * The flags that apply to any request for data. Possible flag values are {@link + * #FLAG_ALLOW_GZIP}, {@link #FLAG_DONT_CACHE_IF_LENGTH_UNKNOWN}, {@link + * #FLAG_ALLOW_CACHE_FRAGMENTATION}, and {@link #FLAG_MIGHT_NOT_USE_FULL_NETWORK_SPEED}. + */ + @Documented + @Retention(RetentionPolicy.SOURCE) + @IntDef( + flag = true, + value = { + FLAG_ALLOW_GZIP, + FLAG_DONT_CACHE_IF_LENGTH_UNKNOWN, + FLAG_ALLOW_CACHE_FRAGMENTATION, + FLAG_MIGHT_NOT_USE_FULL_NETWORK_SPEED + }) + public @interface Flags {} + /** + * Allows an underlying network stack to request that the server use gzip compression. + * + *

Should not typically be set if the data being requested is already compressed (e.g. most + * audio and video requests). May be set when requesting other data. + * + *

When a {@link DataSource} is used to request data with this flag set, and if the {@link + * DataSource} does make a network request, then the value returned from {@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; + /** Prevents caching if the length cannot be resolved when the {@link DataSource} is opened. */ + public static final int FLAG_DONT_CACHE_IF_LENGTH_UNKNOWN = 1 << 1; + /** + * Allows fragmentation of this request into multiple cache files, meaning a cache eviction policy + * will be able to evict individual fragments of the data. Depending on the cache implementation, + * setting this flag may also enable more concurrent access to the data (e.g. reading one fragment + * whilst writing another). + */ + public static final int FLAG_ALLOW_CACHE_FRAGMENTATION = 1 << 2; + /** + * Indicates there are known external factors that might prevent the data from being loaded at + * full network speed (e.g. server throttling or unfinished live media chunks). + */ + public static final int FLAG_MIGHT_NOT_USE_FULL_NETWORK_SPEED = 1 << 3; + + /** + * HTTP methods supported by ExoPlayer {@link HttpDataSource}s. One of {@link #HTTP_METHOD_GET}, + * {@link #HTTP_METHOD_POST} or {@link #HTTP_METHOD_HEAD}. + */ + @Documented + @Retention(RetentionPolicy.SOURCE) + @IntDef({HTTP_METHOD_GET, HTTP_METHOD_POST, HTTP_METHOD_HEAD}) + public @interface HttpMethod {} + /** HTTP GET method. */ + public static final int HTTP_METHOD_GET = 1; + /** HTTP POST method. */ + public static final int HTTP_METHOD_POST = 2; + /** HTTP HEAD method. */ + public static final int HTTP_METHOD_HEAD = 3; + + /** + * Returns an uppercase HTTP method name (e.g., "GET", "POST", "HEAD") corresponding to the given + * {@link 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: + // Never happens. + throw new IllegalStateException(); + } + } + + /** The {@link Uri} from which data should be read. */ + public final Uri uri; + + /** + * The offset of the data located at {@link #uri} within an original resource. + * + *

Equal to 0 unless {@link #uri} provides access to a subset of an original resource. As an + * example, consider a resource that can be requested over the network and is 1000 bytes long. If + * {@link #uri} points to a local file that contains just bytes [200-300], then this field will be + * set to {@code 200}. + * + *

This field can be ignored except for in specific circumstances where the absolute position + * in the original resource is required in a {@link DataSource} chain. One example is when a + * {@link DataSource} needs to decrypt the content as it's read. In this case the absolute + * position in the original resource is typically needed to correctly initialize the decryption + * algorithm. + */ + public final long uriPositionOffset; + + /** + * The HTTP method to use when requesting the data. This value will be ignored by non-HTTP {@link + * DataSource} implementations. + */ + @HttpMethod public final int httpMethod; + + /** + * The HTTP request body, null otherwise. If the body is non-null, then {@code httpBody.length} + * will be non-zero. + */ + @Nullable public final byte[] httpBody; + + /** + * Additional HTTP headers to use when requesting the data. + * + *

Note: This map is for additional headers specific to the data being requested. It does not + * include headers that are set directly by {@link HttpDataSource} implementations. In particular, + * this means the following headers are not included: + * + *

    + *
  • {@code Range}: {@link HttpDataSource} implementations derive the {@code Range} header + * from {@link #position} and {@link #length}. + *
  • {@code Accept-Encoding}: {@link HttpDataSource} implementations derive the {@code + * Accept-Encoding} header based on whether {@link #flags} includes {@link + * #FLAG_ALLOW_GZIP}. + *
  • {@code User-Agent}: {@link HttpDataSource} implementations set the {@code User-Agent} + * header directly. + *
  • Other headers set at the {@link HttpDataSource} layer. I.e., headers set using {@link + * HttpDataSource#setRequestProperty(String, String)}, and using {@link + * HttpDataSource.RequestProperties#set(String, String)} on the default properties obtained + * from {@link HttpDataSource.Factory#getDefaultRequestProperties()}. + *
+ */ + public final Map httpRequestHeaders; + + /** + * The absolute position of the data in the full stream. + * + * @deprecated Use {@link #position} except for specific use cases where the absolute position + * within the original resource is required within a {@link DataSource} chain. Where the + * absolute position is required, use {@code uriPositionOffset + position}. + */ + @Deprecated public final long absoluteStreamPosition; + + /** The position of the data when read from {@link #uri}. */ + public final long position; + + /** + * The length of the data, or {@link C#LENGTH_UNSET}. + */ + public final long length; + + /** + * A key that uniquely identifies the original stream. Used for cache indexing. May be null if the + * data spec is not intended to be used in conjunction with a cache. + */ + @Nullable public final String key; + + /** Request {@link Flags flags}. */ + @Flags public final int flags; + + /** + * Application specific data. + * + *

This field is intended for advanced use cases in which applications require the ability to + * attach custom data to {@link DataSpec} instances. The custom data should be immutable. + */ + @Nullable public final Object customData; + + /** + * Constructs an instance. + * + * @param uri {@link #uri}. + */ + public DataSpec(Uri uri) { + this(uri, /* position= */ 0, /* length= */ C.LENGTH_UNSET); + } + + /** + * Constructs an instance. + * + * @param uri {@link #uri}. + * @param position {@link #position}. + * @param length {@link #length}. + */ + public DataSpec(Uri uri, long position, long length) { + this( + uri, + /* uriPositionOffset= */ 0, + HTTP_METHOD_GET, + /* httpBody= */ null, + /* httpRequestHeaders= */ Collections.emptyMap(), + position, + length, + /* key= */ null, + /* flags= */ 0, + /* customData= */ null); + } + + /** + * Constructs an instance. + * + * @deprecated Use {@link Builder}. + * @param uri {@link #uri}. + * @param flags {@link #flags}. + */ + @SuppressWarnings("deprecation") + @Deprecated + public DataSpec(Uri uri, @Flags int flags) { + this(uri, /* position= */ 0, C.LENGTH_UNSET, /* key= */ null, flags); + } + + /** + * Constructs an instance. + * + * @deprecated Use {@link Builder}. + * @param uri {@link #uri}. + * @param position {@link #position}. + * @param length {@link #length}. + * @param key {@link #key}. + */ + @SuppressWarnings("deprecation") + @Deprecated + public DataSpec(Uri uri, long position, long length, @Nullable String key) { + this(uri, position, position, length, key, /* flags= */ 0); + } + + /** + * Constructs an instance. + * + * @deprecated Use {@link Builder}. + * @param uri {@link #uri}. + * @param position {@link #position}. + * @param length {@link #length}. + * @param key {@link #key}. + * @param flags {@link #flags}. + */ + @SuppressWarnings("deprecation") + @Deprecated + public DataSpec(Uri uri, long position, long length, @Nullable String key, @Flags int flags) { + this(uri, position, position, length, key, flags); + } + + /** + * Constructs an instance. + * + * @deprecated Use {@link Builder}. + * @param uri {@link #uri}. + * @param position {@link #position}, equal to {@link #position}. + * @param length {@link #length}. + * @param key {@link #key}. + * @param flags {@link #flags}. + * @param httpRequestHeaders {@link #httpRequestHeaders} + */ + @SuppressWarnings("deprecation") + @Deprecated + public DataSpec( + Uri uri, + long position, + long length, + @Nullable String key, + @Flags int flags, + Map httpRequestHeaders) { + this( + uri, + HTTP_METHOD_GET, + /* httpBody= */ null, + position, + position, + length, + key, + flags, + httpRequestHeaders); + } + + /** + * Constructs an instance where {@link #uriPositionOffset} may be non-zero. + * + * @deprecated Use {@link Builder}. + * @param uri {@link #uri}. + * @param absoluteStreamPosition The sum of {@link #uriPositionOffset} and {@link #position}. + * @param position {@link #position}. + * @param length {@link #length}. + * @param key {@link #key}. + * @param flags {@link #flags}. + */ + @SuppressWarnings("deprecation") + @Deprecated + public DataSpec( + Uri uri, + long absoluteStreamPosition, + long position, + long length, + @Nullable String key, + @Flags int flags) { + this(uri, /* postBody= */ null, absoluteStreamPosition, position, length, key, flags); + } + + /** + * Construct a instance where {@link #uriPositionOffset} may be non-zero. The {@link #httpMethod} + * is inferred from {@code postBody}. If {@code postBody} is non-null then {@link #httpMethod} is + * set to {@link #HTTP_METHOD_POST}. If {@code postBody} is null then {@link #httpMethod} is set + * to {@link #HTTP_METHOD_GET}. + * + * @deprecated Use {@link Builder}. + * @param uri {@link #uri}. + * @param postBody {@link #httpBody} The body of the HTTP request, which is also used to infer the + * {@link #httpMethod}. + * @param absoluteStreamPosition The sum of {@link #uriPositionOffset} and {@link #position}. + * @param position {@link #position}. + * @param length {@link #length}. + * @param key {@link #key}. + * @param flags {@link #flags}. + */ + @SuppressWarnings("deprecation") + @Deprecated + public DataSpec( + Uri uri, + @Nullable byte[] postBody, + long absoluteStreamPosition, + long position, + 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 instance where {@link #uriPositionOffset} may be non-zero. + * + * @deprecated Use {@link Builder}. + * @param uri {@link #uri}. + * @param httpMethod {@link #httpMethod}. + * @param httpBody {@link #httpBody}. + * @param absoluteStreamPosition The sum of {@link #uriPositionOffset} and {@link #position}. + * @param position {@link #position}. + * @param length {@link #length}. + * @param key {@link #key}. + * @param flags {@link #flags}. + */ + @SuppressWarnings("deprecation") + @Deprecated + public DataSpec( + Uri uri, + @HttpMethod int httpMethod, + @Nullable byte[] httpBody, + long absoluteStreamPosition, + long position, + long length, + @Nullable String key, + @Flags int flags) { + this( + uri, + httpMethod, + httpBody, + absoluteStreamPosition, + position, + length, + key, + flags, + /* httpRequestHeaders= */ Collections.emptyMap()); + } + + /** + * Construct a instance where {@link #uriPositionOffset} may be non-zero. + * + * @deprecated Use {@link Builder}. + * @param uri {@link #uri}. + * @param httpMethod {@link #httpMethod}. + * @param httpBody {@link #httpBody}. + * @param absoluteStreamPosition The sum of {@link #uriPositionOffset} and {@link #position}. + * @param position {@link #position}. + * @param length {@link #length}. + * @param key {@link #key}. + * @param flags {@link #flags}. + * @param httpRequestHeaders {@link #httpRequestHeaders}. + */ + @Deprecated + public DataSpec( + Uri uri, + @HttpMethod int httpMethod, + @Nullable byte[] httpBody, + long absoluteStreamPosition, + long position, + long length, + @Nullable String key, + @Flags int flags, + Map httpRequestHeaders) { + this( + uri, + /* uriPositionOffset= */ absoluteStreamPosition - position, + httpMethod, + httpBody, + httpRequestHeaders, + position, + length, + key, + flags, + /* customData= */ null); + } + + @SuppressWarnings("deprecation") + private DataSpec( + Uri uri, + long uriPositionOffset, + @HttpMethod int httpMethod, + @Nullable byte[] httpBody, + Map httpRequestHeaders, + long position, + long length, + @Nullable String key, + @Flags int flags, + @Nullable Object customData) { + // TODO: Replace this assertion with a stricter one checking "uriPositionOffset >= 0", after + // validating there are no violations in ExoPlayer and 1P apps. + Assertions.checkArgument(uriPositionOffset + position >= 0); + Assertions.checkArgument(position >= 0); + Assertions.checkArgument(length > 0 || length == C.LENGTH_UNSET); + this.uri = uri; + this.uriPositionOffset = uriPositionOffset; + this.httpMethod = httpMethod; + this.httpBody = httpBody != null && httpBody.length != 0 ? httpBody : null; + this.httpRequestHeaders = Collections.unmodifiableMap(new HashMap<>(httpRequestHeaders)); + this.position = position; + this.absoluteStreamPosition = uriPositionOffset + position; + this.length = length; + this.key = key; + this.flags = flags; + this.customData = customData; + } + + /** + * Returns whether the given flag is set. + * + * @param flag Flag to be checked if it is set. + */ + public boolean isFlagSet(@Flags int flag) { + return (this.flags & flag) == flag; + } + + /** + * Returns the uppercase HTTP method name (e.g., "GET", "POST", "HEAD") corresponding to the + * {@link #httpMethod}. + */ + public final String getHttpMethodString() { + return getStringForHttpMethod(httpMethod); + } + + /** Returns a {@link DataSpec.Builder} initialized with the values of this instance. */ + public DataSpec.Builder buildUpon() { + return new Builder(this); + } + + /** + * Returns a data spec that represents a subrange of the data defined by this DataSpec. The + * subrange includes data from the offset up to the end of this DataSpec. + * + * @param offset The offset of the subrange. + * @return A data spec that represents a subrange of the data defined by this DataSpec. + */ + public DataSpec subrange(long offset) { + return subrange(offset, length == C.LENGTH_UNSET ? C.LENGTH_UNSET : length - offset); + } + + /** + * Returns a data spec that represents a subrange of the data defined by this DataSpec. + * + * @param offset The offset of the subrange. + * @param length The length of the subrange. + * @return A data spec that represents a subrange of the data defined by this DataSpec. + */ + public DataSpec subrange(long offset, long length) { + if (offset == 0 && this.length == length) { + return this; + } else { + return new DataSpec( + uri, + uriPositionOffset, + httpMethod, + httpBody, + httpRequestHeaders, + position + offset, + length, + key, + flags, + customData); + } + } + + /** + * Returns a copy of this data spec with the specified Uri. + * + * @param uri The new source {@link Uri}. + * @return The copied data spec with the specified Uri. + */ + public DataSpec withUri(Uri uri) { + return new DataSpec( + uri, + uriPositionOffset, + httpMethod, + httpBody, + httpRequestHeaders, + position, + length, + key, + flags, + customData); + } + + /** + * Returns a copy of this data spec with the specified HTTP request headers. Headers already in + * the data spec are not copied to the new instance. + * + * @param httpRequestHeaders The HTTP request headers. + * @return The copied data spec with the specified HTTP request headers. + */ + public DataSpec withRequestHeaders(Map httpRequestHeaders) { + return new DataSpec( + uri, + uriPositionOffset, + httpMethod, + httpBody, + httpRequestHeaders, + position, + length, + key, + flags, + customData); + } + + /** + * Returns a copy this data spec with additional HTTP request headers. Headers in {@code + * additionalHttpRequestHeaders} will overwrite any headers already in the data spec that have the + * same keys. + * + * @param additionalHttpRequestHeaders The additional HTTP request headers. + * @return The copied data spec with the additional HTTP request headers. + */ + public DataSpec withAdditionalHeaders(Map additionalHttpRequestHeaders) { + Map httpRequestHeaders = new HashMap<>(this.httpRequestHeaders); + httpRequestHeaders.putAll(additionalHttpRequestHeaders); + return new DataSpec( + uri, + uriPositionOffset, + httpMethod, + httpBody, + httpRequestHeaders, + position, + length, + key, + flags, + customData); + } + + @Override + public String toString() { + return "DataSpec[" + + getHttpMethodString() + + " " + + uri + + ", " + + position + + ", " + + length + + ", " + + key + + ", " + + flags + + "]"; + } +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/HttpDataSource.java b/library/common/src/main/java/com/google/android/exoplayer2/upstream/HttpDataSource.java similarity index 99% rename from library/core/src/main/java/com/google/android/exoplayer2/upstream/HttpDataSource.java rename to library/common/src/main/java/com/google/android/exoplayer2/upstream/HttpDataSource.java index 63cad8786b..9d4f9b6811 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/HttpDataSource.java +++ b/library/common/src/main/java/com/google/android/exoplayer2/upstream/HttpDataSource.java @@ -89,7 +89,7 @@ public interface HttpDataSource extends DataSource { final class RequestProperties { private final Map requestProperties; - private Map requestPropertiesSnapshot; + @Nullable private Map requestPropertiesSnapshot; public RequestProperties() { requestProperties = new HashMap<>(); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/TransferListener.java b/library/common/src/main/java/com/google/android/exoplayer2/upstream/TransferListener.java similarity index 100% rename from library/core/src/main/java/com/google/android/exoplayer2/upstream/TransferListener.java rename to library/common/src/main/java/com/google/android/exoplayer2/upstream/TransferListener.java diff --git a/library/common/src/main/java/com/google/android/exoplayer2/upstream/package-info.java b/library/common/src/main/java/com/google/android/exoplayer2/upstream/package-info.java new file mode 100644 index 0000000000..1fb49d4b96 --- /dev/null +++ b/library/common/src/main/java/com/google/android/exoplayer2/upstream/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2019 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. + */ +@NonNullApi +package com.google.android.exoplayer2.upstream; + +import com.google.android.exoplayer2.util.NonNullApi; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/Assertions.java b/library/common/src/main/java/com/google/android/exoplayer2/util/Assertions.java similarity index 100% rename from library/core/src/main/java/com/google/android/exoplayer2/util/Assertions.java rename to library/common/src/main/java/com/google/android/exoplayer2/util/Assertions.java diff --git a/library/common/src/main/java/com/google/android/exoplayer2/util/CodecSpecificDataUtil.java b/library/common/src/main/java/com/google/android/exoplayer2/util/CodecSpecificDataUtil.java new file mode 100644 index 0000000000..3360e88d4f --- /dev/null +++ b/library/common/src/main/java/com/google/android/exoplayer2/util/CodecSpecificDataUtil.java @@ -0,0 +1,172 @@ +/* + * 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.util; + +import android.util.Pair; +import androidx.annotation.Nullable; +import com.google.android.exoplayer2.C; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** Provides utilities for handling various types of codec-specific data. */ +public final class CodecSpecificDataUtil { + + private static final byte[] NAL_START_CODE = new byte[] {0, 0, 0, 1}; + + /** + * Parses an ALAC AudioSpecificConfig (i.e. an ALACSpecificConfig). + * + * @param audioSpecificConfig A byte array containing the AudioSpecificConfig to parse. + * @return A pair consisting of the sample rate in Hz and the channel count. + */ + public static Pair parseAlacAudioSpecificConfig(byte[] audioSpecificConfig) { + ParsableByteArray byteArray = new ParsableByteArray(audioSpecificConfig); + byteArray.setPosition(9); + int channelCount = byteArray.readUnsignedByte(); + byteArray.setPosition(20); + int sampleRate = byteArray.readUnsignedIntToInt(); + return Pair.create(sampleRate, channelCount); + } + + /** + * Returns initialization data for formats with MIME type {@link MimeTypes#APPLICATION_CEA708}. + * + * @param isWideAspectRatio Whether the CEA-708 closed caption service is formatted for displays + * with 16:9 aspect ratio. + * @return Initialization data for formats with MIME type {@link MimeTypes#APPLICATION_CEA708}. + */ + public static List buildCea708InitializationData(boolean isWideAspectRatio) { + return Collections.singletonList(isWideAspectRatio ? new byte[] {1} : new byte[] {0}); + } + + /** + * Returns whether the CEA-708 closed caption service with the given initialization data is + * formatted for displays with 16:9 aspect ratio. + * + * @param initializationData The initialization data to parse. + * @return Whether the CEA-708 closed caption service is formatted for displays with 16:9 aspect + * ratio. + */ + public static boolean parseCea708InitializationData(List initializationData) { + return initializationData.size() == 1 + && initializationData.get(0).length == 1 + && initializationData.get(0)[0] == 1; + } + + /** + * 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. + * + * @param data An array containing the data that should follow the NAL start code. + * @param offset The start offset into {@code data}. + * @param length The number of bytes to copy from {@code data} + * @return The constructed NAL unit. + */ + public static byte[] buildNalUnit(byte[] data, int offset, int length) { + byte[] nalUnit = new byte[length + NAL_START_CODE.length]; + System.arraycopy(NAL_START_CODE, 0, nalUnit, 0, NAL_START_CODE.length); + System.arraycopy(data, offset, nalUnit, NAL_START_CODE.length, length); + return nalUnit; + } + + /** + * Splits an array of NAL units. + * + *

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. + * + * @param data An array of data. + * @return The individual NAL units, or null if the input did not consist of NAL start code + * delimited units. + */ + @Nullable + public static byte[][] splitNalUnits(byte[] data) { + if (!isNalStartCode(data, 0)) { + // data does not consist of NAL start code delimited units. + return null; + } + List starts = new ArrayList<>(); + int nalUnitIndex = 0; + do { + starts.add(nalUnitIndex); + nalUnitIndex = findNalStartCode(data, nalUnitIndex + NAL_START_CODE.length); + } while (nalUnitIndex != C.INDEX_UNSET); + byte[][] split = new byte[starts.size()][]; + for (int i = 0; i < starts.size(); i++) { + int startIndex = starts.get(i); + int endIndex = i < starts.size() - 1 ? starts.get(i + 1) : data.length; + byte[] nal = new byte[endIndex - startIndex]; + System.arraycopy(data, startIndex, nal, 0, nal.length); + split[i] = nal; + } + return split; + } + + /** + * Finds the next occurrence of the NAL start code from a given index. + * + * @param data The data in which to search. + * @param index The first index to test. + * @return The index of the first byte of the found start code, or {@link C#INDEX_UNSET}. + */ + private static int findNalStartCode(byte[] data, int index) { + int endIndex = data.length - NAL_START_CODE.length; + for (int i = index; i <= endIndex; i++) { + if (isNalStartCode(data, i)) { + return i; + } + } + return C.INDEX_UNSET; + } + + /** + * Tests whether there exists a NAL start code at a given index. + * + * @param data The data. + * @param index The index to test. + * @return Whether there exists a start code that begins at {@code index}. + */ + private static boolean isNalStartCode(byte[] data, int index) { + if (data.length - index <= NAL_START_CODE.length) { + return false; + } + for (int j = 0; j < NAL_START_CODE.length; j++) { + if (data[index + j] != NAL_START_CODE[j]) { + return false; + } + } + return true; + } + + private CodecSpecificDataUtil() {} +} diff --git a/library/common/src/main/java/com/google/android/exoplayer2/util/CopyOnWriteMultiset.java b/library/common/src/main/java/com/google/android/exoplayer2/util/CopyOnWriteMultiset.java new file mode 100644 index 0000000000..e8eb0d0df9 --- /dev/null +++ b/library/common/src/main/java/com/google/android/exoplayer2/util/CopyOnWriteMultiset.java @@ -0,0 +1,139 @@ +/* + * Copyright (C) 2020 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 androidx.annotation.GuardedBy; +import androidx.annotation.Nullable; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * An unordered collection of elements that allows duplicates, but also allows access to a set of + * unique elements. + * + *

This class is thread-safe using the same method as {@link + * java.util.concurrent.CopyOnWriteArrayList}. Mutation methods cause the underlying data to be + * copied. {@link #elementSet()} and {@link #iterator()} return snapshots that are unaffected by + * subsequent mutations. + * + *

Iterating directly on this class reveals duplicate elements. Unique elements can be accessed + * via {@link #elementSet()}. Iteration order for both of these is not defined. + * + * @param The type of element being stored. + */ +public final class CopyOnWriteMultiset implements Iterable { + + private final Object lock; + + @GuardedBy("lock") + private final Map elementCounts; + + @GuardedBy("lock") + private Set elementSet; + + @GuardedBy("lock") + private List elements; + + public CopyOnWriteMultiset() { + lock = new Object(); + elementCounts = new HashMap<>(); + elementSet = Collections.emptySet(); + elements = Collections.emptyList(); + } + + /** + * Adds {@code element} to the multiset. + * + * @param element The element to be added. + */ + public void add(E element) { + synchronized (lock) { + List elements = new ArrayList<>(this.elements); + elements.add(element); + this.elements = Collections.unmodifiableList(elements); + + @Nullable Integer count = elementCounts.get(element); + if (count == null) { + Set elementSet = new HashSet<>(this.elementSet); + elementSet.add(element); + this.elementSet = Collections.unmodifiableSet(elementSet); + } + elementCounts.put(element, count != null ? count + 1 : 1); + } + } + + /** + * Removes {@code element} from the multiset. + * + * @param element The element to be removed. + */ + public void remove(E element) { + synchronized (lock) { + @Nullable Integer count = elementCounts.get(element); + if (count == null) { + return; + } + + List elements = new ArrayList<>(this.elements); + elements.remove(element); + this.elements = Collections.unmodifiableList(elements); + + if (count == 1) { + elementCounts.remove(element); + Set elementSet = new HashSet<>(this.elementSet); + elementSet.remove(element); + this.elementSet = Collections.unmodifiableSet(elementSet); + } else { + elementCounts.put(element, count - 1); + } + } + } + + /** + * Returns a snapshot of the unique elements currently in this multiset. + * + *

Changes to the underlying multiset are not reflected in the returned value. + * + * @return An unmodifiable set containing the unique elements in this multiset. + */ + public Set elementSet() { + synchronized (lock) { + return elementSet; + } + } + + /** + * Returns an iterator over a snapshot of all the elements currently in this multiset (including + * duplicates). + * + *

Changes to the underlying multiset are not reflected in the returned value. + * + * @return An unmodifiable iterator over all the elements in this multiset (including duplicates). + */ + @Override + public Iterator iterator() { + synchronized (lock) { + return elements.iterator(); + } + } +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/FlacConstants.java b/library/common/src/main/java/com/google/android/exoplayer2/util/FlacConstants.java similarity index 77% rename from library/core/src/main/java/com/google/android/exoplayer2/util/FlacConstants.java rename to library/common/src/main/java/com/google/android/exoplayer2/util/FlacConstants.java index 75b153d6f9..0d36d78ff9 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/FlacConstants.java +++ b/library/common/src/main/java/com/google/android/exoplayer2/util/FlacConstants.java @@ -29,5 +29,14 @@ public final class FlacConstants { /** Maximum size of a FLAC frame header in bytes. */ public static final int MAX_FRAME_HEADER_SIZE = 16; + /** Stream info metadata block type. */ + public static final int METADATA_TYPE_STREAM_INFO = 0; + /** Seek table metadata block type. */ + public static final int METADATA_TYPE_SEEK_TABLE = 3; + /** Vorbis comment metadata block type. */ + public static final int METADATA_TYPE_VORBIS_COMMENT = 4; + /** Picture metadata block type. */ + public static final int METADATA_TYPE_PICTURE = 6; + private FlacConstants() {} } diff --git a/library/common/src/main/java/com/google/android/exoplayer2/util/Function.java b/library/common/src/main/java/com/google/android/exoplayer2/util/Function.java new file mode 100644 index 0000000000..900f32db45 --- /dev/null +++ b/library/common/src/main/java/com/google/android/exoplayer2/util/Function.java @@ -0,0 +1,33 @@ +/* + * Copyright 2020 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; + +/** + * A functional interface representing a function taking one argument and returning a result. + * + * @param The input type of the function. + * @param The output type of the function. + */ +public interface Function { + + /** + * Applies this function to the given argument. + * + * @param t The function argument. + * @return The function result, which may be {@code null}. + */ + R apply(T t); +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/Log.java b/library/common/src/main/java/com/google/android/exoplayer2/util/Log.java similarity index 64% rename from library/core/src/main/java/com/google/android/exoplayer2/util/Log.java rename to library/common/src/main/java/com/google/android/exoplayer2/util/Log.java index a29460b84c..e5e6f88d4d 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/Log.java +++ b/library/common/src/main/java/com/google/android/exoplayer2/util/Log.java @@ -21,6 +21,7 @@ import androidx.annotation.Nullable; import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.net.UnknownHostException; /** Wrapper around {@link android.util.Log} which allows to set the log level. */ public final class Log { @@ -69,7 +70,8 @@ public final class Log { } /** - * Sets whether stack traces of {@link Throwable}s will be logged to logcat. + * Sets whether stack traces of {@link Throwable}s will be logged to logcat. Stack trace logging + * is enabled by default. * * @param logStackTraces Whether stack traces will be logged. */ @@ -86,11 +88,7 @@ public final class Log { /** @see android.util.Log#d(String, String, Throwable) */ public static void d(String tag, String message, @Nullable Throwable throwable) { - if (!logStackTraces) { - d(tag, appendThrowableMessage(message, throwable)); - } else if (logLevel == LOG_LEVEL_ALL) { - android.util.Log.d(tag, message, throwable); - } + d(tag, appendThrowableString(message, throwable)); } /** @see android.util.Log#i(String, String) */ @@ -102,11 +100,7 @@ public final class Log { /** @see android.util.Log#i(String, String, Throwable) */ public static void i(String tag, String message, @Nullable Throwable throwable) { - if (!logStackTraces) { - i(tag, appendThrowableMessage(message, throwable)); - } else if (logLevel <= LOG_LEVEL_INFO) { - android.util.Log.i(tag, message, throwable); - } + i(tag, appendThrowableString(message, throwable)); } /** @see android.util.Log#w(String, String) */ @@ -118,11 +112,7 @@ public final class Log { /** @see android.util.Log#w(String, String, Throwable) */ public static void w(String tag, String message, @Nullable Throwable throwable) { - if (!logStackTraces) { - w(tag, appendThrowableMessage(message, throwable)); - } else if (logLevel <= LOG_LEVEL_WARNING) { - android.util.Log.w(tag, message, throwable); - } + w(tag, appendThrowableString(message, throwable)); } /** @see android.util.Log#e(String, String) */ @@ -134,18 +124,54 @@ public final class Log { /** @see android.util.Log#e(String, String, Throwable) */ public static void e(String tag, String message, @Nullable Throwable throwable) { - if (!logStackTraces) { - e(tag, appendThrowableMessage(message, throwable)); - } else if (logLevel <= LOG_LEVEL_ERROR) { - android.util.Log.e(tag, message, throwable); + e(tag, appendThrowableString(message, throwable)); + } + + /** + * Returns a string representation of a {@link Throwable} suitable for logging, taking into + * account whether {@link #setLogStackTraces(boolean)} stack trace logging} is enabled. + * + *

Stack trace logging may be unconditionally suppressed for some expected failure modes (e.g., + * {@link Throwable Throwables} that are expected if the device doesn't have network connectivity) + * to avoid log spam. + * + * @param throwable The {@link Throwable}. + * @return The string representation of the {@link Throwable}. + */ + @Nullable + public static String getThrowableString(@Nullable Throwable throwable) { + if (throwable == null) { + return null; + } else if (isCausedByUnknownHostException(throwable)) { + // UnknownHostException implies the device doesn't have network connectivity. + // UnknownHostException.getMessage() may return a string that's more verbose than desired for + // logging an expected failure mode. Conversely, android.util.Log.getStackTraceString has + // special handling to return the empty string, which can result in logging that doesn't + // indicate the failure mode at all. Hence we special case this exception to always return a + // concise but useful message. + return "UnknownHostException (no network)"; + } else if (!logStackTraces) { + return throwable.getMessage(); + } else { + return android.util.Log.getStackTraceString(throwable).trim().replace("\t", " "); } } - private static String appendThrowableMessage(String message, @Nullable Throwable throwable) { - if (throwable == null) { - return message; + private static String appendThrowableString(String message, @Nullable Throwable throwable) { + @Nullable String throwableString = getThrowableString(throwable); + if (!TextUtils.isEmpty(throwableString)) { + message += "\n " + throwableString.replace("\n", "\n ") + '\n'; } - String throwableMessage = throwable.getMessage(); - return TextUtils.isEmpty(throwableMessage) ? message : message + " - " + throwableMessage; + return message; + } + + private static boolean isCausedByUnknownHostException(@Nullable Throwable throwable) { + while (throwable != null) { + if (throwable instanceof UnknownHostException) { + return true; + } + throwable = throwable.getCause(); + } + return false; } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/LongArray.java b/library/common/src/main/java/com/google/android/exoplayer2/util/LongArray.java similarity index 100% rename from library/core/src/main/java/com/google/android/exoplayer2/util/LongArray.java rename to library/common/src/main/java/com/google/android/exoplayer2/util/LongArray.java diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java b/library/common/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java similarity index 81% rename from library/core/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java rename to library/common/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java index de30cfd214..ea8b362b86 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java +++ b/library/common/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java @@ -38,11 +38,14 @@ public final class MimeTypes { public static final String VIDEO_VP8 = BASE_TYPE_VIDEO + "/x-vnd.on2.vp8"; public static final String VIDEO_VP9 = BASE_TYPE_VIDEO + "/x-vnd.on2.vp9"; public static final String VIDEO_AV1 = BASE_TYPE_VIDEO + "/av01"; + public static final String VIDEO_MP2T = BASE_TYPE_VIDEO + "/mp2t"; public static final String VIDEO_MP4V = BASE_TYPE_VIDEO + "/mp4v-es"; public static final String VIDEO_MPEG = BASE_TYPE_VIDEO + "/mpeg"; + public static final String VIDEO_PS = BASE_TYPE_VIDEO + "/mp2p"; public static final String VIDEO_MPEG2 = BASE_TYPE_VIDEO + "/mpeg2"; public static final String VIDEO_VC1 = BASE_TYPE_VIDEO + "/wvc1"; public static final String VIDEO_DIVX = BASE_TYPE_VIDEO + "/divx"; + public static final String VIDEO_FLV = BASE_TYPE_VIDEO + "/x-flv"; public static final String VIDEO_DOLBY_VISION = BASE_TYPE_VIDEO + "/dolby-vision"; public static final String VIDEO_UNKNOWN = BASE_TYPE_VIDEO + "/x-unknown"; @@ -65,11 +68,13 @@ public final class MimeTypes { public static final String AUDIO_DTS_EXPRESS = BASE_TYPE_AUDIO + "/vnd.dts.hd;profile=lbr"; public static final String AUDIO_VORBIS = BASE_TYPE_AUDIO + "/vorbis"; public static final String AUDIO_OPUS = BASE_TYPE_AUDIO + "/opus"; + public static final String AUDIO_AMR = BASE_TYPE_AUDIO + "/amr"; public static final String AUDIO_AMR_NB = BASE_TYPE_AUDIO + "/3gpp"; public static final String AUDIO_AMR_WB = BASE_TYPE_AUDIO + "/amr-wb"; public static final String AUDIO_FLAC = BASE_TYPE_AUDIO + "/flac"; public static final String AUDIO_ALAC = BASE_TYPE_AUDIO + "/alac"; public static final String AUDIO_MSGSM = BASE_TYPE_AUDIO + "/gsm"; + public static final String AUDIO_OGG = BASE_TYPE_AUDIO + "/ogg"; public static final String AUDIO_UNKNOWN = BASE_TYPE_AUDIO + "/x-unknown"; public static final String TEXT_VTT = BASE_TYPE_TEXT + "/vtt"; @@ -97,6 +102,7 @@ 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"; public static final String APPLICATION_ICY = BASE_TYPE_APPLICATION + "/x-icy"; + public static final String APPLICATION_AIT = BASE_TYPE_APPLICATION + "/vnd.dvb.ait"; private static final ArrayList customMimeTypes = new ArrayList<>(); @@ -122,24 +128,58 @@ public final class MimeTypes { customMimeTypes.add(customMimeType); } - /** Returns whether the given string is an audio mime type. */ + /** Returns whether the given string is an audio MIME type. */ public static boolean isAudio(@Nullable String mimeType) { return BASE_TYPE_AUDIO.equals(getTopLevelType(mimeType)); } - /** Returns whether the given string is a video mime type. */ + /** Returns whether the given string is a video MIME type. */ public static boolean isVideo(@Nullable String mimeType) { return BASE_TYPE_VIDEO.equals(getTopLevelType(mimeType)); } - /** Returns whether the given string is a text mime type. */ + /** + * Returns whether the given string is a text MIME type, including known text types that use + * "application" as their base type. + */ public static boolean isText(@Nullable String mimeType) { - return BASE_TYPE_TEXT.equals(getTopLevelType(mimeType)); + return BASE_TYPE_TEXT.equals(getTopLevelType(mimeType)) + || APPLICATION_CEA608.equals(mimeType) + || APPLICATION_CEA708.equals(mimeType) + || APPLICATION_MP4CEA608.equals(mimeType) + || APPLICATION_SUBRIP.equals(mimeType) + || APPLICATION_TTML.equals(mimeType) + || APPLICATION_TX3G.equals(mimeType) + || APPLICATION_MP4VTT.equals(mimeType) + || APPLICATION_RAWCC.equals(mimeType) + || APPLICATION_VOBSUB.equals(mimeType) + || APPLICATION_PGS.equals(mimeType) + || APPLICATION_DVBSUBS.equals(mimeType); } - /** Returns whether the given string is an application mime type. */ - public static boolean isApplication(@Nullable String mimeType) { - return BASE_TYPE_APPLICATION.equals(getTopLevelType(mimeType)); + /** + * Returns true if it is known that all samples in a stream of the given sample MIME type are + * guaranteed to be sync samples (i.e., {@link C#BUFFER_FLAG_KEY_FRAME} is guaranteed to be set on + * every sample). + * + * @param mimeType The sample MIME type. + * @return True if it is known that all samples in a stream of the given sample MIME type are + * guaranteed to be sync samples. False otherwise, including if {@code null} is passed. + */ + public static boolean allSamplesAreSyncSamples(@Nullable String mimeType) { + if (mimeType == null) { + return false; + } + // TODO: Consider adding additional audio MIME types here. + switch (mimeType) { + case AUDIO_AAC: + case AUDIO_MPEG: + case AUDIO_MPEG_L1: + case AUDIO_MPEG_L2: + return true; + default: + return false; + } } /** @@ -148,13 +188,14 @@ public final class MimeTypes { * @param codecs The codecs attribute. * @return The derived video mimeType, or null if it could not be derived. */ - public static @Nullable String getVideoMediaMimeType(@Nullable String codecs) { + @Nullable + public static String getVideoMediaMimeType(@Nullable String codecs) { if (codecs == null) { return null; } String[] codecList = Util.splitCodecs(codecs); for (String codec : codecList) { - String mimeType = getMediaMimeType(codec); + @Nullable String mimeType = getMediaMimeType(codec); if (mimeType != null && isVideo(mimeType)) { return mimeType; } @@ -168,13 +209,14 @@ public final class MimeTypes { * @param codecs The codecs attribute. * @return The derived audio mimeType, or null if it could not be derived. */ - public static @Nullable String getAudioMediaMimeType(@Nullable String codecs) { + @Nullable + public static String getAudioMediaMimeType(@Nullable String codecs) { if (codecs == null) { return null; } String[] codecList = Util.splitCodecs(codecs); for (String codec : codecList) { - String mimeType = getMediaMimeType(codec); + @Nullable String mimeType = getMediaMimeType(codec); if (mimeType != null && isAudio(mimeType)) { return mimeType; } @@ -182,13 +224,35 @@ public final class MimeTypes { return null; } + /** + * Derives a text sample mimeType from a codecs attribute. + * + * @param codecs The codecs attribute. + * @return The derived text mimeType, or null if it could not be derived. + */ + @Nullable + public static String getTextMediaMimeType(@Nullable String codecs) { + if (codecs == null) { + return null; + } + String[] codecList = Util.splitCodecs(codecs); + for (String codec : codecList) { + @Nullable String mimeType = getMediaMimeType(codec); + if (mimeType != null && isText(mimeType)) { + return mimeType; + } + } + return null; + } + /** * Derives a mimeType from a codec identifier, as defined in RFC 6381. * * @param codec The codec identifier to derive. * @return The mimeType, or null if it could not be derived. */ - public static @Nullable String getMediaMimeType(@Nullable String codec) { + @Nullable + public static String getMediaMimeType(@Nullable String codec) { if (codec == null) { return null; } @@ -209,7 +273,7 @@ public final class MimeTypes { } else if (codec.startsWith("vp8") || codec.startsWith("vp08")) { return MimeTypes.VIDEO_VP8; } else if (codec.startsWith("mp4a")) { - String mimeType = null; + @Nullable String mimeType = null; if (codec.startsWith("mp4a.")) { String objectTypeString = codec.substring(5); // remove the 'mp4a.' prefix if (objectTypeString.length() >= 2) { @@ -218,7 +282,7 @@ public final class MimeTypes { int objectTypeInt = Integer.parseInt(objectTypeHexString, 16); mimeType = getMimeTypeFromMp4ObjectType(objectTypeInt); } catch (NumberFormatException ignored) { - // ignored + // Ignored. } } } @@ -241,6 +305,14 @@ public final class MimeTypes { return MimeTypes.AUDIO_VORBIS; } else if (codec.startsWith("flac")) { return MimeTypes.AUDIO_FLAC; + } else if (codec.startsWith("stpp")) { + return MimeTypes.APPLICATION_TTML; + } else if (codec.startsWith("wvtt")) { + return MimeTypes.TEXT_VTT; + } else if (codec.contains("cea708")) { + return MimeTypes.APPLICATION_CEA708; + } else if (codec.contains("eia608") || codec.contains("cea608")) { + return MimeTypes.APPLICATION_CEA608; } else { return getCustomMimeTypeForCodec(codec); } @@ -317,12 +389,7 @@ public final class MimeTypes { return C.TRACK_TYPE_AUDIO; } else if (isVideo(mimeType)) { return C.TRACK_TYPE_VIDEO; - } else if (isText(mimeType) || APPLICATION_CEA608.equals(mimeType) - || APPLICATION_CEA708.equals(mimeType) || APPLICATION_MP4CEA608.equals(mimeType) - || APPLICATION_SUBRIP.equals(mimeType) || APPLICATION_TTML.equals(mimeType) - || APPLICATION_TX3G.equals(mimeType) || APPLICATION_MP4VTT.equals(mimeType) - || APPLICATION_RAWCC.equals(mimeType) || APPLICATION_VOBSUB.equals(mimeType) - || APPLICATION_PGS.equals(mimeType) || APPLICATION_DVBSUBS.equals(mimeType)) { + } else if (isText(mimeType)) { return C.TRACK_TYPE_TEXT; } else if (APPLICATION_ID3.equals(mimeType) || APPLICATION_EMSG.equals(mimeType) @@ -345,6 +412,8 @@ public final class MimeTypes { */ public static @C.Encoding int getEncoding(String mimeType) { switch (mimeType) { + case MimeTypes.AUDIO_MPEG: + return C.ENCODING_MP3; case MimeTypes.AUDIO_AC3: return C.ENCODING_AC3; case MimeTypes.AUDIO_E_AC3: @@ -378,7 +447,8 @@ public final class MimeTypes { * Returns the top-level type of {@code mimeType}, or null if {@code mimeType} is null or does not * contain a forward slash character ({@code '/'}). */ - private static @Nullable String getTopLevelType(@Nullable String mimeType) { + @Nullable + private static String getTopLevelType(@Nullable String mimeType) { if (mimeType == null) { return null; } @@ -389,7 +459,8 @@ public final class MimeTypes { return mimeType.substring(0, indexOfSlash); } - private static @Nullable String getCustomMimeTypeForCodec(String codec) { + @Nullable + private static String getCustomMimeTypeForCodec(String codec) { int customMimeTypeCount = customMimeTypes.size(); for (int i = 0; i < customMimeTypeCount; i++) { CustomMimeType customMimeType = customMimeTypes.get(i); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/NalUnitUtil.java b/library/common/src/main/java/com/google/android/exoplayer2/util/NalUnitUtil.java similarity index 100% rename from library/core/src/main/java/com/google/android/exoplayer2/util/NalUnitUtil.java rename to library/common/src/main/java/com/google/android/exoplayer2/util/NalUnitUtil.java diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/NonNullApi.java b/library/common/src/main/java/com/google/android/exoplayer2/util/NonNullApi.java similarity index 100% rename from library/core/src/main/java/com/google/android/exoplayer2/util/NonNullApi.java rename to library/common/src/main/java/com/google/android/exoplayer2/util/NonNullApi.java diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/ParsableBitArray.java b/library/common/src/main/java/com/google/android/exoplayer2/util/ParsableBitArray.java similarity index 90% rename from library/core/src/main/java/com/google/android/exoplayer2/util/ParsableBitArray.java rename to library/common/src/main/java/com/google/android/exoplayer2/util/ParsableBitArray.java index 1d8c3021a5..963e43fc7e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/ParsableBitArray.java +++ b/library/common/src/main/java/com/google/android/exoplayer2/util/ParsableBitArray.java @@ -15,6 +15,9 @@ */ package com.google.android.exoplayer2.util; +import com.google.android.exoplayer2.C; +import java.nio.charset.Charset; + /** * Wraps a byte array, providing methods that allow it to be read as a bitstream. */ @@ -195,7 +198,7 @@ public final class ParsableBitArray { if (numBits <= 32) { return Util.toUnsignedLong(readBits(numBits)); } - return Util.toUnsignedLong(readBits(numBits - 32)) << 32 | Util.toUnsignedLong(readBits(32)); + return Util.toLong(readBits(numBits - 32), readBits(32)); } /** @@ -277,6 +280,31 @@ public final class ParsableBitArray { assertValidOffset(); } + /** + * Reads the next {@code length} bytes as a UTF-8 string. Must only be called when the position is + * byte aligned. + * + * @param length The number of bytes to read. + * @return The string encoded by the bytes in UTF-8. + */ + public String readBytesAsString(int length) { + return readBytesAsString(length, Charset.forName(C.UTF8_NAME)); + } + + /** + * Reads the next {@code length} bytes as a string encoded in {@link Charset}. Must only be called + * when the position is byte aligned. + * + * @param length The number of bytes to read. + * @param charset The character set of the encoded characters. + * @return The string encoded by the bytes in the specified character set. + */ + public String readBytesAsString(int length, Charset charset) { + byte[] bytes = new byte[length]; + readBytes(bytes, 0, length); + return new String(bytes, charset); + } + /** * Overwrites {@code numBits} from this array using the {@code numBits} least significant bits * from {@code value}. Bits are written in order from most significant to least significant. The diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/ParsableByteArray.java b/library/common/src/main/java/com/google/android/exoplayer2/util/ParsableByteArray.java similarity index 100% rename from library/core/src/main/java/com/google/android/exoplayer2/util/ParsableByteArray.java rename to library/common/src/main/java/com/google/android/exoplayer2/util/ParsableByteArray.java diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/ParsableNalUnitBitArray.java b/library/common/src/main/java/com/google/android/exoplayer2/util/ParsableNalUnitBitArray.java similarity index 100% rename from library/core/src/main/java/com/google/android/exoplayer2/util/ParsableNalUnitBitArray.java rename to library/common/src/main/java/com/google/android/exoplayer2/util/ParsableNalUnitBitArray.java diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/Predicate.java b/library/common/src/main/java/com/google/android/exoplayer2/util/Predicate.java similarity index 100% rename from library/core/src/main/java/com/google/android/exoplayer2/util/Predicate.java rename to library/common/src/main/java/com/google/android/exoplayer2/util/Predicate.java diff --git a/library/common/src/main/java/com/google/android/exoplayer2/util/Supplier.java b/library/common/src/main/java/com/google/android/exoplayer2/util/Supplier.java new file mode 100644 index 0000000000..723047b1ed --- /dev/null +++ b/library/common/src/main/java/com/google/android/exoplayer2/util/Supplier.java @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2020 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; + +/** + * A functional interface representing a supplier of results. + * + * @param The type of results supplied by this supplier. + */ +public interface Supplier { + + /** Gets a result. */ + T get(); +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/TimestampAdjuster.java b/library/common/src/main/java/com/google/android/exoplayer2/util/TimestampAdjuster.java similarity index 100% rename from library/core/src/main/java/com/google/android/exoplayer2/util/TimestampAdjuster.java rename to library/common/src/main/java/com/google/android/exoplayer2/util/TimestampAdjuster.java diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/TraceUtil.java b/library/common/src/main/java/com/google/android/exoplayer2/util/TraceUtil.java similarity index 95% rename from library/core/src/main/java/com/google/android/exoplayer2/util/TraceUtil.java rename to library/common/src/main/java/com/google/android/exoplayer2/util/TraceUtil.java index 8fb409c04a..823fd1a0a5 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/TraceUtil.java +++ b/library/common/src/main/java/com/google/android/exoplayer2/util/TraceUtil.java @@ -15,7 +15,7 @@ */ package com.google.android.exoplayer2.util; -import android.annotation.TargetApi; +import androidx.annotation.RequiresApi; import com.google.android.exoplayer2.ExoPlayerLibraryInfo; /** @@ -49,12 +49,12 @@ public final class TraceUtil { } } - @TargetApi(18) + @RequiresApi(18) private static void beginSectionV18(String sectionName) { android.os.Trace.beginSection(sectionName); } - @TargetApi(18) + @RequiresApi(18) private static void endSectionV18() { android.os.Trace.endSection(); } diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMetadataType.java b/library/common/src/main/java/com/google/android/exoplayer2/util/UnknownNull.java similarity index 54% rename from library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMetadataType.java rename to library/common/src/main/java/com/google/android/exoplayer2/util/UnknownNull.java index 8fb6c220cf..0ccad43a12 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMetadataType.java +++ b/library/common/src/main/java/com/google/android/exoplayer2/util/UnknownNull.java @@ -12,27 +12,21 @@ * 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; +package com.google.android.exoplayer2.util; -import static java.lang.annotation.RetentionPolicy.SOURCE; - -import androidx.annotation.IntDef; -import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import javax.annotation.Nonnull; +import javax.annotation.meta.TypeQualifierDefault; +import javax.annotation.meta.When; /** - * The types of metadata that can be extracted from HLS streams. - * - *

See {@link HlsMediaSource.Factory#setMetadataType(int)}. + * Annotation for specifying unknown nullness. Useful for clearing the effects of an automatically + * propagated {@link Nonnull} annotation. */ -@Documented -@Retention(SOURCE) -@IntDef({HlsMetadataType.ID3, HlsMetadataType.EMSG}) -public @interface HlsMetadataType { - /** Type for ID3 metadata in HLS streams. */ - int ID3 = 1; - /** Type for ESMG metadata in HLS streams. */ - int EMSG = 3; -} +@Nonnull(when = When.UNKNOWN) +@TypeQualifierDefault(ElementType.TYPE_USE) +@Retention(RetentionPolicy.CLASS) +public @interface UnknownNull {} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java b/library/common/src/main/java/com/google/android/exoplayer2/util/Util.java similarity index 84% rename from library/core/src/main/java/com/google/android/exoplayer2/util/Util.java rename to library/common/src/main/java/com/google/android/exoplayer2/util/Util.java index 10fa43d89e..b1c554cf88 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java +++ b/library/common/src/main/java/com/google/android/exoplayer2/util/Util.java @@ -19,7 +19,6 @@ import static android.content.Context.UI_MODE_SERVICE; import android.Manifest.permission; import android.annotation.SuppressLint; -import android.annotation.TargetApi; import android.app.Activity; import android.app.UiModeManager; import android.content.ComponentName; @@ -39,25 +38,21 @@ import android.os.Build; import android.os.Handler; import android.os.Looper; import android.os.Parcel; +import android.os.SystemClock; import android.security.NetworkSecurityPolicy; import android.telephony.TelephonyManager; import android.text.TextUtils; import android.view.Display; +import android.view.SurfaceView; import android.view.WindowManager; import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlayerLibraryInfo; import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.MediaItem; import com.google.android.exoplayer2.ParserException; -import com.google.android.exoplayer2.Renderer; -import com.google.android.exoplayer2.RendererCapabilities; -import com.google.android.exoplayer2.RenderersFactory; -import com.google.android.exoplayer2.SeekParameters; -import com.google.android.exoplayer2.audio.AudioRendererEventListener; -import com.google.android.exoplayer2.drm.DrmSessionManager; -import com.google.android.exoplayer2.drm.FrameworkMediaCrypto; import com.google.android.exoplayer2.upstream.DataSource; -import com.google.android.exoplayer2.video.VideoRendererEventListener; import java.io.ByteArrayOutputStream; import java.io.Closeable; import java.io.File; @@ -65,7 +60,10 @@ import java.io.IOException; import java.io.InputStream; import java.lang.reflect.Method; import java.math.BigDecimal; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; import java.nio.charset.Charset; +import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; import java.util.Collections; @@ -97,7 +95,7 @@ public final class Util { * Like {@link android.os.Build.VERSION#SDK_INT}, but in a place where it can be conveniently * overridden for local testing. */ - public static final int SDK_INT = Build.VERSION.SDK_INT; + public static final int SDK_INT = "R".equals(Build.VERSION.CODENAME) ? 30 : Build.VERSION.SDK_INT; /** * Like {@link Build#DEVICE}, but in a place where it can be conveniently overridden for local @@ -136,9 +134,8 @@ public final class Util { + "(T(([0-9]*)H)?(([0-9]*)M)?(([0-9.]*)S)?)?$"); private static final Pattern ESCAPED_CHARACTER_PATTERN = Pattern.compile("%([A-Fa-f0-9]{2})"); - // Android standardizes to ISO 639-1 2-letter codes and provides no way to map a 3-letter - // ISO 639-2 code back to the corresponding 2-letter code. - @Nullable private static HashMap languageTagIso3ToIso2; + // Replacement map of ISO language codes used for normalization. + @Nullable private static HashMap languageTagReplacementMap; private Util() {} @@ -185,44 +182,73 @@ public final class Util { * @param uris {@link Uri}s that may require {@link permission#READ_EXTERNAL_STORAGE} to read. * @return Whether a permission request was made. */ - @TargetApi(23) public static boolean maybeRequestReadExternalStoragePermission(Activity activity, Uri... uris) { if (Util.SDK_INT < 23) { return false; } for (Uri uri : uris) { if (isLocalFileUri(uri)) { - if (activity.checkSelfPermission(permission.READ_EXTERNAL_STORAGE) - != PackageManager.PERMISSION_GRANTED) { - activity.requestPermissions(new String[] {permission.READ_EXTERNAL_STORAGE}, 0); - return true; - } - break; + return requestExternalStoragePermission(activity); } } return false; } /** - * Returns whether it may be possible to load the given URIs based on the network security - * policy's cleartext traffic permissions. + * Checks whether it's necessary to request the {@link permission#READ_EXTERNAL_STORAGE} + * permission for the specified {@link MediaItem media items}, requesting the permission if + * necessary. * - * @param uris A list of URIs that will be loaded. - * @return Whether it may be possible to load the given URIs. + * @param activity The host activity for checking and requesting the permission. + * @param mediaItems {@link MediaItem Media items}s that may require {@link + * permission#READ_EXTERNAL_STORAGE} to read. + * @return Whether a permission request was made. */ - @TargetApi(24) - public static boolean checkCleartextTrafficPermitted(Uri... uris) { + public static boolean maybeRequestReadExternalStoragePermission( + Activity activity, MediaItem... mediaItems) { + if (Util.SDK_INT < 23) { + return false; + } + for (MediaItem mediaItem : mediaItems) { + if (mediaItem.playbackProperties == null) { + continue; + } + if (isLocalFileUri(mediaItem.playbackProperties.uri)) { + return requestExternalStoragePermission(activity); + } + for (int i = 0; i < mediaItem.playbackProperties.subtitles.size(); i++) { + if (isLocalFileUri(mediaItem.playbackProperties.subtitles.get(i).uri)) { + return requestExternalStoragePermission(activity); + } + } + } + return false; + } + + /** + * Returns whether it may be possible to load the URIs of the given media items based on the + * network security policy's cleartext traffic permissions. + * + * @param mediaItems A list of {@link MediaItem media items}. + * @return Whether it may be possible to load the URIs of the given media items. + */ + public static boolean checkCleartextTrafficPermitted(MediaItem... mediaItems) { 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(Assertions.checkNotNull(uri.getHost()))) { - // The security policy prevents cleartext traffic. + for (MediaItem mediaItem : mediaItems) { + if (mediaItem.playbackProperties == null) { + continue; + } + if (isTrafficRestricted(mediaItem.playbackProperties.uri)) { return false; } + for (int i = 0; i < mediaItem.playbackProperties.subtitles.size(); i++) { + if (isTrafficRestricted(mediaItem.playbackProperties.subtitles.get(i).uri)) { + return false; + } + } } return true; } @@ -512,26 +538,23 @@ public final class Util { // Locale data (especially for API < 21) may produce tags with '_' instead of the // standard-conformant '-'. String normalizedTag = language.replace('_', '-'); - if (Util.SDK_INT >= 21) { - // Filters out ill-formed sub-tags, replaces deprecated tags and normalizes all valid tags. - normalizedTag = normalizeLanguageCodeSyntaxV21(normalizedTag); - } if (normalizedTag.isEmpty() || "und".equals(normalizedTag)) { // Tag isn't valid, keep using the original. normalizedTag = language; } normalizedTag = Util.toLowerInvariant(normalizedTag); String mainLanguage = Util.splitAtFirst(normalizedTag, "-")[0]; - if (mainLanguage.length() == 3) { - // 3-letter ISO 639-2/B or ISO 639-2/T language codes will not be converted to 2-letter ISO - // 639-1 codes automatically. - if (languageTagIso3ToIso2 == null) { - languageTagIso3ToIso2 = createIso3ToIso2Map(); - } - String iso2Language = languageTagIso3ToIso2.get(mainLanguage); - if (iso2Language != null) { - normalizedTag = iso2Language + normalizedTag.substring(/* beginIndex= */ 3); - } + if (languageTagReplacementMap == null) { + languageTagReplacementMap = createIsoLanguageReplacementMap(); + } + @Nullable String replacedLanguage = languageTagReplacementMap.get(mainLanguage); + if (replacedLanguage != null) { + normalizedTag = + replacedLanguage + normalizedTag.substring(/* beginIndex= */ mainLanguage.length()); + mainLanguage = replacedLanguage; + } + if ("no".equals(mainLanguage) || "i".equals(mainLanguage) || "zh".equals(mainLanguage)) { + normalizedTag = maybeReplaceGrandfatheredLanguageTags(normalizedTag); } return normalizedTag; } @@ -844,6 +867,47 @@ public final class Util { return stayInBounds ? Math.max(0, index) : index; } + /** + * Returns the index of the largest element in {@code longArray} that is less than (or optionally + * equal to) a specified {@code value}. + * + *

The search is performed using a binary search algorithm, so the array must be sorted. If the + * array contains multiple elements equal to {@code value} and {@code inclusive} is true, the + * index of the first one will be returned. + * + * @param longArray The array to search. + * @param value The value being searched for. + * @param inclusive If the value is present in the array, whether to return the corresponding + * index. If false then the returned index corresponds to the largest element strictly less + * than the value. + * @param stayInBounds If true, then 0 will be returned in the case that the value is smaller than + * the smallest element in the array. If false then -1 will be returned. + * @return The index of the largest element in {@code array} that is less than (or optionally + * equal to) {@code value}. + */ + public static int binarySearchFloor( + LongArray longArray, long value, boolean inclusive, boolean stayInBounds) { + int lowIndex = 0; + int highIndex = longArray.size() - 1; + + while (lowIndex <= highIndex) { + int midIndex = (lowIndex + highIndex) >>> 1; + if (longArray.get(midIndex) < value) { + lowIndex = midIndex + 1; + } else { + highIndex = midIndex - 1; + } + } + + if (inclusive && highIndex + 1 < longArray.size() && longArray.get(highIndex + 1) == value) { + highIndex++; + } else if (stayInBounds && highIndex == -1) { + highIndex = 0; + } + + return highIndex; + } + /** * Returns the index of the smallest element in {@code array} that is greater than (or optionally * equal to) a specified {@code value}. @@ -992,13 +1056,16 @@ public final class Util { } /** - * Parses an xs:dateTime attribute value, returning the parsed timestamp in milliseconds since - * the epoch. + * Parses an xs:dateTime attribute value, returning the parsed timestamp in milliseconds since the + * epoch. * * @param value The attribute value to decode. * @return The parsed timestamp in milliseconds since the epoch. * @throws ParserException if an error occurs parsing the dateTime attribute value. */ + // incompatible types in argument. + // dereference of possibly-null reference matcher.group(9) + @SuppressWarnings({"nullness:argument.type.incompatible", "nullness:dereference.of.nullable"}) public static long parseXsDateTime(String value) throws ParserException { Matcher matcher = XS_DATE_TIME_PATTERN.matcher(value); if (!matcher.matches()) { @@ -1149,44 +1216,6 @@ public final class Util { return Math.round((double) mediaDuration / speed); } - /** - * Resolves a seek given the requested seek position, a {@link SeekParameters} and two candidate - * sync points. - * - * @param positionUs The requested seek position, in microseocnds. - * @param seekParameters The {@link SeekParameters}. - * @param firstSyncUs The first candidate seek point, in micrseconds. - * @param secondSyncUs The second candidate seek point, in microseconds. May equal {@code - * firstSyncUs} if there's only one candidate. - * @return The resolved seek position, in microseconds. - */ - public static long resolveSeekPositionUs( - long positionUs, SeekParameters seekParameters, long firstSyncUs, long secondSyncUs) { - if (SeekParameters.EXACT.equals(seekParameters)) { - return positionUs; - } - long minPositionUs = - subtractWithOverflowDefault(positionUs, seekParameters.toleranceBeforeUs, Long.MIN_VALUE); - long maxPositionUs = - addWithOverflowDefault(positionUs, seekParameters.toleranceAfterUs, Long.MAX_VALUE); - boolean firstSyncPositionValid = minPositionUs <= firstSyncUs && firstSyncUs <= maxPositionUs; - boolean secondSyncPositionValid = - minPositionUs <= secondSyncUs && secondSyncUs <= maxPositionUs; - if (firstSyncPositionValid && secondSyncPositionValid) { - if (Math.abs(firstSyncUs - positionUs) <= Math.abs(secondSyncUs - positionUs)) { - return firstSyncUs; - } else { - return secondSyncUs; - } - } else if (firstSyncPositionValid) { - return firstSyncUs; - } else if (secondSyncPositionValid) { - return secondSyncUs; - } else { - return minPositionUs; - } - } - /** * Converts a list of integers to a primitive array. * @@ -1205,6 +1234,23 @@ public final class Util { return intArray; } + /** + * Converts an array of primitive ints to a list of integers. + * + * @param ints The ints. + * @return The input array in list form. + */ + public static List toList(int... ints) { + if (ints == null) { + return new ArrayList<>(); + } + List integers = new ArrayList<>(); + for (int anInt : ints) { + integers.add(anInt); + } + return integers; + } + /** * Returns the integer equal to the big-endian concatenation of the characters in {@code string} * as bytes. The string must be no more than four characters long. @@ -1233,6 +1279,18 @@ public final class Util { return x & 0xFFFFFFFFL; } + /** + * Return the long that is composed of the bits of the 2 specified integers. + * + * @param mostSignificantBits The 32 most significant bits of the long to return. + * @param leastSignificantBits The 32 least significant bits of the long to return. + * @return a long where its 32 most significant bits are {@code mostSignificantBits} bits and its + * 32 least significant bits are {@code leastSignificantBits}. + */ + public static long toLong(int mostSignificantBits, int leastSignificantBits) { + return (toUnsignedLong(mostSignificantBits) << 32) | toUnsignedLong(leastSignificantBits); + } + /** * Returns a byte array containing values parsed from the hex string provided. * @@ -1249,6 +1307,22 @@ public final class Util { return data; } + /** + * Returns a string containing a lower-case hex representation of the bytes provided. + * + * @param bytes The byte data to convert to hex. + * @return A String containing the hex representation of {@code bytes}. + */ + public static String toHexString(byte[] bytes) { + StringBuilder result = new StringBuilder(bytes.length * 2); + for (int i = 0; i < bytes.length; i++) { + result + .append(Character.forDigit((bytes[i] >> 4) & 0xF, 16)) + .append(Character.forDigit(bytes[i] & 0xF, 16)); + } + return result.toString(); + } + /** * Returns a string with comma delimited simple names of each object's class. * @@ -1359,19 +1433,22 @@ public final class Util { public static boolean isEncodingLinearPcm(@C.Encoding int encoding) { return encoding == C.ENCODING_PCM_8BIT || encoding == C.ENCODING_PCM_16BIT + || encoding == C.ENCODING_PCM_16BIT_BIG_ENDIAN || encoding == C.ENCODING_PCM_24BIT || encoding == C.ENCODING_PCM_32BIT || encoding == C.ENCODING_PCM_FLOAT; } /** - * Returns whether {@code encoding} is high resolution (> 16-bit) integer PCM. + * Returns whether {@code encoding} is high resolution (> 16-bit) PCM. * * @param encoding The encoding of the audio data. - * @return Whether the encoding is high resolution integer PCM. + * @return Whether the encoding is high resolution PCM. */ - public static boolean isEncodingHighResolutionIntegerPcm(@C.PcmEncoding int encoding) { - return encoding == C.ENCODING_PCM_24BIT || encoding == C.ENCODING_PCM_32BIT; + public static boolean isEncodingHighResolutionPcm(@C.PcmEncoding int encoding) { + return encoding == C.ENCODING_PCM_24BIT + || encoding == C.ENCODING_PCM_32BIT + || encoding == C.ENCODING_PCM_FLOAT; } /** @@ -1427,14 +1504,13 @@ public final class Util { case C.ENCODING_PCM_8BIT: return channelCount; case C.ENCODING_PCM_16BIT: + case C.ENCODING_PCM_16BIT_BIG_ENDIAN: return channelCount * 2; case C.ENCODING_PCM_24BIT: return channelCount * 3; case C.ENCODING_PCM_32BIT: case C.ENCODING_PCM_FLOAT: return channelCount * 4; - case C.ENCODING_PCM_A_LAW: - case C.ENCODING_PCM_MU_LAW: case C.ENCODING_INVALID: case Format.NO_VALUE: default: @@ -1592,6 +1668,29 @@ public final class Util { } } + /** + * Makes a best guess to infer the type from a {@link Uri} and MIME type. + * + * @param uri The {@link Uri}. + * @param mimeType If not null, used to infer the type. + * @return The content type. + */ + public static int inferContentTypeWithMimeType(Uri uri, @Nullable String mimeType) { + if (mimeType == null) { + return Util.inferContentType(uri); + } + switch (mimeType) { + case MimeTypes.APPLICATION_MPD: + return C.TYPE_DASH; + case MimeTypes.APPLICATION_M3U8: + return C.TYPE_HLS; + case MimeTypes.APPLICATION_SS: + return C.TYPE_SS; + default: + return Util.inferContentType(uri); + } + } + /** * Returns the specified millisecond time formatted as a string. * @@ -1698,7 +1797,8 @@ public final class Util { Matcher matcher = ESCAPED_CHARACTER_PATTERN.matcher(fileName); int startOfNotEscaped = 0; while (percentCharacterCount > 0 && matcher.find()) { - char unescapedCharacter = (char) Integer.parseInt(matcher.group(1), 16); + char unescapedCharacter = + (char) Integer.parseInt(Assertions.checkNotNull(matcher.group(1)), 16); builder.append(fileName, startOfNotEscaped, matcher.start()).append(unescapedCharacter); startOfNotEscaped = matcher.end(); percentCharacterCount--; @@ -1784,6 +1884,21 @@ public final class Util { return initialValue; } + /** + * Absolute get method for reading an int value in {@link ByteOrder#BIG_ENDIAN} in a {@link + * ByteBuffer}. Same as {@link ByteBuffer#getInt(int)} except the buffer's order as returned by + * {@link ByteBuffer#order()} is ignored and {@link ByteOrder#BIG_ENDIAN} is used instead. + * + * @param buffer The buffer from which to read an int in big endian. + * @param index The index from which the bytes will be read. + * @return The int value at the given index with the buffer bytes ordered most significant to + * least significant. + */ + public static int getBigEndianInt(ByteBuffer buffer, int index) { + int value = buffer.getInt(index); + return buffer.order() == ByteOrder.BIG_ENDIAN ? value : Integer.reverseBytes(value); + } + /** * Returns the {@link C.NetworkType} of the current network connection. * @@ -1922,25 +2037,37 @@ public final class Util { } /** - * Gets the physical size of the default display, in pixels. + * Gets the size of the current mode of the default display, in pixels. + * + *

Note that due to application UI scaling, the number of pixels made available to applications + * (as reported by {@link Display#getSize(Point)} may differ from the mode's actual resolution (as + * reported by this function). For example, applications running on a display configured with a 4K + * mode may have their UI laid out and rendered in 1080p and then scaled up. Applications can take + * advantage of the full mode resolution through a {@link SurfaceView} using full size buffers. * * @param context Any context. - * @return The physical display size, in pixels. + * @return The size of the current mode, in pixels. */ - public static Point getPhysicalDisplaySize(Context context) { + public static Point getCurrentDisplayModeSize(Context context) { WindowManager windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); - return getPhysicalDisplaySize(context, windowManager.getDefaultDisplay()); + return getCurrentDisplayModeSize(context, windowManager.getDefaultDisplay()); } /** - * Gets the physical size of the specified display, in pixels. + * Gets the size of the current mode of the specified display, in pixels. + * + *

Note that due to application UI scaling, the number of pixels made available to applications + * (as reported by {@link Display#getSize(Point)} may differ from the mode's actual resolution (as + * reported by this function). For example, applications running on a display configured with a 4K + * mode may have their UI laid out and rendered in 1080p and then scaled up. Applications can take + * advantage of the full mode resolution through a {@link SurfaceView} using full size buffers. * * @param context Any context. * @param display The display whose size is to be returned. - * @return The physical display size, in pixels. + * @return The size of the current mode, in pixels. */ - public static Point getPhysicalDisplaySize(Context context, Display display) { - if (Util.SDK_INT <= 28 && display.getDisplayId() == Display.DEFAULT_DISPLAY && isTv(context)) { + public static Point getCurrentDisplayModeSize(Context context, Display display) { + if (Util.SDK_INT <= 29 && display.getDisplayId() == Display.DEFAULT_DISPLAY && isTv(context)) { // On Android TVs it is common for the UI to be configured for a lower resolution than // SurfaceViews can output. Before API 26 the Display object does not provide a way to // identify this case, and up to and including API 28 many devices still do not correctly set @@ -1989,29 +2116,43 @@ public final class Util { } /** - * Extract renderer capabilities for the renderers created by the provided renderers factory. + * Returns a string representation of a {@code TRACK_TYPE_*} constant defined in {@link C}. * - * @param renderersFactory A {@link RenderersFactory}. - * @param drmSessionManager An optional {@link DrmSessionManager} used by the renderers. - * @return The {@link RendererCapabilities} for each renderer created by the {@code - * renderersFactory}. + * @param trackType A {@code TRACK_TYPE_*} constant, + * @return A string representation of this constant. */ - public static RendererCapabilities[] getRendererCapabilities( - RenderersFactory renderersFactory, - @Nullable DrmSessionManager drmSessionManager) { - Renderer[] renderers = - renderersFactory.createRenderers( - Util.createHandler(), - new VideoRendererEventListener() {}, - new AudioRendererEventListener() {}, - (cues) -> {}, - (metadata) -> {}, - drmSessionManager); - RendererCapabilities[] capabilities = new RendererCapabilities[renderers.length]; - for (int i = 0; i < renderers.length; i++) { - capabilities[i] = renderers[i].getCapabilities(); + public static String getTrackTypeString(int trackType) { + switch (trackType) { + case C.TRACK_TYPE_AUDIO: + return "audio"; + case C.TRACK_TYPE_DEFAULT: + return "default"; + case C.TRACK_TYPE_METADATA: + return "metadata"; + case C.TRACK_TYPE_CAMERA_MOTION: + return "camera motion"; + case C.TRACK_TYPE_NONE: + return "none"; + case C.TRACK_TYPE_TEXT: + return "text"; + case C.TRACK_TYPE_VIDEO: + return "video"; + default: + return trackType >= C.TRACK_TYPE_CUSTOM_BASE ? "custom (" + trackType + ")" : "?"; } - return capabilities; + } + + /** + * Returns the current time in milliseconds since the epoch. + * + * @param elapsedRealtimeEpochOffsetMs The offset between {@link SystemClock#elapsedRealtime()} + * and the time since the Unix epoch, or {@link C#TIME_UNSET} if unknown. + * @return The Unix time in milliseconds since the epoch. + */ + public static long getNowUnixTimeMs(long elapsedRealtimeEpochOffsetMs) { + return elapsedRealtimeEpochOffsetMs == C.TIME_UNSET + ? System.currentTimeMillis() + : SystemClock.elapsedRealtime() + elapsedRealtimeEpochOffsetMs; } @Nullable @@ -2027,14 +2168,14 @@ public final class Util { } } - @TargetApi(23) + @RequiresApi(23) private static void getDisplaySizeV23(Display display, Point outSize) { Display.Mode mode = display.getMode(); outSize.x = mode.getPhysicalWidth(); outSize.y = mode.getPhysicalHeight(); } - @TargetApi(17) + @RequiresApi(17) private static void getDisplaySizeV17(Display display, Point outSize) { display.getRealSize(outSize); } @@ -2050,21 +2191,16 @@ public final class Util { : new String[] {getLocaleLanguageTag(config.locale)}; } - @TargetApi(24) + @RequiresApi(24) private static String[] getSystemLocalesV24(Configuration config) { return Util.split(config.getLocales().toLanguageTags(), ","); } - @TargetApi(21) + @RequiresApi(21) private static String getLocaleLanguageTagV21(Locale locale) { return locale.toLanguageTag(); } - @TargetApi(21) - private static String normalizeLanguageCodeSyntaxV21(String languageTag) { - return Locale.forLanguageTag(languageTag).toLanguageTag(); - } - private static @C.NetworkType int getMobileNetworkType(NetworkInfo networkInfo) { switch (networkInfo.getSubtype()) { case TelephonyManager.NETWORK_TYPE_EDGE: @@ -2086,6 +2222,8 @@ public final class Util { return C.NETWORK_TYPE_3G; case TelephonyManager.NETWORK_TYPE_LTE: return C.NETWORK_TYPE_4G; + case TelephonyManager.NETWORK_TYPE_NR: + return C.NETWORK_TYPE_5G; case TelephonyManager.NETWORK_TYPE_IWLAN: return C.NETWORK_TYPE_WIFI; case TelephonyManager.NETWORK_TYPE_GSM: @@ -2095,32 +2233,63 @@ public final class Util { } } - private static HashMap createIso3ToIso2Map() { + private static HashMap createIsoLanguageReplacementMap() { String[] iso2Languages = Locale.getISOLanguages(); - HashMap iso3ToIso2 = + HashMap replacedLanguages = new HashMap<>( - /* initialCapacity= */ iso2Languages.length + iso3BibliographicalToIso2.length); + /* initialCapacity= */ iso2Languages.length + additionalIsoLanguageReplacements.length); for (String iso2 : iso2Languages) { try { // This returns the ISO 639-2/T code for the language. String iso3 = new Locale(iso2).getISO3Language(); if (!TextUtils.isEmpty(iso3)) { - iso3ToIso2.put(iso3, iso2); + replacedLanguages.put(iso3, iso2); } } catch (MissingResourceException e) { // Shouldn't happen for list of known languages, but we don't want to throw either. } } - // Add additional ISO 639-2/B codes to mapping. - for (int i = 0; i < iso3BibliographicalToIso2.length; i += 2) { - iso3ToIso2.put(iso3BibliographicalToIso2[i], iso3BibliographicalToIso2[i + 1]); + // Add additional replacement mappings. + for (int i = 0; i < additionalIsoLanguageReplacements.length; i += 2) { + replacedLanguages.put( + additionalIsoLanguageReplacements[i], additionalIsoLanguageReplacements[i + 1]); } - return iso3ToIso2; + return replacedLanguages; } - // See https://en.wikipedia.org/wiki/List_of_ISO_639-2_codes. - private static final String[] iso3BibliographicalToIso2 = + @RequiresApi(api = Build.VERSION_CODES.M) + private static boolean requestExternalStoragePermission(Activity activity) { + if (activity.checkSelfPermission(permission.READ_EXTERNAL_STORAGE) + != PackageManager.PERMISSION_GRANTED) { + activity.requestPermissions( + new String[] {permission.READ_EXTERNAL_STORAGE}, /* requestCode= */ 0); + return true; + } + return false; + } + + @RequiresApi(api = Build.VERSION_CODES.N) + private static boolean isTrafficRestricted(Uri uri) { + return "http".equals(uri.getScheme()) + && !NetworkSecurityPolicy.getInstance() + .isCleartextTrafficPermitted(Assertions.checkNotNull(uri.getHost())); + } + + private static String maybeReplaceGrandfatheredLanguageTags(String languageTag) { + for (int i = 0; i < isoGrandfatheredTagReplacements.length; i += 2) { + if (languageTag.startsWith(isoGrandfatheredTagReplacements[i])) { + return isoGrandfatheredTagReplacements[i + 1] + + languageTag.substring(/* beginIndex= */ isoGrandfatheredTagReplacements[i].length()); + } + } + return languageTag; + } + + // Additional mapping from ISO3 to ISO2 language codes. + private static final String[] additionalIsoLanguageReplacements = new String[] { + // Bibliographical codes defined in ISO 639-2/B, replaced by terminological code defined in + // ISO 639-2/T. See https://en.wikipedia.org/wiki/List_of_ISO_639-2_codes. "alb", "sq", "arm", "hy", "baq", "eu", @@ -2139,8 +2308,50 @@ public final class Util { "may", "ms", "per", "fa", "rum", "ro", + "scc", "hbs-srp", "slo", "sk", - "wel", "cy" + "wel", "cy", + // Deprecated 2-letter codes, replaced by modern equivalent (including macrolanguage) + // See https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes, "ISO 639:1988" + "id", "ms-ind", + "iw", "he", + "heb", "he", + "ji", "yi", + // Individual macrolanguage codes mapped back to full macrolanguage code. + // See https://en.wikipedia.org/wiki/ISO_639_macrolanguage + "in", "ms-ind", + "ind", "ms-ind", + "nb", "no-nob", + "nob", "no-nob", + "nn", "no-nno", + "nno", "no-nno", + "tw", "ak-twi", + "twi", "ak-twi", + "bs", "hbs-bos", + "bos", "hbs-bos", + "hr", "hbs-hrv", + "hrv", "hbs-hrv", + "sr", "hbs-srp", + "srp", "hbs-srp", + "cmn", "zh-cmn", + "hak", "zh-hak", + "nan", "zh-nan", + "hsn", "zh-hsn" + }; + + // "Grandfathered tags", replaced by modern equivalents (including macrolanguage) + // See https://www.iana.org/assignments/language-subtag-registry/language-subtag-registry. + private static final String[] isoGrandfatheredTagReplacements = + new String[] { + "i-lux", "lb", + "i-hak", "zh-hak", + "i-navajo", "nv", + "no-bok", "no-nob", + "no-nyn", "no-nno", + "zh-guoyu", "zh-cmn", + "zh-hakka", "zh-hak", + "zh-min-nan", "zh-nan", + "zh-xiang", "zh-hsn" }; /** diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/package-info.java b/library/common/src/main/java/com/google/android/exoplayer2/util/package-info.java similarity index 100% rename from library/core/src/main/java/com/google/android/exoplayer2/util/package-info.java rename to library/common/src/main/java/com/google/android/exoplayer2/util/package-info.java diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/AvcConfig.java b/library/common/src/main/java/com/google/android/exoplayer2/video/AvcConfig.java similarity index 100% rename from library/core/src/main/java/com/google/android/exoplayer2/video/AvcConfig.java rename to library/common/src/main/java/com/google/android/exoplayer2/video/AvcConfig.java diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/ColorInfo.java b/library/common/src/main/java/com/google/android/exoplayer2/video/ColorInfo.java similarity index 94% rename from library/core/src/main/java/com/google/android/exoplayer2/video/ColorInfo.java rename to library/common/src/main/java/com/google/android/exoplayer2/video/ColorInfo.java index ed2ca9c034..d45d6c55b2 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/ColorInfo.java +++ b/library/common/src/main/java/com/google/android/exoplayer2/video/ColorInfo.java @@ -43,12 +43,11 @@ public final class ColorInfo implements Parcelable { public final int colorRange; /** - * The color transfer characteristicks of the video. Valid values are {@link - * C#COLOR_TRANSFER_HLG}, {@link C#COLOR_TRANSFER_ST2084}, {@link C#COLOR_TRANSFER_SDR} or {@link - * Format#NO_VALUE} if unknown. + * The color transfer characteristics of the video. Valid values are {@link C#COLOR_TRANSFER_HLG}, + * {@link C#COLOR_TRANSFER_ST2084}, {@link C#COLOR_TRANSFER_SDR} or {@link Format#NO_VALUE} if + * unknown. */ - @C.ColorTransfer - public final int colorTransfer; + @C.ColorTransfer public final int colorTransfer; /** HdrStaticInfo as defined in CTA-861.3, or null if none specified. */ @Nullable public final byte[] hdrStaticInfo; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/DolbyVisionConfig.java b/library/common/src/main/java/com/google/android/exoplayer2/video/DolbyVisionConfig.java similarity index 100% rename from library/core/src/main/java/com/google/android/exoplayer2/video/DolbyVisionConfig.java rename to library/common/src/main/java/com/google/android/exoplayer2/video/DolbyVisionConfig.java diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/HevcConfig.java b/library/common/src/main/java/com/google/android/exoplayer2/video/HevcConfig.java similarity index 100% rename from library/core/src/main/java/com/google/android/exoplayer2/video/HevcConfig.java rename to library/common/src/main/java/com/google/android/exoplayer2/video/HevcConfig.java diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/package-info.java b/library/common/src/main/java/com/google/android/exoplayer2/video/package-info.java similarity index 100% rename from library/core/src/main/java/com/google/android/exoplayer2/video/package-info.java rename to library/common/src/main/java/com/google/android/exoplayer2/video/package-info.java diff --git a/library/common/src/main/proguard-rules.txt b/library/common/src/main/proguard-rules.txt new file mode 120000 index 0000000000..499fb08b36 --- /dev/null +++ b/library/common/src/main/proguard-rules.txt @@ -0,0 +1 @@ +../../proguard-rules.txt \ No newline at end of file diff --git a/library/common/src/test/AndroidManifest.xml b/library/common/src/test/AndroidManifest.xml new file mode 100644 index 0000000000..46c19f53c9 --- /dev/null +++ b/library/common/src/test/AndroidManifest.xml @@ -0,0 +1,19 @@ + + + + + + diff --git a/library/core/src/test/java/com/google/android/exoplayer2/CTest.java b/library/common/src/test/java/com/google/android/exoplayer2/CTest.java similarity index 72% rename from library/core/src/test/java/com/google/android/exoplayer2/CTest.java rename to library/common/src/test/java/com/google/android/exoplayer2/CTest.java index 26a7102b16..ac5edc6f6b 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/CTest.java +++ b/library/common/src/test/java/com/google/android/exoplayer2/CTest.java @@ -18,6 +18,7 @@ package com.google.android.exoplayer2; import static com.google.common.truth.Truth.assertThat; import android.annotation.SuppressLint; +import android.media.AudioFormat; import android.media.MediaCodec; import androidx.test.ext.junit.runners.AndroidJUnit4; import org.junit.Test; @@ -29,11 +30,19 @@ public class CTest { @SuppressLint("InlinedApi") @Test - public void testConstants() { + public void bufferFlagConstants_equalToMediaCodecConstants() { // Sanity check that constant values match those defined by the platform. assertThat(C.BUFFER_FLAG_KEY_FRAME).isEqualTo(MediaCodec.BUFFER_FLAG_KEY_FRAME); assertThat(C.BUFFER_FLAG_END_OF_STREAM).isEqualTo(MediaCodec.BUFFER_FLAG_END_OF_STREAM); assertThat(C.CRYPTO_MODE_AES_CTR).isEqualTo(MediaCodec.CRYPTO_MODE_AES_CTR); } + @SuppressLint("InlinedApi") + @Test + public void encodingConstants_equalToAudioFormatConstants() { + // Sanity check that encoding constant values match those defined by the platform. + assertThat(C.ENCODING_PCM_16BIT).isEqualTo(AudioFormat.ENCODING_PCM_16BIT); + assertThat(C.ENCODING_MP3).isEqualTo(AudioFormat.ENCODING_MP3); + assertThat(C.ENCODING_PCM_FLOAT).isEqualTo(AudioFormat.ENCODING_PCM_FLOAT); + } } diff --git a/library/common/src/test/java/com/google/android/exoplayer2/FormatTest.java b/library/common/src/test/java/com/google/android/exoplayer2/FormatTest.java new file mode 100644 index 0000000000..135aace2a3 --- /dev/null +++ b/library/common/src/test/java/com/google/android/exoplayer2/FormatTest.java @@ -0,0 +1,125 @@ +/* + * 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; + +import static com.google.android.exoplayer2.C.WIDEVINE_UUID; +import static com.google.android.exoplayer2.util.MimeTypes.VIDEO_MP4; +import static com.google.android.exoplayer2.util.MimeTypes.VIDEO_WEBM; +import static com.google.common.truth.Truth.assertThat; + +import android.os.Parcel; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.android.exoplayer2.drm.DrmInitData; +import com.google.android.exoplayer2.drm.ExoMediaCrypto; +import com.google.android.exoplayer2.metadata.Metadata; +import com.google.android.exoplayer2.metadata.id3.TextInformationFrame; +import com.google.android.exoplayer2.testutil.TestUtil; +import com.google.android.exoplayer2.util.MimeTypes; +import com.google.android.exoplayer2.video.ColorInfo; +import java.util.ArrayList; +import java.util.List; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Unit test for {@link Format}. */ +@RunWith(AndroidJUnit4.class) +public final class FormatTest { + + @Test + public void buildUponFormat_createsEqualFormat() { + Format testFormat = createTestFormat(); + assertThat(testFormat.buildUpon().build()).isEqualTo(testFormat); + } + + @Test + public void parcelFormat_createsEqualFormat_exceptExoMediaCryptoType() { + Format formatToParcel = createTestFormat(); + + Parcel parcel = Parcel.obtain(); + formatToParcel.writeToParcel(parcel, 0); + parcel.setDataPosition(0); + + Format formatFromParcel = Format.CREATOR.createFromParcel(parcel); + Format expectedFormat = formatToParcel.buildUpon().setExoMediaCryptoType(null).build(); + + assertThat(formatFromParcel.exoMediaCryptoType).isNull(); + assertThat(formatFromParcel).isEqualTo(expectedFormat); + + parcel.recycle(); + } + + private static Format createTestFormat() { + byte[] initData1 = new byte[] {1, 2, 3}; + byte[] initData2 = new byte[] {4, 5, 6}; + List initializationData = new ArrayList<>(); + initializationData.add(initData1); + initializationData.add(initData2); + + DrmInitData.SchemeData drmData1 = + new DrmInitData.SchemeData( + WIDEVINE_UUID, VIDEO_MP4, TestUtil.buildTestData(128, 1 /* data seed */)); + DrmInitData.SchemeData drmData2 = + new DrmInitData.SchemeData( + C.UUID_NIL, VIDEO_WEBM, TestUtil.buildTestData(128, 1 /* data seed */)); + DrmInitData drmInitData = new DrmInitData(drmData1, drmData2); + + byte[] projectionData = new byte[] {1, 2, 3}; + + Metadata metadata = + new Metadata( + new TextInformationFrame("id1", "description1", "value1"), + new TextInformationFrame("id2", "description2", "value2")); + + 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}); + + return new Format( + "id", + "label", + "language", + C.SELECTION_FLAG_DEFAULT, + C.ROLE_FLAG_MAIN, + /* averageBitrate= */ 1024, + /* peakBitrate= */ 2048, + "codec", + metadata, + /* containerMimeType= */ MimeTypes.VIDEO_MP4, + /* sampleMimeType= */ MimeTypes.VIDEO_H264, + /* maxInputSize= */ 5000, + initializationData, + drmInitData, + Format.OFFSET_SAMPLE_RELATIVE, + /* width= */ 1920, + /* height= */ 1080, + /* frameRate= */ 24, + /* rotationDegrees= */ 90, + /* pixelWidthHeightRatio= */ 4, + projectionData, + C.STEREO_MODE_TOP_BOTTOM, + colorInfo, + /* channelCount= */ 6, + /* sampleRate= */ 44100, + C.ENCODING_PCM_24BIT, + /* encoderDelay= */ 1001, + /* encoderPadding= */ 1002, + /* accessibilityChannel= */ 2, + /* exoMediaCryptoType= */ ExoMediaCrypto.class); + } +} diff --git a/library/common/src/test/java/com/google/android/exoplayer2/MediaItemTest.java b/library/common/src/test/java/com/google/android/exoplayer2/MediaItemTest.java new file mode 100644 index 0000000000..d62735a6ba --- /dev/null +++ b/library/common/src/test/java/com/google/android/exoplayer2/MediaItemTest.java @@ -0,0 +1,325 @@ +/* + * Copyright 2020 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; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; + +import android.net.Uri; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.android.exoplayer2.offline.StreamKey; +import com.google.android.exoplayer2.util.MimeTypes; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Unit test for {@link MediaItem MediaItems}. */ +@RunWith(AndroidJUnit4.class) +public class MediaItemTest { + + private static final String URI_STRING = "http://www.google.com"; + + @Test + public void builder_needsUriOrMediaId() { + assertThrows(NullPointerException.class, () -> new MediaItem.Builder().build()); + } + + @Test + public void builderWithUri_setsUri() { + Uri uri = Uri.parse(URI_STRING); + + MediaItem mediaItem = MediaItem.fromUri(uri); + + assertThat(mediaItem.playbackProperties.uri.toString()).isEqualTo(URI_STRING); + assertThat(mediaItem.mediaId).isEqualTo(URI_STRING); + assertThat(mediaItem.mediaMetadata).isNotNull(); + } + + @Test + public void builderWithUriAsString_setsUri() { + MediaItem mediaItem = MediaItem.fromUri(URI_STRING); + + assertThat(mediaItem.playbackProperties.uri.toString()).isEqualTo(URI_STRING); + assertThat(mediaItem.mediaId).isEqualTo(URI_STRING); + } + + @Test + public void builderSetMimeType_isNullByDefault() { + MediaItem mediaItem = MediaItem.fromUri(URI_STRING); + + assertThat(mediaItem.playbackProperties.mimeType).isNull(); + } + + @Test + public void builderSetMimeType_setsMimeType() { + MediaItem mediaItem = + new MediaItem.Builder().setUri(URI_STRING).setMimeType(MimeTypes.APPLICATION_MPD).build(); + + assertThat(mediaItem.playbackProperties.mimeType).isEqualTo(MimeTypes.APPLICATION_MPD); + } + + @Test + public void builderSetDrmConfig_isNullByDefault() { + // Null value by default. + MediaItem mediaItem = new MediaItem.Builder().setUri(URI_STRING).build(); + assertThat(mediaItem.playbackProperties.drmConfiguration).isNull(); + } + + @Test + public void builderSetDrmConfig_setsAllProperties() { + Uri licenseUri = Uri.parse(URI_STRING); + Map requestHeaders = new HashMap<>(); + requestHeaders.put("Referer", "http://www.google.com"); + byte[] keySetId = new byte[] {1, 2, 3}; + MediaItem mediaItem = + new MediaItem.Builder() + .setUri(URI_STRING) + .setDrmUuid(C.WIDEVINE_UUID) + .setDrmLicenseUri(licenseUri) + .setDrmLicenseRequestHeaders(requestHeaders) + .setDrmMultiSession(/* multiSession= */ true) + .setDrmPlayClearContentWithoutKey(true) + .setDrmSessionForClearTypes(Collections.singletonList(C.TRACK_TYPE_AUDIO)) + .setDrmKeySetId(keySetId) + .build(); + + assertThat(mediaItem.playbackProperties.drmConfiguration).isNotNull(); + assertThat(mediaItem.playbackProperties.drmConfiguration.uuid).isEqualTo(C.WIDEVINE_UUID); + assertThat(mediaItem.playbackProperties.drmConfiguration.licenseUri).isEqualTo(licenseUri); + assertThat(mediaItem.playbackProperties.drmConfiguration.requestHeaders) + .isEqualTo(requestHeaders); + assertThat(mediaItem.playbackProperties.drmConfiguration.multiSession).isTrue(); + assertThat(mediaItem.playbackProperties.drmConfiguration.playClearContentWithoutKey).isTrue(); + assertThat(mediaItem.playbackProperties.drmConfiguration.sessionForClearTypes) + .containsExactly(C.TRACK_TYPE_AUDIO); + assertThat(mediaItem.playbackProperties.drmConfiguration.getKeySetId()).isEqualTo(keySetId); + } + + @Test + public void builderSetDrmSessionForClearPeriods_setsAudioAndVideoTracks() { + Uri licenseUri = Uri.parse(URI_STRING); + MediaItem mediaItem = + new MediaItem.Builder() + .setUri(URI_STRING) + .setDrmUuid(C.WIDEVINE_UUID) + .setDrmLicenseUri(licenseUri) + .setDrmSessionForClearTypes(Arrays.asList(C.TRACK_TYPE_AUDIO)) + .setDrmSessionForClearPeriods(true) + .build(); + + assertThat(mediaItem.playbackProperties.drmConfiguration.sessionForClearTypes) + .containsExactly(C.TRACK_TYPE_AUDIO, C.TRACK_TYPE_VIDEO); + } + + @Test + public void builderSetDrmUuid_notCalled_throwsIllegalStateException() { + assertThrows( + IllegalStateException.class, + () -> + new MediaItem.Builder() + .setUri(URI_STRING) + // missing uuid + .setDrmLicenseUri(Uri.parse(URI_STRING)) + .build()); + } + + @Test + public void builderSetCustomCacheKey_setsCustomCacheKey() { + MediaItem mediaItem = + new MediaItem.Builder().setUri(URI_STRING).setCustomCacheKey("key").build(); + + assertThat(mediaItem.playbackProperties.customCacheKey).isEqualTo("key"); + } + + @Test + public void builderSetStreamKeys_setsStreamKeys() { + List streamKeys = new ArrayList<>(); + streamKeys.add(new StreamKey(1, 0, 0)); + streamKeys.add(new StreamKey(0, 1, 1)); + + MediaItem mediaItem = + new MediaItem.Builder().setUri(URI_STRING).setStreamKeys(streamKeys).build(); + + assertThat(mediaItem.playbackProperties.streamKeys).isEqualTo(streamKeys); + } + + @Test + public void builderSetSubtitles_setsSubtitles() { + List subtitles = + Arrays.asList( + new MediaItem.Subtitle( + Uri.parse(URI_STRING + "/en"), MimeTypes.APPLICATION_TTML, /* language= */ "en"), + new MediaItem.Subtitle( + Uri.parse(URI_STRING + "/de"), + MimeTypes.APPLICATION_TTML, + /* language= */ null, + C.SELECTION_FLAG_DEFAULT)); + + MediaItem mediaItem = + new MediaItem.Builder().setUri(URI_STRING).setSubtitles(subtitles).build(); + + assertThat(mediaItem.playbackProperties.subtitles).isEqualTo(subtitles); + } + + @Test + public void builderSetTag_isNullByDefault() { + MediaItem mediaItem = new MediaItem.Builder().setUri(URI_STRING).build(); + + assertThat(mediaItem.playbackProperties.tag).isNull(); + } + + @Test + public void builderSetTag_setsTag() { + Object tag = new Object(); + + MediaItem mediaItem = new MediaItem.Builder().setUri(URI_STRING).setTag(tag).build(); + + assertThat(mediaItem.playbackProperties.tag).isEqualTo(tag); + } + + @Test + public void builderSetStartPositionMs_setsStartPositionMs() { + MediaItem mediaItem = + new MediaItem.Builder().setUri(URI_STRING).setClipStartPositionMs(1000L).build(); + + assertThat(mediaItem.clippingProperties.startPositionMs).isEqualTo(1000L); + } + + @Test + public void builderSetStartPositionMs_zeroByDefault() { + MediaItem mediaItem = new MediaItem.Builder().setUri(URI_STRING).build(); + + assertThat(mediaItem.clippingProperties.startPositionMs).isEqualTo(0); + } + + @Test + public void builderSetStartPositionMs_negativeValue_throws() { + MediaItem.Builder builder = new MediaItem.Builder(); + + assertThrows(IllegalArgumentException.class, () -> builder.setClipStartPositionMs(-1)); + } + + @Test + public void builderSetEndPositionMs_setsEndPositionMs() { + MediaItem mediaItem = + new MediaItem.Builder().setUri(URI_STRING).setClipEndPositionMs(1000L).build(); + + assertThat(mediaItem.clippingProperties.endPositionMs).isEqualTo(1000L); + } + + @Test + public void builderSetEndPositionMs_timeEndOfSourceByDefault() { + MediaItem mediaItem = new MediaItem.Builder().setUri(URI_STRING).build(); + + assertThat(mediaItem.clippingProperties.endPositionMs).isEqualTo(C.TIME_END_OF_SOURCE); + } + + @Test + public void builderSetEndPositionMs_timeEndOfSource_setsEndPositionMs() { + MediaItem mediaItem = + new MediaItem.Builder() + .setUri(URI_STRING) + .setClipEndPositionMs(1000) + .setClipEndPositionMs(C.TIME_END_OF_SOURCE) + .build(); + + assertThat(mediaItem.clippingProperties.endPositionMs).isEqualTo(C.TIME_END_OF_SOURCE); + } + + @Test + public void builderSetEndPositionMs_negativeValue_throws() { + MediaItem.Builder builder = new MediaItem.Builder(); + + assertThrows(IllegalArgumentException.class, () -> builder.setClipEndPositionMs(-1)); + } + + @Test + public void builderSetClippingFlags_setsClippingFlags() { + MediaItem mediaItem = + new MediaItem.Builder() + .setUri(URI_STRING) + .setClipRelativeToDefaultPosition(true) + .setClipRelativeToLiveWindow(true) + .setClipStartsAtKeyFrame(true) + .build(); + + assertThat(mediaItem.clippingProperties.relativeToDefaultPosition).isTrue(); + assertThat(mediaItem.clippingProperties.relativeToLiveWindow).isTrue(); + assertThat(mediaItem.clippingProperties.startsAtKeyFrame).isTrue(); + } + + @Test + public void builderSetAdTagUri_setsAdTagUri() { + Uri adTagUri = Uri.parse(URI_STRING + "/ad"); + + MediaItem mediaItem = new MediaItem.Builder().setUri(URI_STRING).setAdTagUri(adTagUri).build(); + + assertThat(mediaItem.playbackProperties.adTagUri).isEqualTo(adTagUri); + } + + @Test + public void builderSetMediaMetadata_setsMetadata() { + MediaMetadata mediaMetadata = new MediaMetadata.Builder().setTitle("title").build(); + + MediaItem mediaItem = + new MediaItem.Builder().setUri(URI_STRING).setMediaMetadata(mediaMetadata).build(); + + assertThat(mediaItem.mediaMetadata).isEqualTo(mediaMetadata); + } + + @Test + public void buildUpon_equalsToOriginal() { + MediaItem mediaItem = + new MediaItem.Builder() + .setAdTagUri(URI_STRING) + .setClipEndPositionMs(1000) + .setClipRelativeToDefaultPosition(true) + .setClipRelativeToLiveWindow(true) + .setClipStartPositionMs(100) + .setClipStartsAtKeyFrame(true) + .setCustomCacheKey("key") + .setDrmUuid(C.WIDEVINE_UUID) + .setDrmLicenseUri(URI_STRING + "/license") + .setDrmLicenseRequestHeaders( + Collections.singletonMap("Referer", "http://www.google.com")) + .setDrmMultiSession(true) + .setDrmPlayClearContentWithoutKey(true) + .setDrmSessionForClearTypes(Collections.singletonList(C.TRACK_TYPE_AUDIO)) + .setDrmKeySetId(new byte[] {1, 2, 3}) + .setMediaId("mediaId") + .setMediaMetadata(new MediaMetadata.Builder().setTitle("title").build()) + .setMimeType(MimeTypes.APPLICATION_MP4) + .setUri(URI_STRING) + .setStreamKeys(Collections.singletonList(new StreamKey(1, 0, 0))) + .setSubtitles( + Collections.singletonList( + new MediaItem.Subtitle( + Uri.parse(URI_STRING + "/en"), + MimeTypes.APPLICATION_TTML, + /* language= */ "en"))) + .setTag(new Object()) + .build(); + + MediaItem copy = mediaItem.buildUpon().build(); + + assertThat(copy).isEqualTo(mediaItem); + } +} diff --git a/library/common/src/test/java/com/google/android/exoplayer2/MediaMetadataTest.java b/library/common/src/test/java/com/google/android/exoplayer2/MediaMetadataTest.java new file mode 100644 index 0000000000..43d5bc5a2c --- /dev/null +++ b/library/common/src/test/java/com/google/android/exoplayer2/MediaMetadataTest.java @@ -0,0 +1,43 @@ +/* + * Copyright 2020 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; + +import static com.google.common.truth.Truth.assertThat; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Unit test for {@link MediaMetadata}. */ +@RunWith(AndroidJUnit4.class) +public class MediaMetadataTest { + + @Test + public void builder_minimal_correctDefaults() { + MediaMetadata mediaMetadata = new MediaMetadata.Builder().build(); + + assertThat(mediaMetadata.title).isNull(); + } + + @Test + public void builderSetTitle_setsTitle() { + String title = "title"; + + MediaMetadata mediaMetadata = new MediaMetadata.Builder().setTitle(title).build(); + + assertThat(mediaMetadata.title).isEqualTo(title); + } +} diff --git a/library/core/src/test/java/com/google/android/exoplayer2/audio/Ac3UtilTest.java b/library/common/src/test/java/com/google/android/exoplayer2/audio/Ac3UtilTest.java similarity index 91% rename from library/core/src/test/java/com/google/android/exoplayer2/audio/Ac3UtilTest.java rename to library/common/src/test/java/com/google/android/exoplayer2/audio/Ac3UtilTest.java index 8a7d0b6f06..26c4214b7a 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/audio/Ac3UtilTest.java +++ b/library/common/src/test/java/com/google/android/exoplayer2/audio/Ac3UtilTest.java @@ -33,13 +33,13 @@ public final class Ac3UtilTest { Util.getBytesFromHexString("A025048860224E6F6DEDB6D5B6DBAFE6"); @Test - public void testParseTrueHdSyncframeAudioSampleCount_nonSyncframe() { + public void parseTrueHdSyncframeAudioSampleCount_nonSyncframe() { assertThat(Ac3Util.parseTrueHdSyncframeAudioSampleCount(TRUEHD_NON_SYNCFRAME_HEADER)) .isEqualTo(0); } @Test - public void testParseTrueHdSyncframeAudioSampleCount_syncframe() { + public void parseTrueHdSyncframeAudioSampleCount_syncframe() { assertThat(Ac3Util.parseTrueHdSyncframeAudioSampleCount(TRUEHD_SYNCFRAME_HEADER)) .isEqualTo(TRUEHD_SYNCFRAME_SAMPLE_COUNT); } diff --git a/library/common/src/test/java/com/google/android/exoplayer2/decoder/CryptoInfoTest.java b/library/common/src/test/java/com/google/android/exoplayer2/decoder/CryptoInfoTest.java new file mode 100644 index 0000000000..2426d473e9 --- /dev/null +++ b/library/common/src/test/java/com/google/android/exoplayer2/decoder/CryptoInfoTest.java @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2020 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.decoder; + +import static com.google.common.truth.Truth.assertThat; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Unit tests for {@link CryptoInfo} */ +@RunWith(AndroidJUnit4.class) +public class CryptoInfoTest { + + private CryptoInfo cryptoInfo; + + @Before + public void setUp() { + cryptoInfo = new CryptoInfo(); + } + + @Test + public void increaseClearDataFirstSubSampleBy_numBytesOfClearDataIsNullAndZeroInput_isNoOp() { + cryptoInfo.increaseClearDataFirstSubSampleBy(0); + + assertThat(cryptoInfo.numBytesOfClearData).isNull(); + assertThat(cryptoInfo.getFrameworkCryptoInfo().numBytesOfClearData).isNull(); + } + + @Test + public void increaseClearDataFirstSubSampleBy_withNumBytesOfClearDataSetAndZeroInput_isNoOp() { + int[] data = new int[] {1, 1, 1, 1}; + cryptoInfo.numBytesOfClearData = data; + cryptoInfo.getFrameworkCryptoInfo().numBytesOfClearData = data; + + cryptoInfo.increaseClearDataFirstSubSampleBy(5); + + assertThat(cryptoInfo.numBytesOfClearData[0]).isEqualTo(6); + assertThat(cryptoInfo.getFrameworkCryptoInfo().numBytesOfClearData[0]).isEqualTo(6); + } + + @Test + public void increaseClearDataFirstSubSampleBy_withSharedClearDataPointer_setsValue() { + int[] data = new int[] {1, 1, 1, 1}; + cryptoInfo.numBytesOfClearData = data; + cryptoInfo.getFrameworkCryptoInfo().numBytesOfClearData = data; + + cryptoInfo.increaseClearDataFirstSubSampleBy(5); + + assertThat(cryptoInfo.numBytesOfClearData[0]).isEqualTo(6); + assertThat(cryptoInfo.getFrameworkCryptoInfo().numBytesOfClearData[0]).isEqualTo(6); + } +} diff --git a/library/core/src/test/java/com/google/android/exoplayer2/drm/DrmInitDataTest.java b/library/common/src/test/java/com/google/android/exoplayer2/drm/DrmInitDataTest.java similarity index 96% rename from library/core/src/test/java/com/google/android/exoplayer2/drm/DrmInitDataTest.java rename to library/common/src/test/java/com/google/android/exoplayer2/drm/DrmInitDataTest.java index e3d7cdbbf4..e7b46e5c99 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/drm/DrmInitDataTest.java +++ b/library/common/src/test/java/com/google/android/exoplayer2/drm/DrmInitDataTest.java @@ -47,7 +47,7 @@ public class DrmInitDataTest { TestUtil.buildTestData(128, 3 /* data seed */)); @Test - public void testParcelable() { + public void parcelable() { DrmInitData drmInitDataToParcel = new DrmInitData(DATA_1, DATA_2); Parcel parcel = Parcel.obtain(); @@ -61,7 +61,7 @@ public class DrmInitDataTest { } @Test - public void testEquals() { + public void equals() { DrmInitData drmInitData = new DrmInitData(DATA_1, DATA_2); // Basic non-referential equality test. @@ -95,7 +95,7 @@ public class DrmInitDataTest { @Test @SuppressWarnings("deprecation") - public void testGetByUuid() { + public void getByUuid() { // Basic matching. DrmInitData testInitData = new DrmInitData(DATA_1, DATA_2); assertThat(testInitData.get(WIDEVINE_UUID)).isEqualTo(DATA_1); @@ -122,14 +122,14 @@ public class DrmInitDataTest { } @Test - public void testGetByIndex() { + public void getByIndex() { DrmInitData testInitData = new DrmInitData(DATA_1, DATA_2); assertThat(getAllSchemeData(testInitData)).containsAtLeast(DATA_1, DATA_2); } @Test @SuppressWarnings("deprecation") - public void testSchemeDatasWithSameUuid() { + public void schemeDatasWithSameUuid() { DrmInitData testInitData = new DrmInitData(DATA_1, DATA_1B); assertThat(testInitData.schemeDataCount).isEqualTo(2); // Deprecated get method should return first entry. @@ -140,7 +140,7 @@ public class DrmInitDataTest { } @Test - public void testSchemeDataMatches() { + public void schemeDataMatches() { assertThat(DATA_1.matches(WIDEVINE_UUID)).isTrue(); assertThat(DATA_1.matches(PLAYREADY_UUID)).isFalse(); assertThat(DATA_2.matches(UUID_NIL)).isFalse(); diff --git a/library/common/src/test/java/com/google/android/exoplayer2/metadata/MetadataTest.java b/library/common/src/test/java/com/google/android/exoplayer2/metadata/MetadataTest.java new file mode 100644 index 0000000000..ac3bfdcef9 --- /dev/null +++ b/library/common/src/test/java/com/google/android/exoplayer2/metadata/MetadataTest.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2019 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; + +import static com.google.common.truth.Truth.assertThat; + +import android.os.Parcel; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.android.exoplayer2.metadata.id3.BinaryFrame; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Tests for {@link Metadata}. */ +@RunWith(AndroidJUnit4.class) +public class MetadataTest { + + @Test + public void parcelable() { + Metadata metadataToParcel = + new Metadata( + new BinaryFrame("id1", new byte[] {1}), new BinaryFrame("id2", new byte[] {2})); + + Parcel parcel = Parcel.obtain(); + metadataToParcel.writeToParcel(parcel, 0); + parcel.setDataPosition(0); + + Metadata metadataFromParcel = Metadata.CREATOR.createFromParcel(parcel); + assertThat(metadataFromParcel).isEqualTo(metadataToParcel); + + parcel.recycle(); + } +} diff --git a/library/core/src/test/java/com/google/android/exoplayer2/metadata/emsg/EventMessageDecoderTest.java b/library/common/src/test/java/com/google/android/exoplayer2/metadata/emsg/EventMessageDecoderTest.java similarity index 62% rename from library/core/src/test/java/com/google/android/exoplayer2/metadata/emsg/EventMessageDecoderTest.java rename to library/common/src/test/java/com/google/android/exoplayer2/metadata/emsg/EventMessageDecoderTest.java index 88a61d0bce..ee2c55a735 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/metadata/emsg/EventMessageDecoderTest.java +++ b/library/common/src/test/java/com/google/android/exoplayer2/metadata/emsg/EventMessageDecoderTest.java @@ -16,13 +16,14 @@ package com.google.android.exoplayer2.metadata.emsg; import static com.google.android.exoplayer2.testutil.TestUtil.createByteArray; +import static com.google.android.exoplayer2.testutil.TestUtil.createMetadataInputBuffer; import static com.google.android.exoplayer2.testutil.TestUtil.joinByteArrays; import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.MetadataInputBuffer; -import java.nio.ByteBuffer; import org.junit.Test; import org.junit.runner.RunWith; @@ -31,7 +32,7 @@ import org.junit.runner.RunWith; public final class EventMessageDecoderTest { @Test - public void testDecodeEventMessage() { + public void decodeEventMessage() { byte[] rawEmsgBody = joinByteArrays( createByteArray(117, 114, 110, 58, 116, 101, 115, 116, 0), // scheme_id_uri = "urn:test" @@ -40,10 +41,8 @@ public final class EventMessageDecoderTest { createByteArray(0, 15, 67, 211), // id = 1000403 createByteArray(0, 1, 2, 3, 4)); // message_data = {0, 1, 2, 3, 4} EventMessageDecoder decoder = new EventMessageDecoder(); - MetadataInputBuffer buffer = new MetadataInputBuffer(); - buffer.data = ByteBuffer.allocate(rawEmsgBody.length).put(rawEmsgBody); - Metadata metadata = decoder.decode(buffer); + Metadata metadata = decoder.decode(createMetadataInputBuffer(rawEmsgBody)); assertThat(metadata.length()).isEqualTo(1); EventMessage eventMessage = (EventMessage) metadata.get(0); @@ -54,4 +53,31 @@ public final class EventMessageDecoderTest { assertThat(eventMessage.messageData).isEqualTo(new byte[]{0, 1, 2, 3, 4}); } + @Test + public void decodeEventMessage_failsIfPositionNonZero() { + EventMessageDecoder decoder = new EventMessageDecoder(); + MetadataInputBuffer buffer = createMetadataInputBuffer(createByteArray(1, 2, 3)); + buffer.data.position(1); + + assertThrows(IllegalArgumentException.class, () -> decoder.decode(buffer)); + } + + @Test + public void decodeEventMessage_failsIfBufferHasNoArray() { + EventMessageDecoder decoder = new EventMessageDecoder(); + MetadataInputBuffer buffer = createMetadataInputBuffer(createByteArray(1, 2, 3)); + buffer.data = buffer.data.asReadOnlyBuffer(); + + assertThrows(IllegalArgumentException.class, () -> decoder.decode(buffer)); + } + + @Test + public void decodeEventMessage_failsIfArrayOffsetNonZero() { + EventMessageDecoder decoder = new EventMessageDecoder(); + MetadataInputBuffer buffer = createMetadataInputBuffer(createByteArray(1, 2, 3)); + buffer.data.position(1); + buffer.data = buffer.data.slice(); + + assertThrows(IllegalArgumentException.class, () -> decoder.decode(buffer)); + } } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/metadata/emsg/EventMessageEncoderTest.java b/library/common/src/test/java/com/google/android/exoplayer2/metadata/emsg/EventMessageEncoderTest.java similarity index 88% rename from library/core/src/test/java/com/google/android/exoplayer2/metadata/emsg/EventMessageEncoderTest.java rename to library/common/src/test/java/com/google/android/exoplayer2/metadata/emsg/EventMessageEncoderTest.java index 56830035cc..fc73b0cdaf 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/metadata/emsg/EventMessageEncoderTest.java +++ b/library/common/src/test/java/com/google/android/exoplayer2/metadata/emsg/EventMessageEncoderTest.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.metadata.emsg; import static com.google.android.exoplayer2.testutil.TestUtil.createByteArray; +import static com.google.android.exoplayer2.testutil.TestUtil.createMetadataInputBuffer; import static com.google.android.exoplayer2.testutil.TestUtil.joinByteArrays; import static com.google.common.truth.Truth.assertThat; @@ -23,7 +24,6 @@ import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.MetadataInputBuffer; import java.io.IOException; -import java.nio.ByteBuffer; import org.junit.Test; import org.junit.runner.RunWith; @@ -43,18 +43,15 @@ public final class EventMessageEncoderTest { createByteArray(0, 1, 2, 3, 4)); // message_data = {0, 1, 2, 3, 4} @Test - public void testEncodeEventStream() throws IOException { - byte[] foo = new byte[] {1, 2, 3}; - + public void encodeEventStream() throws IOException { byte[] encodedByteArray = new EventMessageEncoder().encode(DECODED_MESSAGE); assertThat(encodedByteArray).isEqualTo(ENCODED_MESSAGE); } @Test - public void testEncodeDecodeEventStream() throws IOException { + public void encodeDecodeEventStream() throws IOException { byte[] encodedByteArray = new EventMessageEncoder().encode(DECODED_MESSAGE); - MetadataInputBuffer buffer = new MetadataInputBuffer(); - buffer.data = ByteBuffer.allocate(encodedByteArray.length).put(encodedByteArray); + MetadataInputBuffer buffer = createMetadataInputBuffer(encodedByteArray); EventMessageDecoder decoder = new EventMessageDecoder(); Metadata metadata = decoder.decode(buffer); @@ -63,7 +60,7 @@ public final class EventMessageEncoderTest { } @Test - public void testEncodeEventStreamMultipleTimesWorkingCorrectly() throws IOException { + public void encodeEventStreamMultipleTimesWorkingCorrectly() throws IOException { EventMessage eventMessage1 = new EventMessage("urn:test", "123", 3000, 1000402, new byte[] {4, 3, 2, 1, 0}); byte[] expectedEmsgBody1 = diff --git a/library/core/src/test/java/com/google/android/exoplayer2/metadata/emsg/EventMessageTest.java b/library/common/src/test/java/com/google/android/exoplayer2/metadata/emsg/EventMessageTest.java similarity index 96% rename from library/core/src/test/java/com/google/android/exoplayer2/metadata/emsg/EventMessageTest.java rename to library/common/src/test/java/com/google/android/exoplayer2/metadata/emsg/EventMessageTest.java index d9e9ab7ea7..9a47b81d44 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/metadata/emsg/EventMessageTest.java +++ b/library/common/src/test/java/com/google/android/exoplayer2/metadata/emsg/EventMessageTest.java @@ -27,7 +27,7 @@ import org.junit.runner.RunWith; public final class EventMessageTest { @Test - public void testEventMessageParcelable() { + public void eventMessageParcelable() { EventMessage eventMessage = new EventMessage("urn:test", "123", 3000, 1000403, new byte[] {0, 1, 2, 3, 4}); // Write to parcel. diff --git a/library/core/src/test/java/com/google/android/exoplayer2/metadata/flac/PictureFrameTest.java b/library/common/src/test/java/com/google/android/exoplayer2/metadata/flac/PictureFrameTest.java similarity index 97% rename from library/core/src/test/java/com/google/android/exoplayer2/metadata/flac/PictureFrameTest.java rename to library/common/src/test/java/com/google/android/exoplayer2/metadata/flac/PictureFrameTest.java index 3f07dbc26d..6b7a18e287 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/metadata/flac/PictureFrameTest.java +++ b/library/common/src/test/java/com/google/android/exoplayer2/metadata/flac/PictureFrameTest.java @@ -27,7 +27,7 @@ import org.junit.runner.RunWith; public final class PictureFrameTest { @Test - public void testParcelable() { + public void parcelable() { PictureFrame pictureFrameToParcel = new PictureFrame(0, "", "", 0, 0, 0, 0, new byte[0]); Parcel parcel = Parcel.obtain(); diff --git a/library/core/src/test/java/com/google/android/exoplayer2/metadata/flac/VorbisCommentTest.java b/library/common/src/test/java/com/google/android/exoplayer2/metadata/flac/VorbisCommentTest.java similarity index 97% rename from library/core/src/test/java/com/google/android/exoplayer2/metadata/flac/VorbisCommentTest.java rename to library/common/src/test/java/com/google/android/exoplayer2/metadata/flac/VorbisCommentTest.java index bb118e381a..e3a12a8013 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/metadata/flac/VorbisCommentTest.java +++ b/library/common/src/test/java/com/google/android/exoplayer2/metadata/flac/VorbisCommentTest.java @@ -27,7 +27,7 @@ import org.junit.runner.RunWith; public final class VorbisCommentTest { @Test - public void testParcelable() { + public void parcelable() { VorbisComment vorbisCommentFrameToParcel = new VorbisComment("key", "value"); Parcel parcel = Parcel.obtain(); diff --git a/library/core/src/test/java/com/google/android/exoplayer2/metadata/id3/ChapterFrameTest.java b/library/common/src/test/java/com/google/android/exoplayer2/metadata/id3/ChapterFrameTest.java similarity index 97% rename from library/core/src/test/java/com/google/android/exoplayer2/metadata/id3/ChapterFrameTest.java rename to library/common/src/test/java/com/google/android/exoplayer2/metadata/id3/ChapterFrameTest.java index fbd824c7c1..f3e883833a 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/metadata/id3/ChapterFrameTest.java +++ b/library/common/src/test/java/com/google/android/exoplayer2/metadata/id3/ChapterFrameTest.java @@ -27,7 +27,7 @@ import org.junit.runner.RunWith; public final class ChapterFrameTest { @Test - public void testParcelable() { + public void parcelable() { Id3Frame[] subFrames = new Id3Frame[] { new TextInformationFrame("TIT2", null, "title"), new UrlLinkFrame("WXXX", "description", "url") diff --git a/library/core/src/test/java/com/google/android/exoplayer2/metadata/id3/ChapterTocFrameTest.java b/library/common/src/test/java/com/google/android/exoplayer2/metadata/id3/ChapterTocFrameTest.java similarity index 98% rename from library/core/src/test/java/com/google/android/exoplayer2/metadata/id3/ChapterTocFrameTest.java rename to library/common/src/test/java/com/google/android/exoplayer2/metadata/id3/ChapterTocFrameTest.java index daf9ff1bb5..0b4a9859c9 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/metadata/id3/ChapterTocFrameTest.java +++ b/library/common/src/test/java/com/google/android/exoplayer2/metadata/id3/ChapterTocFrameTest.java @@ -27,7 +27,7 @@ import org.junit.runner.RunWith; public final class ChapterTocFrameTest { @Test - public void testParcelable() { + public void parcelable() { String[] children = new String[] {"child0", "child1"}; Id3Frame[] subFrames = new Id3Frame[] { new TextInformationFrame("TIT2", null, "title"), diff --git a/library/core/src/test/java/com/google/android/exoplayer2/metadata/id3/Id3DecoderTest.java b/library/common/src/test/java/com/google/android/exoplayer2/metadata/id3/Id3DecoderTest.java similarity index 88% rename from library/core/src/test/java/com/google/android/exoplayer2/metadata/id3/Id3DecoderTest.java rename to library/common/src/test/java/com/google/android/exoplayer2/metadata/id3/Id3DecoderTest.java index a20cbb66a2..6389417464 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/metadata/id3/Id3DecoderTest.java +++ b/library/common/src/test/java/com/google/android/exoplayer2/metadata/id3/Id3DecoderTest.java @@ -15,11 +15,15 @@ */ package com.google.android.exoplayer2.metadata.id3; +import static com.google.android.exoplayer2.testutil.TestUtil.createByteArray; +import static com.google.android.exoplayer2.testutil.TestUtil.createMetadataInputBuffer; import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.metadata.Metadata; +import com.google.android.exoplayer2.metadata.MetadataInputBuffer; import com.google.android.exoplayer2.util.Assertions; import java.nio.charset.Charset; import java.util.Arrays; @@ -35,7 +39,7 @@ public final class Id3DecoderTest { private static final int ID3_TEXT_ENCODING_UTF_8 = 3; @Test - public void testDecodeTxxxFrame() { + public void decodeTxxxFrame() { 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(); @@ -62,7 +66,7 @@ public final class Id3DecoderTest { } @Test - public void testDecodeTextInformationFrame() { + public void decodeTextInformationFrame() { byte[] rawId3 = buildSingleFrameTag("TIT2", new byte[] {3, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 0}); Id3Decoder decoder = new Id3Decoder(); @@ -89,7 +93,7 @@ public final class Id3DecoderTest { } @Test - public void testDecodeWxxxFrame() { + public void decodeWxxxFrame() { 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}); @@ -117,7 +121,7 @@ public final class Id3DecoderTest { } @Test - public void testDecodeUrlLinkFrame() { + public void decodeUrlLinkFrame() { 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(); @@ -139,7 +143,7 @@ public final class Id3DecoderTest { } @Test - public void testDecodePrivFrame() { + public void decodePrivFrame() { 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); @@ -158,7 +162,7 @@ public final class Id3DecoderTest { } @Test - public void testDecodeApicFrame() { + public void decodeApicFrame() { 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}); @@ -174,7 +178,7 @@ public final class Id3DecoderTest { } @Test - public void testDecodeCommentFrame() { + public void decodeCommentFrame() { 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(); @@ -201,7 +205,7 @@ public final class Id3DecoderTest { } @Test - public void testDecodeMultiFrames() { + public void decodeMultiFrames() { byte[] rawId3 = buildMultiFramesTag( new FrameSpec( @@ -233,6 +237,34 @@ public final class Id3DecoderTest { assertThat(apicFrame.pictureData).isEqualTo(new byte[] {1, 2, 3, 4, 5, 6, 7, 8, 9, 0}); } + @Test + public void decodeFailsIfPositionNonZero() { + Id3Decoder decoder = new Id3Decoder(); + MetadataInputBuffer buffer = createMetadataInputBuffer(createByteArray(1, 2, 3)); + buffer.data.position(1); + + assertThrows(IllegalArgumentException.class, () -> decoder.decode(buffer)); + } + + @Test + public void decodeFailsIfBufferHasNoArray() { + Id3Decoder decoder = new Id3Decoder(); + MetadataInputBuffer buffer = createMetadataInputBuffer(createByteArray(1, 2, 3)); + buffer.data = buffer.data.asReadOnlyBuffer(); + + assertThrows(IllegalArgumentException.class, () -> decoder.decode(buffer)); + } + + @Test + public void decodeFailsIfArrayOffsetNonZero() { + Id3Decoder decoder = new Id3Decoder(); + MetadataInputBuffer buffer = createMetadataInputBuffer(createByteArray(1, 2, 3)); + buffer.data.position(1); + buffer.data = buffer.data.slice(); + + assertThrows(IllegalArgumentException.class, () -> decoder.decode(buffer)); + } + public static byte[] buildSingleFrameTag(String frameId, byte[] frameData) { return buildMultiFramesTag(new FrameSpec(frameId, frameData)); } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/metadata/id3/MlltFrameTest.java b/library/common/src/test/java/com/google/android/exoplayer2/metadata/id3/MlltFrameTest.java similarity index 97% rename from library/core/src/test/java/com/google/android/exoplayer2/metadata/id3/MlltFrameTest.java rename to library/common/src/test/java/com/google/android/exoplayer2/metadata/id3/MlltFrameTest.java index d6bbecdf6c..48e8fcede3 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/metadata/id3/MlltFrameTest.java +++ b/library/common/src/test/java/com/google/android/exoplayer2/metadata/id3/MlltFrameTest.java @@ -27,7 +27,7 @@ import org.junit.runner.RunWith; public final class MlltFrameTest { @Test - public void testParcelable() { + public void parcelable() { MlltFrame mlltFrameToParcel = new MlltFrame( /* mpegFramesBetweenReference= */ 1, diff --git a/library/core/src/test/java/com/google/android/exoplayer2/offline/StreamKeyTest.java b/library/common/src/test/java/com/google/android/exoplayer2/offline/StreamKeyTest.java similarity index 97% rename from library/core/src/test/java/com/google/android/exoplayer2/offline/StreamKeyTest.java rename to library/common/src/test/java/com/google/android/exoplayer2/offline/StreamKeyTest.java index a18c278913..0c14f32f9a 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/offline/StreamKeyTest.java +++ b/library/common/src/test/java/com/google/android/exoplayer2/offline/StreamKeyTest.java @@ -27,7 +27,7 @@ import org.junit.runner.RunWith; public class StreamKeyTest { @Test - public void testParcelable() { + public void parcelable() { StreamKey streamKeyToParcel = new StreamKey(1, 2, 3); Parcel parcel = Parcel.obtain(); streamKeyToParcel.writeToParcel(parcel, 0); diff --git a/library/common/src/test/java/com/google/android/exoplayer2/upstream/DataSpecTest.java b/library/common/src/test/java/com/google/android/exoplayer2/upstream/DataSpecTest.java new file mode 100644 index 0000000000..b3441bbf56 --- /dev/null +++ b/library/common/src/test/java/com/google/android/exoplayer2/upstream/DataSpecTest.java @@ -0,0 +1,366 @@ +/* + * Copyright (C) 2019 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 static junit.framework.TestCase.fail; + +import android.net.Uri; +import androidx.annotation.Nullable; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.android.exoplayer2.C; +import java.util.HashMap; +import java.util.Map; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Unit tests for {@link DataSpec}. */ +@RunWith(AndroidJUnit4.class) +public class DataSpecTest { + + @SuppressWarnings("deprecation") + @Test + public void createDataSpec_withDefaultValues() { + Uri uri = Uri.parse("www.google.com"); + + DataSpec dataSpec = new DataSpec(uri); + assertDefaultDataSpec(dataSpec, uri); + + dataSpec = new DataSpec(uri, /* flags= */ 0); + assertDefaultDataSpec(dataSpec, uri); + + dataSpec = new DataSpec(uri, /* position= */ 0, C.LENGTH_UNSET, /* key= */ null); + assertDefaultDataSpec(dataSpec, uri); + + dataSpec = + new DataSpec(uri, /* position= */ 0, C.LENGTH_UNSET, /* key= */ null, /* flags= */ 0); + assertDefaultDataSpec(dataSpec, uri); + + dataSpec = + new DataSpec( + uri, + /* position= */ 0, + /* length= */ C.LENGTH_UNSET, + /* key= */ null, + /* flags= */ 0, + new HashMap<>()); + assertDefaultDataSpec(dataSpec, uri); + + dataSpec = + new DataSpec( + uri, + /* absoluteStreamPosition= */ 0, + /* position= */ 0, + /* length= */ C.LENGTH_UNSET, + null, + /* flags= */ 0); + assertDefaultDataSpec(dataSpec, uri); + + dataSpec = + new DataSpec( + uri, + DataSpec.HTTP_METHOD_GET, + /* httpBody= */ null, + /* absoluteStreamPosition= */ 0, + /* position= */ 0, + /* length= */ C.LENGTH_UNSET, + /* key= */ null, + /* flags= */ 0); + assertDefaultDataSpec(dataSpec, uri); + + dataSpec = + new DataSpec( + uri, + DataSpec.HTTP_METHOD_GET, + /* httpBody= */ null, + /* absoluteStreamPosition= */ 0, + /* position= */ 0, + /* length= */ C.LENGTH_UNSET, + /* key= */ null, + /* flags= */ 0, + new HashMap<>()); + assertDefaultDataSpec(dataSpec, uri); + } + + @Test + public void createDataSpec_withBuilder_withDefaultValues() { + Uri uri = Uri.parse("www.google.com"); + + DataSpec dataSpec = new DataSpec.Builder().setUri(uri).build(); + assertDefaultDataSpec(dataSpec, uri); + } + + @SuppressWarnings("deprecation") + @Test + public void createDataSpec_setsValues() { + Uri uri = Uri.parse("www.google.com"); + Map httpRequestHeaders = createHttpRequestHeaders(3); + byte[] httpBody = new byte[] {0, 1, 2, 3}; + + DataSpec dataSpec = + new DataSpec( + uri, + DataSpec.HTTP_METHOD_POST, + httpBody, + /* absoluteStreamPosition= */ 200, + /* position= */ 150, + /* length= */ 5, + /* key= */ "key", + /* flags= */ DataSpec.FLAG_ALLOW_GZIP, + httpRequestHeaders); + + assertThat(dataSpec.uri).isEqualTo(uri); + // uriPositionOffset = absoluteStreamPosition - position + assertThat(dataSpec.uriPositionOffset).isEqualTo(50); + assertThat(dataSpec.httpMethod).isEqualTo(DataSpec.HTTP_METHOD_POST); + assertThat(dataSpec.httpBody).isEqualTo(httpBody); + assertThat(dataSpec.httpRequestHeaders).isEqualTo(httpRequestHeaders); + assertThat(dataSpec.absoluteStreamPosition).isEqualTo(200); + assertThat(dataSpec.position).isEqualTo(150); + assertThat(dataSpec.length).isEqualTo(5); + assertThat(dataSpec.key).isEqualTo("key"); + assertThat(dataSpec.flags).isEqualTo(DataSpec.FLAG_ALLOW_GZIP); + assertHttpRequestHeadersReadOnly(dataSpec); + } + + @SuppressWarnings("deprecation") + @Test + public void createDataSpec_withBuilder_setsValues() { + Uri uri = Uri.parse("www.google.com"); + Map httpRequestHeaders = createHttpRequestHeaders(3); + byte[] httpBody = new byte[] {0, 1, 2, 3}; + Object customData = new Object(); + + DataSpec dataSpec = + new DataSpec.Builder() + .setUri(uri) + .setUriPositionOffset(50) + .setHttpMethod(DataSpec.HTTP_METHOD_POST) + .setHttpBody(httpBody) + .setPosition(150) + .setLength(5) + .setKey("key") + .setFlags(DataSpec.FLAG_ALLOW_GZIP) + .setHttpRequestHeaders(httpRequestHeaders) + .setCustomData(customData) + .build(); + + assertThat(dataSpec.uri).isEqualTo(uri); + assertThat(dataSpec.uriPositionOffset).isEqualTo(50); + assertThat(dataSpec.httpMethod).isEqualTo(DataSpec.HTTP_METHOD_POST); + assertThat(dataSpec.httpBody).isEqualTo(httpBody); + assertThat(dataSpec.httpRequestHeaders).isEqualTo(httpRequestHeaders); + // absoluteStreamPosition = uriPositionOffset + position + assertThat(dataSpec.absoluteStreamPosition).isEqualTo(200); + assertThat(dataSpec.position).isEqualTo(150); + assertThat(dataSpec.length).isEqualTo(5); + assertThat(dataSpec.key).isEqualTo("key"); + assertThat(dataSpec.flags).isEqualTo(DataSpec.FLAG_ALLOW_GZIP); + assertThat(dataSpec.customData).isEqualTo(customData); + assertHttpRequestHeadersReadOnly(dataSpec); + } + + @SuppressWarnings("deprecation") + @Test + public void buildUponDataSpec_setsValues() { + Uri uri = Uri.parse("www.google.com"); + Map httpRequestHeaders = createHttpRequestHeaders(3); + byte[] httpBody = new byte[] {0, 1, 2, 3}; + Object customData = new Object(); + + DataSpec dataSpec = + new DataSpec.Builder() + .setUri(uri) + .setUriPositionOffset(50) + .setHttpMethod(DataSpec.HTTP_METHOD_POST) + .setHttpBody(httpBody) + .setPosition(150) + .setLength(5) + .setKey("key") + .setFlags(DataSpec.FLAG_ALLOW_GZIP) + .setHttpRequestHeaders(httpRequestHeaders) + .setCustomData(customData) + .build(); + + // Build upon the DataSpec. + dataSpec = dataSpec.buildUpon().build(); + + assertThat(dataSpec.uri).isEqualTo(uri); + assertThat(dataSpec.uriPositionOffset).isEqualTo(50); + assertThat(dataSpec.httpMethod).isEqualTo(DataSpec.HTTP_METHOD_POST); + assertThat(dataSpec.httpBody).isEqualTo(httpBody); + assertThat(dataSpec.httpRequestHeaders).isEqualTo(httpRequestHeaders); + // absoluteStreamPosition = uriPositionOffset + position + assertThat(dataSpec.absoluteStreamPosition).isEqualTo(200); + assertThat(dataSpec.position).isEqualTo(150); + assertThat(dataSpec.length).isEqualTo(5); + assertThat(dataSpec.key).isEqualTo("key"); + assertThat(dataSpec.flags).isEqualTo(DataSpec.FLAG_ALLOW_GZIP); + assertThat(dataSpec.customData).isEqualTo(customData); + assertHttpRequestHeadersReadOnly(dataSpec); + } + + @SuppressWarnings("deprecation") + @Test + public void createDataSpec_setsHttpMethodAndPostBody() { + Uri uri = Uri.parse("www.google.com"); + + @Nullable byte[] postBody = new byte[] {0, 1, 2, 3}; + DataSpec dataSpec = + new DataSpec( + uri, + postBody, + /* absoluteStreamPosition= */ 0, + /* position= */ 0, + /* length= */ C.LENGTH_UNSET, + /* key= */ null, + /* flags= */ 0); + assertThat(dataSpec.httpMethod).isEqualTo(DataSpec.HTTP_METHOD_POST); + assertThat(dataSpec.httpBody).isEqualTo(postBody); + + postBody = new byte[0]; + dataSpec = + new DataSpec( + uri, + postBody, + /* absoluteStreamPosition= */ 0, + /* position= */ 0, + /* length= */ C.LENGTH_UNSET, + /* key= */ null, + /* flags= */ 0); + assertThat(dataSpec.httpMethod).isEqualTo(DataSpec.HTTP_METHOD_POST); + assertThat(dataSpec.httpBody).isNull(); + + postBody = null; + dataSpec = + new DataSpec( + uri, + postBody, + /* absoluteStreamPosition= */ 0, + /* position= */ 0, + /* length= */ C.LENGTH_UNSET, + /* key= */ null, + /* flags= */ 0); + assertThat(dataSpec.httpMethod).isEqualTo(DataSpec.HTTP_METHOD_GET); + assertThat(dataSpec.httpBody).isNull(); + } + + @Test + public void withUri_copiesHttpRequestHeaders() { + Map httpRequestHeaders = createHttpRequestHeaders(5); + DataSpec dataSpec = createDataSpecWithHttpRequestHeaders(httpRequestHeaders); + + DataSpec dataSpecCopy = dataSpec.withUri(Uri.parse("www.new-uri.com")); + + assertThat(dataSpecCopy.httpRequestHeaders).isEqualTo(httpRequestHeaders); + } + + @Test + public void subrange_copiesHttpRequestHeaders() { + Map httpRequestHeaders = createHttpRequestHeaders(5); + DataSpec dataSpec = createDataSpecWithHttpRequestHeaders(httpRequestHeaders); + + DataSpec dataSpecCopy = dataSpec.subrange(2); + + assertThat(dataSpecCopy.httpRequestHeaders).isEqualTo(httpRequestHeaders); + } + + @Test + public void subrange_withOffsetAndLength_copiesHttpRequestHeaders() { + Map httpRequestHeaders = createHttpRequestHeaders(5); + DataSpec dataSpec = createDataSpecWithHttpRequestHeaders(httpRequestHeaders); + + DataSpec dataSpecCopy = dataSpec.subrange(2, 2); + + assertThat(dataSpecCopy.httpRequestHeaders).isEqualTo(httpRequestHeaders); + } + + @Test + public void withRequestHeaders_setsCorrectHeaders() { + Map httpRequestHeaders = createHttpRequestHeaders(5); + DataSpec dataSpec = createDataSpecWithHttpRequestHeaders(httpRequestHeaders); + + Map newRequestHeaders = createHttpRequestHeaders(5, 10); + DataSpec dataSpecCopy = dataSpec.withRequestHeaders(newRequestHeaders); + + assertThat(dataSpecCopy.httpRequestHeaders).isEqualTo(newRequestHeaders); + } + + @Test + public void withAdditionalHeaders_setsCorrectHeaders() { + Map httpRequestHeaders = createHttpRequestHeaders(5); + DataSpec dataSpec = createDataSpecWithHttpRequestHeaders(httpRequestHeaders); + Map additionalHeaders = createHttpRequestHeaders(5, 10); + // additionalHeaders may overwrite a header key + String existingKey = httpRequestHeaders.keySet().iterator().next(); + additionalHeaders.put(existingKey, "overwritten"); + Map expectedHeaders = new HashMap<>(httpRequestHeaders); + expectedHeaders.putAll(additionalHeaders); + + DataSpec dataSpecCopy = dataSpec.withAdditionalHeaders(additionalHeaders); + + assertThat(dataSpecCopy.httpRequestHeaders).isEqualTo(expectedHeaders); + } + + private static Map createHttpRequestHeaders(int howMany) { + return createHttpRequestHeaders(0, howMany); + } + + private static Map createHttpRequestHeaders(int from, int to) { + assertThat(from).isLessThan(to); + + Map httpRequestParameters = new HashMap<>(); + for (int i = from; i < to; i++) { + httpRequestParameters.put("key-" + i, "value-" + i); + } + + return httpRequestParameters; + } + + private static DataSpec createDataSpecWithHttpRequestHeaders( + Map httpRequestHeaders) { + return new DataSpec.Builder() + .setUri("www.google.com") + .setHttpRequestHeaders(httpRequestHeaders) + .build(); + } + + @SuppressWarnings("deprecation") + private static void assertDefaultDataSpec(DataSpec dataSpec, Uri uri) { + assertThat(dataSpec.uri).isEqualTo(uri); + assertThat(dataSpec.uriPositionOffset).isEqualTo(0); + assertThat(dataSpec.httpMethod).isEqualTo(DataSpec.HTTP_METHOD_GET); + assertThat(dataSpec.httpBody).isNull(); + assertThat(dataSpec.httpRequestHeaders).isEmpty(); + assertThat(dataSpec.absoluteStreamPosition).isEqualTo(0); + assertThat(dataSpec.position).isEqualTo(0); + assertThat(dataSpec.length).isEqualTo(C.LENGTH_UNSET); + assertThat(dataSpec.key).isNull(); + assertThat(dataSpec.flags).isEqualTo(0); + assertThat(dataSpec.customData).isNull(); + assertHttpRequestHeadersReadOnly(dataSpec); + } + + private static void assertHttpRequestHeadersReadOnly(DataSpec dataSpec) { + try { + dataSpec.httpRequestHeaders.put("key", "value"); + fail(); + } catch (UnsupportedOperationException expected) { + // Expected + } + } +} diff --git a/library/core/src/test/java/com/google/android/exoplayer2/util/CodecSpecificDataUtilTest.java b/library/common/src/test/java/com/google/android/exoplayer2/util/CodecSpecificDataUtilTest.java similarity index 100% rename from library/core/src/test/java/com/google/android/exoplayer2/util/CodecSpecificDataUtilTest.java rename to library/common/src/test/java/com/google/android/exoplayer2/util/CodecSpecificDataUtilTest.java diff --git a/library/common/src/test/java/com/google/android/exoplayer2/util/CopyOnWriteMultisetTest.java b/library/common/src/test/java/com/google/android/exoplayer2/util/CopyOnWriteMultisetTest.java new file mode 100644 index 0000000000..92e4124a6a --- /dev/null +++ b/library/common/src/test/java/com/google/android/exoplayer2/util/CopyOnWriteMultisetTest.java @@ -0,0 +1,110 @@ +/* + * Copyright (C) 2020 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 static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import java.util.Iterator; +import java.util.Set; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Tests for {@link CopyOnWriteMultiset}. */ +@RunWith(AndroidJUnit4.class) +public final class CopyOnWriteMultisetTest { + + @Test + public void multipleEqualObjectsCountedAsExpected() { + String item1 = "a string"; + String item2 = "a string"; + String item3 = "different string"; + + CopyOnWriteMultiset multiset = new CopyOnWriteMultiset<>(); + + multiset.add(item1); + multiset.add(item2); + multiset.add(item3); + + assertThat(multiset).containsExactly("a string", "a string", "different string"); + assertThat(multiset.elementSet()).containsExactly("a string", "different string"); + } + + @Test + public void removingObjectDecrementsCount() { + String item1 = "a string"; + String item2 = "a string"; + String item3 = "different string"; + + CopyOnWriteMultiset multiset = new CopyOnWriteMultiset<>(); + + multiset.add(item1); + multiset.add(item2); + multiset.add(item3); + + multiset.remove("a string"); + + assertThat(multiset).containsExactly("a string", "different string"); + assertThat(multiset.elementSet()).containsExactly("a string", "different string"); + } + + @Test + public void removingLastObjectRemovesCompletely() { + String item1 = "a string"; + String item2 = "a string"; + String item3 = "different string"; + + CopyOnWriteMultiset multiset = new CopyOnWriteMultiset<>(); + + multiset.add(item1); + multiset.add(item2); + multiset.add(item3); + + multiset.remove("different string"); + + assertThat(multiset).containsExactly("a string", "a string"); + assertThat(multiset.elementSet()).containsExactly("a string"); + } + + @Test + public void removingNonexistentElementSucceeds() { + CopyOnWriteMultiset multiset = new CopyOnWriteMultiset<>(); + + multiset.remove("a string"); + } + + @Test + public void modifyingIteratorFails() { + CopyOnWriteMultiset multiset = new CopyOnWriteMultiset<>(); + multiset.add("a string"); + + Iterator iterator = multiset.iterator(); + + assertThrows(UnsupportedOperationException.class, iterator::remove); + } + + @Test + public void modifyingElementSetFails() { + CopyOnWriteMultiset multiset = new CopyOnWriteMultiset<>(); + multiset.add("a string"); + + Set elementSet = multiset.elementSet(); + + assertThrows(UnsupportedOperationException.class, () -> elementSet.remove("a string")); + } +} diff --git a/library/core/src/test/java/com/google/android/exoplayer2/util/MimeTypesTest.java b/library/common/src/test/java/com/google/android/exoplayer2/util/MimeTypesTest.java similarity index 76% rename from library/core/src/test/java/com/google/android/exoplayer2/util/MimeTypesTest.java rename to library/common/src/test/java/com/google/android/exoplayer2/util/MimeTypesTest.java index 288ad918b2..e88385bbca 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/util/MimeTypesTest.java +++ b/library/common/src/test/java/com/google/android/exoplayer2/util/MimeTypesTest.java @@ -26,7 +26,30 @@ import org.junit.runner.RunWith; public final class MimeTypesTest { @Test - public void testGetMediaMimeType_fromValidCodecs_returnsCorrectMimeType() { + public void isText_returnsCorrectResult() { + assertThat(MimeTypes.isText(MimeTypes.TEXT_VTT)).isTrue(); + assertThat(MimeTypes.isText(MimeTypes.TEXT_SSA)).isTrue(); + assertThat(MimeTypes.isText(MimeTypes.APPLICATION_CEA608)).isTrue(); + assertThat(MimeTypes.isText(MimeTypes.APPLICATION_CEA708)).isTrue(); + assertThat(MimeTypes.isText(MimeTypes.APPLICATION_MP4CEA608)).isTrue(); + assertThat(MimeTypes.isText(MimeTypes.APPLICATION_SUBRIP)).isTrue(); + assertThat(MimeTypes.isText(MimeTypes.APPLICATION_TTML)).isTrue(); + assertThat(MimeTypes.isText(MimeTypes.APPLICATION_TX3G)).isTrue(); + assertThat(MimeTypes.isText(MimeTypes.APPLICATION_MP4VTT)).isTrue(); + assertThat(MimeTypes.isText(MimeTypes.APPLICATION_VOBSUB)).isTrue(); + assertThat(MimeTypes.isText(MimeTypes.APPLICATION_PGS)).isTrue(); + assertThat(MimeTypes.isText(MimeTypes.APPLICATION_DVBSUBS)).isTrue(); + assertThat(MimeTypes.isText("text/custom")).isTrue(); + + assertThat(MimeTypes.isText(MimeTypes.VIDEO_MP4)).isFalse(); + assertThat(MimeTypes.isText(MimeTypes.VIDEO_H264)).isFalse(); + assertThat(MimeTypes.isText(MimeTypes.AUDIO_MP4)).isFalse(); + assertThat(MimeTypes.isText(MimeTypes.AUDIO_AAC)).isFalse(); + assertThat(MimeTypes.isText("application/custom")).isFalse(); + } + + @Test + public void getMediaMimeType_fromValidCodecs_returnsCorrectMimeType() { assertThat(MimeTypes.getMediaMimeType("avc1")).isEqualTo(MimeTypes.VIDEO_H264); assertThat(MimeTypes.getMediaMimeType("avc1.42E01E")).isEqualTo(MimeTypes.VIDEO_H264); assertThat(MimeTypes.getMediaMimeType("avc1.42E01F")).isEqualTo(MimeTypes.VIDEO_H264); @@ -73,10 +96,17 @@ public final class MimeTypesTest { assertThat(MimeTypes.getMediaMimeType("mp4a.AA")).isEqualTo(MimeTypes.AUDIO_DTS_HD); assertThat(MimeTypes.getMediaMimeType("mp4a.AB")).isEqualTo(MimeTypes.AUDIO_DTS_HD); assertThat(MimeTypes.getMediaMimeType("mp4a.AD")).isEqualTo(MimeTypes.AUDIO_OPUS); + + assertThat(MimeTypes.getMediaMimeType("wvtt")).isEqualTo(MimeTypes.TEXT_VTT); + assertThat(MimeTypes.getMediaMimeType("stpp.")).isEqualTo(MimeTypes.APPLICATION_TTML); + assertThat(MimeTypes.getMediaMimeType("stpp.ttml.im1t")).isEqualTo(MimeTypes.APPLICATION_TTML); + assertThat(MimeTypes.getMediaMimeType("eia608.")).isEqualTo(MimeTypes.APPLICATION_CEA608); + assertThat(MimeTypes.getMediaMimeType("cea608")).isEqualTo(MimeTypes.APPLICATION_CEA608); + assertThat(MimeTypes.getMediaMimeType("cea708")).isEqualTo(MimeTypes.APPLICATION_CEA708); } @Test - public void testGetMimeTypeFromMp4ObjectType_forValidObjectType_returnsCorrectMimeType() { + public void getMimeTypeFromMp4ObjectType_forValidObjectType_returnsCorrectMimeType() { assertThat(MimeTypes.getMimeTypeFromMp4ObjectType(0x60)).isEqualTo(MimeTypes.VIDEO_MPEG2); assertThat(MimeTypes.getMimeTypeFromMp4ObjectType(0x61)).isEqualTo(MimeTypes.VIDEO_MPEG2); assertThat(MimeTypes.getMimeTypeFromMp4ObjectType(0x20)).isEqualTo(MimeTypes.VIDEO_MP4V); @@ -97,7 +127,7 @@ public final class MimeTypesTest { } @Test - public void testGetMimeTypeFromMp4ObjectType_forInvalidObjectType_returnsNull() { + public void getMimeTypeFromMp4ObjectType_forInvalidObjectType_returnsNull() { assertThat(MimeTypes.getMimeTypeFromMp4ObjectType(0)).isNull(); assertThat(MimeTypes.getMimeTypeFromMp4ObjectType(0x600)).isNull(); assertThat(MimeTypes.getMimeTypeFromMp4ObjectType(0x01)).isNull(); diff --git a/library/core/src/test/java/com/google/android/exoplayer2/util/NalUnitUtilTest.java b/library/common/src/test/java/com/google/android/exoplayer2/util/NalUnitUtilTest.java similarity index 97% rename from library/core/src/test/java/com/google/android/exoplayer2/util/NalUnitUtilTest.java rename to library/common/src/test/java/com/google/android/exoplayer2/util/NalUnitUtilTest.java index 2cc26feda3..365cff8aff 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/util/NalUnitUtilTest.java +++ b/library/common/src/test/java/com/google/android/exoplayer2/util/NalUnitUtilTest.java @@ -36,7 +36,7 @@ public final class NalUnitUtilTest { private static final int SPS_TEST_DATA_OFFSET = 3; @Test - public void testFindNalUnit() { + public void findNalUnit() { byte[] data = buildTestData(); // Should find NAL unit. @@ -57,7 +57,7 @@ public final class NalUnitUtilTest { } @Test - public void testFindNalUnitWithPrefix() { + public void findNalUnitWithPrefix() { byte[] data = buildTestData(); // First byte of NAL unit in data1, rest in data2. @@ -121,7 +121,7 @@ public final class NalUnitUtilTest { } @Test - public void testParseSpsNalUnit() { + public void parseSpsNalUnit() { NalUnitUtil.SpsData data = NalUnitUtil.parseSpsNalUnit(SPS_TEST_DATA, SPS_TEST_DATA_OFFSET, SPS_TEST_DATA.length); assertThat(data.width).isEqualTo(640); @@ -137,7 +137,7 @@ public final class NalUnitUtilTest { } @Test - public void testUnescapeDoesNotModifyBuffersWithoutStartCodes() { + public void unescapeDoesNotModifyBuffersWithoutStartCodes() { assertUnescapeDoesNotModify(""); assertUnescapeDoesNotModify("0000"); assertUnescapeDoesNotModify("172BF38A3C"); @@ -145,13 +145,13 @@ public final class NalUnitUtilTest { } @Test - public void testUnescapeModifiesBuffersWithStartCodes() { + public void unescapeModifiesBuffersWithStartCodes() { assertUnescapeMatchesExpected("00000301", "000001"); assertUnescapeMatchesExpected("0000030200000300", "000002000000"); } @Test - public void testDiscardToSps() { + public void discardToSps() { assertDiscardToSpsMatchesExpected("", ""); assertDiscardToSpsMatchesExpected("00", ""); assertDiscardToSpsMatchesExpected("FFFF000001", ""); diff --git a/library/core/src/test/java/com/google/android/exoplayer2/util/ParsableBitArrayTest.java b/library/common/src/test/java/com/google/android/exoplayer2/util/ParsableBitArrayTest.java similarity index 82% rename from library/core/src/test/java/com/google/android/exoplayer2/util/ParsableBitArrayTest.java rename to library/common/src/test/java/com/google/android/exoplayer2/util/ParsableBitArrayTest.java index f031468461..9a2d17cbfc 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/util/ParsableBitArrayTest.java +++ b/library/common/src/test/java/com/google/android/exoplayer2/util/ParsableBitArrayTest.java @@ -16,9 +16,12 @@ package com.google.android.exoplayer2.util; import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.testutil.TestUtil; +import java.nio.charset.Charset; import org.junit.Test; import org.junit.runner.RunWith; @@ -27,7 +30,7 @@ import org.junit.runner.RunWith; public final class ParsableBitArrayTest { @Test - public void testReadAllBytes() { + public void readAllBytes() { byte[] testData = TestUtil.createByteArray(0x3C, 0xD2, 0x5F); ParsableBitArray testArray = new ParsableBitArray(testData); byte[] bytesRead = new byte[testData.length]; @@ -40,7 +43,7 @@ public final class ParsableBitArrayTest { } @Test - public void testReadBitInSameByte() { + public void readBitInSameByte() { byte[] testData = TestUtil.createByteArray(0, 0b00110000); ParsableBitArray testArray = new ParsableBitArray(testData); testArray.setPosition(10); @@ -52,7 +55,7 @@ public final class ParsableBitArrayTest { } @Test - public void testReadBitInMultipleBytes() { + public void readBitInMultipleBytes() { byte[] testData = TestUtil.createByteArray(1, 1 << 7); ParsableBitArray testArray = new ParsableBitArray(testData); testArray.setPosition(6); @@ -64,7 +67,7 @@ public final class ParsableBitArrayTest { } @Test - public void testReadBits0Bits() { + public void readBits0Bits() { byte[] testData = TestUtil.createByteArray(0x3C); ParsableBitArray testArray = new ParsableBitArray(testData); @@ -74,7 +77,7 @@ public final class ParsableBitArrayTest { } @Test - public void testReadBitsByteAligned() { + public void readBitsByteAligned() { byte[] testData = TestUtil.createByteArray(0x3C, 0xD2, 0x5F, 0x01); ParsableBitArray testArray = new ParsableBitArray(testData); testArray.readBits(8); @@ -86,7 +89,7 @@ public final class ParsableBitArrayTest { } @Test - public void testReadBitsNonByteAligned() { + public void readBitsNonByteAligned() { byte[] testData = TestUtil.createByteArray(0x3C, 0xD2, 0x5F); ParsableBitArray testArray = new ParsableBitArray(testData); testArray.readBits(3); @@ -98,7 +101,7 @@ public final class ParsableBitArrayTest { } @Test - public void testReadBitsNegativeValue() { + public void readBitsNegativeValue() { byte[] testData = TestUtil.createByteArray(0xF0, 0, 0, 0); ParsableBitArray testArray = new ParsableBitArray(testData); @@ -108,7 +111,7 @@ public final class ParsableBitArrayTest { } @Test - public void testReadBitsToLong0Bits() { + public void readBitsToLong0Bits() { byte[] testData = TestUtil.createByteArray(0x3C); ParsableBitArray testArray = new ParsableBitArray(testData); @@ -118,7 +121,7 @@ public final class ParsableBitArrayTest { } @Test - public void testReadBitsToLongByteAligned() { + public void readBitsToLongByteAligned() { byte[] testData = TestUtil.createByteArray(0x3C, 0xD2, 0x5F, 0x01, 0xFF, 0x14, 0x60); ParsableBitArray testArray = new ParsableBitArray(testData); testArray.readBits(8); @@ -130,7 +133,7 @@ public final class ParsableBitArrayTest { } @Test - public void testReadBitsToLongNonByteAligned() { + public void readBitsToLongNonByteAligned() { byte[] testData = TestUtil.createByteArray(0x3C, 0xD2, 0x5F, 0x01, 0xFF, 0x14, 0x60); ParsableBitArray testArray = new ParsableBitArray(testData); testArray.readBits(3); @@ -142,7 +145,7 @@ public final class ParsableBitArrayTest { } @Test - public void testReadBitsToLongNegativeValue() { + public void readBitsToLongNegativeValue() { byte[] testData = TestUtil.createByteArray(0xF0, 0, 0, 0, 0, 0, 0, 0); ParsableBitArray testArray = new ParsableBitArray(testData); @@ -152,7 +155,7 @@ public final class ParsableBitArrayTest { } @Test - public void testReadBitsToByteArray() { + public void readBitsToByteArray() { byte[] testData = TestUtil.createByteArray(0x3C, 0xD2, 0x5F, 0x01, 0xFF, 0x14, 0x60, 0x99); ParsableBitArray testArray = new ParsableBitArray(testData); @@ -201,7 +204,7 @@ public final class ParsableBitArrayTest { } @Test - public void testSkipBytes() { + public void skipBytes() { byte[] testData = TestUtil.createByteArray(0x3C, 0xD2, 0x5F, 0x01); ParsableBitArray testArray = new ParsableBitArray(testData); @@ -211,7 +214,7 @@ public final class ParsableBitArrayTest { } @Test - public void testSkipBitsByteAligned() { + public void skipBitsByteAligned() { byte[] testData = TestUtil.createByteArray(0x3C, 0xD2, 0x5F, 0x01); ParsableBitArray testArray = new ParsableBitArray(testData); @@ -221,7 +224,7 @@ public final class ParsableBitArrayTest { } @Test - public void testSkipBitsNonByteAligned() { + public void skipBitsNonByteAligned() { byte[] testData = TestUtil.createByteArray(0x3C, 0xD2, 0x5F, 0x01); ParsableBitArray testArray = new ParsableBitArray(testData); @@ -231,7 +234,7 @@ public final class ParsableBitArrayTest { } @Test - public void testSetPositionByteAligned() { + public void setPositionByteAligned() { byte[] testData = TestUtil.createByteArray(0x3C, 0xD2, 0x5F, 0x01); ParsableBitArray testArray = new ParsableBitArray(testData); @@ -241,7 +244,7 @@ public final class ParsableBitArrayTest { } @Test - public void testSetPositionNonByteAligned() { + public void setPositionNonByteAligned() { byte[] testData = TestUtil.createByteArray(0x3C, 0xD2, 0x5F, 0x01); ParsableBitArray testArray = new ParsableBitArray(testData); @@ -251,7 +254,7 @@ public final class ParsableBitArrayTest { } @Test - public void testByteAlignFromNonByteAligned() { + public void byteAlignFromNonByteAligned() { byte[] testData = TestUtil.createByteArray(0x3C, 0xD2, 0x5F, 0x01); ParsableBitArray testArray = new ParsableBitArray(testData); testArray.setPosition(11); @@ -264,7 +267,7 @@ public final class ParsableBitArrayTest { } @Test - public void testByteAlignFromByteAligned() { + public void byteAlignFromByteAligned() { byte[] testData = TestUtil.createByteArray(0x3C, 0xD2, 0x5F, 0x01); ParsableBitArray testArray = new ParsableBitArray(testData); testArray.setPosition(16); @@ -277,7 +280,36 @@ public final class ParsableBitArrayTest { } @Test - public void testPutBitsWithinByte() { + public void readBytesAsStringDefaultsToUtf8() { + byte[] testData = "a non-åscii strìng".getBytes(Charset.forName(C.UTF8_NAME)); + ParsableBitArray testArray = new ParsableBitArray(testData); + + testArray.skipBytes(2); + assertThat(testArray.readBytesAsString(testData.length - 2)).isEqualTo("non-åscii strìng"); + } + + @Test + public void readBytesAsStringExplicitCharset() { + byte[] testData = "a non-åscii strìng".getBytes(Charset.forName(C.UTF16_NAME)); + ParsableBitArray testArray = new ParsableBitArray(testData); + + testArray.skipBytes(6); + assertThat(testArray.readBytesAsString(testData.length - 6, Charset.forName(C.UTF16_NAME))) + .isEqualTo("non-åscii strìng"); + } + + @Test + public void readBytesNotByteAligned() { + String testString = "test string"; + byte[] testData = testString.getBytes(Charset.forName(C.UTF8_NAME)); + ParsableBitArray testArray = new ParsableBitArray(testData); + + testArray.skipBit(); + assertThrows(IllegalStateException.class, () -> testArray.readBytesAsString(2)); + } + + @Test + public void putBitsWithinByte() { ParsableBitArray output = new ParsableBitArray(new byte[4]); output.skipBits(1); @@ -288,7 +320,7 @@ public final class ParsableBitArrayTest { } @Test - public void testPutBitsAcrossTwoBytes() { + public void putBitsAcrossTwoBytes() { ParsableBitArray output = new ParsableBitArray(new byte[4]); output.setPosition(12); @@ -299,7 +331,7 @@ public final class ParsableBitArrayTest { } @Test - public void testPutBitsAcrossMultipleBytes() { + public void putBitsAcrossMultipleBytes() { ParsableBitArray output = new ParsableBitArray(new byte[8]); output.setPosition(31); // Writing starts at 31 to test the 30th bit is not modified. @@ -310,7 +342,7 @@ public final class ParsableBitArrayTest { } @Test - public void testPut32Bits() { + public void put32Bits() { ParsableBitArray output = new ParsableBitArray(new byte[5]); output.setPosition(4); @@ -321,7 +353,7 @@ public final class ParsableBitArrayTest { } @Test - public void testPutFullBytes() { + public void putFullBytes() { ParsableBitArray output = new ParsableBitArray(new byte[2]); output.putInt(0x81, 8); @@ -331,7 +363,7 @@ public final class ParsableBitArrayTest { } @Test - public void testNoOverwriting() { + public void noOverwriting() { ParsableBitArray output = new ParsableBitArray(TestUtil.createByteArray(0xFF, 0xFF, 0xFF, 0xFF, 0xFF)); output.setPosition(1); diff --git a/library/core/src/test/java/com/google/android/exoplayer2/util/ParsableByteArrayTest.java b/library/common/src/test/java/com/google/android/exoplayer2/util/ParsableByteArrayTest.java similarity index 92% rename from library/core/src/test/java/com/google/android/exoplayer2/util/ParsableByteArrayTest.java rename to library/common/src/test/java/com/google/android/exoplayer2/util/ParsableByteArrayTest.java index 7b441b83a1..894de47e6e 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/util/ParsableByteArrayTest.java +++ b/library/common/src/test/java/com/google/android/exoplayer2/util/ParsableByteArrayTest.java @@ -39,7 +39,7 @@ public final class ParsableByteArrayTest { } @Test - public void testReadShort() { + public void readShort() { testReadShort((short) -1); testReadShort((short) 0); testReadShort((short) 1); @@ -65,7 +65,7 @@ public final class ParsableByteArrayTest { } @Test - public void testReadInt() { + public void readInt() { testReadInt(0); testReadInt(1); testReadInt(-1); @@ -91,7 +91,7 @@ public final class ParsableByteArrayTest { } @Test - public void testReadUnsignedInt() { + public void readUnsignedInt() { testReadUnsignedInt(0); testReadUnsignedInt(1); testReadUnsignedInt(Integer.MAX_VALUE); @@ -117,7 +117,7 @@ public final class ParsableByteArrayTest { } @Test - public void testReadUnsignedIntToInt() { + public void readUnsignedIntToInt() { testReadUnsignedIntToInt(0); testReadUnsignedIntToInt(1); testReadUnsignedIntToInt(Integer.MAX_VALUE); @@ -153,7 +153,7 @@ public final class ParsableByteArrayTest { } @Test - public void testReadUnsignedLongToLong() { + public void readUnsignedLongToLong() { testReadUnsignedLongToLong(0); testReadUnsignedLongToLong(1); testReadUnsignedLongToLong(Long.MAX_VALUE); @@ -189,7 +189,7 @@ public final class ParsableByteArrayTest { } @Test - public void testReadLong() { + public void readLong() { testReadLong(0); testReadLong(1); testReadLong(-1); @@ -215,7 +215,7 @@ public final class ParsableByteArrayTest { } @Test - public void testReadingMovesPosition() { + public void readingMovesPosition() { ParsableByteArray parsableByteArray = getTestDataArray(); // Given an array at the start @@ -226,7 +226,7 @@ public final class ParsableByteArrayTest { } @Test - public void testOutOfBoundsThrows() { + public void outOfBoundsThrows() { ParsableByteArray parsableByteArray = getTestDataArray(); // Given an array at the end @@ -242,7 +242,7 @@ public final class ParsableByteArrayTest { } @Test - public void testModificationsAffectParsableArray() { + public void modificationsAffectParsableArray() { ParsableByteArray parsableByteArray = getTestDataArray(); // When modifying the wrapped byte array @@ -255,7 +255,7 @@ public final class ParsableByteArrayTest { } @Test - public void testReadingUnsignedLongWithMsbSetThrows() { + public void readingUnsignedLongWithMsbSetThrows() { ParsableByteArray parsableByteArray = getTestDataArray(); // Given an array with the most-significant bit set on the top byte @@ -271,7 +271,7 @@ public final class ParsableByteArrayTest { } @Test - public void testReadUnsignedFixedPoint1616() { + public void readUnsignedFixedPoint1616() { ParsableByteArray parsableByteArray = getTestDataArray(); // When reading the integer part of a 16.16 fixed point value @@ -282,7 +282,7 @@ public final class ParsableByteArrayTest { } @Test - public void testReadingBytesReturnsCopy() { + public void readingBytesReturnsCopy() { ParsableByteArray parsableByteArray = getTestDataArray(); // When reading all the bytes back @@ -295,7 +295,7 @@ public final class ParsableByteArrayTest { } @Test - public void testReadLittleEndianLong() { + public void readLittleEndianLong() { ParsableByteArray byteArray = new ParsableByteArray(new byte[] { 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, (byte) 0xFF @@ -305,7 +305,7 @@ public final class ParsableByteArrayTest { } @Test - public void testReadLittleEndianUnsignedInt() { + public void readLittleEndianUnsignedInt() { ParsableByteArray byteArray = new ParsableByteArray(new byte[] { 0x10, 0x00, 0x00, (byte) 0xFF }); @@ -314,7 +314,7 @@ public final class ParsableByteArrayTest { } @Test - public void testReadLittleEndianInt() { + public void readLittleEndianInt() { ParsableByteArray byteArray = new ParsableByteArray(new byte[] { 0x01, 0x00, 0x00, (byte) 0xFF }); @@ -323,7 +323,7 @@ public final class ParsableByteArrayTest { } @Test - public void testReadLittleEndianUnsignedInt24() { + public void readLittleEndianUnsignedInt24() { byte[] data = {0x01, 0x02, (byte) 0xFF}; ParsableByteArray byteArray = new ParsableByteArray(data); assertThat(byteArray.readLittleEndianUnsignedInt24()).isEqualTo(0xFF0201); @@ -331,7 +331,7 @@ public final class ParsableByteArrayTest { } @Test - public void testReadInt24Positive() { + public void readInt24Positive() { byte[] data = {0x01, 0x02, (byte) 0xFF}; ParsableByteArray byteArray = new ParsableByteArray(data); assertThat(byteArray.readInt24()).isEqualTo(0x0102FF); @@ -339,7 +339,7 @@ public final class ParsableByteArrayTest { } @Test - public void testReadInt24Negative() { + public void readInt24Negative() { byte[] data = {(byte) 0xFF, 0x02, (byte) 0x01}; ParsableByteArray byteArray = new ParsableByteArray(data); assertThat(byteArray.readInt24()).isEqualTo(0xFFFF0201); @@ -347,7 +347,7 @@ public final class ParsableByteArrayTest { } @Test - public void testReadLittleEndianUnsignedShort() { + public void readLittleEndianUnsignedShort() { ParsableByteArray byteArray = new ParsableByteArray(new byte[] { 0x01, (byte) 0xFF, 0x02, (byte) 0xFF }); @@ -358,7 +358,7 @@ public final class ParsableByteArrayTest { } @Test - public void testReadLittleEndianShort() { + public void readLittleEndianShort() { ParsableByteArray byteArray = new ParsableByteArray(new byte[] { 0x01, (byte) 0xFF, 0x02, (byte) 0xFF }); @@ -369,7 +369,7 @@ public final class ParsableByteArrayTest { } @Test - public void testReadString() { + public void readString() { byte[] data = { (byte) 0xC3, (byte) 0xA4, (byte) 0x20, (byte) 0xC3, (byte) 0xB6, (byte) 0x20, @@ -385,7 +385,7 @@ public final class ParsableByteArrayTest { } @Test - public void testReadAsciiString() { + public void readAsciiString() { byte[] data = new byte[] {'t', 'e', 's', 't'}; ParsableByteArray testArray = new ParsableByteArray(data); assertThat(testArray.readString(data.length, forName("US-ASCII"))).isEqualTo("test"); @@ -393,7 +393,7 @@ public final class ParsableByteArrayTest { } @Test - public void testReadStringOutOfBoundsDoesNotMovePosition() { + public void readStringOutOfBoundsDoesNotMovePosition() { byte[] data = { (byte) 0xC3, (byte) 0xA4, (byte) 0x20 }; @@ -407,14 +407,14 @@ public final class ParsableByteArrayTest { } @Test - public void testReadEmptyString() { + public void readEmptyString() { byte[] bytes = new byte[0]; ParsableByteArray parser = new ParsableByteArray(bytes); assertThat(parser.readLine()).isNull(); } @Test - public void testReadNullTerminatedStringWithLengths() { + public void readNullTerminatedStringWithLengths() { byte[] bytes = new byte[] { 'f', 'o', 'o', 0, 'b', 'a', 'r', 0 }; @@ -449,7 +449,7 @@ public final class ParsableByteArrayTest { } @Test - public void testReadNullTerminatedString() { + public void readNullTerminatedString() { byte[] bytes = new byte[] { 'f', 'o', 'o', 0, 'b', 'a', 'r', 0 }; @@ -473,7 +473,7 @@ public final class ParsableByteArrayTest { } @Test - public void testReadNullTerminatedStringWithoutEndingNull() { + public void readNullTerminatedStringWithoutEndingNull() { byte[] bytes = new byte[] { 'f', 'o', 'o', 0, 'b', 'a', 'r' }; @@ -484,7 +484,7 @@ public final class ParsableByteArrayTest { } @Test - public void testReadSingleLineWithoutEndingTrail() { + public void readSingleLineWithoutEndingTrail() { byte[] bytes = new byte[] { 'f', 'o', 'o' }; @@ -494,7 +494,7 @@ public final class ParsableByteArrayTest { } @Test - public void testReadSingleLineWithEndingLf() { + public void readSingleLineWithEndingLf() { byte[] bytes = new byte[] { 'f', 'o', 'o', '\n' }; @@ -504,7 +504,7 @@ public final class ParsableByteArrayTest { } @Test - public void testReadTwoLinesWithCrFollowedByLf() { + public void readTwoLinesWithCrFollowedByLf() { byte[] bytes = new byte[] { 'f', 'o', 'o', '\r', '\n', 'b', 'a', 'r' }; @@ -515,7 +515,7 @@ public final class ParsableByteArrayTest { } @Test - public void testReadThreeLinesWithEmptyLine() { + public void readThreeLinesWithEmptyLine() { byte[] bytes = new byte[] { 'f', 'o', 'o', '\r', '\n', '\r', 'b', 'a', 'r' }; @@ -527,7 +527,7 @@ public final class ParsableByteArrayTest { } @Test - public void testReadFourLinesWithLfFollowedByCr() { + public void readFourLinesWithLfFollowedByCr() { byte[] bytes = new byte[] { 'f', 'o', 'o', '\n', '\r', '\r', 'b', 'a', 'r', '\r', '\n' }; diff --git a/library/core/src/test/java/com/google/android/exoplayer2/util/ParsableNalUnitBitArrayTest.java b/library/common/src/test/java/com/google/android/exoplayer2/util/ParsableNalUnitBitArrayTest.java similarity index 94% rename from library/core/src/test/java/com/google/android/exoplayer2/util/ParsableNalUnitBitArrayTest.java rename to library/common/src/test/java/com/google/android/exoplayer2/util/ParsableNalUnitBitArrayTest.java index 3940c3d2a1..8fffb9a5d4 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/util/ParsableNalUnitBitArrayTest.java +++ b/library/common/src/test/java/com/google/android/exoplayer2/util/ParsableNalUnitBitArrayTest.java @@ -32,7 +32,7 @@ public final class ParsableNalUnitBitArrayTest { private static final byte[] MIX_TEST_DATA = createByteArray(255, 0, 0, 3, 255, 0, 0, 127); @Test - public void testReadNoEscaping() { + public void readNoEscaping() { ParsableNalUnitBitArray array = new ParsableNalUnitBitArray(NO_ESCAPING_TEST_DATA, 0, NO_ESCAPING_TEST_DATA.length); assertThat(array.readBits(24)).isEqualTo(0x000300); @@ -44,7 +44,7 @@ public final class ParsableNalUnitBitArrayTest { } @Test - public void testReadNoEscapingTruncated() { + public void readNoEscapingTruncated() { ParsableNalUnitBitArray array = new ParsableNalUnitBitArray(NO_ESCAPING_TEST_DATA, 0, 4); assertThat(array.canReadBits(32)).isTrue(); array.skipBits(32); @@ -58,7 +58,7 @@ public final class ParsableNalUnitBitArrayTest { } @Test - public void testReadAllEscaping() { + public void readAllEscaping() { ParsableNalUnitBitArray array = new ParsableNalUnitBitArray(ALL_ESCAPING_TEST_DATA, 0, ALL_ESCAPING_TEST_DATA.length); assertThat(array.canReadBits(48)).isTrue(); @@ -70,7 +70,7 @@ public final class ParsableNalUnitBitArrayTest { } @Test - public void testReadMix() { + public void readMix() { ParsableNalUnitBitArray array = new ParsableNalUnitBitArray(MIX_TEST_DATA, 0, MIX_TEST_DATA.length); assertThat(array.canReadBits(56)).isTrue(); @@ -84,7 +84,7 @@ public final class ParsableNalUnitBitArrayTest { } @Test - public void testReadExpGolomb() { + public void readExpGolomb() { ParsableNalUnitBitArray array = new ParsableNalUnitBitArray(createByteArray(0x9E), 0, 1); assertThat(array.canReadExpGolombCodedNum()).isTrue(); assertThat(array.readUnsignedExpGolombCodedInt()).isEqualTo(0); @@ -100,7 +100,7 @@ public final class ParsableNalUnitBitArrayTest { } @Test - public void testReadExpGolombWithEscaping() { + public void readExpGolombWithEscaping() { ParsableNalUnitBitArray array = new ParsableNalUnitBitArray(createByteArray(0, 0, 3, 128, 0), 0, 5); assertThat(array.canReadExpGolombCodedNum()).isFalse(); @@ -111,7 +111,7 @@ public final class ParsableNalUnitBitArrayTest { } @Test - public void testReset() { + public void reset() { ParsableNalUnitBitArray array = new ParsableNalUnitBitArray(createByteArray(0, 0), 0, 2); assertThat(array.canReadExpGolombCodedNum()).isFalse(); assertThat(array.canReadBits(16)).isTrue(); diff --git a/library/common/src/test/java/com/google/android/exoplayer2/util/UtilTest.java b/library/common/src/test/java/com/google/android/exoplayer2/util/UtilTest.java new file mode 100644 index 0000000000..2e523a32c6 --- /dev/null +++ b/library/common/src/test/java/com/google/android/exoplayer2/util/UtilTest.java @@ -0,0 +1,995 @@ +/* + * 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.util; + +import static com.google.android.exoplayer2.util.Util.binarySearchCeil; +import static com.google.android.exoplayer2.util.Util.binarySearchFloor; +import static com.google.android.exoplayer2.util.Util.escapeFileName; +import static com.google.android.exoplayer2.util.Util.getCodecsOfType; +import static com.google.android.exoplayer2.util.Util.parseXsDateTime; +import static com.google.android.exoplayer2.util.Util.parseXsDuration; +import static com.google.android.exoplayer2.util.Util.unescapeFileName; +import static com.google.common.truth.Truth.assertThat; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.testutil.TestUtil; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Random; +import java.util.zip.Deflater; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.annotation.Config; + +/** Unit tests for {@link Util}. */ +@RunWith(AndroidJUnit4.class) +public class UtilTest { + + @Test + public void addWithOverflowDefault_withoutOverFlow_returnsSum() { + long res = Util.addWithOverflowDefault(5, 10, /* overflowResult= */ 0); + assertThat(res).isEqualTo(15); + + res = Util.addWithOverflowDefault(Long.MAX_VALUE - 1, 1, /* overflowResult= */ 12345); + assertThat(res).isEqualTo(Long.MAX_VALUE); + + res = Util.addWithOverflowDefault(Long.MIN_VALUE + 1, -1, /* overflowResult= */ 12345); + assertThat(res).isEqualTo(Long.MIN_VALUE); + } + + @Test + public void addWithOverflowDefault_withOverFlow_returnsOverflowDefault() { + long res = Util.addWithOverflowDefault(Long.MAX_VALUE, 1, /* overflowResult= */ 12345); + assertThat(res).isEqualTo(12345); + + res = Util.addWithOverflowDefault(Long.MIN_VALUE, -1, /* overflowResult= */ 12345); + assertThat(res).isEqualTo(12345); + } + + @Test + public void subtrackWithOverflowDefault_withoutUnderflow_returnsSubtract() { + long res = Util.subtractWithOverflowDefault(5, 10, /* overflowResult= */ 0); + assertThat(res).isEqualTo(-5); + + res = Util.subtractWithOverflowDefault(Long.MIN_VALUE + 1, 1, /* overflowResult= */ 12345); + assertThat(res).isEqualTo(Long.MIN_VALUE); + + res = Util.subtractWithOverflowDefault(Long.MAX_VALUE - 1, -1, /* overflowResult= */ 12345); + assertThat(res).isEqualTo(Long.MAX_VALUE); + } + + @Test + public void subtrackWithOverflowDefault_withUnderflow_returnsOverflowDefault() { + long res = Util.subtractWithOverflowDefault(Long.MIN_VALUE, 1, /* overflowResult= */ 12345); + assertThat(res).isEqualTo(12345); + + res = Util.subtractWithOverflowDefault(Long.MAX_VALUE, -1, /* overflowResult= */ 12345); + assertThat(res).isEqualTo(12345); + } + + @Test + public void inferContentType_returnsInferredResult() { + assertThat(Util.inferContentType("http://a.b/c.ism")).isEqualTo(C.TYPE_SS); + assertThat(Util.inferContentType("http://a.b/c.isml")).isEqualTo(C.TYPE_SS); + assertThat(Util.inferContentType("http://a.b/c.ism/Manifest")).isEqualTo(C.TYPE_SS); + assertThat(Util.inferContentType("http://a.b/c.isml/manifest")).isEqualTo(C.TYPE_SS); + assertThat(Util.inferContentType("http://a.b/c.isml/manifest(filter=x)")).isEqualTo(C.TYPE_SS); + + assertThat(Util.inferContentType("http://a.b/c.ism/prefix-manifest")).isEqualTo(C.TYPE_OTHER); + assertThat(Util.inferContentType("http://a.b/c.ism/manifest-suffix")).isEqualTo(C.TYPE_OTHER); + } + + @Test + public void arrayBinarySearchFloor_emptyArrayAndStayInBoundsFalse_returnsMinus1() { + assertThat( + binarySearchFloor( + new int[0], /* value= */ 0, /* inclusive= */ false, /* stayInBounds= */ false)) + .isEqualTo(-1); + } + + @Test + public void arrayBinarySearchFloor_emptyArrayAndStayInBoundsTrue_returns0() { + assertThat( + binarySearchFloor( + new int[0], /* value= */ 0, /* inclusive= */ false, /* stayInBounds= */ true)) + .isEqualTo(0); + } + + @Test + public void arrayBinarySearchFloor_targetSmallerThanValuesAndStayInBoundsFalse_returnsMinus1() { + assertThat( + binarySearchFloor( + new int[] {1, 3, 5}, + /* value= */ 0, + /* inclusive= */ false, + /* stayInBounds= */ false)) + .isEqualTo(-1); + } + + @Test + public void arrayBinarySearchFloor_targetSmallerThanValuesAndStayInBoundsTrue_returns0() { + assertThat( + binarySearchFloor( + new int[] {1, 3, 5}, + /* value= */ 0, + /* inclusive= */ false, + /* stayInBounds= */ true)) + .isEqualTo(0); + } + + @Test + public void arrayBinarySearchFloor_targetBiggerThanValues_returnsLastIndex() { + assertThat( + binarySearchFloor( + new int[] {1, 3, 5}, + /* value= */ 6, + /* inclusive= */ false, + /* stayInBounds= */ false)) + .isEqualTo(2); + } + + @Test + public void + arrayBinarySearchFloor_targetEqualToFirstValueAndInclusiveFalseAndStayInBoundsFalse_returnsMinus1() { + assertThat( + binarySearchFloor( + new int[] {1, 1, 1, 1, 1, 3, 5}, + /* value= */ 1, + /* inclusive= */ false, + /* stayInBounds= */ false)) + .isEqualTo(-1); + } + + @Test + public void + arrayBinarySearchFloor_targetEqualToFirstValueAndInclusiveFalseAndStayInBoundsTrue_returns0() { + assertThat( + binarySearchFloor( + new int[] {1, 1, 1, 1, 1, 3, 5}, + /* value= */ 1, + /* inclusive= */ false, + /* stayInBounds= */ true)) + .isEqualTo(0); + } + + @Test + public void + arrayBinarySearchFloor_targetInArrayAndInclusiveTrue_returnsFirstIndexWithValueEqualToTarget() { + assertThat( + binarySearchFloor( + new int[] {1, 1, 1, 1, 1, 3, 5}, + /* value= */ 1, + /* inclusive= */ true, + /* stayInBounds= */ false)) + .isEqualTo(0); + } + + @Test + public void + arrayBinarySearchFloor_targetBetweenValuesAndInclusiveFalse_returnsIndexWhereTargetShouldBeInserted() { + assertThat( + binarySearchFloor( + new int[] {1, 1, 1, 1, 1, 3, 5}, + /* value= */ 2, + /* inclusive= */ false, + /* stayInBounds= */ false)) + .isEqualTo(4); + } + + @Test + public void + arrayBinarySearchFloor_targetBetweenValuesAndInclusiveTrue_returnsIndexWhereTargetShouldBeInserted() { + assertThat( + binarySearchFloor( + new int[] {1, 1, 1, 1, 1, 3, 5}, + /* value= */ 2, + /* inclusive= */ true, + /* stayInBounds= */ false)) + .isEqualTo(4); + } + + @Test + public void longArrayBinarySearchFloor_emptyArrayAndStayInBoundsFalse_returnsMinus1() { + assertThat( + binarySearchFloor( + new LongArray(), /* value= */ 0, /* inclusive= */ false, /* stayInBounds= */ false)) + .isEqualTo(-1); + } + + @Test + public void longArrayBinarySearchFloor_emptyArrayAndStayInBoundsTrue_returns0() { + assertThat( + binarySearchFloor( + new LongArray(), /* value= */ 0, /* inclusive= */ false, /* stayInBounds= */ true)) + .isEqualTo(0); + } + + @Test + public void + longArrayBinarySearchFloor_targetSmallerThanValuesAndStayInBoundsFalse_returnsMinus1() { + assertThat( + binarySearchFloor( + newLongArray(1, 3, 5), + /* value= */ 0, + /* inclusive= */ false, + /* stayInBounds= */ false)) + .isEqualTo(-1); + } + + @Test + public void longArrayBinarySearchFloor_targetSmallerThanValuesAndStayInBoundsTrue_returns0() { + assertThat( + binarySearchFloor( + newLongArray(1, 3, 5), + /* value= */ 0, + /* inclusive= */ false, + /* stayInBounds= */ true)) + .isEqualTo(0); + } + + @Test + public void longArrayBinarySearchFloor_targetBiggerThanValues_returnsLastIndex() { + assertThat( + binarySearchFloor( + newLongArray(1, 3, 5), + /* value= */ 6, + /* inclusive= */ false, + /* stayInBounds= */ false)) + .isEqualTo(2); + } + + @Test + public void + longArrayBinarySearchFloor_targetEqualToFirstValueAndInclusiveFalseAndStayInBoundsFalse_returnsMinus1() { + assertThat( + binarySearchFloor( + newLongArray(1, 1, 1, 1, 1, 3, 5), + /* value= */ 1, + /* inclusive= */ false, + /* stayInBounds= */ false)) + .isEqualTo(-1); + } + + @Test + public void + longArrayBinarySearchFloor_targetEqualToFirstValueAndInclusiveFalseAndStayInBoundsTrue_returns0() { + assertThat( + binarySearchFloor( + newLongArray(1, 1, 1, 1, 1, 3, 5), + /* value= */ 1, + /* inclusive= */ false, + /* stayInBounds= */ true)) + .isEqualTo(0); + } + + @Test + public void + longArrayBinarySearchFloor_targetInArrayAndInclusiveTrue_returnsFirstIndexWithValueEqualToTarget() { + assertThat( + binarySearchFloor( + newLongArray(1, 1, 1, 1, 1, 3, 5), + /* value= */ 1, + /* inclusive= */ true, + /* stayInBounds= */ false)) + .isEqualTo(0); + } + + @Test + public void + longArrayBinarySearchFloor_targetBetweenValuesAndInclusiveFalse_returnsIndexWhereTargetShouldBeInserted() { + assertThat( + binarySearchFloor( + newLongArray(1, 1, 1, 1, 1, 3, 5), + /* value= */ 2, + /* inclusive= */ false, + /* stayInBounds= */ false)) + .isEqualTo(4); + } + + @Test + public void + longArrayBinarySearchFloor_targetBetweenValuesAndInclusiveTrue_returnsIndexWhereTargetShouldBeInserted() { + assertThat( + binarySearchFloor( + newLongArray(1, 1, 1, 1, 1, 3, 5), + /* value= */ 2, + /* inclusive= */ true, + /* stayInBounds= */ false)) + .isEqualTo(4); + } + + @Test + public void listBinarySearchFloor_emptyListAndStayInBoundsFalse_returnsMinus1() { + assertThat( + binarySearchFloor( + new ArrayList<>(), + /* value= */ 0, + /* inclusive= */ false, + /* stayInBounds= */ false)) + .isEqualTo(-1); + } + + @Test + public void listBinarySearchFloor_emptyListAndStayInBoundsTrue_returns0() { + assertThat( + binarySearchFloor( + new ArrayList<>(), + /* value= */ 0, + /* inclusive= */ false, + /* stayInBounds= */ true)) + .isEqualTo(0); + } + + @Test + public void listBinarySearchFloor_targetSmallerThanValuesAndStayInBoundsFalse_returnsMinus1() { + assertThat( + binarySearchFloor( + Arrays.asList(1, 3, 5), + /* value= */ 0, + /* inclusive= */ false, + /* stayInBounds= */ false)) + .isEqualTo(-1); + } + + @Test + public void listBinarySearchFloor_targetSmallerThanValuesAndStayInBoundsTrue_returns0() { + assertThat( + binarySearchFloor( + Arrays.asList(1, 3, 5), + /* value= */ 0, + /* inclusive= */ false, + /* stayInBounds= */ true)) + .isEqualTo(0); + } + + @Test + public void listBinarySearchFloor_targetBiggerThanValues_returnsLastIndex() { + assertThat( + binarySearchFloor( + Arrays.asList(1, 3, 5), + /* value= */ 6, + /* inclusive= */ false, + /* stayInBounds= */ false)) + .isEqualTo(2); + } + + @Test + public void + listBinarySearchFloor_targetEqualToFirstValueAndInclusiveFalseAndStayInBoundsFalse_returnsMinus1() { + assertThat( + binarySearchFloor( + Arrays.asList(1, 1, 1, 1, 1, 3, 5), + /* value= */ 1, + /* inclusive= */ false, + /* stayInBounds= */ false)) + .isEqualTo(-1); + } + + @Test + public void + listBinarySearchFloor_targetEqualToFirstValueAndInclusiveFalseAndStayInBoundsTrue_returns0() { + assertThat( + binarySearchFloor( + Arrays.asList(1, 1, 1, 1, 1, 3, 5), + /* value= */ 1, + /* inclusive= */ false, + /* stayInBounds= */ true)) + .isEqualTo(0); + } + + @Test + public void + listBinarySearchFloor_targetInListAndInclusiveTrue_returnsFirstIndexWithValueEqualToTarget() { + assertThat( + binarySearchFloor( + Arrays.asList(1, 1, 1, 1, 1, 3, 5), + /* value= */ 1, + /* inclusive= */ true, + /* stayInBounds= */ false)) + .isEqualTo(0); + } + + @Test + public void + listBinarySearchFloor_targetBetweenValuesAndInclusiveFalse_returnsIndexWhereTargetShouldBeInserted() { + assertThat( + binarySearchFloor( + Arrays.asList(1, 1, 1, 1, 1, 3, 5), + /* value= */ 2, + /* inclusive= */ false, + /* stayInBounds= */ false)) + .isEqualTo(4); + } + + @Test + public void + listBinarySearchFloor_targetBetweenValuesAndInclusiveTrue_returnsIndexWhereTargetShouldBeInserted() { + assertThat( + binarySearchFloor( + Arrays.asList(1, 1, 1, 1, 1, 3, 5), + /* value= */ 2, + /* inclusive= */ true, + /* stayInBounds= */ false)) + .isEqualTo(4); + } + + @Test + public void arrayBinarySearchCeil_emptyArrayAndStayInBoundsFalse_returns0() { + assertThat( + binarySearchCeil( + new int[0], /* value= */ 0, /* inclusive= */ false, /* stayInBounds= */ false)) + .isEqualTo(0); + } + + @Test + public void arrayBinarySearchCeil_emptyArrayAndStayInBoundsTrue_returnsMinus1() { + assertThat( + binarySearchCeil( + new int[0], /* value= */ 0, /* inclusive= */ false, /* stayInBounds= */ true)) + .isEqualTo(-1); + } + + @Test + public void arrayBinarySearchCeil_targetSmallerThanValues_returns0() { + assertThat( + binarySearchCeil( + new int[] {1, 3, 5}, + /* value= */ 0, + /* inclusive= */ false, + /* stayInBounds= */ false)) + .isEqualTo(0); + } + + @Test + public void arrayBinarySearchCeil_targetBiggerThanValuesAndStayInBoundsFalse_returnsLength() { + assertThat( + binarySearchCeil( + new int[] {1, 3, 5}, + /* value= */ 6, + /* inclusive= */ false, + /* stayInBounds= */ false)) + .isEqualTo(3); + } + + @Test + public void arrayBinarySearchCeil_targetBiggerThanValuesAndStayInBoundsTrue_returnsLastIndex() { + assertThat( + binarySearchCeil( + new int[] {1, 3, 5}, + /* value= */ 6, + /* inclusive= */ false, + /* stayInBounds= */ true)) + .isEqualTo(2); + } + + @Test + public void + arrayBinarySearchCeil_targetEqualToLastValueAndInclusiveFalseAndStayInBoundsFalse_returnsLength() { + assertThat( + binarySearchCeil( + new int[] {1, 3, 5, 5, 5, 5, 5}, + /* value= */ 5, + /* inclusive= */ false, + /* stayInBounds= */ false)) + .isEqualTo(7); + } + + @Test + public void + arrayBinarySearchCeil_targetEqualToLastValueAndInclusiveFalseAndStayInBoundsTrue_returnsLastIndex() { + assertThat( + binarySearchCeil( + new int[] {1, 3, 5, 5, 5, 5, 5}, + /* value= */ 5, + /* inclusive= */ false, + /* stayInBounds= */ true)) + .isEqualTo(6); + } + + @Test + public void + arrayBinarySearchCeil_targetInArrayAndInclusiveTrue_returnsLastIndexWithValueEqualToTarget() { + assertThat( + binarySearchCeil( + new int[] {1, 3, 5, 5, 5, 5, 5}, + /* value= */ 5, + /* inclusive= */ true, + /* stayInBounds= */ false)) + .isEqualTo(6); + } + + @Test + public void + arrayBinarySearchCeil_targetBetweenValuesAndInclusiveFalse_returnsIndexWhereTargetShouldBeInserted() { + assertThat( + binarySearchCeil( + new int[] {1, 3, 5, 5, 5, 5, 5}, + /* value= */ 4, + /* inclusive= */ false, + /* stayInBounds= */ false)) + .isEqualTo(2); + } + + @Test + public void + arrayBinarySearchCeil_targetBetweenValuesAndInclusiveTrue_returnsIndexWhereTargetShouldBeInserted() { + assertThat( + binarySearchCeil( + new int[] {1, 3, 5, 5, 5, 5, 5}, + /* value= */ 4, + /* inclusive= */ true, + /* stayInBounds= */ false)) + .isEqualTo(2); + } + + @Test + public void listBinarySearchCeil_emptyListAndStayInBoundsFalse_returns0() { + assertThat( + binarySearchCeil( + new ArrayList<>(), + /* value= */ 0, + /* inclusive= */ false, + /* stayInBounds= */ false)) + .isEqualTo(0); + } + + @Test + public void listBinarySearchCeil_emptyListAndStayInBoundsTrue_returnsMinus1() { + assertThat( + binarySearchCeil( + new ArrayList<>(), + /* value= */ 0, + /* inclusive= */ false, + /* stayInBounds= */ true)) + .isEqualTo(-1); + } + + @Test + public void listBinarySearchCeil_targetSmallerThanValues_returns0() { + assertThat( + binarySearchCeil( + Arrays.asList(1, 3, 5), + /* value= */ 0, + /* inclusive= */ false, + /* stayInBounds= */ false)) + .isEqualTo(0); + } + + @Test + public void listBinarySearchCeil_targetBiggerThanValuesAndStayInBoundsFalse_returnsLength() { + assertThat( + binarySearchCeil( + Arrays.asList(1, 3, 5), + /* value= */ 6, + /* inclusive= */ false, + /* stayInBounds= */ false)) + .isEqualTo(3); + } + + @Test + public void listBinarySearchCeil_targetBiggerThanValuesAndStayInBoundsTrue_returnsLastIndex() { + assertThat( + binarySearchCeil( + Arrays.asList(1, 3, 5), + /* value= */ 6, + /* inclusive= */ false, + /* stayInBounds= */ true)) + .isEqualTo(2); + } + + @Test + public void + listBinarySearchCeil_targetEqualToLastValueAndInclusiveFalseAndStayInBoundsFalse_returnsLength() { + assertThat( + binarySearchCeil( + Arrays.asList(1, 3, 5, 5, 5, 5, 5), + /* value= */ 5, + /* inclusive= */ false, + /* stayInBounds= */ false)) + .isEqualTo(7); + } + + @Test + public void + listBinarySearchCeil_targetEqualToLastValueAndInclusiveFalseAndStayInBoundsTrue_returnsLastIndex() { + assertThat( + binarySearchCeil( + Arrays.asList(1, 3, 5, 5, 5, 5, 5), + /* value= */ 5, + /* inclusive= */ false, + /* stayInBounds= */ true)) + .isEqualTo(6); + } + + @Test + public void + listBinarySearchCeil_targetInListAndInclusiveTrue_returnsLastIndexWithValueEqualToTarget() { + assertThat( + binarySearchCeil( + Arrays.asList(1, 3, 5, 5, 5, 5, 5), + /* value= */ 5, + /* inclusive= */ true, + /* stayInBounds= */ false)) + .isEqualTo(6); + } + + @Test + public void + listBinarySearchCeil_targetBetweenValuesAndInclusiveFalse_returnsIndexWhereTargetShouldBeInserted() { + assertThat( + binarySearchCeil( + Arrays.asList(1, 3, 5, 5, 5, 5, 5), + /* value= */ 4, + /* inclusive= */ false, + /* stayInBounds= */ false)) + .isEqualTo(2); + } + + @Test + public void + listBinarySearchCeil_targetBetweenValuesAndInclusiveTrue_returnsIndexWhereTargetShouldBeInserted() { + assertThat( + binarySearchCeil( + Arrays.asList(1, 3, 5, 5, 5, 5, 5), + /* value= */ 4, + /* inclusive= */ true, + /* stayInBounds= */ false)) + .isEqualTo(2); + } + + @Test + public void parseXsDuration_returnsParsedDurationInMillis() { + assertThat(parseXsDuration("PT150.279S")).isEqualTo(150279L); + assertThat(parseXsDuration("PT1.500S")).isEqualTo(1500L); + } + + @Test + public void parseXsDateTime_returnsParsedDateTimeInMillis() throws Exception { + assertThat(parseXsDateTime("2014-06-19T23:07:42")).isEqualTo(1403219262000L); + assertThat(parseXsDateTime("2014-08-06T11:00:00Z")).isEqualTo(1407322800000L); + assertThat(parseXsDateTime("2014-08-06T11:00:00,000Z")).isEqualTo(1407322800000L); + assertThat(parseXsDateTime("2014-09-19T13:18:55-08:00")).isEqualTo(1411161535000L); + assertThat(parseXsDateTime("2014-09-19T13:18:55-0800")).isEqualTo(1411161535000L); + assertThat(parseXsDateTime("2014-09-19T13:18:55.000-0800")).isEqualTo(1411161535000L); + assertThat(parseXsDateTime("2014-09-19T13:18:55.000-800")).isEqualTo(1411161535000L); + } + + @Test + public void toUnsignedLong_withPositiveValue_returnsValue() { + int x = 0x05D67F23; + + long result = Util.toUnsignedLong(x); + + assertThat(result).isEqualTo(0x05D67F23L); + } + + @Test + public void toUnsignedLong_withNegativeValue_returnsValue() { + int x = 0xF5D67F23; + + long result = Util.toUnsignedLong(x); + + assertThat(result).isEqualTo(0xF5D67F23L); + } + + @Test + public void toLong_withZeroValue_returnsZero() { + assertThat(Util.toLong(0, 0)).isEqualTo(0); + } + + @Test + public void toLong_withLongValue_returnsValue() { + assertThat(Util.toLong(1, -4)).isEqualTo(0x1FFFFFFFCL); + } + + @Test + public void toLong_withBigValue_returnsValue() { + assertThat(Util.toLong(0x7ABCDEF, 0x12345678)).isEqualTo(0x7ABCDEF_12345678L); + } + + @Test + public void toLong_withMaxValue_returnsValue() { + assertThat(Util.toLong(0x0FFFFFFF, 0xFFFFFFFF)).isEqualTo(0x0FFFFFFF_FFFFFFFFL); + } + + @Test + public void toLong_withBigNegativeValue_returnsValue() { + assertThat(Util.toLong(0xFEDCBA, 0x87654321)).isEqualTo(0xFEDCBA_87654321L); + } + + @Test + public void toHexString_returnsHexString() { + byte[] bytes = TestUtil.createByteArray(0x12, 0xFC, 0x06); + + assertThat(Util.toHexString(bytes)).isEqualTo("12fc06"); + } + + @Test + public void getCodecsOfType_withNull_returnsNull() { + assertThat(getCodecsOfType(null, C.TRACK_TYPE_VIDEO)).isNull(); + } + + @Test + public void getCodecsOfType_withInvalidTrackType_returnsNull() { + assertThat(getCodecsOfType("avc1.64001e,vp9.63.1", C.TRACK_TYPE_AUDIO)).isNull(); + } + + @Test + public void getCodecsOfType_withAudioTrack_returnsCodec() { + assertThat(getCodecsOfType(" vp9.63.1, ec-3 ", C.TRACK_TYPE_AUDIO)).isEqualTo("ec-3"); + } + + @Test + public void getCodecsOfType_withVideoTrack_returnsCodec() { + assertThat(getCodecsOfType("avc1.61e, vp9.63.1, ec-3 ", C.TRACK_TYPE_VIDEO)) + .isEqualTo("avc1.61e,vp9.63.1"); + assertThat(getCodecsOfType("avc1.61e, vp9.63.1, ec-3 ", C.TRACK_TYPE_VIDEO)) + .isEqualTo("avc1.61e,vp9.63.1"); + } + + @Test + public void getCodecsOfType_withInvalidCodec_returnsNull() { + assertThat(getCodecsOfType("invalidCodec1, invalidCodec2 ", C.TRACK_TYPE_AUDIO)).isNull(); + } + + @Test + public void unescapeFileName_invalidFileName_returnsNull() { + assertThat(Util.unescapeFileName("%a")).isNull(); + assertThat(Util.unescapeFileName("%xyz")).isNull(); + } + + @Test + public void escapeUnescapeFileName_returnsEscapedString() { + assertEscapeUnescapeFileName("just+a regular+fileName", "just+a regular+fileName"); + assertEscapeUnescapeFileName("key:value", "key%3avalue"); + assertEscapeUnescapeFileName("<>:\"/\\|?*%", "%3c%3e%3a%22%2f%5c%7c%3f%2a%25"); + + Random random = new Random(0); + for (int i = 0; i < 1000; i++) { + String string = TestUtil.buildTestString(1000, random); + assertEscapeUnescapeFileName(string); + } + } + + @Test + public void crc32_returnsUpdatedCrc32() { + byte[] bytes = {0x5F, 0x78, 0x04, 0x7B, 0x5F}; + int start = 1; + int end = 4; + int initialValue = 0xFFFFFFFF; + + int result = Util.crc32(bytes, start, end, initialValue); + + assertThat(result).isEqualTo(0x67CE9747); + } + + @Test + public void crc8_returnsUpdatedCrc8() { + byte[] bytes = {0x5F, 0x78, 0x04, 0x7B, 0x5F}; + int start = 1; + int end = 4; + int initialValue = 0; + + int result = Util.crc8(bytes, start, end, initialValue); + + assertThat(result).isEqualTo(0x4); + } + + @Test + public void getBigEndianInt_fromBigEndian() { + byte[] bytes = {0x1F, 0x2E, 0x3D, 0x4C}; + ByteBuffer byteBuffer = ByteBuffer.wrap(bytes).order(ByteOrder.BIG_ENDIAN); + + assertThat(Util.getBigEndianInt(byteBuffer, 0)).isEqualTo(0x1F2E3D4C); + } + + @Test + public void getBigEndianInt_fromLittleEndian() { + byte[] bytes = {(byte) 0xC2, (byte) 0xD3, (byte) 0xE4, (byte) 0xF5}; + ByteBuffer byteBuffer = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN); + + assertThat(Util.getBigEndianInt(byteBuffer, 0)).isEqualTo(0xC2D3E4F5); + } + + @Test + public void getBigEndianInt_unaligned() { + byte[] bytes = {9, 8, 7, 6, 5}; + ByteBuffer byteBuffer = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN); + + assertThat(Util.getBigEndianInt(byteBuffer, 1)).isEqualTo(0x08070605); + } + + @Test + public void inflate_withDeflatedData_success() { + 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); + } + + // TODO: Revert to @Config(sdk = Config.ALL_SDKS) once b/143232359 is resolved + @Test + @Config(minSdk = Config.OLDEST_SDK, maxSdk = Config.TARGET_SDK) + public void normalizeLanguageCode_keepsUndefinedTagsUnchanged() { + assertThat(Util.normalizeLanguageCode(null)).isNull(); + assertThat(Util.normalizeLanguageCode("")).isEmpty(); + assertThat(Util.normalizeLanguageCode("und")).isEqualTo("und"); + assertThat(Util.normalizeLanguageCode("DoesNotExist")).isEqualTo("doesnotexist"); + } + + // TODO: Revert to @Config(sdk = Config.ALL_SDKS) once b/143232359 is resolved + @Test + @Config(minSdk = Config.OLDEST_SDK, maxSdk = Config.TARGET_SDK) + public void normalizeLanguageCode_normalizesCodeToTwoLetterISOAndLowerCase_keepingAllSubtags() { + assertThat(Util.normalizeLanguageCode("es")).isEqualTo("es"); + assertThat(Util.normalizeLanguageCode("spa")).isEqualTo("es"); + assertThat(Util.normalizeLanguageCode("es-AR")).isEqualTo("es-ar"); + assertThat(Util.normalizeLanguageCode("SpA-ar")).isEqualTo("es-ar"); + assertThat(Util.normalizeLanguageCode("es_AR")).isEqualTo("es-ar"); + assertThat(Util.normalizeLanguageCode("spa_ar")).isEqualTo("es-ar"); + assertThat(Util.normalizeLanguageCode("es-AR-dialect")).isEqualTo("es-ar-dialect"); + // Regional subtag (South America) + assertThat(Util.normalizeLanguageCode("ES-419")).isEqualTo("es-419"); + // Script subtag (Simplified Taiwanese) + assertThat(Util.normalizeLanguageCode("zh-hans-tw")).isEqualTo("zh-hans-tw"); + assertThat(Util.normalizeLanguageCode("zho-hans-tw")).isEqualTo("zh-hans-tw"); + // Non-spec compliant subtags. + assertThat(Util.normalizeLanguageCode("sv-illegalSubtag")).isEqualTo("sv-illegalsubtag"); + } + + // TODO: Revert to @Config(sdk = Config.ALL_SDKS) once b/143232359 is resolved + @Test + @Config(minSdk = Config.OLDEST_SDK, maxSdk = Config.TARGET_SDK) + public void normalizeLanguageCode_iso6392BibliographicalAndTextualCodes_areNormalizedToSameTag() { + // See https://en.wikipedia.org/wiki/List_of_ISO_639-2_codes. + assertThat(Util.normalizeLanguageCode("alb")).isEqualTo(Util.normalizeLanguageCode("sqi")); + assertThat(Util.normalizeLanguageCode("arm")).isEqualTo(Util.normalizeLanguageCode("hye")); + assertThat(Util.normalizeLanguageCode("baq")).isEqualTo(Util.normalizeLanguageCode("eus")); + assertThat(Util.normalizeLanguageCode("bur")).isEqualTo(Util.normalizeLanguageCode("mya")); + assertThat(Util.normalizeLanguageCode("chi")).isEqualTo(Util.normalizeLanguageCode("zho")); + assertThat(Util.normalizeLanguageCode("cze")).isEqualTo(Util.normalizeLanguageCode("ces")); + assertThat(Util.normalizeLanguageCode("dut")).isEqualTo(Util.normalizeLanguageCode("nld")); + assertThat(Util.normalizeLanguageCode("fre")).isEqualTo(Util.normalizeLanguageCode("fra")); + assertThat(Util.normalizeLanguageCode("geo")).isEqualTo(Util.normalizeLanguageCode("kat")); + assertThat(Util.normalizeLanguageCode("ger")).isEqualTo(Util.normalizeLanguageCode("deu")); + assertThat(Util.normalizeLanguageCode("gre")).isEqualTo(Util.normalizeLanguageCode("ell")); + assertThat(Util.normalizeLanguageCode("ice")).isEqualTo(Util.normalizeLanguageCode("isl")); + assertThat(Util.normalizeLanguageCode("mac")).isEqualTo(Util.normalizeLanguageCode("mkd")); + assertThat(Util.normalizeLanguageCode("mao")).isEqualTo(Util.normalizeLanguageCode("mri")); + assertThat(Util.normalizeLanguageCode("may")).isEqualTo(Util.normalizeLanguageCode("msa")); + assertThat(Util.normalizeLanguageCode("per")).isEqualTo(Util.normalizeLanguageCode("fas")); + assertThat(Util.normalizeLanguageCode("rum")).isEqualTo(Util.normalizeLanguageCode("ron")); + assertThat(Util.normalizeLanguageCode("slo")).isEqualTo(Util.normalizeLanguageCode("slk")); + assertThat(Util.normalizeLanguageCode("scc")).isEqualTo(Util.normalizeLanguageCode("srp")); + assertThat(Util.normalizeLanguageCode("tib")).isEqualTo(Util.normalizeLanguageCode("bod")); + assertThat(Util.normalizeLanguageCode("wel")).isEqualTo(Util.normalizeLanguageCode("cym")); + } + + // TODO: Revert to @Config(sdk = Config.ALL_SDKS) once b/143232359 is resolved + @Test + @Config(minSdk = Config.OLDEST_SDK, maxSdk = Config.TARGET_SDK) + public void + normalizeLanguageCode_deprecatedLanguageTagsAndModernReplacement_areNormalizedToSameTag() { + // See https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes, "ISO 639:1988" + assertThat(Util.normalizeLanguageCode("in")).isEqualTo(Util.normalizeLanguageCode("id")); + assertThat(Util.normalizeLanguageCode("in")).isEqualTo(Util.normalizeLanguageCode("ind")); + assertThat(Util.normalizeLanguageCode("iw")).isEqualTo(Util.normalizeLanguageCode("he")); + assertThat(Util.normalizeLanguageCode("iw")).isEqualTo(Util.normalizeLanguageCode("heb")); + assertThat(Util.normalizeLanguageCode("ji")).isEqualTo(Util.normalizeLanguageCode("yi")); + assertThat(Util.normalizeLanguageCode("ji")).isEqualTo(Util.normalizeLanguageCode("yid")); + + // Grandfathered tags + assertThat(Util.normalizeLanguageCode("i-lux")).isEqualTo(Util.normalizeLanguageCode("lb")); + assertThat(Util.normalizeLanguageCode("i-lux")).isEqualTo(Util.normalizeLanguageCode("ltz")); + assertThat(Util.normalizeLanguageCode("i-hak")).isEqualTo(Util.normalizeLanguageCode("hak")); + assertThat(Util.normalizeLanguageCode("i-hak")).isEqualTo(Util.normalizeLanguageCode("zh-hak")); + assertThat(Util.normalizeLanguageCode("i-navajo")).isEqualTo(Util.normalizeLanguageCode("nv")); + assertThat(Util.normalizeLanguageCode("i-navajo")).isEqualTo(Util.normalizeLanguageCode("nav")); + assertThat(Util.normalizeLanguageCode("no-bok")).isEqualTo(Util.normalizeLanguageCode("nb")); + assertThat(Util.normalizeLanguageCode("no-bok")).isEqualTo(Util.normalizeLanguageCode("nob")); + assertThat(Util.normalizeLanguageCode("no-nyn")).isEqualTo(Util.normalizeLanguageCode("nn")); + assertThat(Util.normalizeLanguageCode("no-nyn")).isEqualTo(Util.normalizeLanguageCode("nno")); + assertThat(Util.normalizeLanguageCode("zh-guoyu")).isEqualTo(Util.normalizeLanguageCode("cmn")); + assertThat(Util.normalizeLanguageCode("zh-guoyu")) + .isEqualTo(Util.normalizeLanguageCode("zh-cmn")); + assertThat(Util.normalizeLanguageCode("zh-hakka")).isEqualTo(Util.normalizeLanguageCode("hak")); + assertThat(Util.normalizeLanguageCode("zh-hakka")) + .isEqualTo(Util.normalizeLanguageCode("zh-hak")); + assertThat(Util.normalizeLanguageCode("zh-min-nan")) + .isEqualTo(Util.normalizeLanguageCode("nan")); + assertThat(Util.normalizeLanguageCode("zh-min-nan")) + .isEqualTo(Util.normalizeLanguageCode("zh-nan")); + assertThat(Util.normalizeLanguageCode("zh-xiang")).isEqualTo(Util.normalizeLanguageCode("hsn")); + assertThat(Util.normalizeLanguageCode("zh-xiang")) + .isEqualTo(Util.normalizeLanguageCode("zh-hsn")); + } + + // TODO: Revert to @Config(sdk = Config.ALL_SDKS) once b/143232359 is resolved + @Test + @Config(minSdk = Config.OLDEST_SDK, maxSdk = Config.TARGET_SDK) + public void normalizeLanguageCode_macrolanguageTags_areFullyMaintained() { + // See https://en.wikipedia.org/wiki/ISO_639_macrolanguage + assertThat(Util.normalizeLanguageCode("zh-cmn")).isEqualTo("zh-cmn"); + assertThat(Util.normalizeLanguageCode("zho-cmn")).isEqualTo("zh-cmn"); + assertThat(Util.normalizeLanguageCode("ar-ayl")).isEqualTo("ar-ayl"); + assertThat(Util.normalizeLanguageCode("ara-ayl")).isEqualTo("ar-ayl"); + + // Special case of short codes that are actually part of a macrolanguage. + assertThat(Util.normalizeLanguageCode("nb")).isEqualTo("no-nob"); + assertThat(Util.normalizeLanguageCode("nn")).isEqualTo("no-nno"); + assertThat(Util.normalizeLanguageCode("nob")).isEqualTo("no-nob"); + assertThat(Util.normalizeLanguageCode("nno")).isEqualTo("no-nno"); + assertThat(Util.normalizeLanguageCode("tw")).isEqualTo("ak-twi"); + assertThat(Util.normalizeLanguageCode("twi")).isEqualTo("ak-twi"); + assertThat(Util.normalizeLanguageCode("bs")).isEqualTo("hbs-bos"); + assertThat(Util.normalizeLanguageCode("bos")).isEqualTo("hbs-bos"); + assertThat(Util.normalizeLanguageCode("hr")).isEqualTo("hbs-hrv"); + assertThat(Util.normalizeLanguageCode("hrv")).isEqualTo("hbs-hrv"); + assertThat(Util.normalizeLanguageCode("sr")).isEqualTo("hbs-srp"); + assertThat(Util.normalizeLanguageCode("srp")).isEqualTo("hbs-srp"); + assertThat(Util.normalizeLanguageCode("id")).isEqualTo("ms-ind"); + assertThat(Util.normalizeLanguageCode("ind")).isEqualTo("ms-ind"); + assertThat(Util.normalizeLanguageCode("cmn")).isEqualTo("zh-cmn"); + assertThat(Util.normalizeLanguageCode("hak")).isEqualTo("zh-hak"); + assertThat(Util.normalizeLanguageCode("nan")).isEqualTo("zh-nan"); + assertThat(Util.normalizeLanguageCode("hsn")).isEqualTo("zh-hsn"); + } + + @Test + public void toList() { + assertThat(Util.toList(0, 3, 4)).containsExactly(0, 3, 4).inOrder(); + } + + @Test + public void toList_nullPassed_returnsEmptyList() { + assertThat(Util.toList(null)).isEmpty(); + } + + @Test + public void toList_emptyArrayPassed_returnsEmptyList() { + assertThat(Util.toList(new int[0])).isEmpty(); + } + + private static void assertEscapeUnescapeFileName(String fileName, String escapedFileName) { + assertThat(escapeFileName(fileName)).isEqualTo(escapedFileName); + assertThat(unescapeFileName(escapedFileName)).isEqualTo(fileName); + } + + private static void assertEscapeUnescapeFileName(String fileName) { + String escapedFileName = Util.escapeFileName(fileName); + assertThat(unescapeFileName(escapedFileName)).isEqualTo(fileName); + } + + private static LongArray newLongArray(long... values) { + LongArray longArray = new LongArray(); + for (long value : values) { + longArray.add(value); + } + return longArray; + } +} diff --git a/library/core/build.gradle b/library/core/build.gradle index e145a179d9..8b8c3fd520 100644 --- a/library/core/build.gradle +++ b/library/core/build.gradle @@ -41,26 +41,30 @@ android { } } + sourceSets { + androidTest.assets.srcDir '../../testdata/src/test/assets/' + test.assets.srcDir '../../testdata/src/test/assets/' + } + testOptions.unitTests.includeAndroidResources = true } dependencies { + api project(modulePrefix + 'library-common') + api project(modulePrefix + 'library-extractor') implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion compileOnly 'com.google.code.findbugs:jsr305:' + jsr305Version compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion compileOnly 'org.checkerframework:checker-compat-qual:' + checkerframeworkVersion compileOnly 'org.jetbrains.kotlin:kotlin-annotations-jvm:' + kotlinAnnotationsVersion androidTestImplementation 'androidx.test:runner:' + androidxTestRunnerVersion - androidTestImplementation 'androidx.test.ext:junit:' + androidxTestJUnitVersion - androidTestImplementation 'com.google.truth:truth:' + truthVersion + androidTestImplementation 'com.google.guava:guava:' + guavaVersion androidTestImplementation 'com.linkedin.dexmaker:dexmaker:' + dexmakerVersion androidTestImplementation 'com.linkedin.dexmaker:dexmaker-mockito:' + dexmakerVersion - androidTestImplementation 'org.mockito:mockito-core:' + mockitoVersion - androidTestImplementation project(modulePrefix + 'testutils') - testImplementation 'androidx.test:core:' + androidxTestCoreVersion - testImplementation 'androidx.test.ext:junit:' + androidxTestJUnitVersion - testImplementation 'com.google.truth:truth:' + truthVersion - testImplementation 'org.mockito:mockito-core:' + mockitoVersion + androidTestImplementation(project(modulePrefix + 'testutils')) { + exclude module: modulePrefix.substring(1) + 'library-core' + } + testImplementation 'com.google.guava:guava:' + guavaVersion testImplementation 'org.robolectric:robolectric:' + robolectricVersion testImplementation project(modulePrefix + 'testutils') } diff --git a/library/core/proguard-rules.txt b/library/core/proguard-rules.txt index 67646be956..cbeb74cf6c 100644 --- a/library/core/proguard-rules.txt +++ b/library/core/proguard-rules.txt @@ -1,10 +1,34 @@ # Proguard rules specific to the core module. +# Constant folding for resource integers may mean that a resource passed to this method appears to be unused. Keep the method to prevent this from happening. +-keep class com.google.android.exoplayer2.upstream.RawResourceDataSource { + public static android.net.Uri buildRawResourceUri(int); +} + +# Methods accessed via reflection in DefaultExtractorsFactory +-dontnote com.google.android.exoplayer2.ext.flac.FlacLibrary +-keepclassmembers class com.google.android.exoplayer2.ext.flac.FlacLibrary { + public static boolean isAvailable(); +} + +# Some members of this class are being accessed from native methods. Keep them unobfuscated. +-keep class com.google.android.exoplayer2.video.VideoDecoderOutputBuffer { + *; +} + # Constructors accessed via reflection in DefaultRenderersFactory -dontnote com.google.android.exoplayer2.ext.vp9.LibvpxVideoRenderer -keepclassmembers class com.google.android.exoplayer2.ext.vp9.LibvpxVideoRenderer { (long, android.os.Handler, com.google.android.exoplayer2.video.VideoRendererEventListener, int); } +-dontnote com.google.android.exoplayer2.ext.av1.Libgav1VideoRenderer +-keepclassmembers class com.google.android.exoplayer2.ext.av1.Libgav1VideoRenderer { + (long, android.os.Handler, com.google.android.exoplayer2.video.VideoRendererEventListener, int); +} +-dontnote com.google.android.exoplayer2.ext.ffmpeg.FfmpegVideoRenderer +-keepclassmembers class com.google.android.exoplayer2.ext.ffmpeg.FfmpegVideoRenderer { + (long, android.os.Handler, com.google.android.exoplayer2.video.VideoRendererEventListener, int); +} -dontnote com.google.android.exoplayer2.ext.opus.LibopusAudioRenderer -keepclassmembers class com.google.android.exoplayer2.ext.opus.LibopusAudioRenderer { (android.os.Handler, com.google.android.exoplayer2.audio.AudioRendererEventListener, com.google.android.exoplayer2.audio.AudioProcessor[]); @@ -18,12 +42,6 @@ (android.os.Handler, com.google.android.exoplayer2.audio.AudioRendererEventListener, com.google.android.exoplayer2.audio.AudioProcessor[]); } -# Constructors accessed via reflection in DefaultExtractorsFactory --dontnote com.google.android.exoplayer2.ext.flac.FlacExtractor --keepclassmembers class com.google.android.exoplayer2.ext.flac.FlacExtractor { - (); -} - # Constructors accessed via reflection in DefaultDataSource -dontnote com.google.android.exoplayer2.ext.rtmp.RtmpDataSource -keepclassmembers class com.google.android.exoplayer2.ext.rtmp.RtmpDataSource { @@ -33,18 +51,18 @@ # Constructors accessed via reflection in DefaultDownloaderFactory -dontnote com.google.android.exoplayer2.source.dash.offline.DashDownloader -keepclassmembers class com.google.android.exoplayer2.source.dash.offline.DashDownloader { - (android.net.Uri, java.util.List, com.google.android.exoplayer2.offline.DownloaderConstructorHelper); + (android.net.Uri, java.util.List, com.google.android.exoplayer2.upstream.cache.CacheDataSource.Factory, java.util.concurrent.Executor); } -dontnote com.google.android.exoplayer2.source.hls.offline.HlsDownloader -keepclassmembers class com.google.android.exoplayer2.source.hls.offline.HlsDownloader { - (android.net.Uri, java.util.List, com.google.android.exoplayer2.offline.DownloaderConstructorHelper); + (android.net.Uri, java.util.List, com.google.android.exoplayer2.upstream.cache.CacheDataSource.Factory, java.util.concurrent.Executor); } -dontnote com.google.android.exoplayer2.source.smoothstreaming.offline.SsDownloader -keepclassmembers class com.google.android.exoplayer2.source.smoothstreaming.offline.SsDownloader { - (android.net.Uri, java.util.List, com.google.android.exoplayer2.offline.DownloaderConstructorHelper); + (android.net.Uri, java.util.List, com.google.android.exoplayer2.upstream.cache.CacheDataSource.Factory, java.util.concurrent.Executor); } -# Constructors accessed via reflection in DownloadHelper +# Constructors accessed via reflection in DefaultMediaSourceFactory and DownloadHelper -dontnote com.google.android.exoplayer2.source.dash.DashMediaSource$Factory -keepclasseswithmembers class com.google.android.exoplayer2.source.dash.DashMediaSource$Factory { (com.google.android.exoplayer2.upstream.DataSource$Factory); @@ -61,8 +79,4 @@ # Don't warn about checkerframework and Kotlin annotations -dontwarn org.checkerframework.** -dontwarn kotlin.annotations.jvm.** - -# Some members of this class are being accessed from native methods. Keep them unobfuscated. --keep class com.google.android.exoplayer2.ext.video.VideoDecoderOutputBuffer { - *; -} +-dontwarn javax.annotation.** diff --git a/library/core/src/androidTest/AndroidManifest.xml b/library/core/src/androidTest/AndroidManifest.xml index 831ad47831..04a07c4d50 100644 --- a/library/core/src/androidTest/AndroidManifest.xml +++ b/library/core/src/androidTest/AndroidManifest.xml @@ -24,7 +24,6 @@ - diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/StreamVolumeManagerTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/StreamVolumeManagerTest.java new file mode 100644 index 0000000000..792492dc4a --- /dev/null +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/StreamVolumeManagerTest.java @@ -0,0 +1,296 @@ +/* + * Copyright 2020 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; + +import static com.google.common.truth.Truth.assertThat; +import static java.util.concurrent.TimeUnit.MILLISECONDS; + +import android.content.Context; +import android.media.AudioManager; +import android.os.Handler; +import android.os.Looper; +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SdkSuppress; +import com.google.android.exoplayer2.testutil.DummyMainThread; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicInteger; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Unit tests for {@link StreamVolumeManager}. */ +@RunWith(AndroidJUnit4.class) +public class StreamVolumeManagerTest { + + private static final long TIMEOUT_MS = 1_000; + + private AudioManager audioManager; + private TestListener testListener; + private DummyMainThread testThread; + private StreamVolumeManager streamVolumeManager; + + @Before + public void setUp() { + Context context = ApplicationProvider.getApplicationContext(); + + audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); + testListener = new TestListener(); + + testThread = new DummyMainThread(); + testThread.runOnMainThread( + () -> + streamVolumeManager = + new StreamVolumeManager(context, new Handler(Looper.myLooper()), testListener)); + } + + @After + public void tearDown() { + testThread.runOnMainThread(() -> streamVolumeManager.release()); + testThread.release(); + } + + @Test + @SdkSuppress(minSdkVersion = 28) + public void getMinVolume_returnsStreamMinVolume() { + testThread.runOnMainThread( + () -> { + int streamMinVolume = audioManager.getStreamMinVolume(C.STREAM_TYPE_DEFAULT); + assertThat(streamVolumeManager.getMinVolume()).isEqualTo(streamMinVolume); + }); + } + + @Test + public void getMaxVolume_returnsStreamMaxVolume() { + testThread.runOnMainThread( + () -> { + int streamMaxVolume = audioManager.getStreamMaxVolume(C.STREAM_TYPE_DEFAULT); + assertThat(streamVolumeManager.getMaxVolume()).isEqualTo(streamMaxVolume); + }); + } + + @Test + public void getVolume_returnsStreamVolume() { + testThread.runOnMainThread( + () -> { + int streamVolume = audioManager.getStreamVolume(C.STREAM_TYPE_DEFAULT); + assertThat(streamVolumeManager.getVolume()).isEqualTo(streamVolume); + }); + } + + @Test + public void setVolume_changesStreamVolume() { + testThread.runOnMainThread( + () -> { + int minVolume = streamVolumeManager.getMinVolume(); + int maxVolume = streamVolumeManager.getMaxVolume(); + if (minVolume == maxVolume) { + return; + } + + int oldVolume = streamVolumeManager.getVolume(); + int targetVolume = oldVolume == maxVolume ? minVolume : maxVolume; + + streamVolumeManager.setVolume(targetVolume); + + assertThat(streamVolumeManager.getVolume()).isEqualTo(targetVolume); + assertThat(testListener.lastStreamVolume).isEqualTo(targetVolume); + assertThat(audioManager.getStreamVolume(C.STREAM_TYPE_DEFAULT)).isEqualTo(targetVolume); + }); + } + + @Test + public void setVolume_withOutOfRange_isIgnored() { + testThread.runOnMainThread( + () -> { + int maxVolume = streamVolumeManager.getMaxVolume(); + int minVolume = streamVolumeManager.getMinVolume(); + int oldVolume = streamVolumeManager.getVolume(); + + streamVolumeManager.setVolume(maxVolume + 1); + assertThat(streamVolumeManager.getVolume()).isEqualTo(oldVolume); + + streamVolumeManager.setVolume(minVolume - 1); + assertThat(streamVolumeManager.getVolume()).isEqualTo(oldVolume); + }); + } + + @Test + public void increaseVolume_increasesStreamVolumeByOne() { + testThread.runOnMainThread( + () -> { + int minVolume = streamVolumeManager.getMinVolume(); + int maxVolume = streamVolumeManager.getMaxVolume(); + if (minVolume == maxVolume) { + return; + } + + streamVolumeManager.setVolume(minVolume); + int targetVolume = minVolume + 1; + + streamVolumeManager.increaseVolume(); + + assertThat(streamVolumeManager.getVolume()).isEqualTo(targetVolume); + assertThat(testListener.lastStreamVolume).isEqualTo(targetVolume); + assertThat(audioManager.getStreamVolume(C.STREAM_TYPE_DEFAULT)).isEqualTo(targetVolume); + }); + } + + @Test + public void increaseVolume_onMaxVolume_isIgnored() { + testThread.runOnMainThread( + () -> { + int maxVolume = streamVolumeManager.getMaxVolume(); + + streamVolumeManager.setVolume(maxVolume); + streamVolumeManager.increaseVolume(); + + assertThat(streamVolumeManager.getVolume()).isEqualTo(maxVolume); + }); + } + + @Test + public void decreaseVolume_decreasesStreamVolumeByOne() { + testThread.runOnMainThread( + () -> { + int minVolume = streamVolumeManager.getMinVolume(); + int maxVolume = streamVolumeManager.getMaxVolume(); + if (minVolume == maxVolume) { + return; + } + + streamVolumeManager.setVolume(maxVolume); + int targetVolume = maxVolume - 1; + + streamVolumeManager.decreaseVolume(); + + assertThat(streamVolumeManager.getVolume()).isEqualTo(targetVolume); + assertThat(testListener.lastStreamVolume).isEqualTo(targetVolume); + assertThat(audioManager.getStreamVolume(C.STREAM_TYPE_DEFAULT)).isEqualTo(targetVolume); + }); + } + + @Test + public void decreaseVolume_onMinVolume_isIgnored() { + testThread.runOnMainThread( + () -> { + int minVolume = streamVolumeManager.getMinVolume(); + + streamVolumeManager.setVolume(minVolume); + streamVolumeManager.decreaseVolume(); + + assertThat(streamVolumeManager.getVolume()).isEqualTo(minVolume); + }); + } + + @Test + public void setVolumeMuted_changesMuteState() { + testThread.runOnMainThread( + () -> { + int minVolume = streamVolumeManager.getMinVolume(); + int maxVolume = streamVolumeManager.getMaxVolume(); + if (minVolume == maxVolume || minVolume > 0) { + return; + } + + streamVolumeManager.setVolume(maxVolume); + assertThat(streamVolumeManager.isMuted()).isFalse(); + + streamVolumeManager.setMuted(true); + assertThat(streamVolumeManager.isMuted()).isTrue(); + assertThat(testListener.lastStreamVolumeMuted).isTrue(); + + streamVolumeManager.setMuted(false); + assertThat(streamVolumeManager.isMuted()).isFalse(); + assertThat(testListener.lastStreamVolumeMuted).isFalse(); + assertThat(testListener.lastStreamVolume).isEqualTo(maxVolume); + }); + } + + @Test + public void setStreamType_notifiesStreamTypeAndVolume() { + testThread.runOnMainThread( + () -> { + int minVolume = streamVolumeManager.getMinVolume(); + int maxVolume = streamVolumeManager.getMaxVolume(); + if (minVolume == maxVolume) { + return; + } + + int testStreamType = C.STREAM_TYPE_ALARM; + int testStreamVolume = audioManager.getStreamVolume(testStreamType); + + int oldVolume = streamVolumeManager.getVolume(); + if (oldVolume == testStreamVolume) { + int differentVolume = oldVolume == minVolume ? maxVolume : minVolume; + streamVolumeManager.setVolume(differentVolume); + } + + streamVolumeManager.setStreamType(testStreamType); + + assertThat(testListener.lastStreamType).isEqualTo(testStreamType); + assertThat(testListener.lastStreamVolume).isEqualTo(testStreamVolume); + assertThat(streamVolumeManager.getVolume()).isEqualTo(testStreamVolume); + }); + } + + @Test + public void onStreamVolumeChanged_isCalled_whenAudioManagerChangesIt() throws Exception { + AtomicInteger targetVolumeRef = new AtomicInteger(); + testThread.runOnMainThread( + () -> { + int minVolume = streamVolumeManager.getMinVolume(); + int maxVolume = streamVolumeManager.getMaxVolume(); + if (minVolume == maxVolume) { + return; + } + + int oldVolume = streamVolumeManager.getVolume(); + int targetVolume = oldVolume == maxVolume ? minVolume : maxVolume; + targetVolumeRef.set(targetVolume); + + audioManager.setStreamVolume(C.STREAM_TYPE_DEFAULT, targetVolume, /* flags= */ 0); + }); + + testListener.onStreamVolumeChangedLatch.await(TIMEOUT_MS, MILLISECONDS); + assertThat(testListener.lastStreamVolume).isEqualTo(targetVolumeRef.get()); + } + + private static class TestListener implements StreamVolumeManager.Listener { + + @C.StreamType private int lastStreamType; + private int lastStreamVolume; + private boolean lastStreamVolumeMuted; + public final CountDownLatch onStreamVolumeChangedLatch; + + public TestListener() { + onStreamVolumeChangedLatch = new CountDownLatch(1); + } + + @Override + public void onStreamTypeChanged(@C.StreamType int streamType) { + lastStreamType = streamType; + } + + @Override + public void onStreamVolumeChanged(int streamVolume, boolean streamMuted) { + lastStreamVolume = streamVolume; + lastStreamVolumeMuted = streamMuted; + onStreamVolumeChangedLatch.countDown(); + } + } +} 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 2021cb21c2..39c12e1b75 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 @@ -43,40 +43,40 @@ import org.junit.runner.RunWith; 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"; + private static final String DATA_PATH = "mp3/1024_incrementing_bytes.mp3"; @Test - public void testRead() throws Exception { + public void read() throws Exception { assertData(0, C.LENGTH_UNSET, false); } @Test - public void testReadPipeMode() throws Exception { + public void readPipeMode() throws Exception { assertData(0, C.LENGTH_UNSET, true); } @Test - public void testReadFixedLength() throws Exception { + public void readFixedLength() throws Exception { assertData(0, 100, false); } @Test - public void testReadFromOffsetToEndOfInput() throws Exception { + public void readFromOffsetToEndOfInput() throws Exception { assertData(1, C.LENGTH_UNSET, false); } @Test - public void testReadFromOffsetToEndOfInputPipeMode() throws Exception { + public void readFromOffsetToEndOfInputPipeMode() throws Exception { assertData(1, C.LENGTH_UNSET, true); } @Test - public void testReadFromOffsetFixedLength() throws Exception { + public void readFromOffsetFixedLength() throws Exception { assertData(1, 100, false); } @Test - public void testReadInvalidUri() throws Exception { + public void readInvalidUri() throws Exception { ContentDataSource dataSource = new ContentDataSource(InstrumentationRegistry.getTargetContext()); Uri contentUri = TestContentProvider.buildUri("does/not.exist", false); @@ -97,7 +97,7 @@ public final class ContentDataSourceTest { ContentDataSource dataSource = new ContentDataSource(InstrumentationRegistry.getTargetContext()); try { - DataSpec dataSpec = new DataSpec(contentUri, offset, length, null); + DataSpec dataSpec = new DataSpec(contentUri, offset, length); byte[] completeData = TestUtil.getByteArray(InstrumentationRegistry.getTargetContext(), DATA_PATH); byte[] expectedData = Arrays.copyOfRange(completeData, offset, diff --git a/library/core/src/main/java/com/google/android/exoplayer2/AudioFocusManager.java b/library/core/src/main/java/com/google/android/exoplayer2/AudioFocusManager.java index c9aa9e54a8..5aeca440ff 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/AudioFocusManager.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/AudioFocusManager.java @@ -24,7 +24,6 @@ import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import androidx.annotation.VisibleForTesting; import com.google.android.exoplayer2.audio.AudioAttributes; -import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.Util; @@ -76,15 +75,12 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; @Documented @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. */ @@ -101,7 +97,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; private final AudioManager audioManager; private final AudioFocusListener focusListener; - private final PlayerControl playerControl; + @Nullable private PlayerControl playerControl; @Nullable private AudioAttributes audioAttributes; @AudioFocusState private int audioFocusState; @@ -134,64 +130,45 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /** * Sets audio attributes that should be used to manage audio focus. * + *

Call {@link #updateAudioFocus(boolean, int)} to update the audio focus based on these + * attributes. + * * @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 {@link PlayerCommand} to execute on the player. */ - @PlayerCommand - public int setAudioAttributes( - @Nullable AudioAttributes audioAttributes, boolean playWhenReady, int playerState) { + public void setAudioAttributes(@Nullable AudioAttributes audioAttributes) { 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(); - } } - - return playerState == Player.STATE_IDLE - ? handleIdle(playWhenReady) - : handlePrepare(playWhenReady); } /** - * Called by a player as part of {@link ExoPlayer#prepare(MediaSource, boolean, boolean)}. + * Called by the player to abandon or request audio focus based on the desired player state. * - * @param playWhenReady The current state of {@link ExoPlayer#getPlayWhenReady()}. + * @param playWhenReady The desired value of playWhenReady. + * @param playbackState The desired playback state. * @return A {@link PlayerCommand} to execute on the player. */ @PlayerCommand - public int handlePrepare(boolean playWhenReady) { + public int updateAudioFocus(boolean playWhenReady, @Player.State int playbackState) { + if (shouldAbandonAudioFocus(playbackState)) { + abandonAudioFocus(); + return playWhenReady ? PLAYER_COMMAND_PLAY_WHEN_READY : PLAYER_COMMAND_DO_NOT_PLAY; + } 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 {@link PlayerCommand} to execute on the player. + * Called when the manager is no longer required. Audio focus will be released without making any + * calls to the {@link PlayerControl}. */ - @PlayerCommand - public int handleSetPlayWhenReady(boolean playWhenReady, int playerState) { - if (!playWhenReady) { - abandonAudioFocus(); - return PLAYER_COMMAND_DO_NOT_PLAY; - } - - return playerState == Player.STATE_IDLE ? handleIdle(playWhenReady) : requestAudioFocus(); - } - - /** Called by the player as part of {@link ExoPlayer#stop(boolean)}. */ - public void handleStop() { - abandonAudioFocus(/* forceAbandon= */ true); + public void release() { + playerControl = null; + abandonAudioFocus(); } // Internal methods. @@ -201,62 +178,35 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; return focusListener; } - @PlayerCommand - private int handleIdle(boolean playWhenReady) { - return playWhenReady ? PLAYER_COMMAND_PLAY_WHEN_READY : PLAYER_COMMAND_DO_NOT_PLAY; + private boolean shouldAbandonAudioFocus(@Player.State int playbackState) { + return playbackState == Player.STATE_IDLE || focusGain != C.AUDIOFOCUS_GAIN; } @PlayerCommand private int requestAudioFocus() { - int focusRequestResult; - - if (focusGain == C.AUDIOFOCUS_NONE) { - if (audioFocusState != AUDIO_FOCUS_STATE_NO_FOCUS) { - abandonAudioFocus(/* forceAbandon= */ true); - } + if (audioFocusState == AUDIO_FOCUS_STATE_HAVE_FOCUS) { 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) { + int requestResult = Util.SDK_INT >= 26 ? requestAudioFocusV26() : requestAudioFocusDefault(); + if (requestResult == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) { + setAudioFocusState(AUDIO_FOCUS_STATE_HAVE_FOCUS); + return PLAYER_COMMAND_PLAY_WHEN_READY; + } else { + setAudioFocusState(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) { + if (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; + if (Util.SDK_INT >= 26) { + abandonAudioFocusV26(); + } else { + abandonAudioFocusDefault(); } + setAudioFocusState(AUDIO_FOCUS_STATE_NO_FOCUS); } private int requestAudioFocusDefault() { @@ -312,7 +262,6 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; */ @C.AudioFocusGain 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) @@ -382,63 +331,55 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; } } - private void handleAudioFocusChange(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); + private void setAudioFocusState(@AudioFocusState int audioFocusState) { + if (this.audioFocusState == audioFocusState) { + return; } + this.audioFocusState = 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; + if (this.volumeMultiplier == volumeMultiplier) { + return; + } + this.volumeMultiplier = volumeMultiplier; + if (playerControl != null) { playerControl.setVolumeMultiplier(volumeMultiplier); } } + private void handlePlatformAudioFocusChange(int focusChange) { + switch (focusChange) { + case AudioManager.AUDIOFOCUS_GAIN: + setAudioFocusState(AUDIO_FOCUS_STATE_HAVE_FOCUS); + executePlayerCommand(PLAYER_COMMAND_PLAY_WHEN_READY); + return; + case AudioManager.AUDIOFOCUS_LOSS: + executePlayerCommand(PLAYER_COMMAND_DO_NOT_PLAY); + abandonAudioFocus(); + return; + case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT: + case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK: + if (focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT || willPauseWhenDucked()) { + executePlayerCommand(PLAYER_COMMAND_WAIT_FOR_CALLBACK); + setAudioFocusState(AUDIO_FOCUS_STATE_LOSS_TRANSIENT); + } else { + setAudioFocusState(AUDIO_FOCUS_STATE_LOSS_TRANSIENT_DUCK); + } + return; + default: + Log.w(TAG, "Unknown focus change type: " + focusChange); + } + } + + private void executePlayerCommand(@PlayerCommand int playerCommand) { + if (playerControl != null) { + playerControl.executePlayerCommand(playerCommand); + } + } + // Internal audio focus listener. private class AudioFocusListener implements AudioManager.OnAudioFocusChangeListener { @@ -450,7 +391,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; @Override public void onAudioFocusChange(int focusChange) { - eventHandler.post(() -> handleAudioFocusChange(focusChange)); + eventHandler.post(() -> handlePlatformAudioFocusChange(focusChange)); } } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/BasePlayer.java b/library/core/src/main/java/com/google/android/exoplayer2/BasePlayer.java index 2646cbc035..5692b1dae7 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/BasePlayer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/BasePlayer.java @@ -17,6 +17,8 @@ package com.google.android.exoplayer2; import androidx.annotation.Nullable; import com.google.android.exoplayer2.util.Util; +import java.util.Collections; +import java.util.List; /** Abstract base {@link Player} which implements common implementation independent methods. */ public abstract class BasePlayer implements Player { @@ -27,6 +29,64 @@ public abstract class BasePlayer implements Player { window = new Timeline.Window(); } + @Override + public void setMediaItem(MediaItem mediaItem) { + setMediaItems(Collections.singletonList(mediaItem)); + } + + @Override + public void setMediaItem(MediaItem mediaItem, long startPositionMs) { + setMediaItems(Collections.singletonList(mediaItem), /* startWindowIndex= */ 0, startPositionMs); + } + + @Override + public void setMediaItem(MediaItem mediaItem, boolean resetPosition) { + setMediaItems(Collections.singletonList(mediaItem), resetPosition); + } + + @Override + public void setMediaItems(List mediaItems, boolean resetPosition) { + setMediaItems( + mediaItems, /* startWindowIndex= */ C.INDEX_UNSET, /* startPositionMs= */ C.TIME_UNSET); + } + + @Override + public void setMediaItems(List mediaItems) { + setMediaItems(mediaItems, /* resetPosition= */ true); + } + + @Override + public void addMediaItem(int index, MediaItem mediaItem) { + addMediaItems(index, Collections.singletonList(mediaItem)); + } + + @Override + public void addMediaItem(MediaItem mediaItem) { + addMediaItems(Collections.singletonList(mediaItem)); + } + + @Override + public void moveMediaItem(int currentIndex, int newIndex) { + if (currentIndex != newIndex) { + moveMediaItems(/* fromIndex= */ currentIndex, /* toIndex= */ currentIndex + 1, newIndex); + } + } + + @Override + public void removeMediaItem(int index) { + removeMediaItems(/* fromIndex= */ index, /* toIndex= */ index + 1); + } + + @Override + public final void play() { + setPlayWhenReady(true); + } + + @Override + public final void pause() { + setPlayWhenReady(false); + } + @Override public final boolean isPlaying() { return getPlaybackState() == Player.STATE_READY @@ -133,6 +193,19 @@ public abstract class BasePlayer implements Player { return !timeline.isEmpty() && timeline.getWindow(getCurrentWindowIndex(), window).isLive; } + @Override + public final long getCurrentLiveOffset() { + Timeline timeline = getCurrentTimeline(); + if (timeline.isEmpty()) { + return C.TIME_UNSET; + } + long windowStartTimeMs = timeline.getWindow(getCurrentWindowIndex(), window).windowStartTimeMs; + if (windowStartTimeMs == C.TIME_UNSET) { + return C.TIME_UNSET; + } + return window.getCurrentUnixTimeMs() - window.windowStartTimeMs - getContentPosition(); + } + @Override public final boolean isCurrentWindowSeekable() { Timeline timeline = getCurrentTimeline(); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/BaseRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/BaseRenderer.java index 3cdab8baf1..fc2cbbce28 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/BaseRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/BaseRenderer.java @@ -15,17 +15,11 @@ */ package com.google.android.exoplayer2; -import android.os.Looper; import androidx.annotation.Nullable; import com.google.android.exoplayer2.decoder.DecoderInputBuffer; -import com.google.android.exoplayer2.drm.DrmInitData; -import com.google.android.exoplayer2.drm.DrmSession; -import com.google.android.exoplayer2.drm.DrmSessionManager; -import com.google.android.exoplayer2.drm.ExoMediaCrypto; import com.google.android.exoplayer2.source.SampleStream; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.MediaClock; -import com.google.android.exoplayer2.util.Util; import java.io.IOException; /** @@ -44,6 +38,7 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities { private long streamOffsetUs; private long readingPositionUs; private boolean streamIsFinal; + private boolean throwRendererExceptionIsExecuting; /** * @param trackType The track type that the renderer handles. One of the {@link C} @@ -82,13 +77,19 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities { } @Override - public final void enable(RendererConfiguration configuration, Format[] formats, - SampleStream stream, long positionUs, boolean joining, long offsetUs) + public final void enable( + RendererConfiguration configuration, + Format[] formats, + SampleStream stream, + long positionUs, + boolean joining, + boolean mayRenderStartOfStream, + long offsetUs) throws ExoPlaybackException { Assertions.checkState(state == STATE_DISABLED); this.configuration = configuration; state = STATE_ENABLED; - onEnabled(joining); + onEnabled(joining, mayRenderStartOfStream); replaceStream(formats, stream, offsetUs); onPositionReset(positionUs, joining); } @@ -177,6 +178,7 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities { // RendererCapabilities implementation. @Override + @AdaptiveSupport public int supportsMixedMimeTypeAdaptation() throws ExoPlaybackException { return ADAPTIVE_NOT_SUPPORTED; } @@ -192,27 +194,30 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities { /** * Called when the renderer is enabled. - *

- * The default implementation is a no-op. + * + *

The default implementation is a no-op. * * @param joining Whether this renderer is being enabled to join an ongoing playback. + * @param mayRenderStartOfStream Whether this renderer is allowed to render the start of the + * stream even if the state is not {@link #STATE_STARTED} yet. * @throws ExoPlaybackException If an error occurs. */ - protected void onEnabled(boolean joining) throws ExoPlaybackException { + protected void onEnabled(boolean joining, boolean mayRenderStartOfStream) + throws ExoPlaybackException { // Do nothing. } /** * Called when the renderer's stream has changed. This occurs when the renderer is enabled after - * {@link #onEnabled(boolean)} has been called, and also when the stream has been replaced whilst - * the renderer is enabled or started. - *

- * The default implementation is a no-op. + * {@link #onEnabled(boolean, boolean)} has been called, and also when the stream has been + * replaced whilst the renderer is enabled or started. + * + *

The default implementation is a no-op. * * @param formats The enabled formats. - * @param offsetUs The offset that will be added to the timestamps of buffers read via - * {@link #readSource(FormatHolder, DecoderInputBuffer, boolean)} so that decoder input - * buffers have monotonically increasing timestamps. + * @param offsetUs The offset that will be added to the timestamps of buffers read via {@link + * #readSource(FormatHolder, DecoderInputBuffer, boolean)} so that decoder input buffers have + * monotonically increasing timestamps. * @throws ExoPlaybackException If an error occurs. */ protected void onStreamChanged(Format[] formats, long offsetUs) throws ExoPlaybackException { @@ -297,35 +302,6 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities { return configuration; } - /** Returns a {@link DrmSession} ready for assignment, handling resource management. */ - @Nullable - protected final DrmSession getUpdatedSourceDrmSession( - @Nullable Format oldFormat, - Format newFormat, - @Nullable DrmSessionManager drmSessionManager, - @Nullable DrmSession existingSourceSession) - throws ExoPlaybackException { - boolean drmInitDataChanged = - !Util.areEqual(newFormat.drmInitData, oldFormat == null ? null : oldFormat.drmInitData); - if (!drmInitDataChanged) { - return existingSourceSession; - } - @Nullable DrmSession newSourceDrmSession = null; - if (newFormat.drmInitData != null) { - if (drmSessionManager == null) { - throw ExoPlaybackException.createForRenderer( - new IllegalStateException("Media requires a DrmSessionManager"), getIndex()); - } - newSourceDrmSession = - drmSessionManager.acquireSession( - Assertions.checkNotNull(Looper.myLooper()), newFormat.drmInitData); - } - if (existingSourceSession != null) { - existingSourceSession.release(); - } - return newSourceDrmSession; - } - /** * Returns the index of the renderer within the player. */ @@ -333,6 +309,31 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities { return index; } + /** + * Creates an {@link ExoPlaybackException} of type {@link ExoPlaybackException#TYPE_RENDERER} for + * this renderer. + * + * @param cause The cause of the exception. + * @param format The current format used by the renderer. May be null. + */ + protected final ExoPlaybackException createRendererException( + Exception cause, @Nullable Format format) { + @FormatSupport int formatSupport = RendererCapabilities.FORMAT_HANDLED; + if (format != null && !throwRendererExceptionIsExecuting) { + // Prevent recursive re-entry from subclass supportsFormat implementations. + throwRendererExceptionIsExecuting = true; + try { + formatSupport = RendererCapabilities.getFormatSupport(supportsFormat(format)); + } catch (ExoPlaybackException e) { + // Ignore, we are already failing. + } finally { + throwRendererExceptionIsExecuting = false; + } + } + return ExoPlaybackException.createForRenderer( + cause, getName(), getIndex(), format, formatSupport); + } + /** * Reads from the enabled upstream source. If the upstream source has been read to the end then * {@link C#RESULT_BUFFER_READ} is only returned if {@link #setCurrentStreamFinal()} has been @@ -340,17 +341,17 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities { * * @param formatHolder A {@link FormatHolder} to populate in the case of reading a format. * @param buffer A {@link DecoderInputBuffer} to populate in the case of reading a sample or the - * end of the stream. If the end of the stream has been reached, the - * {@link C#BUFFER_FLAG_END_OF_STREAM} flag will be set on the buffer. + * end of the stream. If the end of the stream has been reached, the {@link + * C#BUFFER_FLAG_END_OF_STREAM} flag will be set on the buffer. * @param formatRequired Whether the caller requires that the format of the stream be read even if * it's not changing. A sample will never be read if set to true, however it is still possible * for the end of stream or nothing to be read. - * @return The result, which can be {@link C#RESULT_NOTHING_READ}, {@link C#RESULT_FORMAT_READ} or - * {@link C#RESULT_BUFFER_READ}. + * @return The status of read, one of {@link SampleStream.ReadDataResult}. */ - protected final int readSource(FormatHolder formatHolder, DecoderInputBuffer buffer, - boolean formatRequired) { - int result = stream.readData(formatHolder, buffer, formatRequired); + @SampleStream.ReadDataResult + protected final int readSource( + FormatHolder formatHolder, DecoderInputBuffer buffer, boolean formatRequired) { + @SampleStream.ReadDataResult int result = stream.readData(formatHolder, buffer, formatRequired); if (result == C.RESULT_BUFFER_READ) { if (buffer.isEndOfStream()) { readingPositionUs = C.TIME_END_OF_SOURCE; @@ -361,7 +362,11 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities { } else if (result == C.RESULT_FORMAT_READ) { Format format = formatHolder.format; if (format.subsampleOffsetUs != Format.OFFSET_SAMPLE_RELATIVE) { - format = format.copyWithSubsampleOffsetUs(format.subsampleOffsetUs + streamOffsetUs); + format = + format + .buildUpon() + .setSubsampleOffsetUs(format.subsampleOffsetUs + streamOffsetUs) + .build(); formatHolder.format = format; } } @@ -385,26 +390,4 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities { protected final boolean isSourceReady() { return hasReadStreamToEnd() ? streamIsFinal : stream.isReady(); } - - /** - * Returns whether {@code drmSessionManager} supports the specified {@code drmInitData}, or true - * if {@code drmInitData} is null. - * - * @param drmSessionManager The drm session manager. - * @param drmInitData {@link DrmInitData} of the format to check for support. - * @return Whether {@code drmSessionManager} supports the specified {@code drmInitData}, or - * true if {@code drmInitData} is null. - */ - protected static boolean supportsFormatDrm(@Nullable DrmSessionManager drmSessionManager, - @Nullable DrmInitData drmInitData) { - if (drmInitData == null) { - // Content is unencrypted. - return true; - } else if (drmSessionManager == null) { - // Content is encrypted, but no drm session manager is available. - return false; - } - return drmSessionManager.canAcquireSession(drmInitData); - } - } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ControlDispatcher.java b/library/core/src/main/java/com/google/android/exoplayer2/ControlDispatcher.java index f8749fc1a8..7b78147e12 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ControlDispatcher.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ControlDispatcher.java @@ -46,6 +46,38 @@ public interface ControlDispatcher { */ boolean dispatchSeekTo(Player player, int windowIndex, long positionMs); + /** + * Dispatches a {@link Player#previous()} operation. + * + * @param player The {@link Player} to which the operation should be dispatched. + * @return True if the operation was dispatched. False if suppressed. + */ + boolean dispatchPrevious(Player player); + + /** + * Dispatches a {@link Player#next()} operation. + * + * @param player The {@link Player} to which the operation should be dispatched. + * @return True if the operation was dispatched. False if suppressed. + */ + boolean dispatchNext(Player player); + + /** + * Dispatches a rewind operation. + * + * @param player The {@link Player} to which the operation should be dispatched. + * @return True if the operation was dispatched. False if suppressed. + */ + boolean dispatchRewind(Player player); + + /** + * Dispatches a fast forward operation. + * + * @param player The {@link Player} to which the operation should be dispatched. + * @return True if the operation was dispatched. False if suppressed. + */ + boolean dispatchFastForward(Player player); + /** * Dispatches a {@link Player#setRepeatMode(int)} operation. * @@ -72,4 +104,10 @@ public interface ControlDispatcher { * @return True if the operation was dispatched. False if suppressed. */ boolean dispatchStop(Player player, boolean reset); + + /** Returns {@code true} if rewind is enabled, {@code false} otherwise. */ + boolean isRewindEnabled(); + + /** Returns {@code true} if fast forward is enabled, {@code false} otherwise. */ + boolean isFastForwardEnabled(); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/DefaultControlDispatcher.java b/library/core/src/main/java/com/google/android/exoplayer2/DefaultControlDispatcher.java index df3ef36b88..7f24e6113f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/DefaultControlDispatcher.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/DefaultControlDispatcher.java @@ -15,14 +15,40 @@ */ package com.google.android.exoplayer2; -import com.google.android.exoplayer2.Player.RepeatMode; - -/** - * Default {@link ControlDispatcher} that dispatches all operations to the player without - * modification. - */ +/** Default {@link ControlDispatcher}. */ public class DefaultControlDispatcher implements ControlDispatcher { + /** The default fast forward increment, in milliseconds. */ + public static final int DEFAULT_FAST_FORWARD_MS = 15000; + /** The default rewind increment, in milliseconds. */ + public static final int DEFAULT_REWIND_MS = 5000; + + private static final int MAX_POSITION_FOR_SEEK_TO_PREVIOUS = 3000; + + private final Timeline.Window window; + + private long rewindIncrementMs; + private long fastForwardIncrementMs; + + /** Creates an instance. */ + public DefaultControlDispatcher() { + this(DEFAULT_FAST_FORWARD_MS, DEFAULT_REWIND_MS); + } + + /** + * Creates an instance with the given increments. + * + * @param fastForwardIncrementMs The fast forward increment in milliseconds. A non-positive value + * disables the fast forward operation. + * @param rewindIncrementMs The rewind increment in milliseconds. A non-positive value disables + * the rewind operation. + */ + public DefaultControlDispatcher(long fastForwardIncrementMs, long rewindIncrementMs) { + this.fastForwardIncrementMs = fastForwardIncrementMs; + this.rewindIncrementMs = rewindIncrementMs; + window = new Timeline.Window(); + } + @Override public boolean dispatchSetPlayWhenReady(Player player, boolean playWhenReady) { player.setPlayWhenReady(playWhenReady); @@ -36,7 +62,58 @@ public class DefaultControlDispatcher implements ControlDispatcher { } @Override - public boolean dispatchSetRepeatMode(Player player, @RepeatMode int repeatMode) { + public boolean dispatchPrevious(Player player) { + Timeline timeline = player.getCurrentTimeline(); + if (timeline.isEmpty() || player.isPlayingAd()) { + return true; + } + int windowIndex = player.getCurrentWindowIndex(); + timeline.getWindow(windowIndex, window); + int previousWindowIndex = player.getPreviousWindowIndex(); + if (previousWindowIndex != C.INDEX_UNSET + && (player.getCurrentPosition() <= MAX_POSITION_FOR_SEEK_TO_PREVIOUS + || (window.isDynamic && !window.isSeekable))) { + player.seekTo(previousWindowIndex, C.TIME_UNSET); + } else { + player.seekTo(windowIndex, /* positionMs= */ 0); + } + return true; + } + + @Override + public boolean dispatchNext(Player player) { + Timeline timeline = player.getCurrentTimeline(); + if (timeline.isEmpty() || player.isPlayingAd()) { + return true; + } + int windowIndex = player.getCurrentWindowIndex(); + int nextWindowIndex = player.getNextWindowIndex(); + if (nextWindowIndex != C.INDEX_UNSET) { + player.seekTo(nextWindowIndex, C.TIME_UNSET); + } else if (timeline.getWindow(windowIndex, window).isLive) { + player.seekTo(windowIndex, C.TIME_UNSET); + } + return true; + } + + @Override + public boolean dispatchRewind(Player player) { + if (isRewindEnabled() && player.isCurrentWindowSeekable()) { + seekToOffset(player, -rewindIncrementMs); + } + return true; + } + + @Override + public boolean dispatchFastForward(Player player) { + if (isFastForwardEnabled() && player.isCurrentWindowSeekable()) { + seekToOffset(player, fastForwardIncrementMs); + } + return true; + } + + @Override + public boolean dispatchSetRepeatMode(Player player, @Player.RepeatMode int repeatMode) { player.setRepeatMode(repeatMode); return true; } @@ -52,4 +129,54 @@ public class DefaultControlDispatcher implements ControlDispatcher { player.stop(reset); return true; } + + @Override + public boolean isRewindEnabled() { + return rewindIncrementMs > 0; + } + + @Override + public boolean isFastForwardEnabled() { + return fastForwardIncrementMs > 0; + } + + /** Returns the rewind increment in milliseconds. */ + public long getRewindIncrementMs() { + return rewindIncrementMs; + } + + /** Returns the fast forward increment in milliseconds. */ + public long getFastForwardIncrementMs() { + return fastForwardIncrementMs; + } + + /** + * @deprecated Create a new instance instead and pass the new instance to the UI component. This + * makes sure the UI gets updated and is in sync with the new values. + */ + @Deprecated + public void setRewindIncrementMs(long rewindMs) { + this.rewindIncrementMs = rewindMs; + } + + /** + * @deprecated Create a new instance instead and pass the new instance to the UI component. This + * makes sure the UI gets updated and is in sync with the new values. + */ + @Deprecated + public void setFastForwardIncrementMs(long fastForwardMs) { + this.fastForwardIncrementMs = fastForwardMs; + } + + // Internal methods. + + private static void seekToOffset(Player player, long offsetMs) { + long positionMs = player.getCurrentPosition() + offsetMs; + long durationMs = player.getDuration(); + if (durationMs != C.TIME_UNSET) { + positionMs = Math.min(positionMs, durationMs); + } + positionMs = Math.max(positionMs, 0); + player.seekTo(player.getCurrentWindowIndex(), positionMs); + } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/DefaultLoadControl.java b/library/core/src/main/java/com/google/android/exoplayer2/DefaultLoadControl.java index 1244b96d94..5eb14021a3 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/DefaultLoadControl.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/DefaultLoadControl.java @@ -20,6 +20,7 @@ import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.DefaultAllocator; import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.Util; /** @@ -29,14 +30,12 @@ public class DefaultLoadControl implements LoadControl { /** * The default minimum duration of media that the player will attempt to ensure is buffered at all - * times, in milliseconds. This value is only applied to playbacks without video. + * times, in milliseconds. */ - public static final int DEFAULT_MIN_BUFFER_MS = 15000; + public static final int DEFAULT_MIN_BUFFER_MS = 50000; /** * The default maximum duration of media that the player will attempt to buffer, in milliseconds. - * For playbacks with video, this is also the default minimum duration of media that the player - * will attempt to ensure is buffered. */ public static final int DEFAULT_MAX_BUFFER_MS = 50000; @@ -59,7 +58,7 @@ public class DefaultLoadControl implements LoadControl { public static final int DEFAULT_TARGET_BUFFER_BYTES = C.LENGTH_UNSET; /** The default prioritization of buffer time constraints over size constraints. */ - public static final boolean DEFAULT_PRIORITIZE_TIME_OVER_SIZE_THRESHOLDS = true; + public static final boolean DEFAULT_PRIORITIZE_TIME_OVER_SIZE_THRESHOLDS = false; /** The default back buffer duration in milliseconds. */ public static final int DEFAULT_BACK_BUFFER_DURATION_MS = 0; @@ -68,10 +67,10 @@ public class DefaultLoadControl implements LoadControl { public static final boolean DEFAULT_RETAIN_BACK_BUFFER_FROM_KEYFRAME = false; /** A default size in bytes for a video buffer. */ - public static final int DEFAULT_VIDEO_BUFFER_SIZE = 500 * C.DEFAULT_BUFFER_SEGMENT_SIZE; + public static final int DEFAULT_VIDEO_BUFFER_SIZE = 2000 * C.DEFAULT_BUFFER_SEGMENT_SIZE; /** A default size in bytes for an audio buffer. */ - public static final int DEFAULT_AUDIO_BUFFER_SIZE = 54 * C.DEFAULT_BUFFER_SEGMENT_SIZE; + public static final int DEFAULT_AUDIO_BUFFER_SIZE = 200 * C.DEFAULT_BUFFER_SEGMENT_SIZE; /** A default size in bytes for a text buffer. */ public static final int DEFAULT_TEXT_BUFFER_SIZE = 2 * C.DEFAULT_BUFFER_SEGMENT_SIZE; @@ -86,12 +85,17 @@ public class DefaultLoadControl implements LoadControl { public static final int DEFAULT_MUXED_BUFFER_SIZE = DEFAULT_VIDEO_BUFFER_SIZE + DEFAULT_AUDIO_BUFFER_SIZE + DEFAULT_TEXT_BUFFER_SIZE; + /** + * The buffer size in bytes that will be used as a minimum target buffer in all cases. This is + * also the default target buffer before tracks are selected. + */ + public static final int DEFAULT_MIN_BUFFER_SIZE = 200 * C.DEFAULT_BUFFER_SEGMENT_SIZE; + /** Builder for {@link DefaultLoadControl}. */ public static final class Builder { private DefaultAllocator allocator; - private int minBufferAudioMs; - private int minBufferVideoMs; + private int minBufferMs; private int maxBufferMs; private int bufferForPlaybackMs; private int bufferForPlaybackAfterRebufferMs; @@ -103,8 +107,7 @@ public class DefaultLoadControl implements LoadControl { /** Constructs a new instance. */ public Builder() { - minBufferAudioMs = DEFAULT_MIN_BUFFER_MS; - minBufferVideoMs = DEFAULT_MAX_BUFFER_MS; + minBufferMs = DEFAULT_MIN_BUFFER_MS; maxBufferMs = DEFAULT_MAX_BUFFER_MS; bufferForPlaybackMs = DEFAULT_BUFFER_FOR_PLAYBACK_MS; bufferForPlaybackAfterRebufferMs = DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS; @@ -158,8 +161,7 @@ public class DefaultLoadControl implements LoadControl { "minBufferMs", "bufferForPlaybackAfterRebufferMs"); assertGreaterOrEqual(maxBufferMs, minBufferMs, "maxBufferMs", "minBufferMs"); - this.minBufferAudioMs = minBufferMs; - this.minBufferVideoMs = minBufferMs; + this.minBufferMs = minBufferMs; this.maxBufferMs = maxBufferMs; this.bufferForPlaybackMs = bufferForPlaybackMs; this.bufferForPlaybackAfterRebufferMs = bufferForPlaybackAfterRebufferMs; @@ -222,8 +224,7 @@ public class DefaultLoadControl implements LoadControl { } return new DefaultLoadControl( allocator, - minBufferAudioMs, - minBufferVideoMs, + minBufferMs, maxBufferMs, bufferForPlaybackMs, bufferForPlaybackAfterRebufferMs, @@ -236,8 +237,7 @@ public class DefaultLoadControl implements LoadControl { private final DefaultAllocator allocator; - private final long minBufferAudioUs; - private final long minBufferVideoUs; + private final long minBufferUs; private final long maxBufferUs; private final long bufferForPlaybackUs; private final long bufferForPlaybackAfterRebufferUs; @@ -246,9 +246,8 @@ public class DefaultLoadControl implements LoadControl { private final long backBufferDurationUs; private final boolean retainBackBufferFromKeyframe; - private int targetBufferSize; + private int targetBufferBytes; private boolean isBuffering; - private boolean hasVideo; /** Constructs a new instance, using the {@code DEFAULT_*} constants defined in this class. */ @SuppressWarnings("deprecation") @@ -261,8 +260,7 @@ public class DefaultLoadControl implements LoadControl { public DefaultLoadControl(DefaultAllocator allocator) { this( allocator, - /* minBufferAudioMs= */ DEFAULT_MIN_BUFFER_MS, - /* minBufferVideoMs= */ DEFAULT_MAX_BUFFER_MS, + DEFAULT_MIN_BUFFER_MS, DEFAULT_MAX_BUFFER_MS, DEFAULT_BUFFER_FOR_PLAYBACK_MS, DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS, @@ -284,8 +282,7 @@ public class DefaultLoadControl implements LoadControl { boolean prioritizeTimeOverSizeThresholds) { this( allocator, - /* minBufferAudioMs= */ minBufferMs, - /* minBufferVideoMs= */ minBufferMs, + minBufferMs, maxBufferMs, bufferForPlaybackMs, bufferForPlaybackAfterRebufferMs, @@ -297,8 +294,7 @@ public class DefaultLoadControl implements LoadControl { protected DefaultLoadControl( DefaultAllocator allocator, - int minBufferAudioMs, - int minBufferVideoMs, + int minBufferMs, int maxBufferMs, int bufferForPlaybackMs, int bufferForPlaybackAfterRebufferMs, @@ -309,31 +305,25 @@ public class DefaultLoadControl implements LoadControl { assertGreaterOrEqual(bufferForPlaybackMs, 0, "bufferForPlaybackMs", "0"); assertGreaterOrEqual( bufferForPlaybackAfterRebufferMs, 0, "bufferForPlaybackAfterRebufferMs", "0"); + assertGreaterOrEqual(minBufferMs, bufferForPlaybackMs, "minBufferMs", "bufferForPlaybackMs"); assertGreaterOrEqual( - minBufferAudioMs, bufferForPlaybackMs, "minBufferAudioMs", "bufferForPlaybackMs"); - assertGreaterOrEqual( - minBufferVideoMs, bufferForPlaybackMs, "minBufferVideoMs", "bufferForPlaybackMs"); - assertGreaterOrEqual( - minBufferAudioMs, + minBufferMs, bufferForPlaybackAfterRebufferMs, - "minBufferAudioMs", + "minBufferMs", "bufferForPlaybackAfterRebufferMs"); - assertGreaterOrEqual( - minBufferVideoMs, - bufferForPlaybackAfterRebufferMs, - "minBufferVideoMs", - "bufferForPlaybackAfterRebufferMs"); - assertGreaterOrEqual(maxBufferMs, minBufferAudioMs, "maxBufferMs", "minBufferAudioMs"); - assertGreaterOrEqual(maxBufferMs, minBufferVideoMs, "maxBufferMs", "minBufferVideoMs"); + assertGreaterOrEqual(maxBufferMs, minBufferMs, "maxBufferMs", "minBufferMs"); assertGreaterOrEqual(backBufferDurationMs, 0, "backBufferDurationMs", "0"); this.allocator = allocator; - this.minBufferAudioUs = C.msToUs(minBufferAudioMs); - this.minBufferVideoUs = C.msToUs(minBufferVideoMs); + this.minBufferUs = C.msToUs(minBufferMs); this.maxBufferUs = C.msToUs(maxBufferMs); this.bufferForPlaybackUs = C.msToUs(bufferForPlaybackMs); this.bufferForPlaybackAfterRebufferUs = C.msToUs(bufferForPlaybackAfterRebufferMs); this.targetBufferBytesOverwrite = targetBufferBytes; + this.targetBufferBytes = + targetBufferBytesOverwrite != C.LENGTH_UNSET + ? targetBufferBytesOverwrite + : DEFAULT_MIN_BUFFER_SIZE; this.prioritizeTimeOverSizeThresholds = prioritizeTimeOverSizeThresholds; this.backBufferDurationUs = C.msToUs(backBufferDurationMs); this.retainBackBufferFromKeyframe = retainBackBufferFromKeyframe; @@ -347,12 +337,11 @@ public class DefaultLoadControl implements LoadControl { @Override public void onTracksSelected(Renderer[] renderers, TrackGroupArray trackGroups, TrackSelectionArray trackSelections) { - hasVideo = hasVideo(renderers, trackSelections); - targetBufferSize = + targetBufferBytes = targetBufferBytesOverwrite == C.LENGTH_UNSET - ? calculateTargetBufferSize(renderers, trackSelections) + ? calculateTargetBufferBytes(renderers, trackSelections) : targetBufferBytesOverwrite; - allocator.setTargetBufferSize(targetBufferSize); + allocator.setTargetBufferSize(targetBufferBytes); } @Override @@ -381,9 +370,10 @@ public class DefaultLoadControl implements LoadControl { } @Override - public boolean shouldContinueLoading(long bufferedDurationUs, float playbackSpeed) { - boolean targetBufferSizeReached = allocator.getTotalBytesAllocated() >= targetBufferSize; - long minBufferUs = hasVideo ? minBufferVideoUs : minBufferAudioUs; + public boolean shouldContinueLoading( + long playbackPositionUs, long bufferedDurationUs, float playbackSpeed) { + boolean targetBufferSizeReached = allocator.getTotalBytesAllocated() >= targetBufferBytes; + long minBufferUs = this.minBufferUs; if (playbackSpeed > 1) { // The playback speed is faster than real time, so scale up the minimum required media // duration to keep enough media buffered for a playout duration of minBufferUs. @@ -391,8 +381,15 @@ public class DefaultLoadControl implements LoadControl { Util.getMediaDurationForPlayoutDuration(minBufferUs, playbackSpeed); minBufferUs = Math.min(mediaDurationMinBufferUs, maxBufferUs); } + // Prevent playback from getting stuck if minBufferUs is too small. + minBufferUs = Math.max(minBufferUs, 500_000); if (bufferedDurationUs < minBufferUs) { isBuffering = prioritizeTimeOverSizeThresholds || !targetBufferSizeReached; + if (!isBuffering && bufferedDurationUs < 500_000) { + Log.w( + "DefaultLoadControl", + "Target buffer size reached with less than 500ms of buffered media data."); + } } else if (bufferedDurationUs >= maxBufferUs || targetBufferSizeReached) { isBuffering = false; } // Else don't change the buffering state @@ -407,7 +404,7 @@ public class DefaultLoadControl implements LoadControl { return minBufferDurationUs <= 0 || bufferedDurationUs >= minBufferDurationUs || (!prioritizeTimeOverSizeThresholds - && allocator.getTotalBytesAllocated() >= targetBufferSize); + && allocator.getTotalBytesAllocated() >= targetBufferBytes); } /** @@ -418,7 +415,7 @@ public class DefaultLoadControl implements LoadControl { * @param trackSelectionArray The selected tracks. * @return The target buffer size in bytes. */ - protected int calculateTargetBufferSize( + protected int calculateTargetBufferBytes( Renderer[] renderers, TrackSelectionArray trackSelectionArray) { int targetBufferSize = 0; for (int i = 0; i < renderers.length; i++) { @@ -426,11 +423,14 @@ public class DefaultLoadControl implements LoadControl { targetBufferSize += getDefaultBufferSize(renderers[i].getTrackType()); } } - return targetBufferSize; + return Math.max(DEFAULT_MIN_BUFFER_SIZE, targetBufferSize); } private void reset(boolean resetAllocator) { - targetBufferSize = 0; + targetBufferBytes = + targetBufferBytesOverwrite == C.LENGTH_UNSET + ? DEFAULT_MIN_BUFFER_SIZE + : targetBufferBytesOverwrite; isBuffering = false; if (resetAllocator) { allocator.reset(); @@ -458,15 +458,6 @@ public class DefaultLoadControl implements LoadControl { } } - private static boolean hasVideo(Renderer[] renderers, TrackSelectionArray trackSelectionArray) { - for (int i = 0; i < renderers.length; i++) { - if (renderers[i].getTrackType() == C.TRACK_TYPE_VIDEO && trackSelectionArray.get(i) != null) { - return true; - } - } - return false; - } - private static void assertGreaterOrEqual(int value1, int value2, String name1, String name2) { Assertions.checkArgument(value1 >= value2, name1 + " cannot be less than " + name2); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/DefaultMediaClock.java b/library/core/src/main/java/com/google/android/exoplayer2/DefaultMediaClock.java index 1971a4cefc..5700964967 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/DefaultMediaClock.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/DefaultMediaClock.java @@ -26,22 +26,20 @@ import com.google.android.exoplayer2.util.StandaloneMediaClock; */ /* package */ final class DefaultMediaClock implements MediaClock { - /** - * Listener interface to be notified of changes to the active playback parameters. - */ - public interface PlaybackParameterListener { + /** Listener interface to be notified of changes to the active playback speed. */ + public interface PlaybackSpeedListener { /** - * Called when the active playback parameters changed. Will not be called for {@link - * #setPlaybackParameters(PlaybackParameters)}. + * Called when the active playback speed changed. Will not be called for {@link + * #setPlaybackSpeed(float)}. * - * @param newPlaybackParameters The newly active {@link PlaybackParameters}. + * @param newPlaybackSpeed The newly active playback speed. */ - void onPlaybackParametersChanged(PlaybackParameters newPlaybackParameters); + void onPlaybackSpeedChanged(float newPlaybackSpeed); } private final StandaloneMediaClock standaloneClock; - private final PlaybackParameterListener listener; + private final PlaybackSpeedListener listener; @Nullable private Renderer rendererClockSource; @Nullable private MediaClock rendererClock; @@ -49,14 +47,13 @@ import com.google.android.exoplayer2.util.StandaloneMediaClock; private boolean standaloneClockIsStarted; /** - * Creates a new instance with listener for playback parameter changes and a {@link Clock} to use - * for the standalone clock implementation. + * Creates a new instance with listener for playback speed changes and a {@link Clock} to use for + * the standalone clock implementation. * - * @param listener A {@link PlaybackParameterListener} to listen for playback parameter - * changes. + * @param listener A {@link PlaybackSpeedListener} to listen for playback speed changes. * @param clock A {@link Clock}. */ - public DefaultMediaClock(PlaybackParameterListener listener, Clock clock) { + public DefaultMediaClock(PlaybackSpeedListener listener, Clock clock) { this.listener = listener; this.standaloneClock = new StandaloneMediaClock(clock); isUsingStandaloneClock = true; @@ -96,7 +93,7 @@ import com.google.android.exoplayer2.util.StandaloneMediaClock; * clock is already provided. */ public void onRendererEnabled(Renderer renderer) throws ExoPlaybackException { - MediaClock rendererMediaClock = renderer.getMediaClock(); + @Nullable MediaClock rendererMediaClock = renderer.getMediaClock(); if (rendererMediaClock != null && rendererMediaClock != rendererClock) { if (rendererClock != null) { throw ExoPlaybackException.createForUnexpected( @@ -104,7 +101,7 @@ import com.google.android.exoplayer2.util.StandaloneMediaClock; } this.rendererClock = rendererMediaClock; this.rendererClockSource = renderer; - rendererClock.setPlaybackParameters(standaloneClock.getPlaybackParameters()); + rendererClock.setPlaybackSpeed(standaloneClock.getPlaybackSpeed()); } } @@ -140,19 +137,19 @@ import com.google.android.exoplayer2.util.StandaloneMediaClock; } @Override - public void setPlaybackParameters(PlaybackParameters playbackParameters) { + public void setPlaybackSpeed(float playbackSpeed) { if (rendererClock != null) { - rendererClock.setPlaybackParameters(playbackParameters); - playbackParameters = rendererClock.getPlaybackParameters(); + rendererClock.setPlaybackSpeed(playbackSpeed); + playbackSpeed = rendererClock.getPlaybackSpeed(); } - standaloneClock.setPlaybackParameters(playbackParameters); + standaloneClock.setPlaybackSpeed(playbackSpeed); } @Override - public PlaybackParameters getPlaybackParameters() { + public float getPlaybackSpeed() { return rendererClock != null - ? rendererClock.getPlaybackParameters() - : standaloneClock.getPlaybackParameters(); + ? rendererClock.getPlaybackSpeed() + : standaloneClock.getPlaybackSpeed(); } private void syncClocks(boolean isReadingAhead) { @@ -177,10 +174,10 @@ import com.google.android.exoplayer2.util.StandaloneMediaClock; } // Continuously sync stand-alone clock to renderer clock so that it can take over if needed. standaloneClock.resetPosition(rendererClockPositionUs); - PlaybackParameters playbackParameters = rendererClock.getPlaybackParameters(); - if (!playbackParameters.equals(standaloneClock.getPlaybackParameters())) { - standaloneClock.setPlaybackParameters(playbackParameters); - listener.onPlaybackParametersChanged(playbackParameters); + float playbackSpeed = rendererClock.getPlaybackSpeed(); + if (playbackSpeed != standaloneClock.getPlaybackSpeed()) { + standaloneClock.setPlaybackSpeed(playbackSpeed); + listener.onPlaybackSpeedChanged(playbackSpeed); } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/DefaultRenderersFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/DefaultRenderersFactory.java index f53d72f598..a09f85d42f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/DefaultRenderersFactory.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/DefaultRenderersFactory.java @@ -20,14 +20,12 @@ import android.media.MediaCodec; import android.os.Handler; import android.os.Looper; import androidx.annotation.IntDef; -import androidx.annotation.Nullable; import com.google.android.exoplayer2.audio.AudioCapabilities; import com.google.android.exoplayer2.audio.AudioProcessor; import com.google.android.exoplayer2.audio.AudioRendererEventListener; import com.google.android.exoplayer2.audio.DefaultAudioSink; import com.google.android.exoplayer2.audio.MediaCodecAudioRenderer; -import com.google.android.exoplayer2.drm.DrmSessionManager; -import com.google.android.exoplayer2.drm.FrameworkMediaCrypto; +import com.google.android.exoplayer2.mediacodec.MediaCodecRenderer; import com.google.android.exoplayer2.mediacodec.MediaCodecSelector; import com.google.android.exoplayer2.metadata.MetadataOutput; import com.google.android.exoplayer2.metadata.MetadataRenderer; @@ -87,12 +85,11 @@ public class DefaultRenderersFactory implements RenderersFactory { protected static final int MAX_DROPPED_VIDEO_FRAME_COUNT_TO_NOTIFY = 50; private final Context context; - @Nullable private DrmSessionManager drmSessionManager; @ExtensionRendererMode private int extensionRendererMode; private long allowedVideoJoiningTimeMs; - private boolean playClearSamplesWithoutKeys; private boolean enableDecoderFallback; private MediaCodecSelector mediaCodecSelector; + private @MediaCodecRenderer.MediaCodecOperationMode int mediaCodecOperationMode; /** @param context A {@link Context}. */ public DefaultRenderersFactory(Context context) { @@ -100,17 +97,7 @@ public class DefaultRenderersFactory implements RenderersFactory { extensionRendererMode = EXTENSION_RENDERER_MODE_OFF; allowedVideoJoiningTimeMs = DEFAULT_ALLOWED_VIDEO_JOINING_TIME_MS; mediaCodecSelector = MediaCodecSelector.DEFAULT; - } - - /** - * @deprecated Use {@link #DefaultRenderersFactory(Context)} and pass {@link DrmSessionManager} - * directly to {@link SimpleExoPlayer.Builder}. - */ - @Deprecated - @SuppressWarnings("deprecation") - public DefaultRenderersFactory( - Context context, @Nullable DrmSessionManager drmSessionManager) { - this(context, drmSessionManager, EXTENSION_RENDERER_MODE_OFF); + mediaCodecOperationMode = MediaCodecRenderer.OPERATION_MODE_SYNCHRONOUS; } /** @@ -124,48 +111,18 @@ public class DefaultRenderersFactory implements RenderersFactory { this(context, extensionRendererMode, DEFAULT_ALLOWED_VIDEO_JOINING_TIME_MS); } - /** - * @deprecated Use {@link #DefaultRenderersFactory(Context)} and {@link - * #setExtensionRendererMode(int)}, and pass {@link DrmSessionManager} directly to {@link - * SimpleExoPlayer.Builder}. - */ - @Deprecated - @SuppressWarnings("deprecation") - public DefaultRenderersFactory( - Context context, - @Nullable DrmSessionManager drmSessionManager, - @ExtensionRendererMode int extensionRendererMode) { - this(context, drmSessionManager, extensionRendererMode, DEFAULT_ALLOWED_VIDEO_JOINING_TIME_MS); - } - /** * @deprecated Use {@link #DefaultRenderersFactory(Context)}, {@link * #setExtensionRendererMode(int)} and {@link #setAllowedVideoJoiningTimeMs(long)}. */ @Deprecated - @SuppressWarnings("deprecation") public DefaultRenderersFactory( Context context, @ExtensionRendererMode int extensionRendererMode, long allowedVideoJoiningTimeMs) { - this(context, null, extensionRendererMode, allowedVideoJoiningTimeMs); - } - - /** - * @deprecated Use {@link #DefaultRenderersFactory(Context)}, {@link - * #setExtensionRendererMode(int)} and {@link #setAllowedVideoJoiningTimeMs(long)}, and pass - * {@link DrmSessionManager} directly to {@link SimpleExoPlayer.Builder}. - */ - @Deprecated - public DefaultRenderersFactory( - Context context, - @Nullable DrmSessionManager drmSessionManager, - @ExtensionRendererMode int extensionRendererMode, - long allowedVideoJoiningTimeMs) { this.context = context; this.extensionRendererMode = extensionRendererMode; this.allowedVideoJoiningTimeMs = allowedVideoJoiningTimeMs; - this.drmSessionManager = drmSessionManager; mediaCodecSelector = MediaCodecSelector.DEFAULT; } @@ -186,21 +143,17 @@ public class DefaultRenderersFactory implements RenderersFactory { } /** - * Sets whether renderers are permitted to play clear regions of encrypted media prior to having - * obtained the keys necessary to decrypt encrypted regions of the media. For encrypted media that - * starts with a short clear region, this allows playback to begin in parallel with key - * acquisition, which can reduce startup latency. + * Set the {@link MediaCodecRenderer.MediaCodecOperationMode} of {@link MediaCodecRenderer} + * instances. * - *

The default value is {@code false}. + *

This method is experimental, and will be renamed or removed in a future release. * - * @param playClearSamplesWithoutKeys Whether renderers are permitted to play clear regions of - * encrypted media prior to having obtained the keys necessary to decrypt encrypted regions of - * the media. + * @param mode The {@link MediaCodecRenderer.MediaCodecOperationMode} to set. * @return This factory, for convenience. */ - public DefaultRenderersFactory setPlayClearSamplesWithoutKeys( - boolean playClearSamplesWithoutKeys) { - this.playClearSamplesWithoutKeys = playClearSamplesWithoutKeys; + public DefaultRenderersFactory experimental_setMediaCodecOperationMode( + @MediaCodecRenderer.MediaCodecOperationMode int mode) { + mediaCodecOperationMode = mode; return this; } @@ -251,18 +204,12 @@ public class DefaultRenderersFactory implements RenderersFactory { VideoRendererEventListener videoRendererEventListener, AudioRendererEventListener audioRendererEventListener, TextOutput textRendererOutput, - MetadataOutput metadataRendererOutput, - @Nullable DrmSessionManager drmSessionManager) { - if (drmSessionManager == null) { - drmSessionManager = this.drmSessionManager; - } + MetadataOutput metadataRendererOutput) { ArrayList renderersList = new ArrayList<>(); buildVideoRenderers( context, extensionRendererMode, mediaCodecSelector, - drmSessionManager, - playClearSamplesWithoutKeys, enableDecoderFallback, eventHandler, videoRendererEventListener, @@ -272,8 +219,6 @@ public class DefaultRenderersFactory implements RenderersFactory { context, extensionRendererMode, mediaCodecSelector, - drmSessionManager, - playClearSamplesWithoutKeys, enableDecoderFallback, buildAudioProcessors(), eventHandler, @@ -294,11 +239,6 @@ public class DefaultRenderersFactory implements RenderersFactory { * @param context The {@link Context} associated with the player. * @param extensionRendererMode The extension renderer mode. * @param mediaCodecSelector A decoder selector. - * @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the player will - * not be used for DRM protected playbacks. - * @param playClearSamplesWithoutKeys Whether renderers are permitted to play clear regions of - * encrypted media prior to having obtained the keys necessary to decrypt encrypted regions of - * the media. * @param enableDecoderFallback Whether to enable fallback to lower-priority decoders if decoder * initialization fails. This may result in using a decoder that is slower/less efficient than * the primary decoder. @@ -312,24 +252,22 @@ public class DefaultRenderersFactory implements RenderersFactory { Context context, @ExtensionRendererMode int extensionRendererMode, MediaCodecSelector mediaCodecSelector, - @Nullable DrmSessionManager drmSessionManager, - boolean playClearSamplesWithoutKeys, boolean enableDecoderFallback, Handler eventHandler, VideoRendererEventListener eventListener, long allowedVideoJoiningTimeMs, ArrayList out) { - out.add( + MediaCodecVideoRenderer videoRenderer = new MediaCodecVideoRenderer( context, mediaCodecSelector, allowedVideoJoiningTimeMs, - drmSessionManager, - playClearSamplesWithoutKeys, enableDecoderFallback, eventHandler, eventListener, - MAX_DROPPED_VIDEO_FRAME_COUNT_TO_NOTIFY)); + MAX_DROPPED_VIDEO_FRAME_COUNT_TO_NOTIFY); + videoRenderer.experimental_setMediaCodecOperationMode(mediaCodecOperationMode); + out.add(videoRenderer); if (extensionRendererMode == EXTENSION_RENDERER_MODE_OFF) { return; @@ -392,6 +330,34 @@ public class DefaultRenderersFactory implements RenderersFactory { // The extension is present, but instantiation failed. throw new RuntimeException("Error instantiating AV1 extension", e); } + + try { + // Full class names used for constructor args so the LINT rule triggers if any of them move. + // LINT.IfChange + Class clazz = + Class.forName("com.google.android.exoplayer2.ext.ffmpeg.FfmpegVideoRenderer"); + Constructor constructor = + clazz.getConstructor( + long.class, + android.os.Handler.class, + com.google.android.exoplayer2.video.VideoRendererEventListener.class, + int.class); + // LINT.ThenChange(../../../../../../../proguard-rules.txt) + Renderer renderer = + (Renderer) + constructor.newInstance( + allowedVideoJoiningTimeMs, + eventHandler, + eventListener, + MAX_DROPPED_VIDEO_FRAME_COUNT_TO_NOTIFY); + out.add(extensionRendererIndex++, renderer); + Log.i(TAG, "Loaded FfmpegVideoRenderer."); + } catch (ClassNotFoundException e) { + // Expected if the app was built without the extension. + } catch (Exception e) { + // The extension is present, but instantiation failed. + throw new RuntimeException("Error instantiating FFmpeg extension", e); + } } /** @@ -400,11 +366,6 @@ public class DefaultRenderersFactory implements RenderersFactory { * @param context The {@link Context} associated with the player. * @param extensionRendererMode The extension renderer mode. * @param mediaCodecSelector A decoder selector. - * @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the player will - * not be used for DRM protected playbacks. - * @param playClearSamplesWithoutKeys Whether renderers are permitted to play clear regions of - * encrypted media prior to having obtained the keys necessary to decrypt encrypted regions of - * the media. * @param enableDecoderFallback Whether to enable fallback to lower-priority decoders if decoder * initialization fails. This may result in using a decoder that is slower/less efficient than * the primary decoder. @@ -418,23 +379,21 @@ public class DefaultRenderersFactory implements RenderersFactory { Context context, @ExtensionRendererMode int extensionRendererMode, MediaCodecSelector mediaCodecSelector, - @Nullable DrmSessionManager drmSessionManager, - boolean playClearSamplesWithoutKeys, boolean enableDecoderFallback, AudioProcessor[] audioProcessors, Handler eventHandler, AudioRendererEventListener eventListener, ArrayList out) { - out.add( + MediaCodecAudioRenderer audioRenderer = new MediaCodecAudioRenderer( context, mediaCodecSelector, - drmSessionManager, - playClearSamplesWithoutKeys, enableDecoderFallback, eventHandler, eventListener, - new DefaultAudioSink(AudioCapabilities.getCapabilities(context), audioProcessors))); + new DefaultAudioSink(AudioCapabilities.getCapabilities(context), audioProcessors)); + audioRenderer.experimental_setMediaCodecOperationMode(mediaCodecOperationMode); + out.add(audioRenderer); if (extensionRendererMode == EXTENSION_RENDERER_MODE_OFF) { return; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlaybackException.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlaybackException.java index 49aacd9638..cd9662a251 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlaybackException.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlaybackException.java @@ -16,8 +16,10 @@ package com.google.android.exoplayer2; import android.os.SystemClock; +import android.text.TextUtils; import androidx.annotation.IntDef; import androidx.annotation.Nullable; +import com.google.android.exoplayer2.RendererCapabilities.FormatSupport; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.util.Assertions; import java.io.IOException; @@ -69,11 +71,25 @@ public final class ExoPlaybackException extends Exception { /** The {@link Type} of the playback failure. */ @Type public final int type; - /** - * If {@link #type} is {@link #TYPE_RENDERER}, this is the index of the renderer. - */ + /** If {@link #type} is {@link #TYPE_RENDERER}, this is the name of the renderer. */ + @Nullable public final String rendererName; + + /** If {@link #type} is {@link #TYPE_RENDERER}, this is the index of the renderer. */ public final int rendererIndex; + /** + * If {@link #type} is {@link #TYPE_RENDERER}, this is the {@link Format} the renderer was using + * at the time of the exception, or null if the renderer wasn't using a {@link Format}. + */ + @Nullable public final Format rendererFormat; + + /** + * If {@link #type} is {@link #TYPE_RENDERER}, this is the level of {@link FormatSupport} of the + * renderer for {@link #rendererFormat}. If {@link #rendererFormat} is null, this is {@link + * RendererCapabilities#FORMAT_HANDLED}. + */ + @FormatSupport public final int rendererFormatSupport; + /** The value of {@link SystemClock#elapsedRealtime()} when this exception was created. */ public final long timestampMs; @@ -86,7 +102,7 @@ public final class ExoPlaybackException extends Exception { * @return The created instance. */ public static ExoPlaybackException createForSource(IOException cause) { - return new ExoPlaybackException(TYPE_SOURCE, cause, /* rendererIndex= */ C.INDEX_UNSET); + return new ExoPlaybackException(TYPE_SOURCE, cause); } /** @@ -94,10 +110,26 @@ public final class ExoPlaybackException extends Exception { * * @param cause The cause of the failure. * @param rendererIndex The index of the renderer in which the failure occurred. + * @param rendererFormat The {@link Format} the renderer was using at the time of the exception, + * or null if the renderer wasn't using a {@link Format}. + * @param rendererFormatSupport The {@link FormatSupport} of the renderer for {@code + * rendererFormat}. Ignored if {@code rendererFormat} is null. * @return The created instance. */ - public static ExoPlaybackException createForRenderer(Exception cause, int rendererIndex) { - return new ExoPlaybackException(TYPE_RENDERER, cause, rendererIndex); + public static ExoPlaybackException createForRenderer( + Exception cause, + String rendererName, + int rendererIndex, + @Nullable Format rendererFormat, + @FormatSupport int rendererFormatSupport) { + return new ExoPlaybackException( + TYPE_RENDERER, + cause, + /* customMessage= */ null, + rendererName, + rendererIndex, + rendererFormat, + rendererFormat == null ? RendererCapabilities.FORMAT_HANDLED : rendererFormatSupport); } /** @@ -107,7 +139,7 @@ public final class ExoPlaybackException extends Exception { * @return The created instance. */ public static ExoPlaybackException createForUnexpected(RuntimeException cause) { - return new ExoPlaybackException(TYPE_UNEXPECTED, cause, /* rendererIndex= */ C.INDEX_UNSET); + return new ExoPlaybackException(TYPE_UNEXPECTED, cause); } /** @@ -127,22 +159,54 @@ public final class ExoPlaybackException extends Exception { * @return The created instance. */ public static ExoPlaybackException createForOutOfMemoryError(OutOfMemoryError cause) { - return new ExoPlaybackException(TYPE_OUT_OF_MEMORY, cause, /* rendererIndex= */ C.INDEX_UNSET); + return new ExoPlaybackException(TYPE_OUT_OF_MEMORY, cause); } - private ExoPlaybackException(@Type int type, Throwable cause, int rendererIndex) { - super(cause); - this.type = type; - this.cause = cause; - this.rendererIndex = rendererIndex; - timestampMs = SystemClock.elapsedRealtime(); + private ExoPlaybackException(@Type int type, Throwable cause) { + this( + type, + cause, + /* customMessage= */ null, + /* rendererName= */ null, + /* rendererIndex= */ C.INDEX_UNSET, + /* rendererFormat= */ null, + /* rendererFormatSupport= */ RendererCapabilities.FORMAT_HANDLED); } private ExoPlaybackException(@Type int type, String message) { - super(message); + this( + type, + /* cause= */ null, + /* customMessage= */ message, + /* rendererName= */ null, + /* rendererIndex= */ C.INDEX_UNSET, + /* rendererFormat= */ null, + /* rendererFormatSupport= */ RendererCapabilities.FORMAT_HANDLED); + } + + private ExoPlaybackException( + @Type int type, + @Nullable Throwable cause, + @Nullable String customMessage, + @Nullable String rendererName, + int rendererIndex, + @Nullable Format rendererFormat, + @FormatSupport int rendererFormatSupport) { + super( + deriveMessage( + type, + customMessage, + rendererName, + rendererIndex, + rendererFormat, + rendererFormatSupport), + cause); this.type = type; - rendererIndex = C.INDEX_UNSET; - cause = null; + this.cause = cause; + this.rendererName = rendererName; + this.rendererIndex = rendererIndex; + this.rendererFormat = rendererFormat; + this.rendererFormatSupport = rendererFormatSupport; timestampMs = SystemClock.elapsedRealtime(); } @@ -185,4 +249,45 @@ public final class ExoPlaybackException extends Exception { Assertions.checkState(type == TYPE_OUT_OF_MEMORY); return (OutOfMemoryError) Assertions.checkNotNull(cause); } + + @Nullable + private static String deriveMessage( + @Type int type, + @Nullable String customMessage, + @Nullable String rendererName, + int rendererIndex, + @Nullable Format rendererFormat, + @FormatSupport int rendererFormatSupport) { + @Nullable String message; + switch (type) { + case TYPE_SOURCE: + message = "Source error"; + break; + case TYPE_RENDERER: + message = + rendererName + + " error" + + ", index=" + + rendererIndex + + ", format=" + + rendererFormat + + ", format_supported=" + + RendererCapabilities.getFormatSupportString(rendererFormatSupport); + break; + case TYPE_REMOTE: + message = "Remote error"; + break; + case TYPE_OUT_OF_MEMORY: + message = "Out of memory error"; + break; + case TYPE_UNEXPECTED: + default: + message = "Unexpected runtime error"; + break; + } + if (!TextUtils.isEmpty(customMessage)) { + message += ": " + customMessage; + } + return message; + } } 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 4d947e27cf..d779037817 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 @@ -24,10 +24,13 @@ import com.google.android.exoplayer2.audio.MediaCodecAudioRenderer; import com.google.android.exoplayer2.metadata.MetadataRenderer; import com.google.android.exoplayer2.source.ClippingMediaSource; import com.google.android.exoplayer2.source.ConcatenatingMediaSource; +import com.google.android.exoplayer2.source.DefaultMediaSourceFactory; import com.google.android.exoplayer2.source.LoopingMediaSource; import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.source.MediaSourceFactory; import com.google.android.exoplayer2.source.MergingMediaSource; import com.google.android.exoplayer2.source.ProgressiveMediaSource; +import com.google.android.exoplayer2.source.ShuffleOrder; import com.google.android.exoplayer2.source.SingleSampleMediaSource; import com.google.android.exoplayer2.text.TextRenderer; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; @@ -39,6 +42,7 @@ import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Clock; import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.video.MediaCodecVideoRenderer; +import java.util.List; /** * An extensible media player that plays {@link MediaSource}s. Instances can be obtained from {@link @@ -93,8 +97,8 @@ import com.google.android.exoplayer2.video.MediaCodecVideoRenderer; * *

The figure below shows ExoPlayer's threading model. * - *

ExoPlayer's threading
- * model + *

ExoPlayer's
+ * threading model * *

    *
  • ExoPlayer instances must be accessed from a single application thread. For the vast @@ -136,14 +140,16 @@ public interface ExoPlayer extends Player { private Clock clock; private TrackSelector trackSelector; + private MediaSourceFactory mediaSourceFactory; private LoadControl loadControl; private BandwidthMeter bandwidthMeter; private Looper looper; - private AnalyticsCollector analyticsCollector; + @Nullable private AnalyticsCollector analyticsCollector; private boolean useLazyPreparation; private boolean buildCalled; private long releaseTimeoutMs; + private boolean throwWhenStuckBuffering; /** * Creates a builder with a list of {@link Renderer Renderers}. @@ -152,6 +158,7 @@ public interface ExoPlayer extends Player { * *
      *
    • {@link TrackSelector}: {@link DefaultTrackSelector} + *
    • {@link MediaSourceFactory}: {@link DefaultMediaSourceFactory} *
    • {@link LoadControl}: {@link DefaultLoadControl} *
    • {@link BandwidthMeter}: {@link DefaultBandwidthMeter#getSingletonInstance(Context)} *
    • {@link Looper}: The {@link Looper} associated with the current thread, or the {@link @@ -169,10 +176,11 @@ public interface ExoPlayer extends Player { this( renderers, new DefaultTrackSelector(context), + DefaultMediaSourceFactory.newInstance(context), new DefaultLoadControl(), DefaultBandwidthMeter.getSingletonInstance(context), Util.getLooper(), - new AnalyticsCollector(Clock.DEFAULT), + /* analyticsCollector= */ null, /* useLazyPreparation= */ true, Clock.DEFAULT); } @@ -186,6 +194,7 @@ public interface ExoPlayer extends Player { * * @param renderers The {@link Renderer Renderers} to be used by the player. * @param trackSelector A {@link TrackSelector}. + * @param mediaSourceFactory A {@link MediaSourceFactory}. * @param loadControl A {@link LoadControl}. * @param bandwidthMeter A {@link BandwidthMeter}. * @param looper A {@link Looper} that must be used for all calls to the player. @@ -196,15 +205,17 @@ public interface ExoPlayer extends Player { public Builder( Renderer[] renderers, TrackSelector trackSelector, + MediaSourceFactory mediaSourceFactory, LoadControl loadControl, BandwidthMeter bandwidthMeter, Looper looper, - AnalyticsCollector analyticsCollector, + @Nullable AnalyticsCollector analyticsCollector, boolean useLazyPreparation, Clock clock) { Assertions.checkArgument(renderers.length > 0); this.renderers = renderers; this.trackSelector = trackSelector; + this.mediaSourceFactory = mediaSourceFactory; this.loadControl = loadControl; this.bandwidthMeter = bandwidthMeter; this.looper = looper; @@ -218,8 +229,7 @@ public interface ExoPlayer extends Player { * ExoPlayer#release()} takes more than {@code timeoutMs} milliseconds to complete, the player * will raise an error via {@link Player.EventListener#onPlayerError}. * - *

      This method is experimental, and will be renamed or removed in a future release. It should - * only be called before the player is used. + *

      This method is experimental, and will be renamed or removed in a future release. * * @param timeoutMs The time limit in milliseconds, or 0 for no limit. */ @@ -228,6 +238,19 @@ public interface ExoPlayer extends Player { return this; } + /** + * Sets whether the player should throw when it detects it's stuck buffering. + * + *

      This method is experimental, and will be renamed or removed in a future release. + * + * @param throwWhenStuckBuffering Whether to throw when the player detects it's stuck buffering. + * @return This builder. + */ + public Builder experimental_setThrowWhenStuckBuffering(boolean throwWhenStuckBuffering) { + this.throwWhenStuckBuffering = throwWhenStuckBuffering; + return this; + } + /** * Sets the {@link TrackSelector} that will be used by the player. * @@ -241,6 +264,19 @@ public interface ExoPlayer extends Player { return this; } + /** + * Sets the {@link MediaSourceFactory} that will be used by the player. + * + * @param mediaSourceFactory A {@link MediaSourceFactory}. + * @return This builder. + * @throws IllegalStateException If {@link #build()} has already been called. + */ + public Builder setMediaSourceFactory(MediaSourceFactory mediaSourceFactory) { + Assertions.checkState(!buildCalled); + this.mediaSourceFactory = mediaSourceFactory; + return this; + } + /** * Sets the {@link LoadControl} that will be used by the player. * @@ -329,17 +365,29 @@ public interface ExoPlayer extends Player { /** * Builds an {@link ExoPlayer} instance. * - * @throws IllegalStateException If {@link #build()} has already been called. + * @throws IllegalStateException If {@code build} has already been called. */ public ExoPlayer build() { Assertions.checkState(!buildCalled); buildCalled = true; ExoPlayerImpl player = - new ExoPlayerImpl(renderers, trackSelector, loadControl, bandwidthMeter, clock, looper); + new ExoPlayerImpl( + renderers, + trackSelector, + mediaSourceFactory, + loadControl, + bandwidthMeter, + analyticsCollector, + useLazyPreparation, + clock, + looper); if (releaseTimeoutMs > 0) { player.experimental_setReleaseTimeoutMs(releaseTimeoutMs); } + if (throwWhenStuckBuffering) { + player.experimental_throwWhenStuckBuffering(); + } return player; } @@ -348,56 +396,113 @@ public interface ExoPlayer extends Player { /** Returns the {@link Looper} associated with the playback thread. */ Looper getPlaybackLooper(); - /** - * Retries a failed or stopped playback. Does nothing if the player has been reset, or if playback - * has not failed or been stopped. - */ + /** @deprecated Use {@link #prepare()} instead. */ + @Deprecated void retry(); - /** Prepares the player. */ - void prepare(); - - /** - * @deprecated Use {@code setMediaItem(mediaSource, C.TIME_UNSET)} and {@link #prepare()} instead. - */ + /** @deprecated Use {@link #setMediaSource(MediaSource)} and {@link #prepare()} instead. */ @Deprecated void prepare(MediaSource mediaSource); - /** @deprecated Use {@link #setMediaItem(MediaSource, long)} and {@link #prepare()} instead. */ + /** + * @deprecated Use {@link #setMediaSource(MediaSource, boolean)} and {@link #prepare()} instead. + */ @Deprecated void prepare(MediaSource mediaSource, boolean resetPosition, boolean resetState); /** - * Sets the specified {@link MediaSource}. + * Clears the playlist, adds the specified {@link MediaSource MediaSources} and resets the + * position to the default position. * - *

      Note: This is an intermediate implementation towards a larger change. Until then {@link - * #prepare()} has to be called immediately after calling this method. - * - * @param mediaItem The new {@link MediaSource}. + * @param mediaSources The new {@link MediaSource MediaSources}. */ - void setMediaItem(MediaSource mediaItem); + void setMediaSources(List mediaSources); /** - * Sets the specified {@link MediaSource}. + * Clears the playlist and adds the specified {@link MediaSource MediaSources}. * - *

      Note: This is an intermediate implementation towards a larger change. Until then {@link - * #prepare()} has to be called immediately after calling this method. + * @param mediaSources The new {@link MediaSource MediaSources}. + * @param resetPosition Whether the playback position should be reset to the default position in + * the first {@link Timeline.Window}. If false, playback will start from the position defined + * by {@link #getCurrentWindowIndex()} and {@link #getCurrentPosition()}. + */ + void setMediaSources(List mediaSources, boolean resetPosition); + + /** + * Clears the playlist and adds the specified {@link MediaSource MediaSources}. * - *

      This intermediate implementation calls {@code stop(true)} before seeking to avoid seeking in - * a media item that has been set previously. It is equivalent with calling + * @param mediaSources The new {@link MediaSource MediaSources}. + * @param startWindowIndex The window index to start playback from. If {@link C#INDEX_UNSET} is + * passed, the current position is not reset. + * @param startPositionMs The position in milliseconds to start playback from. If {@link + * C#TIME_UNSET} is passed, the default position of the given window is used. In any case, if + * {@code startWindowIndex} is set to {@link C#INDEX_UNSET}, this parameter is ignored and the + * position is not reset at all. + */ + void setMediaSources(List mediaSources, int startWindowIndex, long startPositionMs); + + /** + * Clears the playlist, adds the specified {@link MediaSource} and resets the position to the + * default position. * - *

      
      -   *   if (!getCurrentTimeline().isEmpty()) {
      -   *     player.stop(true);
      -   *   }
      -   *   player.seekTo(0, startPositionMs);
      -   *   player.setMediaItem(mediaItem);
      -   * 
      + * @param mediaSource The new {@link MediaSource}. + */ + void setMediaSource(MediaSource mediaSource); + + /** + * Clears the playlist and adds the specified {@link MediaSource}. * - * @param mediaItem The new {@link MediaSource}. + * @param mediaSource The new {@link MediaSource}. * @param startPositionMs The position in milliseconds to start playback from. */ - void setMediaItem(MediaSource mediaItem, long startPositionMs); + void setMediaSource(MediaSource mediaSource, long startPositionMs); + + /** + * Clears the playlist and adds the specified {@link MediaSource}. + * + * @param mediaSource The new {@link MediaSource}. + * @param resetPosition Whether the playback position should be reset to the default position. If + * false, playback will start from the position defined by {@link #getCurrentWindowIndex()} + * and {@link #getCurrentPosition()}. + */ + void setMediaSource(MediaSource mediaSource, boolean resetPosition); + + /** + * Adds a media source to the end of the playlist. + * + * @param mediaSource The {@link MediaSource} to add. + */ + void addMediaSource(MediaSource mediaSource); + + /** + * Adds a media source at the given index of the playlist. + * + * @param index The index at which to add the source. + * @param mediaSource The {@link MediaSource} to add. + */ + void addMediaSource(int index, MediaSource mediaSource); + + /** + * Adds a list of media sources to the end of the playlist. + * + * @param mediaSources The {@link MediaSource MediaSources} to add. + */ + void addMediaSources(List mediaSources); + + /** + * Adds a list of media sources at the given index of the playlist. + * + * @param index The index at which to add the media sources. + * @param mediaSources The {@link MediaSource MediaSources} to add. + */ + void addMediaSources(int index, List mediaSources); + + /** + * Sets the shuffle order. + * + * @param shuffleOrder The shuffle order. + */ + void setShuffleOrder(ShuffleOrder shuffleOrder); /** * Creates a message that can be sent to a {@link PlayerMessage.Target}. By default, the message @@ -449,4 +554,23 @@ public interface ExoPlayer extends Player { * idle state. */ void setForegroundMode(boolean foregroundMode); + + /** + * Sets whether to pause playback at the end of each media item. + * + *

      This means the player will pause at the end of each window in the current {@link + * #getCurrentTimeline() timeline}. Listeners will be informed by a call to {@link + * Player.EventListener#onPlayWhenReadyChanged(boolean, int)} with the reason {@link + * Player#PLAY_WHEN_READY_CHANGE_REASON_END_OF_MEDIA_ITEM} when this happens. + * + * @param pauseAtEndOfMediaItems Whether to pause playback at the end of each media item. + */ + void setPauseAtEndOfMediaItems(boolean pauseAtEndOfMediaItems); + + /** + * Returns whether the player pauses playback at the end of each media item. + * + * @see #setPauseAtEndOfMediaItems(boolean) + */ + boolean getPauseAtEndOfMediaItems(); } 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 e4f239df77..32d00d90c1 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 @@ -17,11 +17,8 @@ package com.google.android.exoplayer2; import android.content.Context; import android.os.Looper; -import androidx.annotation.Nullable; import com.google.android.exoplayer2.analytics.AnalyticsCollector; -import com.google.android.exoplayer2.drm.DrmSessionManager; -import com.google.android.exoplayer2.drm.FrameworkMediaCrypto; -import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.source.DefaultMediaSourceFactory; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import com.google.android.exoplayer2.trackselection.TrackSelector; import com.google.android.exoplayer2.upstream.BandwidthMeter; @@ -35,45 +32,33 @@ public final class ExoPlayerFactory { private ExoPlayerFactory() {} - /** - * @deprecated Use {@link SimpleExoPlayer.Builder} instead. The {@link DrmSessionManager} cannot - * be passed to {@link SimpleExoPlayer.Builder} and should instead be injected into the {@link - * MediaSource} factories. - */ + /** @deprecated Use {@link SimpleExoPlayer.Builder} instead. */ @Deprecated @SuppressWarnings("deprecation") public static SimpleExoPlayer newSimpleInstance( Context context, TrackSelector trackSelector, LoadControl loadControl, - @Nullable DrmSessionManager drmSessionManager, @DefaultRenderersFactory.ExtensionRendererMode int extensionRendererMode) { RenderersFactory renderersFactory = new DefaultRenderersFactory(context).setExtensionRendererMode(extensionRendererMode); - return newSimpleInstance( - context, renderersFactory, trackSelector, loadControl, drmSessionManager); + return newSimpleInstance(context, renderersFactory, trackSelector, loadControl); } - /** - * @deprecated Use {@link SimpleExoPlayer.Builder} instead. The {@link DrmSessionManager} cannot - * be passed to {@link SimpleExoPlayer.Builder} and should instead be injected into the {@link - * MediaSource} factories. - */ + /** @deprecated Use {@link SimpleExoPlayer.Builder} instead. */ @Deprecated @SuppressWarnings("deprecation") public static SimpleExoPlayer newSimpleInstance( Context context, TrackSelector trackSelector, LoadControl loadControl, - @Nullable DrmSessionManager drmSessionManager, @DefaultRenderersFactory.ExtensionRendererMode int extensionRendererMode, long allowedVideoJoiningTimeMs) { RenderersFactory renderersFactory = new DefaultRenderersFactory(context) .setExtensionRendererMode(extensionRendererMode) .setAllowedVideoJoiningTimeMs(allowedVideoJoiningTimeMs); - return newSimpleInstance( - context, renderersFactory, trackSelector, loadControl, drmSessionManager); + return newSimpleInstance(context, renderersFactory, trackSelector, loadControl); } /** @deprecated Use {@link SimpleExoPlayer.Builder} instead. */ @@ -107,39 +92,6 @@ public final class ExoPlayerFactory { return newSimpleInstance(context, renderersFactory, trackSelector, loadControl); } - /** - * @deprecated Use {@link SimpleExoPlayer.Builder} instead. The {@link DrmSessionManager} cannot - * be passed to {@link SimpleExoPlayer.Builder} and should instead be injected into the {@link - * MediaSource} factories. - */ - @Deprecated - @SuppressWarnings("deprecation") - public static SimpleExoPlayer newSimpleInstance( - Context context, - TrackSelector trackSelector, - LoadControl loadControl, - @Nullable DrmSessionManager drmSessionManager) { - RenderersFactory renderersFactory = new DefaultRenderersFactory(context); - return newSimpleInstance( - context, renderersFactory, trackSelector, loadControl, drmSessionManager); - } - - /** - * @deprecated Use {@link SimpleExoPlayer.Builder} instead. The {@link DrmSessionManager} cannot - * be passed to {@link SimpleExoPlayer.Builder} and should instead be injected into the {@link - * MediaSource} factories. - */ - @Deprecated - @SuppressWarnings("deprecation") - public static SimpleExoPlayer newSimpleInstance( - Context context, - RenderersFactory renderersFactory, - TrackSelector trackSelector, - @Nullable DrmSessionManager drmSessionManager) { - return newSimpleInstance( - context, renderersFactory, trackSelector, new DefaultLoadControl(), drmSessionManager); - } - /** @deprecated Use {@link SimpleExoPlayer.Builder} instead. */ @Deprecated @SuppressWarnings("deprecation") @@ -153,15 +105,10 @@ public final class ExoPlayerFactory { renderersFactory, trackSelector, loadControl, - /* drmSessionManager= */ null, Util.getLooper()); } - /** - * @deprecated Use {@link SimpleExoPlayer.Builder} instead. The {@link DrmSessionManager} cannot - * be passed to {@link SimpleExoPlayer.Builder} and should instead be injected into the {@link - * MediaSource} factories. - */ + /** @deprecated Use {@link SimpleExoPlayer.Builder} instead. */ @Deprecated @SuppressWarnings("deprecation") public static SimpleExoPlayer newSimpleInstance( @@ -169,41 +116,18 @@ public final class ExoPlayerFactory { RenderersFactory renderersFactory, TrackSelector trackSelector, LoadControl loadControl, - @Nullable DrmSessionManager drmSessionManager) { - return newSimpleInstance( - context, renderersFactory, trackSelector, loadControl, drmSessionManager, Util.getLooper()); - } - - /** - * @deprecated Use {@link SimpleExoPlayer.Builder} instead. The {@link DrmSessionManager} cannot - * be passed to {@link SimpleExoPlayer.Builder} and should instead be injected into the {@link - * MediaSource} factories. - */ - @Deprecated - @SuppressWarnings("deprecation") - 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(Clock.DEFAULT), Util.getLooper()); } - /** - * @deprecated Use {@link SimpleExoPlayer.Builder} instead. The {@link DrmSessionManager} cannot - * be passed to {@link SimpleExoPlayer.Builder} and should instead be injected into the {@link - * MediaSource} factories. - */ + /** @deprecated Use {@link SimpleExoPlayer.Builder} instead. */ @Deprecated @SuppressWarnings("deprecation") public static SimpleExoPlayer newSimpleInstance( @@ -211,23 +135,17 @@ public final class ExoPlayerFactory { RenderersFactory renderersFactory, TrackSelector trackSelector, LoadControl loadControl, - @Nullable DrmSessionManager drmSessionManager, AnalyticsCollector analyticsCollector) { return newSimpleInstance( context, renderersFactory, trackSelector, loadControl, - drmSessionManager, analyticsCollector, Util.getLooper()); } - /** - * @deprecated Use {@link SimpleExoPlayer.Builder} instead. The {@link DrmSessionManager} cannot - * be passed to {@link SimpleExoPlayer.Builder} and should instead be injected into the {@link - * MediaSource} factories. - */ + /** @deprecated Use {@link SimpleExoPlayer.Builder} instead. */ @Deprecated @SuppressWarnings("deprecation") public static SimpleExoPlayer newSimpleInstance( @@ -235,23 +153,17 @@ public final class ExoPlayerFactory { RenderersFactory renderersFactory, TrackSelector trackSelector, LoadControl loadControl, - @Nullable DrmSessionManager drmSessionManager, - Looper looper) { + Looper applicationLooper) { return newSimpleInstance( context, renderersFactory, trackSelector, loadControl, - drmSessionManager, new AnalyticsCollector(Clock.DEFAULT), - looper); + applicationLooper); } - /** - * @deprecated Use {@link SimpleExoPlayer.Builder} instead. The {@link DrmSessionManager} cannot - * be passed to {@link SimpleExoPlayer.Builder} and should instead be injected into the {@link - * MediaSource} factories. - */ + /** @deprecated Use {@link SimpleExoPlayer.Builder} instead. */ @Deprecated @SuppressWarnings("deprecation") public static SimpleExoPlayer newSimpleInstance( @@ -259,25 +171,19 @@ public final class ExoPlayerFactory { RenderersFactory renderersFactory, TrackSelector trackSelector, LoadControl loadControl, - @Nullable DrmSessionManager drmSessionManager, AnalyticsCollector analyticsCollector, - Looper looper) { + Looper applicationLooper) { return newSimpleInstance( context, renderersFactory, trackSelector, loadControl, - drmSessionManager, DefaultBandwidthMeter.getSingletonInstance(context), analyticsCollector, - looper); + applicationLooper); } - /** - * @deprecated Use {@link SimpleExoPlayer.Builder} instead. The {@link DrmSessionManager} cannot - * be passed to {@link SimpleExoPlayer.Builder} and should instead be injected into the {@link - * MediaSource} factories. - */ + /** @deprecated Use {@link SimpleExoPlayer.Builder} instead. */ @SuppressWarnings("deprecation") @Deprecated public static SimpleExoPlayer newSimpleInstance( @@ -285,20 +191,20 @@ public final class ExoPlayerFactory { RenderersFactory renderersFactory, TrackSelector trackSelector, LoadControl loadControl, - @Nullable DrmSessionManager drmSessionManager, BandwidthMeter bandwidthMeter, AnalyticsCollector analyticsCollector, - Looper looper) { + Looper applicationLooper) { return new SimpleExoPlayer( context, renderersFactory, trackSelector, + DefaultMediaSourceFactory.newInstance(context), loadControl, - drmSessionManager, bandwidthMeter, analyticsCollector, + /* useLazyPreparation= */ true, Clock.DEFAULT, - looper); + applicationLooper); } /** @deprecated Use {@link ExoPlayer.Builder} instead. */ @@ -325,14 +231,14 @@ public final class ExoPlayerFactory { Renderer[] renderers, TrackSelector trackSelector, LoadControl loadControl, - Looper looper) { + Looper applicationLooper) { return newInstance( context, renderers, trackSelector, loadControl, DefaultBandwidthMeter.getSingletonInstance(context), - looper); + applicationLooper); } /** @deprecated Use {@link ExoPlayer.Builder} instead. */ @@ -343,8 +249,16 @@ public final class ExoPlayerFactory { TrackSelector trackSelector, LoadControl loadControl, BandwidthMeter bandwidthMeter, - Looper looper) { + Looper applicationLooper) { return new ExoPlayerImpl( - renderers, trackSelector, loadControl, bandwidthMeter, Clock.DEFAULT, looper); + renderers, + trackSelector, + DefaultMediaSourceFactory.newInstance(context), + loadControl, + bandwidthMeter, + /* analyticsCollector= */ null, + /* useLazyPreparation= */ true, + Clock.DEFAULT, + applicationLooper); } } 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 e79a61da9a..756a09dde3 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 @@ -15,6 +15,8 @@ */ package com.google.android.exoplayer2; +import static com.google.android.exoplayer2.util.Assertions.checkNotNull; + import android.annotation.SuppressLint; import android.os.Handler; import android.os.Looper; @@ -22,9 +24,13 @@ import android.os.Message; import android.util.Pair; import androidx.annotation.Nullable; import com.google.android.exoplayer2.PlayerMessage.Target; +import com.google.android.exoplayer2.analytics.AnalyticsCollector; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; +import com.google.android.exoplayer2.source.MediaSourceFactory; +import com.google.android.exoplayer2.source.ShuffleOrder; import com.google.android.exoplayer2.source.TrackGroupArray; +import com.google.android.exoplayer2.source.ads.AdsMediaSource; import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.trackselection.TrackSelector; @@ -35,6 +41,9 @@ import com.google.android.exoplayer2.util.Clock; import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.Util; import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.TimeoutException; @@ -56,25 +65,29 @@ import java.util.concurrent.TimeoutException; private final Renderer[] renderers; private final TrackSelector trackSelector; - private final Handler eventHandler; + private final Handler applicationHandler; private final ExoPlayerImplInternal internalPlayer; private final Handler internalPlayerHandler; private final CopyOnWriteArrayList listeners; private final Timeline.Period period; private final ArrayDeque pendingListenerNotifications; + private final List mediaSourceHolders; + private final boolean useLazyPreparation; + private final MediaSourceFactory mediaSourceFactory; - @Nullable private MediaSource mediaSource; - private boolean playWhenReady; - @PlaybackSuppressionReason private int playbackSuppressionReason; @RepeatMode private int repeatMode; private boolean shuffleModeEnabled; private int pendingOperationAcks; - private boolean hasPendingPrepare; - private boolean hasPendingSeek; + private boolean hasPendingDiscontinuity; + @DiscontinuityReason private int pendingDiscontinuityReason; + @PlayWhenReadyChangeReason private int pendingPlayWhenReadyChangeReason; private boolean foregroundMode; - private int pendingSetPlaybackParametersAcks; - private PlaybackParameters playbackParameters; + private int pendingSetPlaybackSpeedAcks; + private float playbackSpeed; private SeekParameters seekParameters; + private ShuffleOrder shuffleOrder; + private boolean pauseAtEndOfMediaItems; + private boolean hasAdsMediaSource; // Playback information when there is no pending seek/set source operation. private PlaybackInfo playbackInfo; @@ -87,49 +100,62 @@ import java.util.concurrent.TimeoutException; /** * Constructs an instance. Must be called from a thread that has an associated {@link Looper}. * - * @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. + * @param renderers The {@link Renderer}s. + * @param trackSelector The {@link TrackSelector}. + * @param mediaSourceFactory The {@link MediaSourceFactory}. + * @param loadControl The {@link LoadControl}. + * @param bandwidthMeter The {@link BandwidthMeter}. + * @param analyticsCollector The {@link AnalyticsCollector}. + * @param useLazyPreparation Whether playlist items are prepared lazily. If false, all manifest + * loads and other initial preparation steps happen immediately. If true, these initial + * preparations are triggered only when the player starts buffering the media. + * @param clock The {@link Clock}. + * @param applicationLooper The {@link Looper} that 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, + MediaSourceFactory mediaSourceFactory, LoadControl loadControl, BandwidthMeter bandwidthMeter, + @Nullable AnalyticsCollector analyticsCollector, + boolean useLazyPreparation, Clock clock, - Looper looper) { + Looper applicationLooper) { Log.i(TAG, "Init " + Integer.toHexString(System.identityHashCode(this)) + " [" + ExoPlayerLibraryInfo.VERSION_SLASHY + "] [" + Util.DEVICE_DEBUG_INFO + "]"); Assertions.checkState(renderers.length > 0); - this.renderers = Assertions.checkNotNull(renderers); - this.trackSelector = Assertions.checkNotNull(trackSelector); - this.playWhenReady = false; - this.repeatMode = Player.REPEAT_MODE_OFF; - this.shuffleModeEnabled = false; - this.listeners = new CopyOnWriteArrayList<>(); + this.renderers = checkNotNull(renderers); + this.trackSelector = checkNotNull(trackSelector); + this.mediaSourceFactory = mediaSourceFactory; + this.useLazyPreparation = useLazyPreparation; + repeatMode = Player.REPEAT_MODE_OFF; + listeners = new CopyOnWriteArrayList<>(); + mediaSourceHolders = new ArrayList<>(); + shuffleOrder = new ShuffleOrder.DefaultShuffleOrder(/* length= */ 0); emptyTrackSelectorResult = new TrackSelectorResult( new RendererConfiguration[renderers.length], new TrackSelection[renderers.length], null); period = new Timeline.Period(); - playbackParameters = PlaybackParameters.DEFAULT; + playbackSpeed = Player.DEFAULT_PLAYBACK_SPEED; seekParameters = SeekParameters.DEFAULT; - playbackSuppressionReason = PLAYBACK_SUPPRESSION_REASON_NONE; - eventHandler = - new Handler(looper) { + maskingWindowIndex = C.INDEX_UNSET; + applicationHandler = + new Handler(applicationLooper) { @Override public void handleMessage(Message msg) { ExoPlayerImpl.this.handleEvent(msg); } }; - playbackInfo = PlaybackInfo.createDummy(/* startPositionUs= */ 0, emptyTrackSelectorResult); + playbackInfo = PlaybackInfo.createDummy(emptyTrackSelectorResult); pendingListenerNotifications = new ArrayDeque<>(); + if (analyticsCollector != null) { + analyticsCollector.setPlayer(this); + } internalPlayer = new ExoPlayerImplInternal( renderers, @@ -137,10 +163,10 @@ import java.util.concurrent.TimeoutException; emptyTrackSelectorResult, loadControl, bandwidthMeter, - playWhenReady, repeatMode, shuffleModeEnabled, - eventHandler, + analyticsCollector, + applicationHandler, clock); internalPlayerHandler = new Handler(internalPlayer.getPlaybackLooper()); } @@ -159,6 +185,16 @@ import java.util.concurrent.TimeoutException; internalPlayer.experimental_setReleaseTimeoutMs(timeoutMs); } + /** + * Configures the player to throw when it detects it's stuck buffering. + * + *

      This method is experimental, and will be renamed or removed in a future release. It should + * only be called before the player is used. + */ + public void experimental_throwWhenStuckBuffering() { + internalPlayer.experimental_throwWhenStuckBuffering(); + } + @Override @Nullable public AudioComponent getAudioComponent() { @@ -183,6 +219,12 @@ import java.util.concurrent.TimeoutException; return null; } + @Override + @Nullable + public DeviceComponent getDeviceComponent() { + return null; + } + @Override public Looper getPlaybackLooper() { return internalPlayer.getPlaybackLooper(); @@ -190,7 +232,7 @@ import java.util.concurrent.TimeoutException; @Override public Looper getApplicationLooper() { - return eventHandler.getLooper(); + return applicationHandler.getLooper(); } @Override @@ -217,97 +259,268 @@ import java.util.concurrent.TimeoutException; @Override @PlaybackSuppressionReason public int getPlaybackSuppressionReason() { - return playbackSuppressionReason; + return playbackInfo.playbackSuppressionReason; + } + + @Deprecated + @Override + @Nullable + public ExoPlaybackException getPlaybackError() { + return getPlayerError(); } @Override @Nullable - public ExoPlaybackException getPlaybackError() { + public ExoPlaybackException getPlayerError() { return playbackInfo.playbackError; } + /** @deprecated Use {@link #prepare()} instead. */ + @Deprecated @Override public void retry() { - if (mediaSource != null && playbackInfo.playbackState == Player.STATE_IDLE) { - prepare(mediaSource, /* resetPosition= */ false, /* resetState= */ false); - } - } - - @Override - @Deprecated - public void prepare(MediaSource mediaSource) { - setMediaItem(mediaSource); - prepareInternal(/* resetPosition= */ true, /* resetState= */ true); - } - - @Override - @Deprecated - public void prepare(MediaSource mediaSource, boolean resetPosition, boolean resetState) { - setMediaItem(mediaSource); - prepareInternal(resetPosition, resetState); + prepare(); } @Override public void prepare() { - Assertions.checkNotNull(mediaSource); - prepareInternal(/* resetPosition= */ false, /* resetState= */ true); - } - - @Override - public void setMediaItem(MediaSource mediaItem, long startPositionMs) { - if (!getCurrentTimeline().isEmpty()) { - stop(/* reset= */ true); + if (playbackInfo.playbackState != Player.STATE_IDLE) { + return; } - seekTo(/* windowIndex= */ 0, startPositionMs); - setMediaItem(mediaItem); + PlaybackInfo playbackInfo = + getResetPlaybackInfo( + /* clearPlaylist= */ false, + /* resetError= */ true, + /* playbackState= */ this.playbackInfo.timeline.isEmpty() + ? Player.STATE_ENDED + : Player.STATE_BUFFERING); + // Trigger internal prepare first before updating the playback info and notifying external + // listeners to ensure that new operations issued in the listener notifications reach the + // player after this prepare. The internal player can't change the playback info immediately + // because it uses a callback. + pendingOperationAcks++; + internalPlayer.prepare(); + updatePlaybackInfo( + playbackInfo, + /* positionDiscontinuity= */ false, + /* ignored */ DISCONTINUITY_REASON_INTERNAL, + /* ignored */ TIMELINE_CHANGE_REASON_SOURCE_UPDATE, + /* ignored */ PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST, + /* seekProcessed= */ false); + } + + /** + * @deprecated Use {@link #setMediaSource(MediaSource)} and {@link ExoPlayer#prepare()} instead. + */ + @Deprecated + @Override + public void prepare(MediaSource mediaSource) { + setMediaSource(mediaSource); + prepare(); + } + + /** + * @deprecated Use {@link #setMediaSource(MediaSource, boolean)} and {@link ExoPlayer#prepare()} + * instead. + */ + @Deprecated + @Override + public void prepare(MediaSource mediaSource, boolean resetPosition, boolean resetState) { + setMediaSource(mediaSource, resetPosition); + prepare(); } @Override - public void setMediaItem(MediaSource mediaItem) { - mediaSource = mediaItem; + public void setMediaItems( + List mediaItems, int startWindowIndex, long startPositionMs) { + setMediaSources(createMediaSources(mediaItems), startWindowIndex, startPositionMs); + } + + @Override + public void setMediaSource(MediaSource mediaSource) { + setMediaSources(Collections.singletonList(mediaSource)); + } + + @Override + public void setMediaSource(MediaSource mediaSource, long startPositionMs) { + setMediaSources( + Collections.singletonList(mediaSource), /* startWindowIndex= */ 0, startPositionMs); + } + + @Override + public void setMediaSource(MediaSource mediaSource, boolean resetPosition) { + setMediaSources(Collections.singletonList(mediaSource), resetPosition); + } + + @Override + public void setMediaSources(List mediaSources) { + setMediaSources(mediaSources, /* resetPosition= */ true); + } + + @Override + public void setMediaSources(List mediaSources, boolean resetPosition) { + setMediaSourcesInternal( + mediaSources, + /* startWindowIndex= */ C.INDEX_UNSET, + /* startPositionMs= */ C.TIME_UNSET, + /* resetToDefaultPosition= */ resetPosition); + } + + @Override + public void setMediaSources( + List mediaSources, int startWindowIndex, long startPositionMs) { + setMediaSourcesInternal( + mediaSources, startWindowIndex, startPositionMs, /* resetToDefaultPosition= */ false); + } + + @Override + public void addMediaItems(List mediaItems) { + addMediaItems(/* index= */ mediaSourceHolders.size(), mediaItems); + } + + @Override + public void addMediaItems(int index, List mediaItems) { + addMediaSources(index, createMediaSources(mediaItems)); + } + + @Override + public void addMediaSource(MediaSource mediaSource) { + addMediaSources(Collections.singletonList(mediaSource)); + } + + @Override + public void addMediaSource(int index, MediaSource mediaSource) { + addMediaSources(index, Collections.singletonList(mediaSource)); + } + + @Override + public void addMediaSources(List mediaSources) { + addMediaSources(/* index= */ mediaSourceHolders.size(), mediaSources); + } + + @Override + public void addMediaSources(int index, List mediaSources) { + Assertions.checkArgument(index >= 0); + validateMediaSources(mediaSources, /* mediaSourceReplacement= */ false); + int currentWindowIndex = getCurrentWindowIndex(); + long currentPositionMs = getCurrentPosition(); + Timeline oldTimeline = getCurrentTimeline(); + pendingOperationAcks++; + List holders = addMediaSourceHolders(index, mediaSources); + PlaybackInfo playbackInfo = + maskTimelineAndWindowIndex(currentWindowIndex, currentPositionMs, oldTimeline); + internalPlayer.addMediaSources(index, holders, shuffleOrder); + updatePlaybackInfo( + playbackInfo, + /* positionDiscontinuity= */ false, + /* ignored */ DISCONTINUITY_REASON_INTERNAL, + /* timelineChangeReason= */ TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, + /* ignored */ PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST, + /* seekProcessed= */ false); + } + + @Override + public void removeMediaItems(int fromIndex, int toIndex) { + Assertions.checkArgument(toIndex > fromIndex); + removeMediaItemsInternal(fromIndex, toIndex); + } + + @Override + public void moveMediaItems(int fromIndex, int toIndex, int newFromIndex) { + Assertions.checkArgument( + fromIndex >= 0 + && fromIndex <= toIndex + && toIndex <= mediaSourceHolders.size() + && newFromIndex >= 0); + int currentWindowIndex = getCurrentWindowIndex(); + long currentPositionMs = getCurrentPosition(); + Timeline oldTimeline = getCurrentTimeline(); + pendingOperationAcks++; + newFromIndex = Math.min(newFromIndex, mediaSourceHolders.size() - (toIndex - fromIndex)); + MediaSourceList.moveMediaSourceHolders(mediaSourceHolders, fromIndex, toIndex, newFromIndex); + PlaybackInfo playbackInfo = + maskTimelineAndWindowIndex(currentWindowIndex, currentPositionMs, oldTimeline); + internalPlayer.moveMediaSources(fromIndex, toIndex, newFromIndex, shuffleOrder); + updatePlaybackInfo( + playbackInfo, + /* positionDiscontinuity= */ false, + /* ignored */ DISCONTINUITY_REASON_INTERNAL, + /* timelineChangeReason= */ TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, + /* ignored */ PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST, + /* seekProcessed= */ false); + } + + @Override + public void clearMediaItems() { + if (mediaSourceHolders.isEmpty()) { + return; + } + removeMediaItemsInternal(/* fromIndex= */ 0, /* toIndex= */ mediaSourceHolders.size()); + } + + @Override + public void setShuffleOrder(ShuffleOrder shuffleOrder) { + PlaybackInfo playbackInfo = maskTimeline(); + maskWithCurrentPosition(); + pendingOperationAcks++; + this.shuffleOrder = shuffleOrder; + internalPlayer.setShuffleOrder(shuffleOrder); + updatePlaybackInfo( + playbackInfo, + /* positionDiscontinuity= */ false, + /* ignored */ DISCONTINUITY_REASON_INTERNAL, + /* timelineChangeReason= */ TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, + /* ignored */ PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST, + /* seekProcessed= */ false); } @Override public void setPlayWhenReady(boolean playWhenReady) { - setPlayWhenReady(playWhenReady, PLAYBACK_SUPPRESSION_REASON_NONE); + setPlayWhenReady( + playWhenReady, + PLAYBACK_SUPPRESSION_REASON_NONE, + PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST); + } + + @Override + public void setPauseAtEndOfMediaItems(boolean pauseAtEndOfMediaItems) { + if (this.pauseAtEndOfMediaItems == pauseAtEndOfMediaItems) { + return; + } + this.pauseAtEndOfMediaItems = pauseAtEndOfMediaItems; + internalPlayer.setPauseAtEndOfWindow(pauseAtEndOfMediaItems); + } + + @Override + public boolean getPauseAtEndOfMediaItems() { + return pauseAtEndOfMediaItems; } public void setPlayWhenReady( - boolean playWhenReady, @PlaybackSuppressionReason int playbackSuppressionReason) { - boolean oldIsPlaying = isPlaying(); - boolean oldInternalPlayWhenReady = - this.playWhenReady && this.playbackSuppressionReason == PLAYBACK_SUPPRESSION_REASON_NONE; - boolean internalPlayWhenReady = - playWhenReady && playbackSuppressionReason == PLAYBACK_SUPPRESSION_REASON_NONE; - if (oldInternalPlayWhenReady != internalPlayWhenReady) { - internalPlayer.setPlayWhenReady(internalPlayWhenReady); - } - boolean playWhenReadyChanged = this.playWhenReady != playWhenReady; - boolean suppressionReasonChanged = this.playbackSuppressionReason != playbackSuppressionReason; - this.playWhenReady = playWhenReady; - this.playbackSuppressionReason = playbackSuppressionReason; - boolean isPlaying = isPlaying(); - boolean isPlayingChanged = oldIsPlaying != isPlaying; - if (playWhenReadyChanged || suppressionReasonChanged || isPlayingChanged) { - int playbackState = playbackInfo.playbackState; - notifyListeners( - listener -> { - if (playWhenReadyChanged) { - listener.onPlayerStateChanged(playWhenReady, playbackState); - } - if (suppressionReasonChanged) { - listener.onPlaybackSuppressionReasonChanged(playbackSuppressionReason); - } - if (isPlayingChanged) { - listener.onIsPlayingChanged(isPlaying); - } - }); + boolean playWhenReady, + @PlaybackSuppressionReason int playbackSuppressionReason, + @PlayWhenReadyChangeReason int playWhenReadyChangeReason) { + if (playbackInfo.playWhenReady == playWhenReady + && playbackInfo.playbackSuppressionReason == playbackSuppressionReason) { + return; } + maskWithCurrentPosition(); + pendingOperationAcks++; + PlaybackInfo playbackInfo = + this.playbackInfo.copyWithPlayWhenReady(playWhenReady, playbackSuppressionReason); + internalPlayer.setPlayWhenReady(playWhenReady, playbackSuppressionReason); + updatePlaybackInfo( + playbackInfo, + /* positionDiscontinuity= */ false, + /* ignored */ DISCONTINUITY_REASON_INTERNAL, + /* ignored */ TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, + playWhenReadyChangeReason, + /* seekProcessed= */ false); } @Override public boolean getPlayWhenReady() { - return playWhenReady; + return playbackInfo.playWhenReady; } @Override @@ -349,14 +562,13 @@ import java.util.concurrent.TimeoutException; if (windowIndex < 0 || (!timeline.isEmpty() && windowIndex >= timeline.getWindowCount())) { throw new IllegalSeekPositionException(timeline, windowIndex, positionMs); } - hasPendingSeek = true; pendingOperationAcks++; if (isPlayingAd()) { // TODO: Investigate adding support for seeking during ads. This is complicated to do in // general because the midroll ad preceding the seek destination must be played before the // content position can be played, if a different ad is playing at the moment. Log.w(TAG, "seekTo ignored because an ad is playing"); - eventHandler + applicationHandler .obtainMessage( ExoPlayerImplInternal.MSG_PLAYBACK_INFO_CHANGED, /* operationAcks */ 1, @@ -365,40 +577,59 @@ import java.util.concurrent.TimeoutException; .sendToTarget(); return; } - maskingWindowIndex = windowIndex; - if (timeline.isEmpty()) { - maskingWindowPositionMs = positionMs == C.TIME_UNSET ? 0 : positionMs; - maskingPeriodIndex = 0; - } else { - long windowPositionUs = positionMs == C.TIME_UNSET - ? timeline.getWindow(windowIndex, window).getDefaultPositionUs() : C.msToUs(positionMs); - Pair periodUidAndPosition = - timeline.getPeriodPosition(window, period, windowIndex, windowPositionUs); - maskingWindowPositionMs = C.usToMs(windowPositionUs); - maskingPeriodIndex = timeline.getIndexOfPeriod(periodUidAndPosition.first); - } + maskWindowIndexAndPositionForSeek(timeline, windowIndex, positionMs); + @Player.State + int newPlaybackState = + getPlaybackState() == Player.STATE_IDLE ? Player.STATE_IDLE : Player.STATE_BUFFERING; + PlaybackInfo playbackInfo = this.playbackInfo.copyWithPlaybackState(newPlaybackState); internalPlayer.seekTo(timeline, windowIndex, C.msToUs(positionMs)); - notifyListeners(listener -> listener.onPositionDiscontinuity(DISCONTINUITY_REASON_SEEK)); + updatePlaybackInfo( + playbackInfo, + /* positionDiscontinuity= */ true, + /* positionDiscontinuityReason= */ DISCONTINUITY_REASON_SEEK, + /* ignored */ TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, + /* ignored */ PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST, + /* seekProcessed= */ true); } + /** @deprecated Use {@link #setPlaybackSpeed(float)} instead. */ + @SuppressWarnings("deprecation") + @Deprecated @Override public void setPlaybackParameters(@Nullable PlaybackParameters playbackParameters) { - if (playbackParameters == null) { - playbackParameters = PlaybackParameters.DEFAULT; - } - if (this.playbackParameters.equals(playbackParameters)) { + setPlaybackSpeed( + playbackParameters != null ? playbackParameters.speed : Player.DEFAULT_PLAYBACK_SPEED); + } + + /** @deprecated Use {@link #getPlaybackSpeed()} instead. */ + @SuppressWarnings("deprecation") + @Deprecated + @Override + public PlaybackParameters getPlaybackParameters() { + return new PlaybackParameters(playbackSpeed); + } + + @SuppressWarnings("deprecation") + @Override + public void setPlaybackSpeed(float playbackSpeed) { + Assertions.checkState(playbackSpeed > 0); + if (this.playbackSpeed == playbackSpeed) { return; } - pendingSetPlaybackParametersAcks++; - this.playbackParameters = playbackParameters; - internalPlayer.setPlaybackParameters(playbackParameters); - PlaybackParameters playbackParametersToNotify = playbackParameters; - notifyListeners(listener -> listener.onPlaybackParametersChanged(playbackParametersToNotify)); + pendingSetPlaybackSpeedAcks++; + this.playbackSpeed = playbackSpeed; + PlaybackParameters playbackParameters = new PlaybackParameters(playbackSpeed); + internalPlayer.setPlaybackSpeed(playbackSpeed); + notifyListeners( + listener -> { + listener.onPlaybackParametersChanged(playbackParameters); + listener.onPlaybackSpeedChanged(playbackSpeed); + }); } @Override - public PlaybackParameters getPlaybackParameters() { - return playbackParameters; + public float getPlaybackSpeed() { + return playbackSpeed; } @Override @@ -427,13 +658,9 @@ import java.util.concurrent.TimeoutException; @Override public void stop(boolean reset) { - if (reset) { - mediaSource = null; - } PlaybackInfo playbackInfo = getResetPlaybackInfo( - /* resetPosition= */ reset, - /* resetState= */ reset, + /* clearPlaylist= */ reset, /* resetError= */ reset, /* playbackState= */ Player.STATE_IDLE); // Trigger internal stop first before updating the playback info and notifying external @@ -446,7 +673,8 @@ import java.util.concurrent.TimeoutException; playbackInfo, /* positionDiscontinuity= */ false, /* ignored */ DISCONTINUITY_REASON_INTERNAL, - TIMELINE_CHANGE_REASON_RESET, + TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, + /* ignored */ PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST, /* seekProcessed= */ false); } @@ -455,7 +683,6 @@ import java.util.concurrent.TimeoutException; Log.i(TAG, "Release " + Integer.toHexString(System.identityHashCode(this)) + " [" + ExoPlayerLibraryInfo.VERSION_SLASHY + "] [" + Util.DEVICE_DEBUG_INFO + "] [" + ExoPlayerLibraryInfo.registeredModules() + "]"); - mediaSource = null; if (!internalPlayer.release()) { notifyListeners( listener -> @@ -463,11 +690,10 @@ import java.util.concurrent.TimeoutException; ExoPlaybackException.createForUnexpected( new RuntimeException(new TimeoutException("Player release timed out."))))); } - eventHandler.removeCallbacksAndMessages(null); + applicationHandler.removeCallbacksAndMessages(null); playbackInfo = getResetPlaybackInfo( - /* resetPosition= */ false, - /* resetState= */ false, + /* clearPlaylist= */ false, /* resetError= */ false, /* playbackState= */ Player.STATE_IDLE); } @@ -493,12 +719,8 @@ import java.util.concurrent.TimeoutException; @Override public int getCurrentWindowIndex() { - if (shouldMaskPosition()) { - return maskingWindowIndex; - } else { - return playbackInfo.timeline.getPeriodByUid(playbackInfo.periodId.periodUid, period) - .windowIndex; - } + int currentWindowIndex = getCurrentWindowIndexInternal(); + return currentWindowIndex == C.INDEX_UNSET ? 0 : currentWindowIndex; } @Override @@ -557,9 +779,9 @@ import java.util.concurrent.TimeoutException; public long getContentPosition() { if (isPlayingAd()) { playbackInfo.timeline.getPeriodByUid(playbackInfo.periodId.periodUid, period); - return playbackInfo.contentPositionUs == C.TIME_UNSET + return playbackInfo.requestedContentPositionUs == C.TIME_UNSET ? playbackInfo.timeline.getWindow(getCurrentWindowIndex(), window).getDefaultPositionMs() - : period.getPositionInWindowMs() + C.usToMs(playbackInfo.contentPositionUs); + : period.getPositionInWindowMs() + C.usToMs(playbackInfo.requestedContentPositionUs); } else { return getCurrentPosition(); } @@ -617,144 +839,124 @@ import java.util.concurrent.TimeoutException; /* package */ void handleEvent(Message msg) { switch (msg.what) { case ExoPlayerImplInternal.MSG_PLAYBACK_INFO_CHANGED: - handlePlaybackInfo( - (PlaybackInfo) msg.obj, - /* operationAcks= */ msg.arg1, - /* positionDiscontinuity= */ msg.arg2 != C.INDEX_UNSET, - /* positionDiscontinuityReason= */ msg.arg2); + handlePlaybackInfo((ExoPlayerImplInternal.PlaybackInfoUpdate) msg.obj); break; - case ExoPlayerImplInternal.MSG_PLAYBACK_PARAMETERS_CHANGED: - handlePlaybackParameters((PlaybackParameters) msg.obj, /* operationAck= */ msg.arg1 != 0); + case ExoPlayerImplInternal.MSG_PLAYBACK_SPEED_CHANGED: + handlePlaybackSpeed((Float) msg.obj, /* operationAck= */ msg.arg1 != 0); break; default: throw new IllegalStateException(); } } - /* package */ void prepareInternal(boolean resetPosition, boolean resetState) { - Assertions.checkNotNull(mediaSource); - PlaybackInfo playbackInfo = - getResetPlaybackInfo( - resetPosition, - resetState, - /* resetError= */ true, - /* playbackState= */ Player.STATE_BUFFERING); - // Trigger internal prepare first before updating the playback info and notifying external - // listeners to ensure that new operations issued in the listener notifications reach the - // player after this prepare. The internal player can't change the playback info immediately - // because it uses a callback. - hasPendingPrepare = true; - pendingOperationAcks++; - internalPlayer.prepare(mediaSource, resetPosition, resetState); - updatePlaybackInfo( - playbackInfo, - /* positionDiscontinuity= */ false, - /* ignored */ DISCONTINUITY_REASON_INTERNAL, - TIMELINE_CHANGE_REASON_RESET, - /* seekProcessed= */ false); + private int getCurrentWindowIndexInternal() { + if (shouldMaskPosition()) { + return maskingWindowIndex; + } else { + return playbackInfo.timeline.getPeriodByUid(playbackInfo.periodId.periodUid, period) + .windowIndex; + } } - private void handlePlaybackParameters( - PlaybackParameters playbackParameters, boolean operationAck) { + private List createMediaSources(List mediaItems) { + List mediaSources = new ArrayList<>(); + for (int i = 0; i < mediaItems.size(); i++) { + mediaSources.add(mediaSourceFactory.createMediaSource(mediaItems.get(i))); + } + return mediaSources; + } + + @SuppressWarnings("deprecation") + private void handlePlaybackSpeed(float playbackSpeed, boolean operationAck) { if (operationAck) { - pendingSetPlaybackParametersAcks--; + pendingSetPlaybackSpeedAcks--; } - if (pendingSetPlaybackParametersAcks == 0) { - if (!this.playbackParameters.equals(playbackParameters)) { - this.playbackParameters = playbackParameters; - notifyListeners(listener -> listener.onPlaybackParametersChanged(playbackParameters)); + if (pendingSetPlaybackSpeedAcks == 0) { + if (this.playbackSpeed != playbackSpeed) { + this.playbackSpeed = playbackSpeed; + notifyListeners( + listener -> { + listener.onPlaybackParametersChanged(new PlaybackParameters(playbackSpeed)); + listener.onPlaybackSpeedChanged(playbackSpeed); + }); } } } - private void handlePlaybackInfo( - PlaybackInfo playbackInfo, - int operationAcks, - boolean positionDiscontinuity, - @DiscontinuityReason int positionDiscontinuityReason) { - pendingOperationAcks -= operationAcks; + private void handlePlaybackInfo(ExoPlayerImplInternal.PlaybackInfoUpdate playbackInfoUpdate) { + pendingOperationAcks -= playbackInfoUpdate.operationAcks; + if (playbackInfoUpdate.positionDiscontinuity) { + hasPendingDiscontinuity = true; + pendingDiscontinuityReason = playbackInfoUpdate.discontinuityReason; + } + if (playbackInfoUpdate.hasPlayWhenReadyChangeReason) { + pendingPlayWhenReadyChangeReason = playbackInfoUpdate.playWhenReadyChangeReason; + } if (pendingOperationAcks == 0) { - if (playbackInfo.startPositionUs == C.TIME_UNSET) { - // Replace internal unset start position with externally visible start position of zero. - playbackInfo = - playbackInfo.copyWithNewPosition( - playbackInfo.periodId, - /* positionUs= */ 0, - playbackInfo.contentPositionUs, - playbackInfo.totalBufferedDurationUs); - } - if (!this.playbackInfo.timeline.isEmpty() && playbackInfo.timeline.isEmpty()) { + if (!this.playbackInfo.timeline.isEmpty() + && playbackInfoUpdate.playbackInfo.timeline.isEmpty()) { // Update the masking variables, which are used when the timeline becomes empty. - maskingPeriodIndex = 0; - maskingWindowIndex = 0; - maskingWindowPositionMs = 0; + resetMaskingPosition(); } - @Player.TimelineChangeReason - int timelineChangeReason = - hasPendingPrepare - ? Player.TIMELINE_CHANGE_REASON_PREPARED - : Player.TIMELINE_CHANGE_REASON_DYNAMIC; - boolean seekProcessed = hasPendingSeek; - hasPendingPrepare = false; - hasPendingSeek = false; + boolean positionDiscontinuity = hasPendingDiscontinuity; + hasPendingDiscontinuity = false; updatePlaybackInfo( - playbackInfo, + playbackInfoUpdate.playbackInfo, positionDiscontinuity, - positionDiscontinuityReason, - timelineChangeReason, - seekProcessed); + pendingDiscontinuityReason, + TIMELINE_CHANGE_REASON_SOURCE_UPDATE, + pendingPlayWhenReadyChangeReason, + /* seekProcessed= */ false); } } private PlaybackInfo getResetPlaybackInfo( - boolean resetPosition, - boolean resetState, - boolean resetError, - @Player.State int playbackState) { - if (resetPosition) { - maskingWindowIndex = 0; - maskingPeriodIndex = 0; - maskingWindowPositionMs = 0; + boolean clearPlaylist, boolean resetError, @Player.State int playbackState) { + if (clearPlaylist) { + // Reset list of media source holders which are used for creating the masking timeline. + removeMediaSourceHolders( + /* fromIndex= */ 0, /* toIndexExclusive= */ mediaSourceHolders.size()); + resetMaskingPosition(); } else { - maskingWindowIndex = getCurrentWindowIndex(); - maskingPeriodIndex = getCurrentPeriodIndex(); - maskingWindowPositionMs = getCurrentPosition(); + maskWithCurrentPosition(); + } + Timeline timeline = playbackInfo.timeline; + MediaPeriodId mediaPeriodId = playbackInfo.periodId; + long requestedContentPositionUs = playbackInfo.requestedContentPositionUs; + long positionUs = playbackInfo.positionUs; + if (clearPlaylist) { + timeline = Timeline.EMPTY; + mediaPeriodId = PlaybackInfo.getDummyPeriodForEmptyTimeline(); + requestedContentPositionUs = C.TIME_UNSET; + positionUs = 0; } - // Also reset period-based PlaybackInfo positions if resetting the state. - resetPosition = resetPosition || resetState; - MediaPeriodId mediaPeriodId = - resetPosition - ? playbackInfo.getDummyFirstMediaPeriodId(shuffleModeEnabled, window, period) - : playbackInfo.periodId; - long startPositionUs = resetPosition ? 0 : playbackInfo.positionUs; - long contentPositionUs = resetPosition ? C.TIME_UNSET : playbackInfo.contentPositionUs; return new PlaybackInfo( - resetState ? Timeline.EMPTY : playbackInfo.timeline, + timeline, mediaPeriodId, - startPositionUs, - contentPositionUs, + requestedContentPositionUs, playbackState, resetError ? null : playbackInfo.playbackError, /* isLoading= */ false, - resetState ? TrackGroupArray.EMPTY : playbackInfo.trackGroups, - resetState ? emptyTrackSelectorResult : playbackInfo.trackSelectorResult, + clearPlaylist ? TrackGroupArray.EMPTY : playbackInfo.trackGroups, + clearPlaylist ? emptyTrackSelectorResult : playbackInfo.trackSelectorResult, mediaPeriodId, - startPositionUs, + playbackInfo.playWhenReady, + playbackInfo.playbackSuppressionReason, + positionUs, /* totalBufferedDurationUs= */ 0, - startPositionUs); + positionUs); } private void updatePlaybackInfo( PlaybackInfo playbackInfo, boolean positionDiscontinuity, - @Player.DiscontinuityReason int positionDiscontinuityReason, - @Player.TimelineChangeReason int timelineChangeReason, + @DiscontinuityReason int positionDiscontinuityReason, + @TimelineChangeReason int timelineChangeReason, + @PlayWhenReadyChangeReason int playWhenReadyChangeReason, boolean seekProcessed) { - boolean previousIsPlaying = isPlaying(); // Assign playback info immediately such that all getters return the right values. PlaybackInfo previousPlaybackInfo = this.playbackInfo; this.playbackInfo = playbackInfo; - boolean isPlaying = isPlaying(); notifyListeners( new PlaybackInfoUpdate( playbackInfo, @@ -764,9 +966,253 @@ import java.util.concurrent.TimeoutException; positionDiscontinuity, positionDiscontinuityReason, timelineChangeReason, - seekProcessed, - playWhenReady, - /* isPlayingChanged= */ previousIsPlaying != isPlaying)); + playWhenReadyChangeReason, + seekProcessed)); + } + + private void setMediaSourcesInternal( + List mediaSources, + int startWindowIndex, + long startPositionMs, + boolean resetToDefaultPosition) { + validateMediaSources(mediaSources, /* mediaSourceReplacement= */ true); + int currentWindowIndex = getCurrentWindowIndexInternal(); + long currentPositionMs = getCurrentPosition(); + pendingOperationAcks++; + if (!mediaSourceHolders.isEmpty()) { + removeMediaSourceHolders( + /* fromIndex= */ 0, /* toIndexExclusive= */ mediaSourceHolders.size()); + } + List holders = + addMediaSourceHolders(/* index= */ 0, mediaSources); + PlaybackInfo playbackInfo = maskTimeline(); + Timeline timeline = playbackInfo.timeline; + if (!timeline.isEmpty() && startWindowIndex >= timeline.getWindowCount()) { + throw new IllegalSeekPositionException(timeline, startWindowIndex, startPositionMs); + } + // Evaluate the actual start position. + if (resetToDefaultPosition) { + startWindowIndex = timeline.getFirstWindowIndex(shuffleModeEnabled); + startPositionMs = C.TIME_UNSET; + } else if (startWindowIndex == C.INDEX_UNSET) { + startWindowIndex = currentWindowIndex; + startPositionMs = currentPositionMs; + } + maskWindowIndexAndPositionForSeek( + timeline, startWindowIndex == C.INDEX_UNSET ? 0 : startWindowIndex, startPositionMs); + // Mask the playback state. + int maskingPlaybackState = playbackInfo.playbackState; + if (startWindowIndex != C.INDEX_UNSET && playbackInfo.playbackState != STATE_IDLE) { + // Position reset to startWindowIndex (results in pending initial seek). + if (timeline.isEmpty() || startWindowIndex >= timeline.getWindowCount()) { + // Setting an empty timeline or invalid seek transitions to ended. + maskingPlaybackState = STATE_ENDED; + } else { + maskingPlaybackState = STATE_BUFFERING; + } + } + playbackInfo = playbackInfo.copyWithPlaybackState(maskingPlaybackState); + internalPlayer.setMediaSources( + holders, startWindowIndex, C.msToUs(startPositionMs), shuffleOrder); + updatePlaybackInfo( + playbackInfo, + /* positionDiscontinuity= */ false, + /* ignored */ Player.DISCONTINUITY_REASON_INTERNAL, + /* timelineChangeReason= */ TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, + /* ignored */ PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST, + /* seekProcessed= */ false); + } + + private List addMediaSourceHolders( + int index, List mediaSources) { + List holders = new ArrayList<>(); + for (int i = 0; i < mediaSources.size(); i++) { + MediaSourceList.MediaSourceHolder holder = + new MediaSourceList.MediaSourceHolder(mediaSources.get(i), useLazyPreparation); + holders.add(holder); + mediaSourceHolders.add(i + index, holder); + } + shuffleOrder = + shuffleOrder.cloneAndInsert( + /* insertionIndex= */ index, /* insertionCount= */ holders.size()); + return holders; + } + + private void removeMediaItemsInternal(int fromIndex, int toIndex) { + Assertions.checkArgument( + fromIndex >= 0 && toIndex >= fromIndex && toIndex <= mediaSourceHolders.size()); + int currentWindowIndex = getCurrentWindowIndex(); + long currentPositionMs = getCurrentPosition(); + Timeline oldTimeline = getCurrentTimeline(); + int currentMediaSourceCount = mediaSourceHolders.size(); + pendingOperationAcks++; + removeMediaSourceHolders(fromIndex, /* toIndexExclusive= */ toIndex); + PlaybackInfo playbackInfo = + maskTimelineAndWindowIndex(currentWindowIndex, currentPositionMs, oldTimeline); + // Player transitions to STATE_ENDED if the current index is part of the removed tail. + final boolean transitionsToEnded = + playbackInfo.playbackState != STATE_IDLE + && playbackInfo.playbackState != STATE_ENDED + && fromIndex < toIndex + && toIndex == currentMediaSourceCount + && currentWindowIndex >= playbackInfo.timeline.getWindowCount(); + if (transitionsToEnded) { + playbackInfo = playbackInfo.copyWithPlaybackState(STATE_ENDED); + } + internalPlayer.removeMediaSources(fromIndex, toIndex, shuffleOrder); + updatePlaybackInfo( + playbackInfo, + /* positionDiscontinuity= */ false, + /* ignored */ Player.DISCONTINUITY_REASON_INTERNAL, + /* timelineChangeReason= */ TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, + /* ignored */ PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST, + /* seekProcessed= */ false); + } + + private List removeMediaSourceHolders( + int fromIndex, int toIndexExclusive) { + List removed = new ArrayList<>(); + for (int i = toIndexExclusive - 1; i >= fromIndex; i--) { + removed.add(mediaSourceHolders.remove(i)); + } + shuffleOrder = shuffleOrder.cloneAndRemove(fromIndex, toIndexExclusive); + if (mediaSourceHolders.isEmpty()) { + hasAdsMediaSource = false; + } + return removed; + } + + /** + * Validates media sources before any modification of the existing list of media sources is made. + * This way we can throw an exception before changing the state of the player in case of a + * validation failure. + * + * @param mediaSources The media sources to set or add. + * @param mediaSourceReplacement Whether the given media sources will replace existing ones. + */ + private void validateMediaSources( + List mediaSources, boolean mediaSourceReplacement) { + if (hasAdsMediaSource && !mediaSourceReplacement && !mediaSources.isEmpty()) { + // Adding media sources to an ads media source is not allowed + // (see https://github.com/google/ExoPlayer/issues/3750). + throw new IllegalStateException(); + } + int sizeAfterModification = + mediaSources.size() + (mediaSourceReplacement ? 0 : mediaSourceHolders.size()); + for (int i = 0; i < mediaSources.size(); i++) { + MediaSource mediaSource = checkNotNull(mediaSources.get(i)); + if (mediaSource instanceof AdsMediaSource) { + if (sizeAfterModification > 1) { + // Ads media sources only allowed with a single source + // (see https://github.com/google/ExoPlayer/issues/3750). + throw new IllegalArgumentException(); + } + hasAdsMediaSource = true; + } + } + } + + private PlaybackInfo maskTimeline() { + return playbackInfo.copyWithTimeline( + mediaSourceHolders.isEmpty() + ? Timeline.EMPTY + : new MediaSourceList.PlaylistTimeline(mediaSourceHolders, shuffleOrder)); + } + + private PlaybackInfo maskTimelineAndWindowIndex( + int currentWindowIndex, long currentPositionMs, Timeline oldTimeline) { + PlaybackInfo playbackInfo = maskTimeline(); + Timeline maskingTimeline = playbackInfo.timeline; + if (oldTimeline.isEmpty()) { + // The index is the default index or was set by a seek in the empty old timeline. + maskingWindowIndex = currentWindowIndex; + if (!maskingTimeline.isEmpty() && currentWindowIndex >= maskingTimeline.getWindowCount()) { + // The seek is not valid in the new timeline. + maskWithDefaultPosition(maskingTimeline); + } + return playbackInfo; + } + @Nullable + Pair periodPosition = + oldTimeline.getPeriodPosition( + window, + period, + currentWindowIndex, + C.msToUs(currentPositionMs), + /* defaultPositionProjectionUs= */ 0); + Object periodUid = Util.castNonNull(periodPosition).first; + if (maskingTimeline.getIndexOfPeriod(periodUid) != C.INDEX_UNSET) { + // Get the window index of the current period that exists in the new timeline also. + maskingWindowIndex = maskingTimeline.getPeriodByUid(periodUid, period).windowIndex; + maskingPeriodIndex = maskingTimeline.getIndexOfPeriod(periodUid); + maskingWindowPositionMs = currentPositionMs; + } else { + // Period uid not found in new timeline. Try to get subsequent period. + @Nullable + Object nextPeriodUid = + ExoPlayerImplInternal.resolveSubsequentPeriod( + window, + period, + repeatMode, + shuffleModeEnabled, + periodUid, + oldTimeline, + maskingTimeline); + if (nextPeriodUid != null) { + // Set masking to the default position of the window of the subsequent period. + maskingWindowIndex = maskingTimeline.getPeriodByUid(nextPeriodUid, period).windowIndex; + maskingPeriodIndex = maskingTimeline.getWindow(maskingWindowIndex, window).firstPeriodIndex; + maskingWindowPositionMs = window.getDefaultPositionMs(); + } else { + // Reset if no subsequent period is found. + maskWithDefaultPosition(maskingTimeline); + } + } + return playbackInfo; + } + + private void maskWindowIndexAndPositionForSeek( + Timeline timeline, int windowIndex, long positionMs) { + maskingWindowIndex = windowIndex; + if (timeline.isEmpty()) { + maskingWindowPositionMs = positionMs == C.TIME_UNSET ? 0 : positionMs; + maskingPeriodIndex = 0; + } else if (windowIndex >= timeline.getWindowCount()) { + // An initial seek now proves to be invalid in the actual timeline. + maskWithDefaultPosition(timeline); + } else { + long windowPositionUs = + positionMs == C.TIME_UNSET + ? timeline.getWindow(windowIndex, window).getDefaultPositionUs() + : C.msToUs(positionMs); + Pair periodUidAndPosition = + timeline.getPeriodPosition(window, period, windowIndex, windowPositionUs); + maskingWindowPositionMs = C.usToMs(windowPositionUs); + maskingPeriodIndex = timeline.getIndexOfPeriod(periodUidAndPosition.first); + } + } + + private void maskWithCurrentPosition() { + maskingWindowIndex = getCurrentWindowIndexInternal(); + maskingPeriodIndex = getCurrentPeriodIndex(); + maskingWindowPositionMs = getCurrentPosition(); + } + + private void maskWithDefaultPosition(Timeline timeline) { + if (timeline.isEmpty()) { + resetMaskingPosition(); + return; + } + maskingWindowIndex = timeline.getFirstWindowIndex(shuffleModeEnabled); + timeline.getWindow(maskingWindowIndex, window); + maskingWindowPositionMs = window.getDefaultPositionMs(); + maskingPeriodIndex = window.firstPeriodIndex; + } + + private void resetMaskingPosition() { + maskingWindowIndex = C.INDEX_UNSET; + maskingWindowPositionMs = 0; + maskingPeriodIndex = 0; } private void notifyListeners(ListenerInvocation listenerInvocation) { @@ -803,16 +1249,18 @@ import java.util.concurrent.TimeoutException; private final CopyOnWriteArrayList listenerSnapshot; private final TrackSelector trackSelector; private final boolean positionDiscontinuity; - private final @Player.DiscontinuityReason int positionDiscontinuityReason; - private final @Player.TimelineChangeReason int timelineChangeReason; + @DiscontinuityReason private final int positionDiscontinuityReason; + @TimelineChangeReason private final int timelineChangeReason; + @PlayWhenReadyChangeReason private final int playWhenReadyChangeReason; private final boolean seekProcessed; private final boolean playbackStateChanged; private final boolean playbackErrorChanged; private final boolean timelineChanged; private final boolean isLoadingChanged; private final boolean trackSelectorResultChanged; - private final boolean playWhenReady; private final boolean isPlayingChanged; + private final boolean playWhenReadyChanged; + private final boolean playbackSuppressionReasonChanged; public PlaybackInfoUpdate( PlaybackInfo playbackInfo, @@ -822,31 +1270,34 @@ import java.util.concurrent.TimeoutException; boolean positionDiscontinuity, @DiscontinuityReason int positionDiscontinuityReason, @TimelineChangeReason int timelineChangeReason, - boolean seekProcessed, - boolean playWhenReady, - boolean isPlayingChanged) { + @PlayWhenReadyChangeReason int playWhenReadyChangeReason, + boolean seekProcessed) { this.playbackInfo = playbackInfo; this.listenerSnapshot = new CopyOnWriteArrayList<>(listeners); this.trackSelector = trackSelector; this.positionDiscontinuity = positionDiscontinuity; this.positionDiscontinuityReason = positionDiscontinuityReason; this.timelineChangeReason = timelineChangeReason; + this.playWhenReadyChangeReason = playWhenReadyChangeReason; this.seekProcessed = seekProcessed; - this.playWhenReady = playWhenReady; - this.isPlayingChanged = isPlayingChanged; playbackStateChanged = previousPlaybackInfo.playbackState != playbackInfo.playbackState; playbackErrorChanged = previousPlaybackInfo.playbackError != playbackInfo.playbackError && playbackInfo.playbackError != null; - timelineChanged = previousPlaybackInfo.timeline != playbackInfo.timeline; isLoadingChanged = previousPlaybackInfo.isLoading != playbackInfo.isLoading; + timelineChanged = !previousPlaybackInfo.timeline.equals(playbackInfo.timeline); trackSelectorResultChanged = previousPlaybackInfo.trackSelectorResult != playbackInfo.trackSelectorResult; + playWhenReadyChanged = previousPlaybackInfo.playWhenReady != playbackInfo.playWhenReady; + playbackSuppressionReasonChanged = + previousPlaybackInfo.playbackSuppressionReason != playbackInfo.playbackSuppressionReason; + isPlayingChanged = isPlaying(previousPlaybackInfo) != isPlaying(playbackInfo); } + @SuppressWarnings("deprecation") @Override public void run() { - if (timelineChanged || timelineChangeReason == TIMELINE_CHANGE_REASON_PREPARED) { + if (timelineChanged) { invokeAll( listenerSnapshot, listener -> listener.onTimelineChanged(playbackInfo.timeline, timelineChangeReason)); @@ -868,23 +1319,49 @@ import java.util.concurrent.TimeoutException; playbackInfo.trackGroups, playbackInfo.trackSelectorResult.selections)); } if (isLoadingChanged) { - invokeAll(listenerSnapshot, listener -> listener.onLoadingChanged(playbackInfo.isLoading)); + invokeAll( + listenerSnapshot, listener -> listener.onIsLoadingChanged(playbackInfo.isLoading)); + } + if (playbackStateChanged || playWhenReadyChanged) { + invokeAll( + listenerSnapshot, + listener -> + listener.onPlayerStateChanged( + playbackInfo.playWhenReady, playbackInfo.playbackState)); } if (playbackStateChanged) { invokeAll( listenerSnapshot, - listener -> listener.onPlayerStateChanged(playWhenReady, playbackInfo.playbackState)); + listener -> listener.onPlaybackStateChanged(playbackInfo.playbackState)); } - if (isPlayingChanged) { + if (playWhenReadyChanged) { invokeAll( listenerSnapshot, listener -> - listener.onIsPlayingChanged(playbackInfo.playbackState == Player.STATE_READY)); + listener.onPlayWhenReadyChanged( + playbackInfo.playWhenReady, playWhenReadyChangeReason)); + } + if (playbackSuppressionReasonChanged) { + invokeAll( + listenerSnapshot, + listener -> + listener.onPlaybackSuppressionReasonChanged( + playbackInfo.playbackSuppressionReason)); + } + if (isPlayingChanged) { + invokeAll( + listenerSnapshot, listener -> listener.onIsPlayingChanged(isPlaying(playbackInfo))); } if (seekProcessed) { invokeAll(listenerSnapshot, EventListener::onSeekProcessed); } } + + private static boolean isPlaying(PlaybackInfo playbackInfo) { + return playbackInfo.playbackState == Player.STATE_READY + && playbackInfo.playWhenReady + && playbackInfo.playbackSuppressionReason == PLAYBACK_SUPPRESSION_REASON_NONE; + } } private static void invokeAll( 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 a88335b0ff..96e8f3d8ac 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 @@ -22,14 +22,18 @@ import android.os.Message; import android.os.Process; import android.os.SystemClock; import android.util.Pair; +import androidx.annotation.CheckResult; import androidx.annotation.Nullable; -import com.google.android.exoplayer2.DefaultMediaClock.PlaybackParameterListener; +import com.google.android.exoplayer2.DefaultMediaClock.PlaybackSpeedListener; import com.google.android.exoplayer2.Player.DiscontinuityReason; +import com.google.android.exoplayer2.Player.PlayWhenReadyChangeReason; +import com.google.android.exoplayer2.Player.PlaybackSuppressionReason; +import com.google.android.exoplayer2.Player.RepeatMode; +import com.google.android.exoplayer2.analytics.AnalyticsCollector; import com.google.android.exoplayer2.source.MediaPeriod; -import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; -import com.google.android.exoplayer2.source.MediaSource.MediaSourceCaller; import com.google.android.exoplayer2.source.SampleStream; +import com.google.android.exoplayer2.source.ShuffleOrder; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.trackselection.TrackSelector; @@ -44,6 +48,7 @@ import com.google.android.exoplayer2.util.Util; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; +import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; /** Implements the internal behavior of {@link ExoPlayerImpl}. */ @@ -51,35 +56,41 @@ import java.util.concurrent.atomic.AtomicBoolean; implements Handler.Callback, MediaPeriod.Callback, TrackSelector.InvalidationListener, - MediaSourceCaller, - PlaybackParameterListener, + MediaSourceList.MediaSourceListInfoRefreshListener, + PlaybackSpeedListener, PlayerMessage.Sender { private static final String TAG = "ExoPlayerImplInternal"; // External messages public static final int MSG_PLAYBACK_INFO_CHANGED = 0; - public static final int MSG_PLAYBACK_PARAMETERS_CHANGED = 1; + public static final int MSG_PLAYBACK_SPEED_CHANGED = 1; // Internal messages private static final int MSG_PREPARE = 0; private static final int MSG_SET_PLAY_WHEN_READY = 1; private static final int MSG_DO_SOME_WORK = 2; private static final int MSG_SEEK_TO = 3; - private static final int MSG_SET_PLAYBACK_PARAMETERS = 4; + private static final int MSG_SET_PLAYBACK_SPEED = 4; private static final int MSG_SET_SEEK_PARAMETERS = 5; private static final int MSG_STOP = 6; private static final int MSG_RELEASE = 7; - private static final int MSG_REFRESH_SOURCE_INFO = 8; - private static final int MSG_PERIOD_PREPARED = 9; - private static final int MSG_SOURCE_CONTINUE_LOADING_REQUESTED = 10; - private static final int MSG_TRACK_SELECTION_INVALIDATED = 11; - private static final int MSG_SET_REPEAT_MODE = 12; - private static final int MSG_SET_SHUFFLE_ENABLED = 13; - private static final int MSG_SET_FOREGROUND_MODE = 14; - private static final int MSG_SEND_MESSAGE = 15; - private static final int MSG_SEND_MESSAGE_TO_TARGET_THREAD = 16; - private static final int MSG_PLAYBACK_PARAMETERS_CHANGED_INTERNAL = 17; + private static final int MSG_PERIOD_PREPARED = 8; + private static final int MSG_SOURCE_CONTINUE_LOADING_REQUESTED = 9; + private static final int MSG_TRACK_SELECTION_INVALIDATED = 10; + private static final int MSG_SET_REPEAT_MODE = 11; + private static final int MSG_SET_SHUFFLE_ENABLED = 12; + private static final int MSG_SET_FOREGROUND_MODE = 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_SPEED_CHANGED_INTERNAL = 16; + private static final int MSG_SET_MEDIA_SOURCES = 17; + private static final int MSG_ADD_MEDIA_SOURCES = 18; + private static final int MSG_MOVE_MEDIA_SOURCES = 19; + private static final int MSG_REMOVE_MEDIA_SOURCES = 20; + private static final int MSG_SET_SHUFFLE_ORDER = 21; + private static final int MSG_PLAYLIST_UPDATE_REQUESTED = 22; + private static final int MSG_SET_PAUSE_AT_END_OF_WINDOW = 23; private static final int ACTIVE_INTERVAL_MS = 10; private static final int IDLE_INTERVAL_MS = 1000; @@ -98,32 +109,33 @@ import java.util.concurrent.atomic.AtomicBoolean; private final long backBufferDurationUs; private final boolean retainBackBufferFromKeyframe; private final DefaultMediaClock mediaClock; - private final PlaybackInfoUpdate playbackInfoUpdate; private final ArrayList pendingMessages; private final Clock clock; private final MediaPeriodQueue queue; + private final MediaSourceList mediaSourceList; @SuppressWarnings("unused") private SeekParameters seekParameters; private PlaybackInfo playbackInfo; - private MediaSource mediaSource; - private Renderer[] enabledRenderers; + private PlaybackInfoUpdate playbackInfoUpdate; private boolean released; - private boolean playWhenReady; + private boolean pauseAtEndOfWindow; + private boolean pendingPauseAtEndOfPeriod; private boolean rebuffering; private boolean shouldContinueLoading; @Player.RepeatMode private int repeatMode; private boolean shuffleModeEnabled; private boolean foregroundMode; - private int pendingPrepareCount; - private SeekPosition pendingInitialSeekPosition; + private int enabledRendererCount; + @Nullable private SeekPosition pendingInitialSeekPosition; private long rendererPositionUs; - private int nextPendingMessageIndex; + private int nextPendingMessageIndexHint; private boolean deliverPendingMessageAtStartPositionRequired; private long releaseTimeoutMs; + private boolean throwWhenStuckBuffering; public ExoPlayerImplInternal( Renderer[] renderers, @@ -131,9 +143,9 @@ import java.util.concurrent.atomic.AtomicBoolean; TrackSelectorResult emptyTrackSelectorResult, LoadControl loadControl, BandwidthMeter bandwidthMeter, - boolean playWhenReady, @Player.RepeatMode int repeatMode, boolean shuffleModeEnabled, + @Nullable AnalyticsCollector analyticsCollector, Handler eventHandler, Clock clock) { this.renderers = renderers; @@ -141,7 +153,6 @@ import java.util.concurrent.atomic.AtomicBoolean; this.emptyTrackSelectorResult = emptyTrackSelectorResult; this.loadControl = loadControl; this.bandwidthMeter = bandwidthMeter; - this.playWhenReady = playWhenReady; this.repeatMode = repeatMode; this.shuffleModeEnabled = shuffleModeEnabled; this.eventHandler = eventHandler; @@ -152,9 +163,8 @@ import java.util.concurrent.atomic.AtomicBoolean; retainBackBufferFromKeyframe = loadControl.retainBackBufferFromKeyframe(); seekParameters = SeekParameters.DEFAULT; - playbackInfo = - PlaybackInfo.createDummy(/* startPositionUs= */ C.TIME_UNSET, emptyTrackSelectorResult); - playbackInfoUpdate = new PlaybackInfoUpdate(); + playbackInfo = PlaybackInfo.createDummy(emptyTrackSelectorResult); + playbackInfoUpdate = new PlaybackInfoUpdate(playbackInfo); rendererCapabilities = new RendererCapabilities[renderers.length]; for (int i = 0; i < renderers.length; i++) { renderers[i].setIndex(i); @@ -162,32 +172,45 @@ import java.util.concurrent.atomic.AtomicBoolean; } mediaClock = new DefaultMediaClock(this, clock); pendingMessages = new ArrayList<>(); - enabledRenderers = new Renderer[0]; window = new Timeline.Window(); period = new Timeline.Period(); 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. - internalPlaybackThread = - new HandlerThread("ExoPlayerImplInternal:Handler", Process.THREAD_PRIORITY_AUDIO); + internalPlaybackThread = new HandlerThread("ExoPlayer:Playback", Process.THREAD_PRIORITY_AUDIO); internalPlaybackThread.start(); handler = clock.createHandler(internalPlaybackThread.getLooper(), this); deliverPendingMessageAtStartPositionRequired = true; + mediaSourceList = new MediaSourceList(this); + if (analyticsCollector != null) { + mediaSourceList.setAnalyticsCollector(eventHandler, analyticsCollector); + } } public void experimental_setReleaseTimeoutMs(long releaseTimeoutMs) { this.releaseTimeoutMs = releaseTimeoutMs; } - public void prepare(MediaSource mediaSource, boolean resetPosition, boolean resetState) { + public void experimental_throwWhenStuckBuffering() { + throwWhenStuckBuffering = true; + } + + public void prepare() { + handler.obtainMessage(MSG_PREPARE).sendToTarget(); + } + + public void setPlayWhenReady( + boolean playWhenReady, @PlaybackSuppressionReason int playbackSuppressionReason) { handler - .obtainMessage(MSG_PREPARE, resetPosition ? 1 : 0, resetState ? 1 : 0, mediaSource) + .obtainMessage(MSG_SET_PLAY_WHEN_READY, playWhenReady ? 1 : 0, playbackSuppressionReason) .sendToTarget(); } - public void setPlayWhenReady(boolean playWhenReady) { - handler.obtainMessage(MSG_SET_PLAY_WHEN_READY, playWhenReady ? 1 : 0, 0).sendToTarget(); + public void setPauseAtEndOfWindow(boolean pauseAtEndOfWindow) { + handler + .obtainMessage(MSG_SET_PAUSE_AT_END_OF_WINDOW, pauseAtEndOfWindow ? 1 : 0, /* ignored */ 0) + .sendToTarget(); } public void setRepeatMode(@Player.RepeatMode int repeatMode) { @@ -204,8 +227,8 @@ import java.util.concurrent.atomic.AtomicBoolean; .sendToTarget(); } - public void setPlaybackParameters(PlaybackParameters playbackParameters) { - handler.obtainMessage(MSG_SET_PLAYBACK_PARAMETERS, playbackParameters).sendToTarget(); + public void setPlaybackSpeed(float playbackSpeed) { + handler.obtainMessage(MSG_SET_PLAYBACK_SPEED, playbackSpeed).sendToTarget(); } public void setSeekParameters(SeekParameters seekParameters) { @@ -216,6 +239,50 @@ import java.util.concurrent.atomic.AtomicBoolean; handler.obtainMessage(MSG_STOP, reset ? 1 : 0, 0).sendToTarget(); } + public void setMediaSources( + List mediaSources, + int windowIndex, + long positionUs, + ShuffleOrder shuffleOrder) { + handler + .obtainMessage( + MSG_SET_MEDIA_SOURCES, + new MediaSourceListUpdateMessage(mediaSources, shuffleOrder, windowIndex, positionUs)) + .sendToTarget(); + } + + public void addMediaSources( + int index, List mediaSources, ShuffleOrder shuffleOrder) { + handler + .obtainMessage( + MSG_ADD_MEDIA_SOURCES, + index, + /* ignored */ 0, + new MediaSourceListUpdateMessage( + mediaSources, + shuffleOrder, + /* windowIndex= */ C.INDEX_UNSET, + /* positionUs= */ C.TIME_UNSET)) + .sendToTarget(); + } + + public void removeMediaSources(int fromIndex, int toIndex, ShuffleOrder shuffleOrder) { + handler + .obtainMessage(MSG_REMOVE_MEDIA_SOURCES, fromIndex, toIndex, shuffleOrder) + .sendToTarget(); + } + + public void moveMediaSources( + int fromIndex, int toIndex, int newFromIndex, ShuffleOrder shuffleOrder) { + MoveMediaItemsMessage moveMediaItemsMessage = + new MoveMediaItemsMessage(fromIndex, toIndex, newFromIndex, shuffleOrder); + handler.obtainMessage(MSG_MOVE_MEDIA_SOURCES, moveMediaItemsMessage).sendToTarget(); + } + + public void setShuffleOrder(ShuffleOrder shuffleOrder) { + handler.obtainMessage(MSG_SET_SHUFFLE_ORDER, shuffleOrder).sendToTarget(); + } + @Override public synchronized void sendMessage(PlayerMessage message) { if (released || !internalPlaybackThread.isAlive()) { @@ -275,13 +342,11 @@ import java.util.concurrent.atomic.AtomicBoolean; return internalPlaybackThread.getLooper(); } - // MediaSource.MediaSourceCaller implementation. + // Playlist.PlaylistInfoRefreshListener implementation. @Override - public void onSourceInfoRefreshed(MediaSource source, Timeline timeline) { - handler - .obtainMessage(MSG_REFRESH_SOURCE_INFO, new MediaSourceRefreshInfo(source, timeline)) - .sendToTarget(); + public void onPlaylistUpdateRequested() { + handler.sendEmptyMessage(MSG_PLAYLIST_UPDATE_REQUESTED); } // MediaPeriod.Callback implementation. @@ -303,11 +368,11 @@ import java.util.concurrent.atomic.AtomicBoolean; handler.sendEmptyMessage(MSG_TRACK_SELECTION_INVALIDATED); } - // DefaultMediaClock.PlaybackParameterListener implementation. + // DefaultMediaClock.PlaybackSpeedListener implementation. @Override - public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) { - sendPlaybackParametersChangedInternal(playbackParameters, /* acknowledgeCommand= */ false); + public void onPlaybackSpeedChanged(float playbackSpeed) { + sendPlaybackSpeedChangedInternal(playbackSpeed, /* acknowledgeCommand= */ false); } // Handler.Callback implementation. @@ -317,13 +382,14 @@ import java.util.concurrent.atomic.AtomicBoolean; try { switch (msg.what) { case MSG_PREPARE: - prepareInternal( - (MediaSource) msg.obj, - /* resetPosition= */ msg.arg1 != 0, - /* resetState= */ msg.arg2 != 0); + prepareInternal(); break; case MSG_SET_PLAY_WHEN_READY: - setPlayWhenReadyInternal(msg.arg1 != 0); + setPlayWhenReadyInternal( + /* playWhenReady= */ msg.arg1 != 0, + /* playbackSuppressionReason= */ msg.arg2, + /* operationAck= */ true, + Player.PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST); break; case MSG_SET_REPEAT_MODE: setRepeatModeInternal(msg.arg1); @@ -337,8 +403,8 @@ import java.util.concurrent.atomic.AtomicBoolean; case MSG_SEEK_TO: seekToInternal((SeekPosition) msg.obj); break; - case MSG_SET_PLAYBACK_PARAMETERS: - setPlaybackParametersInternal((PlaybackParameters) msg.obj); + case MSG_SET_PLAYBACK_SPEED: + setPlaybackSpeedInternal((Float) msg.obj); break; case MSG_SET_SEEK_PARAMETERS: setSeekParametersInternal((SeekParameters) msg.obj); @@ -356,18 +422,14 @@ import java.util.concurrent.atomic.AtomicBoolean; case MSG_PERIOD_PREPARED: handlePeriodPrepared((MediaPeriod) msg.obj); break; - case MSG_REFRESH_SOURCE_INFO: - handleSourceInfoRefreshed((MediaSourceRefreshInfo) msg.obj); - break; case MSG_SOURCE_CONTINUE_LOADING_REQUESTED: handleContinueLoadingRequested((MediaPeriod) msg.obj); break; case MSG_TRACK_SELECTION_INVALIDATED: reselectTracksInternal(); break; - case MSG_PLAYBACK_PARAMETERS_CHANGED_INTERNAL: - handlePlaybackParameters( - (PlaybackParameters) msg.obj, /* acknowledgeCommand= */ msg.arg1 != 0); + case MSG_PLAYBACK_SPEED_CHANGED_INTERNAL: + handlePlaybackSpeed((Float) msg.obj, /* acknowledgeCommand= */ msg.arg1 != 0); break; case MSG_SEND_MESSAGE: sendMessageInternal((PlayerMessage) msg.obj); @@ -375,6 +437,27 @@ import java.util.concurrent.atomic.AtomicBoolean; case MSG_SEND_MESSAGE_TO_TARGET_THREAD: sendMessageToTargetThread((PlayerMessage) msg.obj); break; + case MSG_SET_MEDIA_SOURCES: + setMediaItemsInternal((MediaSourceListUpdateMessage) msg.obj); + break; + case MSG_ADD_MEDIA_SOURCES: + addMediaItemsInternal((MediaSourceListUpdateMessage) msg.obj, msg.arg1); + break; + case MSG_MOVE_MEDIA_SOURCES: + moveMediaItemsInternal((MoveMediaItemsMessage) msg.obj); + break; + case MSG_REMOVE_MEDIA_SOURCES: + removeMediaItemsInternal(msg.arg1, msg.arg2, (ShuffleOrder) msg.obj); + break; + case MSG_SET_SHUFFLE_ORDER: + setShuffleOrderInternal((ShuffleOrder) msg.obj); + break; + case MSG_PLAYLIST_UPDATE_REQUESTED: + mediaSourceListUpdateRequestedInternal(); + break; + case MSG_SET_PAUSE_AT_END_OF_WINDOW: + setPauseAtEndOfWindowInternal(msg.arg1 != 0); + break; case MSG_RELEASE: releaseInternal(); // Return immediately to not send playback info updates after release. @@ -384,7 +467,7 @@ import java.util.concurrent.atomic.AtomicBoolean; } maybeNotifyPlaybackInfoChanged(); } catch (ExoPlaybackException e) { - Log.e(TAG, "Playback error.", e); + Log.e(TAG, "Playback error", e); stopInternal( /* forceResetRenderers= */ true, /* resetPositionAndState= */ false, @@ -392,19 +475,20 @@ import java.util.concurrent.atomic.AtomicBoolean; playbackInfo = playbackInfo.copyWithPlaybackError(e); maybeNotifyPlaybackInfoChanged(); } catch (IOException e) { - Log.e(TAG, "Source error.", e); + ExoPlaybackException error = ExoPlaybackException.createForSource(e); + Log.e(TAG, "Playback error", error); stopInternal( /* forceResetRenderers= */ false, /* resetPositionAndState= */ false, /* acknowledgeStop= */ false); - playbackInfo = playbackInfo.copyWithPlaybackError(ExoPlaybackException.createForSource(e)); + playbackInfo = playbackInfo.copyWithPlaybackError(error); maybeNotifyPlaybackInfoChanged(); } catch (RuntimeException | OutOfMemoryError e) { - Log.e(TAG, "Internal runtime error.", e); ExoPlaybackException error = e instanceof OutOfMemoryError ? ExoPlaybackException.createForOutOfMemoryError((OutOfMemoryError) e) : ExoPlaybackException.createForUnexpected((RuntimeException) e); + Log.e(TAG, "Playback error", error); stopInternal( /* forceResetRenderers= */ true, /* resetPositionAndState= */ false, @@ -481,39 +565,97 @@ import java.util.concurrent.atomic.AtomicBoolean; } private void maybeNotifyPlaybackInfoChanged() { - if (playbackInfoUpdate.hasPendingUpdate(playbackInfo)) { - eventHandler - .obtainMessage( - MSG_PLAYBACK_INFO_CHANGED, - playbackInfoUpdate.operationAcks, - playbackInfoUpdate.positionDiscontinuity - ? playbackInfoUpdate.discontinuityReason - : C.INDEX_UNSET, - playbackInfo) - .sendToTarget(); - playbackInfoUpdate.reset(playbackInfo); + playbackInfoUpdate.setPlaybackInfo(playbackInfo); + if (playbackInfoUpdate.hasPendingChange) { + eventHandler.obtainMessage(MSG_PLAYBACK_INFO_CHANGED, playbackInfoUpdate).sendToTarget(); + playbackInfoUpdate = new PlaybackInfoUpdate(playbackInfo); } } - private void prepareInternal(MediaSource mediaSource, boolean resetPosition, boolean resetState) { - pendingPrepareCount++; + private void prepareInternal() { + playbackInfoUpdate.incrementPendingOperationAcks(/* operationAcks= */ 1); resetInternal( /* resetRenderers= */ false, - /* releaseMediaSource= */ true, - resetPosition, - resetState, + /* resetPosition= */ false, + /* releaseMediaSourceList= */ false, + /* clearMediaSourceList= */ false, /* resetError= */ true); loadControl.onPrepared(); - this.mediaSource = mediaSource; - setState(Player.STATE_BUFFERING); - mediaSource.prepareSource(/* caller= */ this, bandwidthMeter.getTransferListener()); + setState(playbackInfo.timeline.isEmpty() ? Player.STATE_ENDED : Player.STATE_BUFFERING); + mediaSourceList.prepare(bandwidthMeter.getTransferListener()); handler.sendEmptyMessage(MSG_DO_SOME_WORK); } - private void setPlayWhenReadyInternal(boolean playWhenReady) throws ExoPlaybackException { + private void setMediaItemsInternal(MediaSourceListUpdateMessage mediaSourceListUpdateMessage) + throws ExoPlaybackException { + playbackInfoUpdate.incrementPendingOperationAcks(/* operationAcks= */ 1); + if (mediaSourceListUpdateMessage.windowIndex != C.INDEX_UNSET) { + pendingInitialSeekPosition = + new SeekPosition( + new MediaSourceList.PlaylistTimeline( + mediaSourceListUpdateMessage.mediaSourceHolders, + mediaSourceListUpdateMessage.shuffleOrder), + mediaSourceListUpdateMessage.windowIndex, + mediaSourceListUpdateMessage.positionUs); + } + Timeline timeline = + mediaSourceList.setMediaSources( + mediaSourceListUpdateMessage.mediaSourceHolders, + mediaSourceListUpdateMessage.shuffleOrder); + handleMediaSourceListInfoRefreshed(timeline); + } + + private void addMediaItemsInternal(MediaSourceListUpdateMessage addMessage, int insertionIndex) + throws ExoPlaybackException { + playbackInfoUpdate.incrementPendingOperationAcks(/* operationAcks= */ 1); + Timeline timeline = + mediaSourceList.addMediaSources( + insertionIndex == C.INDEX_UNSET ? mediaSourceList.getSize() : insertionIndex, + addMessage.mediaSourceHolders, + addMessage.shuffleOrder); + handleMediaSourceListInfoRefreshed(timeline); + } + + private void moveMediaItemsInternal(MoveMediaItemsMessage moveMediaItemsMessage) + throws ExoPlaybackException { + playbackInfoUpdate.incrementPendingOperationAcks(/* operationAcks= */ 1); + Timeline timeline = + mediaSourceList.moveMediaSourceRange( + moveMediaItemsMessage.fromIndex, + moveMediaItemsMessage.toIndex, + moveMediaItemsMessage.newFromIndex, + moveMediaItemsMessage.shuffleOrder); + handleMediaSourceListInfoRefreshed(timeline); + } + + private void removeMediaItemsInternal(int fromIndex, int toIndex, ShuffleOrder shuffleOrder) + throws ExoPlaybackException { + playbackInfoUpdate.incrementPendingOperationAcks(/* operationAcks= */ 1); + Timeline timeline = mediaSourceList.removeMediaSourceRange(fromIndex, toIndex, shuffleOrder); + handleMediaSourceListInfoRefreshed(timeline); + } + + private void mediaSourceListUpdateRequestedInternal() throws ExoPlaybackException { + handleMediaSourceListInfoRefreshed(mediaSourceList.createTimeline()); + } + + private void setShuffleOrderInternal(ShuffleOrder shuffleOrder) throws ExoPlaybackException { + playbackInfoUpdate.incrementPendingOperationAcks(/* operationAcks= */ 1); + Timeline timeline = mediaSourceList.setShuffleOrder(shuffleOrder); + handleMediaSourceListInfoRefreshed(timeline); + } + + private void setPlayWhenReadyInternal( + boolean playWhenReady, + @PlaybackSuppressionReason int playbackSuppressionReason, + boolean operationAck, + @Player.PlayWhenReadyChangeReason int reason) + throws ExoPlaybackException { + playbackInfoUpdate.incrementPendingOperationAcks(operationAck ? 1 : 0); + playbackInfoUpdate.setPlayWhenReadyChangeReason(reason); + playbackInfo = playbackInfo.copyWithPlayWhenReady(playWhenReady, playbackSuppressionReason); rebuffering = false; - this.playWhenReady = playWhenReady; - if (!playWhenReady) { + if (!shouldPlayWhenReady()) { stopRenderers(); updatePlaybackPositions(); } else { @@ -526,10 +668,20 @@ import java.util.concurrent.atomic.AtomicBoolean; } } + private void setPauseAtEndOfWindowInternal(boolean pauseAtEndOfWindow) + throws ExoPlaybackException { + this.pauseAtEndOfWindow = pauseAtEndOfWindow; + if (queue.getReadingPeriod() != queue.getPlayingPeriod()) { + seekToCurrentPosition(/* sendDiscontinuity= */ true); + } + resetPendingPauseAtEndOfPeriod(); + handleLoadingMediaPeriodChanged(/* loadingTrackSelectionChanged= */ false); + } + private void setRepeatModeInternal(@Player.RepeatMode int repeatMode) throws ExoPlaybackException { this.repeatMode = repeatMode; - if (!queue.updateRepeatMode(repeatMode)) { + if (!queue.updateRepeatMode(playbackInfo.timeline, repeatMode)) { seekToCurrentPosition(/* sendDiscontinuity= */ true); } handleLoadingMediaPeriodChanged(/* loadingTrackSelectionChanged= */ false); @@ -538,7 +690,7 @@ import java.util.concurrent.atomic.AtomicBoolean; private void setShuffleModeEnabledInternal(boolean shuffleModeEnabled) throws ExoPlaybackException { this.shuffleModeEnabled = shuffleModeEnabled; - if (!queue.updateShuffleModeEnabled(shuffleModeEnabled)) { + if (!queue.updateShuffleModeEnabled(playbackInfo.timeline, shuffleModeEnabled)) { seekToCurrentPosition(/* sendDiscontinuity= */ true); } handleLoadingMediaPeriodChanged(/* loadingTrackSelectionChanged= */ false); @@ -549,9 +701,15 @@ import java.util.concurrent.atomic.AtomicBoolean; // position of the playing period to make sure none of the removed period is played. MediaPeriodId periodId = queue.getPlayingPeriod().info.id; long newPositionUs = - seekToPeriodPosition(periodId, playbackInfo.positionUs, /* forceDisableRenderers= */ true); + seekToPeriodPosition( + periodId, + playbackInfo.positionUs, + /* forceDisableRenderers= */ true, + /* forceBufferingState= */ false); if (newPositionUs != playbackInfo.positionUs) { - playbackInfo = copyWithNewPosition(periodId, newPositionUs, playbackInfo.contentPositionUs); + playbackInfo = + handlePositionDiscontinuity( + periodId, newPositionUs, playbackInfo.requestedContentPositionUs); if (sendDiscontinuity) { playbackInfoUpdate.setPositionDiscontinuity(Player.DISCONTINUITY_REASON_INTERNAL); } @@ -561,15 +719,19 @@ import java.util.concurrent.atomic.AtomicBoolean; private void startRenderers() throws ExoPlaybackException { rebuffering = false; mediaClock.start(); - for (Renderer renderer : enabledRenderers) { - renderer.start(); + for (Renderer renderer : renderers) { + if (isRendererEnabled(renderer)) { + renderer.start(); + } } } private void stopRenderers() throws ExoPlaybackException { mediaClock.stop(); - for (Renderer renderer : enabledRenderers) { - ensureStopped(renderer); + for (Renderer renderer : renderers) { + if (isRendererEnabled(renderer)) { + ensureStopped(renderer); + } } } @@ -590,8 +752,10 @@ import java.util.concurrent.atomic.AtomicBoolean; // renderers are flushed. Only report the discontinuity externally if the position changed. if (discontinuityPositionUs != playbackInfo.positionUs) { playbackInfo = - copyWithNewPosition( - playbackInfo.periodId, discontinuityPositionUs, playbackInfo.contentPositionUs); + handlePositionDiscontinuity( + playbackInfo.periodId, + discontinuityPositionUs, + playbackInfo.requestedContentPositionUs); playbackInfoUpdate.setPositionDiscontinuity(Player.DISCONTINUITY_REASON_INTERNAL); } } else { @@ -639,7 +803,7 @@ import java.util.concurrent.atomic.AtomicBoolean; playbackInfo.positionUs - backBufferDurationUs, retainBackBufferFromKeyframe); for (int i = 0; i < renderers.length; i++) { Renderer renderer = renderers[i]; - if (renderer.getState() == Renderer.STATE_DISABLED) { + if (!isRendererEnabled(renderer)) { continue; } // TODO: Each renderer should return the maximum delay before which it wishes to be called @@ -669,36 +833,57 @@ import java.util.concurrent.atomic.AtomicBoolean; } long playingPeriodDurationUs = playingPeriodHolder.info.durationUs; - if (renderersEnded - && playingPeriodHolder.prepared - && (playingPeriodDurationUs == C.TIME_UNSET - || playingPeriodDurationUs <= playbackInfo.positionUs) - && playingPeriodHolder.info.isFinal) { + boolean finishedRendering = + renderersEnded + && playingPeriodHolder.prepared + && (playingPeriodDurationUs == C.TIME_UNSET + || playingPeriodDurationUs <= playbackInfo.positionUs); + if (finishedRendering && pendingPauseAtEndOfPeriod) { + pendingPauseAtEndOfPeriod = false; + setPlayWhenReadyInternal( + /* playWhenReady= */ false, + playbackInfo.playbackSuppressionReason, + /* operationAck= */ false, + Player.PLAY_WHEN_READY_CHANGE_REASON_END_OF_MEDIA_ITEM); + } + if (finishedRendering && playingPeriodHolder.info.isFinal) { setState(Player.STATE_ENDED); stopRenderers(); } else if (playbackInfo.playbackState == Player.STATE_BUFFERING && shouldTransitionToReadyState(renderersAllowPlayback)) { setState(Player.STATE_READY); - if (playWhenReady) { + if (shouldPlayWhenReady()) { startRenderers(); } } else if (playbackInfo.playbackState == Player.STATE_READY - && !(enabledRenderers.length == 0 ? isTimelineReady() : renderersAllowPlayback)) { - rebuffering = playWhenReady; + && !(enabledRendererCount == 0 ? isTimelineReady() : renderersAllowPlayback)) { + rebuffering = shouldPlayWhenReady(); setState(Player.STATE_BUFFERING); stopRenderers(); } if (playbackInfo.playbackState == Player.STATE_BUFFERING) { - for (Renderer renderer : enabledRenderers) { - renderer.maybeThrowStreamError(); + for (int i = 0; i < renderers.length; i++) { + if (isRendererEnabled(renderers[i]) + && renderers[i].getStream() == playingPeriodHolder.sampleStreams[i]) { + renderers[i].maybeThrowStreamError(); + } + } + if (throwWhenStuckBuffering + && !playbackInfo.isLoading + && playbackInfo.totalBufferedDurationUs < 500_000 + && isLoadingPossible()) { + // Throw if the LoadControl prevents loading even if the buffer is empty or almost empty. We + // can't compare against 0 to account for small differences between the renderer position + // and buffered position in the media at the point where playback gets stuck. + throw new IllegalStateException("Playback stuck buffering and not loading"); } } - if ((playWhenReady && playbackInfo.playbackState == Player.STATE_READY) + if ((shouldPlayWhenReady() && playbackInfo.playbackState == Player.STATE_READY) || playbackInfo.playbackState == Player.STATE_BUFFERING) { scheduleNextWork(operationStartTimeMs, ACTIVE_INTERVAL_MS); - } else if (enabledRenderers.length != 0 && playbackInfo.playbackState != Player.STATE_ENDED) { + } else if (enabledRendererCount != 0 && playbackInfo.playbackState != Player.STATE_ENDED) { scheduleNextWork(operationStartTimeMs, IDLE_INTERVAL_MS); } else { handler.removeMessages(MSG_DO_SOME_WORK); @@ -717,43 +902,63 @@ import java.util.concurrent.atomic.AtomicBoolean; MediaPeriodId periodId; long periodPositionUs; - long contentPositionUs; + long requestedContentPosition; boolean seekPositionAdjusted; + @Nullable Pair resolvedSeekPosition = - resolveSeekPosition(seekPosition, /* trySubsequentPeriods= */ true); + resolveSeekPosition( + playbackInfo.timeline, + seekPosition, + /* trySubsequentPeriods= */ true, + repeatMode, + shuffleModeEnabled, + window, + period); 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 = playbackInfo.getDummyFirstMediaPeriodId(shuffleModeEnabled, window, period); - periodPositionUs = C.TIME_UNSET; - contentPositionUs = C.TIME_UNSET; - seekPositionAdjusted = true; + Pair firstPeriodAndPosition = + getDummyFirstMediaPeriodPosition(playbackInfo.timeline); + periodId = firstPeriodAndPosition.first; + periodPositionUs = firstPeriodAndPosition.second; + requestedContentPosition = C.TIME_UNSET; + seekPositionAdjusted = !playbackInfo.timeline.isEmpty(); } else { // Update the resolved seek position to take ads into account. Object periodUid = resolvedSeekPosition.first; - contentPositionUs = resolvedSeekPosition.second; - periodId = queue.resolveMediaPeriodIdForAds(periodUid, contentPositionUs); + long resolvedContentPosition = resolvedSeekPosition.second; + requestedContentPosition = + seekPosition.windowPositionUs == C.TIME_UNSET ? C.TIME_UNSET : resolvedContentPosition; + periodId = + queue.resolveMediaPeriodIdForAds( + playbackInfo.timeline, periodUid, resolvedContentPosition); if (periodId.isAd()) { - periodPositionUs = 0; + playbackInfo.timeline.getPeriodByUid(periodId.periodUid, period); + periodPositionUs = + period.getFirstAdIndexToPlay(periodId.adGroupIndex) == periodId.adIndexInAdGroup + ? period.getAdResumePositionUs() + : 0; seekPositionAdjusted = true; } else { - periodPositionUs = resolvedSeekPosition.second; + periodPositionUs = resolvedContentPosition; seekPositionAdjusted = seekPosition.windowPositionUs == C.TIME_UNSET; } } try { - if (mediaSource == null || pendingPrepareCount > 0) { + if (playbackInfo.timeline.isEmpty()) { // Save seek position for later, as we are still waiting for a prepared source. pendingInitialSeekPosition = seekPosition; - } else if (periodPositionUs == C.TIME_UNSET) { + } else if (resolvedSeekPosition == null) { // End playback, as we didn't manage to find a valid seek position. - setState(Player.STATE_ENDED); + if (playbackInfo.playbackState != Player.STATE_IDLE) { + setState(Player.STATE_ENDED); + } resetInternal( /* resetRenderers= */ false, - /* releaseMediaSource= */ false, /* resetPosition= */ true, - /* resetState= */ false, + /* releaseMediaSourceList= */ false, + /* clearMediaSourceList= */ false, /* resetError= */ true); } else { // Execute the seek in the current media periods. @@ -767,49 +972,62 @@ import java.util.concurrent.atomic.AtomicBoolean; playingPeriodHolder.mediaPeriod.getAdjustedSeekPositionUs( newPeriodPositionUs, seekParameters); } - if (C.usToMs(newPeriodPositionUs) == C.usToMs(playbackInfo.positionUs)) { + if (C.usToMs(newPeriodPositionUs) == C.usToMs(playbackInfo.positionUs) + && (playbackInfo.playbackState == Player.STATE_BUFFERING + || playbackInfo.playbackState == Player.STATE_READY)) { // Seek will be performed to the current position. Do nothing. periodPositionUs = playbackInfo.positionUs; return; } } - newPeriodPositionUs = seekToPeriodPosition(periodId, newPeriodPositionUs); + newPeriodPositionUs = + seekToPeriodPosition( + periodId, + newPeriodPositionUs, + /* forceBufferingState= */ playbackInfo.playbackState == Player.STATE_ENDED); seekPositionAdjusted |= periodPositionUs != newPeriodPositionUs; periodPositionUs = newPeriodPositionUs; } } finally { - playbackInfo = copyWithNewPosition(periodId, periodPositionUs, contentPositionUs); + playbackInfo = + handlePositionDiscontinuity(periodId, periodPositionUs, requestedContentPosition); if (seekPositionAdjusted) { playbackInfoUpdate.setPositionDiscontinuity(Player.DISCONTINUITY_REASON_SEEK_ADJUSTMENT); } } } - private long seekToPeriodPosition(MediaPeriodId periodId, long periodPositionUs) + private long seekToPeriodPosition( + MediaPeriodId periodId, long periodPositionUs, boolean forceBufferingState) throws ExoPlaybackException { // Force disable renderers if they are reading from a period other than the one being played. return seekToPeriodPosition( - periodId, periodPositionUs, queue.getPlayingPeriod() != queue.getReadingPeriod()); + periodId, + periodPositionUs, + queue.getPlayingPeriod() != queue.getReadingPeriod(), + forceBufferingState); } private long seekToPeriodPosition( - MediaPeriodId periodId, long periodPositionUs, boolean forceDisableRenderers) + MediaPeriodId periodId, + long periodPositionUs, + boolean forceDisableRenderers, + boolean forceBufferingState) throws ExoPlaybackException { stopRenderers(); rebuffering = false; - if (playbackInfo.playbackState != Player.STATE_IDLE && !playbackInfo.timeline.isEmpty()) { + if (forceBufferingState || playbackInfo.playbackState == Player.STATE_READY) { setState(Player.STATE_BUFFERING); } - // Clear the timeline, but keep the requested period if it is already prepared. - MediaPeriodHolder oldPlayingPeriodHolder = queue.getPlayingPeriod(); - MediaPeriodHolder newPlayingPeriodHolder = oldPlayingPeriodHolder; + // Find the requested period if it already exists. + @Nullable MediaPeriodHolder oldPlayingPeriodHolder = queue.getPlayingPeriod(); + @Nullable MediaPeriodHolder newPlayingPeriodHolder = oldPlayingPeriodHolder; while (newPlayingPeriodHolder != null) { - if (periodId.equals(newPlayingPeriodHolder.info.id) && newPlayingPeriodHolder.prepared) { - queue.removeAfter(newPlayingPeriodHolder); + if (periodId.equals(newPlayingPeriodHolder.info.id)) { break; } - newPlayingPeriodHolder = queue.advancePlayingPeriod(); + newPlayingPeriodHolder = newPlayingPeriodHolder.getNext(); } // Disable all renderers if the period being played is changing, if the seek results in negative @@ -818,31 +1036,43 @@ import java.util.concurrent.atomic.AtomicBoolean; || oldPlayingPeriodHolder != newPlayingPeriodHolder || (newPlayingPeriodHolder != null && newPlayingPeriodHolder.toRendererTime(periodPositionUs) < 0)) { - for (Renderer renderer : enabledRenderers) { + for (Renderer renderer : renderers) { disableRenderer(renderer); } - enabledRenderers = new Renderer[0]; - oldPlayingPeriodHolder = null; if (newPlayingPeriodHolder != null) { + // Update the queue and reenable renderers if the requested media period already exists. + while (queue.getPlayingPeriod() != newPlayingPeriodHolder) { + queue.advancePlayingPeriod(); + } + queue.removeAfter(newPlayingPeriodHolder); newPlayingPeriodHolder.setRendererOffset(/* rendererPositionOffsetUs= */ 0); + enableRenderers(); } } - // Update the holders. + // Do the actual seeking. if (newPlayingPeriodHolder != null) { - updatePlayingPeriodRenderers(oldPlayingPeriodHolder); - if (newPlayingPeriodHolder.hasEnabledTracks) { - periodPositionUs = newPlayingPeriodHolder.mediaPeriod.seekToUs(periodPositionUs); - newPlayingPeriodHolder.mediaPeriod.discardBuffer( - periodPositionUs - backBufferDurationUs, retainBackBufferFromKeyframe); + queue.removeAfter(newPlayingPeriodHolder); + if (!newPlayingPeriodHolder.prepared) { + newPlayingPeriodHolder.info = + newPlayingPeriodHolder.info.copyWithStartPositionUs(periodPositionUs); + } else { + if (newPlayingPeriodHolder.info.durationUs != C.TIME_UNSET + && periodPositionUs >= newPlayingPeriodHolder.info.durationUs) { + // Make sure seek position doesn't exceed period duration. + periodPositionUs = Math.max(0, newPlayingPeriodHolder.info.durationUs - 1); + } + if (newPlayingPeriodHolder.hasEnabledTracks) { + periodPositionUs = newPlayingPeriodHolder.mediaPeriod.seekToUs(periodPositionUs); + newPlayingPeriodHolder.mediaPeriod.discardBuffer( + periodPositionUs - backBufferDurationUs, retainBackBufferFromKeyframe); + } } resetRendererPosition(periodPositionUs); maybeContinueLoading(); } else { - queue.clear(/* keepFrontPeriodUid= */ true); // New period has not been prepared. - playbackInfo = - playbackInfo.copyWithTrackInfo(TrackGroupArray.EMPTY, emptyTrackSelectorResult); + queue.clear(); resetRendererPosition(periodPositionUs); } @@ -858,16 +1088,17 @@ import java.util.concurrent.atomic.AtomicBoolean; ? periodPositionUs : playingMediaPeriod.toRendererTime(periodPositionUs); mediaClock.resetPosition(rendererPositionUs); - for (Renderer renderer : enabledRenderers) { - renderer.resetPosition(rendererPositionUs); + for (Renderer renderer : renderers) { + if (isRendererEnabled(renderer)) { + renderer.resetPosition(rendererPositionUs); + } } notifyTrackSelectionDiscontinuity(); } - private void setPlaybackParametersInternal(PlaybackParameters playbackParameters) { - mediaClock.setPlaybackParameters(playbackParameters); - sendPlaybackParametersChangedInternal( - mediaClock.getPlaybackParameters(), /* acknowledgeCommand= */ true); + private void setPlaybackSpeedInternal(float playbackSpeed) { + mediaClock.setPlaybackSpeed(playbackSpeed); + sendPlaybackSpeedChangedInternal(mediaClock.getPlaybackSpeed(), /* acknowledgeCommand= */ true); } private void setSeekParametersInternal(SeekParameters seekParameters) { @@ -880,7 +1111,7 @@ import java.util.concurrent.atomic.AtomicBoolean; this.foregroundMode = foregroundMode; if (!foregroundMode) { for (Renderer renderer : renderers) { - if (renderer.getState() == Renderer.STATE_DISABLED) { + if (!isRendererEnabled(renderer)) { renderer.reset(); } } @@ -898,13 +1129,11 @@ import java.util.concurrent.atomic.AtomicBoolean; boolean forceResetRenderers, boolean resetPositionAndState, boolean acknowledgeStop) { resetInternal( /* resetRenderers= */ forceResetRenderers || !foregroundMode, - /* releaseMediaSource= */ true, /* resetPosition= */ resetPositionAndState, - /* resetState= */ resetPositionAndState, + /* releaseMediaSourceList= */ true, + /* clearMediaSourceList= */ resetPositionAndState, /* resetError= */ resetPositionAndState); - playbackInfoUpdate.incrementPendingOperationAcks( - pendingPrepareCount + (acknowledgeStop ? 1 : 0)); - pendingPrepareCount = 0; + playbackInfoUpdate.incrementPendingOperationAcks(acknowledgeStop ? 1 : 0); loadControl.onStopped(); setState(Player.STATE_IDLE); } @@ -912,9 +1141,9 @@ import java.util.concurrent.atomic.AtomicBoolean; private void releaseInternal() { resetInternal( /* resetRenderers= */ true, - /* releaseMediaSource= */ true, /* resetPosition= */ true, - /* resetState= */ true, + /* releaseMediaSourceList= */ true, + /* clearMediaSourceList= */ true, /* resetError= */ false); loadControl.onReleased(); setState(Player.STATE_IDLE); @@ -927,15 +1156,15 @@ import java.util.concurrent.atomic.AtomicBoolean; private void resetInternal( boolean resetRenderers, - boolean releaseMediaSource, boolean resetPosition, - boolean resetState, + boolean releaseMediaSourceList, + boolean clearMediaSourceList, boolean resetError) { handler.removeMessages(MSG_DO_SOME_WORK); rebuffering = false; mediaClock.stop(); rendererPositionUs = 0; - for (Renderer renderer : enabledRenderers) { + for (Renderer renderer : renderers) { try { disableRenderer(renderer); } catch (ExoPlaybackException | RuntimeException e) { @@ -953,72 +1182,99 @@ import java.util.concurrent.atomic.AtomicBoolean; } } } - enabledRenderers = new Renderer[0]; + enabledRendererCount = 0; - if (resetPosition) { - pendingInitialSeekPosition = null; - } else if (resetState) { - // When resetting the state, also reset the period-based PlaybackInfo position and convert - // existing position to initial seek instead. - resetPosition = true; - if (pendingInitialSeekPosition == null && !playbackInfo.timeline.isEmpty()) { - playbackInfo.timeline.getPeriodByUid(playbackInfo.periodId.periodUid, period); - long windowPositionUs = playbackInfo.positionUs + period.getPositionInWindowUs(); - pendingInitialSeekPosition = - new SeekPosition(Timeline.EMPTY, period.windowIndex, windowPositionUs); - } - } - - queue.clear(/* keepFrontPeriodUid= */ !resetState); - shouldContinueLoading = false; - if (resetState) { - queue.setTimeline(Timeline.EMPTY); + Timeline timeline = playbackInfo.timeline; + if (clearMediaSourceList) { + timeline = mediaSourceList.clear(/* shuffleOrder= */ null); for (PendingMessageInfo pendingMessageInfo : pendingMessages) { pendingMessageInfo.message.markAsProcessed(/* isDelivered= */ false); } pendingMessages.clear(); - nextPendingMessageIndex = 0; + resetPosition = true; } - MediaPeriodId mediaPeriodId = - resetPosition - ? playbackInfo.getDummyFirstMediaPeriodId(shuffleModeEnabled, window, period) - : playbackInfo.periodId; - // Set the start position to TIME_UNSET so that a subsequent seek to 0 isn't ignored. - long startPositionUs = resetPosition ? C.TIME_UNSET : playbackInfo.positionUs; - long contentPositionUs = resetPosition ? C.TIME_UNSET : playbackInfo.contentPositionUs; + MediaPeriodId mediaPeriodId = playbackInfo.periodId; + long startPositionUs = playbackInfo.positionUs; + long requestedContentPositionUs = + shouldUseRequestedContentPosition(playbackInfo, period, window) + ? playbackInfo.requestedContentPositionUs + : playbackInfo.positionUs; + boolean resetTrackInfo = clearMediaSourceList; + if (resetPosition) { + pendingInitialSeekPosition = null; + Pair firstPeriodAndPosition = getDummyFirstMediaPeriodPosition(timeline); + mediaPeriodId = firstPeriodAndPosition.first; + startPositionUs = firstPeriodAndPosition.second; + requestedContentPositionUs = C.TIME_UNSET; + if (!mediaPeriodId.equals(playbackInfo.periodId)) { + resetTrackInfo = true; + } + } + + queue.clear(); + shouldContinueLoading = false; + playbackInfo = new PlaybackInfo( - resetState ? Timeline.EMPTY : playbackInfo.timeline, + timeline, mediaPeriodId, - startPositionUs, - contentPositionUs, + requestedContentPositionUs, playbackInfo.playbackState, resetError ? null : playbackInfo.playbackError, /* isLoading= */ false, - resetState ? TrackGroupArray.EMPTY : playbackInfo.trackGroups, - resetState ? emptyTrackSelectorResult : playbackInfo.trackSelectorResult, + resetTrackInfo ? TrackGroupArray.EMPTY : playbackInfo.trackGroups, + resetTrackInfo ? emptyTrackSelectorResult : playbackInfo.trackSelectorResult, mediaPeriodId, + playbackInfo.playWhenReady, + playbackInfo.playbackSuppressionReason, startPositionUs, /* totalBufferedDurationUs= */ 0, startPositionUs); - if (releaseMediaSource) { - if (mediaSource != null) { - mediaSource.releaseSource(/* caller= */ this); - mediaSource = null; - } + if (releaseMediaSourceList) { + mediaSourceList.release(); } } + private Pair getDummyFirstMediaPeriodPosition(Timeline timeline) { + if (timeline.isEmpty()) { + return Pair.create(PlaybackInfo.getDummyPeriodForEmptyTimeline(), 0L); + } + int firstWindowIndex = timeline.getFirstWindowIndex(shuffleModeEnabled); + Pair firstPeriodAndPosition = + timeline.getPeriodPosition( + window, period, firstWindowIndex, /* windowPositionUs= */ C.TIME_UNSET); + // Add ad metadata if any and propagate the window sequence number to new period id. + MediaPeriodId firstPeriodId = + queue.resolveMediaPeriodIdForAds( + timeline, firstPeriodAndPosition.first, /* positionUs= */ 0); + long positionUs = firstPeriodAndPosition.second; + if (firstPeriodId.isAd()) { + timeline.getPeriodByUid(firstPeriodId.periodUid, period); + positionUs = + firstPeriodId.adIndexInAdGroup == period.getFirstAdIndexToPlay(firstPeriodId.adGroupIndex) + ? period.getAdResumePositionUs() + : 0; + } + return Pair.create(firstPeriodId, positionUs); + } + private void sendMessageInternal(PlayerMessage message) throws ExoPlaybackException { if (message.getPositionMs() == C.TIME_UNSET) { // If no delivery time is specified, trigger immediate message delivery. sendMessageToTarget(message); - } else if (mediaSource == null || pendingPrepareCount > 0) { + } else if (playbackInfo.timeline.isEmpty()) { // Still waiting for initial timeline to resolve position. pendingMessages.add(new PendingMessageInfo(message)); } else { PendingMessageInfo pendingMessageInfo = new PendingMessageInfo(message); - if (resolvePendingMessagePosition(pendingMessageInfo)) { + if (resolvePendingMessagePosition( + pendingMessageInfo, + /* newTimeline= */ playbackInfo.timeline, + /* previousTimeline= */ playbackInfo.timeline, + repeatMode, + shuffleModeEnabled, + window, + period)) { pendingMessages.add(pendingMessageInfo); // Ensure new message is inserted according to playback order. Collections.sort(pendingMessages); @@ -1070,9 +1326,20 @@ import java.util.concurrent.atomic.AtomicBoolean; } } - private void resolvePendingMessagePositions() { + private void resolvePendingMessagePositions(Timeline newTimeline, Timeline previousTimeline) { + if (newTimeline.isEmpty() && previousTimeline.isEmpty()) { + // Keep all messages unresolved until we have a non-empty timeline. + return; + } for (int i = pendingMessages.size() - 1; i >= 0; i--) { - if (!resolvePendingMessagePosition(pendingMessages.get(i))) { + if (!resolvePendingMessagePosition( + pendingMessages.get(i), + newTimeline, + previousTimeline, + repeatMode, + shuffleModeEnabled, + window, + period)) { // Unable to resolve a new position for the message. Remove it. pendingMessages.get(i).message.markAsProcessed(/* isDelivered= */ false); pendingMessages.remove(i); @@ -1082,50 +1349,22 @@ import java.util.concurrent.atomic.AtomicBoolean; Collections.sort(pendingMessages); } - private boolean resolvePendingMessagePosition(PendingMessageInfo pendingMessageInfo) { - if (pendingMessageInfo.resolvedPeriodUid == null) { - // Position is still unresolved. Try to find window in current timeline. - Pair periodPosition = - resolveSeekPosition( - new SeekPosition( - pendingMessageInfo.message.getTimeline(), - pendingMessageInfo.message.getWindowIndex(), - C.msToUs(pendingMessageInfo.message.getPositionMs())), - /* trySubsequentPeriods= */ false); - if (periodPosition == null) { - return false; - } - pendingMessageInfo.setResolvedPosition( - playbackInfo.timeline.getIndexOfPeriod(periodPosition.first), - periodPosition.second, - 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); - if (index == C.INDEX_UNSET) { - return false; - } - pendingMessageInfo.resolvedPeriodIndex = index; - } - return true; - } - private void maybeTriggerPendingMessages(long oldPeriodPositionUs, long newPeriodPositionUs) throws ExoPlaybackException { if (pendingMessages.isEmpty() || playbackInfo.periodId.isAd()) { return; } - // If this is the first call from the start position, include oldPeriodPositionUs in potential - // trigger positions, but make sure we deliver it only once. - if (playbackInfo.startPositionUs == oldPeriodPositionUs - && deliverPendingMessageAtStartPositionRequired) { + // If this is the first call after resetting the renderer position, include oldPeriodPositionUs + // in potential trigger positions, but make sure we deliver it only once. + if (deliverPendingMessageAtStartPositionRequired) { oldPeriodPositionUs--; + deliverPendingMessageAtStartPositionRequired = false; } - deliverPendingMessageAtStartPositionRequired = false; // Correct next index if necessary (e.g. after seeking, timeline changes, or new messages) int currentPeriodIndex = playbackInfo.timeline.getIndexOfPeriod(playbackInfo.periodId.periodUid); + int nextPendingMessageIndex = Math.min(nextPendingMessageIndexHint, pendingMessages.size()); PendingMessageInfo previousInfo = nextPendingMessageIndex > 0 ? pendingMessages.get(nextPendingMessageIndex - 1) : null; while (previousInfo != null @@ -1171,6 +1410,7 @@ import java.util.concurrent.atomic.AtomicBoolean; ? pendingMessages.get(nextPendingMessageIndex) : null; } + nextPendingMessageIndexHint = nextPendingMessageIndex; } private void ensureStopped(Renderer renderer) throws ExoPlaybackException { @@ -1180,13 +1420,17 @@ import java.util.concurrent.atomic.AtomicBoolean; } private void disableRenderer(Renderer renderer) throws ExoPlaybackException { + if (!isRendererEnabled(renderer)) { + return; + } mediaClock.onRendererDisabled(renderer); ensureStopped(renderer); renderer.disable(); + enabledRendererCount--; } private void reselectTracksInternal() throws ExoPlaybackException { - float playbackSpeed = mediaClock.getPlaybackParameters().speed; + float playbackSpeed = mediaClock.getPlaybackSpeed(); // Reselect tracks on each period in turn, until the selection changes. MediaPeriodHolder periodHolder = queue.getPlayingPeriod(); MediaPeriodHolder readingPeriodHolder = queue.getReadingPeriod(); @@ -1218,24 +1462,20 @@ import java.util.concurrent.atomic.AtomicBoolean; long periodPositionUs = playingPeriodHolder.applyTrackSelection( newTrackSelectorResult, playbackInfo.positionUs, recreateStreams, streamResetFlags); + playbackInfo = + handlePositionDiscontinuity( + playbackInfo.periodId, periodPositionUs, playbackInfo.requestedContentPositionUs); if (playbackInfo.playbackState != Player.STATE_ENDED && periodPositionUs != playbackInfo.positionUs) { - playbackInfo = - copyWithNewPosition( - playbackInfo.periodId, periodPositionUs, playbackInfo.contentPositionUs); playbackInfoUpdate.setPositionDiscontinuity(Player.DISCONTINUITY_REASON_INTERNAL); resetRendererPosition(periodPositionUs); } - int enabledRendererCount = 0; boolean[] rendererWasEnabledFlags = new boolean[renderers.length]; for (int i = 0; i < renderers.length; i++) { Renderer renderer = renderers[i]; - rendererWasEnabledFlags[i] = renderer.getState() != Renderer.STATE_DISABLED; + rendererWasEnabledFlags[i] = isRendererEnabled(renderer); SampleStream sampleStream = playingPeriodHolder.sampleStreams[i]; - if (sampleStream != null) { - enabledRendererCount++; - } if (rendererWasEnabledFlags[i]) { if (sampleStream != renderer.getStream()) { // We need to disable the renderer. @@ -1246,10 +1486,7 @@ import java.util.concurrent.atomic.AtomicBoolean; } } } - playbackInfo = - playbackInfo.copyWithTrackInfo( - playingPeriodHolder.getTrackGroups(), playingPeriodHolder.getTrackSelectorResult()); - enableRenderers(rendererWasEnabledFlags, enabledRendererCount); + enableRenderers(rendererWasEnabledFlags); } else { // Release and re-prepare/buffer periods after the one whose selection changed. queue.removeAfter(periodHolder); @@ -1295,7 +1532,7 @@ import java.util.concurrent.atomic.AtomicBoolean; } private boolean shouldTransitionToReadyState(boolean renderersReadyOrEnded) { - if (enabledRenderers.length == 0) { + if (enabledRendererCount == 0) { // If there are no enabled renderers, determine whether we're ready based on the timeline. return isTimelineReady(); } @@ -1312,7 +1549,7 @@ import java.util.concurrent.atomic.AtomicBoolean; boolean bufferedToEnd = loadingHolder.isFullyBuffered() && loadingHolder.info.isFinal; return bufferedToEnd || loadControl.shouldStartPlayback( - getTotalBufferedDurationUs(), mediaClock.getPlaybackParameters().speed, rebuffering); + getTotalBufferedDurationUs(), mediaClock.getPlaybackSpeed(), rebuffering); } private boolean isTimelineReady() { @@ -1320,114 +1557,87 @@ import java.util.concurrent.atomic.AtomicBoolean; long playingPeriodDurationUs = playingPeriodHolder.info.durationUs; return playingPeriodHolder.prepared && (playingPeriodDurationUs == C.TIME_UNSET - || playbackInfo.positionUs < playingPeriodDurationUs); + || playbackInfo.positionUs < playingPeriodDurationUs + || !shouldPlayWhenReady()); } private void maybeThrowSourceInfoRefreshError() throws IOException { MediaPeriodHolder loadingPeriodHolder = queue.getLoadingPeriod(); if (loadingPeriodHolder != null) { // Defer throwing until we read all available media periods. - for (Renderer renderer : enabledRenderers) { - if (!renderer.hasReadStreamToEnd()) { + for (Renderer renderer : renderers) { + if (isRendererEnabled(renderer) && !renderer.hasReadStreamToEnd()) { return; } } } - mediaSource.maybeThrowSourceInfoRefreshError(); + mediaSourceList.maybeThrowSourceInfoRefreshError(); } - private void handleSourceInfoRefreshed(MediaSourceRefreshInfo sourceRefreshInfo) - throws ExoPlaybackException { - if (sourceRefreshInfo.source != mediaSource) { - // Stale event. - return; - } - playbackInfoUpdate.incrementPendingOperationAcks(pendingPrepareCount); - pendingPrepareCount = 0; + private void handleMediaSourceListInfoRefreshed(Timeline timeline) throws ExoPlaybackException { + PositionUpdateForPlaylistChange positionUpdate = + resolvePositionForPlaylistChange( + timeline, + playbackInfo, + pendingInitialSeekPosition, + queue, + repeatMode, + shuffleModeEnabled, + window, + period); + MediaPeriodId newPeriodId = positionUpdate.periodId; + long newRequestedContentPositionUs = positionUpdate.requestedContentPositionUs; + boolean forceBufferingState = positionUpdate.forceBufferingState; + long newPositionUs = positionUpdate.periodPositionUs; + boolean periodPositionChanged = + !playbackInfo.periodId.equals(newPeriodId) || newPositionUs != playbackInfo.positionUs; - Timeline oldTimeline = playbackInfo.timeline; - Timeline timeline = sourceRefreshInfo.timeline; - queue.setTimeline(timeline); - playbackInfo = playbackInfo.copyWithTimeline(timeline); - resolvePendingMessagePositions(); - - MediaPeriodId newPeriodId = playbackInfo.periodId; - long oldContentPositionUs = - playbackInfo.periodId.isAd() ? playbackInfo.contentPositionUs : playbackInfo.positionUs; - long newContentPositionUs = oldContentPositionUs; - if (pendingInitialSeekPosition != null) { - // Resolve initial seek position. - Pair periodPosition = - resolveSeekPosition(pendingInitialSeekPosition, /* trySubsequentPeriods= */ true); - pendingInitialSeekPosition = null; - if (periodPosition == null) { - // The seek position was valid for the timeline that it was performed into, but the - // timeline has changed and a suitable seek position could not be resolved in the new one. - handleSourceInfoRefreshEndedPlayback(); - return; - } - newContentPositionUs = periodPosition.second; - newPeriodId = queue.resolveMediaPeriodIdForAds(periodPosition.first, newContentPositionUs); - } else if (oldContentPositionUs == C.TIME_UNSET && !timeline.isEmpty()) { - // Resolve unset start position to default position. - Pair defaultPosition = - getPeriodPosition( - timeline, timeline.getFirstWindowIndex(shuffleModeEnabled), C.TIME_UNSET); - newPeriodId = queue.resolveMediaPeriodIdForAds(defaultPosition.first, defaultPosition.second); - if (!newPeriodId.isAd()) { - // Keep unset start position if we need to play an ad first. - newContentPositionUs = defaultPosition.second; - } - } else if (timeline.getIndexOfPeriod(newPeriodId.periodUid) == C.INDEX_UNSET) { - // The current period isn't in the new timeline. Attempt to resolve a subsequent period whose - // window we can restart from. - Object newPeriodUid = resolveSubsequentPeriod(newPeriodId.periodUid, oldTimeline, timeline); - if (newPeriodUid == null) { - // We failed to resolve a suitable restart position. - handleSourceInfoRefreshEndedPlayback(); - return; - } - // We resolved a subsequent period. Start at the default position in the corresponding window. - Pair defaultPosition = - getPeriodPosition( - timeline, timeline.getPeriodByUid(newPeriodUid, period).windowIndex, C.TIME_UNSET); - newContentPositionUs = defaultPosition.second; - newPeriodId = queue.resolveMediaPeriodIdForAds(defaultPosition.first, newContentPositionUs); - } else { - // Recheck if the current ad still needs to be played or if we need to start playing an ad. - newPeriodId = - queue.resolveMediaPeriodIdForAds(playbackInfo.periodId.periodUid, newContentPositionUs); - if (!playbackInfo.periodId.isAd() && !newPeriodId.isAd()) { - // Drop update if we keep playing the same content (MediaPeriod.periodUid are identical) and - // only MediaPeriodId.nextAdGroupIndex may have changed. This postpones a potential - // discontinuity until we reach the former next ad group position. - newPeriodId = playbackInfo.periodId; - } - } - - if (playbackInfo.periodId.equals(newPeriodId) && oldContentPositionUs == newContentPositionUs) { - // We can keep the current playing period. Update the rest of the queued periods. - if (!queue.updateQueuedPeriods(rendererPositionUs, getMaxRendererReadPositionUs())) { - seekToCurrentPosition(/* sendDiscontinuity= */ false); - } - } else { - // Something changed. Seek to new start position. - MediaPeriodHolder periodHolder = queue.getPlayingPeriod(); - if (periodHolder != null) { - // Update the new playing media period info if it already exists. - while (periodHolder.getNext() != null) { - periodHolder = periodHolder.getNext(); - if (periodHolder.info.id.equals(newPeriodId)) { - periodHolder.info = queue.getUpdatedMediaPeriodInfo(periodHolder.info); - } + try { + if (positionUpdate.endPlayback) { + if (playbackInfo.playbackState != Player.STATE_IDLE) { + setState(Player.STATE_ENDED); } + resetInternal( + /* resetRenderers= */ false, + /* resetPosition= */ false, + /* releaseMediaSourceList= */ false, + /* clearMediaSourceList= */ false, + /* resetError= */ true); } - // Actually do the seek. - long newPositionUs = newPeriodId.isAd() ? 0 : newContentPositionUs; - long seekedToPositionUs = seekToPeriodPosition(newPeriodId, newPositionUs); - playbackInfo = copyWithNewPosition(newPeriodId, seekedToPositionUs, newContentPositionUs); + if (!periodPositionChanged) { + // We can keep the current playing period. Update the rest of the queued periods. + if (!queue.updateQueuedPeriods( + timeline, rendererPositionUs, getMaxRendererReadPositionUs())) { + seekToCurrentPosition(/* sendDiscontinuity= */ false); + } + } else if (!timeline.isEmpty()) { + // Something changed. Seek to new start position. + @Nullable MediaPeriodHolder periodHolder = queue.getPlayingPeriod(); + while (periodHolder != null) { + // Update the new playing media period info if it already exists. + if (periodHolder.info.id.equals(newPeriodId)) { + periodHolder.info = queue.getUpdatedMediaPeriodInfo(timeline, periodHolder.info); + } + periodHolder = periodHolder.getNext(); + } + newPositionUs = seekToPeriodPosition(newPeriodId, newPositionUs, forceBufferingState); + } + } finally { + if (periodPositionChanged + || newRequestedContentPositionUs != playbackInfo.requestedContentPositionUs) { + playbackInfo = + handlePositionDiscontinuity(newPeriodId, newPositionUs, newRequestedContentPositionUs); + } + resetPendingPauseAtEndOfPeriod(); + resolvePendingMessagePositions( + /* newTimeline= */ timeline, /* previousTimeline= */ playbackInfo.timeline); + playbackInfo = playbackInfo.copyWithTimeline(timeline); + if (!timeline.isEmpty()) { + // Retain pending seek position only while the timeline is still empty. + pendingInitialSeekPosition = null; + } + handleLoadingMediaPeriodChanged(/* loadingTrackSelectionChanged= */ false); } - handleLoadingMediaPeriodChanged(/* loadingTrackSelectionChanged= */ false); } private long getMaxRendererReadPositionUs() { @@ -1440,7 +1650,7 @@ import java.util.concurrent.atomic.AtomicBoolean; return maxReadPositionUs; } for (int i = 0; i < renderers.length; i++) { - if (renderers[i].getState() == Renderer.STATE_DISABLED + if (!isRendererEnabled(renderers[i]) || renderers[i].getStream() != readingHolder.sampleStreams[i]) { // Ignore disabled renderers and renderers with sample streams from previous periods. continue; @@ -1455,127 +1665,15 @@ import java.util.concurrent.atomic.AtomicBoolean; return maxReadPositionUs; } - private void handleSourceInfoRefreshEndedPlayback() { - if (playbackInfo.playbackState != Player.STATE_IDLE) { - setState(Player.STATE_ENDED); - } - // Reset, but retain the source so that it can still be used should a seek occur. - resetInternal( - /* resetRenderers= */ false, - /* releaseMediaSource= */ false, - /* resetPosition= */ true, - /* resetState= */ false, - /* resetError= */ true); - } - - /** - * Given a period index into an old timeline, finds the first subsequent period that also exists - * in a new timeline. The uid of this period in the new timeline is returned. - * - * @param oldPeriodUid The index of the period in the old timeline. - * @param oldTimeline The old timeline. - * @param newTimeline The new timeline. - * @return The uid in the new timeline of the first subsequent period, or null if no such period - * was found. - */ - private @Nullable Object resolveSubsequentPeriod( - Object oldPeriodUid, Timeline oldTimeline, Timeline newTimeline) { - int oldPeriodIndex = oldTimeline.getIndexOfPeriod(oldPeriodUid); - int newPeriodIndex = C.INDEX_UNSET; - int maxIterations = oldTimeline.getPeriodCount(); - for (int i = 0; i < maxIterations && newPeriodIndex == C.INDEX_UNSET; i++) { - oldPeriodIndex = - oldTimeline.getNextPeriodIndex( - oldPeriodIndex, period, window, repeatMode, shuffleModeEnabled); - if (oldPeriodIndex == C.INDEX_UNSET) { - // We've reached the end of the old timeline. - break; - } - newPeriodIndex = newTimeline.getIndexOfPeriod(oldTimeline.getUidOfPeriod(oldPeriodIndex)); - } - return newPeriodIndex == C.INDEX_UNSET ? null : newTimeline.getUidOfPeriod(newPeriodIndex); - } - - /** - * Converts a {@link SeekPosition} into the corresponding (periodUid, periodPositionUs) for the - * internal timeline. - * - * @param seekPosition The position to resolve. - * @param trySubsequentPeriods Whether the position can be resolved to a subsequent matching - * period if the original period is no longer available. - * @return The resolved position, or null if resolution was not successful. - * @throws IllegalSeekPositionException If the window index of the seek position is outside the - * bounds of the timeline. - */ - @Nullable - private Pair resolveSeekPosition( - SeekPosition seekPosition, boolean trySubsequentPeriods) { - Timeline timeline = playbackInfo.timeline; - Timeline seekTimeline = seekPosition.timeline; - if (timeline.isEmpty()) { - // We don't have a valid timeline yet, so we can't resolve the position. - return null; - } - if (seekTimeline.isEmpty()) { - // The application performed a blind seek with an empty timeline (most likely based on - // knowledge of what the future timeline will be). Use the internal timeline. - seekTimeline = timeline; - } - // Map the SeekPosition to a position in the corresponding timeline. - Pair periodPosition; - try { - periodPosition = - seekTimeline.getPeriodPosition( - window, period, seekPosition.windowIndex, seekPosition.windowPositionUs); - } catch (IndexOutOfBoundsException e) { - // The window index of the seek position was outside the bounds of the timeline. - return null; - } - if (timeline == seekTimeline) { - // Our internal timeline is the seek timeline, so the mapped position is correct. - return periodPosition; - } - // Attempt to find the mapped period in the internal timeline. - int periodIndex = timeline.getIndexOfPeriod(periodPosition.first); - if (periodIndex != C.INDEX_UNSET) { - // We successfully located the period in the internal timeline. - return periodPosition; - } - if (trySubsequentPeriods) { - // Try and find a subsequent period from the seek timeline in the internal timeline. - @Nullable - Object periodUid = resolveSubsequentPeriod(periodPosition.first, seekTimeline, timeline); - if (periodUid != null) { - // We found one. Use the default position of the corresponding window. - return getPeriodPosition( - timeline, timeline.getPeriodByUid(periodUid, period).windowIndex, C.TIME_UNSET); - } - } - // We didn't find one. Give up. - return null; - } - - /** - * Calls {@link Timeline#getPeriodPosition(Timeline.Window, Timeline.Period, int, long)} using the - * current timeline. - */ - private Pair getPeriodPosition( - Timeline timeline, int windowIndex, long windowPositionUs) { - return timeline.getPeriodPosition(window, period, windowIndex, windowPositionUs); - } - private void updatePeriods() throws ExoPlaybackException, IOException { - if (mediaSource == null) { - // The player has no media source yet. - return; - } - if (pendingPrepareCount > 0) { + if (playbackInfo.timeline.isEmpty() || !mediaSourceList.isPrepared()) { // We're waiting to get information about periods. - mediaSource.maybeThrowSourceInfoRefreshError(); + mediaSourceList.maybeThrowSourceInfoRefreshError(); return; } maybeUpdateLoadingPeriod(); maybeUpdateReadingPeriod(); + maybeUpdateReadingRenderers(); maybeUpdatePlayingPeriod(); } @@ -1591,7 +1689,7 @@ import java.util.concurrent.atomic.AtomicBoolean; rendererCapabilities, trackSelector, loadControl.getAllocator(), - mediaSource, + mediaSourceList, info, emptyTrackSelectorResult); mediaPeriodHolder.mediaPeriod.prepare(this, info.startPositionUs); @@ -1602,6 +1700,8 @@ import java.util.concurrent.atomic.AtomicBoolean; } } if (shouldContinueLoading) { + // We should still be loading, except in the case that it's no longer possible (i.e., because + // we've loaded the current playlist to the end). shouldContinueLoading = isLoadingPossible(); updateIsLoading(); } else { @@ -1609,15 +1709,16 @@ import java.util.concurrent.atomic.AtomicBoolean; } } - private void maybeUpdateReadingPeriod() throws ExoPlaybackException { - MediaPeriodHolder readingPeriodHolder = queue.getReadingPeriod(); + private void maybeUpdateReadingPeriod() { + @Nullable MediaPeriodHolder readingPeriodHolder = queue.getReadingPeriod(); if (readingPeriodHolder == null) { return; } - if (readingPeriodHolder.getNext() == null) { - // We don't have a successor to advance the reading period to. - if (readingPeriodHolder.info.isFinal) { + if (readingPeriodHolder.getNext() == null || pendingPauseAtEndOfPeriod) { + // We don't have a successor to advance the reading period to or we want to let them end + // intentionally to pause at the end of the period. + if (readingPeriodHolder.info.isFinal || pendingPauseAtEndOfPeriod) { for (int i = 0; i < renderers.length; i++) { Renderer renderer = renderers[i]; SampleStream sampleStream = readingPeriodHolder.sampleStreams[i]; @@ -1637,8 +1738,9 @@ import java.util.concurrent.atomic.AtomicBoolean; return; } - if (!readingPeriodHolder.getNext().prepared) { - // The successor is not prepared yet. + if (!readingPeriodHolder.getNext().prepared + && rendererPositionUs < readingPeriodHolder.getNext().getStartPositionRendererTime()) { + // The successor is not prepared yet and playback hasn't reached the transition point. return; } @@ -1646,47 +1748,77 @@ import java.util.concurrent.atomic.AtomicBoolean; readingPeriodHolder = queue.advanceReadingPeriod(); TrackSelectorResult newTrackSelectorResult = readingPeriodHolder.getTrackSelectorResult(); - if (readingPeriodHolder.mediaPeriod.readDiscontinuity() != C.TIME_UNSET) { + if (readingPeriodHolder.prepared + && readingPeriodHolder.mediaPeriod.readDiscontinuity() != C.TIME_UNSET) { // The new period starts with a discontinuity, so the renderers will play out all data, then // be disabled and re-enabled when they start playing the next period. setAllRendererStreamsFinal(); return; } for (int i = 0; i < renderers.length; i++) { - Renderer renderer = renderers[i]; - boolean rendererWasEnabled = oldTrackSelectorResult.isRendererEnabled(i); - if (rendererWasEnabled && !renderer.isCurrentStreamFinal()) { - // The renderer is enabled and its stream is not final, so we still have a chance to replace - // the sample streams. - TrackSelection newSelection = newTrackSelectorResult.selections.get(i); - boolean newRendererEnabled = newTrackSelectorResult.isRendererEnabled(i); + boolean oldRendererEnabled = oldTrackSelectorResult.isRendererEnabled(i); + boolean newRendererEnabled = newTrackSelectorResult.isRendererEnabled(i); + if (oldRendererEnabled && !renderers[i].isCurrentStreamFinal()) { boolean isNoSampleRenderer = rendererCapabilities[i].getTrackType() == C.TRACK_TYPE_NONE; RendererConfiguration oldConfig = oldTrackSelectorResult.rendererConfigurations[i]; RendererConfiguration newConfig = newTrackSelectorResult.rendererConfigurations[i]; - if (newRendererEnabled && newConfig.equals(oldConfig) && !isNoSampleRenderer) { - // Replace the renderer's SampleStream so the transition to playing the next period can - // be seamless. - // This should be avoided for no-sample renderer, because skipping ahead for such - // renderer doesn't have any benefit (the renderer does not consume the sample stream), - // and it will change the provided rendererOffsetUs while the renderer is still - // rendering from the playing media period. - Format[] formats = getFormats(newSelection); - renderer.replaceStream( - formats, - readingPeriodHolder.sampleStreams[i], - readingPeriodHolder.getRendererOffset()); - } else { + if (!newRendererEnabled || !newConfig.equals(oldConfig) || isNoSampleRenderer) { // The renderer will be disabled when transitioning to playing the next period, because // there's no new selection, or because a configuration change is required, or because // it's a no-sample renderer for which rendererOffsetUs should be updated only when // starting to play the next period. Mark the SampleStream as final to play out any // remaining data. - renderer.setCurrentStreamFinal(); + renderers[i].setCurrentStreamFinal(); } } } } + private void maybeUpdateReadingRenderers() throws ExoPlaybackException { + @Nullable MediaPeriodHolder readingPeriod = queue.getReadingPeriod(); + if (readingPeriod == null + || queue.getPlayingPeriod() == readingPeriod + || readingPeriod.allRenderersEnabled) { + // Not reading ahead or all renderers updated. + return; + } + if (replaceStreamsOrDisableRendererForTransition()) { + enableRenderers(); + } + } + + private boolean replaceStreamsOrDisableRendererForTransition() throws ExoPlaybackException { + MediaPeriodHolder readingPeriodHolder = queue.getReadingPeriod(); + TrackSelectorResult newTrackSelectorResult = readingPeriodHolder.getTrackSelectorResult(); + boolean needsToWaitForRendererToEnd = false; + for (int i = 0; i < renderers.length; i++) { + Renderer renderer = renderers[i]; + if (!isRendererEnabled(renderer)) { + continue; + } + boolean rendererIsReadingOldStream = + renderer.getStream() != readingPeriodHolder.sampleStreams[i]; + boolean rendererShouldBeEnabled = newTrackSelectorResult.isRendererEnabled(i); + if (rendererShouldBeEnabled && !rendererIsReadingOldStream) { + // All done. + continue; + } + if (!renderer.isCurrentStreamFinal()) { + // The renderer stream is not final, so we can replace the sample streams immediately. + Format[] formats = getFormats(newTrackSelectorResult.selections.get(i)); + renderer.replaceStream( + formats, readingPeriodHolder.sampleStreams[i], readingPeriodHolder.getRendererOffset()); + } else if (renderer.isEnded()) { + // The renderer has finished playback, so we can disable it now. + disableRenderer(renderer); + } else { + // We need to wait until rendering finished before disabling the renderer. + needsToWaitForRendererToEnd = true; + } + } + return !needsToWaitForRendererToEnd; + } + private void maybeUpdatePlayingPeriod() throws ExoPlaybackException { boolean advancedPlayingPeriod = false; while (shouldAdvancePlayingPeriod()) { @@ -1695,30 +1827,34 @@ import java.util.concurrent.atomic.AtomicBoolean; maybeNotifyPlaybackInfoChanged(); } MediaPeriodHolder oldPlayingPeriodHolder = queue.getPlayingPeriod(); - if (oldPlayingPeriodHolder == queue.getReadingPeriod()) { - // The reading period hasn't advanced yet, so we can't seamlessly replace the SampleStreams - // anymore and need to re-enable the renderers. Set all current streams final to do that. - setAllRendererStreamsFinal(); - } MediaPeriodHolder newPlayingPeriodHolder = queue.advancePlayingPeriod(); - updatePlayingPeriodRenderers(oldPlayingPeriodHolder); playbackInfo = - copyWithNewPosition( + handlePositionDiscontinuity( newPlayingPeriodHolder.info.id, newPlayingPeriodHolder.info.startPositionUs, - newPlayingPeriodHolder.info.contentPositionUs); + newPlayingPeriodHolder.info.requestedContentPositionUs); int discontinuityReason = oldPlayingPeriodHolder.info.isLastInTimelinePeriod ? Player.DISCONTINUITY_REASON_PERIOD_TRANSITION : Player.DISCONTINUITY_REASON_AD_INSERTION; playbackInfoUpdate.setPositionDiscontinuity(discontinuityReason); + resetPendingPauseAtEndOfPeriod(); updatePlaybackPositions(); advancedPlayingPeriod = true; } } + private void resetPendingPauseAtEndOfPeriod() { + @Nullable MediaPeriodHolder playingPeriod = queue.getPlayingPeriod(); + pendingPauseAtEndOfPeriod = + playingPeriod != null && playingPeriod.info.isLastInTimelineWindow && pauseAtEndOfWindow; + } + private boolean shouldAdvancePlayingPeriod() { - if (!playWhenReady) { + if (!shouldPlayWhenReady()) { + return false; + } + if (pendingPauseAtEndOfPeriod) { return false; } MediaPeriodHolder playingPeriodHolder = queue.getPlayingPeriod(); @@ -1726,14 +1862,9 @@ import java.util.concurrent.atomic.AtomicBoolean; return false; } MediaPeriodHolder nextPlayingPeriodHolder = playingPeriodHolder.getNext(); - if (nextPlayingPeriodHolder == null) { - return false; - } - MediaPeriodHolder readingPeriodHolder = queue.getReadingPeriod(); - if (playingPeriodHolder == readingPeriodHolder && !hasReadingPeriodFinishedReading()) { - return false; - } - return rendererPositionUs >= nextPlayingPeriodHolder.getStartPositionRendererTime(); + return nextPlayingPeriodHolder != null + && rendererPositionUs >= nextPlayingPeriodHolder.getStartPositionRendererTime() + && nextPlayingPeriodHolder.allRenderersEnabled; } private boolean hasReadingPeriodFinishedReading() { @@ -1767,14 +1898,18 @@ import java.util.concurrent.atomic.AtomicBoolean; return; } MediaPeriodHolder loadingPeriodHolder = queue.getLoadingPeriod(); - loadingPeriodHolder.handlePrepared( - mediaClock.getPlaybackParameters().speed, playbackInfo.timeline); + loadingPeriodHolder.handlePrepared(mediaClock.getPlaybackSpeed(), playbackInfo.timeline); updateLoadControlTrackSelection( loadingPeriodHolder.getTrackGroups(), loadingPeriodHolder.getTrackSelectorResult()); if (loadingPeriodHolder == queue.getPlayingPeriod()) { // This is the first prepared period, so update the position and the renderers. resetRendererPosition(loadingPeriodHolder.info.startPositionUs); - updatePlayingPeriodRenderers(/* oldPlayingPeriodHolder= */ null); + enableRenderers(); + playbackInfo = + handlePositionDiscontinuity( + playbackInfo.periodId, + loadingPeriodHolder.info.startPositionUs, + playbackInfo.requestedContentPositionUs); } maybeContinueLoading(); } @@ -1788,17 +1923,15 @@ import java.util.concurrent.atomic.AtomicBoolean; maybeContinueLoading(); } - private void handlePlaybackParameters( - PlaybackParameters playbackParameters, boolean acknowledgeCommand) + private void handlePlaybackSpeed(float playbackSpeed, boolean acknowledgeCommand) throws ExoPlaybackException { eventHandler - .obtainMessage( - MSG_PLAYBACK_PARAMETERS_CHANGED, acknowledgeCommand ? 1 : 0, 0, playbackParameters) + .obtainMessage(MSG_PLAYBACK_SPEED_CHANGED, acknowledgeCommand ? 1 : 0, 0, playbackSpeed) .sendToTarget(); - updateTrackSelectionPlaybackSpeed(playbackParameters.speed); + updateTrackSelectionPlaybackSpeed(playbackSpeed); for (Renderer renderer : renderers) { if (renderer != null) { - renderer.setOperatingRate(playbackParameters.speed); + renderer.setOperatingRate(playbackSpeed); } } } @@ -1815,17 +1948,16 @@ import java.util.concurrent.atomic.AtomicBoolean; if (!isLoadingPossible()) { return false; } + MediaPeriodHolder loadingPeriodHolder = queue.getLoadingPeriod(); long bufferedDurationUs = - getTotalBufferedDurationUs(queue.getLoadingPeriod().getNextLoadPositionUs()); - if (bufferedDurationUs < 500_000) { - // Prevent loading from getting stuck even if LoadControl.shouldContinueLoading returns false - // when the buffer is empty or almost empty. We can't compare against 0 to account for small - // differences between the renderer position and buffered position in the media at the point - // where playback gets stuck. - return true; - } - float playbackSpeed = mediaClock.getPlaybackParameters().speed; - return loadControl.shouldContinueLoading(bufferedDurationUs, playbackSpeed); + getTotalBufferedDurationUs(loadingPeriodHolder.getNextLoadPositionUs()); + long playbackPositionUs = + loadingPeriodHolder == queue.getPlayingPeriod() + ? loadingPeriodHolder.toPeriodTime(rendererPositionUs) + : loadingPeriodHolder.toPeriodTime(rendererPositionUs) + - loadingPeriodHolder.info.startPositionUs; + return loadControl.shouldContinueLoading( + playbackPositionUs, bufferedDurationUs, mediaClock.getPlaybackSpeed()); } private boolean isLoadingPossible() { @@ -1849,50 +1981,47 @@ import java.util.concurrent.atomic.AtomicBoolean; } } - private PlaybackInfo copyWithNewPosition( + @CheckResult + private PlaybackInfo handlePositionDiscontinuity( MediaPeriodId mediaPeriodId, long positionUs, long contentPositionUs) { - deliverPendingMessageAtStartPositionRequired = true; + deliverPendingMessageAtStartPositionRequired = + deliverPendingMessageAtStartPositionRequired + || positionUs != playbackInfo.positionUs + || !mediaPeriodId.equals(playbackInfo.periodId); + resetPendingPauseAtEndOfPeriod(); + TrackGroupArray trackGroupArray = playbackInfo.trackGroups; + TrackSelectorResult trackSelectorResult = playbackInfo.trackSelectorResult; + if (mediaSourceList.isPrepared()) { + @Nullable MediaPeriodHolder playingPeriodHolder = queue.getPlayingPeriod(); + trackGroupArray = + playingPeriodHolder == null + ? TrackGroupArray.EMPTY + : playingPeriodHolder.getTrackGroups(); + trackSelectorResult = + playingPeriodHolder == null + ? emptyTrackSelectorResult + : playingPeriodHolder.getTrackSelectorResult(); + } else if (!mediaPeriodId.equals(playbackInfo.periodId)) { + // Reset previously kept track info if unprepared and the period changes. + trackGroupArray = TrackGroupArray.EMPTY; + trackSelectorResult = emptyTrackSelectorResult; + } return playbackInfo.copyWithNewPosition( - mediaPeriodId, positionUs, contentPositionUs, getTotalBufferedDurationUs()); + mediaPeriodId, + positionUs, + contentPositionUs, + getTotalBufferedDurationUs(), + trackGroupArray, + trackSelectorResult); } - @SuppressWarnings("ParameterNotNullable") - private void updatePlayingPeriodRenderers(@Nullable MediaPeriodHolder oldPlayingPeriodHolder) - throws ExoPlaybackException { - MediaPeriodHolder newPlayingPeriodHolder = queue.getPlayingPeriod(); - if (newPlayingPeriodHolder == null || oldPlayingPeriodHolder == newPlayingPeriodHolder) { - return; - } - int enabledRendererCount = 0; - boolean[] rendererWasEnabledFlags = new boolean[renderers.length]; - for (int i = 0; i < renderers.length; i++) { - Renderer renderer = renderers[i]; - rendererWasEnabledFlags[i] = renderer.getState() != Renderer.STATE_DISABLED; - if (newPlayingPeriodHolder.getTrackSelectorResult().isRendererEnabled(i)) { - enabledRendererCount++; - } - if (rendererWasEnabledFlags[i] - && (!newPlayingPeriodHolder.getTrackSelectorResult().isRendererEnabled(i) - || (renderer.isCurrentStreamFinal() - && renderer.getStream() == oldPlayingPeriodHolder.sampleStreams[i]))) { - // The renderer should be disabled before playing the next period, either because it's not - // needed to play the next period, or because we need to re-enable it as its current stream - // is final and it's not reading ahead. - disableRenderer(renderer); - } - } - playbackInfo = - playbackInfo.copyWithTrackInfo( - newPlayingPeriodHolder.getTrackGroups(), - newPlayingPeriodHolder.getTrackSelectorResult()); - enableRenderers(rendererWasEnabledFlags, enabledRendererCount); + private void enableRenderers() throws ExoPlaybackException { + enableRenderers(/* rendererWasEnabledFlags= */ new boolean[renderers.length]); } - private void enableRenderers(boolean[] rendererWasEnabledFlags, int totalEnabledRendererCount) - throws ExoPlaybackException { - enabledRenderers = new Renderer[totalEnabledRendererCount]; - int enabledRendererCount = 0; - TrackSelectorResult trackSelectorResult = queue.getPlayingPeriod().getTrackSelectorResult(); + private void enableRenderers(boolean[] rendererWasEnabledFlags) throws ExoPlaybackException { + MediaPeriodHolder readingMediaPeriod = queue.getReadingPeriod(); + TrackSelectorResult trackSelectorResult = readingMediaPeriod.getTrackSelectorResult(); // Reset all disabled renderers before enabling any new ones. This makes sure resources released // by the disabled renderers will be available to renderers that are being enabled. for (int i = 0; i < renderers.length; i++) { @@ -1903,40 +2032,43 @@ import java.util.concurrent.atomic.AtomicBoolean; // Enable the renderers. for (int i = 0; i < renderers.length; i++) { if (trackSelectorResult.isRendererEnabled(i)) { - enableRenderer(i, rendererWasEnabledFlags[i], enabledRendererCount++); + enableRenderer(i, rendererWasEnabledFlags[i]); } } + readingMediaPeriod.allRenderersEnabled = true; } - private void enableRenderer( - int rendererIndex, boolean wasRendererEnabled, int enabledRendererIndex) + private void enableRenderer(int rendererIndex, boolean wasRendererEnabled) throws ExoPlaybackException { - MediaPeriodHolder playingPeriodHolder = queue.getPlayingPeriod(); Renderer renderer = renderers[rendererIndex]; - enabledRenderers[enabledRendererIndex] = renderer; - if (renderer.getState() == Renderer.STATE_DISABLED) { - TrackSelectorResult trackSelectorResult = playingPeriodHolder.getTrackSelectorResult(); - RendererConfiguration rendererConfiguration = - trackSelectorResult.rendererConfigurations[rendererIndex]; - TrackSelection newSelection = trackSelectorResult.selections.get(rendererIndex); - Format[] formats = getFormats(newSelection); - // The renderer needs enabling with its new track selection. - boolean playing = playWhenReady && playbackInfo.playbackState == Player.STATE_READY; - // Consider as joining only if the renderer was previously disabled. - boolean joining = !wasRendererEnabled && playing; - // Enable the renderer. - renderer.enable( - rendererConfiguration, - formats, - playingPeriodHolder.sampleStreams[rendererIndex], - rendererPositionUs, - joining, - playingPeriodHolder.getRendererOffset()); - mediaClock.onRendererEnabled(renderer); - // Start the renderer if playing. - if (playing) { - renderer.start(); - } + if (isRendererEnabled(renderer)) { + return; + } + MediaPeriodHolder periodHolder = queue.getReadingPeriod(); + boolean mayRenderStartOfStream = periodHolder == queue.getPlayingPeriod(); + TrackSelectorResult trackSelectorResult = periodHolder.getTrackSelectorResult(); + RendererConfiguration rendererConfiguration = + trackSelectorResult.rendererConfigurations[rendererIndex]; + TrackSelection newSelection = trackSelectorResult.selections.get(rendererIndex); + Format[] formats = getFormats(newSelection); + // The renderer needs enabling with its new track selection. + boolean playing = shouldPlayWhenReady() && playbackInfo.playbackState == Player.STATE_READY; + // Consider as joining only if the renderer was previously disabled. + boolean joining = !wasRendererEnabled && playing; + // Enable the renderer. + enabledRendererCount++; + renderer.enable( + rendererConfiguration, + formats, + periodHolder.sampleStreams[rendererIndex], + rendererPositionUs, + joining, + mayRenderStartOfStream, + periodHolder.getRendererOffset()); + mediaClock.onRendererEnabled(renderer); + // Start the renderer if playing. + if (playing) { + renderer.start(); } } @@ -1982,17 +2114,392 @@ import java.util.concurrent.atomic.AtomicBoolean; loadControl.onTracksSelected(renderers, trackGroups, trackSelectorResult.selections); } - private void sendPlaybackParametersChangedInternal( - PlaybackParameters playbackParameters, boolean acknowledgeCommand) { + private void sendPlaybackSpeedChangedInternal(float playbackSpeed, boolean acknowledgeCommand) { handler .obtainMessage( - MSG_PLAYBACK_PARAMETERS_CHANGED_INTERNAL, - acknowledgeCommand ? 1 : 0, - 0, - playbackParameters) + MSG_PLAYBACK_SPEED_CHANGED_INTERNAL, acknowledgeCommand ? 1 : 0, 0, playbackSpeed) .sendToTarget(); } + private boolean shouldPlayWhenReady() { + return playbackInfo.playWhenReady + && playbackInfo.playbackSuppressionReason == Player.PLAYBACK_SUPPRESSION_REASON_NONE; + } + + private static PositionUpdateForPlaylistChange resolvePositionForPlaylistChange( + Timeline timeline, + PlaybackInfo playbackInfo, + @Nullable SeekPosition pendingInitialSeekPosition, + MediaPeriodQueue queue, + @RepeatMode int repeatMode, + boolean shuffleModeEnabled, + Timeline.Window window, + Timeline.Period period) { + if (timeline.isEmpty()) { + return new PositionUpdateForPlaylistChange( + PlaybackInfo.getDummyPeriodForEmptyTimeline(), + /* periodPositionUs= */ 0, + /* requestedContentPositionUs= */ C.TIME_UNSET, + /* forceBufferingState= */ false, + /* endPlayback= */ true); + } + MediaPeriodId oldPeriodId = playbackInfo.periodId; + Object newPeriodUid = oldPeriodId.periodUid; + boolean shouldUseRequestedContentPosition = + shouldUseRequestedContentPosition(playbackInfo, period, window); + long oldContentPositionUs = + shouldUseRequestedContentPosition + ? playbackInfo.requestedContentPositionUs + : playbackInfo.positionUs; + long newContentPositionUs = oldContentPositionUs; + int startAtDefaultPositionWindowIndex = C.INDEX_UNSET; + boolean forceBufferingState = false; + boolean endPlayback = false; + if (pendingInitialSeekPosition != null) { + // Resolve initial seek position. + @Nullable + Pair periodPosition = + resolveSeekPosition( + timeline, + pendingInitialSeekPosition, + /* trySubsequentPeriods= */ true, + repeatMode, + shuffleModeEnabled, + window, + period); + if (periodPosition == null) { + // The initial seek in the empty old timeline is invalid in the new timeline. + endPlayback = true; + startAtDefaultPositionWindowIndex = timeline.getFirstWindowIndex(shuffleModeEnabled); + } else { + // The pending seek has been resolved successfully in the new timeline. + if (pendingInitialSeekPosition.windowPositionUs == C.TIME_UNSET) { + startAtDefaultPositionWindowIndex = + timeline.getPeriodByUid(periodPosition.first, period).windowIndex; + } else { + newPeriodUid = periodPosition.first; + newContentPositionUs = periodPosition.second; + } + forceBufferingState = playbackInfo.playbackState == Player.STATE_ENDED; + } + } else if (playbackInfo.timeline.isEmpty()) { + // Resolve to default position if the old timeline is empty and no seek is requested above. + startAtDefaultPositionWindowIndex = timeline.getFirstWindowIndex(shuffleModeEnabled); + } else if (timeline.getIndexOfPeriod(newPeriodUid) == C.INDEX_UNSET) { + // The current period isn't in the new timeline. Attempt to resolve a subsequent period whose + // window we can restart from. + @Nullable + Object subsequentPeriodUid = + resolveSubsequentPeriod( + window, + period, + repeatMode, + shuffleModeEnabled, + newPeriodUid, + playbackInfo.timeline, + timeline); + if (subsequentPeriodUid == null) { + // We failed to resolve a suitable restart position but the timeline is not empty. + endPlayback = true; + startAtDefaultPositionWindowIndex = timeline.getFirstWindowIndex(shuffleModeEnabled); + } else { + // We resolved a subsequent period. Start at the default position in the corresponding + // window. + startAtDefaultPositionWindowIndex = + timeline.getPeriodByUid(subsequentPeriodUid, period).windowIndex; + } + } else if (shouldUseRequestedContentPosition) { + // We previously requested a content position, but haven't used it yet. Re-resolve the + // requested window position to the period uid and position in case they changed. + if (oldContentPositionUs == C.TIME_UNSET) { + startAtDefaultPositionWindowIndex = + timeline.getPeriodByUid(newPeriodUid, period).windowIndex; + } else { + playbackInfo.timeline.getPeriodByUid(oldPeriodId.periodUid, period); + long windowPositionUs = oldContentPositionUs + period.getPositionInWindowUs(); + int windowIndex = timeline.getPeriodByUid(newPeriodUid, period).windowIndex; + Pair periodPosition = + timeline.getPeriodPosition(window, period, windowIndex, windowPositionUs); + newPeriodUid = periodPosition.first; + newContentPositionUs = periodPosition.second; + } + } + + // Set period uid for default positions and resolve position for ad resolution. + long contentPositionForAdResolutionUs = newContentPositionUs; + if (startAtDefaultPositionWindowIndex != C.INDEX_UNSET) { + Pair defaultPosition = + timeline.getPeriodPosition( + window, + period, + startAtDefaultPositionWindowIndex, + /* windowPositionUs= */ C.TIME_UNSET); + newPeriodUid = defaultPosition.first; + contentPositionForAdResolutionUs = defaultPosition.second; + newContentPositionUs = C.TIME_UNSET; + } + + // Ensure ad insertion metadata is up to date. + MediaPeriodId periodIdWithAds = + queue.resolveMediaPeriodIdForAds(timeline, newPeriodUid, contentPositionForAdResolutionUs); + boolean earliestCuePointIsUnchangedOrLater = + periodIdWithAds.nextAdGroupIndex == C.INDEX_UNSET + || (oldPeriodId.nextAdGroupIndex != C.INDEX_UNSET + && periodIdWithAds.adGroupIndex >= oldPeriodId.nextAdGroupIndex); + // Drop update if we keep playing the same content (MediaPeriod.periodUid are identical) and + // the only change is that MediaPeriodId.nextAdGroupIndex increased. This postpones a potential + // discontinuity until we reach the former next ad group position. + boolean oldAndNewPeriodIdAreSame = + oldPeriodId.periodUid.equals(newPeriodUid) + && !oldPeriodId.isAd() + && !periodIdWithAds.isAd() + && earliestCuePointIsUnchangedOrLater; + MediaPeriodId newPeriodId = oldAndNewPeriodIdAreSame ? oldPeriodId : periodIdWithAds; + + long periodPositionUs = contentPositionForAdResolutionUs; + if (newPeriodId.isAd()) { + if (newPeriodId.equals(oldPeriodId)) { + periodPositionUs = playbackInfo.positionUs; + } else { + timeline.getPeriodByUid(newPeriodId.periodUid, period); + periodPositionUs = + newPeriodId.adIndexInAdGroup == period.getFirstAdIndexToPlay(newPeriodId.adGroupIndex) + ? period.getAdResumePositionUs() + : 0; + } + } + + return new PositionUpdateForPlaylistChange( + newPeriodId, periodPositionUs, newContentPositionUs, forceBufferingState, endPlayback); + } + + private static boolean shouldUseRequestedContentPosition( + PlaybackInfo playbackInfo, Timeline.Period period, Timeline.Window window) { + // Only use the actual position as content position if it's not an ad and we already have + // prepared media information. Otherwise use the requested position. + MediaPeriodId periodId = playbackInfo.periodId; + Timeline timeline = playbackInfo.timeline; + return periodId.isAd() + || timeline.isEmpty() + || timeline.getWindow( + timeline.getPeriodByUid(periodId.periodUid, period).windowIndex, window) + .isPlaceholder; + } + + /** + * Updates pending message to a new timeline. + * + * @param pendingMessageInfo The pending message. + * @param newTimeline The new timeline. + * @param previousTimeline The previous timeline used to set the message positions. + * @param repeatMode The current repeat mode. + * @param shuffleModeEnabled The current shuffle mode. + * @param window A scratch window. + * @param period A scratch period. + * @return Whether the message position could be resolved to the current timeline. + */ + private static boolean resolvePendingMessagePosition( + PendingMessageInfo pendingMessageInfo, + Timeline newTimeline, + Timeline previousTimeline, + @Player.RepeatMode int repeatMode, + boolean shuffleModeEnabled, + Timeline.Window window, + Timeline.Period period) { + if (pendingMessageInfo.resolvedPeriodUid == null) { + // Position is still unresolved. Try to find window in new timeline. + long requestPositionUs = + pendingMessageInfo.message.getPositionMs() == C.TIME_END_OF_SOURCE + ? C.TIME_UNSET + : C.msToUs(pendingMessageInfo.message.getPositionMs()); + @Nullable + Pair periodPosition = + resolveSeekPosition( + newTimeline, + new SeekPosition( + pendingMessageInfo.message.getTimeline(), + pendingMessageInfo.message.getWindowIndex(), + requestPositionUs), + /* trySubsequentPeriods= */ false, + repeatMode, + shuffleModeEnabled, + window, + period); + if (periodPosition == null) { + return false; + } + pendingMessageInfo.setResolvedPosition( + /* periodIndex= */ newTimeline.getIndexOfPeriod(periodPosition.first), + /* periodTimeUs= */ periodPosition.second, + /* periodUid= */ periodPosition.first); + if (pendingMessageInfo.message.getPositionMs() == C.TIME_END_OF_SOURCE) { + resolvePendingMessageEndOfStreamPosition(newTimeline, pendingMessageInfo, window, period); + } + return true; + } + // Position has been resolved for a previous timeline. Try to find the updated period index. + int index = newTimeline.getIndexOfPeriod(pendingMessageInfo.resolvedPeriodUid); + if (index == C.INDEX_UNSET) { + return false; + } + if (pendingMessageInfo.message.getPositionMs() == C.TIME_END_OF_SOURCE) { + // Re-resolve end of stream in case the duration changed. + resolvePendingMessageEndOfStreamPosition(newTimeline, pendingMessageInfo, window, period); + return true; + } + pendingMessageInfo.resolvedPeriodIndex = index; + previousTimeline.getPeriodByUid(pendingMessageInfo.resolvedPeriodUid, period); + if (previousTimeline.getWindow(period.windowIndex, window).isPlaceholder) { + // The position needs to be re-resolved because the window in the previous timeline wasn't + // fully prepared. + long windowPositionUs = + pendingMessageInfo.resolvedPeriodTimeUs + period.getPositionInWindowUs(); + int windowIndex = + newTimeline.getPeriodByUid(pendingMessageInfo.resolvedPeriodUid, period).windowIndex; + Pair periodPosition = + newTimeline.getPeriodPosition(window, period, windowIndex, windowPositionUs); + pendingMessageInfo.setResolvedPosition( + /* periodIndex= */ newTimeline.getIndexOfPeriod(periodPosition.first), + /* periodTimeUs= */ periodPosition.second, + /* periodUid= */ periodPosition.first); + } + return true; + } + + private static void resolvePendingMessageEndOfStreamPosition( + Timeline timeline, + PendingMessageInfo messageInfo, + Timeline.Window window, + Timeline.Period period) { + int windowIndex = timeline.getPeriodByUid(messageInfo.resolvedPeriodUid, period).windowIndex; + int lastPeriodIndex = timeline.getWindow(windowIndex, window).lastPeriodIndex; + Object lastPeriodUid = timeline.getPeriod(lastPeriodIndex, period, /* setIds= */ true).uid; + long positionUs = period.durationUs != C.TIME_UNSET ? period.durationUs - 1 : Long.MAX_VALUE; + messageInfo.setResolvedPosition(lastPeriodIndex, positionUs, lastPeriodUid); + } + + /** + * Converts a {@link SeekPosition} into the corresponding (periodUid, periodPositionUs) for the + * internal timeline. + * + * @param seekPosition The position to resolve. + * @param trySubsequentPeriods Whether the position can be resolved to a subsequent matching + * period if the original period is no longer available. + * @return The resolved position, or null if resolution was not successful. + * @throws IllegalSeekPositionException If the window index of the seek position is outside the + * bounds of the timeline. + */ + @Nullable + private static Pair resolveSeekPosition( + Timeline timeline, + SeekPosition seekPosition, + boolean trySubsequentPeriods, + @RepeatMode int repeatMode, + boolean shuffleModeEnabled, + Timeline.Window window, + Timeline.Period period) { + Timeline seekTimeline = seekPosition.timeline; + if (timeline.isEmpty()) { + // We don't have a valid timeline yet, so we can't resolve the position. + return null; + } + if (seekTimeline.isEmpty()) { + // The application performed a blind seek with an empty timeline (most likely based on + // knowledge of what the future timeline will be). Use the internal timeline. + seekTimeline = timeline; + } + // Map the SeekPosition to a position in the corresponding timeline. + Pair periodPosition; + try { + periodPosition = + seekTimeline.getPeriodPosition( + window, period, seekPosition.windowIndex, seekPosition.windowPositionUs); + } catch (IndexOutOfBoundsException e) { + // The window index of the seek position was outside the bounds of the timeline. + return null; + } + if (timeline.equals(seekTimeline)) { + // Our internal timeline is the seek timeline, so the mapped position is correct. + return periodPosition; + } + // Attempt to find the mapped period in the internal timeline. + int periodIndex = timeline.getIndexOfPeriod(periodPosition.first); + if (periodIndex != C.INDEX_UNSET) { + // We successfully located the period in the internal timeline. + seekTimeline.getPeriodByUid(periodPosition.first, period); + if (seekTimeline.getWindow(period.windowIndex, window).isPlaceholder) { + // The seek timeline was using a placeholder, so we need to re-resolve using the updated + // timeline in case the resolved position changed. + int newWindowIndex = timeline.getPeriodByUid(periodPosition.first, period).windowIndex; + periodPosition = + timeline.getPeriodPosition( + window, period, newWindowIndex, seekPosition.windowPositionUs); + } + return periodPosition; + } + if (trySubsequentPeriods) { + // Try and find a subsequent period from the seek timeline in the internal timeline. + @Nullable + Object periodUid = + resolveSubsequentPeriod( + window, + period, + repeatMode, + shuffleModeEnabled, + periodPosition.first, + seekTimeline, + timeline); + if (periodUid != null) { + // We found one. Use the default position of the corresponding window. + return timeline.getPeriodPosition( + window, + period, + timeline.getPeriodByUid(periodUid, period).windowIndex, + /* windowPositionUs= */ C.TIME_UNSET); + } + } + // We didn't find one. Give up. + return null; + } + + /** + * Given a period index into an old timeline, finds the first subsequent period that also exists + * in a new timeline. The uid of this period in the new timeline is returned. + * + * @param window A {@link Timeline.Window} to be used internally. + * @param period A {@link Timeline.Period} to be used internally. + * @param repeatMode The repeat mode to use. + * @param shuffleModeEnabled Whether the shuffle mode is enabled. + * @param oldPeriodUid The index of the period in the old timeline. + * @param oldTimeline The old timeline. + * @param newTimeline The new timeline. + * @return The uid in the new timeline of the first subsequent period, or null if no such period + * was found. + */ + /* package */ static @Nullable Object resolveSubsequentPeriod( + Timeline.Window window, + Timeline.Period period, + @Player.RepeatMode int repeatMode, + boolean shuffleModeEnabled, + Object oldPeriodUid, + Timeline oldTimeline, + Timeline newTimeline) { + int oldPeriodIndex = oldTimeline.getIndexOfPeriod(oldPeriodUid); + int newPeriodIndex = C.INDEX_UNSET; + int maxIterations = oldTimeline.getPeriodCount(); + for (int i = 0; i < maxIterations && newPeriodIndex == C.INDEX_UNSET; i++) { + oldPeriodIndex = + oldTimeline.getNextPeriodIndex( + oldPeriodIndex, period, window, repeatMode, shuffleModeEnabled); + if (oldPeriodIndex == C.INDEX_UNSET) { + // We've reached the end of the old timeline. + break; + } + newPeriodIndex = newTimeline.getIndexOfPeriod(oldTimeline.getUidOfPeriod(oldPeriodIndex)); + } + return newPeriodIndex == C.INDEX_UNSET ? null : newTimeline.getUidOfPeriod(newPeriodIndex); + } + private static Format[] getFormats(TrackSelection newSelection) { // Build an array of formats contained by the selection. int length = newSelection != null ? newSelection.length() : 0; @@ -2003,6 +2510,10 @@ import java.util.concurrent.atomic.AtomicBoolean; return formats; } + private static boolean isRendererEnabled(Renderer renderer) { + return renderer.getState() != Renderer.STATE_DISABLED; + } + private static final class SeekPosition { public final Timeline timeline; @@ -2016,6 +2527,27 @@ import java.util.concurrent.atomic.AtomicBoolean; } } + private static final class PositionUpdateForPlaylistChange { + public final MediaPeriodId periodId; + public final long periodPositionUs; + public final long requestedContentPositionUs; + public final boolean forceBufferingState; + public final boolean endPlayback; + + public PositionUpdateForPlaylistChange( + MediaPeriodId periodId, + long periodPositionUs, + long requestedContentPositionUs, + boolean forceBufferingState, + boolean endPlayback) { + this.periodId = periodId; + this.periodPositionUs = periodPositionUs; + this.requestedContentPositionUs = requestedContentPositionUs; + this.forceBufferingState = forceBufferingState; + this.endPlayback = endPlayback; + } + } + private static final class PendingMessageInfo implements Comparable { public final PlayerMessage message; @@ -2053,38 +2585,66 @@ import java.util.concurrent.atomic.AtomicBoolean; } } - private static final class MediaSourceRefreshInfo { + private static final class MediaSourceListUpdateMessage { - public final MediaSource source; - public final Timeline timeline; + private final List mediaSourceHolders; + private final ShuffleOrder shuffleOrder; + private final int windowIndex; + private final long positionUs; - public MediaSourceRefreshInfo(MediaSource source, Timeline timeline) { - this.source = source; - this.timeline = timeline; + private MediaSourceListUpdateMessage( + List mediaSourceHolders, + ShuffleOrder shuffleOrder, + int windowIndex, + long positionUs) { + this.mediaSourceHolders = mediaSourceHolders; + this.shuffleOrder = shuffleOrder; + this.windowIndex = windowIndex; + this.positionUs = positionUs; } } - private static final class PlaybackInfoUpdate { + private static class MoveMediaItemsMessage { - private PlaybackInfo lastPlaybackInfo; - private int operationAcks; - private boolean positionDiscontinuity; - private @DiscontinuityReason int discontinuityReason; + public final int fromIndex; + public final int toIndex; + public final int newFromIndex; + public final ShuffleOrder shuffleOrder; - public boolean hasPendingUpdate(PlaybackInfo playbackInfo) { - return playbackInfo != lastPlaybackInfo || operationAcks > 0 || positionDiscontinuity; + public MoveMediaItemsMessage( + int fromIndex, int toIndex, int newFromIndex, ShuffleOrder shuffleOrder) { + this.fromIndex = fromIndex; + this.toIndex = toIndex; + this.newFromIndex = newFromIndex; + this.shuffleOrder = shuffleOrder; } + } - public void reset(PlaybackInfo playbackInfo) { - lastPlaybackInfo = playbackInfo; - operationAcks = 0; - positionDiscontinuity = false; + /* package */ static final class PlaybackInfoUpdate { + + private boolean hasPendingChange; + + public PlaybackInfo playbackInfo; + public int operationAcks; + public boolean positionDiscontinuity; + @DiscontinuityReason public int discontinuityReason; + public boolean hasPlayWhenReadyChangeReason; + @PlayWhenReadyChangeReason public int playWhenReadyChangeReason; + + public PlaybackInfoUpdate(PlaybackInfo playbackInfo) { + this.playbackInfo = playbackInfo; } public void incrementPendingOperationAcks(int operationAcks) { + hasPendingChange |= operationAcks > 0; this.operationAcks += operationAcks; } + public void setPlaybackInfo(PlaybackInfo playbackInfo) { + hasPendingChange |= this.playbackInfo != playbackInfo; + this.playbackInfo = playbackInfo; + } + public void setPositionDiscontinuity(@DiscontinuityReason int discontinuityReason) { if (positionDiscontinuity && this.discontinuityReason != Player.DISCONTINUITY_REASON_INTERNAL) { @@ -2093,9 +2653,16 @@ import java.util.concurrent.atomic.AtomicBoolean; Assertions.checkArgument(discontinuityReason == Player.DISCONTINUITY_REASON_INTERNAL); return; } + hasPendingChange = true; positionDiscontinuity = true; this.discontinuityReason = discontinuityReason; } - } + public void setPlayWhenReadyChangeReason( + @PlayWhenReadyChangeReason int playWhenReadyChangeReason) { + hasPendingChange = true; + this.hasPlayWhenReadyChangeReason = true; + this.playWhenReadyChangeReason = playWhenReadyChangeReason; + } + } } 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 deleted file mode 100644 index 4fb6cec1e8..0000000000 --- a/library/core/src/main/java/com/google/android/exoplayer2/Format.java +++ /dev/null @@ -1,1756 +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; - -import android.os.Parcel; -import android.os.Parcelable; -import androidx.annotation.Nullable; -import com.google.android.exoplayer2.drm.DrmInitData; -import com.google.android.exoplayer2.drm.DrmSession; -import com.google.android.exoplayer2.drm.ExoMediaCrypto; -import com.google.android.exoplayer2.metadata.Metadata; -import com.google.android.exoplayer2.util.MimeTypes; -import com.google.android.exoplayer2.util.Util; -import com.google.android.exoplayer2.video.ColorInfo; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; - -/** - * Representation of a media format. - */ -public final class Format implements Parcelable { - - /** - * A value for various fields to indicate that the field's value is unknown or not applicable. - */ - public static final int NO_VALUE = -1; - - /** - * A value for {@link #subsampleOffsetUs} to indicate that subsample timestamps are relative to - * the timestamps of their parent samples. - */ - public static final long OFFSET_SAMPLE_RELATIVE = Long.MAX_VALUE; - - /** An identifier for the format, or null if unknown or not applicable. */ - @Nullable public final String id; - /** The human readable label, or null if unknown or not applicable. */ - @Nullable public final String label; - /** Track selection flags. */ - @C.SelectionFlags public final int selectionFlags; - /** Track role flags. */ - @C.RoleFlags public final int roleFlags; - /** - * The average bandwidth in bits per second, or {@link #NO_VALUE} if unknown or not applicable. - */ - public final int bitrate; - /** Codecs of the format as described in RFC 6381, or null if unknown or not applicable. */ - @Nullable public final String codecs; - /** Metadata, or null if unknown or not applicable. */ - @Nullable public final Metadata metadata; - - // Container specific. - - /** The mime type of the container, or null if unknown or not applicable. */ - @Nullable public final String containerMimeType; - - // Elementary stream specific. - - /** - * The mime type of the elementary stream (i.e. the individual samples), or null if unknown or not - * applicable. - */ - @Nullable public final String sampleMimeType; - /** - * The maximum size of a buffer of data (typically one sample), or {@link #NO_VALUE} if unknown or - * not applicable. - */ - public final int maxInputSize; - /** - * Initialization data that must be provided to the decoder. Will not be null, but may be empty - * if initialization data is not required. - */ - public final List initializationData; - /** DRM initialization data if the stream is protected, or null otherwise. */ - @Nullable public final 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. - - /** - * The width of the video in pixels, or {@link #NO_VALUE} if unknown or not applicable. - */ - public final int width; - /** - * The height of the video in pixels, or {@link #NO_VALUE} if unknown or not applicable. - */ - public final int height; - /** - * The frame rate in frames per second, or {@link #NO_VALUE} if unknown or not applicable. - */ - public final float frameRate; - /** - * The clockwise rotation that should be applied to the video for it to be rendered in the correct - * orientation, or 0 if unknown or not applicable. Only 0, 90, 180 and 270 are supported. - */ - public final int rotationDegrees; - /** The width to height ratio of pixels in the video, or 1.0 if unknown or not applicable. */ - public final float pixelWidthHeightRatio; - /** - * The stereo layout for 360/3D/VR video, or {@link #NO_VALUE} if not applicable. Valid stereo - * modes are {@link C#STEREO_MODE_MONO}, {@link C#STEREO_MODE_TOP_BOTTOM}, {@link - * C#STEREO_MODE_LEFT_RIGHT}, {@link C#STEREO_MODE_STEREO_MESH}. - */ - @C.StereoMode - public final int stereoMode; - /** The projection data for 360/VR video, or null if not applicable. */ - @Nullable public final byte[] projectionData; - /** The color metadata associated with the video, helps with accurate color reproduction. */ - @Nullable public final ColorInfo colorInfo; - - // Audio specific. - - /** - * The number of audio channels, or {@link #NO_VALUE} if unknown or not applicable. - */ - public final int channelCount; - /** - * The audio sampling rate in Hz, or {@link #NO_VALUE} if unknown or not applicable. - */ - 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}, {@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. - */ - 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. - */ - public final int encoderDelay; - /** - * The number of frames to trim from the end of the decoded audio stream, or 0 if not applicable. - */ - public final int encoderPadding; - - // Audio and text specific. - - /** The language as an IETF BCP 47 conformant tag, or null if unknown or not applicable. */ - @Nullable public final String language; - /** - * The Accessibility channel, or {@link #NO_VALUE} if not known or applicable. - */ - public final int accessibilityChannel; - - // Provided by source. - - /** - * The type of the {@link ExoMediaCrypto} provided by the media source, if the media source can - * acquire a {@link DrmSession} for {@link #drmInitData}. Null if the media source cannot acquire - * a session for {@link #drmInitData}, or if not applicable. - */ - @Nullable public final Class exoMediaCryptoType; - - // Lazily initialized hashcode. - private int hashCode; - - // Video. - - /** - * @deprecated Use {@link #createVideoContainerFormat(String, String, String, String, String, - * Metadata, int, int, int, float, List, int, int)} instead. - */ - @Deprecated - public static Format createVideoContainerFormat( - @Nullable String id, - @Nullable String containerMimeType, - @Nullable String sampleMimeType, - @Nullable String codecs, - int bitrate, - int width, - int height, - float frameRate, - @Nullable List initializationData, - @C.SelectionFlags int selectionFlags) { - return createVideoContainerFormat( - id, - /* label= */ null, - containerMimeType, - sampleMimeType, - codecs, - /* metadata= */ null, - bitrate, - width, - height, - frameRate, - initializationData, - selectionFlags, - /* roleFlags= */ 0); - } - - public static Format createVideoContainerFormat( - @Nullable String id, - @Nullable String label, - @Nullable String containerMimeType, - @Nullable String sampleMimeType, - @Nullable String codecs, - @Nullable Metadata metadata, - int bitrate, - int width, - int height, - float frameRate, - @Nullable List initializationData, - @C.SelectionFlags int selectionFlags, - @C.RoleFlags int roleFlags) { - return new Format( - id, - label, - selectionFlags, - roleFlags, - bitrate, - codecs, - metadata, - containerMimeType, - sampleMimeType, - /* maxInputSize= */ NO_VALUE, - initializationData, - /* drmInitData= */ null, - OFFSET_SAMPLE_RELATIVE, - 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, - /* language= */ null, - /* accessibilityChannel= */ NO_VALUE, - /* exoMediaCryptoType= */ null); - } - - public static Format createVideoSampleFormat( - @Nullable String id, - @Nullable String sampleMimeType, - @Nullable String codecs, - int bitrate, - int maxInputSize, - int width, - int height, - float frameRate, - @Nullable List initializationData, - @Nullable DrmInitData drmInitData) { - return createVideoSampleFormat( - id, - sampleMimeType, - codecs, - bitrate, - maxInputSize, - width, - height, - frameRate, - initializationData, - /* rotationDegrees= */ NO_VALUE, - /* pixelWidthHeightRatio= */ NO_VALUE, - drmInitData); - } - - public static Format createVideoSampleFormat( - @Nullable String id, - @Nullable String sampleMimeType, - @Nullable String codecs, - int bitrate, - int maxInputSize, - int width, - int height, - float frameRate, - @Nullable List initializationData, - int rotationDegrees, - float pixelWidthHeightRatio, - @Nullable DrmInitData 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( - @Nullable String id, - @Nullable String sampleMimeType, - @Nullable String codecs, - int bitrate, - int maxInputSize, - int width, - int height, - float frameRate, - @Nullable List initializationData, - int rotationDegrees, - float pixelWidthHeightRatio, - @Nullable byte[] projectionData, - @C.StereoMode int stereoMode, - @Nullable ColorInfo colorInfo, - @Nullable DrmInitData drmInitData) { - return new Format( - id, - /* label= */ null, - /* selectionFlags= */ 0, - /* roleFlags= */ 0, - bitrate, - codecs, - /* metadata= */ null, - /* containerMimeType= */ null, - sampleMimeType, - maxInputSize, - initializationData, - drmInitData, - OFFSET_SAMPLE_RELATIVE, - width, - height, - frameRate, - rotationDegrees, - pixelWidthHeightRatio, - projectionData, - stereoMode, - colorInfo, - /* channelCount= */ NO_VALUE, - /* sampleRate= */ NO_VALUE, - /* pcmEncoding= */ NO_VALUE, - /* encoderDelay= */ NO_VALUE, - /* encoderPadding= */ NO_VALUE, - /* language= */ null, - /* accessibilityChannel= */ NO_VALUE, - /* exoMediaCryptoType= */ null); - } - - // Audio. - - /** - * @deprecated Use {@link #createAudioContainerFormat(String, String, String, String, String, - * Metadata, int, int, int, List, int, int, String)} instead. - */ - @Deprecated - public static Format createAudioContainerFormat( - @Nullable String id, - @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 createAudioContainerFormat( - id, - /* label= */ null, - containerMimeType, - sampleMimeType, - codecs, - /* metadata= */ null, - bitrate, - channelCount, - sampleRate, - initializationData, - selectionFlags, - /* roleFlags= */ 0, - language); - } - - public static Format createAudioContainerFormat( - @Nullable String id, - @Nullable String label, - @Nullable String containerMimeType, - @Nullable String sampleMimeType, - @Nullable String codecs, - @Nullable Metadata metadata, - int bitrate, - int channelCount, - int sampleRate, - @Nullable List initializationData, - @C.SelectionFlags int selectionFlags, - @C.RoleFlags int roleFlags, - @Nullable String language) { - return new Format( - id, - label, - selectionFlags, - roleFlags, - bitrate, - codecs, - metadata, - containerMimeType, - sampleMimeType, - /* maxInputSize= */ NO_VALUE, - initializationData, - /* drmInitData= */ null, - OFFSET_SAMPLE_RELATIVE, - /* 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, - language, - /* accessibilityChannel= */ NO_VALUE, - /* exoMediaCryptoType= */ null); - } - - public static Format createAudioSampleFormat( - @Nullable String id, - @Nullable String sampleMimeType, - @Nullable String codecs, - int bitrate, - int maxInputSize, - int channelCount, - int sampleRate, - @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, - initializationData, - drmInitData, - selectionFlags, - language); - } - - public static Format createAudioSampleFormat( - @Nullable String id, - @Nullable String sampleMimeType, - @Nullable String codecs, - int bitrate, - int maxInputSize, - int channelCount, - int sampleRate, - @C.PcmEncoding int pcmEncoding, - @Nullable List initializationData, - @Nullable DrmInitData drmInitData, - @C.SelectionFlags int selectionFlags, - @Nullable String language) { - 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( - @Nullable String id, - @Nullable String sampleMimeType, - @Nullable String codecs, - int bitrate, - int maxInputSize, - int channelCount, - int sampleRate, - @C.PcmEncoding int pcmEncoding, - int encoderDelay, - int encoderPadding, - @Nullable List initializationData, - @Nullable DrmInitData drmInitData, - @C.SelectionFlags int selectionFlags, - @Nullable String language, - @Nullable Metadata metadata) { - return new Format( - id, - /* label= */ null, - selectionFlags, - /* roleFlags= */ 0, - bitrate, - codecs, - metadata, - /* containerMimeType= */ null, - sampleMimeType, - maxInputSize, - initializationData, - drmInitData, - OFFSET_SAMPLE_RELATIVE, - /* 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, - language, - /* accessibilityChannel= */ NO_VALUE, - /* exoMediaCryptoType= */ null); - } - - // Text. - - 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, - @C.RoleFlags int roleFlags, - @Nullable String language) { - return createTextContainerFormat( - id, - label, - containerMimeType, - sampleMimeType, - codecs, - bitrate, - selectionFlags, - roleFlags, - language, - /* accessibilityChannel= */ NO_VALUE); - } - - 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, - @C.RoleFlags int roleFlags, - @Nullable String language, - int accessibilityChannel) { - return new Format( - id, - label, - selectionFlags, - roleFlags, - bitrate, - codecs, - /* metadata= */ null, - containerMimeType, - sampleMimeType, - /* maxInputSize= */ NO_VALUE, - /* initializationData= */ null, - /* drmInitData= */ null, - OFFSET_SAMPLE_RELATIVE, - /* 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, - language, - accessibilityChannel, - /* exoMediaCryptoType= */ null); - } - - public static Format createTextSampleFormat( - @Nullable String id, - @Nullable String sampleMimeType, - @C.SelectionFlags int selectionFlags, - @Nullable String language) { - return createTextSampleFormat(id, sampleMimeType, selectionFlags, language, null); - } - - public static Format createTextSampleFormat( - @Nullable String id, - @Nullable String sampleMimeType, - @C.SelectionFlags int selectionFlags, - @Nullable String language, - @Nullable DrmInitData drmInitData) { - return createTextSampleFormat( - id, - sampleMimeType, - /* codecs= */ null, - /* bitrate= */ NO_VALUE, - selectionFlags, - language, - NO_VALUE, - drmInitData, - OFFSET_SAMPLE_RELATIVE, - Collections.emptyList()); - } - - public static Format createTextSampleFormat( - @Nullable String id, - @Nullable String sampleMimeType, - @Nullable String codecs, - int bitrate, - @C.SelectionFlags int selectionFlags, - @Nullable String language, - int accessibilityChannel, - @Nullable DrmInitData drmInitData) { - return createTextSampleFormat( - id, - sampleMimeType, - codecs, - bitrate, - selectionFlags, - language, - accessibilityChannel, - drmInitData, - OFFSET_SAMPLE_RELATIVE, - Collections.emptyList()); - } - - public static Format createTextSampleFormat( - @Nullable String id, - @Nullable String sampleMimeType, - @Nullable String codecs, - int bitrate, - @C.SelectionFlags int selectionFlags, - @Nullable String language, - @Nullable DrmInitData drmInitData, - long subsampleOffsetUs) { - return createTextSampleFormat( - id, - sampleMimeType, - codecs, - bitrate, - selectionFlags, - language, - /* accessibilityChannel= */ NO_VALUE, - drmInitData, - subsampleOffsetUs, - Collections.emptyList()); - } - - public static Format createTextSampleFormat( - @Nullable String id, - @Nullable String sampleMimeType, - @Nullable String codecs, - int bitrate, - @C.SelectionFlags int selectionFlags, - @Nullable String language, - int accessibilityChannel, - @Nullable DrmInitData drmInitData, - long subsampleOffsetUs, - @Nullable List initializationData) { - return new Format( - id, - /* label= */ null, - selectionFlags, - /* roleFlags= */ 0, - bitrate, - codecs, - /* metadata= */ null, - /* containerMimeType= */ null, - sampleMimeType, - /* maxInputSize= */ NO_VALUE, - initializationData, - drmInitData, - subsampleOffsetUs, - /* 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, - language, - accessibilityChannel, - /* exoMediaCryptoType= */ null); - } - - // Image. - - public static Format createImageSampleFormat( - @Nullable String id, - @Nullable String sampleMimeType, - @Nullable String codecs, - int bitrate, - @C.SelectionFlags int selectionFlags, - @Nullable List initializationData, - @Nullable String language, - @Nullable DrmInitData drmInitData) { - return new Format( - id, - /* label= */ null, - selectionFlags, - /* roleFlags= */ 0, - bitrate, - codecs, - /* metadata=*/ null, - /* containerMimeType= */ null, - sampleMimeType, - /* maxInputSize= */ NO_VALUE, - initializationData, - drmInitData, - OFFSET_SAMPLE_RELATIVE, - /* 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, - language, - /* accessibilityChannel= */ NO_VALUE, - /* exoMediaCryptoType= */ null); - } - - // Generic. - - /** - * @deprecated Use {@link #createContainerFormat(String, String, String, String, String, int, int, - * int, String)} instead. - */ - @Deprecated - public static Format createContainerFormat( - @Nullable String id, - @Nullable String containerMimeType, - @Nullable String sampleMimeType, - @Nullable String codecs, - int bitrate, - @C.SelectionFlags int selectionFlags, - @Nullable String language) { - return createContainerFormat( - id, - /* label= */ null, - containerMimeType, - sampleMimeType, - codecs, - bitrate, - selectionFlags, - /* roleFlags= */ 0, - 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, - @C.RoleFlags int roleFlags, - @Nullable String language) { - return new Format( - id, - label, - selectionFlags, - roleFlags, - bitrate, - codecs, - /* metadata= */ null, - containerMimeType, - sampleMimeType, - /* maxInputSize= */ NO_VALUE, - /* initializationData= */ null, - /* drmInitData= */ null, - OFFSET_SAMPLE_RELATIVE, - /* 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, - language, - /* accessibilityChannel= */ NO_VALUE, - /* exoMediaCryptoType= */ null); - } - - public static Format createSampleFormat( - @Nullable String id, @Nullable String sampleMimeType, long subsampleOffsetUs) { - return new Format( - id, - /* label= */ null, - /* selectionFlags= */ 0, - /* roleFlags= */ 0, - /* bitrate= */ NO_VALUE, - /* codecs= */ null, - /* metadata= */ null, - /* containerMimeType= */ null, - sampleMimeType, - /* maxInputSize= */ NO_VALUE, - /* initializationData= */ null, - /* drmInitData= */ null, - subsampleOffsetUs, - /* 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, - /* language= */ null, - /* accessibilityChannel= */ NO_VALUE, - /* exoMediaCryptoType= */ null); - } - - public static Format createSampleFormat( - @Nullable String id, - @Nullable String sampleMimeType, - @Nullable String codecs, - int bitrate, - @Nullable DrmInitData drmInitData) { - return new Format( - id, - /* label= */ null, - /* selectionFlags= */ 0, - /* roleFlags= */ 0, - bitrate, - codecs, - /* metadata= */ null, - /* containerMimeType= */ null, - sampleMimeType, - /* maxInputSize= */ NO_VALUE, - /* initializationData= */ null, - drmInitData, - OFFSET_SAMPLE_RELATIVE, - /* 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, - /* language= */ null, - /* accessibilityChannel= */ NO_VALUE, - /* exoMediaCryptoType= */ null); - } - - /* package */ Format( - @Nullable String id, - @Nullable String label, - @C.SelectionFlags int selectionFlags, - @C.RoleFlags int roleFlags, - int bitrate, - @Nullable String codecs, - @Nullable Metadata metadata, - // Container specific. - @Nullable String containerMimeType, - // Elementary stream specific. - @Nullable String sampleMimeType, - int maxInputSize, - @Nullable List initializationData, - @Nullable DrmInitData drmInitData, - long subsampleOffsetUs, - // Video specific. - int width, - int height, - float frameRate, - int rotationDegrees, - float pixelWidthHeightRatio, - @Nullable byte[] projectionData, - @C.StereoMode int stereoMode, - @Nullable ColorInfo colorInfo, - // Audio specific. - int channelCount, - int sampleRate, - @C.PcmEncoding int pcmEncoding, - int encoderDelay, - int encoderPadding, - // Audio and text specific. - @Nullable String language, - int accessibilityChannel, - // Provided by source. - @Nullable Class exoMediaCryptoType) { - this.id = id; - this.label = label; - this.selectionFlags = selectionFlags; - this.roleFlags = roleFlags; - this.bitrate = bitrate; - this.codecs = codecs; - this.metadata = metadata; - // Container specific. - this.containerMimeType = containerMimeType; - // Elementary stream specific. - this.sampleMimeType = sampleMimeType; - this.maxInputSize = maxInputSize; - this.initializationData = - initializationData == null ? Collections.emptyList() : initializationData; - this.drmInitData = drmInitData; - this.subsampleOffsetUs = subsampleOffsetUs; - // Video specific. - this.width = width; - this.height = height; - this.frameRate = frameRate; - this.rotationDegrees = rotationDegrees == Format.NO_VALUE ? 0 : rotationDegrees; - this.pixelWidthHeightRatio = - pixelWidthHeightRatio == Format.NO_VALUE ? 1 : pixelWidthHeightRatio; - this.projectionData = projectionData; - this.stereoMode = stereoMode; - this.colorInfo = colorInfo; - // Audio specific. - this.channelCount = channelCount; - this.sampleRate = sampleRate; - this.pcmEncoding = pcmEncoding; - this.encoderDelay = encoderDelay == Format.NO_VALUE ? 0 : encoderDelay; - this.encoderPadding = encoderPadding == Format.NO_VALUE ? 0 : encoderPadding; - // Audio and text specific. - this.language = Util.normalizeLanguageCode(language); - this.accessibilityChannel = accessibilityChannel; - // Provided by source. - this.exoMediaCryptoType = exoMediaCryptoType; - } - - @SuppressWarnings("ResourceType") - /* package */ Format(Parcel in) { - id = in.readString(); - label = in.readString(); - selectionFlags = in.readInt(); - roleFlags = in.readInt(); - bitrate = in.readInt(); - codecs = in.readString(); - metadata = in.readParcelable(Metadata.class.getClassLoader()); - // Container specific. - containerMimeType = in.readString(); - // Elementary stream specific. - sampleMimeType = in.readString(); - maxInputSize = in.readInt(); - int initializationDataSize = in.readInt(); - initializationData = new ArrayList<>(initializationDataSize); - for (int i = 0; i < initializationDataSize; i++) { - initializationData.add(in.createByteArray()); - } - drmInitData = in.readParcelable(DrmInitData.class.getClassLoader()); - subsampleOffsetUs = in.readLong(); - // Video specific. - width = in.readInt(); - height = in.readInt(); - frameRate = in.readFloat(); - rotationDegrees = in.readInt(); - pixelWidthHeightRatio = in.readFloat(); - boolean hasProjectionData = Util.readBoolean(in); - projectionData = hasProjectionData ? in.createByteArray() : null; - stereoMode = in.readInt(); - colorInfo = in.readParcelable(ColorInfo.class.getClassLoader()); - // Audio specific. - channelCount = in.readInt(); - sampleRate = in.readInt(); - pcmEncoding = in.readInt(); - encoderDelay = in.readInt(); - encoderPadding = in.readInt(); - // Audio and text specific. - language = in.readString(); - accessibilityChannel = in.readInt(); - // Provided by source. - exoMediaCryptoType = null; - } - - public Format copyWithMaxInputSize(int maxInputSize) { - return new Format( - id, - label, - selectionFlags, - roleFlags, - bitrate, - codecs, - metadata, - containerMimeType, - sampleMimeType, - maxInputSize, - initializationData, - drmInitData, - subsampleOffsetUs, - width, - height, - frameRate, - rotationDegrees, - pixelWidthHeightRatio, - projectionData, - stereoMode, - colorInfo, - channelCount, - sampleRate, - pcmEncoding, - encoderDelay, - encoderPadding, - language, - accessibilityChannel, - exoMediaCryptoType); - } - - public Format copyWithSubsampleOffsetUs(long subsampleOffsetUs) { - return new Format( - id, - label, - selectionFlags, - roleFlags, - bitrate, - codecs, - metadata, - containerMimeType, - sampleMimeType, - maxInputSize, - initializationData, - drmInitData, - subsampleOffsetUs, - width, - height, - frameRate, - rotationDegrees, - pixelWidthHeightRatio, - projectionData, - stereoMode, - colorInfo, - channelCount, - sampleRate, - pcmEncoding, - encoderDelay, - encoderPadding, - language, - accessibilityChannel, - exoMediaCryptoType); - } - - public Format copyWithLabel(@Nullable String label) { - return new Format( - id, - label, - selectionFlags, - roleFlags, - bitrate, - codecs, - metadata, - containerMimeType, - sampleMimeType, - maxInputSize, - initializationData, - drmInitData, - subsampleOffsetUs, - width, - height, - frameRate, - rotationDegrees, - pixelWidthHeightRatio, - projectionData, - stereoMode, - colorInfo, - channelCount, - sampleRate, - pcmEncoding, - encoderDelay, - encoderPadding, - language, - accessibilityChannel, - exoMediaCryptoType); - } - - public Format copyWithContainerInfo( - @Nullable String id, - @Nullable String label, - @Nullable String sampleMimeType, - @Nullable String codecs, - @Nullable Metadata metadata, - int bitrate, - int width, - int height, - int channelCount, - @C.SelectionFlags int selectionFlags, - @Nullable String language) { - - if (this.metadata != null) { - metadata = this.metadata.copyWithAppendedEntriesFrom(metadata); - } - - return new Format( - id, - label, - selectionFlags, - roleFlags, - bitrate, - codecs, - metadata, - containerMimeType, - sampleMimeType, - maxInputSize, - initializationData, - drmInitData, - subsampleOffsetUs, - width, - height, - frameRate, - rotationDegrees, - pixelWidthHeightRatio, - projectionData, - stereoMode, - colorInfo, - channelCount, - sampleRate, - pcmEncoding, - encoderDelay, - encoderPadding, - language, - accessibilityChannel, - exoMediaCryptoType); - } - - @SuppressWarnings("ReferenceEquality") - public Format copyWithManifestFormatInfo(Format manifestFormat) { - if (this == manifestFormat) { - // No need to copy from ourselves. - return this; - } - - int trackType = MimeTypes.getTrackType(sampleMimeType); - - // Use manifest value only. - String id = manifestFormat.id; - - // 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; - 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; - } - } - - Metadata metadata = - this.metadata == null - ? manifestFormat.metadata - : this.metadata.copyWithAppendedEntriesFrom(manifestFormat.metadata); - - 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; - @C.RoleFlags int roleFlags = this.roleFlags | manifestFormat.roleFlags; - DrmInitData drmInitData = - DrmInitData.createSessionCreationData(manifestFormat.drmInitData, this.drmInitData); - - return new Format( - id, - label, - selectionFlags, - roleFlags, - bitrate, - codecs, - metadata, - containerMimeType, - sampleMimeType, - maxInputSize, - initializationData, - drmInitData, - subsampleOffsetUs, - width, - height, - frameRate, - rotationDegrees, - pixelWidthHeightRatio, - projectionData, - stereoMode, - colorInfo, - channelCount, - sampleRate, - pcmEncoding, - encoderDelay, - encoderPadding, - language, - accessibilityChannel, - exoMediaCryptoType); - } - - public Format copyWithGaplessInfo(int encoderDelay, int encoderPadding) { - return new Format( - id, - label, - selectionFlags, - roleFlags, - bitrate, - codecs, - metadata, - containerMimeType, - sampleMimeType, - maxInputSize, - initializationData, - drmInitData, - subsampleOffsetUs, - width, - height, - frameRate, - rotationDegrees, - pixelWidthHeightRatio, - projectionData, - stereoMode, - colorInfo, - channelCount, - sampleRate, - pcmEncoding, - encoderDelay, - encoderPadding, - language, - accessibilityChannel, - exoMediaCryptoType); - } - - public Format copyWithFrameRate(float frameRate) { - return new Format( - id, - label, - selectionFlags, - roleFlags, - bitrate, - codecs, - metadata, - containerMimeType, - sampleMimeType, - maxInputSize, - initializationData, - drmInitData, - subsampleOffsetUs, - width, - height, - frameRate, - rotationDegrees, - pixelWidthHeightRatio, - projectionData, - stereoMode, - colorInfo, - channelCount, - sampleRate, - pcmEncoding, - encoderDelay, - encoderPadding, - language, - accessibilityChannel, - exoMediaCryptoType); - } - - public Format copyWithDrmInitData(@Nullable DrmInitData drmInitData) { - return copyWithAdjustments(drmInitData, metadata); - } - - public Format copyWithMetadata(@Nullable Metadata metadata) { - return copyWithAdjustments(drmInitData, metadata); - } - - @SuppressWarnings("ReferenceEquality") - public Format copyWithAdjustments( - @Nullable DrmInitData drmInitData, @Nullable Metadata metadata) { - if (drmInitData == this.drmInitData && metadata == this.metadata) { - return this; - } - return new Format( - id, - label, - selectionFlags, - roleFlags, - bitrate, - codecs, - metadata, - containerMimeType, - sampleMimeType, - maxInputSize, - initializationData, - drmInitData, - subsampleOffsetUs, - width, - height, - frameRate, - rotationDegrees, - pixelWidthHeightRatio, - projectionData, - stereoMode, - colorInfo, - channelCount, - sampleRate, - pcmEncoding, - encoderDelay, - encoderPadding, - language, - accessibilityChannel, - exoMediaCryptoType); - } - - public Format copyWithRotationDegrees(int rotationDegrees) { - return new Format( - id, - label, - selectionFlags, - roleFlags, - bitrate, - codecs, - metadata, - containerMimeType, - sampleMimeType, - maxInputSize, - initializationData, - drmInitData, - subsampleOffsetUs, - width, - height, - frameRate, - rotationDegrees, - pixelWidthHeightRatio, - projectionData, - stereoMode, - colorInfo, - channelCount, - sampleRate, - pcmEncoding, - encoderDelay, - encoderPadding, - language, - accessibilityChannel, - exoMediaCryptoType); - } - - public Format copyWithBitrate(int bitrate) { - return new Format( - id, - label, - selectionFlags, - roleFlags, - bitrate, - codecs, - metadata, - containerMimeType, - sampleMimeType, - maxInputSize, - initializationData, - drmInitData, - subsampleOffsetUs, - width, - height, - frameRate, - rotationDegrees, - pixelWidthHeightRatio, - projectionData, - stereoMode, - colorInfo, - channelCount, - sampleRate, - pcmEncoding, - encoderDelay, - encoderPadding, - language, - accessibilityChannel, - exoMediaCryptoType); - } - - public Format copyWithVideoSize(int width, int height) { - return new Format( - id, - label, - selectionFlags, - roleFlags, - bitrate, - codecs, - metadata, - containerMimeType, - sampleMimeType, - maxInputSize, - initializationData, - drmInitData, - subsampleOffsetUs, - width, - height, - frameRate, - rotationDegrees, - pixelWidthHeightRatio, - projectionData, - stereoMode, - colorInfo, - channelCount, - sampleRate, - pcmEncoding, - encoderDelay, - encoderPadding, - language, - accessibilityChannel, - exoMediaCryptoType); - } - - public Format copyWithExoMediaCryptoType( - @Nullable Class exoMediaCryptoType) { - return new Format( - id, - label, - selectionFlags, - roleFlags, - bitrate, - codecs, - metadata, - containerMimeType, - sampleMimeType, - maxInputSize, - initializationData, - drmInitData, - subsampleOffsetUs, - width, - height, - frameRate, - rotationDegrees, - pixelWidthHeightRatio, - projectionData, - stereoMode, - colorInfo, - channelCount, - sampleRate, - pcmEncoding, - encoderDelay, - encoderPadding, - language, - accessibilityChannel, - exoMediaCryptoType); - } - - /** - * Returns the number of pixels if this is a video format whose {@link #width} and {@link #height} - * are known, or {@link #NO_VALUE} otherwise - */ - public int getPixelCount() { - return width == NO_VALUE || height == NO_VALUE ? NO_VALUE : (width * height); - } - - @Override - public String toString() { - return "Format(" - + id - + ", " - + label - + ", " - + containerMimeType - + ", " - + sampleMimeType - + ", " - + codecs - + ", " - + bitrate - + ", " - + language - + ", [" - + width - + ", " - + height - + ", " - + frameRate - + "]" - + ", [" - + channelCount - + ", " - + sampleRate - + "])"; - } - - @Override - public int hashCode() { - if (hashCode == 0) { - // Some fields for which hashing is expensive are deliberately omitted. - int result = 17; - result = 31 * result + (id == null ? 0 : id.hashCode()); - result = 31 * result + (label != null ? label.hashCode() : 0); - result = 31 * result + selectionFlags; - result = 31 * result + roleFlags; - result = 31 * result + bitrate; - result = 31 * result + (codecs == null ? 0 : codecs.hashCode()); - result = 31 * result + (metadata == null ? 0 : metadata.hashCode()); - // Container specific. - result = 31 * result + (containerMimeType == null ? 0 : containerMimeType.hashCode()); - // Elementary stream specific. - result = 31 * result + (sampleMimeType == null ? 0 : sampleMimeType.hashCode()); - result = 31 * result + maxInputSize; - // [Omitted] initializationData. - // [Omitted] drmInitData. - result = 31 * result + (int) subsampleOffsetUs; - // Video specific. - result = 31 * result + width; - result = 31 * result + height; - result = 31 * result + Float.floatToIntBits(frameRate); - result = 31 * result + rotationDegrees; - result = 31 * result + Float.floatToIntBits(pixelWidthHeightRatio); - // [Omitted] projectionData. - result = 31 * result + stereoMode; - // [Omitted] colorInfo. - // Audio specific. - result = 31 * result + channelCount; - result = 31 * result + sampleRate; - result = 31 * result + pcmEncoding; - result = 31 * result + encoderDelay; - result = 31 * result + encoderPadding; - // Audio and text specific. - result = 31 * result + (language == null ? 0 : language.hashCode()); - result = 31 * result + accessibilityChannel; - // Provided by source. - result = 31 * result + (exoMediaCryptoType == null ? 0 : exoMediaCryptoType.hashCode()); - hashCode = result; - } - return hashCode; - } - - @Override - public boolean equals(@Nullable Object obj) { - if (this == obj) { - return true; - } - if (obj == null || getClass() != obj.getClass()) { - return false; - } - Format other = (Format) obj; - if (hashCode != 0 && other.hashCode != 0 && hashCode != other.hashCode) { - return false; - } - // Field equality checks ordered by type, with the cheapest checks first. - return selectionFlags == other.selectionFlags - && roleFlags == other.roleFlags - && bitrate == other.bitrate - && maxInputSize == other.maxInputSize - && subsampleOffsetUs == other.subsampleOffsetUs - && width == other.width - && height == other.height - && rotationDegrees == other.rotationDegrees - && stereoMode == other.stereoMode - && channelCount == other.channelCount - && sampleRate == other.sampleRate - && pcmEncoding == other.pcmEncoding - && encoderDelay == other.encoderDelay - && encoderPadding == other.encoderPadding - && accessibilityChannel == other.accessibilityChannel - && Float.compare(frameRate, other.frameRate) == 0 - && Float.compare(pixelWidthHeightRatio, other.pixelWidthHeightRatio) == 0 - && Util.areEqual(exoMediaCryptoType, other.exoMediaCryptoType) - && Util.areEqual(id, other.id) - && Util.areEqual(label, other.label) - && Util.areEqual(codecs, other.codecs) - && Util.areEqual(containerMimeType, other.containerMimeType) - && Util.areEqual(sampleMimeType, other.sampleMimeType) - && Util.areEqual(language, other.language) - && Arrays.equals(projectionData, other.projectionData) - && Util.areEqual(metadata, other.metadata) - && Util.areEqual(colorInfo, other.colorInfo) - && Util.areEqual(drmInitData, other.drmInitData) - && initializationDataEquals(other); - } - - /** - * Returns whether the {@link #initializationData}s belonging to this format and {@code other} are - * equal. - * - * @param other The other format whose {@link #initializationData} is being compared. - * @return Whether the {@link #initializationData}s belonging to this format and {@code other} are - * equal. - */ - public boolean initializationDataEquals(Format other) { - if (initializationData.size() != other.initializationData.size()) { - return false; - } - for (int i = 0; i < initializationData.size(); i++) { - if (!Arrays.equals(initializationData.get(i), other.initializationData.get(i))) { - return false; - } - } - return true; - } - - // Utility methods - - /** Returns a prettier {@link String} than {@link #toString()}, intended for logging. */ - public static String toLogString(@Nullable Format format) { - if (format == null) { - return "null"; - } - StringBuilder builder = new StringBuilder(); - builder.append("id=").append(format.id).append(", mimeType=").append(format.sampleMimeType); - 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); - } - if (format.frameRate != Format.NO_VALUE) { - builder.append(", fps=").append(format.frameRate); - } - if (format.channelCount != Format.NO_VALUE) { - builder.append(", channels=").append(format.channelCount); - } - if (format.sampleRate != Format.NO_VALUE) { - builder.append(", sample_rate=").append(format.sampleRate); - } - if (format.language != null) { - builder.append(", language=").append(format.language); - } - if (format.label != null) { - builder.append(", label=").append(format.label); - } - return builder.toString(); - } - - // Parcelable implementation. - - @Override - public int describeContents() { - return 0; - } - - @Override - public void writeToParcel(Parcel dest, int flags) { - dest.writeString(id); - dest.writeString(label); - dest.writeInt(selectionFlags); - dest.writeInt(roleFlags); - dest.writeInt(bitrate); - dest.writeString(codecs); - dest.writeParcelable(metadata, 0); - // Container specific. - dest.writeString(containerMimeType); - // Elementary stream specific. - dest.writeString(sampleMimeType); - dest.writeInt(maxInputSize); - int initializationDataSize = initializationData.size(); - dest.writeInt(initializationDataSize); - for (int i = 0; i < initializationDataSize; i++) { - dest.writeByteArray(initializationData.get(i)); - } - dest.writeParcelable(drmInitData, 0); - dest.writeLong(subsampleOffsetUs); - // Video specific. - dest.writeInt(width); - dest.writeInt(height); - dest.writeFloat(frameRate); - dest.writeInt(rotationDegrees); - dest.writeFloat(pixelWidthHeightRatio); - Util.writeBoolean(dest, projectionData != null); - if (projectionData != null) { - dest.writeByteArray(projectionData); - } - dest.writeInt(stereoMode); - dest.writeParcelable(colorInfo, flags); - // Audio specific. - dest.writeInt(channelCount); - dest.writeInt(sampleRate); - dest.writeInt(pcmEncoding); - dest.writeInt(encoderDelay); - dest.writeInt(encoderPadding); - // Audio and text specific. - dest.writeString(language); - dest.writeInt(accessibilityChannel); - } - - public static final Creator CREATOR = new Creator() { - - @Override - public Format createFromParcel(Parcel in) { - return new Format(in); - } - - @Override - public Format[] newArray(int size) { - return new Format[size]; - } - - }; -} 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 7d21182de2..13c0659ccc 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 @@ -23,20 +23,14 @@ import com.google.android.exoplayer2.drm.DrmSession; */ public final class FormatHolder { - /** Whether the {@link #format} setter also sets the {@link #drmSession} field. */ - // TODO: Remove once all Renderers and MediaSources have migrated to the new DRM model [Internal - // ref: b/129764794]. - public boolean includesDrmSession; - /** An accompanying context for decrypting samples in the format. */ - @Nullable public DrmSession drmSession; + @Nullable public DrmSession drmSession; /** The held {@link Format}. */ @Nullable public Format format; /** Clears the holder. */ public void clear() { - includesDrmSession = false; drmSession = null; format = null; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/LoadControl.java b/library/core/src/main/java/com/google/android/exoplayer2/LoadControl.java index 80be0b9e71..94f61bb618 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/LoadControl.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/LoadControl.java @@ -90,11 +90,17 @@ public interface LoadControl { /** * Called by the player to determine whether it should continue to load the source. * + * @param playbackPositionUs The current playback position in microseconds, relative to the start + * of the {@link Timeline.Period period} that will continue to be loaded if this method + * returns {@code true}. If playback of this period has not yet started, the value will be + * negative and equal in magnitude to the duration of any media in previous periods still to + * be played. * @param bufferedDurationUs The duration of media that's currently buffered. * @param playbackSpeed The current playback speed. * @return Whether the loading should continue. */ - boolean shouldContinueLoading(long bufferedDurationUs, float playbackSpeed); + boolean shouldContinueLoading( + long playbackPositionUs, long bufferedDurationUs, float playbackSpeed); /** * Called repeatedly by the player when it's loading the source, has yet to start playback, and 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 850d2b7d10..3c7a41439a 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 @@ -19,7 +19,6 @@ import androidx.annotation.Nullable; import com.google.android.exoplayer2.source.ClippingMediaPeriod; import com.google.android.exoplayer2.source.EmptySampleStream; import com.google.android.exoplayer2.source.MediaPeriod; -import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; import com.google.android.exoplayer2.source.SampleStream; import com.google.android.exoplayer2.source.TrackGroupArray; @@ -52,11 +51,18 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; public boolean hasEnabledTracks; /** {@link MediaPeriodInfo} about this media period. */ public MediaPeriodInfo info; + /** + * Whether all required renderers have been enabled with the {@link #sampleStreams} for this + * {@link #mediaPeriod}. This means either {@link Renderer#enable(RendererConfiguration, Format[], + * SampleStream, long, boolean, boolean, long)} or {@link Renderer#replaceStream(Format[], + * SampleStream, long)} has been called. + */ + public boolean allRenderersEnabled; private final boolean[] mayRetainStreamFlags; private final RendererCapabilities[] rendererCapabilities; private final TrackSelector trackSelector; - private final MediaSource mediaSource; + private final MediaSourceList mediaSourceList; @Nullable private MediaPeriodHolder next; private TrackGroupArray trackGroups; @@ -70,7 +76,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; * @param rendererPositionOffsetUs The renderer time of the start of the period, in microseconds. * @param trackSelector The track selector. * @param allocator The allocator. - * @param mediaSource The media source that produced the media period. + * @param mediaSourceList The playlist. * @param info Information used to identify this media period in its timeline period. * @param emptyTrackSelectorResult A {@link TrackSelectorResult} with empty selections for each * renderer. @@ -80,13 +86,13 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; long rendererPositionOffsetUs, TrackSelector trackSelector, Allocator allocator, - MediaSource mediaSource, + MediaSourceList mediaSourceList, MediaPeriodInfo info, TrackSelectorResult emptyTrackSelectorResult) { this.rendererCapabilities = rendererCapabilities; this.rendererPositionOffsetUs = rendererPositionOffsetUs; this.trackSelector = trackSelector; - this.mediaSource = mediaSource; + this.mediaSourceList = mediaSourceList; this.uid = info.id.periodUid; this.info = info; this.trackGroups = TrackGroupArray.EMPTY; @@ -95,7 +101,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; mayRetainStreamFlags = new boolean[rendererCapabilities.length]; mediaPeriod = createMediaPeriod( - info.id, mediaSource, allocator, info.startPositionUs, info.endPositionUs); + info.id, mediaSourceList, allocator, info.startPositionUs, info.endPositionUs); } /** @@ -173,9 +179,14 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; prepared = true; trackGroups = mediaPeriod.getTrackGroups(); TrackSelectorResult selectorResult = selectTracks(playbackSpeed, timeline); + long requestedStartPositionUs = info.startPositionUs; + if (info.durationUs != C.TIME_UNSET && requestedStartPositionUs >= info.durationUs) { + // Make sure start position doesn't exceed period duration. + requestedStartPositionUs = Math.max(0, info.durationUs - 1); + } long newStartPositionUs = applyTrackSelection( - selectorResult, info.startPositionUs, /* forceRecreateStreams= */ false); + selectorResult, requestedStartPositionUs, /* forceRecreateStreams= */ false); rendererPositionOffsetUs += info.startPositionUs - newStartPositionUs; info = info.copyWithStartPositionUs(newStartPositionUs); } @@ -305,7 +316,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; /** Releases the media period. No other method should be called after the release. */ public void release() { disableTrackSelectionsInResult(); - releaseMediaPeriod(info.endPositionUs, mediaSource, mediaPeriod); + releaseMediaPeriod(info.endPositionUs, mediaSourceList, mediaPeriod); } /** @@ -402,11 +413,11 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; /** Returns a media period corresponding to the given {@code id}. */ private static MediaPeriod createMediaPeriod( MediaPeriodId id, - MediaSource mediaSource, + MediaSourceList mediaSourceList, Allocator allocator, long startPositionUs, long endPositionUs) { - MediaPeriod mediaPeriod = mediaSource.createPeriod(id, allocator, startPositionUs); + MediaPeriod mediaPeriod = mediaSourceList.createPeriod(id, allocator, startPositionUs); if (endPositionUs != C.TIME_UNSET && endPositionUs != C.TIME_END_OF_SOURCE) { mediaPeriod = new ClippingMediaPeriod( @@ -417,12 +428,12 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; /** Releases the given {@code mediaPeriod}, logging and suppressing any errors. */ private static void releaseMediaPeriod( - long endPositionUs, MediaSource mediaSource, MediaPeriod mediaPeriod) { + long endPositionUs, MediaSourceList mediaSourceList, MediaPeriod mediaPeriod) { try { if (endPositionUs != C.TIME_UNSET && endPositionUs != C.TIME_END_OF_SOURCE) { - mediaSource.releasePeriod(((ClippingMediaPeriod) mediaPeriod).mediaPeriod); + mediaSourceList.releasePeriod(((ClippingMediaPeriod) mediaPeriod).mediaPeriod); } else { - mediaSource.releasePeriod(mediaPeriod); + mediaSourceList.releasePeriod(mediaPeriod); } } catch (RuntimeException e) { // There's nothing we can do. 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 2733df7ba6..b14af5e1de 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 @@ -28,11 +28,13 @@ import com.google.android.exoplayer2.util.Util; /** The start position of the media to play within the media period, in microseconds. */ public final long startPositionUs; /** - * If this is an ad, the position to play in the next content media period. {@link C#TIME_UNSET} - * if this is not an ad or the next content media period should be played from its default - * position. + * The requested next start position for the current timeline period, in microseconds, or {@link + * C#TIME_UNSET} if the period was requested to start at its default position. + * + *

      Note that if {@link #id} refers to an ad, this is the requested start position for the + * suspended content. */ - public final long contentPositionUs; + public final long requestedContentPositionUs; /** * The end position to which the media period's content is clipped in order to play a following ad * group, in microseconds, or {@link C#TIME_UNSET} if there is no following ad group or if this @@ -51,6 +53,8 @@ import com.google.android.exoplayer2.util.Util; * period corresponding to a timeline period without ads). */ public final boolean isLastInTimelinePeriod; + /** Whether this is the last media period in its timeline window. */ + public final boolean isLastInTimelineWindow; /** * Whether this is the last media period in the entire timeline. If true, {@link * #isLastInTimelinePeriod} will also be true. @@ -60,17 +64,19 @@ import com.google.android.exoplayer2.util.Util; MediaPeriodInfo( MediaPeriodId id, long startPositionUs, - long contentPositionUs, + long requestedContentPositionUs, long endPositionUs, long durationUs, boolean isLastInTimelinePeriod, + boolean isLastInTimelineWindow, boolean isFinal) { this.id = id; this.startPositionUs = startPositionUs; - this.contentPositionUs = contentPositionUs; + this.requestedContentPositionUs = requestedContentPositionUs; this.endPositionUs = endPositionUs; this.durationUs = durationUs; this.isLastInTimelinePeriod = isLastInTimelinePeriod; + this.isLastInTimelineWindow = isLastInTimelineWindow; this.isFinal = isFinal; } @@ -84,27 +90,29 @@ import com.google.android.exoplayer2.util.Util; : new MediaPeriodInfo( id, startPositionUs, - contentPositionUs, + requestedContentPositionUs, endPositionUs, durationUs, isLastInTimelinePeriod, + isLastInTimelineWindow, isFinal); } /** - * Returns a copy of this instance with the content position set to the specified value. May - * return the same instance if nothing changed. + * Returns a copy of this instance with the requested content position set to the specified value. + * May return the same instance if nothing changed. */ - public MediaPeriodInfo copyWithContentPositionUs(long contentPositionUs) { - return contentPositionUs == this.contentPositionUs + public MediaPeriodInfo copyWithRequestedContentPositionUs(long requestedContentPositionUs) { + return requestedContentPositionUs == this.requestedContentPositionUs ? this : new MediaPeriodInfo( id, startPositionUs, - contentPositionUs, + requestedContentPositionUs, endPositionUs, durationUs, isLastInTimelinePeriod, + isLastInTimelineWindow, isFinal); } @@ -118,10 +126,11 @@ import com.google.android.exoplayer2.util.Util; } MediaPeriodInfo that = (MediaPeriodInfo) o; return startPositionUs == that.startPositionUs - && contentPositionUs == that.contentPositionUs + && requestedContentPositionUs == that.requestedContentPositionUs && endPositionUs == that.endPositionUs && durationUs == that.durationUs && isLastInTimelinePeriod == that.isLastInTimelinePeriod + && isLastInTimelineWindow == that.isLastInTimelineWindow && isFinal == that.isFinal && Util.areEqual(id, that.id); } @@ -131,10 +140,11 @@ import com.google.android.exoplayer2.util.Util; int result = 17; result = 31 * result + id.hashCode(); result = 31 * result + (int) startPositionUs; - result = 31 * result + (int) contentPositionUs; + result = 31 * result + (int) requestedContentPositionUs; result = 31 * result + (int) endPositionUs; result = 31 * result + (int) durationUs; result = 31 * result + (isLastInTimelinePeriod ? 1 : 0); + result = 31 * result + (isLastInTimelineWindow ? 1 : 0); result = 31 * result + (isFinal ? 1 : 0); return result; } 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 901b7b4d94..a749f09f93 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 @@ -19,7 +19,6 @@ import android.util.Pair; import androidx.annotation.Nullable; import com.google.android.exoplayer2.Player.RepeatMode; import com.google.android.exoplayer2.source.MediaPeriod; -import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; import com.google.android.exoplayer2.trackselection.TrackSelector; import com.google.android.exoplayer2.trackselection.TrackSelectorResult; @@ -44,7 +43,6 @@ import com.google.android.exoplayer2.util.Assertions; private final Timeline.Window window; private long nextWindowSequenceNumber; - private Timeline timeline; private @RepeatMode int repeatMode; private boolean shuffleModeEnabled; @Nullable private MediaPeriodHolder playing; @@ -58,33 +56,32 @@ import com.google.android.exoplayer2.util.Assertions; public MediaPeriodQueue() { period = new Timeline.Period(); window = new Timeline.Window(); - timeline = Timeline.EMPTY; - } - - /** - * Sets the {@link Timeline}. Call {@link #updateQueuedPeriods(long, long)} to update the queued - * media periods to take into account the new timeline. - */ - public void setTimeline(Timeline timeline) { - this.timeline = timeline; } /** * Sets the {@link RepeatMode} and returns whether the repeat mode change has been fully handled. * If not, it is necessary to seek to the current playback position. + * + * @param timeline The current timeline. + * @param repeatMode The new repeat mode. + * @return Whether the repeat mode change has been fully handled. */ - public boolean updateRepeatMode(@RepeatMode int repeatMode) { + public boolean updateRepeatMode(Timeline timeline, @RepeatMode int repeatMode) { this.repeatMode = repeatMode; - return updateForPlaybackModeChange(); + return updateForPlaybackModeChange(timeline); } /** * Sets whether shuffling is enabled and returns whether the shuffle mode change has been fully * handled. If not, it is necessary to seek to the current playback position. + * + * @param timeline The current timeline. + * @param shuffleModeEnabled Whether shuffling mode is enabled. + * @return Whether the shuffle mode change has been fully handled. */ - public boolean updateShuffleModeEnabled(boolean shuffleModeEnabled) { + public boolean updateShuffleModeEnabled(Timeline timeline, boolean shuffleModeEnabled) { this.shuffleModeEnabled = shuffleModeEnabled; - return updateForPlaybackModeChange(); + return updateForPlaybackModeChange(timeline); } /** Returns whether {@code mediaPeriod} is the current loading media period. */ @@ -120,11 +117,12 @@ import com.google.android.exoplayer2.util.Assertions; * @return The {@link MediaPeriodInfo} for the next media period to load, or {@code null} if not * yet known. */ - public @Nullable MediaPeriodInfo getNextMediaPeriodInfo( + @Nullable + public MediaPeriodInfo getNextMediaPeriodInfo( long rendererPositionUs, PlaybackInfo playbackInfo) { return loading == null ? getFirstMediaPeriodInfo(playbackInfo) - : getFollowingMediaPeriodInfo(loading, rendererPositionUs); + : getFollowingMediaPeriodInfo(playbackInfo.timeline, loading, rendererPositionUs); } /** @@ -134,7 +132,7 @@ import com.google.android.exoplayer2.util.Assertions; * @param rendererCapabilities The renderer capabilities. * @param trackSelector The track selector. * @param allocator The allocator. - * @param mediaSource The media source that produced the media period. + * @param mediaSourceList The list of media sources. * @param info Information used to identify this media period in its timeline period. * @param emptyTrackSelectorResult A {@link TrackSelectorResult} with empty selections for each * renderer. @@ -143,13 +141,13 @@ import com.google.android.exoplayer2.util.Assertions; RendererCapabilities[] rendererCapabilities, TrackSelector trackSelector, Allocator allocator, - MediaSource mediaSource, + MediaSourceList mediaSourceList, MediaPeriodInfo info, TrackSelectorResult emptyTrackSelectorResult) { long rendererPositionOffsetUs = loading == null - ? (info.id.isAd() && info.contentPositionUs != C.TIME_UNSET - ? info.contentPositionUs + ? (info.id.isAd() && info.requestedContentPositionUs != C.TIME_UNSET + ? info.requestedContentPositionUs : 0) : (loading.getRendererOffset() + loading.info.durationUs - info.startPositionUs); MediaPeriodHolder newPeriodHolder = @@ -158,7 +156,7 @@ import com.google.android.exoplayer2.util.Assertions; rendererPositionOffsetUs, trackSelector, allocator, - mediaSource, + mediaSourceList, info, emptyTrackSelectorResult); if (loading != null) { @@ -258,21 +256,14 @@ import com.google.android.exoplayer2.util.Assertions; return removedReading; } - /** - * Clears the queue. - * - * @param keepFrontPeriodUid Whether the queue should keep the id of the media period in the front - * of queue (typically the playing one) for later reuse. - */ - public void clear(boolean keepFrontPeriodUid) { + /** Clears the queue. */ + public void clear() { MediaPeriodHolder front = playing; if (front != null) { - oldFrontPeriodUid = keepFrontPeriodUid ? front.uid : null; + oldFrontPeriodUid = front.uid; oldFrontPeriodWindowSequenceNumber = front.info.id.windowSequenceNumber; removeAfter(front); front.release(); - } else if (!keepFrontPeriodUid) { - oldFrontPeriodUid = null; } playing = null; loading = null; @@ -286,13 +277,15 @@ import com.google.android.exoplayer2.util.Assertions; * current playback position. The method assumes that the first media period in the queue is still * consistent with the new timeline. * + * @param timeline The new timeline. * @param rendererPositionUs The current renderer position in microseconds. * @param maxRendererReadPositionUs The maximum renderer position up to which renderers have read * the current reading media period in microseconds, or {@link C#TIME_END_OF_SOURCE} if they * have read to the end. * @return Whether the timeline change has been handled completely. */ - public boolean updateQueuedPeriods(long rendererPositionUs, long maxRendererReadPositionUs) { + public boolean updateQueuedPeriods( + Timeline timeline, long rendererPositionUs, long maxRendererReadPositionUs) { // TODO: Merge this into setTimeline so that the queue gets updated as soon as the new timeline // is set, once all cases handled by ExoPlayerImplInternal.handleSourceInfoRefreshed can be // handled here. @@ -307,9 +300,10 @@ import com.google.android.exoplayer2.util.Assertions; // The id and start position of the first period have already been verified by // ExoPlayerImplInternal.handleSourceInfoRefreshed. Just update duration, isLastInTimeline // and isLastInPeriod flags. - newPeriodInfo = getUpdatedMediaPeriodInfo(oldPeriodInfo); + newPeriodInfo = getUpdatedMediaPeriodInfo(timeline, oldPeriodInfo); } else { - newPeriodInfo = getFollowingMediaPeriodInfo(previousPeriodHolder, rendererPositionUs); + newPeriodInfo = + getFollowingMediaPeriodInfo(timeline, previousPeriodHolder, rendererPositionUs); if (newPeriodInfo == null) { // We've loaded a next media period that is not in the new timeline. return !removeAfter(previousPeriodHolder); @@ -320,8 +314,11 @@ import com.google.android.exoplayer2.util.Assertions; } } - // Use new period info, but keep old content position. - periodHolder.info = newPeriodInfo.copyWithContentPositionUs(oldPeriodInfo.contentPositionUs); + // Use the new period info, but keep the old requested content position to avoid overriding it + // by the default content position generated in getFollowingMediaPeriodInfo. + periodHolder.info = + newPeriodInfo.copyWithRequestedContentPositionUs( + oldPeriodInfo.requestedContentPositionUs); if (!areDurationsCompatible(oldPeriodInfo.durationUs, newPeriodInfo.durationUs)) { // The period duration changed. Remove all subsequent periods and check whether we read @@ -349,13 +346,15 @@ import com.google.android.exoplayer2.util.Assertions; * account the current timeline. This method must only be called if the period is still part of * the current timeline. * + * @param timeline The current timeline used to update the media period. * @param info Media period info for a media period based on an old timeline. * @return The updated media period info for the current timeline. */ - public MediaPeriodInfo getUpdatedMediaPeriodInfo(MediaPeriodInfo info) { + public MediaPeriodInfo getUpdatedMediaPeriodInfo(Timeline timeline, MediaPeriodInfo info) { MediaPeriodId id = info.id; boolean isLastInPeriod = isLastInPeriod(id); - boolean isLastInTimeline = isLastInTimeline(id, isLastInPeriod); + boolean isLastInWindow = isLastInWindow(timeline, id); + boolean isLastInTimeline = isLastInTimeline(timeline, id, isLastInPeriod); timeline.getPeriodByUid(info.id.periodUid, period); long durationUs = id.isAd() @@ -366,10 +365,11 @@ import com.google.android.exoplayer2.util.Assertions; return new MediaPeriodInfo( id, info.startPositionUs, - info.contentPositionUs, + info.requestedContentPositionUs, info.endPositionUs, durationUs, isLastInPeriod, + isLastInWindow, isLastInTimeline); } @@ -378,13 +378,16 @@ import com.google.android.exoplayer2.util.Assertions; * played, returning an identifier for an ad group if one needs to be played before the specified * position, or an identifier for a content media period if not. * + * @param timeline The timeline the period is part of. * @param periodUid The uid of the timeline period to play. * @param positionUs The next content position in the period to play. * @return The identifier for the first media period to play, taking into account unplayed ads. */ - public MediaPeriodId resolveMediaPeriodIdForAds(Object periodUid, long positionUs) { - long windowSequenceNumber = resolvePeriodIndexToWindowSequenceNumber(periodUid); - return resolveMediaPeriodIdForAds(periodUid, positionUs, windowSequenceNumber); + public MediaPeriodId resolveMediaPeriodIdForAds( + Timeline timeline, Object periodUid, long positionUs) { + long windowSequenceNumber = resolvePeriodIndexToWindowSequenceNumber(timeline, periodUid); + return resolveMediaPeriodIdForAds( + timeline, periodUid, positionUs, windowSequenceNumber, period); } // Internal methods. @@ -394,14 +397,20 @@ import com.google.android.exoplayer2.util.Assertions; * played, returning an identifier for an ad group if one needs to be played before the specified * position, or an identifier for a content media period if not. * + * @param timeline The timeline the period is part of. * @param periodUid The uid of the timeline period to play. * @param positionUs The next content position in the period to play. * @param windowSequenceNumber The sequence number of the window in the buffered sequence of * windows this period is part of. + * @param period A scratch {@link Timeline.Period}. * @return The identifier for the first media period to play, taking into account unplayed ads. */ - private MediaPeriodId resolveMediaPeriodIdForAds( - Object periodUid, long positionUs, long windowSequenceNumber) { + private static MediaPeriodId resolveMediaPeriodIdForAds( + Timeline timeline, + Object periodUid, + long positionUs, + long windowSequenceNumber, + Timeline.Period period) { timeline.getPeriodByUid(periodUid, period); int adGroupIndex = period.getAdGroupIndexForPositionUs(positionUs); if (adGroupIndex == C.INDEX_UNSET) { @@ -418,10 +427,11 @@ import com.google.android.exoplayer2.util.Assertions; * the window sequence number of an existing matching media period or by creating a new window * sequence number. * + * @param timeline The timeline the period is part of. * @param periodUid The uid of the timeline period. * @return A window sequence number for a media period created for this timeline period. */ - private long resolvePeriodIndexToWindowSequenceNumber(Object periodUid) { + private long resolvePeriodIndexToWindowSequenceNumber(Timeline timeline, Object periodUid) { int windowIndex = timeline.getPeriodByUid(periodUid, period).windowIndex; if (oldFrontPeriodUid != null) { int oldFrontPeriodIndex = timeline.getIndexOfPeriod(oldFrontPeriodUid); @@ -481,8 +491,10 @@ import com.google.android.exoplayer2.util.Assertions; /** * Updates the queue for any playback mode change, and returns whether the change was fully * handled. If not, it is necessary to seek to the current playback position. + * + * @param timeline The current timeline. */ - private boolean updateForPlaybackModeChange() { + private boolean updateForPlaybackModeChange(Timeline timeline) { // Find the last existing period holder that matches the new period order. MediaPeriodHolder lastValidPeriodHolder = playing; if (lastValidPeriodHolder == null) { @@ -514,7 +526,7 @@ import com.google.android.exoplayer2.util.Assertions; boolean readingPeriodRemoved = removeAfter(lastValidPeriodHolder); // Update the period info for the last holder, as it may now be the last period in the timeline. - lastValidPeriodHolder.info = getUpdatedMediaPeriodInfo(lastValidPeriodHolder.info); + lastValidPeriodHolder.info = getUpdatedMediaPeriodInfo(timeline, lastValidPeriodHolder.info); // If renderers may have read from a period that's been removed, it is necessary to restart. return !readingPeriodRemoved; @@ -525,20 +537,25 @@ import com.google.android.exoplayer2.util.Assertions; */ private MediaPeriodInfo getFirstMediaPeriodInfo(PlaybackInfo playbackInfo) { return getMediaPeriodInfo( - playbackInfo.periodId, playbackInfo.contentPositionUs, playbackInfo.startPositionUs); + playbackInfo.timeline, + playbackInfo.periodId, + playbackInfo.requestedContentPositionUs, + playbackInfo.positionUs); } /** * Returns the {@link MediaPeriodInfo} for the media period following {@code mediaPeriodHolder}'s * media period. * + * @param timeline The current timeline. * @param mediaPeriodHolder The media period holder. * @param rendererPositionUs The current renderer position in microseconds. * @return The following media period's info, or {@code null} if it is not yet possible to get the * next media period info. */ - private @Nullable MediaPeriodInfo getFollowingMediaPeriodInfo( - MediaPeriodHolder mediaPeriodHolder, long rendererPositionUs) { + @Nullable + private MediaPeriodInfo getFollowingMediaPeriodInfo( + Timeline timeline, MediaPeriodHolder mediaPeriodHolder, long rendererPositionUs) { // TODO: This method is called repeatedly from ExoPlayerImplInternal.maybeUpdateLoadingPeriod // but if the timeline is not ready to provide the next period it can't return a non-null value // until the timeline is updated. Store whether the next timeline period is ready when the @@ -570,6 +587,7 @@ import com.google.android.exoplayer2.util.Assertions; // want it to be from its default start position, so project the default start position // forward by the duration of the buffer, and start buffering from this point. contentPositionUs = C.TIME_UNSET; + @Nullable Pair defaultPosition = timeline.getPeriodPosition( window, @@ -594,8 +612,9 @@ import com.google.android.exoplayer2.util.Assertions; contentPositionUs = 0; } MediaPeriodId periodId = - resolveMediaPeriodIdForAds(nextPeriodUid, startPositionUs, windowSequenceNumber); - return getMediaPeriodInfo(periodId, contentPositionUs, startPositionUs); + resolveMediaPeriodIdForAds( + timeline, nextPeriodUid, startPositionUs, windowSequenceNumber, period); + return getMediaPeriodInfo(timeline, periodId, contentPositionUs, startPositionUs); } MediaPeriodId currentPeriodId = mediaPeriodInfo.id; @@ -613,17 +632,19 @@ import com.google.android.exoplayer2.util.Assertions; return !period.isAdAvailable(adGroupIndex, nextAdIndexInAdGroup) ? null : getMediaPeriodInfoForAd( + timeline, currentPeriodId.periodUid, adGroupIndex, nextAdIndexInAdGroup, - mediaPeriodInfo.contentPositionUs, + mediaPeriodInfo.requestedContentPositionUs, currentPeriodId.windowSequenceNumber); } else { // Play content from the ad group position. - long startPositionUs = mediaPeriodInfo.contentPositionUs; + long startPositionUs = mediaPeriodInfo.requestedContentPositionUs; if (startPositionUs == C.TIME_UNSET) { // If we're transitioning from an ad group to content starting from its default position, // project the start position forward as if this were a transition to a new window. + @Nullable Pair defaultPosition = timeline.getPeriodPosition( window, @@ -637,7 +658,11 @@ import com.google.android.exoplayer2.util.Assertions; startPositionUs = defaultPosition.second; } return getMediaPeriodInfoForContent( - currentPeriodId.periodUid, startPositionUs, currentPeriodId.windowSequenceNumber); + timeline, + currentPeriodId.periodUid, + startPositionUs, + mediaPeriodInfo.requestedContentPositionUs, + currentPeriodId.windowSequenceNumber); } } else { // Play the next ad group if it's available. @@ -645,14 +670,17 @@ import com.google.android.exoplayer2.util.Assertions; if (nextAdGroupIndex == C.INDEX_UNSET) { // The next ad group can't be played. Play content from the previous end position instead. return getMediaPeriodInfoForContent( + timeline, currentPeriodId.periodUid, /* startPositionUs= */ mediaPeriodInfo.durationUs, + /* requestedContentPositionUs= */ mediaPeriodInfo.durationUs, currentPeriodId.windowSequenceNumber); } int adIndexInAdGroup = period.getFirstAdIndexToPlay(nextAdGroupIndex); return !period.isAdAvailable(nextAdGroupIndex, adIndexInAdGroup) ? null : getMediaPeriodInfoForAd( + timeline, currentPeriodId.periodUid, nextAdGroupIndex, adIndexInAdGroup, @@ -662,24 +690,31 @@ import com.google.android.exoplayer2.util.Assertions; } private MediaPeriodInfo getMediaPeriodInfo( - MediaPeriodId id, long contentPositionUs, long startPositionUs) { + Timeline timeline, MediaPeriodId id, long requestedContentPositionUs, long startPositionUs) { timeline.getPeriodByUid(id.periodUid, period); if (id.isAd()) { if (!period.isAdAvailable(id.adGroupIndex, id.adIndexInAdGroup)) { return null; } return getMediaPeriodInfoForAd( + timeline, id.periodUid, id.adGroupIndex, id.adIndexInAdGroup, - contentPositionUs, + requestedContentPositionUs, id.windowSequenceNumber); } else { - return getMediaPeriodInfoForContent(id.periodUid, startPositionUs, id.windowSequenceNumber); + return getMediaPeriodInfoForContent( + timeline, + id.periodUid, + startPositionUs, + requestedContentPositionUs, + id.windowSequenceNumber); } } private MediaPeriodInfo getMediaPeriodInfoForAd( + Timeline timeline, Object periodUid, int adGroupIndex, int adIndexInAdGroup, @@ -695,6 +730,10 @@ import com.google.android.exoplayer2.util.Assertions; adIndexInAdGroup == period.getFirstAdIndexToPlay(adGroupIndex) ? period.getAdResumePositionUs() : 0; + if (durationUs != C.TIME_UNSET && startPositionUs >= durationUs) { + // Ensure start position doesn't exceed duration. + startPositionUs = Math.max(0, durationUs - 1); + } return new MediaPeriodInfo( id, startPositionUs, @@ -702,15 +741,22 @@ import com.google.android.exoplayer2.util.Assertions; /* endPositionUs= */ C.TIME_UNSET, durationUs, /* isLastInTimelinePeriod= */ false, + /* isLastInTimelineWindow= */ false, /* isFinal= */ false); } private MediaPeriodInfo getMediaPeriodInfoForContent( - Object periodUid, long startPositionUs, long windowSequenceNumber) { + Timeline timeline, + Object periodUid, + long startPositionUs, + long requestedContentPositionUs, + long windowSequenceNumber) { + timeline.getPeriodByUid(periodUid, period); int nextAdGroupIndex = period.getAdGroupIndexAfterPositionUs(startPositionUs); MediaPeriodId id = new MediaPeriodId(periodUid, windowSequenceNumber, nextAdGroupIndex); boolean isLastInPeriod = isLastInPeriod(id); - boolean isLastInTimeline = isLastInTimeline(id, isLastInPeriod); + boolean isLastInWindow = isLastInWindow(timeline, id); + boolean isLastInTimeline = isLastInTimeline(timeline, id, isLastInPeriod); long endPositionUs = nextAdGroupIndex != C.INDEX_UNSET ? period.getAdGroupTimeUs(nextAdGroupIndex) @@ -719,13 +765,18 @@ import com.google.android.exoplayer2.util.Assertions; endPositionUs == C.TIME_UNSET || endPositionUs == C.TIME_END_OF_SOURCE ? period.durationUs : endPositionUs; + if (durationUs != C.TIME_UNSET && startPositionUs >= durationUs) { + // Ensure start position doesn't exceed duration. + startPositionUs = Math.max(0, durationUs - 1); + } return new MediaPeriodInfo( id, startPositionUs, - /* contentPositionUs= */ C.TIME_UNSET, + requestedContentPositionUs, endPositionUs, durationUs, isLastInPeriod, + isLastInWindow, isLastInTimeline); } @@ -733,7 +784,17 @@ import com.google.android.exoplayer2.util.Assertions; return !id.isAd() && id.nextAdGroupIndex == C.INDEX_UNSET; } - private boolean isLastInTimeline(MediaPeriodId id, boolean isLastMediaPeriodInPeriod) { + private boolean isLastInWindow(Timeline timeline, MediaPeriodId id) { + if (!isLastInPeriod(id)) { + return false; + } + int windowIndex = timeline.getPeriodByUid(id.periodUid, period).windowIndex; + int periodIndex = timeline.getIndexOfPeriod(id.periodUid); + return timeline.getWindow(windowIndex, window).lastPeriodIndex == periodIndex; + } + + private boolean isLastInTimeline( + Timeline timeline, MediaPeriodId id, boolean isLastMediaPeriodInPeriod) { int periodIndex = timeline.getIndexOfPeriod(id.periodUid); int windowIndex = timeline.getPeriod(periodIndex, period).windowIndex; return !timeline.getWindow(windowIndex, window).isDynamic diff --git a/library/core/src/main/java/com/google/android/exoplayer2/Playlist.java b/library/core/src/main/java/com/google/android/exoplayer2/MediaSourceList.java similarity index 90% rename from library/core/src/main/java/com/google/android/exoplayer2/Playlist.java rename to library/core/src/main/java/com/google/android/exoplayer2/MediaSourceList.java index b11b454d65..bdd00fe54d 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/Playlist.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/MediaSourceList.java @@ -18,6 +18,7 @@ package com.google.android.exoplayer2; import android.os.Handler; import androidx.annotation.Nullable; import com.google.android.exoplayer2.analytics.AnalyticsCollector; +import com.google.android.exoplayer2.drm.DrmSessionEventListener; import com.google.android.exoplayer2.source.LoadEventInfo; import com.google.android.exoplayer2.source.MaskingMediaPeriod; import com.google.android.exoplayer2.source.MaskingMediaSource; @@ -30,6 +31,7 @@ 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.Log; import com.google.android.exoplayer2.util.Util; import java.io.IOException; import java.util.ArrayList; @@ -50,10 +52,10 @@ import java.util.Set; * *

      With the exception of the constructor, all methods are called on the playback thread. */ -/* package */ class Playlist { +/* package */ class MediaSourceList { /** Listener for source events. */ - public interface PlaylistInfoRefreshListener { + public interface MediaSourceListInfoRefreshListener { /** * Called when the timeline of a media item has changed and a new timeline that reflects the @@ -64,12 +66,14 @@ import java.util.Set; void onPlaylistUpdateRequested(); } + private static final String TAG = "MediaSourceList"; + private final List mediaSourceHolders; private final Map mediaSourceByMediaPeriod; private final Map mediaSourceByUid; - private final PlaylistInfoRefreshListener playlistInfoListener; + private final MediaSourceListInfoRefreshListener mediaSourceListInfoListener; private final MediaSourceEventListener.EventDispatcher eventDispatcher; - private final HashMap childSources; + private final HashMap childSources; private final Set enabledMediaSourceHolders; private ShuffleOrder shuffleOrder; @@ -78,8 +82,8 @@ import java.util.Set; @Nullable private TransferListener mediaTransferListener; @SuppressWarnings("initialization") - public Playlist(PlaylistInfoRefreshListener listener) { - playlistInfoListener = listener; + public MediaSourceList(MediaSourceListInfoRefreshListener listener) { + mediaSourceListInfoListener = listener; shuffleOrder = new DefaultShuffleOrder(0); mediaSourceByMediaPeriod = new IdentityHashMap<>(); mediaSourceByUid = new HashMap<>(); @@ -247,7 +251,8 @@ import java.util.Set; * @param analyticsCollector The analytics collector. */ public final void setAnalyticsCollector(Handler handler, AnalyticsCollector analyticsCollector) { - eventDispatcher.addEventListener(handler, analyticsCollector); + eventDispatcher.addEventListener(handler, analyticsCollector, MediaSourceEventListener.class); + eventDispatcher.addEventListener(handler, analyticsCollector, DrmSessionEventListener.class); } /** @@ -321,7 +326,12 @@ import java.util.Set; /** Releases the playlist. */ public final void release() { for (MediaSourceAndListener childSource : childSources.values()) { - childSource.mediaSource.releaseSource(childSource.caller); + try { + childSource.mediaSource.releaseSource(childSource.caller); + } catch (RuntimeException e) { + // There's nothing we can do. + Log.e(TAG, "Failed to release child source.", e); + } childSource.mediaSource.removeEventListener(childSource.eventListener); } childSources.clear(); @@ -424,10 +434,11 @@ import java.util.Set; private void prepareChildSource(MediaSourceHolder holder) { MediaSource mediaSource = holder.mediaSource; MediaSource.MediaSourceCaller caller = - (source, timeline) -> playlistInfoListener.onPlaylistUpdateRequested(); - MediaSourceEventListener eventListener = new ForwardingEventListener(holder); + (source, timeline) -> mediaSourceListInfoListener.onPlaylistUpdateRequested(); + ForwardingEventListener eventListener = new ForwardingEventListener(holder); childSources.put(holder, new MediaSourceAndListener(mediaSource, caller, eventListener)); mediaSource.addEventListener(Util.createHandler(), eventListener); + mediaSource.addDrmEventListener(Util.createHandler(), eventListener); mediaSource.prepareSource(caller, mediaTransferListener); } @@ -588,16 +599,19 @@ import java.util.Set; } } - private final class ForwardingEventListener implements MediaSourceEventListener { + private final class ForwardingEventListener + implements MediaSourceEventListener, DrmSessionEventListener { - private final Playlist.MediaSourceHolder id; + private final MediaSourceList.MediaSourceHolder id; private EventDispatcher eventDispatcher; - public ForwardingEventListener(Playlist.MediaSourceHolder id) { - eventDispatcher = Playlist.this.eventDispatcher; + public ForwardingEventListener(MediaSourceList.MediaSourceHolder id) { + eventDispatcher = MediaSourceList.this.eventDispatcher; this.id = id; } + // MediaSourceEventListener implementation + @Override public void onMediaPeriodCreated(int windowIndex, MediaSource.MediaPeriodId mediaPeriodId) { if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) { @@ -685,6 +699,50 @@ import java.util.Set; } } + // DrmSessionEventListener implementation + + @Override + public void onDrmSessionAcquired() { + eventDispatcher.dispatch( + (listener, windowIndex, mediaPeriodId) -> listener.onDrmSessionAcquired(), + DrmSessionEventListener.class); + } + + @Override + public void onDrmKeysLoaded() { + eventDispatcher.dispatch( + (listener, windowIndex, mediaPeriodId) -> listener.onDrmKeysLoaded(), + DrmSessionEventListener.class); + } + + @Override + public void onDrmSessionManagerError(Exception error) { + eventDispatcher.dispatch( + (listener, windowIndex, mediaPeriodId) -> listener.onDrmSessionManagerError(error), + DrmSessionEventListener.class); + } + + @Override + public void onDrmKeysRestored() { + eventDispatcher.dispatch( + (listener, windowIndex, mediaPeriodId) -> listener.onDrmKeysRestored(), + DrmSessionEventListener.class); + } + + @Override + public void onDrmKeysRemoved() { + eventDispatcher.dispatch( + (listener, windowIndex, mediaPeriodId) -> listener.onDrmKeysRemoved(), + DrmSessionEventListener.class); + } + + @Override + public void onDrmSessionReleased() { + eventDispatcher.dispatch( + (listener, windowIndex, mediaPeriodId) -> listener.onDrmSessionReleased(), + DrmSessionEventListener.class); + } + /** Updates the event dispatcher and returns whether the event should be dispatched. */ private boolean maybeUpdateEventDispatcher( int childWindowIndex, @Nullable MediaSource.MediaPeriodId childMediaPeriodId) { @@ -700,7 +758,7 @@ import java.util.Set; if (eventDispatcher.windowIndex != windowIndex || !Util.areEqual(eventDispatcher.mediaPeriodId, mediaPeriodId)) { eventDispatcher = - Playlist.this.eventDispatcher.withParameters( + MediaSourceList.this.eventDispatcher.withParameters( windowIndex, mediaPeriodId, /* mediaTimeOffsetMs= */ 0L); } return true; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/NoSampleRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/NoSampleRenderer.java index 52bf4b3d06..47ed8cec6a 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/NoSampleRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/NoSampleRenderer.java @@ -28,7 +28,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; */ public abstract class NoSampleRenderer implements Renderer, RendererCapabilities { - @MonotonicNonNull private RendererConfiguration configuration; + private @MonotonicNonNull RendererConfiguration configuration; private int index; private int state; @Nullable private SampleStream stream; @@ -60,24 +60,15 @@ public abstract class NoSampleRenderer implements Renderer, RendererCapabilities return state; } - /** - * Replaces the {@link SampleStream} that will be associated with this renderer. - *

      - * This method may be called when the renderer is in the following states: - * {@link #STATE_DISABLED}. - * - * @param configuration The renderer configuration. - * @param formats The enabled formats. Should be empty. - * @param stream The {@link SampleStream} from which the renderer should consume. - * @param positionUs The player's current position. - * @param joining Whether this renderer is being enabled to join an ongoing playback. - * @param offsetUs The offset that should be subtracted from {@code positionUs} - * to get the playback position with respect to the media. - * @throws ExoPlaybackException If an error occurs. - */ @Override - public final void enable(RendererConfiguration configuration, Format[] formats, - SampleStream stream, long positionUs, boolean joining, long offsetUs) + public final void enable( + RendererConfiguration configuration, + Format[] formats, + SampleStream stream, + long positionUs, + boolean joining, + boolean mayRenderStartOfStream, + long offsetUs) throws ExoPlaybackException { Assertions.checkState(state == STATE_DISABLED); this.configuration = configuration; @@ -94,18 +85,6 @@ public abstract class NoSampleRenderer implements Renderer, RendererCapabilities onStarted(); } - /** - * Replaces the {@link SampleStream} that will be associated with this renderer. - *

      - * This method may be called when the renderer is in the following states: - * {@link #STATE_ENABLED}, {@link #STATE_STARTED}. - * - * @param formats The enabled formats. Should be empty. - * @param stream The {@link SampleStream} to be associated with this renderer. - * @param offsetUs The offset that should be subtracted from {@code positionUs} in - * {@link #render(long, long)} to get the playback position with respect to the media. - * @throws ExoPlaybackException If an error occurs. - */ @Override public final void replaceStream(Format[] formats, SampleStream stream, long offsetUs) throws ExoPlaybackException { @@ -185,11 +164,13 @@ public abstract class NoSampleRenderer implements Renderer, RendererCapabilities // RendererCapabilities implementation. @Override + @Capabilities public int supportsFormat(Format format) throws ExoPlaybackException { - return FORMAT_UNSUPPORTED_TYPE; + return RendererCapabilities.create(FORMAT_UNSUPPORTED_TYPE); } @Override + @AdaptiveSupport public int supportsMixedMimeTypeAdaptation() throws ExoPlaybackException { return ADAPTIVE_NOT_SUPPORTED; } 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 9d2a3b5459..f183af0d8c 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 @@ -17,6 +17,7 @@ package com.google.android.exoplayer2; import androidx.annotation.CheckResult; import androidx.annotation.Nullable; +import com.google.android.exoplayer2.Player.PlaybackSuppressionReason; import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.trackselection.TrackSelectorResult; @@ -28,7 +29,7 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult; /** * 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)}. + * is used when playback infos are created with {@link #createDummy(TrackSelectorResult)}. */ private static final MediaPeriodId DUMMY_MEDIA_PERIOD_ID = new MediaPeriodId(/* periodUid= */ new Object()); @@ -38,18 +39,14 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult; /** 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. Note that this value changes for - * each position discontinuity. + * The requested next start position for the current period in the {@link #timeline}, in + * microseconds, or {@link C#TIME_UNSET} if the period was requested to start at its default + * position. + * + *

      Note that if {@link #periodId} refers to an ad, this is the requested start position for the + * suspended content. */ - 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 or if the suspended content should be played from - * its default position. - */ - public final long contentPositionUs; + public final long requestedContentPositionUs; /** The current playback state. One of the {@link Player}.STATE_ constants. */ @Player.State public final int playbackState; /** The current playback error, or null if this is not an error state. */ @@ -62,6 +59,10 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult; public final TrackSelectorResult trackSelectorResult; /** The {@link MediaPeriodId} of the currently loading media period in the {@link #timeline}. */ public final MediaPeriodId loadingMediaPeriodId; + /** Whether playback should proceed when {@link #playbackState} == {@link Player#STATE_READY}. */ + public final boolean playWhenReady; + /** Reason why playback is suppressed even though {@link #playWhenReady} is {@code true}. */ + @PlaybackSuppressionReason public final int playbackSuppressionReason; /** * Position up to which media is buffered in {@link #loadingMediaPeriodId) relative to the start @@ -83,27 +84,26 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult; * 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) { + public static PlaybackInfo createDummy(TrackSelectorResult emptyTrackSelectorResult) { return new PlaybackInfo( Timeline.EMPTY, DUMMY_MEDIA_PERIOD_ID, - startPositionUs, - /* contentPositionUs= */ C.TIME_UNSET, + /* requestedContentPositionUs= */ C.TIME_UNSET, Player.STATE_IDLE, /* playbackError= */ null, /* isLoading= */ false, TrackGroupArray.EMPTY, emptyTrackSelectorResult, DUMMY_MEDIA_PERIOD_ID, - startPositionUs, + /* playWhenReady= */ false, + Player.PLAYBACK_SUPPRESSION_REASON_NONE, + /* bufferedPositionUs= */ 0, /* totalBufferedDurationUs= */ 0, - startPositionUs); + /* positionUs= */ 0); } /** @@ -111,8 +111,7 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult; * * @param timeline See {@link #timeline}. * @param periodId See {@link #periodId}. - * @param startPositionUs See {@link #startPositionUs}. - * @param contentPositionUs See {@link #contentPositionUs}. + * @param requestedContentPositionUs See {@link #requestedContentPositionUs}. * @param playbackState See {@link #playbackState}. * @param isLoading See {@link #isLoading}. * @param trackGroups See {@link #trackGroups}. @@ -125,57 +124,37 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult; public PlaybackInfo( Timeline timeline, MediaPeriodId periodId, - long startPositionUs, - long contentPositionUs, + long requestedContentPositionUs, @Player.State int playbackState, @Nullable ExoPlaybackException playbackError, boolean isLoading, TrackGroupArray trackGroups, TrackSelectorResult trackSelectorResult, MediaPeriodId loadingMediaPeriodId, + boolean playWhenReady, + @PlaybackSuppressionReason int playbackSuppressionReason, long bufferedPositionUs, long totalBufferedDurationUs, long positionUs) { this.timeline = timeline; this.periodId = periodId; - this.startPositionUs = startPositionUs; - this.contentPositionUs = contentPositionUs; + this.requestedContentPositionUs = requestedContentPositionUs; this.playbackState = playbackState; this.playbackError = playbackError; this.isLoading = isLoading; this.trackGroups = trackGroups; this.trackSelectorResult = trackSelectorResult; this.loadingMediaPeriodId = loadingMediaPeriodId; + this.playWhenReady = playWhenReady; + this.playbackSuppressionReason = playbackSuppressionReason; this.bufferedPositionUs = bufferedPositionUs; this.totalBufferedDurationUs = totalBufferedDurationUs; this.positionUs = positionUs; } - /** - * Returns dummy media period id for the first-to-be-played period of the current timeline. - * - * @param shuffleModeEnabled Whether shuffle mode is enabled. - * @param window A writable {@link Timeline.Window}. - * @param period A writable {@link Timeline.Period}. - * @return A dummy media period id for the first-to-be-played period of the current timeline. - */ - public MediaPeriodId getDummyFirstMediaPeriodId( - boolean shuffleModeEnabled, Timeline.Window window, Timeline.Period period) { - if (timeline.isEmpty()) { - return DUMMY_MEDIA_PERIOD_ID; - } - int firstWindowIndex = timeline.getFirstWindowIndex(shuffleModeEnabled); - int firstPeriodIndex = timeline.getWindow(firstWindowIndex, window).firstPeriodIndex; - int currentPeriodIndex = timeline.getIndexOfPeriod(periodId.periodUid); - long windowSequenceNumber = C.INDEX_UNSET; - if (currentPeriodIndex != C.INDEX_UNSET) { - int currentWindowIndex = timeline.getPeriod(currentPeriodIndex, period).windowIndex; - if (firstWindowIndex == currentWindowIndex) { - // Keep window sequence number if the new position is still in the same window. - windowSequenceNumber = periodId.windowSequenceNumber; - } - } - return new MediaPeriodId(timeline.getUidOfPeriod(firstPeriodIndex), windowSequenceNumber); + /** Returns dummy period id for an empty timeline. */ + public static MediaPeriodId getDummyPeriodForEmptyTimeline() { + return DUMMY_MEDIA_PERIOD_ID; } /** @@ -183,28 +162,34 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult; * * @param periodId New playing media period. See {@link #periodId}. * @param positionUs New position. See {@link #positionUs}. - * @param contentPositionUs New content position. See {@link #contentPositionUs}. Value is ignored - * if {@code periodId.isAd()} is true. + * @param requestedContentPositionUs New requested content position. See {@link + * #requestedContentPositionUs}. * @param totalBufferedDurationUs New buffered duration. See {@link #totalBufferedDurationUs}. + * @param trackGroups The track groups for the new position. See {@link #trackGroups}. + * @param trackSelectorResult The track selector result for the new position. See {@link + * #trackSelectorResult}. * @return Copied playback info with new playing position. */ @CheckResult public PlaybackInfo copyWithNewPosition( MediaPeriodId periodId, long positionUs, - long contentPositionUs, - long totalBufferedDurationUs) { + long requestedContentPositionUs, + long totalBufferedDurationUs, + TrackGroupArray trackGroups, + TrackSelectorResult trackSelectorResult) { return new PlaybackInfo( timeline, periodId, - positionUs, - periodId.isAd() ? contentPositionUs : C.TIME_UNSET, + requestedContentPositionUs, playbackState, playbackError, isLoading, trackGroups, trackSelectorResult, loadingMediaPeriodId, + playWhenReady, + playbackSuppressionReason, bufferedPositionUs, totalBufferedDurationUs, positionUs); @@ -221,14 +206,15 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult; return new PlaybackInfo( timeline, periodId, - startPositionUs, - contentPositionUs, + requestedContentPositionUs, playbackState, playbackError, isLoading, trackGroups, trackSelectorResult, loadingMediaPeriodId, + playWhenReady, + playbackSuppressionReason, bufferedPositionUs, totalBufferedDurationUs, positionUs); @@ -245,14 +231,15 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult; return new PlaybackInfo( timeline, periodId, - startPositionUs, - contentPositionUs, + requestedContentPositionUs, playbackState, playbackError, isLoading, trackGroups, trackSelectorResult, loadingMediaPeriodId, + playWhenReady, + playbackSuppressionReason, bufferedPositionUs, totalBufferedDurationUs, positionUs); @@ -269,14 +256,15 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult; return new PlaybackInfo( timeline, periodId, - startPositionUs, - contentPositionUs, + requestedContentPositionUs, playbackState, playbackError, isLoading, trackGroups, trackSelectorResult, loadingMediaPeriodId, + playWhenReady, + playbackSuppressionReason, bufferedPositionUs, totalBufferedDurationUs, positionUs); @@ -293,40 +281,15 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult; return new PlaybackInfo( timeline, periodId, - startPositionUs, - contentPositionUs, - playbackState, - playbackError, - isLoading, - trackGroups, - trackSelectorResult, - loadingMediaPeriodId, - bufferedPositionUs, - totalBufferedDurationUs, - positionUs); - } - - /** - * Copies playback info with new track information. - * - * @param trackGroups New track groups. See {@link #trackGroups}. - * @param trackSelectorResult New track selector result. See {@link #trackSelectorResult}. - * @return Copied playback info with new track information. - */ - @CheckResult - public PlaybackInfo copyWithTrackInfo( - TrackGroupArray trackGroups, TrackSelectorResult trackSelectorResult) { - return new PlaybackInfo( - timeline, - periodId, - startPositionUs, - contentPositionUs, + requestedContentPositionUs, playbackState, playbackError, isLoading, trackGroups, trackSelectorResult, loadingMediaPeriodId, + playWhenReady, + playbackSuppressionReason, bufferedPositionUs, totalBufferedDurationUs, positionUs); @@ -343,14 +306,44 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult; return new PlaybackInfo( timeline, periodId, - startPositionUs, - contentPositionUs, + requestedContentPositionUs, playbackState, playbackError, isLoading, trackGroups, trackSelectorResult, loadingMediaPeriodId, + playWhenReady, + playbackSuppressionReason, + bufferedPositionUs, + totalBufferedDurationUs, + positionUs); + } + + /** + * Copies playback info with new information about whether playback should proceed when ready. + * + * @param playWhenReady Whether playback should proceed when {@link #playbackState} == {@link + * Player#STATE_READY}. + * @param playbackSuppressionReason Reason why playback is suppressed even though {@link + * #playWhenReady} is {@code true}. + * @return Copied playback info with new information. + */ + @CheckResult + public PlaybackInfo copyWithPlayWhenReady( + boolean playWhenReady, @PlaybackSuppressionReason int playbackSuppressionReason) { + return new PlaybackInfo( + timeline, + periodId, + requestedContentPositionUs, + playbackState, + playbackError, + isLoading, + trackGroups, + trackSelectorResult, + loadingMediaPeriodId, + playWhenReady, + playbackSuppressionReason, bufferedPositionUs, totalBufferedDurationUs, positionUs); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/PlaybackParameters.java b/library/core/src/main/java/com/google/android/exoplayer2/PlaybackParameters.java index 057cb371e5..afa0a7ebc4 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/PlaybackParameters.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/PlaybackParameters.java @@ -19,25 +19,19 @@ import androidx.annotation.Nullable; import com.google.android.exoplayer2.util.Assertions; /** - * The parameters that apply to playback. + * @deprecated Use {@link Player#setPlaybackSpeed(float)} and {@link + * Player.AudioComponent#setSkipSilenceEnabled(boolean)} instead. */ +@SuppressWarnings("deprecation") +@Deprecated public final class PlaybackParameters { - /** - * The default playback parameters: real-time playback with no pitch modification or silence - * skipping. - */ + /** The default playback parameters: real-time playback with no silence skipping. */ public static final PlaybackParameters DEFAULT = new PlaybackParameters(/* speed= */ 1f); /** The factor by which playback will be sped up. */ public final float speed; - /** The factor by which the audio pitch will be scaled. */ - public final float pitch; - - /** Whether to skip silence in the input. */ - public final boolean skipSilence; - private final int scaledUsPerMs; /** @@ -46,33 +40,8 @@ public final class PlaybackParameters { * @param speed The factor by which playback will be sped up. Must be greater than zero. */ public PlaybackParameters(float speed) { - this(speed, /* pitch= */ 1f, /* skipSilence= */ false); - } - - /** - * Creates new playback parameters that set the playback speed and audio pitch scaling factor. - * - * @param speed The factor by which playback will be sped up. Must be greater than zero. - * @param pitch The factor by which the audio pitch will be scaled. Must be greater than zero. - */ - public PlaybackParameters(float speed, float pitch) { - this(speed, pitch, /* skipSilence= */ false); - } - - /** - * Creates new playback parameters that set the playback speed, audio pitch scaling factor and - * whether to skip silence in the audio stream. - * - * @param speed The factor by which playback will be sped up. Must be greater than zero. - * @param pitch The factor by which the audio pitch will be scaled. Must be greater than zero. - * @param skipSilence Whether to skip silences in the audio stream. - */ - public PlaybackParameters(float speed, float pitch, boolean skipSilence) { Assertions.checkArgument(speed > 0); - Assertions.checkArgument(pitch > 0); this.speed = speed; - this.pitch = pitch; - this.skipSilence = skipSilence; scaledUsPerMs = Math.round(speed * 1000f); } @@ -96,18 +65,11 @@ public final class PlaybackParameters { return false; } PlaybackParameters other = (PlaybackParameters) obj; - return this.speed == other.speed - && this.pitch == other.pitch - && this.skipSilence == other.skipSilence; + return this.speed == other.speed; } @Override public int hashCode() { - int result = 17; - result = 31 * result + Float.floatToRawIntBits(speed); - result = 31 * result + Float.floatToRawIntBits(pitch); - result = 31 * result + (skipSilence ? 1 : 0); - return result; + return Float.floatToRawIntBits(speed); } - } 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 a29851fefc..b06484aef2 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 @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2; +import android.content.Context; import android.os.Looper; import android.view.Surface; import android.view.SurfaceHolder; @@ -22,10 +23,11 @@ import android.view.SurfaceView; import android.view.TextureView; import androidx.annotation.IntDef; import androidx.annotation.Nullable; -import com.google.android.exoplayer2.C.VideoScalingMode; 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.device.DeviceInfo; +import com.google.android.exoplayer2.device.DeviceListener; import com.google.android.exoplayer2.metadata.MetadataOutput; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.text.TextOutput; @@ -38,24 +40,25 @@ import com.google.android.exoplayer2.video.spherical.CameraMotionListener; import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.util.List; /** * A media player interface defining traditional high-level functionality, such as the ability to * play, pause, seek and query properties of the currently playing media. - *

      - * Some important properties of media players that implement this interface are: + * + *

      Some important properties of media players that implement this interface are: + * *

        - *
      • They can provide a {@link Timeline} representing the structure of the media being played, - * which can be obtained by calling {@link #getCurrentTimeline()}.
      • - *
      • They can provide a {@link TrackGroupArray} defining the currently available tracks, - * which can be obtained by calling {@link #getCurrentTrackGroups()}.
      • - *
      • They contain a number of renderers, each of which is able to render tracks of a single - * type (e.g. audio, video or text). The number of renderers and their respective track types - * can be obtained by calling {@link #getRendererCount()} and {@link #getRendererType(int)}. - *
      • - *
      • They can provide a {@link TrackSelectionArray} defining which of the currently available - * tracks are selected to be rendered by each renderer. This can be obtained by calling - * {@link #getCurrentTrackSelections()}}.
      • + *
      • They can provide a {@link Timeline} representing the structure of the media being played, + * which can be obtained by calling {@link #getCurrentTimeline()}. + *
      • They can provide a {@link TrackGroupArray} defining the currently available tracks, which + * can be obtained by calling {@link #getCurrentTrackGroups()}. + *
      • They contain a number of renderers, each of which is able to render tracks of a single type + * (e.g. audio, video or text). The number of renderers and their respective track types can + * be obtained by calling {@link #getRendererCount()} and {@link #getRendererType(int)}. + *
      • They can provide a {@link TrackSelectionArray} defining which of the currently available + * tracks are selected to be rendered by each renderer. This can be obtained by calling {@link + * #getCurrentTrackSelections()}}. *
      */ public interface Player { @@ -123,6 +126,18 @@ public interface Player { /** Returns the attributes for audio playback. */ AudioAttributes getAudioAttributes(); + /** + * Sets the ID of the audio session to attach to the underlying {@link + * android.media.AudioTrack}. + * + *

      The audio session ID can be generated using {@link C#generateAudioSessionIdV21(Context)} + * for API 21+. + * + * @param audioSessionId The audio session ID, or {@link C#AUDIO_SESSION_ID_UNSET} if it should + * be generated by the framework. + */ + void setAudioSessionId(int audioSessionId); + /** Returns the audio session identifier, or {@link C#AUDIO_SESSION_ID_UNSET} if not set. */ int getAudioSessionId(); @@ -141,20 +156,30 @@ public interface Player { /** Returns the audio volume, with 0 being silence and 1 being unity gain. */ float getVolume(); + + /** + * Sets whether skipping silences in the audio stream is enabled. + * + * @param skipSilenceEnabled Whether skipping silences in the audio stream is enabled. + */ + void setSkipSilenceEnabled(boolean skipSilenceEnabled); + + /** Returns whether skipping silences in the audio stream is enabled. */ + boolean getSkipSilenceEnabled(); } /** The video component of a {@link Player}. */ interface VideoComponent { /** - * Sets the {@link VideoScalingMode}. + * Sets the {@link Renderer.VideoScalingMode}. * - * @param videoScalingMode The {@link VideoScalingMode}. + * @param videoScalingMode The {@link Renderer.VideoScalingMode}. */ - void setVideoScalingMode(@VideoScalingMode int videoScalingMode); + void setVideoScalingMode(@Renderer.VideoScalingMode int videoScalingMode); - /** Returns the {@link VideoScalingMode}. */ - @VideoScalingMode + /** Returns the {@link Renderer.VideoScalingMode}. */ + @Renderer.VideoScalingMode int getVideoScalingMode(); /** @@ -343,6 +368,54 @@ public interface Player { void removeMetadataOutput(MetadataOutput output); } + /** The device component of a {@link Player}. */ + // Note: It's mostly from the androidx.media.VolumeProviderCompat and + // androidx.media.MediaControllerCompat.PlaybackInfo. + interface DeviceComponent { + + /** Adds a listener to receive device events. */ + void addDeviceListener(DeviceListener listener); + + /** Removes a listener of device events. */ + void removeDeviceListener(DeviceListener listener); + + /** Gets the device information. */ + DeviceInfo getDeviceInfo(); + + /** + * Gets the current volume of the device. + * + *

      For devices with {@link DeviceInfo#PLAYBACK_TYPE_LOCAL local playback}, the volume + * returned by this method varies according to the current {@link C.StreamType stream type}. The + * stream type is determined by {@link AudioAttributes#usage} which can be converted to stream + * type with {@link Util#getStreamTypeForAudioUsage(int)}. The audio attributes can be set to + * the player by calling {@link AudioComponent#setAudioAttributes}. + * + *

      For devices with {@link DeviceInfo#PLAYBACK_TYPE_REMOTE remote playback}, the volume of + * the remote device is returned. + */ + int getDeviceVolume(); + + /** Gets whether the device is muted or not. */ + boolean isDeviceMuted(); + + /** + * Sets the volume of the device. + * + * @param volume The volume to set. + */ + void setDeviceVolume(int volume); + + /** Increases the volume of the device. */ + void increaseDeviceVolume(); + + /** Decreases the volume of the device. */ + void decreaseDeviceVolume(); + + /** Sets the mute state of the device. */ + void setDeviceMuted(boolean muted); + } + /** * Listener of changes in player state. All methods have no-op default implementations to allow * selective overrides. @@ -381,7 +454,8 @@ public interface Player { * {@link #onPositionDiscontinuity(int)}. * * @param timeline The latest timeline. Never null, but may be empty. - * @param manifest The latest manifest. May be null. + * @param manifest The latest manifest in case the timeline has a single window only. Always + * null if the timeline has more than a single window. * @param reason The {@link TimelineChangeReason} responsible for this timeline change. * @deprecated Use {@link #onTimelineChanged(Timeline, int)} instead. The manifest can be * accessed by using {@link #getCurrentManifest()} or {@code timeline.getWindow(windowIndex, @@ -406,16 +480,37 @@ public interface Player { * * @param isLoading Whether the source is currently being loaded. */ + @SuppressWarnings("deprecation") + default void onIsLoadingChanged(boolean isLoading) { + onLoadingChanged(isLoading); + } + + /** @deprecated Use {@link #onIsLoadingChanged(boolean)} instead. */ + @Deprecated default void onLoadingChanged(boolean isLoading) {} /** - * Called when the value returned from either {@link #getPlayWhenReady()} or {@link - * #getPlaybackState()} changes. + * @deprecated Use {@link #onPlaybackStateChanged(int)} and {@link + * #onPlayWhenReadyChanged(boolean, int)} instead. + */ + @Deprecated + default void onPlayerStateChanged(boolean playWhenReady, @State int playbackState) {} + + /** + * Called when the value returned from {@link #getPlaybackState()} changes. + * + * @param state The new playback {@link State state}. + */ + default void onPlaybackStateChanged(@State int state) {} + + /** + * Called when the value returned from {@link #getPlayWhenReady()} changes. * * @param playWhenReady Whether playback will proceed when ready. - * @param playbackState The new {@link State playback state}. + * @param reason The {@link PlayWhenReadyChangeReason reason} for the change. */ - default void onPlayerStateChanged(boolean playWhenReady, @State int playbackState) {} + default void onPlayWhenReadyChanged( + boolean playWhenReady, @PlayWhenReadyChangeReason int reason) {} /** * Called when the value returned from {@link #getPlaybackSuppressionReason()} changes. @@ -470,20 +565,26 @@ public interface Player { default void onPositionDiscontinuity(@DiscontinuityReason int reason) {} /** - * Called when the current playback parameters change. The playback parameters may change due to - * a call to {@link #setPlaybackParameters(PlaybackParameters)}, or the player itself may change - * them (for example, if audio playback switches to passthrough mode, where speed adjustment is - * no longer possible). - * - * @param playbackParameters The playback parameters. + * @deprecated Use {@link #onPlaybackSpeedChanged(float)} and {@link + * AudioListener#onSkipSilenceEnabledChanged(boolean)} instead. */ + @SuppressWarnings("deprecation") + @Deprecated 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)}. + * Called when the current playback speed changes. The normal playback speed is 1. The speed may + * change due to a call to {@link #setPlaybackSpeed(float)}, or the player itself may change it + * (for example, if audio playback switches to passthrough mode, where speed adjustment is no + * longer possible). */ + default void onPlaybackSpeedChanged(float playbackSpeed) {} + + /** + * @deprecated Seeks are processed without delay. Listen to {@link + * #onPositionDiscontinuity(int)} with reason {@link #DISCONTINUITY_REASON_SEEK} instead. + */ + @Deprecated default void onSeekProcessed() {} } @@ -548,6 +649,35 @@ public interface Player { */ int STATE_ENDED = 4; + /** + * Reasons for {@link #getPlayWhenReady() playWhenReady} changes. One of {@link + * #PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST}, {@link + * #PLAY_WHEN_READY_CHANGE_REASON_AUDIO_FOCUS_LOSS}, {@link + * #PLAY_WHEN_READY_CHANGE_REASON_AUDIO_BECOMING_NOISY}, {@link + * #PLAY_WHEN_READY_CHANGE_REASON_REMOTE} or {@link + * #PLAY_WHEN_READY_CHANGE_REASON_END_OF_MEDIA_ITEM}. + */ + @Documented + @Retention(RetentionPolicy.SOURCE) + @IntDef({ + PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST, + PLAY_WHEN_READY_CHANGE_REASON_AUDIO_FOCUS_LOSS, + PLAY_WHEN_READY_CHANGE_REASON_AUDIO_BECOMING_NOISY, + PLAY_WHEN_READY_CHANGE_REASON_REMOTE, + PLAY_WHEN_READY_CHANGE_REASON_END_OF_MEDIA_ITEM + }) + @interface PlayWhenReadyChangeReason {} + /** Playback has been started or paused by a call to {@link #setPlayWhenReady(boolean)}. */ + int PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST = 1; + /** Playback has been paused because of a loss of audio focus. */ + int PLAY_WHEN_READY_CHANGE_REASON_AUDIO_FOCUS_LOSS = 2; + /** Playback has been paused to avoid becoming noisy. */ + int PLAY_WHEN_READY_CHANGE_REASON_AUDIO_BECOMING_NOISY = 3; + /** Playback has been started or paused because of a remote change. */ + int PLAY_WHEN_READY_CHANGE_REASON_REMOTE = 4; + /** Playback has been paused at the end of a media item. */ + int PLAY_WHEN_READY_CHANGE_REASON_END_OF_MEDIA_ITEM = 5; + /** * Reason why playback is suppressed even though {@link #getPlayWhenReady()} is {@code true}. One * of {@link #PLAYBACK_SUPPRESSION_REASON_NONE} or {@link @@ -619,25 +749,20 @@ public interface Player { int DISCONTINUITY_REASON_INTERNAL = 4; /** - * Reasons for timeline changes. One of {@link #TIMELINE_CHANGE_REASON_PREPARED}, {@link - * #TIMELINE_CHANGE_REASON_RESET} or {@link #TIMELINE_CHANGE_REASON_DYNAMIC}. + * Reasons for timeline changes. One of {@link #TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED} or {@link + * #TIMELINE_CHANGE_REASON_SOURCE_UPDATE}. */ @Documented @Retention(RetentionPolicy.SOURCE) - @IntDef({ - TIMELINE_CHANGE_REASON_PREPARED, - TIMELINE_CHANGE_REASON_RESET, - TIMELINE_CHANGE_REASON_DYNAMIC - }) + @IntDef({TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, TIMELINE_CHANGE_REASON_SOURCE_UPDATE}) @interface TimelineChangeReason {} - /** Timeline and manifest changed as a result of a player initialization with new media. */ - int TIMELINE_CHANGE_REASON_PREPARED = 0; - /** Timeline and manifest changed as a result of a player reset. */ - int TIMELINE_CHANGE_REASON_RESET = 1; - /** - * Timeline or manifest changed as a result of an dynamic update introduced by the played media. - */ - int TIMELINE_CHANGE_REASON_DYNAMIC = 2; + /** Timeline changed as a result of a change of the playlist items or the order of the items. */ + int TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED = 0; + /** Timeline changed as a result of a dynamic update introduced by the played media. */ + int TIMELINE_CHANGE_REASON_SOURCE_UPDATE = 1; + + /** The default playback speed. */ + float DEFAULT_PLAYBACK_SPEED = 1.0f; /** Returns the component of this player for audio output, or null if audio is not supported. */ @Nullable @@ -657,6 +782,10 @@ public interface Player { @Nullable MetadataComponent getMetadataComponent(); + /** Returns the component of this player for playback device, or null if it's not supported. */ + @Nullable + DeviceComponent getDeviceComponent(); + /** * Returns the {@link Looper} associated with the application thread that's used to access the * player and on which player events are received. @@ -679,6 +808,134 @@ public interface Player { */ void removeListener(EventListener listener); + /** + * Clears the playlist, adds the specified {@link MediaItem MediaItems} and resets the position to + * the default position. + * + * @param mediaItems The new {@link MediaItem MediaItems}. + */ + void setMediaItems(List mediaItems); + + /** + * Clears the playlist and adds the specified {@link MediaItem MediaItems}. + * + * @param mediaItems The new {@link MediaItem MediaItems}. + * @param resetPosition Whether the playback position should be reset to the default position in + * the first {@link Timeline.Window}. If false, playback will start from the position defined + * by {@link #getCurrentWindowIndex()} and {@link #getCurrentPosition()}. + */ + void setMediaItems(List mediaItems, boolean resetPosition); + + /** + * Clears the playlist and adds the specified {@link MediaItem MediaItems}. + * + * @param mediaItems The new {@link MediaItem MediaItems}. + * @param startWindowIndex The window index to start playback from. If {@link C#INDEX_UNSET} is + * passed, the current position is not reset. + * @param startPositionMs The position in milliseconds to start playback from. If {@link + * C#TIME_UNSET} is passed, the default position of the given window is used. In any case, if + * {@code startWindowIndex} is set to {@link C#INDEX_UNSET}, this parameter is ignored and the + * position is not reset at all. + */ + void setMediaItems(List mediaItems, int startWindowIndex, long startPositionMs); + + /** + * Clears the playlist, adds the specified {@link MediaItem} and resets the position to the + * default position. + * + * @param mediaItem The new {@link MediaItem}. + */ + void setMediaItem(MediaItem mediaItem); + + /** + * Clears the playlist and adds the specified {@link MediaItem}. + * + * @param mediaItem The new {@link MediaItem}. + * @param startPositionMs The position in milliseconds to start playback from. + */ + void setMediaItem(MediaItem mediaItem, long startPositionMs); + + /** + * Clears the playlist and adds the specified {@link MediaItem}. + * + * @param mediaItem The new {@link MediaItem}. + * @param resetPosition Whether the playback position should be reset to the default position. If + * false, playback will start from the position defined by {@link #getCurrentWindowIndex()} + * and {@link #getCurrentPosition()}. + */ + void setMediaItem(MediaItem mediaItem, boolean resetPosition); + + /** + * Adds a media item to the end of the playlist. + * + * @param mediaItem The {@link MediaItem} to add. + */ + void addMediaItem(MediaItem mediaItem); + + /** + * Adds a media item at the given index of the playlist. + * + * @param index The index at which to add the item. + * @param mediaItem The {@link MediaItem} to add. + */ + void addMediaItem(int index, MediaItem mediaItem); + + /** + * Adds a list of media items to the end of the playlist. + * + * @param mediaItems The {@link MediaItem MediaItems} to add. + */ + void addMediaItems(List mediaItems); + + /** + * Adds a list of media items at the given index of the playlist. + * + * @param index The index at which to add the media items. + * @param mediaItems The {@link MediaItem MediaItems} to add. + */ + void addMediaItems(int index, List mediaItems); + + /** + * Moves the media item at the current index to the new index. + * + * @param currentIndex The current index of the media item to move. + * @param newIndex The new index of the media item. If the new index is larger than the size of + * the playlist the item is moved to the end of the playlist. + */ + void moveMediaItem(int currentIndex, int newIndex); + + /** + * Moves the media item range to the new index. + * + * @param fromIndex The start of the range to move. + * @param toIndex The first item not to be included in the range (exclusive). + * @param newIndex The new index of the first media item of the range. If the new index is larger + * than the size of the remaining playlist after removing the range, the range is moved to the + * end of the playlist. + */ + void moveMediaItems(int fromIndex, int toIndex, int newIndex); + + /** + * Removes the media item at the given index of the playlist. + * + * @param index The index at which to remove the media item. + */ + void removeMediaItem(int index); + + /** + * Removes a range of media items from the playlist. + * + * @param fromIndex The index at which to start removing media items. + * @param toIndex The index of the first item to be kept (exclusive). + */ + void removeMediaItems(int fromIndex, int toIndex); + + /** Clears the playlist. */ + void clearMediaItems(); + + /** Prepares the player. */ + void prepare(); + /** * Returns the current {@link State playback state} of the player. * @@ -723,13 +980,26 @@ public interface Player { * @return The error, or {@code null}. */ @Nullable + ExoPlaybackException getPlayerError(); + + /** @deprecated Use {@link #getPlayerError()} instead. */ + @Deprecated + @Nullable ExoPlaybackException getPlaybackError(); + /** + * Resumes playback as soon as {@link #getPlaybackState()} == {@link #STATE_READY}. Equivalent to + * {@code setPlayWhenReady(true)}. + */ + void play(); + + /** Pauses playback. Equivalent to {@code setPlayWhenReady(false)}. */ + void pause(); + /** * Sets whether playback should proceed when {@link #getPlaybackState()} == {@link #STATE_READY}. - *

      - * If the player is already in the ready state then this method can be used to pause and resume - * playback. + * + *

      If the player is already in the ready state then this method pauses and resumes playback. * * @param playWhenReady Whether playback should proceed when ready. */ @@ -838,27 +1108,42 @@ public interface Player { void next(); /** - * Attempts to set the playback parameters. Passing {@code null} sets the parameters to the - * default, {@link PlaybackParameters#DEFAULT}, which means there is no speed or pitch adjustment. - * - *

      Playback parameters changes may cause the player to buffer. {@link - * EventListener#onPlaybackParametersChanged(PlaybackParameters)} will be called whenever the - * currently active playback parameters change. - * - * @param playbackParameters The playback parameters, or {@code null} to use the defaults. + * @deprecated Use {@link #setPlaybackSpeed(float)} or {@link + * AudioComponent#setSkipSilenceEnabled(boolean)} instead. */ + @SuppressWarnings("deprecation") + @Deprecated void setPlaybackParameters(@Nullable PlaybackParameters playbackParameters); /** - * Returns the currently active playback parameters. - * - * @see EventListener#onPlaybackParametersChanged(PlaybackParameters) + * @deprecated Use {@link #getPlaybackSpeed()} or {@link AudioComponent#getSkipSilenceEnabled()} + * instead. */ + @SuppressWarnings("deprecation") + @Deprecated PlaybackParameters getPlaybackParameters(); /** - * Stops playback without resetting the player. Use {@code setPlayWhenReady(false)} rather than - * this method if the intention is to pause playback. + * Attempts to set the playback speed. + * + *

      Playback speed changes may cause the player to buffer. {@link + * EventListener#onPlaybackSpeedChanged(float)} will be called whenever the currently active + * playback speed change. + * + * @param playbackSpeed The playback speed. + */ + void setPlaybackSpeed(float playbackSpeed); + + /** + * Returns the currently active playback speed. + * + * @see EventListener#onPlaybackSpeedChanged(float) + */ + float getPlaybackSpeed(); + + /** + * Stops playback without resetting the player. Use {@link #pause()} rather than this method if + * the intention is to pause playback. * *

      Calling this method will cause the playback state to transition to {@link #STATE_IDLE}. The * player instance can still be used, and {@link #release()} must still be called on the player if @@ -869,8 +1154,8 @@ public interface Player { void stop(); /** - * Stops playback and optionally resets the player. Use {@code setPlayWhenReady(false)} rather - * than this method if the intention is to pause playback. + * Stops playback and optionally resets the player. Use {@link #pause()} rather than this method + * if the intention is to pause playback. * *

      Calling this method will cause the playback state to transition to {@link #STATE_IDLE}. The * player instance can still be used, and {@link #release()} must still be called on the player if @@ -992,6 +1277,19 @@ public interface Player { */ boolean isCurrentWindowLive(); + /** + * Returns the offset of the current playback position from the live edge in milliseconds, or + * {@link C#TIME_UNSET} if the current window {@link #isCurrentWindowLive() isn't live} or the + * offset is unknown. + * + *

      The offset is calculated as {@code currentTime - playbackPosition}, so should usually be + * positive. + * + *

      Note that this offset may rely on an accurate local time, so this method may return an + * incorrect value if the difference between system clock and server clock is unknown. + */ + long getCurrentLiveOffset(); + /** * Returns whether the current window is seekable, or {@code false} if the {@link Timeline} is * empty. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/PlayerMessage.java b/library/core/src/main/java/com/google/android/exoplayer2/PlayerMessage.java index e7ade7b68b..be7c7ce973 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/PlayerMessage.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/PlayerMessage.java @@ -162,7 +162,9 @@ public final class PlayerMessage { /** * Returns position in window at {@link #getWindowIndex()} at which the message will be delivered, - * in milliseconds. If {@link C#TIME_UNSET}, the message will be delivered immediately. + * in milliseconds. If {@link C#TIME_UNSET}, the message will be delivered immediately. If {@link + * C#TIME_END_OF_SOURCE}, the message will be delivered at the end of the window at {@link + * #getWindowIndex()}. */ public long getPositionMs() { return positionMs; @@ -172,7 +174,8 @@ public final class PlayerMessage { * Sets a position in the current window at which the message will be delivered. * * @param positionMs The position in the current window at which the message will be sent, in - * milliseconds. + * milliseconds, or {@link C#TIME_END_OF_SOURCE} to deliver the message at the end of the + * current window. * @return This message. * @throws IllegalStateException If {@link #send()} has already been called. */ @@ -187,7 +190,8 @@ public final class PlayerMessage { * * @param windowIndex The index of the window at which the message will be sent. * @param positionMs The position in the window with index {@code windowIndex} at which the - * message will be sent, in milliseconds. + * message will be sent, in milliseconds, or {@link C#TIME_END_OF_SOURCE} to deliver the + * message at the end of the window with index {@code windowIndex}. * @return This message. * @throws IllegalSeekPositionException If the timeline returned by {@link #getTimeline()} is not * empty and the provided window index is not within the bounds of the timeline. 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 9e44e3741c..fa73f9257d 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 @@ -15,10 +15,19 @@ */ package com.google.android.exoplayer2; +import android.media.MediaCodec; +import android.view.Surface; import androidx.annotation.IntDef; import androidx.annotation.Nullable; +import com.google.android.exoplayer2.PlayerMessage.Target; +import com.google.android.exoplayer2.audio.AuxEffectInfo; import com.google.android.exoplayer2.source.SampleStream; import com.google.android.exoplayer2.util.MediaClock; +import com.google.android.exoplayer2.util.Util; +import com.google.android.exoplayer2.video.DecoderVideoRenderer; +import com.google.android.exoplayer2.video.VideoDecoderOutputBufferRenderer; +import com.google.android.exoplayer2.video.VideoFrameMetadataListener; +import com.google.android.exoplayer2.video.spherical.CameraMotionListener; import java.io.IOException; import java.lang.annotation.Documented; import java.lang.annotation.Retention; @@ -32,10 +41,135 @@ import java.lang.annotation.RetentionPolicy; * valid state transitions are shown below, annotated with the methods that are called during each * transition. * - *

      Renderer state transitions + *

      Renderer state
+ * transitions */ public interface Renderer extends PlayerMessage.Target { + /** + * The type of a message that can be passed to a video renderer via {@link + * ExoPlayer#createMessage(Target)}. The message payload should be the target {@link Surface}, or + * null. + */ + @SuppressWarnings("deprecation") + int MSG_SET_SURFACE = C.MSG_SET_SURFACE; + /** + * A type of a message that can be passed to an audio renderer via {@link + * ExoPlayer#createMessage(Target)}. The message payload should be a {@link Float} with 0 being + * silence and 1 being unity gain. + */ + @SuppressWarnings("deprecation") + int MSG_SET_VOLUME = C.MSG_SET_VOLUME; + /** + * A type of a message that can be passed to an audio renderer via {@link + * ExoPlayer#createMessage(Target)}. The message payload should be an {@link + * com.google.android.exoplayer2.audio.AudioAttributes} instance that will configure 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)}. + * + *

      To get audio attributes that are equivalent to a legacy stream type, pass the stream type to + * {@link Util#getAudioUsageForStreamType(int)} and use the returned {@link C.AudioUsage} to build + * an audio attributes instance. + */ + @SuppressWarnings("deprecation") + int MSG_SET_AUDIO_ATTRIBUTES = C.MSG_SET_AUDIO_ATTRIBUTES; + /** + * The type of a message that can be passed to a {@link MediaCodec}-based video renderer via + * {@link ExoPlayer#createMessage(Target)}. The message payload should be one of the integer + * scaling modes in {@link VideoScalingMode}. + * + *

      Note that the scaling mode only applies if the {@link Surface} targeted by the renderer is + * owned by a {@link android.view.SurfaceView}. + */ + @SuppressWarnings("deprecation") + int MSG_SET_SCALING_MODE = C.MSG_SET_SCALING_MODE; + /** + * A type of a message that can be passed to an audio 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. + */ + @SuppressWarnings("deprecation") + int MSG_SET_AUX_EFFECT_INFO = C.MSG_SET_AUX_EFFECT_INFO; + /** + * The type of a message that can be passed to a video renderer via {@link + * ExoPlayer#createMessage(Target)}. The message payload should be a {@link + * VideoFrameMetadataListener} instance, or null. + */ + @SuppressWarnings("deprecation") + int MSG_SET_VIDEO_FRAME_METADATA_LISTENER = C.MSG_SET_VIDEO_FRAME_METADATA_LISTENER; + /** + * The type of a message that can be passed to a camera motion renderer via {@link + * ExoPlayer#createMessage(Target)}. The message payload should be a {@link CameraMotionListener} + * instance, or null. + */ + @SuppressWarnings("deprecation") + int MSG_SET_CAMERA_MOTION_LISTENER = C.MSG_SET_CAMERA_MOTION_LISTENER; + /** + * The type of a message that can be passed to a {@link DecoderVideoRenderer} via {@link + * ExoPlayer#createMessage(Target)}. The message payload should be the target {@link + * VideoDecoderOutputBufferRenderer}, or null. + * + *

      This message is intended only for use with extension renderers that expect a {@link + * VideoDecoderOutputBufferRenderer}. For other use cases, an output surface should be passed via + * {@link #MSG_SET_SURFACE} instead. + */ + @SuppressWarnings("deprecation") + int MSG_SET_VIDEO_DECODER_OUTPUT_BUFFER_RENDERER = C.MSG_SET_VIDEO_DECODER_OUTPUT_BUFFER_RENDERER; + /** + * The type of a message that can be passed to an audio renderer via {@link + * ExoPlayer#createMessage(Target)}. The message payload should be a {@link Boolean} instance + * telling whether to enable or disable skipping silences in the audio stream. + */ + int MSG_SET_SKIP_SILENCE_ENABLED = 101; + /** + * A type of a message that can be passed to an audio renderer via {@link + * ExoPlayer#createMessage(Target)}. The message payload should be an {@link Integer} instance + * representing the audio session ID that will be attached to the underlying audio track. + */ + int MSG_SET_AUDIO_SESSION_ID = 102; + /** + * Applications or extensions may define custom {@code MSG_*} constants that can be passed to + * renderers. These custom constants must be greater than or equal to this value. + */ + @SuppressWarnings("deprecation") + int MSG_CUSTOM_BASE = C.MSG_CUSTOM_BASE; + + /** + * Video scaling modes for {@link MediaCodec}-based renderers. One of {@link + * #VIDEO_SCALING_MODE_SCALE_TO_FIT} or {@link #VIDEO_SCALING_MODE_SCALE_TO_FIT_WITH_CROPPING}. + */ + // VIDEO_SCALING_MODE_DEFAULT is an intentionally duplicated constant. + @SuppressWarnings("UniqueConstants") + @Documented + @Retention(RetentionPolicy.SOURCE) + @IntDef( + value = { + VIDEO_SCALING_MODE_DEFAULT, + VIDEO_SCALING_MODE_SCALE_TO_FIT, + VIDEO_SCALING_MODE_SCALE_TO_FIT_WITH_CROPPING + }) + @interface VideoScalingMode {} + /** See {@link MediaCodec#VIDEO_SCALING_MODE_SCALE_TO_FIT}. */ + @SuppressWarnings("deprecation") + int VIDEO_SCALING_MODE_SCALE_TO_FIT = C.VIDEO_SCALING_MODE_SCALE_TO_FIT; + /** See {@link MediaCodec#VIDEO_SCALING_MODE_SCALE_TO_FIT_WITH_CROPPING}. */ + @SuppressWarnings("deprecation") + int VIDEO_SCALING_MODE_SCALE_TO_FIT_WITH_CROPPING = + C.VIDEO_SCALING_MODE_SCALE_TO_FIT_WITH_CROPPING; + /** A default video scaling mode for {@link MediaCodec}-based renderers. */ + @SuppressWarnings("deprecation") + int VIDEO_SCALING_MODE_DEFAULT = C.VIDEO_SCALING_MODE_DEFAULT; + /** * The renderer states. One of {@link #STATE_DISABLED}, {@link #STATE_ENABLED} or {@link * #STATE_STARTED}. @@ -63,9 +197,17 @@ public interface Renderer extends PlayerMessage.Target { int STATE_STARTED = 2; /** - * Returns the track type that the {@link Renderer} handles. For example, a video renderer will - * return {@link C#TRACK_TYPE_VIDEO}, an audio renderer will return {@link C#TRACK_TYPE_AUDIO}, a - * text renderer will return {@link C#TRACK_TYPE_TEXT}, and so on. + * Returns the name of this renderer, for logging and debugging purposes. Should typically be the + * renderer's (un-obfuscated) class name. + * + * @return The name of this renderer. + */ + String getName(); + + /** + * Returns the track type that the renderer handles. For example, a video renderer will return + * {@link C#TRACK_TYPE_VIDEO}, an audio renderer will return {@link C#TRACK_TYPE_AUDIO}, a text + * renderer will return {@link C#TRACK_TYPE_TEXT}, and so on. * * @return One of the {@code TRACK_TYPE_*} constants defined in {@link C}. */ @@ -107,21 +249,30 @@ public interface Renderer extends PlayerMessage.Target { /** * Enables the renderer to consume from the specified {@link SampleStream}. - *

      - * This method may be called when the renderer is in the following states: - * {@link #STATE_DISABLED}. + * + *

      This method may be called when the renderer is in the following states: {@link + * #STATE_DISABLED}. * * @param configuration The renderer configuration. * @param formats The enabled formats. * @param stream The {@link SampleStream} from which the renderer should consume. * @param positionUs The player's current position. * @param joining Whether this renderer is being enabled to join an ongoing playback. - * @param offsetUs The offset to be added to timestamps of buffers read from {@code stream} - * before they are rendered. + * @param mayRenderStartOfStream Whether this renderer is allowed to render the start of the + * stream even if the state is not {@link #STATE_STARTED} yet. + * @param offsetUs The offset to be added to timestamps of buffers read from {@code stream} before + * they are rendered. * @throws ExoPlaybackException If an error occurs. */ - void enable(RendererConfiguration configuration, Format[] formats, SampleStream stream, - long positionUs, boolean joining, long offsetUs) throws ExoPlaybackException; + void enable( + RendererConfiguration configuration, + Format[] formats, + SampleStream stream, + long positionUs, + boolean joining, + boolean mayRenderStartOfStream, + long offsetUs) + throws ExoPlaybackException; /** * Starts the renderer, meaning that calls to {@link #render(long, long)} will cause media to be @@ -226,21 +377,26 @@ public interface Renderer extends PlayerMessage.Target { /** * Incrementally renders the {@link SampleStream}. - *

      - * If the renderer is in the {@link #STATE_ENABLED} state then each call to this method will do - * work toward being ready to render the {@link SampleStream} when the renderer is started. It may - * also render the very start of the media, for example the first frame of a video stream. If the + * + *

      If the renderer is in the {@link #STATE_ENABLED} state then each call to this method will do + * work toward being ready to render the {@link SampleStream} when the renderer is started. If the * renderer is in the {@link #STATE_STARTED} state then calls to this method will render the * {@link SampleStream} in sync with the specified media positions. - *

      - * This method should return quickly, and should not block if the renderer is unable to make - * useful progress. - *

      - * This method may be called when the renderer is in the following states: - * {@link #STATE_ENABLED}, {@link #STATE_STARTED}. * - * @param positionUs The current media time in microseconds, measured at the start of the - * current iteration of the rendering loop. + *

      The renderer may also render the very start of the media at the current position (e.g. the + * first frame of a video stream) while still in the {@link #STATE_ENABLED} state, unless it's the + * initial start of the media after calling {@link #enable(RendererConfiguration, Format[], + * SampleStream, long, boolean, boolean, long)} with {@code mayRenderStartOfStream} set to {@code + * false}. + * + *

      This method should return quickly, and should not block if the renderer is unable to make + * useful progress. + * + *

      This method may be called when the renderer is in the following states: {@link + * #STATE_ENABLED}, {@link #STATE_STARTED}. + * + * @param positionUs The current media time in microseconds, measured at the start of the current + * iteration of the rendering loop. * @param elapsedRealtimeUs {@link android.os.SystemClock#elapsedRealtime()} in microseconds, * measured at the start of the current iteration of the rendering loop. * @throws ExoPlaybackException If an error occurs. @@ -265,12 +421,12 @@ public interface Renderer extends PlayerMessage.Target { boolean isReady(); /** - * Whether the renderer is ready for the {@link ExoPlayer} instance to transition to - * {@link Player#STATE_ENDED}. The player will make this transition as soon as {@code true} is - * returned by all of its {@link Renderer}s. - *

      - * This method may be called when the renderer is in the following states: - * {@link #STATE_ENABLED}, {@link #STATE_STARTED}. + * Whether the renderer is ready for the {@link ExoPlayer} instance to transition to {@link + * Player#STATE_ENDED}. The player will make this transition as soon as {@code true} is returned + * by all of its renderers. + * + *

      This method may be called when the renderer is in the following states: {@link + * #STATE_ENABLED}, {@link #STATE_STARTED}. * * @return Whether the renderer is ready for the player to transition to the ended state. */ diff --git a/library/core/src/main/java/com/google/android/exoplayer2/RendererCapabilities.java b/library/core/src/main/java/com/google/android/exoplayer2/RendererCapabilities.java index de0d481386..882c0d1141 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/RendererCapabilities.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/RendererCapabilities.java @@ -15,7 +15,12 @@ */ package com.google.android.exoplayer2; +import android.annotation.SuppressLint; +import androidx.annotation.IntDef; import com.google.android.exoplayer2.util.MimeTypes; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; /** * Defines the capabilities of a {@link Renderer}. @@ -23,10 +28,22 @@ import com.google.android.exoplayer2.util.MimeTypes; public interface RendererCapabilities { /** - * A mask to apply to the result of {@link #supportsFormat(Format)} to obtain one of - * {@link #FORMAT_HANDLED}, {@link #FORMAT_EXCEEDS_CAPABILITIES}, {@link #FORMAT_UNSUPPORTED_DRM}, - * {@link #FORMAT_UNSUPPORTED_SUBTYPE} and {@link #FORMAT_UNSUPPORTED_TYPE}. + * Level of renderer support for a format. One of {@link #FORMAT_HANDLED}, {@link + * #FORMAT_EXCEEDS_CAPABILITIES}, {@link #FORMAT_UNSUPPORTED_DRM}, {@link + * #FORMAT_UNSUPPORTED_SUBTYPE} or {@link #FORMAT_UNSUPPORTED_TYPE}. */ + @Documented + @Retention(RetentionPolicy.SOURCE) + @IntDef({ + FORMAT_HANDLED, + FORMAT_EXCEEDS_CAPABILITIES, + FORMAT_UNSUPPORTED_DRM, + FORMAT_UNSUPPORTED_SUBTYPE, + FORMAT_UNSUPPORTED_TYPE + }) + @interface FormatSupport {} + + /** A mask to apply to {@link Capabilities} to obtain the {@link FormatSupport} only. */ int FORMAT_SUPPORT_MASK = 0b111; /** * The {@link Renderer} is capable of rendering the format. @@ -72,9 +89,15 @@ public interface RendererCapabilities { int FORMAT_UNSUPPORTED_TYPE = 0b000; /** - * A mask to apply to the result of {@link #supportsFormat(Format)} to obtain one of - * {@link #ADAPTIVE_SEAMLESS}, {@link #ADAPTIVE_NOT_SEAMLESS} and {@link #ADAPTIVE_NOT_SUPPORTED}. + * Level of renderer support for adaptive format switches. One of {@link #ADAPTIVE_SEAMLESS}, + * {@link #ADAPTIVE_NOT_SEAMLESS} or {@link #ADAPTIVE_NOT_SUPPORTED}. */ + @Documented + @Retention(RetentionPolicy.SOURCE) + @IntDef({ADAPTIVE_SEAMLESS, ADAPTIVE_NOT_SEAMLESS, ADAPTIVE_NOT_SUPPORTED}) + @interface AdaptiveSupport {} + + /** A mask to apply to {@link Capabilities} to obtain the {@link AdaptiveSupport} only. */ int ADAPTIVE_SUPPORT_MASK = 0b11000; /** * The {@link Renderer} can seamlessly adapt between formats. @@ -91,9 +114,15 @@ public interface RendererCapabilities { int ADAPTIVE_NOT_SUPPORTED = 0b00000; /** - * A mask to apply to the result of {@link #supportsFormat(Format)} to obtain one of - * {@link #TUNNELING_SUPPORTED} and {@link #TUNNELING_NOT_SUPPORTED}. + * Level of renderer support for tunneling. One of {@link #TUNNELING_SUPPORTED} or {@link + * #TUNNELING_NOT_SUPPORTED}. */ + @Documented + @Retention(RetentionPolicy.SOURCE) + @IntDef({TUNNELING_SUPPORTED, TUNNELING_NOT_SUPPORTED}) + @interface TunnelingSupport {} + + /** A mask to apply to {@link Capabilities} to obtain the {@link TunnelingSupport} only. */ int TUNNELING_SUPPORT_MASK = 0b100000; /** * The {@link Renderer} supports tunneled output. @@ -104,6 +133,136 @@ public interface RendererCapabilities { */ int TUNNELING_NOT_SUPPORTED = 0b000000; + /** + * Combined renderer capabilities. + * + *

      This is a bitwise OR of {@link FormatSupport}, {@link AdaptiveSupport} and {@link + * TunnelingSupport}. Use {@link #getFormatSupport(int)}, {@link #getAdaptiveSupport(int)} or + * {@link #getTunnelingSupport(int)} to obtain the individual flags. And use {@link #create(int)} + * or {@link #create(int, int, int)} to create the combined capabilities. + * + *

      Possible values: + * + *

        + *
      • {@link FormatSupport}: The level of support for the format itself. One of {@link + * #FORMAT_HANDLED}, {@link #FORMAT_EXCEEDS_CAPABILITIES}, {@link #FORMAT_UNSUPPORTED_DRM}, + * {@link #FORMAT_UNSUPPORTED_SUBTYPE} and {@link #FORMAT_UNSUPPORTED_TYPE}. + *
      • {@link AdaptiveSupport}: The level of support for adapting from the format to another + * format of the same mime type. One of {@link #ADAPTIVE_SEAMLESS}, {@link + * #ADAPTIVE_NOT_SEAMLESS} and {@link #ADAPTIVE_NOT_SUPPORTED}. Only set if the level of + * support for the format itself is {@link #FORMAT_HANDLED} or {@link + * #FORMAT_EXCEEDS_CAPABILITIES}. + *
      • {@link TunnelingSupport}: The level of support for tunneling. One of {@link + * #TUNNELING_SUPPORTED} and {@link #TUNNELING_NOT_SUPPORTED}. Only set if the level of + * support for the format itself is {@link #FORMAT_HANDLED} or {@link + * #FORMAT_EXCEEDS_CAPABILITIES}. + *
      + */ + @Documented + @Retention(RetentionPolicy.SOURCE) + // Intentionally empty to prevent assignment or comparison with individual flags without masking. + @IntDef({}) + @interface Capabilities {} + + /** + * Returns {@link Capabilities} for the given {@link FormatSupport}. + * + *

      The {@link AdaptiveSupport} is set to {@link #ADAPTIVE_NOT_SUPPORTED} and {{@link + * TunnelingSupport} is set to {@link #TUNNELING_NOT_SUPPORTED}. + * + * @param formatSupport The {@link FormatSupport}. + * @return The combined {@link Capabilities} of the given {@link FormatSupport}, {@link + * #ADAPTIVE_NOT_SUPPORTED} and {@link #TUNNELING_NOT_SUPPORTED}. + */ + @Capabilities + static int create(@FormatSupport int formatSupport) { + return create(formatSupport, ADAPTIVE_NOT_SUPPORTED, TUNNELING_NOT_SUPPORTED); + } + + /** + * Returns {@link Capabilities} combining the given {@link FormatSupport}, {@link AdaptiveSupport} + * and {@link TunnelingSupport}. + * + * @param formatSupport The {@link FormatSupport}. + * @param adaptiveSupport The {@link AdaptiveSupport}. + * @param tunnelingSupport The {@link TunnelingSupport}. + * @return The combined {@link Capabilities}. + */ + // Suppression needed for IntDef casting. + @SuppressLint("WrongConstant") + @Capabilities + static int create( + @FormatSupport int formatSupport, + @AdaptiveSupport int adaptiveSupport, + @TunnelingSupport int tunnelingSupport) { + return formatSupport | adaptiveSupport | tunnelingSupport; + } + + /** + * Returns the {@link FormatSupport} from the combined {@link Capabilities}. + * + * @param supportFlags The combined {@link Capabilities}. + * @return The {@link FormatSupport} only. + */ + // Suppression needed for IntDef casting. + @SuppressLint("WrongConstant") + @FormatSupport + static int getFormatSupport(@Capabilities int supportFlags) { + return supportFlags & FORMAT_SUPPORT_MASK; + } + + /** + * Returns the {@link AdaptiveSupport} from the combined {@link Capabilities}. + * + * @param supportFlags The combined {@link Capabilities}. + * @return The {@link AdaptiveSupport} only. + */ + // Suppression needed for IntDef casting. + @SuppressLint("WrongConstant") + @AdaptiveSupport + static int getAdaptiveSupport(@Capabilities int supportFlags) { + return supportFlags & ADAPTIVE_SUPPORT_MASK; + } + + /** + * Returns the {@link TunnelingSupport} from the combined {@link Capabilities}. + * + * @param supportFlags The combined {@link Capabilities}. + * @return The {@link TunnelingSupport} only. + */ + // Suppression needed for IntDef casting. + @SuppressLint("WrongConstant") + @TunnelingSupport + static int getTunnelingSupport(@Capabilities int supportFlags) { + return supportFlags & TUNNELING_SUPPORT_MASK; + } + + /** + * Returns string representation of a {@link FormatSupport} flag. + * + * @param formatSupport A {@link FormatSupport} flag. + * @return A string representation of the flag. + */ + static String getFormatSupportString(@FormatSupport int formatSupport) { + switch (formatSupport) { + case RendererCapabilities.FORMAT_HANDLED: + return "YES"; + case RendererCapabilities.FORMAT_EXCEEDS_CAPABILITIES: + return "NO_EXCEEDS_CAPABILITIES"; + case RendererCapabilities.FORMAT_UNSUPPORTED_DRM: + return "NO_UNSUPPORTED_DRM"; + case RendererCapabilities.FORMAT_UNSUPPORTED_SUBTYPE: + return "NO_UNSUPPORTED_TYPE"; + case RendererCapabilities.FORMAT_UNSUPPORTED_TYPE: + return "NO"; + default: + throw new IllegalStateException(); + } + } + + /** Returns the name of the {@link Renderer}. */ + String getName(); + /** * Returns the track type that the {@link Renderer} handles. For example, a video renderer will * return {@link C#TRACK_TYPE_VIDEO}, an audio renderer will return {@link C#TRACK_TYPE_AUDIO}, a @@ -115,39 +274,23 @@ public interface RendererCapabilities { int getTrackType(); /** - * Returns the extent to which the {@link Renderer} supports a given format. The returned value is - * the bitwise OR of three properties: - *

        - *
      • The level of support for the format itself. One of {@link #FORMAT_HANDLED}, - * {@link #FORMAT_EXCEEDS_CAPABILITIES}, {@link #FORMAT_UNSUPPORTED_DRM}, - * {@link #FORMAT_UNSUPPORTED_SUBTYPE} and {@link #FORMAT_UNSUPPORTED_TYPE}.
      • - *
      • The level of support for adapting from the format to another format of the same mime type. - * One of {@link #ADAPTIVE_SEAMLESS}, {@link #ADAPTIVE_NOT_SEAMLESS} and - * {@link #ADAPTIVE_NOT_SUPPORTED}. Only set if the level of support for the format itself is - * {@link #FORMAT_HANDLED} or {@link #FORMAT_EXCEEDS_CAPABILITIES}.
      • - *
      • The level of support for tunneling. One of {@link #TUNNELING_SUPPORTED} and - * {@link #TUNNELING_NOT_SUPPORTED}. Only set if the level of support for the format itself is - * {@link #FORMAT_HANDLED} or {@link #FORMAT_EXCEEDS_CAPABILITIES}.
      • - *
      - * The individual properties can be retrieved by performing a bitwise AND with - * {@link #FORMAT_SUPPORT_MASK}, {@link #ADAPTIVE_SUPPORT_MASK} and - * {@link #TUNNELING_SUPPORT_MASK} respectively. + * Returns the extent to which the {@link Renderer} supports a given format. * * @param format The format. - * @return The extent to which the renderer is capable of supporting the given format. + * @return The {@link Capabilities} for this format. * @throws ExoPlaybackException If an error occurs. */ + @Capabilities int supportsFormat(Format format) throws ExoPlaybackException; /** * Returns the extent to which the {@link Renderer} supports adapting between supported formats - * that have different mime types. + * that have different MIME types. * - * @return The extent to which the renderer supports adapting between supported formats that have - * different mime types. One of {@link #ADAPTIVE_SEAMLESS}, {@link #ADAPTIVE_NOT_SEAMLESS} and - * {@link #ADAPTIVE_NOT_SUPPORTED}. + * @return The {@link AdaptiveSupport} for adapting between supported formats that have different + * MIME types. * @throws ExoPlaybackException If an error occurs. */ + @AdaptiveSupport int supportsMixedMimeTypeAdaptation() throws ExoPlaybackException; - } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/RenderersFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/RenderersFactory.java index 6f0d125bcf..74ee923961 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/RenderersFactory.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/RenderersFactory.java @@ -16,10 +16,7 @@ package com.google.android.exoplayer2; import android.os.Handler; -import androidx.annotation.Nullable; import com.google.android.exoplayer2.audio.AudioRendererEventListener; -import com.google.android.exoplayer2.drm.DrmSessionManager; -import com.google.android.exoplayer2.drm.FrameworkMediaCrypto; import com.google.android.exoplayer2.metadata.MetadataOutput; import com.google.android.exoplayer2.text.TextOutput; import com.google.android.exoplayer2.video.VideoRendererEventListener; @@ -37,7 +34,6 @@ public interface RenderersFactory { * @param audioRendererEventListener An event listener for audio renderers. * @param textRendererOutput An output for text renderers. * @param metadataRendererOutput An output for metadata renderers. - * @param drmSessionManager A drm session manager used by renderers. * @return The {@link Renderer instances}. */ Renderer[] createRenderers( @@ -45,6 +41,5 @@ public interface RenderersFactory { VideoRendererEventListener videoRendererEventListener, AudioRendererEventListener audioRendererEventListener, TextOutput textRendererOutput, - MetadataOutput metadataRendererOutput, - @Nullable DrmSessionManager drmSessionManager); + MetadataOutput metadataRendererOutput); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/SeekParameters.java b/library/core/src/main/java/com/google/android/exoplayer2/SeekParameters.java index 7a0ad67a28..7b31896c2d 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/SeekParameters.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/SeekParameters.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer2; import androidx.annotation.Nullable; import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.Util; /** * Parameters that apply to seeking. @@ -71,6 +72,41 @@ public final class SeekParameters { this.toleranceAfterUs = toleranceAfterUs; } + /** + * Resolves a seek based on the parameters, given the requested seek position and two candidate + * sync points. + * + * @param positionUs The requested seek position, in microseocnds. + * @param firstSyncUs The first candidate seek point, in micrseconds. + * @param secondSyncUs The second candidate seek point, in microseconds. May equal {@code + * firstSyncUs} if there's only one candidate. + * @return The resolved seek position, in microseconds. + */ + public long resolveSeekPositionUs(long positionUs, long firstSyncUs, long secondSyncUs) { + if (toleranceBeforeUs == 0 && toleranceAfterUs == 0) { + return positionUs; + } + long minPositionUs = + Util.subtractWithOverflowDefault(positionUs, toleranceBeforeUs, Long.MIN_VALUE); + long maxPositionUs = Util.addWithOverflowDefault(positionUs, toleranceAfterUs, Long.MAX_VALUE); + boolean firstSyncPositionValid = minPositionUs <= firstSyncUs && firstSyncUs <= maxPositionUs; + boolean secondSyncPositionValid = + minPositionUs <= secondSyncUs && secondSyncUs <= maxPositionUs; + if (firstSyncPositionValid && secondSyncPositionValid) { + if (Math.abs(firstSyncUs - positionUs) <= Math.abs(secondSyncUs - positionUs)) { + return firstSyncUs; + } else { + return secondSyncUs; + } + } else if (firstSyncPositionValid) { + return firstSyncUs; + } else if (secondSyncPositionValid) { + return secondSyncUs; + } else { + return minPositionUs; + } + } + @Override public boolean equals(@Nullable Object obj) { if (this == obj) { 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 98ef62fde9..93ebacf5ab 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 @@ -15,7 +15,6 @@ */ package com.google.android.exoplayer2; -import android.annotation.TargetApi; import android.content.Context; import android.graphics.Rect; import android.graphics.SurfaceTexture; @@ -28,6 +27,7 @@ import android.view.SurfaceHolder; import android.view.SurfaceView; import android.view.TextureView; import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; import androidx.annotation.VisibleForTesting; import com.google.android.exoplayer2.analytics.AnalyticsCollector; import com.google.android.exoplayer2.analytics.AnalyticsListener; @@ -36,12 +36,14 @@ 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; -import com.google.android.exoplayer2.drm.FrameworkMediaCrypto; +import com.google.android.exoplayer2.device.DeviceInfo; +import com.google.android.exoplayer2.device.DeviceListener; import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.MetadataOutput; +import com.google.android.exoplayer2.source.DefaultMediaSourceFactory; import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.source.MediaSourceFactory; +import com.google.android.exoplayer2.source.ShuffleOrder; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.text.Cue; import com.google.android.exoplayer2.text.TextOutput; @@ -73,7 +75,8 @@ public class SimpleExoPlayer extends BasePlayer Player.AudioComponent, Player.VideoComponent, Player.TextComponent, - Player.MetadataComponent { + Player.MetadataComponent, + Player.DeviceComponent { /** @deprecated Use {@link com.google.android.exoplayer2.video.VideoListener}. */ @Deprecated @@ -91,11 +94,13 @@ public class SimpleExoPlayer extends BasePlayer private Clock clock; private TrackSelector trackSelector; + private MediaSourceFactory mediaSourceFactory; private LoadControl loadControl; private BandwidthMeter bandwidthMeter; private AnalyticsCollector analyticsCollector; private Looper looper; private boolean useLazyPreparation; + private boolean throwWhenStuckBuffering; private boolean buildCalled; /** @@ -110,6 +115,7 @@ public class SimpleExoPlayer extends BasePlayer *
        *
      • {@link RenderersFactory}: {@link DefaultRenderersFactory} *
      • {@link TrackSelector}: {@link DefaultTrackSelector} + *
      • {@link MediaSourceFactory}: {@link DefaultMediaSourceFactory} *
      • {@link LoadControl}: {@link DefaultLoadControl} *
      • {@link BandwidthMeter}: {@link DefaultBandwidthMeter#getSingletonInstance(Context)} *
      • {@link Looper}: The {@link Looper} associated with the current thread, or the {@link @@ -140,6 +146,7 @@ public class SimpleExoPlayer extends BasePlayer context, renderersFactory, new DefaultTrackSelector(context), + DefaultMediaSourceFactory.newInstance(context), new DefaultLoadControl(), DefaultBandwidthMeter.getSingletonInstance(context), Util.getLooper(), @@ -159,17 +166,21 @@ public class SimpleExoPlayer extends BasePlayer * @param renderersFactory A factory for creating {@link Renderer Renderers} to be used by the * player. * @param trackSelector A {@link TrackSelector}. + * @param mediaSourceFactory A {@link MediaSourceFactory}. * @param loadControl A {@link LoadControl}. * @param bandwidthMeter A {@link BandwidthMeter}. * @param looper A {@link Looper} that must be used for all calls to the player. * @param analyticsCollector An {@link AnalyticsCollector}. - * @param useLazyPreparation Whether media sources should be initialized lazily. + * @param useLazyPreparation Whether playlist items should be prepared lazily. If false, all + * initial preparation steps (e.g., manifest loads) happen immediately. If true, these + * initial preparations are triggered only when the player starts buffering the media. * @param clock A {@link Clock}. Should always be {@link Clock#DEFAULT}. */ public Builder( Context context, RenderersFactory renderersFactory, TrackSelector trackSelector, + MediaSourceFactory mediaSourceFactory, LoadControl loadControl, BandwidthMeter bandwidthMeter, Looper looper, @@ -179,6 +190,7 @@ public class SimpleExoPlayer extends BasePlayer this.context = context; this.renderersFactory = renderersFactory; this.trackSelector = trackSelector; + this.mediaSourceFactory = mediaSourceFactory; this.loadControl = loadControl; this.bandwidthMeter = bandwidthMeter; this.looper = looper; @@ -200,6 +212,19 @@ public class SimpleExoPlayer extends BasePlayer return this; } + /** + * Sets the {@link MediaSourceFactory} that will be used by the player. + * + * @param mediaSourceFactory A {@link MediaSourceFactory}. + * @return This builder. + * @throws IllegalStateException If {@link #build()} has already been called. + */ + public Builder setMediaSourceFactory(MediaSourceFactory mediaSourceFactory) { + Assertions.checkState(!buildCalled); + this.mediaSourceFactory = mediaSourceFactory; + return this; + } + /** * Sets the {@link LoadControl} that will be used by the player. * @@ -270,6 +295,19 @@ public class SimpleExoPlayer extends BasePlayer return this; } + /** + * Sets whether the player should throw when it detects it's stuck buffering. + * + *

        This method is experimental, and will be renamed or removed in a future release. + * + * @param throwWhenStuckBuffering Whether to throw when the player detects it's stuck buffering. + * @return This builder. + */ + public Builder experimental_setThrowWhenStuckBuffering(boolean throwWhenStuckBuffering) { + this.throwWhenStuckBuffering = throwWhenStuckBuffering; + return this; + } + /** * Sets the {@link Clock} that will be used by the player. Should only be set for testing * purposes. @@ -293,30 +331,25 @@ public class SimpleExoPlayer extends BasePlayer public SimpleExoPlayer build() { Assertions.checkState(!buildCalled); buildCalled = true; - return new SimpleExoPlayer( - context, - renderersFactory, - trackSelector, - loadControl, - bandwidthMeter, - analyticsCollector, - clock, - looper); + return new SimpleExoPlayer(/* builder= */ this); } } private static final String TAG = "SimpleExoPlayer"; + private static final String WRONG_THREAD_ERROR_MESSAGE = + "Player is accessed on the wrong thread. See " + + "https://exoplayer.dev/issues/player-accessed-on-wrong-thread"; protected final Renderer[] renderers; 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 deviceListeners; private final CopyOnWriteArraySet videoDebugListeners; private final CopyOnWriteArraySet audioDebugListeners; private final BandwidthMeter bandwidthMeter; @@ -324,7 +357,9 @@ public class SimpleExoPlayer extends BasePlayer private final AudioBecomingNoisyManager audioBecomingNoisyManager; private final AudioFocusManager audioFocusManager; + private final StreamVolumeManager streamVolumeManager; private final WakeLockManager wakeLockManager; + private final WifiLockManager wifiLockManager; @Nullable private Format videoFormat; @Nullable private Format audioFormat; @@ -332,7 +367,7 @@ public class SimpleExoPlayer extends BasePlayer @Nullable private VideoDecoderOutputBufferRenderer videoDecoderOutputBufferRenderer; @Nullable private Surface surface; private boolean ownsSurface; - private @C.VideoScalingMode int videoScalingMode; + @Renderer.VideoScalingMode private int videoScalingMode; @Nullable private SurfaceHolder surfaceHolder; @Nullable private TextureView textureView; private int surfaceWidth; @@ -342,124 +377,101 @@ public class SimpleExoPlayer extends BasePlayer private int audioSessionId; private AudioAttributes audioAttributes; private float audioVolume; - @Nullable private MediaSource mediaSource; + private boolean skipSilenceEnabled; private List currentCues; @Nullable private VideoFrameMetadataListener videoFrameMetadataListener; @Nullable private CameraMotionListener cameraMotionListener; + private boolean throwsWhenUsingWrongThread; private boolean hasNotifiedFullWrongThreadWarning; @Nullable private PriorityTaskManager priorityTaskManager; private boolean isPriorityTaskManagerRegistered; private boolean playerReleased; + private DeviceInfo deviceInfo; - /** - * @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 analyticsCollector 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. - */ - @SuppressWarnings("deprecation") - protected SimpleExoPlayer( - Context context, - RenderersFactory renderersFactory, - TrackSelector trackSelector, - LoadControl loadControl, - BandwidthMeter bandwidthMeter, - AnalyticsCollector analyticsCollector, - Clock clock, - Looper looper) { - this( - context, - renderersFactory, - trackSelector, - loadControl, - DrmSessionManager.getDummyDrmSessionManager(), - bandwidthMeter, - analyticsCollector, - clock, - 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 analyticsCollector 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. - * @deprecated Use {@link #SimpleExoPlayer(Context, RenderersFactory, TrackSelector, LoadControl, - * BandwidthMeter, AnalyticsCollector, Clock, Looper)} instead, and pass the {@link - * DrmSessionManager} to the {@link MediaSource} factories. - */ + /** @deprecated Use the {@link Builder} and pass it to {@link #SimpleExoPlayer(Builder)}. */ @Deprecated protected SimpleExoPlayer( Context context, RenderersFactory renderersFactory, TrackSelector trackSelector, + MediaSourceFactory mediaSourceFactory, LoadControl loadControl, - @Nullable DrmSessionManager drmSessionManager, BandwidthMeter bandwidthMeter, AnalyticsCollector analyticsCollector, + boolean useLazyPreparation, Clock clock, - Looper looper) { - this.bandwidthMeter = bandwidthMeter; - this.analyticsCollector = analyticsCollector; + Looper applicationLooper) { + this( + new Builder(context, renderersFactory) + .setTrackSelector(trackSelector) + .setMediaSourceFactory(mediaSourceFactory) + .setLoadControl(loadControl) + .setBandwidthMeter(bandwidthMeter) + .setAnalyticsCollector(analyticsCollector) + .setUseLazyPreparation(useLazyPreparation) + .setClock(clock) + .setLooper(applicationLooper)); + } + + /** @param builder The {@link Builder} to obtain all construction parameters. */ + protected SimpleExoPlayer(Builder builder) { + bandwidthMeter = builder.bandwidthMeter; + analyticsCollector = builder.analyticsCollector; componentListener = new ComponentListener(); videoListeners = new CopyOnWriteArraySet<>(); audioListeners = new CopyOnWriteArraySet<>(); textOutputs = new CopyOnWriteArraySet<>(); metadataOutputs = new CopyOnWriteArraySet<>(); + deviceListeners = new CopyOnWriteArraySet<>(); videoDebugListeners = new CopyOnWriteArraySet<>(); audioDebugListeners = new CopyOnWriteArraySet<>(); - eventHandler = new Handler(looper); + Handler eventHandler = new Handler(builder.looper); renderers = - renderersFactory.createRenderers( + builder.renderersFactory.createRenderers( eventHandler, componentListener, componentListener, componentListener, - componentListener, - drmSessionManager); + componentListener); // Set initial values. audioVolume = 1; audioSessionId = C.AUDIO_SESSION_ID_UNSET; audioAttributes = AudioAttributes.DEFAULT; - videoScalingMode = C.VIDEO_SCALING_MODE_DEFAULT; + videoScalingMode = Renderer.VIDEO_SCALING_MODE_DEFAULT; currentCues = Collections.emptyList(); // Build the player and associated objects. player = - new ExoPlayerImpl(renderers, trackSelector, loadControl, bandwidthMeter, clock, looper); + new ExoPlayerImpl( + renderers, + builder.trackSelector, + builder.mediaSourceFactory, + builder.loadControl, + bandwidthMeter, + analyticsCollector, + builder.useLazyPreparation, + builder.clock, + builder.looper); analyticsCollector.setPlayer(player); - addListener(analyticsCollector); - addListener(componentListener); + player.addListener(analyticsCollector); + player.addListener(componentListener); 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); - } audioBecomingNoisyManager = - new AudioBecomingNoisyManager(context, eventHandler, componentListener); - audioFocusManager = new AudioFocusManager(context, eventHandler, componentListener); - wakeLockManager = new WakeLockManager(context); + new AudioBecomingNoisyManager(builder.context, eventHandler, componentListener); + audioFocusManager = new AudioFocusManager(builder.context, eventHandler, componentListener); + streamVolumeManager = new StreamVolumeManager(builder.context, eventHandler, componentListener); + wakeLockManager = new WakeLockManager(builder.context); + wifiLockManager = new WifiLockManager(builder.context); + deviceInfo = createDeviceInfo(streamVolumeManager); + if (builder.throwWhenStuckBuffering) { + player.experimental_throwWhenStuckBuffering(); + } } @Override @@ -486,31 +498,30 @@ public class SimpleExoPlayer extends BasePlayer return this; } + @Override + @Nullable + public DeviceComponent getDeviceComponent() { + return this; + } + /** * Sets the video scaling mode. * *

        Note that the scaling mode only applies if a {@link MediaCodec}-based video {@link Renderer} * is enabled and if the output surface is owned by a {@link android.view.SurfaceView}. * - * @param videoScalingMode The video scaling mode. + * @param videoScalingMode The {@link Renderer.VideoScalingMode}. */ @Override - public void setVideoScalingMode(@C.VideoScalingMode int videoScalingMode) { + public void setVideoScalingMode(@Renderer.VideoScalingMode int videoScalingMode) { verifyApplicationThread(); this.videoScalingMode = videoScalingMode; - for (Renderer renderer : renderers) { - if (renderer.getTrackType() == C.TRACK_TYPE_VIDEO) { - player - .createMessage(renderer) - .setType(C.MSG_SET_SCALING_MODE) - .setPayload(videoScalingMode) - .send(); - } - } + sendRendererMessage(C.TRACK_TYPE_VIDEO, Renderer.MSG_SET_SCALING_MODE, videoScalingMode); } @Override - public @C.VideoScalingMode int getVideoScalingMode() { + @Renderer.VideoScalingMode + public int getVideoScalingMode() { return videoScalingMode; } @@ -670,25 +681,19 @@ public class SimpleExoPlayer extends BasePlayer } 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(); - } - } + sendRendererMessage(C.TRACK_TYPE_AUDIO, Renderer.MSG_SET_AUDIO_ATTRIBUTES, audioAttributes); + streamVolumeManager.setStreamType(Util.getStreamTypeForAudioUsage(audioAttributes.usage)); for (AudioListener audioListener : audioListeners) { audioListener.onAudioAttributesChanged(audioAttributes); } } + audioFocusManager.setAudioAttributes(handleAudioFocus ? audioAttributes : null); + boolean playWhenReady = getPlayWhenReady(); @AudioFocusManager.PlayerCommand - int playerCommand = - audioFocusManager.setAudioAttributes( - handleAudioFocus ? audioAttributes : null, getPlayWhenReady(), getPlaybackState()); - updatePlayWhenReady(getPlayWhenReady(), playerCommand); + int playerCommand = audioFocusManager.updateAudioFocus(playWhenReady, getPlaybackState()); + updatePlayWhenReady( + playWhenReady, playerCommand, getPlayWhenReadyChangeReason(playWhenReady, playerCommand)); } @Override @@ -696,6 +701,19 @@ public class SimpleExoPlayer extends BasePlayer return audioAttributes; } + @Override + public void setAudioSessionId(int audioSessionId) { + verifyApplicationThread(); + if (this.audioSessionId == audioSessionId) { + return; + } + this.audioSessionId = audioSessionId; + sendRendererMessage(C.TRACK_TYPE_AUDIO, Renderer.MSG_SET_AUDIO_SESSION_ID, audioSessionId); + if (audioSessionId != C.AUDIO_SESSION_ID_UNSET) { + notifyAudioSessionIdSet(); + } + } + @Override public int getAudioSessionId() { return audioSessionId; @@ -704,15 +722,7 @@ public class SimpleExoPlayer extends BasePlayer @Override public void setAuxEffectInfo(AuxEffectInfo auxEffectInfo) { verifyApplicationThread(); - for (Renderer renderer : renderers) { - if (renderer.getTrackType() == C.TRACK_TYPE_AUDIO) { - player - .createMessage(renderer) - .setType(C.MSG_SET_AUX_EFFECT_INFO) - .setPayload(auxEffectInfo) - .send(); - } - } + sendRendererMessage(C.TRACK_TYPE_AUDIO, Renderer.MSG_SET_AUX_EFFECT_INFO, auxEffectInfo); } @Override @@ -739,6 +749,23 @@ public class SimpleExoPlayer extends BasePlayer return audioVolume; } + @Override + public boolean getSkipSilenceEnabled() { + return skipSilenceEnabled; + } + + @Override + public void setSkipSilenceEnabled(boolean skipSilenceEnabled) { + verifyApplicationThread(); + if (this.skipSilenceEnabled == skipSilenceEnabled) { + return; + } + this.skipSilenceEnabled = skipSilenceEnabled; + sendRendererMessage( + C.TRACK_TYPE_AUDIO, Renderer.MSG_SET_SKIP_SILENCE_ENABLED, skipSilenceEnabled); + notifySkipSilenceEnabledChanged(); + } + /** * Sets the stream type for audio playback, used by the underlying audio track. * @@ -839,23 +866,19 @@ public class SimpleExoPlayer extends BasePlayer this.priorityTaskManager = priorityTaskManager; } - /** - * Sets the {@link PlaybackParams} governing audio playback. - * - * @deprecated Use {@link #setPlaybackParameters(PlaybackParameters)}. - * @param params The {@link PlaybackParams}, or null to clear any previously set parameters. - */ + /** @deprecated Use {@link #setPlaybackSpeed(float)} instead. */ + @SuppressWarnings("deprecation") @Deprecated - @TargetApi(23) + @RequiresApi(23) public void setPlaybackParams(@Nullable PlaybackParams params) { - PlaybackParameters playbackParameters; + float playbackSpeed; if (params != null) { params.allowDefaults(); - playbackParameters = new PlaybackParameters(params.getSpeed(), params.getPitch()); + playbackSpeed = params.getSpeed(); } else { - playbackParameters = null; + playbackSpeed = 1.0f; } - setPlaybackParameters(playbackParameters); + setPlaybackSpeed(playbackSpeed); } /** Returns the video format currently being played, or null if no video is being played. */ @@ -896,15 +919,8 @@ public class SimpleExoPlayer extends BasePlayer public void setVideoFrameMetadataListener(VideoFrameMetadataListener listener) { verifyApplicationThread(); videoFrameMetadataListener = listener; - for (Renderer renderer : renderers) { - if (renderer.getTrackType() == C.TRACK_TYPE_VIDEO) { - player - .createMessage(renderer) - .setType(C.MSG_SET_VIDEO_FRAME_METADATA_LISTENER) - .setPayload(listener) - .send(); - } - } + sendRendererMessage( + C.TRACK_TYPE_VIDEO, Renderer.MSG_SET_VIDEO_FRAME_METADATA_LISTENER, listener); } @Override @@ -913,30 +929,16 @@ public class SimpleExoPlayer extends BasePlayer if (videoFrameMetadataListener != listener) { return; } - for (Renderer renderer : renderers) { - if (renderer.getTrackType() == C.TRACK_TYPE_VIDEO) { - player - .createMessage(renderer) - .setType(C.MSG_SET_VIDEO_FRAME_METADATA_LISTENER) - .setPayload(null) - .send(); - } - } + sendRendererMessage( + C.TRACK_TYPE_VIDEO, Renderer.MSG_SET_VIDEO_FRAME_METADATA_LISTENER, /* payload= */ null); } @Override public void setCameraMotionListener(CameraMotionListener listener) { verifyApplicationThread(); cameraMotionListener = listener; - for (Renderer renderer : renderers) { - if (renderer.getTrackType() == C.TRACK_TYPE_CAMERA_MOTION) { - player - .createMessage(renderer) - .setType(C.MSG_SET_CAMERA_MOTION_LISTENER) - .setPayload(listener) - .send(); - } - } + sendRendererMessage( + C.TRACK_TYPE_CAMERA_MOTION, Renderer.MSG_SET_CAMERA_MOTION_LISTENER, listener); } @Override @@ -945,15 +947,8 @@ public class SimpleExoPlayer extends BasePlayer if (cameraMotionListener != listener) { return; } - for (Renderer renderer : renderers) { - if (renderer.getTrackType() == C.TRACK_TYPE_CAMERA_MOTION) { - player - .createMessage(renderer) - .setType(C.MSG_SET_CAMERA_MOTION_LISTENER) - .setPayload(null) - .send(); - } - } + sendRendererMessage( + C.TRACK_TYPE_CAMERA_MOTION, Renderer.MSG_SET_CAMERA_MOTION_LISTENER, /* payload= */ null); } /** @@ -1157,64 +1152,242 @@ public class SimpleExoPlayer extends BasePlayer return player.getPlaybackSuppressionReason(); } + /** @deprecated Use {@link #getPlayerError()} instead. */ + @Deprecated @Override @Nullable public ExoPlaybackException getPlaybackError() { - verifyApplicationThread(); - return player.getPlaybackError(); + return getPlayerError(); } @Override - @SuppressWarnings("deprecation") + @Nullable + public ExoPlaybackException getPlayerError() { + verifyApplicationThread(); + return player.getPlayerError(); + } + + /** @deprecated Use {@link #prepare()} instead. */ + @Deprecated + @Override public void retry() { verifyApplicationThread(); - if (mediaSource != null - && (getPlaybackError() != null || getPlaybackState() == Player.STATE_IDLE)) { - prepare(mediaSource, /* resetPosition= */ false, /* resetState= */ false); - } - } - - @Override - @Deprecated - @SuppressWarnings("deprecation") - public void prepare(MediaSource mediaSource) { - prepare(mediaSource, /* resetPosition= */ true, /* resetState= */ true); - } - - @Override - @Deprecated - public void prepare(MediaSource mediaSource, boolean resetPosition, boolean resetState) { - verifyApplicationThread(); - setMediaItem(mediaSource); - prepareInternal(resetPosition, resetState); + prepare(); } @Override public void prepare() { verifyApplicationThread(); - prepareInternal(/* resetPosition= */ false, /* resetState= */ true); + boolean playWhenReady = getPlayWhenReady(); + @AudioFocusManager.PlayerCommand + int playerCommand = audioFocusManager.updateAudioFocus(playWhenReady, Player.STATE_BUFFERING); + updatePlayWhenReady( + playWhenReady, playerCommand, getPlayWhenReadyChangeReason(playWhenReady, playerCommand)); + player.prepare(); + } + + /** + * @deprecated Use {@link #setMediaSource(MediaSource)} and {@link ExoPlayer#prepare()} instead. + */ + @Deprecated + @Override + @SuppressWarnings("deprecation") + public void prepare(MediaSource mediaSource) { + prepare(mediaSource, /* resetPosition= */ true, /* resetState= */ true); + } + + /** + * @deprecated Use {@link #setMediaSource(MediaSource, boolean)} and {@link ExoPlayer#prepare()} + * instead. + */ + @Deprecated + @Override + public void prepare(MediaSource mediaSource, boolean resetPosition, boolean resetState) { + verifyApplicationThread(); + setMediaSources( + Collections.singletonList(mediaSource), + /* startWindowIndex= */ resetPosition ? 0 : C.INDEX_UNSET, + /* startPositionMs= */ C.TIME_UNSET); + prepare(); } @Override - public void setMediaItem(MediaSource mediaItem, long startPositionMs) { + public void setMediaItems(List mediaItems) { verifyApplicationThread(); - setMediaItemInternal(mediaItem); + analyticsCollector.resetForNewPlaylist(); + player.setMediaItems(mediaItems); + } + + @Override + public void setMediaItems(List mediaItems, boolean resetPosition) { + verifyApplicationThread(); + analyticsCollector.resetForNewPlaylist(); + player.setMediaItems(mediaItems, resetPosition); + } + + @Override + public void setMediaItems( + List mediaItems, int startWindowIndex, long startPositionMs) { + verifyApplicationThread(); + analyticsCollector.resetForNewPlaylist(); + player.setMediaItems(mediaItems, startWindowIndex, startPositionMs); + } + + @Override + public void setMediaItem(MediaItem mediaItem) { + verifyApplicationThread(); + analyticsCollector.resetForNewPlaylist(); + player.setMediaItem(mediaItem); + } + + @Override + public void setMediaItem(MediaItem mediaItem, boolean resetPosition) { + verifyApplicationThread(); + analyticsCollector.resetForNewPlaylist(); + player.setMediaItem(mediaItem, resetPosition); + } + + @Override + public void setMediaItem(MediaItem mediaItem, long startPositionMs) { + verifyApplicationThread(); + analyticsCollector.resetForNewPlaylist(); player.setMediaItem(mediaItem, startPositionMs); } @Override - public void setMediaItem(MediaSource mediaItem) { + public void setMediaSources(List mediaSources) { verifyApplicationThread(); - setMediaItemInternal(mediaItem); - player.setMediaItem(mediaItem); + analyticsCollector.resetForNewPlaylist(); + player.setMediaSources(mediaSources); + } + + @Override + public void setMediaSources(List mediaSources, boolean resetPosition) { + verifyApplicationThread(); + analyticsCollector.resetForNewPlaylist(); + player.setMediaSources(mediaSources, resetPosition); + } + + @Override + public void setMediaSources( + List mediaSources, int startWindowIndex, long startPositionMs) { + verifyApplicationThread(); + analyticsCollector.resetForNewPlaylist(); + player.setMediaSources(mediaSources, startWindowIndex, startPositionMs); + } + + @Override + public void setMediaSource(MediaSource mediaSource) { + verifyApplicationThread(); + analyticsCollector.resetForNewPlaylist(); + player.setMediaSource(mediaSource); + } + + @Override + public void setMediaSource(MediaSource mediaSource, boolean resetPosition) { + verifyApplicationThread(); + analyticsCollector.resetForNewPlaylist(); + player.setMediaSource(mediaSource, resetPosition); + } + + @Override + public void setMediaSource(MediaSource mediaSource, long startPositionMs) { + verifyApplicationThread(); + analyticsCollector.resetForNewPlaylist(); + player.setMediaSource(mediaSource, startPositionMs); + } + + @Override + public void addMediaItems(List mediaItems) { + verifyApplicationThread(); + player.addMediaItems(mediaItems); + } + + @Override + public void addMediaItems(int index, List mediaItems) { + verifyApplicationThread(); + player.addMediaItems(index, mediaItems); + } + + @Override + public void addMediaItem(MediaItem mediaItem) { + verifyApplicationThread(); + player.addMediaItem(mediaItem); + } + + @Override + public void addMediaItem(int index, MediaItem mediaItem) { + verifyApplicationThread(); + player.addMediaItem(index, mediaItem); + } + + @Override + public void addMediaSource(MediaSource mediaSource) { + verifyApplicationThread(); + player.addMediaSource(mediaSource); + } + + @Override + public void addMediaSource(int index, MediaSource mediaSource) { + verifyApplicationThread(); + player.addMediaSource(index, mediaSource); + } + + @Override + public void addMediaSources(List mediaSources) { + verifyApplicationThread(); + player.addMediaSources(mediaSources); + } + + @Override + public void addMediaSources(int index, List mediaSources) { + verifyApplicationThread(); + player.addMediaSources(index, mediaSources); + } + + @Override + public void moveMediaItem(int currentIndex, int newIndex) { + verifyApplicationThread(); + player.moveMediaItem(currentIndex, newIndex); + } + + @Override + public void moveMediaItems(int fromIndex, int toIndex, int newIndex) { + verifyApplicationThread(); + player.moveMediaItems(fromIndex, toIndex, newIndex); + } + + @Override + public void removeMediaItem(int index) { + verifyApplicationThread(); + player.removeMediaItem(index); + } + + @Override + public void removeMediaItems(int fromIndex, int toIndex) { + verifyApplicationThread(); + player.removeMediaItems(fromIndex, toIndex); + } + + @Override + public void clearMediaItems() { + verifyApplicationThread(); + player.clearMediaItems(); + } + + @Override + public void setShuffleOrder(ShuffleOrder shuffleOrder) { + verifyApplicationThread(); + player.setShuffleOrder(shuffleOrder); } @Override public void setPlayWhenReady(boolean playWhenReady) { verifyApplicationThread(); @AudioFocusManager.PlayerCommand - int playerCommand = audioFocusManager.handleSetPlayWhenReady(playWhenReady, getPlaybackState()); - updatePlayWhenReady(playWhenReady, playerCommand); + int playerCommand = audioFocusManager.updateAudioFocus(playWhenReady, getPlaybackState()); + updatePlayWhenReady( + playWhenReady, playerCommand, getPlayWhenReadyChangeReason(playWhenReady, playerCommand)); } @Override @@ -1223,6 +1396,18 @@ public class SimpleExoPlayer extends BasePlayer return player.getPlayWhenReady(); } + @Override + public void setPauseAtEndOfMediaItems(boolean pauseAtEndOfMediaItems) { + verifyApplicationThread(); + player.setPauseAtEndOfMediaItems(pauseAtEndOfMediaItems); + } + + @Override + public boolean getPauseAtEndOfMediaItems() { + verifyApplicationThread(); + return player.getPauseAtEndOfMediaItems(); + } + @Override public @RepeatMode int getRepeatMode() { verifyApplicationThread(); @@ -1260,18 +1445,39 @@ public class SimpleExoPlayer extends BasePlayer player.seekTo(windowIndex, positionMs); } + /** + * @deprecated Use {@link #setPlaybackSpeed(float)} and {@link #setSkipSilenceEnabled(boolean)} + * instead. + */ + @SuppressWarnings("deprecation") + @Deprecated @Override public void setPlaybackParameters(@Nullable PlaybackParameters playbackParameters) { verifyApplicationThread(); player.setPlaybackParameters(playbackParameters); } + /** @deprecated Use {@link #getPlaybackSpeed()} and {@link #getSkipSilenceEnabled()} instead. */ + @SuppressWarnings("deprecation") + @Deprecated @Override public PlaybackParameters getPlaybackParameters() { verifyApplicationThread(); return player.getPlaybackParameters(); } + @Override + public void setPlaybackSpeed(float playbackSpeed) { + verifyApplicationThread(); + player.setPlaybackSpeed(playbackSpeed); + } + + @Override + public float getPlaybackSpeed() { + verifyApplicationThread(); + return player.getPlaybackSpeed(); + } + @Override public void setSeekParameters(@Nullable SeekParameters seekParameters) { verifyApplicationThread(); @@ -1286,21 +1492,15 @@ public class SimpleExoPlayer extends BasePlayer @Override public void setForegroundMode(boolean foregroundMode) { + verifyApplicationThread(); player.setForegroundMode(foregroundMode); } @Override public void stop(boolean reset) { verifyApplicationThread(); + audioFocusManager.updateAudioFocus(getPlayWhenReady(), Player.STATE_IDLE); player.stop(reset); - if (mediaSource != null) { - mediaSource.removeEventListener(analyticsCollector); - analyticsCollector.resetForNewMediaSource(); - if (reset) { - mediaSource = null; - } - } - audioFocusManager.handleStop(); currentCues = Collections.emptyList(); } @@ -1308,8 +1508,10 @@ public class SimpleExoPlayer extends BasePlayer public void release() { verifyApplicationThread(); audioBecomingNoisyManager.setEnabled(false); - audioFocusManager.handleStop(); + streamVolumeManager.release(); wakeLockManager.setStayAwake(false); + wifiLockManager.setStayAwake(false); + audioFocusManager.release(); player.release(); removeSurfaceCallbacks(); if (surface != null) { @@ -1318,10 +1520,6 @@ public class SimpleExoPlayer extends BasePlayer } surface = null; } - if (mediaSource != null) { - mediaSource.removeEventListener(analyticsCollector); - mediaSource = null; - } if (isPriorityTaskManagerRegistered) { Assertions.checkNotNull(priorityTaskManager).remove(C.PRIORITY_PLAYBACK); isPriorityTaskManagerRegistered = false; @@ -1448,30 +1646,113 @@ public class SimpleExoPlayer extends BasePlayer * * @param handleWakeLock Whether the player should use a {@link android.os.PowerManager.WakeLock} * to ensure the device stays awake for playback, even when the screen is off. + * @deprecated Use {@link #setWakeMode(int)} instead. */ + @Deprecated public void setHandleWakeLock(boolean handleWakeLock) { - wakeLockManager.setEnabled(handleWakeLock); + setWakeMode(handleWakeLock ? C.WAKE_MODE_LOCAL : C.WAKE_MODE_NONE); + } + + /** + * Sets how the player should keep the device awake for playback when the screen is off. + * + *

        Enabling this feature requires the {@link android.Manifest.permission#WAKE_LOCK} permission. + * It should be used together with a foreground {@link android.app.Service} for use cases where + * playback occurs and the screen is off (e.g. background audio playback). It is not useful when + * the screen will be kept on during playback (e.g. foreground video playback). + * + *

        When enabled, the locks ({@link android.os.PowerManager.WakeLock} / {@link + * android.net.wifi.WifiManager.WifiLock}) will be held whenever the player is in the {@link + * #STATE_READY} or {@link #STATE_BUFFERING} states with {@code playWhenReady = true}. The locks + * held depends on the specified {@link C.WakeMode}. + * + * @param wakeMode The {@link C.WakeMode} option to keep the device awake during playback. + */ + public void setWakeMode(@C.WakeMode int wakeMode) { + switch (wakeMode) { + case C.WAKE_MODE_NONE: + wakeLockManager.setEnabled(false); + wifiLockManager.setEnabled(false); + break; + case C.WAKE_MODE_LOCAL: + wakeLockManager.setEnabled(true); + wifiLockManager.setEnabled(false); + break; + case C.WAKE_MODE_NETWORK: + wakeLockManager.setEnabled(true); + wifiLockManager.setEnabled(true); + break; + default: + break; + } + } + + @Override + public void addDeviceListener(DeviceListener listener) { + deviceListeners.add(listener); + } + + @Override + public void removeDeviceListener(DeviceListener listener) { + deviceListeners.remove(listener); + } + + @Override + public DeviceInfo getDeviceInfo() { + verifyApplicationThread(); + return deviceInfo; + } + + @Override + public int getDeviceVolume() { + verifyApplicationThread(); + return streamVolumeManager.getVolume(); + } + + @Override + public boolean isDeviceMuted() { + verifyApplicationThread(); + return streamVolumeManager.isMuted(); + } + + @Override + public void setDeviceVolume(int volume) { + verifyApplicationThread(); + streamVolumeManager.setVolume(volume); + } + + @Override + public void increaseDeviceVolume() { + verifyApplicationThread(); + streamVolumeManager.increaseVolume(); + } + + @Override + public void decreaseDeviceVolume() { + verifyApplicationThread(); + streamVolumeManager.decreaseVolume(); + } + + @Override + public void setDeviceMuted(boolean muted) { + verifyApplicationThread(); + streamVolumeManager.setMuted(muted); + } + + /** + * Sets whether the player should throw an {@link IllegalStateException} when methods are called + * from a thread other than the one associated with {@link #getApplicationLooper()}. + * + *

        The default is {@code false}, but will change to {@code true} in the future. + * + * @param throwsWhenUsingWrongThread Whether to throw when methods are called from a wrong thread. + */ + public void setThrowsWhenUsingWrongThread(boolean throwsWhenUsingWrongThread) { + this.throwsWhenUsingWrongThread = throwsWhenUsingWrongThread; } // Internal methods. - private void prepareInternal(boolean resetPosition, boolean resetState) { - Assertions.checkNotNull(mediaSource); - @AudioFocusManager.PlayerCommand - int playerCommand = audioFocusManager.handlePrepare(getPlayWhenReady()); - updatePlayWhenReady(getPlayWhenReady(), playerCommand); - player.prepareInternal(resetPosition, resetState); - } - - private void setMediaItemInternal(MediaSource mediaItem) { - if (mediaSource != null) { - mediaSource.removeEventListener(analyticsCollector); - analyticsCollector.resetForNewMediaSource(); - } - mediaSource = mediaItem; - mediaSource.addEventListener(eventHandler, analyticsCollector); - } - private void removeSurfaceCallbacks() { if (textureView != null) { if (textureView.getSurfaceTextureListener() != componentListener) { @@ -1494,7 +1775,11 @@ public class SimpleExoPlayer extends BasePlayer for (Renderer renderer : renderers) { if (renderer.getTrackType() == C.TRACK_TYPE_VIDEO) { messages.add( - player.createMessage(renderer).setType(C.MSG_SET_SURFACE).setPayload(surface).send()); + player + .createMessage(renderer) + .setType(Renderer.MSG_SET_SURFACE) + .setPayload(surface) + .send()); } } if (this.surface != null && this.surface != surface) { @@ -1517,15 +1802,10 @@ public class SimpleExoPlayer extends BasePlayer private void setVideoDecoderOutputBufferRendererInternal( @Nullable VideoDecoderOutputBufferRenderer videoDecoderOutputBufferRenderer) { - for (Renderer renderer : renderers) { - if (renderer.getTrackType() == C.TRACK_TYPE_VIDEO) { - player - .createMessage(renderer) - .setType(C.MSG_SET_VIDEO_DECODER_OUTPUT_BUFFER_RENDERER) - .setPayload(videoDecoderOutputBufferRenderer) - .send(); - } - } + sendRendererMessage( + C.TRACK_TYPE_VIDEO, + Renderer.MSG_SET_VIDEO_DECODER_OUTPUT_BUFFER_RENDERER, + videoDecoderOutputBufferRenderer); this.videoDecoderOutputBufferRenderer = videoDecoderOutputBufferRenderer; } @@ -1541,35 +1821,101 @@ public class SimpleExoPlayer extends BasePlayer 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(); + sendRendererMessage(C.TRACK_TYPE_AUDIO, Renderer.MSG_SET_VOLUME, scaledVolume); + } + + private void notifyAudioSessionIdSet() { + 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(audioSessionId); } } + for (AudioRendererEventListener audioDebugListener : audioDebugListeners) { + audioDebugListener.onAudioSessionId(audioSessionId); + } + } + + @SuppressWarnings("SuspiciousMethodCalls") + private void notifySkipSilenceEnabledChanged() { + for (AudioListener listener : audioListeners) { + // Prevent duplicate notification if a listener is both a AudioRendererEventListener and + // a AudioListener, as they have the same method signature. + if (!audioDebugListeners.contains(listener)) { + listener.onSkipSilenceEnabledChanged(skipSilenceEnabled); + } + } + for (AudioRendererEventListener listener : audioDebugListeners) { + listener.onSkipSilenceEnabledChanged(skipSilenceEnabled); + } } private void updatePlayWhenReady( - boolean playWhenReady, @AudioFocusManager.PlayerCommand int playerCommand) { + boolean playWhenReady, + @AudioFocusManager.PlayerCommand int playerCommand, + @Player.PlayWhenReadyChangeReason int playWhenReadyChangeReason) { playWhenReady = playWhenReady && playerCommand != AudioFocusManager.PLAYER_COMMAND_DO_NOT_PLAY; @PlaybackSuppressionReason int playbackSuppressionReason = playWhenReady && playerCommand != AudioFocusManager.PLAYER_COMMAND_PLAY_WHEN_READY ? Player.PLAYBACK_SUPPRESSION_REASON_TRANSIENT_AUDIO_FOCUS_LOSS : Player.PLAYBACK_SUPPRESSION_REASON_NONE; - player.setPlayWhenReady(playWhenReady, playbackSuppressionReason); + player.setPlayWhenReady(playWhenReady, playbackSuppressionReason, playWhenReadyChangeReason); + } + + private void updateWakeAndWifiLock() { + @State int playbackState = getPlaybackState(); + switch (playbackState) { + case Player.STATE_READY: + case Player.STATE_BUFFERING: + wakeLockManager.setStayAwake(getPlayWhenReady()); + wifiLockManager.setStayAwake(getPlayWhenReady()); + break; + case Player.STATE_ENDED: + case Player.STATE_IDLE: + wakeLockManager.setStayAwake(false); + wifiLockManager.setStayAwake(false); + break; + default: + throw new IllegalStateException(); + } } private void verifyApplicationThread() { if (Looper.myLooper() != getApplicationLooper()) { + if (throwsWhenUsingWrongThread) { + throw new IllegalStateException(WRONG_THREAD_ERROR_MESSAGE); + } Log.w( TAG, - "Player is accessed on the wrong thread. See " - + "https://exoplayer.dev/issues/player-accessed-on-wrong-thread", + WRONG_THREAD_ERROR_MESSAGE, hasNotifiedFullWrongThreadWarning ? null : new IllegalStateException()); hasNotifiedFullWrongThreadWarning = true; } } + private void sendRendererMessage(int trackType, int messageType, @Nullable Object payload) { + for (Renderer renderer : renderers) { + if (renderer.getTrackType() == trackType) { + player.createMessage(renderer).setType(messageType).setPayload(payload).send(); + } + } + } + + private static DeviceInfo createDeviceInfo(StreamVolumeManager streamVolumeManager) { + return new DeviceInfo( + DeviceInfo.PLAYBACK_TYPE_LOCAL, + streamVolumeManager.getMinVolume(), + streamVolumeManager.getMaxVolume()); + } + + private static int getPlayWhenReadyChangeReason(boolean playWhenReady, int playerCommand) { + return playWhenReady && playerCommand != AudioFocusManager.PLAYER_COMMAND_PLAY_WHEN_READY + ? PLAY_WHEN_READY_CHANGE_REASON_AUDIO_FOCUS_LOSS + : PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST; + } + private final class ComponentListener implements VideoRendererEventListener, AudioRendererEventListener, @@ -1579,6 +1925,7 @@ public class SimpleExoPlayer extends BasePlayer TextureView.SurfaceTextureListener, AudioFocusManager.PlayerControl, AudioBecomingNoisyManager.EventListener, + StreamVolumeManager.Listener, Player.EventListener { // VideoRendererEventListener implementation @@ -1653,6 +2000,15 @@ public class SimpleExoPlayer extends BasePlayer videoDecoderCounters = null; } + @Override + public void onVideoFrameProcessingOffset( + long totalProcessingOffsetUs, int frameCount, Format format) { + for (VideoRendererEventListener videoDebugListener : videoDebugListeners) { + videoDebugListener.onVideoFrameProcessingOffset( + totalProcessingOffsetUs, frameCount, format); + } + } + // AudioRendererEventListener implementation @Override @@ -1669,16 +2025,7 @@ public class SimpleExoPlayer extends BasePlayer 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); - } + notifyAudioSessionIdSet(); } @Override @@ -1716,6 +2063,15 @@ public class SimpleExoPlayer extends BasePlayer audioSessionId = C.AUDIO_SESSION_ID_UNSET; } + @Override + public void onSkipSilenceEnabledChanged(boolean skipSilenceEnabled) { + if (SimpleExoPlayer.this.skipSilenceEnabled == skipSilenceEnabled) { + return; + } + SimpleExoPlayer.this.skipSilenceEnabled = skipSilenceEnabled; + notifySkipSilenceEnabledChanged(); + } + // TextOutput implementation @Override @@ -1787,20 +2143,45 @@ public class SimpleExoPlayer extends BasePlayer @Override public void executePlayerCommand(@AudioFocusManager.PlayerCommand int playerCommand) { - updatePlayWhenReady(getPlayWhenReady(), playerCommand); + boolean playWhenReady = getPlayWhenReady(); + updatePlayWhenReady( + playWhenReady, playerCommand, getPlayWhenReadyChangeReason(playWhenReady, playerCommand)); } // AudioBecomingNoisyManager.EventListener implementation. @Override public void onAudioBecomingNoisy() { - setPlayWhenReady(false); + updatePlayWhenReady( + /* playWhenReady= */ false, + AudioFocusManager.PLAYER_COMMAND_DO_NOT_PLAY, + Player.PLAY_WHEN_READY_CHANGE_REASON_AUDIO_BECOMING_NOISY); + } + + // StreamVolumeManager.Listener implementation. + + @Override + public void onStreamTypeChanged(@C.StreamType int streamType) { + DeviceInfo deviceInfo = createDeviceInfo(streamVolumeManager); + if (!deviceInfo.equals(SimpleExoPlayer.this.deviceInfo)) { + SimpleExoPlayer.this.deviceInfo = deviceInfo; + for (DeviceListener deviceListener : deviceListeners) { + deviceListener.onDeviceInfoChanged(deviceInfo); + } + } + } + + @Override + public void onStreamVolumeChanged(int streamVolume, boolean streamMuted) { + for (DeviceListener deviceListener : deviceListeners) { + deviceListener.onDeviceVolumeChanged(streamVolume, streamMuted); + } } // Player.EventListener implementation. @Override - public void onLoadingChanged(boolean isLoading) { + public void onIsLoadingChanged(boolean isLoading) { if (priorityTaskManager != null) { if (isLoading && !isPriorityTaskManagerRegistered) { priorityTaskManager.add(C.PRIORITY_PLAYBACK); @@ -1813,17 +2194,14 @@ public class SimpleExoPlayer extends BasePlayer } @Override - public void onPlayerStateChanged(boolean playWhenReady, @State int playbackState) { - switch (playbackState) { - case Player.STATE_READY: - case Player.STATE_BUFFERING: - wakeLockManager.setStayAwake(playWhenReady); - break; - case Player.STATE_ENDED: - case Player.STATE_IDLE: - wakeLockManager.setStayAwake(false); - break; - } + public void onPlaybackStateChanged(@State int playbackState) { + updateWakeAndWifiLock(); + } + + @Override + public void onPlayWhenReadyChanged( + boolean playWhenReady, @PlayWhenReadyChangeReason int reason) { + updateWakeAndWifiLock(); } } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/StreamVolumeManager.java b/library/core/src/main/java/com/google/android/exoplayer2/StreamVolumeManager.java new file mode 100644 index 0000000000..66216de861 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/StreamVolumeManager.java @@ -0,0 +1,199 @@ +/* + * Copyright 2020 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; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.media.AudioManager; +import android.os.Handler; +import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.Util; + +/** A manager that wraps {@link AudioManager} to control/listen audio stream volume. */ +/* package */ final class StreamVolumeManager { + + /** A listener for changes in the manager. */ + public interface Listener { + + /** Called when the audio stream type is changed. */ + void onStreamTypeChanged(@C.StreamType int streamType); + + /** Called when the audio stream volume or mute state is changed. */ + void onStreamVolumeChanged(int streamVolume, boolean streamMuted); + } + + // TODO(b/151280453): Replace the hidden intent action with an official one. + // Copied from AudioManager#VOLUME_CHANGED_ACTION + private static final String VOLUME_CHANGED_ACTION = "android.media.VOLUME_CHANGED_ACTION"; + + // TODO(b/153317944): Allow users to override these flags. + private static final int VOLUME_FLAGS = AudioManager.FLAG_SHOW_UI; + + private final Context applicationContext; + private final Handler eventHandler; + private final Listener listener; + private final AudioManager audioManager; + private final VolumeChangeReceiver receiver; + + @C.StreamType private int streamType; + private int volume; + private boolean muted; + private boolean released; + + /** Creates a manager. */ + public StreamVolumeManager(Context context, Handler eventHandler, Listener listener) { + applicationContext = context.getApplicationContext(); + this.eventHandler = eventHandler; + this.listener = listener; + audioManager = + Assertions.checkStateNotNull( + (AudioManager) applicationContext.getSystemService(Context.AUDIO_SERVICE)); + + streamType = C.STREAM_TYPE_DEFAULT; + volume = getVolumeFromManager(audioManager, streamType); + muted = getMutedFromManager(audioManager, streamType); + + receiver = new VolumeChangeReceiver(); + IntentFilter filter = new IntentFilter(VOLUME_CHANGED_ACTION); + applicationContext.registerReceiver(receiver, filter); + } + + /** Sets the audio stream type. */ + public void setStreamType(@C.StreamType int streamType) { + if (this.streamType == streamType) { + return; + } + this.streamType = streamType; + + updateVolumeAndNotifyIfChanged(); + listener.onStreamTypeChanged(streamType); + } + + /** + * Gets the minimum volume for the current audio stream. It can be changed if {@link + * #setStreamType(int)} is called. + */ + public int getMinVolume() { + return Util.SDK_INT >= 28 ? audioManager.getStreamMinVolume(streamType) : 0; + } + + /** + * Gets the maximum volume for the current audio stream. It can be changed if {@link + * #setStreamType(int)} is called. + */ + public int getMaxVolume() { + return audioManager.getStreamMaxVolume(streamType); + } + + /** Gets the current volume for the current audio stream. */ + public int getVolume() { + return volume; + } + + /** Gets whether the current audio stream is muted or not. */ + public boolean isMuted() { + return muted; + } + + /** + * Sets the volume with the given value for the current audio stream. The value should be between + * {@link #getMinVolume()} and {@link #getMaxVolume()}, otherwise it will be ignored. + */ + public void setVolume(int volume) { + if (volume < getMinVolume() || volume > getMaxVolume()) { + return; + } + audioManager.setStreamVolume(streamType, volume, VOLUME_FLAGS); + updateVolumeAndNotifyIfChanged(); + } + + /** + * Increases the volume by one for the current audio stream. It will be ignored if the current + * volume is equal to {@link #getMaxVolume()}. + */ + public void increaseVolume() { + if (volume >= getMaxVolume()) { + return; + } + audioManager.adjustStreamVolume(streamType, AudioManager.ADJUST_RAISE, VOLUME_FLAGS); + updateVolumeAndNotifyIfChanged(); + } + + /** + * Decreases the volume by one for the current audio stream. It will be ignored if the current + * volume is equal to {@link #getMinVolume()}. + */ + public void decreaseVolume() { + if (volume <= getMinVolume()) { + return; + } + audioManager.adjustStreamVolume(streamType, AudioManager.ADJUST_LOWER, VOLUME_FLAGS); + updateVolumeAndNotifyIfChanged(); + } + + /** Sets the mute state of the current audio stream. */ + public void setMuted(boolean muted) { + if (Util.SDK_INT >= 23) { + audioManager.adjustStreamVolume( + streamType, muted ? AudioManager.ADJUST_MUTE : AudioManager.ADJUST_UNMUTE, VOLUME_FLAGS); + } else { + audioManager.setStreamMute(streamType, muted); + } + updateVolumeAndNotifyIfChanged(); + } + + /** Releases the manager. It must be called when the manager is no longer required. */ + public void release() { + if (released) { + return; + } + applicationContext.unregisterReceiver(receiver); + released = true; + } + + private void updateVolumeAndNotifyIfChanged() { + int newVolume = getVolumeFromManager(audioManager, streamType); + boolean newMuted = getMutedFromManager(audioManager, streamType); + if (volume != newVolume || muted != newMuted) { + volume = newVolume; + muted = newMuted; + listener.onStreamVolumeChanged(newVolume, newMuted); + } + } + + private static int getVolumeFromManager(AudioManager audioManager, @C.StreamType int streamType) { + return audioManager.getStreamVolume(streamType); + } + + private static boolean getMutedFromManager( + AudioManager audioManager, @C.StreamType int streamType) { + if (Util.SDK_INT >= 23) { + return audioManager.isStreamMute(streamType); + } else { + return audioManager.getStreamVolume(streamType) == 0; + } + } + + private final class VolumeChangeReceiver extends BroadcastReceiver { + + @Override + public void onReceive(Context context, Intent intent) { + eventHandler.post(StreamVolumeManager.this::updateVolumeAndNotifyIfChanged); + } + } +} 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 a860249478..c3d9cab7ab 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 @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2; +import android.os.SystemClock; import android.util.Pair; import androidx.annotation.Nullable; import com.google.android.exoplayer2.source.ads.AdPlaybackState; @@ -45,7 +46,7 @@ import com.google.android.exoplayer2.util.Util; * *

        Single media file or on-demand stream

        * - *

        Example timeline for a
+ * <p style=Example timeline for a
  * single file A timeline for a single media file or on-demand stream consists of a single period * and window. The window spans the whole period, indicating that all parts of the media are * available for playback. The window's default position is typically at the start of the period @@ -53,17 +54,17 @@ import com.google.android.exoplayer2.util.Util; * *

        Playlist of media files or on-demand streams

        * - *

        Example timeline for a playlist
- * of files A timeline for a playlist of media files or on-demand streams consists of multiple - * periods, each with its own window. Each window spans the whole of the corresponding period, and - * typically has a default position at the start of the period. The properties of the periods and - * windows (e.g. their durations and whether the window is seekable) will often only become known - * when the player starts buffering the corresponding file or stream. + *

        Example timeline for a
+ * playlist of files A timeline for a playlist of media files or on-demand streams consists of + * multiple periods, each with its own window. Each window spans the whole of the corresponding + * period, and typically has a default position at the start of the period. The properties of the + * periods and windows (e.g. their durations and whether the window is seekable) will often only + * become known when the player starts buffering the corresponding file or stream. * *

        Live stream with limited availability

        * - *

        Example timeline for a live
- * stream with limited availability A timeline for a live stream consists of a period whose + *

        Example timeline for
+ * a live stream with limited availability A timeline for a live stream consists of a period whose * duration is unknown, since it's continually extending as more content is broadcast. If content * only remains available for a limited period of time then the window may start at a non-zero * position, defining the region of content that can still be played. The window will have {@link @@ -73,24 +74,24 @@ import com.google.android.exoplayer2.util.Util; * *

        Live stream with indefinite availability

        * - *

        Example timeline for a
- * live stream with indefinite availability A timeline for a live stream with indefinite + *

        Example timeline
+ * for a live stream with indefinite availability A timeline for a live stream with indefinite * availability is similar to the Live stream with limited availability * case, except that the window starts at the beginning of the period to indicate that all of the * previously broadcast content can still be played. * *

        Live stream with multiple periods

        * - *

        Example timeline for a
- * live stream with multiple periods This case arises when a live stream is explicitly divided - * into separate periods, for example at content boundaries. This case is similar to the Example timeline
+ * for a live stream with multiple periods This case arises when a live stream is explicitly + * divided into separate periods, for example at content boundaries. This case is similar to the Live stream with limited availability case, except that the window may * span more than one period. Multiple periods are also possible in the indefinite availability * case. * *

        On-demand stream followed by live stream

        * - *

        Example timeline for an
+ * <p style=Example timeline for an
  * on-demand stream followed by a live stream This case is the concatenation of the Single media file or on-demand stream and Live * stream with multiple periods cases. When playback of the on-demand stream ends, playback of @@ -98,10 +99,10 @@ import com.google.android.exoplayer2.util.Util; * *

        On-demand stream with mid-roll ads

        * - *

        Example timeline
- * for an on-demand stream with mid-roll ad groups This case includes mid-roll ad groups, which - * are defined as part of the timeline's single period. The period can be queried for information - * about the ad groups and the ads they contain. + *

        Example
+ * timeline for an on-demand stream with mid-roll ad groups This case includes mid-roll ad groups, + * which are defined as part of the timeline's single period. The period can be queried for + * information about the ad groups and the ads they contain. */ public abstract class Timeline { @@ -112,7 +113,7 @@ public abstract class Timeline { * shows some of the information defined by a window, as well as how this information relates to * corresponding {@link Period Periods} in the timeline. * - *

        Information defined by a
+   * <p style=Information defined by a
    * timeline window */ public static final class Window { @@ -128,27 +129,39 @@ public abstract class Timeline { */ public Object uid; - /** A tag for the window. Not necessarily unique. */ - @Nullable public Object tag; + /** @deprecated Use {@link #mediaItem} instead. */ + @Deprecated @Nullable public Object tag; + + /** The {@link MediaItem} associated to the window. Not necessarily unique. */ + @Nullable public MediaItem mediaItem; /** The manifest of the window. May be {@code null}. */ @Nullable public Object manifest; /** * The start time of the presentation to which this window belongs in milliseconds since the - * epoch, or {@link C#TIME_UNSET} if unknown or not applicable. For informational purposes only. + * Unix epoch, or {@link C#TIME_UNSET} if unknown or not applicable. For informational purposes + * only. */ public long presentationStartTimeMs; /** - * The window's start time in milliseconds since the epoch, or {@link C#TIME_UNSET} if unknown - * or not applicable. For informational purposes only. + * The window's start time in milliseconds since the Unix epoch, or {@link C#TIME_UNSET} if + * unknown or not applicable. For informational purposes only. */ public long windowStartTimeMs; /** - * Whether it's possible to seek within this window. + * The offset between {@link SystemClock#elapsedRealtime()} and the time since the Unix epoch + * according to the clock of the media origin server, or {@link C#TIME_UNSET} if unknown or not + * applicable. + * + *

        Note that the current Unix time can be retrieved using {@link #getCurrentUnixTimeMs()} and + * is calculated as {@code SystemClock.elapsedRealtime() + elapsedRealtimeEpochOffsetMs}. */ + public long elapsedRealtimeEpochOffsetMs; + + /** Whether it's possible to seek within this window. */ public boolean isSeekable; // TODO: Split this to better describe which parts of the window might change. For example it @@ -166,6 +179,12 @@ public abstract class Timeline { */ public boolean isLive; + /** + * Whether this window contains placeholder information because the real information has yet to + * be loaded. + */ + public boolean isPlaceholder; + /** The index of the first period that belongs to this window. */ public int firstPeriodIndex; @@ -198,13 +217,55 @@ public abstract class Timeline { uid = SINGLE_WINDOW_UID; } - /** Sets the data held by this window. */ + /** + * @deprecated Use {@link #set(Object, MediaItem, Object, long, long, long, boolean, boolean, + * boolean, long, long, int, int, long)} instead. + */ + @SuppressWarnings("deprecation") + @Deprecated public Window set( Object uid, @Nullable Object tag, @Nullable Object manifest, long presentationStartTimeMs, long windowStartTimeMs, + long elapsedRealtimeEpochOffsetMs, + boolean isSeekable, + boolean isDynamic, + boolean isLive, + long defaultPositionUs, + long durationUs, + int firstPeriodIndex, + int lastPeriodIndex, + long positionInFirstPeriodUs) { + set( + uid, + /* mediaItem= */ null, + manifest, + presentationStartTimeMs, + windowStartTimeMs, + elapsedRealtimeEpochOffsetMs, + isSeekable, + isDynamic, + isLive, + defaultPositionUs, + durationUs, + firstPeriodIndex, + lastPeriodIndex, + positionInFirstPeriodUs); + this.tag = tag; + return this; + } + + /** Sets the data held by this window. */ + @SuppressWarnings("deprecation") + public Window set( + Object uid, + @Nullable MediaItem mediaItem, + @Nullable Object manifest, + long presentationStartTimeMs, + long windowStartTimeMs, + long elapsedRealtimeEpochOffsetMs, boolean isSeekable, boolean isDynamic, boolean isLive, @@ -214,10 +275,15 @@ public abstract class Timeline { int lastPeriodIndex, long positionInFirstPeriodUs) { this.uid = uid; - this.tag = tag; + this.mediaItem = mediaItem; + this.tag = + mediaItem != null && mediaItem.playbackProperties != null + ? mediaItem.playbackProperties.tag + : null; this.manifest = manifest; this.presentationStartTimeMs = presentationStartTimeMs; this.windowStartTimeMs = windowStartTimeMs; + this.elapsedRealtimeEpochOffsetMs = elapsedRealtimeEpochOffsetMs; this.isSeekable = isSeekable; this.isDynamic = isDynamic; this.isLive = isLive; @@ -226,6 +292,7 @@ public abstract class Timeline { this.firstPeriodIndex = firstPeriodIndex; this.lastPeriodIndex = lastPeriodIndex; this.positionInFirstPeriodUs = positionInFirstPeriodUs; + this.isPlaceholder = false; return this; } @@ -279,6 +346,16 @@ public abstract class Timeline { return positionInFirstPeriodUs; } + /** + * Returns the current time in milliseconds since the Unix epoch. + * + *

        This method applies {@link #elapsedRealtimeEpochOffsetMs known corrections} made available + * by the media such that this time corresponds to the clock of the media origin server. + */ + public long getCurrentUnixTimeMs() { + return Util.getNowUnixTimeMs(elapsedRealtimeEpochOffsetMs); + } + @Override public boolean equals(@Nullable Object obj) { if (this == obj) { @@ -290,12 +367,15 @@ public abstract class Timeline { Window that = (Window) obj; return Util.areEqual(uid, that.uid) && Util.areEqual(tag, that.tag) + && Util.areEqual(mediaItem, that.mediaItem) && Util.areEqual(manifest, that.manifest) && presentationStartTimeMs == that.presentationStartTimeMs && windowStartTimeMs == that.windowStartTimeMs + && elapsedRealtimeEpochOffsetMs == that.elapsedRealtimeEpochOffsetMs && isSeekable == that.isSeekable && isDynamic == that.isDynamic && isLive == that.isLive + && isPlaceholder == that.isPlaceholder && defaultPositionUs == that.defaultPositionUs && durationUs == that.durationUs && firstPeriodIndex == that.firstPeriodIndex @@ -308,12 +388,17 @@ public abstract class Timeline { int result = 7; result = 31 * result + uid.hashCode(); result = 31 * result + (tag == null ? 0 : tag.hashCode()); + result = 31 * result + (mediaItem == null ? 0 : mediaItem.hashCode()); result = 31 * result + (manifest == null ? 0 : manifest.hashCode()); result = 31 * result + (int) (presentationStartTimeMs ^ (presentationStartTimeMs >>> 32)); result = 31 * result + (int) (windowStartTimeMs ^ (windowStartTimeMs >>> 32)); + result = + 31 * result + + (int) (elapsedRealtimeEpochOffsetMs ^ (elapsedRealtimeEpochOffsetMs >>> 32)); result = 31 * result + (isSeekable ? 1 : 0); result = 31 * result + (isDynamic ? 1 : 0); result = 31 * result + (isLive ? 1 : 0); + result = 31 * result + (isPlaceholder ? 1 : 0); result = 31 * result + (int) (defaultPositionUs ^ (defaultPositionUs >>> 32)); result = 31 * result + (int) (durationUs ^ (durationUs >>> 32)); result = 31 * result + firstPeriodIndex; @@ -327,12 +412,12 @@ public abstract class Timeline { * Holds information about a period in a {@link Timeline}. A period defines a single logical piece * of media, for example a media file. It may also define groups of ads inserted into the media, * along with information about whether those ads have been loaded and played. - *

        - * The figure below shows some of the information defined by a period, as well as how this + * + *

        The figure below shows some of the information defined by a period, as well as how this * information relates to a corresponding {@link Window} in the timeline. - *

        - * Information defined by a period - *

        + * + *

        Information defined by a
+   * period */ public static final class Period { @@ -466,8 +551,8 @@ public abstract class Timeline { * microseconds. * * @param adGroupIndex The ad group index. - * @return The time of the ad group at the index, in microseconds, or {@link - * C#TIME_END_OF_SOURCE} for a post-roll ad group. + * @return The time of the ad group at the index relative to the start of the enclosing {@link + * Period}, in microseconds, or {@link C#TIME_END_OF_SOURCE} for a post-roll ad group. */ public long getAdGroupTimeUs(int adGroupIndex) { return adPlaybackState.adGroupTimesUs[adGroupIndex]; @@ -510,22 +595,23 @@ public abstract class Timeline { } /** - * Returns the index of the ad group at or before {@code positionUs}, if that ad group is - * unplayed. Returns {@link C#INDEX_UNSET} if the ad group at or before {@code positionUs} has - * no ads remaining to be played, or if there is no such ad group. + * Returns the index of the ad group at or before {@code positionUs} in the period, if that ad + * group is unplayed. Returns {@link C#INDEX_UNSET} if the ad group at or before {@code + * positionUs} has no ads remaining to be played, or if there is no such ad group. * - * @param positionUs The position at or before which to find an ad group, in microseconds. + * @param positionUs The period position at or before which to find an ad group, in + * microseconds. * @return The index of the ad group, or {@link C#INDEX_UNSET}. */ public int getAdGroupIndexForPositionUs(long positionUs) { - return adPlaybackState.getAdGroupIndexForPositionUs(positionUs); + return adPlaybackState.getAdGroupIndexForPositionUs(positionUs, durationUs); } /** - * Returns the index of the next ad group after {@code positionUs} that has ads remaining to be - * played. Returns {@link C#INDEX_UNSET} if there is no such ad group. + * Returns the index of the next ad group after {@code positionUs} in the period that has ads + * remaining to be played. Returns {@link C#INDEX_UNSET} if there is no such ad group. * - * @param positionUs The position after which to find an ad group, in microseconds. + * @param positionUs The period position after which to find an ad group, in microseconds. * @return The index of the ad group, or {@link C#INDEX_UNSET}. */ public int getAdGroupIndexAfterPositionUs(long positionUs) { @@ -905,4 +991,50 @@ public abstract class Timeline { * @return The unique id of the period. */ public abstract Object getUidOfPeriod(int periodIndex); + + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof Timeline)) { + return false; + } + Timeline other = (Timeline) obj; + if (other.getWindowCount() != getWindowCount() || other.getPeriodCount() != getPeriodCount()) { + return false; + } + Timeline.Window window = new Timeline.Window(); + Timeline.Period period = new Timeline.Period(); + Timeline.Window otherWindow = new Timeline.Window(); + Timeline.Period otherPeriod = new Timeline.Period(); + for (int i = 0; i < getWindowCount(); i++) { + if (!getWindow(i, window).equals(other.getWindow(i, otherWindow))) { + return false; + } + } + for (int i = 0; i < getPeriodCount(); i++) { + if (!getPeriod(i, period, /* setIds= */ true) + .equals(other.getPeriod(i, otherPeriod, /* setIds= */ true))) { + return false; + } + } + return true; + } + + @Override + public int hashCode() { + Window window = new Window(); + Period period = new Period(); + int result = 7; + result = 31 * result + getWindowCount(); + for (int i = 0; i < getWindowCount(); i++) { + result = 31 * result + getWindow(i, window).hashCode(); + } + result = 31 * result + getPeriodCount(); + for (int i = 0; i < getPeriodCount(); i++) { + result = 31 * result + getPeriod(i, period, /* setIds= */ true).hashCode(); + } + return result; + } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/WakeLockManager.java b/library/core/src/main/java/com/google/android/exoplayer2/WakeLockManager.java index f498eea6f4..6de302d62d 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/WakeLockManager.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/WakeLockManager.java @@ -39,7 +39,8 @@ import com.google.android.exoplayer2.util.Log; private boolean stayAwake; public WakeLockManager(Context context) { - powerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE); + powerManager = + (PowerManager) context.getApplicationContext().getSystemService(Context.POWER_SERVICE); } /** @@ -48,18 +49,19 @@ import com.google.android.exoplayer2.util.Log; *

        By default, wake lock handling is not enabled. Enabling this will acquire the wake lock if * necessary. Disabling this will release the wake lock if it is held. * - * @param enabled True if the player should handle a {@link WakeLock}, false otherwise. Please - * note that enabling this requires the {@link android.Manifest.permission#WAKE_LOCK} - * permission. + *

        Enabling {@link WakeLock} requires the {@link android.Manifest.permission#WAKE_LOCK}. + * + * @param enabled True if the player should handle a {@link WakeLock}, false otherwise. */ public void setEnabled(boolean enabled) { if (enabled) { if (wakeLock == null) { if (powerManager == null) { - Log.w(TAG, "PowerManager was null, therefore the WakeLock was not created."); + Log.w(TAG, "PowerManager is null, therefore not creating the WakeLock."); return; } wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, WAKE_LOCK_TAG); + wakeLock.setReferenceCounted(false); } } @@ -86,17 +88,14 @@ import com.google.android.exoplayer2.util.Log; // reasonable timeout that would not affect the user. @SuppressLint("WakelockTimeout") private void updateWakeLock() { - // Needed for the library nullness check. If enabled is true, the wakelock will not be null. - if (wakeLock != null) { - if (enabled) { - if (stayAwake && !wakeLock.isHeld()) { - wakeLock.acquire(); - } else if (!stayAwake && wakeLock.isHeld()) { - wakeLock.release(); - } - } else if (wakeLock.isHeld()) { - wakeLock.release(); - } + if (wakeLock == null) { + return; + } + + if (enabled && stayAwake) { + wakeLock.acquire(); + } else { + wakeLock.release(); } } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/WifiLockManager.java b/library/core/src/main/java/com/google/android/exoplayer2/WifiLockManager.java new file mode 100644 index 0000000000..d3700a646a --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/WifiLockManager.java @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2020 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; + +import android.content.Context; +import android.net.wifi.WifiManager; +import android.net.wifi.WifiManager.WifiLock; +import androidx.annotation.Nullable; +import com.google.android.exoplayer2.util.Log; + +/** + * Handles a {@link WifiLock} + * + *

        The handling of wifi locks requires the {@link android.Manifest.permission#WAKE_LOCK} + * permission. + */ +/* package */ final class WifiLockManager { + + private static final String TAG = "WifiLockManager"; + private static final String WIFI_LOCK_TAG = "ExoPlayer:WifiLockManager"; + + @Nullable private final WifiManager wifiManager; + @Nullable private WifiLock wifiLock; + private boolean enabled; + private boolean stayAwake; + + public WifiLockManager(Context context) { + wifiManager = + (WifiManager) context.getApplicationContext().getSystemService(Context.WIFI_SERVICE); + } + + /** + * Sets whether to enable the usage of a {@link WifiLock}. + * + *

        By default, wifi lock handling is not enabled. Enabling will acquire the wifi lock if + * necessary. Disabling will release the wifi lock if held. + * + *

        Enabling {@link WifiLock} requires the {@link android.Manifest.permission#WAKE_LOCK}. + * + * @param enabled True if the player should handle a {@link WifiLock}. + */ + public void setEnabled(boolean enabled) { + if (enabled && wifiLock == null) { + if (wifiManager == null) { + Log.w(TAG, "WifiManager is null, therefore not creating the WifiLock."); + return; + } + wifiLock = wifiManager.createWifiLock(WifiManager.WIFI_MODE_FULL_HIGH_PERF, WIFI_LOCK_TAG); + wifiLock.setReferenceCounted(false); + } + + this.enabled = enabled; + updateWifiLock(); + } + + /** + * Sets whether to acquire or release the {@link WifiLock}. + * + *

        The wifi lock will not be acquired unless handling has been enabled through {@link + * #setEnabled(boolean)}. + * + * @param stayAwake True if the player should acquire the {@link WifiLock}. False if it should + * release. + */ + public void setStayAwake(boolean stayAwake) { + this.stayAwake = stayAwake; + updateWifiLock(); + } + + private void updateWifiLock() { + if (wifiLock == null) { + return; + } + + if (enabled && stayAwake) { + wifiLock.acquire(); + } else { + wifiLock.release(); + } + } +} 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 efc1650192..715a1c0f14 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 @@ -31,7 +31,7 @@ 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; +import com.google.android.exoplayer2.drm.DrmSessionEventListener; import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.MetadataOutput; import com.google.android.exoplayer2.source.LoadEventInfo; @@ -66,7 +66,7 @@ public class AnalyticsCollector VideoRendererEventListener, MediaSourceEventListener, BandwidthMeter.EventListener, - DefaultDrmSessionEventListener, + DrmSessionEventListener, VideoListener, AudioListener { @@ -76,6 +76,7 @@ public class AnalyticsCollector private final MediaPeriodQueueTracker mediaPeriodQueueTracker; private @MonotonicNonNull Player player; + private boolean isSeeking; /** * Creates an analytics collector. @@ -126,20 +127,17 @@ public class AnalyticsCollector * adjusts its state and position to the seek. */ public final void notifySeekStarted() { - if (!mediaPeriodQueueTracker.isSeeking()) { - EventTime eventTime = generatePlayingMediaPeriodEventTime(); - mediaPeriodQueueTracker.onSeekStarted(); + if (!isSeeking) { + EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime(); + isSeeking = true; for (AnalyticsListener listener : listeners) { listener.onSeekStarted(eventTime); } } } - /** - * Resets the analytics collector for a new media source. Should be called before the player is - * prepared with a new media source. - */ - public final void resetForNewMediaSource() { + /** Resets the analytics collector for a new playlist. */ + public final void resetForNewPlaylist() { // Copying the list is needed because onMediaPeriodReleased will modify the list. List mediaPeriodInfos = new ArrayList<>(mediaPeriodQueueTracker.mediaPeriodInfoQueue); @@ -152,7 +150,7 @@ public class AnalyticsCollector @Override public final void onMetadata(Metadata metadata) { - EventTime eventTime = generatePlayingMediaPeriodEventTime(); + EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime(); for (AnalyticsListener listener : listeners) { listener.onMetadata(eventTime, metadata); } @@ -162,8 +160,7 @@ public class AnalyticsCollector @Override public final void onAudioEnabled(DecoderCounters counters) { - // The renderers are only enabled after we changed the playing media period. - EventTime eventTime = generatePlayingMediaPeriodEventTime(); + EventTime eventTime = generateReadingMediaPeriodEventTime(); for (AnalyticsListener listener : listeners) { listener.onDecoderEnabled(eventTime, C.TRACK_TYPE_AUDIO, counters); } @@ -198,9 +195,7 @@ public class AnalyticsCollector @Override public final void onAudioDisabled(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(); + EventTime eventTime = generatePlayingMediaPeriodEventTime(); for (AnalyticsListener listener : listeners) { listener.onDecoderDisabled(eventTime, C.TRACK_TYPE_AUDIO, counters); } @@ -224,6 +219,14 @@ public class AnalyticsCollector } } + @Override + public void onSkipSilenceEnabledChanged(boolean skipSilenceEnabled) { + EventTime eventTime = generateReadingMediaPeriodEventTime(); + for (AnalyticsListener listener : listeners) { + listener.onSkipSilenceEnabledChanged(eventTime, skipSilenceEnabled); + } + } + @Override public void onVolumeChanged(float audioVolume) { EventTime eventTime = generateReadingMediaPeriodEventTime(); @@ -236,8 +239,7 @@ public class AnalyticsCollector @Override public final void onVideoEnabled(DecoderCounters counters) { - // The renderers are only enabled after we changed the playing media period. - EventTime eventTime = generatePlayingMediaPeriodEventTime(); + EventTime eventTime = generateReadingMediaPeriodEventTime(); for (AnalyticsListener listener : listeners) { listener.onDecoderEnabled(eventTime, C.TRACK_TYPE_VIDEO, counters); } @@ -263,7 +265,7 @@ public class AnalyticsCollector @Override public final void onDroppedFrames(int count, long elapsedMs) { - EventTime eventTime = generateLastReportedPlayingMediaPeriodEventTime(); + EventTime eventTime = generatePlayingMediaPeriodEventTime(); for (AnalyticsListener listener : listeners) { listener.onDroppedVideoFrames(eventTime, count, elapsedMs); } @@ -271,9 +273,7 @@ public class AnalyticsCollector @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(); + EventTime eventTime = generatePlayingMediaPeriodEventTime(); for (AnalyticsListener listener : listeners) { listener.onDecoderDisabled(eventTime, C.TRACK_TYPE_VIDEO, counters); } @@ -287,6 +287,15 @@ public class AnalyticsCollector } } + @Override + public final void onVideoFrameProcessingOffset( + long totalProcessingOffsetUs, int frameCount, Format format) { + EventTime eventTime = generatePlayingMediaPeriodEventTime(); + for (AnalyticsListener listener : listeners) { + listener.onVideoFrameProcessingOffset(eventTime, totalProcessingOffsetUs, frameCount, format); + } + } + // VideoListener implementation. @Override @@ -316,7 +325,8 @@ public class AnalyticsCollector @Override public final void onMediaPeriodCreated(int windowIndex, MediaPeriodId mediaPeriodId) { - mediaPeriodQueueTracker.onMediaPeriodCreated(windowIndex, mediaPeriodId); + mediaPeriodQueueTracker.onMediaPeriodCreated( + windowIndex, mediaPeriodId, Assertions.checkNotNull(player)); EventTime eventTime = generateMediaPeriodEventTime(windowIndex, mediaPeriodId); for (AnalyticsListener listener : listeners) { listener.onMediaPeriodCreated(eventTime); @@ -326,7 +336,8 @@ public class AnalyticsCollector @Override public final void onMediaPeriodReleased(int windowIndex, MediaPeriodId mediaPeriodId) { EventTime eventTime = generateMediaPeriodEventTime(windowIndex, mediaPeriodId); - if (mediaPeriodQueueTracker.onMediaPeriodReleased(mediaPeriodId)) { + if (mediaPeriodQueueTracker.onMediaPeriodReleased( + mediaPeriodId, Assertions.checkNotNull(player))) { for (AnalyticsListener listener : listeners) { listener.onMediaPeriodReleased(eventTime); } @@ -418,8 +429,8 @@ public class AnalyticsCollector @Override public final void onTimelineChanged(Timeline timeline, @Player.TimelineChangeReason int reason) { - mediaPeriodQueueTracker.onTimelineChanged(timeline); - EventTime eventTime = generatePlayingMediaPeriodEventTime(); + mediaPeriodQueueTracker.onTimelineChanged(timeline, Assertions.checkNotNull(player)); + EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime(); for (AnalyticsListener listener : listeners) { listener.onTimelineChanged(eventTime, reason); } @@ -428,32 +439,50 @@ public class AnalyticsCollector @Override public final void onTracksChanged( TrackGroupArray trackGroups, TrackSelectionArray trackSelections) { - EventTime eventTime = generatePlayingMediaPeriodEventTime(); + EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime(); for (AnalyticsListener listener : listeners) { listener.onTracksChanged(eventTime, trackGroups, trackSelections); } } @Override - public final void onLoadingChanged(boolean isLoading) { - EventTime eventTime = generatePlayingMediaPeriodEventTime(); + public final void onIsLoadingChanged(boolean isLoading) { + EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime(); for (AnalyticsListener listener : listeners) { - listener.onLoadingChanged(eventTime, isLoading); + listener.onIsLoadingChanged(eventTime, isLoading); } } + @SuppressWarnings("deprecation") @Override public final void onPlayerStateChanged(boolean playWhenReady, @Player.State int playbackState) { - EventTime eventTime = generatePlayingMediaPeriodEventTime(); + EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime(); for (AnalyticsListener listener : listeners) { listener.onPlayerStateChanged(eventTime, playWhenReady, playbackState); } } + @Override + public final void onPlaybackStateChanged(@Player.State int state) { + EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime(); + for (AnalyticsListener listener : listeners) { + listener.onPlaybackStateChanged(eventTime, state); + } + } + + @Override + public final void onPlayWhenReadyChanged( + boolean playWhenReady, @Player.PlayWhenReadyChangeReason int reason) { + EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime(); + for (AnalyticsListener listener : listeners) { + listener.onPlayWhenReadyChanged(eventTime, playWhenReady, reason); + } + } + @Override public void onPlaybackSuppressionReasonChanged( @PlaybackSuppressionReason int playbackSuppressionReason) { - EventTime eventTime = generatePlayingMediaPeriodEventTime(); + EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime(); for (AnalyticsListener listener : listeners) { listener.onPlaybackSuppressionReasonChanged(eventTime, playbackSuppressionReason); } @@ -461,7 +490,7 @@ public class AnalyticsCollector @Override public void onIsPlayingChanged(boolean isPlaying) { - EventTime eventTime = generatePlayingMediaPeriodEventTime(); + EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime(); for (AnalyticsListener listener : listeners) { listener.onIsPlayingChanged(eventTime, isPlaying); } @@ -469,7 +498,7 @@ public class AnalyticsCollector @Override public final void onRepeatModeChanged(@Player.RepeatMode int repeatMode) { - EventTime eventTime = generatePlayingMediaPeriodEventTime(); + EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime(); for (AnalyticsListener listener : listeners) { listener.onRepeatModeChanged(eventTime, repeatMode); } @@ -477,7 +506,7 @@ public class AnalyticsCollector @Override public final void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) { - EventTime eventTime = generatePlayingMediaPeriodEventTime(); + EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime(); for (AnalyticsListener listener : listeners) { listener.onShuffleModeChanged(eventTime, shuffleModeEnabled); } @@ -485,7 +514,7 @@ public class AnalyticsCollector @Override public final void onPlayerError(ExoPlaybackException error) { - EventTime eventTime = generateLastReportedPlayingMediaPeriodEventTime(); + EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime(); for (AnalyticsListener listener : listeners) { listener.onPlayerError(eventTime, error); } @@ -493,29 +522,44 @@ public class AnalyticsCollector @Override public final void onPositionDiscontinuity(@Player.DiscontinuityReason int reason) { - mediaPeriodQueueTracker.onPositionDiscontinuity(reason); - EventTime eventTime = generatePlayingMediaPeriodEventTime(); + if (reason == Player.DISCONTINUITY_REASON_SEEK) { + isSeeking = false; + } + mediaPeriodQueueTracker.onPositionDiscontinuity(Assertions.checkNotNull(player)); + EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime(); for (AnalyticsListener listener : listeners) { listener.onPositionDiscontinuity(eventTime, reason); } } + /** + * @deprecated Use {@link #onPlaybackSpeedChanged(float)} and {@link + * #onSkipSilenceEnabledChanged(boolean)} instead. + */ + @SuppressWarnings("deprecation") + @Deprecated @Override public final void onPlaybackParametersChanged(PlaybackParameters playbackParameters) { - EventTime eventTime = generatePlayingMediaPeriodEventTime(); + EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime(); for (AnalyticsListener listener : listeners) { listener.onPlaybackParametersChanged(eventTime, playbackParameters); } } + @Override + public void onPlaybackSpeedChanged(float playbackSpeed) { + EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime(); + for (AnalyticsListener listener : listeners) { + listener.onPlaybackSpeedChanged(eventTime, playbackSpeed); + } + } + + @SuppressWarnings("deprecation") @Override public final void onSeekProcessed() { - if (mediaPeriodQueueTracker.isSeeking()) { - mediaPeriodQueueTracker.onSeekProcessed(); - EventTime eventTime = generatePlayingMediaPeriodEventTime(); - for (AnalyticsListener listener : listeners) { - listener.onSeekProcessed(eventTime); - } + EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime(); + for (AnalyticsListener listener : listeners) { + listener.onSeekProcessed(eventTime); } } @@ -573,7 +617,7 @@ public class AnalyticsCollector @Override public final void onDrmSessionReleased() { - EventTime eventTime = generateLastReportedPlayingMediaPeriodEventTime(); + EventTime eventTime = generatePlayingMediaPeriodEventTime(); for (AnalyticsListener listener : listeners) { listener.onDrmSessionReleased(eventTime); } @@ -597,7 +641,8 @@ public class AnalyticsCollector long realtimeMs = clock.elapsedRealtime(); long eventPositionMs; boolean isInCurrentWindow = - timeline == player.getCurrentTimeline() && windowIndex == player.getCurrentWindowIndex(); + timeline.equals(player.getCurrentTimeline()) + && windowIndex == player.getCurrentWindowIndex(); if (mediaPeriodId != null && mediaPeriodId.isAd()) { boolean isCurrentAd = isInCurrentWindow @@ -627,20 +672,17 @@ public class AnalyticsCollector Assertions.checkNotNull(player); if (mediaPeriodInfo == null) { int windowIndex = player.getCurrentWindowIndex(); - mediaPeriodInfo = mediaPeriodQueueTracker.tryResolveWindowIndex(windowIndex); - if (mediaPeriodInfo == null) { - Timeline timeline = player.getCurrentTimeline(); - boolean windowIsInTimeline = windowIndex < timeline.getWindowCount(); - return generateEventTime( - windowIsInTimeline ? timeline : Timeline.EMPTY, windowIndex, /* mediaPeriodId= */ null); - } + Timeline timeline = player.getCurrentTimeline(); + boolean windowIsInTimeline = windowIndex < timeline.getWindowCount(); + return generateEventTime( + windowIsInTimeline ? timeline : Timeline.EMPTY, windowIndex, /* mediaPeriodId= */ null); } return generateEventTime( mediaPeriodInfo.timeline, mediaPeriodInfo.windowIndex, mediaPeriodInfo.mediaPeriodId); } - private EventTime generateLastReportedPlayingMediaPeriodEventTime() { - return generateEventTime(mediaPeriodQueueTracker.getLastReportedPlayingMediaPeriod()); + private EventTime generateCurrentPlayerMediaPeriodEventTime() { + return generateEventTime(mediaPeriodQueueTracker.getCurrentPlayerMediaPeriod()); } private EventTime generatePlayingMediaPeriodEventTime() { @@ -680,11 +722,10 @@ public class AnalyticsCollector private final HashMap mediaPeriodIdToInfo; private final Period period; - @Nullable private MediaPeriodInfo lastPlayingMediaPeriod; - @Nullable private MediaPeriodInfo lastReportedPlayingMediaPeriod; - @Nullable private MediaPeriodInfo readingMediaPeriod; + @Nullable private MediaPeriodInfo currentPlayerMediaPeriod; + private @MonotonicNonNull MediaPeriodInfo playingMediaPeriod; + private @MonotonicNonNull MediaPeriodInfo readingMediaPeriod; private Timeline timeline; - private boolean isSeeking; public MediaPeriodQueueTracker() { mediaPeriodInfoQueue = new ArrayList<>(); @@ -694,34 +735,31 @@ public class AnalyticsCollector } /** - * Returns the {@link MediaPeriodInfo} of the media period in the front of the queue. This is - * the playing media period unless the player hasn't started playing yet (in which case it is - * the loading media period or null). While the player is seeking or preparing, this method will - * always return null to reflect the uncertainty about the current playing period. May also be - * null, if the timeline is empty or no media period is active yet. + * Returns the {@link MediaPeriodInfo} of the media period corresponding the current position of + * the player. + * + *

        May be null if no matching media period has been created yet. */ @Nullable - public MediaPeriodInfo getPlayingMediaPeriod() { - return mediaPeriodInfoQueue.isEmpty() || timeline.isEmpty() || isSeeking - ? null - : mediaPeriodInfoQueue.get(0); + public MediaPeriodInfo getCurrentPlayerMediaPeriod() { + return currentPlayerMediaPeriod; } /** - * Returns the {@link MediaPeriodInfo} of the currently playing media period. This is the - * publicly reported period which should always match {@link Player#getCurrentPeriodIndex()} - * unless the player is currently seeking or being prepared in which case the previous period is - * reported until the seek or preparation is processed. May be null, if no media period is - * active yet. + * Returns the {@link MediaPeriodInfo} of the media period at the front of the queue. If the + * queue is empty, this is the last media period which was at the front of the queue. + * + *

        May be null, if no media period has been created yet. */ @Nullable - public MediaPeriodInfo getLastReportedPlayingMediaPeriod() { - return lastReportedPlayingMediaPeriod; + public MediaPeriodInfo getPlayingMediaPeriod() { + return playingMediaPeriod; } /** * Returns the {@link MediaPeriodInfo} of the media period currently being read by the player. - * May be null, if the player is not reading a media period. + * + *

        May be null, if the player has not started reading any media period. */ @Nullable public MediaPeriodInfo getReadingMediaPeriod() { @@ -730,8 +768,9 @@ public class AnalyticsCollector /** * Returns the {@link MediaPeriodInfo} of the media period at the end of the queue which is - * currently loading or will be the next one loading. May be null, if no media period is active - * yet. + * currently loading or will be the next one loading. + * + *

        May be null, if no media period is active yet. */ @Nullable public MediaPeriodInfo getLoadingMediaPeriod() { @@ -746,74 +785,50 @@ public class AnalyticsCollector return mediaPeriodIdToInfo.get(mediaPeriodId); } - /** Returns whether the player is currently seeking. */ - public boolean isSeeking() { - return isSeeking; - } - - /** - * Tries to find an existing media period info from the specified window index. Only returns a - * non-null media period info if there is a unique, unambiguous match. - */ - @Nullable - public MediaPeriodInfo tryResolveWindowIndex(int windowIndex) { - MediaPeriodInfo match = null; - for (int i = 0; i < mediaPeriodInfoQueue.size(); i++) { - MediaPeriodInfo info = mediaPeriodInfoQueue.get(i); - int periodIndex = timeline.getIndexOfPeriod(info.mediaPeriodId.periodUid); - if (periodIndex != C.INDEX_UNSET - && timeline.getPeriod(periodIndex, period).windowIndex == windowIndex) { - if (match != null) { - // Ambiguous match. - return null; - } - match = info; - } - } - return match; - } - - /** Updates the queue with a reported position discontinuity . */ - public void onPositionDiscontinuity(@Player.DiscontinuityReason int reason) { - lastReportedPlayingMediaPeriod = lastPlayingMediaPeriod; + /** Updates the queue with a reported position discontinuity. */ + public void onPositionDiscontinuity(Player player) { + currentPlayerMediaPeriod = findMatchingMediaPeriodInQueue(player); } /** Updates the queue with a reported timeline change. */ - public void onTimelineChanged(Timeline timeline) { + public void onTimelineChanged(Timeline timeline, Player player) { for (int i = 0; i < mediaPeriodInfoQueue.size(); i++) { MediaPeriodInfo newMediaPeriodInfo = updateMediaPeriodInfoToNewTimeline(mediaPeriodInfoQueue.get(i), timeline); mediaPeriodInfoQueue.set(i, newMediaPeriodInfo); mediaPeriodIdToInfo.put(newMediaPeriodInfo.mediaPeriodId, newMediaPeriodInfo); } + if (!mediaPeriodInfoQueue.isEmpty()) { + playingMediaPeriod = mediaPeriodInfoQueue.get(0); + } else if (playingMediaPeriod != null) { + playingMediaPeriod = updateMediaPeriodInfoToNewTimeline(playingMediaPeriod, timeline); + } if (readingMediaPeriod != null) { readingMediaPeriod = updateMediaPeriodInfoToNewTimeline(readingMediaPeriod, timeline); + } else if (playingMediaPeriod != null) { + readingMediaPeriod = playingMediaPeriod; } this.timeline = timeline; - lastReportedPlayingMediaPeriod = lastPlayingMediaPeriod; - } - - /** Updates the queue with a reported start of seek. */ - public void onSeekStarted() { - isSeeking = true; - } - - /** Updates the queue with a reported processed seek. */ - public void onSeekProcessed() { - isSeeking = false; - lastReportedPlayingMediaPeriod = lastPlayingMediaPeriod; + currentPlayerMediaPeriod = findMatchingMediaPeriodInQueue(player); } /** Updates the queue with a newly created media period. */ - public void onMediaPeriodCreated(int windowIndex, MediaPeriodId mediaPeriodId) { - boolean isInTimeline = timeline.getIndexOfPeriod(mediaPeriodId.periodUid) != C.INDEX_UNSET; + public void onMediaPeriodCreated(int windowIndex, MediaPeriodId mediaPeriodId, Player player) { + int periodIndex = timeline.getIndexOfPeriod(mediaPeriodId.periodUid); + boolean isInTimeline = periodIndex != C.INDEX_UNSET; MediaPeriodInfo mediaPeriodInfo = - new MediaPeriodInfo(mediaPeriodId, isInTimeline ? timeline : Timeline.EMPTY, windowIndex); + new MediaPeriodInfo( + mediaPeriodId, + isInTimeline ? timeline : Timeline.EMPTY, + isInTimeline ? timeline.getPeriod(periodIndex, period).windowIndex : windowIndex); mediaPeriodInfoQueue.add(mediaPeriodInfo); mediaPeriodIdToInfo.put(mediaPeriodId, mediaPeriodInfo); - lastPlayingMediaPeriod = mediaPeriodInfoQueue.get(0); - if (mediaPeriodInfoQueue.size() == 1 && !timeline.isEmpty()) { - lastReportedPlayingMediaPeriod = lastPlayingMediaPeriod; + playingMediaPeriod = mediaPeriodInfoQueue.get(0); + if (currentPlayerMediaPeriod == null && isMatchingPlayingMediaPeriod(player)) { + currentPlayerMediaPeriod = playingMediaPeriod; + } + if (mediaPeriodInfoQueue.size() == 1) { + readingMediaPeriod = playingMediaPeriod; } } @@ -821,25 +836,131 @@ public class AnalyticsCollector * Updates the queue with a released media period. Returns whether the media period was still in * the queue. */ - public boolean onMediaPeriodReleased(MediaPeriodId mediaPeriodId) { - MediaPeriodInfo mediaPeriodInfo = mediaPeriodIdToInfo.remove(mediaPeriodId); + public boolean onMediaPeriodReleased(MediaPeriodId mediaPeriodId, Player player) { + @Nullable MediaPeriodInfo mediaPeriodInfo = mediaPeriodIdToInfo.remove(mediaPeriodId); if (mediaPeriodInfo == null) { - // The media period has already been removed from the queue in resetForNewMediaSource(). + // The media period has already been removed from the queue in resetForNewPlaylist(). return false; } mediaPeriodInfoQueue.remove(mediaPeriodInfo); if (readingMediaPeriod != null && mediaPeriodId.equals(readingMediaPeriod.mediaPeriodId)) { - readingMediaPeriod = mediaPeriodInfoQueue.isEmpty() ? null : mediaPeriodInfoQueue.get(0); + readingMediaPeriod = + mediaPeriodInfoQueue.isEmpty() + ? Assertions.checkNotNull(playingMediaPeriod) + : mediaPeriodInfoQueue.get(0); } if (!mediaPeriodInfoQueue.isEmpty()) { - lastPlayingMediaPeriod = mediaPeriodInfoQueue.get(0); + playingMediaPeriod = mediaPeriodInfoQueue.get(0); + } + if (currentPlayerMediaPeriod == null && isMatchingPlayingMediaPeriod(player)) { + currentPlayerMediaPeriod = playingMediaPeriod; } return true; } /** Update the queue with a change in the reading media period. */ public void onReadingStarted(MediaPeriodId mediaPeriodId) { - readingMediaPeriod = mediaPeriodIdToInfo.get(mediaPeriodId); + @Nullable MediaPeriodInfo mediaPeriodInfo = mediaPeriodIdToInfo.get(mediaPeriodId); + if (mediaPeriodInfo == null) { + // The media period has already been removed from the queue in resetForNewPlaylist(). + return; + } + readingMediaPeriod = mediaPeriodInfo; + } + + @Nullable + private MediaPeriodInfo findMatchingMediaPeriodInQueue(Player player) { + Timeline playerTimeline = player.getCurrentTimeline(); + int playerPeriodIndex = player.getCurrentPeriodIndex(); + @Nullable + Object playerPeriodUid = + playerTimeline.isEmpty() ? null : playerTimeline.getUidOfPeriod(playerPeriodIndex); + int playerNextAdGroupIndex = + player.isPlayingAd() || playerTimeline.isEmpty() + ? C.INDEX_UNSET + : playerTimeline + .getPeriod(playerPeriodIndex, period) + .getAdGroupIndexAfterPositionUs( + C.msToUs(player.getCurrentPosition()) - period.getPositionInWindowUs()); + for (int i = 0; i < mediaPeriodInfoQueue.size(); i++) { + MediaPeriodInfo mediaPeriodInfo = mediaPeriodInfoQueue.get(i); + if (isMatchingMediaPeriod( + mediaPeriodInfo, + playerTimeline, + player.getCurrentWindowIndex(), + playerPeriodUid, + player.isPlayingAd(), + player.getCurrentAdGroupIndex(), + player.getCurrentAdIndexInAdGroup(), + playerNextAdGroupIndex)) { + return mediaPeriodInfo; + } + } + if (mediaPeriodInfoQueue.isEmpty() && playingMediaPeriod != null) { + if (isMatchingMediaPeriod( + playingMediaPeriod, + playerTimeline, + player.getCurrentWindowIndex(), + playerPeriodUid, + player.isPlayingAd(), + player.getCurrentAdGroupIndex(), + player.getCurrentAdIndexInAdGroup(), + playerNextAdGroupIndex)) { + return playingMediaPeriod; + } + } + return null; + } + + private boolean isMatchingPlayingMediaPeriod(Player player) { + if (playingMediaPeriod == null) { + return false; + } + Timeline playerTimeline = player.getCurrentTimeline(); + int playerPeriodIndex = player.getCurrentPeriodIndex(); + @Nullable + Object playerPeriodUid = + playerTimeline.isEmpty() ? null : playerTimeline.getUidOfPeriod(playerPeriodIndex); + int playerNextAdGroupIndex = + player.isPlayingAd() || playerTimeline.isEmpty() + ? C.INDEX_UNSET + : playerTimeline + .getPeriod(playerPeriodIndex, period) + .getAdGroupIndexAfterPositionUs( + C.msToUs(player.getCurrentPosition()) - period.getPositionInWindowUs()); + return isMatchingMediaPeriod( + playingMediaPeriod, + playerTimeline, + player.getCurrentWindowIndex(), + playerPeriodUid, + player.isPlayingAd(), + player.getCurrentAdGroupIndex(), + player.getCurrentAdIndexInAdGroup(), + playerNextAdGroupIndex); + } + + private static boolean isMatchingMediaPeriod( + MediaPeriodInfo mediaPeriodInfo, + Timeline playerTimeline, + int playerWindowIndex, + @Nullable Object playerPeriodUid, + boolean isPlayingAd, + int playerAdGroupIndex, + int playerAdIndexInAdGroup, + int playerNextAdGroupIndex) { + if (mediaPeriodInfo.timeline.isEmpty() + || !mediaPeriodInfo.timeline.equals(playerTimeline) + || mediaPeriodInfo.windowIndex != playerWindowIndex + || !mediaPeriodInfo.mediaPeriodId.periodUid.equals(playerPeriodUid)) { + return false; + } + // Timeline period matches. Still need to check ad information. + return (isPlayingAd + && mediaPeriodInfo.mediaPeriodId.adGroupIndex == playerAdGroupIndex + && mediaPeriodInfo.mediaPeriodId.adIndexInAdGroup == playerAdIndexInAdGroup) + || (!isPlayingAd + && mediaPeriodInfo.mediaPeriodId.adGroupIndex == C.INDEX_UNSET + && mediaPeriodInfo.mediaPeriodId.nextAdGroupIndex == playerNextAdGroupIndex); } private MediaPeriodInfo updateMediaPeriodInfoToNewTimeline( 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 353e7ac340..0b841ab543 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 @@ -124,15 +124,31 @@ public interface AnalyticsListener { } /** - * Called when the player state changed. - * - * @param eventTime The event time. - * @param playWhenReady Whether the playback will proceed when ready. - * @param playbackState The new {@link Player.State playback state}. + * @deprecated Use {@link #onPlaybackStateChanged(EventTime, int)} and {@link + * #onPlayWhenReadyChanged(EventTime, boolean, int)} instead. */ + @Deprecated default void onPlayerStateChanged( EventTime eventTime, boolean playWhenReady, @Player.State int playbackState) {} + /** + * Called when the playback state changed. + * + * @param eventTime The event time. + * @param state The new {@link Player.State playback state}. + */ + default void onPlaybackStateChanged(EventTime eventTime, @Player.State int state) {} + + /** + * Called when the value changed that indicates whether playback will proceed when ready. + * + * @param eventTime The event time. + * @param playWhenReady Whether playback will proceed when ready. + * @param reason The {@link Player.PlayWhenReadyChangeReason reason} of the change. + */ + default void onPlayWhenReadyChanged( + EventTime eventTime, boolean playWhenReady, @Player.PlayWhenReadyChangeReason int reason) {} + /** * Called when playback suppression reason changed. * @@ -174,21 +190,30 @@ public interface AnalyticsListener { default void onSeekStarted(EventTime eventTime) {} /** - * Called when a seek operation was processed. - * - * @param eventTime The event time. + * @deprecated Seeks are processed without delay. Listen to {@link + * #onPositionDiscontinuity(EventTime, int)} with reason {@link + * Player#DISCONTINUITY_REASON_SEEK} instead. */ + @Deprecated default void onSeekProcessed(EventTime eventTime) {} /** - * Called when the playback parameters changed. - * - * @param eventTime The event time. - * @param playbackParameters The new playback parameters. + * @deprecated Use {@link #onPlaybackSpeedChanged(EventTime, float)} and {@link + * #onSkipSilenceEnabledChanged(EventTime, boolean)} instead. */ + @SuppressWarnings("deprecation") + @Deprecated default void onPlaybackParametersChanged( EventTime eventTime, PlaybackParameters playbackParameters) {} + /** + * Called when the playback speed changes. + * + * @param eventTime The event time. + * @param playbackSpeed The playback speed. + */ + default void onPlaybackSpeedChanged(EventTime eventTime, float playbackSpeed) {} + /** * Called when the repeat mode changed. * @@ -211,6 +236,13 @@ public interface AnalyticsListener { * @param eventTime The event time. * @param isLoading Whether the player is loading. */ + @SuppressWarnings("deprecation") + default void onIsLoadingChanged(EventTime eventTime, boolean isLoading) { + onLoadingChanged(eventTime, isLoading); + } + + /** @deprecated Use {@link #onIsLoadingChanged(EventTime, boolean)} instead. */ + @Deprecated default void onLoadingChanged(EventTime eventTime, boolean isLoading) {} /** @@ -427,6 +459,14 @@ public interface AnalyticsListener { default void onAudioUnderrun( EventTime eventTime, int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) {} + /** + * Called when skipping silences is enabled or disabled in the audio stream. + * + * @param eventTime The event time. + * @param skipSilenceEnabled Whether skipping silences in the audio stream is enabled. + */ + default void onSkipSilenceEnabledChanged(EventTime eventTime, boolean skipSilenceEnabled) {} + /** * Called after video frames have been dropped. * @@ -438,6 +478,30 @@ public interface AnalyticsListener { */ default void onDroppedVideoFrames(EventTime eventTime, int droppedFrames, long elapsedMs) {} + /** + * Called when there is an update to the video frame processing offset reported by a video + * renderer. + * + *

        Video processing offset represents how early a video frame is processed compared to the + * player's current position. For each video frame, the offset is calculated as Pvf + * - Ppl where Pvf is the presentation timestamp of the video + * frame and Ppl is the current position of the player. Positive values + * indicate the frame was processed early enough whereas negative values indicate that the + * player's position had progressed beyond the frame's timestamp when the frame was processed (and + * the frame was probably dropped). + * + *

        The renderer reports the sum of video processing offset samples (one sample per processed + * video frame: dropped, skipped or rendered) and the total number of samples (frames). + * + * @param eventTime The event time. + * @param totalProcessingOffsetUs The sum of video frame processing offset samples for all video + * frames processed by the renderer in microseconds. + * @param frameCount The number to samples included in the {@code totalProcessingOffsetUs}. + * @param format The current output {@link Format} rendered by the video renderer. + */ + default void onVideoFrameProcessingOffset( + EventTime eventTime, long totalProcessingOffsetUs, int frameCount, Format format) {} + /** * Called before a frame is rendered for the first time since setting the surface, and each time * there's a change in the size or pixel aspect ratio of the video being rendered. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/analytics/DefaultPlaybackSessionManager.java b/library/core/src/main/java/com/google/android/exoplayer2/analytics/DefaultPlaybackSessionManager.java index 44f8c10afe..04536bb6c1 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/analytics/DefaultPlaybackSessionManager.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/analytics/DefaultPlaybackSessionManager.java @@ -24,35 +24,52 @@ import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.analytics.AnalyticsListener.EventTime; import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.Supplier; import com.google.android.exoplayer2.util.Util; import java.util.HashMap; import java.util.Iterator; import java.util.Random; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; -import org.checkerframework.checker.nullness.qual.RequiresNonNull; /** * Default {@link PlaybackSessionManager} which instantiates a new session for each window in the * timeline and also for each ad within the windows. * - *

        Sessions are identified by Base64-encoded, URL-safe, random strings. + *

        By default, sessions are identified by Base64-encoded, URL-safe, random strings. */ public final class DefaultPlaybackSessionManager implements PlaybackSessionManager { + /** Default generator for unique session ids that are random, Based64-encoded and URL-safe. */ + public static final Supplier DEFAULT_SESSION_ID_GENERATOR = + DefaultPlaybackSessionManager::generateDefaultSessionId; + private static final Random RANDOM = new Random(); private static final int SESSION_ID_LENGTH = 12; private final Timeline.Window window; private final Timeline.Period period; private final HashMap sessions; + private final Supplier sessionIdGenerator; private @MonotonicNonNull Listener listener; private Timeline currentTimeline; - @Nullable private MediaPeriodId currentMediaPeriodId; - @Nullable private String activeSessionId; + @Nullable private String currentSessionId; - /** Creates session manager. */ + /** + * Creates session manager with a {@link #DEFAULT_SESSION_ID_GENERATOR} to generate session ids. + */ public DefaultPlaybackSessionManager() { + this(DEFAULT_SESSION_ID_GENERATOR); + } + + /** + * Creates session manager. + * + * @param sessionIdGenerator A generator for new session ids. All generated session ids must be + * unique. + */ + public DefaultPlaybackSessionManager(Supplier sessionIdGenerator) { + this.sessionIdGenerator = sessionIdGenerator; window = new Timeline.Window(); period = new Timeline.Period(); sessions = new HashMap<>(); @@ -83,22 +100,34 @@ public final class DefaultPlaybackSessionManager implements PlaybackSessionManag @Override public synchronized void updateSessions(EventTime eventTime) { - boolean isObviouslyFinished = - eventTime.mediaPeriodId != null - && currentMediaPeriodId != null - && eventTime.mediaPeriodId.windowSequenceNumber - < currentMediaPeriodId.windowSequenceNumber; - if (!isObviouslyFinished) { - SessionDescriptor descriptor = - getOrAddSession(eventTime.windowIndex, eventTime.mediaPeriodId); - if (!descriptor.isCreated) { - descriptor.isCreated = true; - Assertions.checkNotNull(listener).onSessionCreated(eventTime, descriptor.sessionId); - if (activeSessionId == null) { - updateActiveSession(eventTime, descriptor); - } + Assertions.checkNotNull(listener); + @Nullable SessionDescriptor currentSession = sessions.get(currentSessionId); + if (eventTime.mediaPeriodId != null && currentSession != null) { + // If we receive an event associated with a media period, then it needs to be either part of + // the current window if it's the first created media period, or a window that will be played + // in the future. Otherwise, we know that it belongs to a session that was already finished + // and we can ignore the event. + boolean isAlreadyFinished = + currentSession.windowSequenceNumber == C.INDEX_UNSET + ? currentSession.windowIndex != eventTime.windowIndex + : eventTime.mediaPeriodId.windowSequenceNumber < currentSession.windowSequenceNumber; + if (isAlreadyFinished) { + return; } } + SessionDescriptor eventSession = + getOrAddSession(eventTime.windowIndex, eventTime.mediaPeriodId); + if (currentSessionId == null) { + currentSessionId = eventSession.sessionId; + } + if (!eventSession.isCreated) { + eventSession.isCreated = true; + listener.onSessionCreated(eventTime, eventSession.sessionId); + } + if (eventSession.sessionId.equals(currentSessionId) && !eventSession.isActive) { + eventSession.isActive = true; + listener.onSessionActive(eventTime, eventSession.sessionId); + } } @Override @@ -112,8 +141,8 @@ public final class DefaultPlaybackSessionManager implements PlaybackSessionManag if (!session.tryResolvingToNewTimeline(previousTimeline, currentTimeline)) { iterator.remove(); if (session.isCreated) { - if (session.sessionId.equals(activeSessionId)) { - activeSessionId = null; + if (session.sessionId.equals(currentSessionId)) { + currentSessionId = null; } listener.onSessionFinished( eventTime, session.sessionId, /* automaticTransitionToNextPlayback= */ false); @@ -136,36 +165,55 @@ public final class DefaultPlaybackSessionManager implements PlaybackSessionManag if (session.isFinishedAtEventTime(eventTime)) { iterator.remove(); if (session.isCreated) { - boolean isRemovingActiveSession = session.sessionId.equals(activeSessionId); - boolean isAutomaticTransition = hasAutomaticTransition && isRemovingActiveSession; - if (isRemovingActiveSession) { - activeSessionId = null; + boolean isRemovingCurrentSession = session.sessionId.equals(currentSessionId); + boolean isAutomaticTransition = + hasAutomaticTransition && isRemovingCurrentSession && session.isActive; + if (isRemovingCurrentSession) { + currentSessionId = null; } listener.onSessionFinished(eventTime, session.sessionId, isAutomaticTransition); } } } - SessionDescriptor activeSessionDescriptor = + @Nullable SessionDescriptor previousSessionDescriptor = sessions.get(currentSessionId); + SessionDescriptor currentSessionDescriptor = getOrAddSession(eventTime.windowIndex, eventTime.mediaPeriodId); + currentSessionId = currentSessionDescriptor.sessionId; if (eventTime.mediaPeriodId != null && eventTime.mediaPeriodId.isAd() - && (currentMediaPeriodId == null - || currentMediaPeriodId.windowSequenceNumber + && (previousSessionDescriptor == null + || previousSessionDescriptor.windowSequenceNumber != eventTime.mediaPeriodId.windowSequenceNumber - || currentMediaPeriodId.adGroupIndex != eventTime.mediaPeriodId.adGroupIndex - || currentMediaPeriodId.adIndexInAdGroup != eventTime.mediaPeriodId.adIndexInAdGroup)) { + || previousSessionDescriptor.adMediaPeriodId == null + || previousSessionDescriptor.adMediaPeriodId.adGroupIndex + != eventTime.mediaPeriodId.adGroupIndex + || previousSessionDescriptor.adMediaPeriodId.adIndexInAdGroup + != eventTime.mediaPeriodId.adIndexInAdGroup)) { // New ad playback started. Find corresponding content session and notify ad playback started. MediaPeriodId contentMediaPeriodId = new MediaPeriodId( eventTime.mediaPeriodId.periodUid, eventTime.mediaPeriodId.windowSequenceNumber); SessionDescriptor contentSession = getOrAddSession(eventTime.windowIndex, contentMediaPeriodId); - if (contentSession.isCreated && activeSessionDescriptor.isCreated) { + if (contentSession.isCreated && currentSessionDescriptor.isCreated) { listener.onAdPlaybackStarted( - eventTime, contentSession.sessionId, activeSessionDescriptor.sessionId); + eventTime, contentSession.sessionId, currentSessionDescriptor.sessionId); + } + } + } + + @Override + public void finishAllSessions(EventTime eventTime) { + currentSessionId = null; + Iterator iterator = sessions.values().iterator(); + while (iterator.hasNext()) { + SessionDescriptor session = iterator.next(); + iterator.remove(); + if (session.isCreated && listener != null) { + listener.onSessionFinished( + eventTime, session.sessionId, /* automaticTransitionToNextPlayback= */ false); } } - updateActiveSession(eventTime, activeSessionDescriptor); } private SessionDescriptor getOrAddSession( @@ -192,26 +240,14 @@ public final class DefaultPlaybackSessionManager implements PlaybackSessionManag } } if (bestMatch == null) { - String sessionId = generateSessionId(); + String sessionId = sessionIdGenerator.get(); bestMatch = new SessionDescriptor(sessionId, windowIndex, mediaPeriodId); sessions.put(sessionId, bestMatch); } return bestMatch; } - @RequiresNonNull("listener") - private void updateActiveSession(EventTime eventTime, SessionDescriptor sessionDescriptor) { - currentMediaPeriodId = eventTime.mediaPeriodId; - if (sessionDescriptor.isCreated) { - activeSessionId = sessionDescriptor.sessionId; - if (!sessionDescriptor.isActive) { - sessionDescriptor.isActive = true; - listener.onSessionActive(eventTime, sessionDescriptor.sessionId); - } - } - } - - private static String generateSessionId() { + private static String generateDefaultSessionId() { byte[] randomBytes = new byte[SESSION_ID_LENGTH]; RANDOM.nextBytes(randomBytes); return Base64.encodeToString(randomBytes, Base64.URL_SAFE | Base64.NO_WRAP); @@ -284,8 +320,7 @@ public final class DefaultPlaybackSessionManager implements PlaybackSessionManag int eventWindowIndex, @Nullable MediaPeriodId eventMediaPeriodId) { if (windowSequenceNumber == C.INDEX_UNSET && eventWindowIndex == windowIndex - && eventMediaPeriodId != null - && !eventMediaPeriodId.isAd()) { + && eventMediaPeriodId != null) { // Set window sequence number for this session as soon as we have one. windowSequenceNumber = eventMediaPeriodId.windowSequenceNumber; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/analytics/PlaybackSessionManager.java b/library/core/src/main/java/com/google/android/exoplayer2/analytics/PlaybackSessionManager.java index 53d63e23fc..7045779125 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/analytics/PlaybackSessionManager.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/analytics/PlaybackSessionManager.java @@ -117,4 +117,12 @@ public interface PlaybackSessionManager { * @param reason The {@link DiscontinuityReason}. */ void handlePositionDiscontinuity(EventTime eventTime, @DiscontinuityReason int reason); + + /** + * Finishes all existing sessions and calls their respective {@link + * Listener#onSessionFinished(EventTime, String, boolean)} callback. + * + * @param eventTime The event time at which sessions are finished. + */ + void finishAllSessions(EventTime eventTime); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/analytics/PlaybackStats.java b/library/core/src/main/java/com/google/android/exoplayer2/analytics/PlaybackStats.java index b370c893de..3b1e0567cb 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/analytics/PlaybackStats.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/analytics/PlaybackStats.java @@ -16,8 +16,8 @@ package com.google.android.exoplayer2.analytics; import android.os.SystemClock; -import android.util.Pair; import androidx.annotation.IntDef; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.analytics.AnalyticsListener.EventTime; @@ -28,19 +28,143 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import java.util.Collections; import java.util.List; -import org.checkerframework.checker.nullness.compatqual.NullableType; /** Statistics about playbacks. */ public final class PlaybackStats { + /** Stores a playback state with the event time at which it became active. */ + public static final class EventTimeAndPlaybackState { + /** The event time at which the playback state became active. */ + public final EventTime eventTime; + /** The playback state that became active. */ + public final @PlaybackState int playbackState; + + /** + * Creates a new timed playback state event. + * + * @param eventTime The event time at which the playback state became active. + * @param playbackState The playback state that became active. + */ + public EventTimeAndPlaybackState(EventTime eventTime, @PlaybackState int playbackState) { + this.eventTime = eventTime; + this.playbackState = playbackState; + } + + @Override + public boolean equals(@Nullable Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + EventTimeAndPlaybackState that = (EventTimeAndPlaybackState) o; + if (playbackState != that.playbackState) { + return false; + } + return eventTime.equals(that.eventTime); + } + + @Override + public int hashCode() { + int result = eventTime.hashCode(); + result = 31 * result + playbackState; + return result; + } + } + + /** + * Stores a format with the event time at which it started being used, or {@code null} to indicate + * that no format was used. + */ + public static final class EventTimeAndFormat { + /** The event time associated with {@link #format}. */ + public final EventTime eventTime; + /** The format that started being used, or {@code null} if no format was used. */ + @Nullable public final Format format; + + /** + * Creates a new timed format event. + * + * @param eventTime The event time associated with {@code format}. + * @param format The format that started being used, or {@code null} if no format was used. + */ + public EventTimeAndFormat(EventTime eventTime, @Nullable Format format) { + this.eventTime = eventTime; + this.format = format; + } + + @Override + public boolean equals(@Nullable Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + EventTimeAndFormat that = (EventTimeAndFormat) o; + if (!eventTime.equals(that.eventTime)) { + return false; + } + return format != null ? format.equals(that.format) : that.format == null; + } + + @Override + public int hashCode() { + int result = eventTime.hashCode(); + result = 31 * result + (format != null ? format.hashCode() : 0); + return result; + } + } + + /** Stores an exception with the event time at which it occurred. */ + public static final class EventTimeAndException { + /** The event time at which the exception occurred. */ + public final EventTime eventTime; + /** The exception that was thrown. */ + public final Exception exception; + + /** + * Creates a new timed exception event. + * + * @param eventTime The event time at which the exception occurred. + * @param exception The exception that was thrown. + */ + public EventTimeAndException(EventTime eventTime, Exception exception) { + this.eventTime = eventTime; + this.exception = exception; + } + + @Override + public boolean equals(@Nullable Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + EventTimeAndException that = (EventTimeAndException) o; + if (!eventTime.equals(that.eventTime)) { + return false; + } + return exception.equals(that.exception); + } + + @Override + public int hashCode() { + int result = eventTime.hashCode(); + result = 31 * result + exception.hashCode(); + return result; + } + } + /** * State of a playback. One of {@link #PLAYBACK_STATE_NOT_STARTED}, {@link * #PLAYBACK_STATE_JOINING_FOREGROUND}, {@link #PLAYBACK_STATE_JOINING_BACKGROUND}, {@link * #PLAYBACK_STATE_PLAYING}, {@link #PLAYBACK_STATE_PAUSED}, {@link #PLAYBACK_STATE_SEEKING}, * {@link #PLAYBACK_STATE_BUFFERING}, {@link #PLAYBACK_STATE_PAUSED_BUFFERING}, {@link - * #PLAYBACK_STATE_SEEK_BUFFERING}, {@link #PLAYBACK_STATE_SUPPRESSED}, {@link - * #PLAYBACK_STATE_SUPPRESSED_BUFFERING}, {@link #PLAYBACK_STATE_ENDED}, {@link - * #PLAYBACK_STATE_STOPPED}, {@link #PLAYBACK_STATE_FAILED}, {@link + * #PLAYBACK_STATE_SUPPRESSED}, {@link #PLAYBACK_STATE_SUPPRESSED_BUFFERING}, {@link + * #PLAYBACK_STATE_ENDED}, {@link #PLAYBACK_STATE_STOPPED}, {@link #PLAYBACK_STATE_FAILED}, {@link * #PLAYBACK_STATE_INTERRUPTED_BY_AD} or {@link #PLAYBACK_STATE_ABANDONED}. */ @Documented @@ -55,7 +179,6 @@ public final class PlaybackStats { PLAYBACK_STATE_SEEKING, PLAYBACK_STATE_BUFFERING, PLAYBACK_STATE_PAUSED_BUFFERING, - PLAYBACK_STATE_SEEK_BUFFERING, PLAYBACK_STATE_SUPPRESSED, PLAYBACK_STATE_SUPPRESSED_BUFFERING, PLAYBACK_STATE_ENDED, @@ -81,8 +204,6 @@ public final class PlaybackStats { public static final int PLAYBACK_STATE_BUFFERING = 6; /** Playback is buffering while paused. */ public static final int PLAYBACK_STATE_PAUSED_BUFFERING = 7; - /** Playback is buffering after a seek. */ - public static final int PLAYBACK_STATE_SEEK_BUFFERING = 8; /** Playback is suppressed (e.g. due to audio focus loss). */ public static final int PLAYBACK_STATE_SUPPRESSED = 9; /** Playback is suppressed (e.g. due to audio focus loss) while buffering to resume a playback. */ @@ -258,10 +379,10 @@ public final class PlaybackStats { // Playback state stats. /** - * The playback state history as ordered pairs of the {@link EventTime} at which a state became - * active and the {@link PlaybackState}. + * The playback state history as {@link EventTimeAndPlaybackState EventTimeAndPlaybackStates} + * ordered by {@code EventTime.realTimeMs}. */ - public final List> playbackStateHistory; + public final List playbackStateHistory; /** * The media time history as an ordered list of long[2] arrays with [0] being the realtime as * returned by {@code SystemClock.elapsedRealtime()} and [1] being the media time at this @@ -319,15 +440,15 @@ public final class PlaybackStats { // Format stats. /** - * The video format history as ordered pairs of the {@link EventTime} at which a format started - * being used and the {@link Format}. The {@link Format} may be null if no video format was used. + * The video format history as {@link EventTimeAndFormat EventTimeAndFormats} ordered by {@code + * EventTime.realTimeMs}. The {@link Format} may be null if no video format was used. */ - public final List> videoFormatHistory; + public final List videoFormatHistory; /** - * The audio format history as ordered pairs of the {@link EventTime} at which a format started - * being used and the {@link Format}. The {@link Format} may be null if no audio format was used. + * The audio format history as {@link EventTimeAndFormat EventTimeAndFormats} ordered by {@code + * EventTime.realTimeMs}. The {@link Format} may be null if no audio format was used. */ - public final List> audioFormatHistory; + public final List audioFormatHistory; /** The total media time for which video format height data is available, in milliseconds. */ public final long totalVideoFormatHeightTimeMs; /** @@ -400,23 +521,23 @@ public final class PlaybackStats { */ public final int nonFatalErrorCount; /** - * The history of fatal errors as ordered pairs of the {@link EventTime} at which an error - * occurred and the error. Errors are fatal if playback stopped due to this error. + * The history of fatal errors as {@link EventTimeAndException EventTimeAndExceptions} ordered by + * {@code EventTime.realTimeMs}. Errors are fatal if playback stopped due to this error. */ - public final List> fatalErrorHistory; + public final List fatalErrorHistory; /** - * The history of non-fatal errors as ordered pairs of the {@link EventTime} at which an error - * occurred and the error. Error are non-fatal if playback can recover from the error without - * stopping. + * The history of non-fatal errors as {@link EventTimeAndException EventTimeAndExceptions} ordered + * by {@code EventTime.realTimeMs}. Errors are non-fatal if playback can recover from the error + * without stopping. */ - public final List> nonFatalErrorHistory; + public final List nonFatalErrorHistory; private final long[] playbackStateDurationsMs; /* package */ PlaybackStats( int playbackCount, long[] playbackStateDurationsMs, - List> playbackStateHistory, + List playbackStateHistory, List mediaTimeHistory, long firstReportedTimeMs, int foregroundPlaybackCount, @@ -431,8 +552,8 @@ public final class PlaybackStats { int totalRebufferCount, long maxRebufferTimeMs, int adPlaybackCount, - List> videoFormatHistory, - List> audioFormatHistory, + List videoFormatHistory, + List audioFormatHistory, long totalVideoFormatHeightTimeMs, long totalVideoFormatHeightTimeProduct, long totalVideoFormatBitrateTimeMs, @@ -452,8 +573,8 @@ public final class PlaybackStats { int fatalErrorPlaybackCount, int fatalErrorCount, int nonFatalErrorCount, - List> fatalErrorHistory, - List> nonFatalErrorHistory) { + List fatalErrorHistory, + List nonFatalErrorHistory) { this.playbackCount = playbackCount; this.playbackStateDurationsMs = playbackStateDurationsMs; this.playbackStateHistory = Collections.unmodifiableList(playbackStateHistory); @@ -515,11 +636,11 @@ public final class PlaybackStats { */ public @PlaybackState int getPlaybackStateAtTime(long realtimeMs) { @PlaybackState int state = PLAYBACK_STATE_NOT_STARTED; - for (Pair timeAndState : playbackStateHistory) { - if (timeAndState.first.realtimeMs > realtimeMs) { + for (EventTimeAndPlaybackState timeAndState : playbackStateHistory) { + if (timeAndState.eventTime.realtimeMs > realtimeMs) { break; } - state = timeAndState.second; + state = timeAndState.playbackState; } return state; } @@ -644,8 +765,7 @@ public final class PlaybackStats { * milliseconds. */ public long getTotalSeekTimeMs() { - return getPlaybackStateDurationMs(PLAYBACK_STATE_SEEKING) - + getPlaybackStateDurationMs(PLAYBACK_STATE_SEEK_BUFFERING); + return getPlaybackStateDurationMs(PLAYBACK_STATE_SEEKING); } /** @@ -674,8 +794,7 @@ public final class PlaybackStats { public long getTotalWaitTimeMs() { return getPlaybackStateDurationMs(PLAYBACK_STATE_JOINING_FOREGROUND) + getPlaybackStateDurationMs(PLAYBACK_STATE_BUFFERING) - + getPlaybackStateDurationMs(PLAYBACK_STATE_SEEKING) - + getPlaybackStateDurationMs(PLAYBACK_STATE_SEEK_BUFFERING); + + getPlaybackStateDurationMs(PLAYBACK_STATE_SEEKING); } /** diff --git a/library/core/src/main/java/com/google/android/exoplayer2/analytics/PlaybackStatsListener.java b/library/core/src/main/java/com/google/android/exoplayer2/analytics/PlaybackStatsListener.java index 2f8ca3a8cc..0524f4d3b1 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/analytics/PlaybackStatsListener.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/analytics/PlaybackStatsListener.java @@ -16,15 +16,16 @@ package com.google.android.exoplayer2.analytics; import android.os.SystemClock; -import android.util.Pair; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.Format; -import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.Timeline.Period; +import com.google.android.exoplayer2.analytics.PlaybackStats.EventTimeAndException; +import com.google.android.exoplayer2.analytics.PlaybackStats.EventTimeAndFormat; +import com.google.android.exoplayer2.analytics.PlaybackStats.EventTimeAndPlaybackState; import com.google.android.exoplayer2.analytics.PlaybackStats.PlaybackState; import com.google.android.exoplayer2.source.LoadEventInfo; import com.google.android.exoplayer2.source.MediaLoadData; @@ -42,7 +43,6 @@ import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; -import org.checkerframework.checker.nullness.compatqual.NullableType; /** * {@link AnalyticsListener} to gather {@link PlaybackStats} from the player. @@ -50,7 +50,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; *

        For accurate measurements, the listener should be added to the player before loading media, * i.e., {@link Player#getPlaybackState()} should be {@link Player#STATE_IDLE}. * - *

        Playback stats are gathered separately for all playback session, i.e. each window in the + *

        Playback stats are gathered separately for each playback session, i.e. each window in the * {@link Timeline} and each single ad. */ public final class PlaybackStatsListener @@ -83,6 +83,7 @@ public final class PlaybackStatsListener @Player.State private int playbackState; private boolean isSuppressed; private float playbackSpeed; + private boolean onSeekStartedCalled; /** * Creates listener for playback stats. @@ -131,6 +132,7 @@ public final class PlaybackStatsListener */ @Nullable public PlaybackStats getPlaybackStats() { + @Nullable PlaybackStatsTracker activeStatsTracker = activeAdPlayback != null ? playbackStatsTrackers.get(activeAdPlayback) @@ -148,7 +150,6 @@ public final class PlaybackStatsListener // TODO: Add AnalyticsListener.onAttachedToPlayer and onDetachedFromPlayer to auto-release with // an actual EventTime. Should also simplify other cases where the listener needs to be released // separately from the player. - HashMap trackerCopy = new HashMap<>(playbackStatsTrackers); EventTime dummyEventTime = new EventTime( SystemClock.elapsedRealtime(), @@ -158,9 +159,7 @@ public final class PlaybackStatsListener /* eventPlaybackPositionMs= */ 0, /* currentPlaybackPositionMs= */ 0, /* totalBufferedDurationMs= */ 0); - for (String session : trackerCopy.keySet()) { - onSessionFinished(dummyEventTime, session, /* automaticTransition= */ false); - } + sessionManager.finishAllSessions(dummyEventTime); } // PlaybackSessionManager.Listener implementation. @@ -168,8 +167,11 @@ public final class PlaybackStatsListener @Override public void onSessionCreated(EventTime eventTime, String session) { PlaybackStatsTracker tracker = new PlaybackStatsTracker(keepHistory, eventTime); - tracker.onPlayerStateChanged( - eventTime, playWhenReady, playbackState, /* belongsToPlayback= */ true); + if (onSeekStartedCalled) { + tracker.onSeekStarted(eventTime, /* belongsToPlayback= */ true); + } + tracker.onPlaybackStateChanged(eventTime, playbackState, /* belongsToPlayback= */ true); + tracker.onPlayWhenReadyChanged(eventTime, playWhenReady, /* belongsToPlayback= */ true); tracker.onIsSuppressedChanged(eventTime, isSuppressed, /* belongsToPlayback= */ true); tracker.onPlaybackSpeedChanged(eventTime, playbackSpeed); playbackStatsTrackers.put(session, tracker); @@ -189,11 +191,15 @@ public final class PlaybackStatsListener @Override public void onAdPlaybackStarted(EventTime eventTime, String contentSession, String adSession) { Assertions.checkState(Assertions.checkNotNull(eventTime.mediaPeriodId).isAd()); - long contentPositionUs = + long contentPeriodPositionUs = eventTime .timeline .getPeriodByUid(eventTime.mediaPeriodId.periodUid, period) .getAdGroupTimeUs(eventTime.mediaPeriodId.adGroupIndex); + long contentWindowPositionUs = + contentPeriodPositionUs == C.TIME_END_OF_SOURCE + ? C.TIME_END_OF_SOURCE + : contentPeriodPositionUs + period.getPositionInWindowUs(); EventTime contentEventTime = new EventTime( eventTime.realtimeMs, @@ -203,7 +209,7 @@ public final class PlaybackStatsListener eventTime.mediaPeriodId.periodUid, eventTime.mediaPeriodId.windowSequenceNumber, eventTime.mediaPeriodId.adGroupIndex), - /* eventPlaybackPositionMs= */ C.usToMs(contentPositionUs), + /* eventPlaybackPositionMs= */ C.usToMs(contentWindowPositionUs), eventTime.currentPlaybackPositionMs, eventTime.totalBufferedDurationMs); Assertions.checkNotNull(playbackStatsTrackers.get(contentSession)) @@ -221,8 +227,7 @@ public final class PlaybackStatsListener EventTime startEventTime = Assertions.checkNotNull(sessionStartEventTimes.remove(session)); if (automaticTransition) { // Simulate ENDED state to record natural ending of playback. - tracker.onPlayerStateChanged( - eventTime, /* playWhenReady= */ true, Player.STATE_ENDED, /* belongsToPlayback= */ false); + tracker.onPlaybackStateChanged(eventTime, Player.STATE_ENDED, /* belongsToPlayback= */ false); } tracker.onFinished(eventTime); PlaybackStats playbackStats = tracker.build(/* isFinal= */ true); @@ -235,24 +240,35 @@ public final class PlaybackStatsListener // AnalyticsListener implementation. @Override - public void onPlayerStateChanged( - EventTime eventTime, boolean playWhenReady, @Player.State int playbackState) { - this.playWhenReady = playWhenReady; - this.playbackState = playbackState; - sessionManager.updateSessions(eventTime); + public void onPlaybackStateChanged(EventTime eventTime, @Player.State int state) { + playbackState = state; + maybeAddSession(eventTime); for (String session : playbackStatsTrackers.keySet()) { boolean belongsToPlayback = sessionManager.belongsToSession(eventTime, session); playbackStatsTrackers .get(session) - .onPlayerStateChanged(eventTime, playWhenReady, playbackState, belongsToPlayback); + .onPlaybackStateChanged(eventTime, playbackState, belongsToPlayback); + } + } + + @Override + public void onPlayWhenReadyChanged( + EventTime eventTime, boolean playWhenReady, @Player.PlayWhenReadyChangeReason int reason) { + this.playWhenReady = playWhenReady; + maybeAddSession(eventTime); + for (String session : playbackStatsTrackers.keySet()) { + boolean belongsToPlayback = sessionManager.belongsToSession(eventTime, session); + playbackStatsTrackers + .get(session) + .onPlayWhenReadyChanged(eventTime, playWhenReady, belongsToPlayback); } } @Override public void onPlaybackSuppressionReasonChanged( - EventTime eventTime, int playbackSuppressionReason) { + EventTime eventTime, @Player.PlaybackSuppressionReason int playbackSuppressionReason) { isSuppressed = playbackSuppressionReason != Player.PLAYBACK_SUPPRESSION_REASON_NONE; - sessionManager.updateSessions(eventTime); + maybeAddSession(eventTime); for (String session : playbackStatsTrackers.keySet()) { boolean belongsToPlayback = sessionManager.belongsToSession(eventTime, session); playbackStatsTrackers @@ -262,50 +278,46 @@ public final class PlaybackStatsListener } @Override - public void onTimelineChanged(EventTime eventTime, int reason) { + public void onTimelineChanged(EventTime eventTime, @Player.TimelineChangeReason int reason) { sessionManager.handleTimelineUpdate(eventTime); - sessionManager.updateSessions(eventTime); + maybeAddSession(eventTime); for (String session : playbackStatsTrackers.keySet()) { if (sessionManager.belongsToSession(eventTime, session)) { - playbackStatsTrackers.get(session).onPositionDiscontinuity(eventTime); + playbackStatsTrackers.get(session).onPositionDiscontinuity(eventTime, /* isSeek= */ false); } } } @Override - public void onPositionDiscontinuity(EventTime eventTime, int reason) { + public void onPositionDiscontinuity(EventTime eventTime, @Player.DiscontinuityReason int reason) { sessionManager.handlePositionDiscontinuity(eventTime, reason); - sessionManager.updateSessions(eventTime); + maybeAddSession(eventTime); + if (reason == Player.DISCONTINUITY_REASON_SEEK) { + onSeekStartedCalled = false; + } for (String session : playbackStatsTrackers.keySet()) { if (sessionManager.belongsToSession(eventTime, session)) { - playbackStatsTrackers.get(session).onPositionDiscontinuity(eventTime); + playbackStatsTrackers + .get(session) + .onPositionDiscontinuity( + eventTime, /* isSeek= */ reason == Player.DISCONTINUITY_REASON_SEEK); } } } @Override public void onSeekStarted(EventTime eventTime) { - sessionManager.updateSessions(eventTime); + maybeAddSession(eventTime); for (String session : playbackStatsTrackers.keySet()) { - if (sessionManager.belongsToSession(eventTime, session)) { - playbackStatsTrackers.get(session).onSeekStarted(eventTime); - } - } - } - - @Override - public void onSeekProcessed(EventTime eventTime) { - sessionManager.updateSessions(eventTime); - for (String session : playbackStatsTrackers.keySet()) { - if (sessionManager.belongsToSession(eventTime, session)) { - playbackStatsTrackers.get(session).onSeekProcessed(eventTime); - } + boolean belongsToPlayback = sessionManager.belongsToSession(eventTime, session); + playbackStatsTrackers.get(session).onSeekStarted(eventTime, belongsToPlayback); } + onSeekStartedCalled = true; } @Override public void onPlayerError(EventTime eventTime, ExoPlaybackException error) { - sessionManager.updateSessions(eventTime); + maybeAddSession(eventTime); for (String session : playbackStatsTrackers.keySet()) { if (sessionManager.belongsToSession(eventTime, session)) { playbackStatsTrackers.get(session).onFatalError(eventTime, error); @@ -314,10 +326,9 @@ public final class PlaybackStatsListener } @Override - public void onPlaybackParametersChanged( - EventTime eventTime, PlaybackParameters playbackParameters) { - playbackSpeed = playbackParameters.speed; - sessionManager.updateSessions(eventTime); + public void onPlaybackSpeedChanged(EventTime eventTime, float playbackSpeed) { + this.playbackSpeed = playbackSpeed; + maybeAddSession(eventTime); for (PlaybackStatsTracker tracker : playbackStatsTrackers.values()) { tracker.onPlaybackSpeedChanged(eventTime, playbackSpeed); } @@ -326,7 +337,7 @@ public final class PlaybackStatsListener @Override public void onTracksChanged( EventTime eventTime, TrackGroupArray trackGroups, TrackSelectionArray trackSelections) { - sessionManager.updateSessions(eventTime); + maybeAddSession(eventTime); for (String session : playbackStatsTrackers.keySet()) { if (sessionManager.belongsToSession(eventTime, session)) { playbackStatsTrackers.get(session).onTracksChanged(eventTime, trackSelections); @@ -337,7 +348,7 @@ public final class PlaybackStatsListener @Override public void onLoadStarted( EventTime eventTime, LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData) { - sessionManager.updateSessions(eventTime); + maybeAddSession(eventTime); for (String session : playbackStatsTrackers.keySet()) { if (sessionManager.belongsToSession(eventTime, session)) { playbackStatsTrackers.get(session).onLoadStarted(eventTime); @@ -347,7 +358,7 @@ public final class PlaybackStatsListener @Override public void onDownstreamFormatChanged(EventTime eventTime, MediaLoadData mediaLoadData) { - sessionManager.updateSessions(eventTime); + maybeAddSession(eventTime); for (String session : playbackStatsTrackers.keySet()) { if (sessionManager.belongsToSession(eventTime, session)) { playbackStatsTrackers.get(session).onDownstreamFormatChanged(eventTime, mediaLoadData); @@ -362,7 +373,7 @@ public final class PlaybackStatsListener int height, int unappliedRotationDegrees, float pixelWidthHeightRatio) { - sessionManager.updateSessions(eventTime); + maybeAddSession(eventTime); for (String session : playbackStatsTrackers.keySet()) { if (sessionManager.belongsToSession(eventTime, session)) { playbackStatsTrackers.get(session).onVideoSizeChanged(eventTime, width, height); @@ -373,7 +384,7 @@ public final class PlaybackStatsListener @Override public void onBandwidthEstimate( EventTime eventTime, int totalLoadTimeMs, long totalBytesLoaded, long bitrateEstimate) { - sessionManager.updateSessions(eventTime); + maybeAddSession(eventTime); for (String session : playbackStatsTrackers.keySet()) { if (sessionManager.belongsToSession(eventTime, session)) { playbackStatsTrackers.get(session).onBandwidthData(totalLoadTimeMs, totalBytesLoaded); @@ -384,7 +395,7 @@ public final class PlaybackStatsListener @Override public void onAudioUnderrun( EventTime eventTime, int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) { - sessionManager.updateSessions(eventTime); + maybeAddSession(eventTime); for (String session : playbackStatsTrackers.keySet()) { if (sessionManager.belongsToSession(eventTime, session)) { playbackStatsTrackers.get(session).onAudioUnderrun(); @@ -394,7 +405,7 @@ public final class PlaybackStatsListener @Override public void onDroppedVideoFrames(EventTime eventTime, int droppedFrames, long elapsedMs) { - sessionManager.updateSessions(eventTime); + maybeAddSession(eventTime); for (String session : playbackStatsTrackers.keySet()) { if (sessionManager.belongsToSession(eventTime, session)) { playbackStatsTrackers.get(session).onDroppedVideoFrames(droppedFrames); @@ -409,7 +420,7 @@ public final class PlaybackStatsListener MediaLoadData mediaLoadData, IOException error, boolean wasCanceled) { - sessionManager.updateSessions(eventTime); + maybeAddSession(eventTime); for (String session : playbackStatsTrackers.keySet()) { if (sessionManager.belongsToSession(eventTime, session)) { playbackStatsTrackers.get(session).onNonFatalError(eventTime, error); @@ -419,7 +430,7 @@ public final class PlaybackStatsListener @Override public void onDrmSessionManagerError(EventTime eventTime, Exception error) { - sessionManager.updateSessions(eventTime); + maybeAddSession(eventTime); for (String session : playbackStatsTrackers.keySet()) { if (sessionManager.belongsToSession(eventTime, session)) { playbackStatsTrackers.get(session).onNonFatalError(eventTime, error); @@ -427,18 +438,25 @@ public final class PlaybackStatsListener } } + private void maybeAddSession(EventTime eventTime) { + boolean isCompletelyIdle = eventTime.timeline.isEmpty() && playbackState == Player.STATE_IDLE; + if (!isCompletelyIdle) { + sessionManager.updateSessions(eventTime); + } + } + /** Tracker for playback stats of a single playback. */ private static final class PlaybackStatsTracker { // Final stats. private final boolean keepHistory; private final long[] playbackStateDurationsMs; - private final List> playbackStateHistory; + private final List playbackStateHistory; private final List mediaTimeHistory; - private final List> videoFormatHistory; - private final List> audioFormatHistory; - private final List> fatalErrorHistory; - private final List> nonFatalErrorHistory; + private final List videoFormatHistory; + private final List audioFormatHistory; + private final List fatalErrorHistory; + private final List nonFatalErrorHistory; private final boolean isAd; private long firstReportedTimeMs; @@ -513,27 +531,39 @@ public final class PlaybackStatsListener } /** - * Notifies the tracker of a player state change event, including all player state changes while - * the playback is not in the foreground. + * Notifies the tracker of a playback state change event, including all playback state changes + * while the playback is not in the foreground. + * + * @param eventTime The {@link EventTime}. + * @param state The current {@link Player.State}. + * @param belongsToPlayback Whether the {@code eventTime} belongs to the current playback. + */ + public void onPlaybackStateChanged( + EventTime eventTime, @Player.State int state, boolean belongsToPlayback) { + playerPlaybackState = state; + if (state != Player.STATE_IDLE) { + hasFatalError = false; + } + if (state != Player.STATE_BUFFERING) { + isSeeking = false; + } + if (state == Player.STATE_IDLE || state == Player.STATE_ENDED) { + isInterruptedByAd = false; + } + maybeUpdatePlaybackState(eventTime, belongsToPlayback); + } + + /** + * Notifies the tracker of a play when ready change event, including all play when ready changes + * while the playback is not in the foreground. * * @param eventTime The {@link EventTime}. * @param playWhenReady Whether the playback will proceed when ready. - * @param playbackState The current {@link Player.State}. * @param belongsToPlayback Whether the {@code eventTime} belongs to the current playback. */ - public void onPlayerStateChanged( - EventTime eventTime, - boolean playWhenReady, - @Player.State int playbackState, - boolean belongsToPlayback) { + public void onPlayWhenReadyChanged( + EventTime eventTime, boolean playWhenReady, boolean belongsToPlayback) { this.playWhenReady = playWhenReady; - playerPlaybackState = playbackState; - if (playbackState != Player.STATE_IDLE) { - hasFatalError = false; - } - if (playbackState == Player.STATE_IDLE || playbackState == Player.STATE_ENDED) { - isInterruptedByAd = false; - } maybeUpdatePlaybackState(eventTime, belongsToPlayback); } @@ -555,30 +585,26 @@ public final class PlaybackStatsListener * Notifies the tracker of a position discontinuity or timeline update for the current playback. * * @param eventTime The {@link EventTime}. + * @param isSeek Whether the position discontinuity is for a seek. */ - public void onPositionDiscontinuity(EventTime eventTime) { + public void onPositionDiscontinuity(EventTime eventTime, boolean isSeek) { + if (isSeek && playerPlaybackState == Player.STATE_IDLE) { + isSeeking = false; + } isInterruptedByAd = false; maybeUpdatePlaybackState(eventTime, /* belongsToPlayback= */ true); } /** - * Notifies the tracker of the start of a seek in the current playback. + * Notifies the tracker of the start of a seek, including all seeks while the playback is not in + * the foreground. * * @param eventTime The {@link EventTime}. + * @param belongsToPlayback Whether the {@code eventTime} belongs to the current playback. */ - public void onSeekStarted(EventTime eventTime) { + public void onSeekStarted(EventTime eventTime, boolean belongsToPlayback) { isSeeking = true; - maybeUpdatePlaybackState(eventTime, /* belongsToPlayback= */ true); - } - - /** - * Notifies the tracker of a seek has been processed in the current playback. - * - * @param eventTime The {@link EventTime}. - */ - public void onSeekProcessed(EventTime eventTime) { - isSeeking = false; - maybeUpdatePlaybackState(eventTime, /* belongsToPlayback= */ true); + maybeUpdatePlaybackState(eventTime, belongsToPlayback); } /** @@ -589,7 +615,7 @@ public final class PlaybackStatsListener public void onFatalError(EventTime eventTime, Exception error) { fatalErrorCount++; if (keepHistory) { - fatalErrorHistory.add(Pair.create(eventTime, error)); + fatalErrorHistory.add(new EventTimeAndException(eventTime, error)); } hasFatalError = true; isInterruptedByAd = false; @@ -690,7 +716,8 @@ public final class PlaybackStatsListener */ public void onVideoSizeChanged(EventTime eventTime, int width, int height) { if (currentVideoFormat != null && currentVideoFormat.height == Format.NO_VALUE) { - Format formatWithHeight = currentVideoFormat.copyWithVideoSize(width, height); + Format formatWithHeight = + currentVideoFormat.buildUpon().setWidth(width).setHeight(height).build(); maybeUpdateVideoFormat(eventTime, formatWithHeight); } } @@ -743,7 +770,7 @@ public final class PlaybackStatsListener public void onNonFatalError(EventTime eventTime, Exception error) { nonFatalErrorCount++; if (keepHistory) { - nonFatalErrorHistory.add(Pair.create(eventTime, error)); + nonFatalErrorHistory.add(new EventTimeAndException(eventTime, error)); } } @@ -776,9 +803,9 @@ public final class PlaybackStatsListener : playbackStateDurationsMs[PlaybackStats.PLAYBACK_STATE_JOINING_FOREGROUND]; boolean hasBackgroundJoin = playbackStateDurationsMs[PlaybackStats.PLAYBACK_STATE_JOINING_BACKGROUND] > 0; - List> videoHistory = + List videoHistory = isFinal ? videoFormatHistory : new ArrayList<>(videoFormatHistory); - List> audioHistory = + List audioHistory = isFinal ? audioFormatHistory : new ArrayList<>(audioFormatHistory); return new PlaybackStats( /* playbackCount= */ 1, @@ -864,7 +891,7 @@ public final class PlaybackStatsListener currentPlaybackState = newPlaybackState; currentPlaybackStateStartTimeMs = eventTime.realtimeMs; if (keepHistory) { - playbackStateHistory.add(Pair.create(eventTime, currentPlaybackState)); + playbackStateHistory.add(new EventTimeAndPlaybackState(eventTime, currentPlaybackState)); } } @@ -874,7 +901,7 @@ public final class PlaybackStatsListener return currentPlaybackState == PlaybackStats.PLAYBACK_STATE_ENDED ? PlaybackStats.PLAYBACK_STATE_ENDED : PlaybackStats.PLAYBACK_STATE_ABANDONED; - } else if (isSeeking) { + } else if (isSeeking && isForeground) { // Seeking takes precedence over errors such that we report a seek while in error state. return PlaybackStats.PLAYBACK_STATE_SEEKING; } else if (hasFatalError) { @@ -895,10 +922,6 @@ public final class PlaybackStatsListener || currentPlaybackState == PlaybackStats.PLAYBACK_STATE_INTERRUPTED_BY_AD) { return PlaybackStats.PLAYBACK_STATE_JOINING_FOREGROUND; } - if (currentPlaybackState == PlaybackStats.PLAYBACK_STATE_SEEKING - || currentPlaybackState == PlaybackStats.PLAYBACK_STATE_SEEK_BUFFERING) { - return PlaybackStats.PLAYBACK_STATE_SEEK_BUFFERING; - } if (!playWhenReady) { return PlaybackStats.PLAYBACK_STATE_PAUSED_BUFFERING; } @@ -931,6 +954,9 @@ public final class PlaybackStatsListener } private void maybeUpdateMediaTimeHistory(long realtimeMs, long mediaTimeMs) { + if (!keepHistory) { + return; + } if (currentPlaybackState != PlaybackStats.PLAYBACK_STATE_PLAYING) { if (mediaTimeMs == C.TIME_UNSET) { return; @@ -973,7 +999,7 @@ public final class PlaybackStatsListener } currentVideoFormat = newFormat; if (keepHistory) { - videoFormatHistory.add(Pair.create(eventTime, currentVideoFormat)); + videoFormatHistory.add(new EventTimeAndFormat(eventTime, currentVideoFormat)); } } @@ -989,7 +1015,7 @@ public final class PlaybackStatsListener } currentAudioFormat = newFormat; if (keepHistory) { - audioFormatHistory.add(Pair.create(eventTime, currentAudioFormat)); + audioFormatHistory.add(new EventTimeAndFormat(eventTime, currentAudioFormat)); } } 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 516df8147c..53eed6c551 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 @@ -15,8 +15,8 @@ */ package com.google.android.exoplayer2.audio; -import android.annotation.TargetApi; import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.util.Util; @@ -118,7 +118,7 @@ public final class AudioAttributes { * *

        Field {@link AudioAttributes#allowedCapturePolicy} is ignored for API levels prior to 29. */ - @TargetApi(21) + @RequiresApi(21) public android.media.AudioAttributes getAudioAttributesV21() { if (audioAttributesV21 == null) { android.media.AudioAttributes.Builder builder = 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 25c0e70ae5..4ec8e518b3 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 @@ -16,7 +16,6 @@ package com.google.android.exoplayer2.audio; import android.annotation.SuppressLint; -import android.annotation.TargetApi; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; @@ -25,11 +24,11 @@ import android.media.AudioManager; import android.net.Uri; import android.provider.Settings.Global; import androidx.annotation.Nullable; +import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.util.Util; import java.util.Arrays; /** Represents the set of audio formats that a device is capable of playing. */ -@TargetApi(21) public final class AudioCapabilities { private static final int DEFAULT_MAX_CHANNEL_COUNT = 8; @@ -117,10 +116,10 @@ public final class AudioCapabilities { /** * Returns whether this device supports playback of the specified audio {@code encoding}. * - * @param encoding One of {@link android.media.AudioFormat}'s {@code ENCODING_*} constants. + * @param encoding One of {@link C.Encoding}'s {@code ENCODING_*} constants. * @return Whether this device supports playback the specified audio {@code encoding}. */ - public boolean supportsEncoding(int encoding) { + public boolean supportsEncoding(@C.Encoding int encoding) { return Arrays.binarySearch(supportedEncodings, encoding) >= 0; } 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 fe84c49656..991ed9ee97 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 @@ -54,7 +54,7 @@ public final class AudioCapabilitiesReceiver { @Nullable private final BroadcastReceiver receiver; @Nullable private final ExternalSurroundSoundSettingObserver externalSurroundSoundSettingObserver; - /* package */ @Nullable AudioCapabilities audioCapabilities; + @Nullable /* package */ AudioCapabilities audioCapabilities; private boolean registered; /** 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 index 8ce365b283..f208f602e1 100644 --- 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 @@ -38,4 +38,11 @@ public interface AudioListener { * @param volume The new volume, with 0 being silence and 1 being unity gain. */ default void onVolumeChanged(float volume) {} + + /** + * Called when skipping silences is enabled or disabled in the audio stream. + * + * @param skipSilenceEnabled Whether skipping silences in the audio stream is enabled. + */ + default void onSkipSilenceEnabledChanged(boolean skipSilenceEnabled) {} } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioRendererEventListener.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioRendererEventListener.java index bf5822caf6..7cb05cfa0d 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioRendererEventListener.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioRendererEventListener.java @@ -85,8 +85,13 @@ public interface AudioRendererEventListener { default void onAudioDisabled(DecoderCounters counters) {} /** - * Dispatches events to a {@link AudioRendererEventListener}. + * Called when skipping silences is enabled or disabled in the audio stream. + * + * @param skipSilenceEnabled Whether skipping silences in the audio stream is enabled. */ + default void onSkipSilenceEnabledChanged(boolean skipSilenceEnabled) {} + + /** Dispatches events to a {@link AudioRendererEventListener}. */ final class EventDispatcher { @Nullable private final Handler handler; @@ -170,5 +175,12 @@ public interface AudioRendererEventListener { handler.post(() -> castNonNull(listener).onAudioSessionId(audioSessionId)); } } + + /** Invokes {@link AudioRendererEventListener#onSkipSilenceEnabledChanged(boolean)}. */ + public void skipSilenceEnabledChanged(final boolean skipSilenceEnabled) { + if (handler != null) { + handler.post(() -> castNonNull(listener).onSkipSilenceEnabledChanged(skipSilenceEnabled)); + } + } } } 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 f2458a7471..725e0a8d39 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 @@ -28,18 +28,19 @@ import java.nio.ByteBuffer; *

        Before starting playback, specify the input audio format by calling {@link #configure(int, * int, int, int, int[], int, int)}. * - *

        Call {@link #handleBuffer(ByteBuffer, long)} to write data, and {@link #handleDiscontinuity()} - * when the data being fed is discontinuous. Call {@link #play()} to start playing the written data. + *

        Call {@link #handleBuffer(ByteBuffer, long, int)} to write data, and {@link + * #handleDiscontinuity()} when the data being fed is discontinuous. Call {@link #play()} to start + * playing the written data. * *

        Call {@link #configure(int, int, int, int, int[], int, int)} whenever the input format * changes. The sink will be reinitialized on the next call to {@link #handleBuffer(ByteBuffer, - * long)}. + * long, int)}. * *

        Call {@link #flush()} to prepare the sink to receive audio data from a new playback position. * *

        Call {@link #playToEndOfStream()} repeatedly to play out all data when no more input buffers - * will be provided via {@link #handleBuffer(ByteBuffer, long)} until the next {@link #flush()}. - * Call {@link #reset()} when the instance is no longer required. + * will be provided via {@link #handleBuffer(ByteBuffer, long, int)} until the next {@link + * #flush()}. Call {@link #reset()} when the instance is no longer required. * *

        The implementation may be backed by a platform {@link AudioTrack}. In this case, {@link * #setAudioSessionId(int)}, {@link #setAudioAttributes(AudioAttributes)}, {@link @@ -83,6 +84,12 @@ public interface AudioSink { */ void onUnderrun(int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs); + /** + * Called when skipping silences is enabled or disabled. + * + * @param skipSilenceEnabled Whether skipping silences is enabled. + */ + void onSkipSilenceEnabledChanged(boolean skipSilenceEnabled); } /** @@ -234,11 +241,14 @@ public interface AudioSink { * * @param buffer The buffer containing audio data. * @param presentationTimeUs The presentation timestamp of the buffer in microseconds. + * @param encodedAccessUnitCount The number of encoded access units in the buffer, or 1 if the + * buffer contains PCM audio. This allows batching multiple encoded access units in one + * buffer. * @return Whether the buffer was handled fully. * @throws InitializationException If an error occurs initializing the sink. * @throws WriteException If an error occurs writing the audio data. */ - boolean handleBuffer(ByteBuffer buffer, long presentationTimeUs) + boolean handleBuffer(ByteBuffer buffer, long presentationTimeUs, int encodedAccessUnitCount) throws InitializationException, WriteException; /** @@ -259,18 +269,29 @@ public interface AudioSink { boolean hasPendingData(); /** - * Attempts to set the playback parameters. The audio sink may override these parameters if they - * are not supported. - * - * @param playbackParameters The new playback parameters to attempt to set. + * @deprecated Use {@link #setPlaybackSpeed(float)} and {@link #setSkipSilenceEnabled(boolean)} + * instead. */ + @Deprecated void setPlaybackParameters(PlaybackParameters playbackParameters); - /** - * Gets the active {@link PlaybackParameters}. - */ + /** @deprecated Use {@link #getPlaybackSpeed()} and {@link #getSkipSilenceEnabled()} instead. */ + @SuppressWarnings("deprecation") + @Deprecated PlaybackParameters getPlaybackParameters(); + /** Sets the playback speed. */ + void setPlaybackSpeed(float playbackSpeed); + + /** Gets the playback speed. */ + float getPlaybackSpeed(); + + /** Sets whether silences should be skipped in the audio stream. */ + void setSkipSilenceEnabled(boolean skipSilenceEnabled); + + /** Gets whether silences are skipped in the audio stream. */ + boolean getSkipSilenceEnabled(); + /** * Sets attributes for audio playback. If the attributes have changed and if the sink is not * configured for use with tunneling, then it is reset and the audio session id is cleared. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioTimestampPoller.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioTimestampPoller.java index 0564591f1f..9e870735f2 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioTimestampPoller.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioTimestampPoller.java @@ -20,6 +20,7 @@ import android.media.AudioTimestamp; import android.media.AudioTrack; import androidx.annotation.IntDef; import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.util.Util; import java.lang.annotation.Documented; @@ -115,6 +116,7 @@ import java.lang.annotation.RetentionPolicy; * @param systemTimeUs The current system time, in microseconds. * @return Whether the timestamp was updated. */ + @TargetApi(19) // audioTimestamp will be null if Util.SDK_INT < 19. public boolean maybePollTimestamp(long systemTimeUs) { if (audioTimestamp == null || (systemTimeUs - lastTimestampSampleTimeUs) < sampleIntervalUs) { return false; @@ -220,6 +222,7 @@ import java.lang.annotation.RetentionPolicy; * If {@link #maybePollTimestamp(long)} or {@link #hasTimestamp()} returned {@code true}, returns * the system time at which the latest timestamp was sampled, in microseconds. */ + @TargetApi(19) // audioTimestamp will be null if Util.SDK_INT < 19. public long getTimestampSystemTimeUs() { return audioTimestamp != null ? audioTimestamp.getTimestampSystemTimeUs() : C.TIME_UNSET; } @@ -228,6 +231,7 @@ import java.lang.annotation.RetentionPolicy; * If {@link #maybePollTimestamp(long)} or {@link #hasTimestamp()} returned {@code true}, returns * the latest timestamp's position in frames. */ + @TargetApi(19) // audioTimestamp will be null if Util.SDK_INT < 19. public long getTimestampPositionFrames() { return audioTimestamp != null ? audioTimestamp.getTimestampPositionFrames() : C.POSITION_UNSET; } @@ -257,7 +261,7 @@ import java.lang.annotation.RetentionPolicy; } } - @TargetApi(19) + @RequiresApi(19) private static final class AudioTimestampV19 { private final AudioTrack audioTrack; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/ChannelMappingAudioProcessor.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/ChannelMappingAudioProcessor.java index 4fb6af1af4..b94d972dc5 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/ChannelMappingAudioProcessor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/ChannelMappingAudioProcessor.java @@ -24,7 +24,6 @@ import java.nio.ByteBuffer; * An {@link AudioProcessor} that applies a mapping from input channels onto specified output * channels. This can be used to reorder, duplicate or discard channels. */ -@SuppressWarnings("nullness:initialization.fields.uninitialized") /* package */ final class ChannelMappingAudioProcessor extends BaseAudioProcessor { @Nullable private int[] pendingOutputChannels; 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/DecoderAudioRenderer.java similarity index 72% rename from library/core/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java rename to library/core/src/main/java/com/google/android/exoplayer2/audio/DecoderAudioRenderer.java index 21991008cb..05b70c9315 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/DecoderAudioRenderer.java @@ -26,17 +26,18 @@ import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.FormatHolder; -import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.PlayerMessage.Target; +import com.google.android.exoplayer2.RendererCapabilities; import com.google.android.exoplayer2.audio.AudioRendererEventListener.EventDispatcher; +import com.google.android.exoplayer2.decoder.Decoder; import com.google.android.exoplayer2.decoder.DecoderCounters; +import com.google.android.exoplayer2.decoder.DecoderException; 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.drm.DrmSession; import com.google.android.exoplayer2.drm.DrmSession.DrmSessionException; -import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.drm.ExoMediaCrypto; +import com.google.android.exoplayer2.source.SampleStream; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.MediaClock; import com.google.android.exoplayer2.util.MimeTypes; @@ -47,23 +48,28 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; /** - * Decodes and renders audio using a {@link SimpleDecoder}. + * Decodes and renders audio using a {@link Decoder}. * *

        This renderer accepts the following messages sent via {@link ExoPlayer#createMessage(Target)} * on the playback thread: * *

          - *
        • Message with type {@link C#MSG_SET_VOLUME} to set the volume. The message payload should be + *
        • Message with type {@link #MSG_SET_VOLUME} to set the volume. The message payload should be * a {@link Float} with 0 being silence and 1 being unity gain. - *
        • Message with type {@link C#MSG_SET_AUDIO_ATTRIBUTES} to set the audio attributes. The + *
        • Message with type {@link #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 + *
        • Message with type {@link #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. + *
        • Message with type {@link #MSG_SET_SKIP_SILENCE_ENABLED} to enable or disable skipping + * silences. The message payload should be a {@link Boolean}. + *
        • Message with type {@link #MSG_SET_AUDIO_SESSION_ID} to set the audio session ID. The + * message payload should be a session ID {@link Integer} that will be attached to the * underlying audio track. *
        */ -public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements MediaClock { +public abstract class DecoderAudioRenderer extends BaseRenderer implements MediaClock { @Documented @Retention(RetentionPolicy.SOURCE) @@ -90,8 +96,6 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements */ private static final int REINITIALIZATION_STATE_WAIT_END_OF_STREAM = 2; - private final DrmSessionManager drmSessionManager; - private final boolean playClearSamplesWithoutKeys; private final EventDispatcher eventDispatcher; private final AudioSink audioSink; private final DecoderInputBuffer flagsOnlyBuffer; @@ -100,12 +104,15 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements private Format inputFormat; private int encoderDelay; private int encoderPadding; - private SimpleDecoder decoder; - private DecoderInputBuffer inputBuffer; - private SimpleOutputBuffer outputBuffer; - @Nullable private DrmSession decoderDrmSession; - @Nullable private DrmSession sourceDrmSession; + + @Nullable + private Decoder + decoder; + + @Nullable private DecoderInputBuffer inputBuffer; + @Nullable private SimpleOutputBuffer outputBuffer; + @Nullable private DrmSession decoderDrmSession; + @Nullable private DrmSession sourceDrmSession; @ReinitializationState private int decoderReinitializationState; private boolean decoderReceivedBuffers; @@ -118,7 +125,7 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements private boolean outputStreamEnded; private boolean waitingForKeys; - public SimpleDecoderAudioRenderer() { + public DecoderAudioRenderer() { this(/* eventHandler= */ null, /* eventListener= */ null); } @@ -128,7 +135,7 @@ 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( + public DecoderAudioRenderer( @Nullable Handler eventHandler, @Nullable AudioRendererEventListener eventListener, AudioProcessor... audioProcessors) { @@ -136,8 +143,6 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements eventHandler, eventListener, /* audioCapabilities= */ null, - /* drmSessionManager= */ null, - /* playClearSamplesWithoutKeys= */ false, audioProcessors); } @@ -147,67 +152,27 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements * @param eventListener A listener of events. May be null if delivery of events is not required. * @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( - @Nullable Handler eventHandler, - @Nullable AudioRendererEventListener eventListener, - @Nullable AudioCapabilities audioCapabilities) { - this( - eventHandler, - eventListener, - audioCapabilities, - /* drmSessionManager= */ null, - /* playClearSamplesWithoutKeys= */ false); - } - - /** - * @param eventHandler A handler to use when delivering events to {@code eventListener}. 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 audioCapabilities The audio capabilities for playback on this device. May be null if the - * default capabilities (no encoded audio passthrough support) should be assumed. - * @param drmSessionManager For use with encrypted media. May be null if support for encrypted - * media is not required. - * @param playClearSamplesWithoutKeys Encrypted media may contain clear (un-encrypted) regions. - * For example a media file may start with a short clear region so as to allow playback to - * 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 audioProcessors Optional {@link AudioProcessor}s that will process audio before output. */ - public SimpleDecoderAudioRenderer( + public DecoderAudioRenderer( @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)); + this(eventHandler, eventListener, new DefaultAudioSink(audioCapabilities, audioProcessors)); } /** * @param eventHandler A handler to use when delivering events to {@code eventListener}. 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 drmSessionManager For use with encrypted media. May be null if support for encrypted - * media is not required. - * @param playClearSamplesWithoutKeys Encrypted media may contain clear (un-encrypted) regions. - * For example a media file may start with a short clear region so as to allow playback to - * 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 audioSink The sink to which audio will be output. */ - public SimpleDecoderAudioRenderer( + public DecoderAudioRenderer( @Nullable Handler eventHandler, @Nullable AudioRendererEventListener eventListener, - @Nullable DrmSessionManager drmSessionManager, - boolean playClearSamplesWithoutKeys, AudioSink audioSink) { super(C.TRACK_TYPE_AUDIO); - this.drmSessionManager = drmSessionManager; - this.playClearSamplesWithoutKeys = playClearSamplesWithoutKeys; eventDispatcher = new EventDispatcher(eventHandler, eventListener); this.audioSink = audioSink; audioSink.setListener(new AudioSinkListener()); @@ -217,33 +182,34 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements } @Override + @Nullable public MediaClock getMediaClock() { return this; } @Override + @Capabilities public final int supportsFormat(Format format) { if (!MimeTypes.isAudio(format.sampleMimeType)) { - return FORMAT_UNSUPPORTED_TYPE; + return RendererCapabilities.create(FORMAT_UNSUPPORTED_TYPE); } - int formatSupport = supportsFormatInternal(drmSessionManager, format); + @FormatSupport int formatSupport = supportsFormatInternal(format); if (formatSupport <= FORMAT_UNSUPPORTED_DRM) { - return formatSupport; + return RendererCapabilities.create(formatSupport); } + @TunnelingSupport int tunnelingSupport = Util.SDK_INT >= 21 ? TUNNELING_SUPPORTED : TUNNELING_NOT_SUPPORTED; - return ADAPTIVE_NOT_SEAMLESS | tunnelingSupport | formatSupport; + return RendererCapabilities.create(formatSupport, ADAPTIVE_NOT_SEAMLESS, tunnelingSupport); } /** - * Returns the {@link #FORMAT_SUPPORT_MASK} component of the return value for {@link - * #supportsFormat(Format)}. + * Returns the {@link FormatSupport} for the given {@link Format}. * - * @param drmSessionManager The renderer's {@link DrmSessionManager}. * @param format The format, which has an audio {@link Format#sampleMimeType}. - * @return The extent to which the renderer supports the format itself. + * @return The {@link FormatSupport} for this {@link Format}. */ - protected abstract int supportsFormatInternal( - @Nullable DrmSessionManager drmSessionManager, Format format); + @FormatSupport + protected abstract int supportsFormatInternal(Format format); /** * Returns whether the sink supports the audio format. @@ -260,7 +226,7 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements try { audioSink.playToEndOfStream(); } catch (AudioSink.WriteException e) { - throw ExoPlaybackException.createForRenderer(e, getIndex()); + throw createRendererException(e, inputFormat); } return; } @@ -270,7 +236,7 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements // We don't have a format yet, so try and read one. FormatHolder formatHolder = getFormatHolder(); flagsOnlyBuffer.clear(); - int result = readSource(formatHolder, flagsOnlyBuffer, true); + @SampleStream.ReadDataResult int result = readSource(formatHolder, flagsOnlyBuffer, true); if (result == C.RESULT_FORMAT_READ) { onInputFormatChanged(formatHolder); } else if (result == C.RESULT_BUFFER_READ) { @@ -295,9 +261,11 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements while (drainOutputBuffer()) {} while (feedInputBuffer()) {} TraceUtil.endSection(); - } catch (AudioDecoderException | AudioSink.ConfigurationException - | AudioSink.InitializationException | AudioSink.WriteException e) { - throw ExoPlaybackException.createForRenderer(e, getIndex()); + } catch (DecoderException + | AudioSink.ConfigurationException + | AudioSink.InitializationException + | AudioSink.WriteException e) { + throw createRendererException(e, inputFormat); } decoderCounters.ensureUpdated(); } @@ -309,24 +277,25 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements * order to spatialize the audio channels. For this use case, any {@link Virtualizer} instances * should be released in {@link #onDisabled()} (if not before). * - * @see AudioSink.Listener#onAudioSessionId(int) + *

        See {@link AudioSink.Listener#onAudioSessionId(int)}. */ protected void onAudioSessionId(int audioSessionId) { // Do nothing. } - /** - * @see AudioSink.Listener#onPositionDiscontinuity() - */ + /** See {@link AudioSink.Listener#onPositionDiscontinuity()}. */ protected void onAudioTrackPositionDiscontinuity() { // Do nothing. } - /** - * @see AudioSink.Listener#onUnderrun(int, long, long) - */ - protected void onAudioTrackUnderrun(int bufferSize, long bufferSizeMs, - long elapsedSinceLastFeedMs) { + /** See {@link AudioSink.Listener#onUnderrun(int, long, long)}. */ + protected void onAudioTrackUnderrun( + int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) { + // Do nothing. + } + + /** See {@link AudioSink.Listener#onSkipSilenceEnabledChanged(boolean)}. */ + protected void onAudioTrackSkipSilenceEnabledChanged(boolean skipSilenceEnabled) { // Do nothing. } @@ -337,40 +306,32 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements * @param mediaCrypto The {@link ExoMediaCrypto} object required for decoding encrypted content. * Maybe null and can be ignored if decoder does not handle encrypted content. * @return The decoder. - * @throws AudioDecoderException If an error occurred creating a suitable decoder. + * @throws DecoderException If an error occurred creating a suitable decoder. */ - protected abstract SimpleDecoder< - DecoderInputBuffer, ? extends SimpleOutputBuffer, ? extends AudioDecoderException> - createDecoder(Format format, @Nullable ExoMediaCrypto mediaCrypto) - throws AudioDecoderException; + protected abstract Decoder< + DecoderInputBuffer, ? extends SimpleOutputBuffer, ? extends DecoderException> + createDecoder(Format format, @Nullable ExoMediaCrypto mediaCrypto) throws DecoderException; /** * Returns the format of audio buffers output by the decoder. Will not be called until the first * output buffer has been dequeued, so the decoder may use input data to determine the format. - *

        - * The default implementation returns a 16-bit PCM format with the same channel count and sample - * rate as the input. */ - protected Format getOutputFormat() { - return Format.createAudioSampleFormat(null, MimeTypes.AUDIO_RAW, null, Format.NO_VALUE, - Format.NO_VALUE, inputFormat.channelCount, inputFormat.sampleRate, C.ENCODING_PCM_16BIT, - null, null, 0, null); - } + protected abstract Format getOutputFormat(); /** * Returns whether the existing decoder can be kept for a new format. * * @param oldFormat The previous format. * @param newFormat The new format. - * @return True if the existing decoder can be kept. + * @return Whether the existing decoder can be kept. */ protected boolean canKeepCodec(Format oldFormat, Format newFormat) { return false; } - private boolean drainOutputBuffer() throws ExoPlaybackException, AudioDecoderException, - AudioSink.ConfigurationException, AudioSink.InitializationException, - AudioSink.WriteException { + private boolean drainOutputBuffer() + throws ExoPlaybackException, DecoderException, AudioSink.ConfigurationException, + AudioSink.InitializationException, AudioSink.WriteException { if (outputBuffer == null) { outputBuffer = decoder.dequeueOutputBuffer(); if (outputBuffer == null) { @@ -404,7 +365,8 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements audioTrackNeedsConfigure = false; } - if (audioSink.handleBuffer(outputBuffer.data, outputBuffer.timeUs)) { + if (audioSink.handleBuffer( + outputBuffer.data, outputBuffer.timeUs, /* encodedAccessUnitCount= */ 1)) { decoderCounters.renderedOutputBufferCount++; outputBuffer.release(); outputBuffer = null; @@ -414,7 +376,7 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements return false; } - private boolean feedInputBuffer() throws AudioDecoderException, ExoPlaybackException { + private boolean feedInputBuffer() throws DecoderException, ExoPlaybackException { if (decoder == null || decoderReinitializationState == REINITIALIZATION_STATE_WAIT_END_OF_STREAM || inputStreamEnded) { // We need to reinitialize the decoder or the input stream has ended. @@ -436,7 +398,7 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements return false; } - int result; + @SampleStream.ReadDataResult int result; FormatHolder formatHolder = getFormatHolder(); if (waitingForKeys) { // We've already read an encrypted sample into buffer, and are waiting for keys. @@ -474,13 +436,12 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements private boolean shouldWaitForKeys(boolean bufferEncrypted) throws ExoPlaybackException { if (decoderDrmSession == null - || (!bufferEncrypted - && (playClearSamplesWithoutKeys || decoderDrmSession.playClearSamplesWithoutKeys()))) { + || (!bufferEncrypted && decoderDrmSession.playClearSamplesWithoutKeys())) { return false; } @DrmSession.State int drmSessionState = decoderDrmSession.getState(); if (drmSessionState == DrmSession.STATE_ERROR) { - throw ExoPlaybackException.createForRenderer(decoderDrmSession.getError(), getIndex()); + throw createRendererException(decoderDrmSession.getError(), inputFormat); } return drmSessionState != DrmSession.STATE_OPENED_WITH_KEYS; } @@ -490,7 +451,8 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements try { audioSink.playToEndOfStream(); } catch (AudioSink.WriteException e) { - throw ExoPlaybackException.createForRenderer(e, getIndex()); + // TODO(internal: b/145658993) Use outputFormat for the call from drainOutputBuffer. + throw createRendererException(e, inputFormat); } } @@ -530,17 +492,18 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements } @Override - public void setPlaybackParameters(PlaybackParameters playbackParameters) { - audioSink.setPlaybackParameters(playbackParameters); + public void setPlaybackSpeed(float playbackSpeed) { + audioSink.setPlaybackSpeed(playbackSpeed); } @Override - public PlaybackParameters getPlaybackParameters() { - return audioSink.getPlaybackParameters(); + public float getPlaybackSpeed() { + return audioSink.getPlaybackSpeed(); } @Override - protected void onEnabled(boolean joining) throws ExoPlaybackException { + protected void onEnabled(boolean joining, boolean mayRenderStartOfStream) + throws ExoPlaybackException { decoderCounters = new DecoderCounters(); eventDispatcher.enabled(decoderCounters); int tunnelingAudioSessionId = getConfiguration().tunnelingAudioSessionId; @@ -592,17 +555,23 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements @Override public void handleMessage(int messageType, @Nullable Object message) throws ExoPlaybackException { switch (messageType) { - case C.MSG_SET_VOLUME: + case MSG_SET_VOLUME: audioSink.setVolume((Float) message); break; - case C.MSG_SET_AUDIO_ATTRIBUTES: + case MSG_SET_AUDIO_ATTRIBUTES: AudioAttributes audioAttributes = (AudioAttributes) message; audioSink.setAudioAttributes(audioAttributes); break; - case C.MSG_SET_AUX_EFFECT_INFO: + case MSG_SET_AUX_EFFECT_INFO: AuxEffectInfo auxEffectInfo = (AuxEffectInfo) message; audioSink.setAuxEffectInfo(auxEffectInfo); break; + case MSG_SET_SKIP_SILENCE_ENABLED: + audioSink.setSkipSilenceEnabled((Boolean) message); + break; + case MSG_SET_AUDIO_SESSION_ID: + audioSink.setAudioSessionId((Integer) message); + break; default: super.handleMessage(messageType, message); break; @@ -640,8 +609,8 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements eventDispatcher.decoderInitialized(decoder.getName(), codecInitializedTimestamp, codecInitializedTimestamp - codecInitializingTimestamp); decoderCounters.decoderInitCount++; - } catch (AudioDecoderException e) { - throw ExoPlaybackException.createForRenderer(e, getIndex()); + } catch (DecoderException e) { + throw createRendererException(e, inputFormat); } } @@ -658,29 +627,25 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements setDecoderDrmSession(null); } - private void setSourceDrmSession(@Nullable DrmSession session) { + private void setSourceDrmSession(@Nullable DrmSession session) { DrmSession.replaceSession(sourceDrmSession, session); sourceDrmSession = session; } - private void setDecoderDrmSession(@Nullable DrmSession session) { + private void setDecoderDrmSession(@Nullable DrmSession session) { DrmSession.replaceSession(decoderDrmSession, session); decoderDrmSession = session; } - @SuppressWarnings("unchecked") private void onInputFormatChanged(FormatHolder formatHolder) throws ExoPlaybackException { Format newFormat = Assertions.checkNotNull(formatHolder.format); - if (formatHolder.includesDrmSession) { - setSourceDrmSession((DrmSession) formatHolder.drmSession); - } else { - sourceDrmSession = - getUpdatedSourceDrmSession(inputFormat, newFormat, drmSessionManager, sourceDrmSession); - } + setSourceDrmSession(formatHolder.drmSession); Format oldFormat = inputFormat; inputFormat = newFormat; - if (!canKeepCodec(oldFormat, inputFormat)) { + if (decoder == null) { + maybeInitDecoder(); + } else if (sourceDrmSession != decoderDrmSession || !canKeepCodec(oldFormat, inputFormat)) { if (decoderReceivedBuffers) { // Signal end of stream and wait for any final output buffers before re-initialization. decoderReinitializationState = REINITIALIZATION_STATE_SIGNAL_END_OF_STREAM; @@ -694,7 +659,6 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements encoderDelay = inputFormat.encoderDelay; encoderPadding = inputFormat.encoderPadding; - eventDispatcher.inputFormatChanged(inputFormat); } @@ -726,14 +690,14 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements @Override public void onAudioSessionId(int audioSessionId) { eventDispatcher.audioSessionId(audioSessionId); - SimpleDecoderAudioRenderer.this.onAudioSessionId(audioSessionId); + DecoderAudioRenderer.this.onAudioSessionId(audioSessionId); } @Override public void onPositionDiscontinuity() { onAudioTrackPositionDiscontinuity(); // We are out of sync so allow currentPositionUs to jump backwards. - SimpleDecoderAudioRenderer.this.allowPositionDiscontinuity = true; + DecoderAudioRenderer.this.allowPositionDiscontinuity = true; } @Override @@ -742,6 +706,10 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements onAudioTrackUnderrun(bufferSize, bufferSizeMs, elapsedSinceLastFeedMs); } + @Override + public void onSkipSilenceEnabledChanged(boolean skipSilenceEnabled) { + eventDispatcher.skipSilenceEnabledChanged(skipSilenceEnabled); + onAudioTrackSkipSilenceEnabledChanged(skipSilenceEnabled); + } } - } 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 27823e3006..78699d41f4 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 @@ -16,14 +16,13 @@ package com.google.android.exoplayer2.audio; import android.annotation.SuppressLint; -import android.annotation.TargetApi; import android.media.AudioFormat; import android.media.AudioManager; import android.media.AudioTrack; import android.os.ConditionVariable; import android.os.SystemClock; -import androidx.annotation.IntDef; import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.PlaybackParameters; @@ -31,9 +30,6 @@ import com.google.android.exoplayer2.audio.AudioProcessor.UnhandledAudioFormatEx import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.Util; -import java.lang.annotation.Documented; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.util.ArrayDeque; @@ -85,15 +81,31 @@ public final class DefaultAudioSink implements AudioSink { AudioProcessor[] getAudioProcessors(); /** - * Configures audio processors to apply the specified playback parameters immediately, returning - * the new parameters, which may differ from those passed in. Only called when processors have - * no input pending. - * - * @param playbackParameters The playback parameters to try to apply. - * @return The playback parameters that were actually applied. + * @deprecated Use {@link #applyPlaybackSpeed(float)} and {@link + * #applySkipSilenceEnabled(boolean)} instead. */ + @Deprecated PlaybackParameters applyPlaybackParameters(PlaybackParameters playbackParameters); + /** + * Configures audio processors to apply the specified playback speed immediately, returning the + * new playback speed, which may differ from the speed passed in. Only called when processors + * have no input pending. + * + * @param playbackSpeed The playback speed to try to apply. + * @return The playback speed that was actually applied. + */ + float applyPlaybackSpeed(float playbackSpeed); + + /** + * Configures audio processors to apply whether to skip silences immediately, returning the new + * value. Only called when processors have no input pending. + * + * @param skipSilenceEnabled Whether silences should be skipped in the audio stream. + * @return The new value. + */ + boolean applySkipSilenceEnabled(boolean skipSilenceEnabled); + /** * Scales the specified playout duration to take into account speedup due to audio processing, * returning an input media duration, in arbitrary units. @@ -142,13 +154,26 @@ public final class DefaultAudioSink implements AudioSink { return audioProcessors; } + /** + * @deprecated Use {@link #applyPlaybackSpeed(float)} and {@link + * #applySkipSilenceEnabled(boolean)} instead. + */ + @SuppressWarnings("deprecation") + @Deprecated @Override public PlaybackParameters applyPlaybackParameters(PlaybackParameters playbackParameters) { - silenceSkippingAudioProcessor.setEnabled(playbackParameters.skipSilence); - return new PlaybackParameters( - sonicAudioProcessor.setSpeed(playbackParameters.speed), - sonicAudioProcessor.setPitch(playbackParameters.pitch), - playbackParameters.skipSilence); + return new PlaybackParameters(applyPlaybackSpeed(playbackParameters.speed)); + } + + @Override + public float applyPlaybackSpeed(float playbackSpeed) { + return sonicAudioProcessor.setSpeed(playbackSpeed); + } + + @Override + public boolean applySkipSilenceEnabled(boolean skipSilenceEnabled) { + silenceSkippingAudioProcessor.setEnabled(skipSilenceEnabled); + return skipSilenceEnabled; } @Override @@ -204,19 +229,13 @@ public final class DefaultAudioSink implements AudioSink { */ @SuppressLint("InlinedApi") private static final int WRITE_NON_BLOCKING = AudioTrack.WRITE_NON_BLOCKING; + /** The default playback speed. */ + private static final float DEFAULT_PLAYBACK_SPEED = 1.0f; + /** The default skip silence flag. */ + private static final boolean DEFAULT_SKIP_SILENCE = false; private static final String TAG = "AudioTrack"; - /** Represents states of the {@link #startMediaTimeUs} value. */ - @Documented - @Retention(RetentionPolicy.SOURCE) - @IntDef({START_NOT_SET, START_IN_SYNC, START_NEED_SYNC}) - private @interface StartMediaTimeState {} - - private static final int START_NOT_SET = 0; - private static final int START_IN_SYNC = 1; - private static final int START_NEED_SYNC = 2; - /** * Whether to enable a workaround for an issue where an audio effect does not keep its session * active across releasing/initializing a new audio track, on platform builds where @@ -237,14 +256,14 @@ public final class DefaultAudioSink implements AudioSink { @Nullable private final AudioCapabilities audioCapabilities; private final AudioProcessorChain audioProcessorChain; - private final boolean enableConvertHighResIntPcmToFloat; + private final boolean enableFloatOutput; private final ChannelMappingAudioProcessor channelMappingAudioProcessor; private final TrimmingAudioProcessor trimmingAudioProcessor; private final AudioProcessor[] toIntPcmAvailableAudioProcessors; private final AudioProcessor[] toFloatPcmAvailableAudioProcessors; private final ConditionVariable releasingConditionVariable; private final AudioTrackPositionTracker audioTrackPositionTracker; - private final ArrayDeque playbackParametersCheckpoints; + private final ArrayDeque mediaPositionParametersCheckpoints; @Nullable private Listener listener; /** Used to keep the audio session active on pre-V21 builds (see {@link #initialize(long)}). */ @@ -255,10 +274,8 @@ public final class DefaultAudioSink implements AudioSink { private AudioTrack audioTrack; private AudioAttributes audioAttributes; - @Nullable private PlaybackParameters afterDrainPlaybackParameters; - private PlaybackParameters playbackParameters; - private long playbackParametersOffsetUs; - private long playbackParametersPositionUs; + @Nullable private MediaPositionParameters afterDrainParameters; + private MediaPositionParameters mediaPositionParameters; @Nullable private ByteBuffer avSyncHeader; private int bytesUntilNextAvSync; @@ -268,13 +285,14 @@ public final class DefaultAudioSink implements AudioSink { private long writtenPcmBytes; private long writtenEncodedFrames; private int framesPerEncodedSample; - private @StartMediaTimeState int startMediaTimeState; + private boolean startMediaTimeUsNeedsSync; private long startMediaTimeUs; private float volume; private AudioProcessor[] activeAudioProcessors; private ByteBuffer[] outputBuffers; @Nullable private ByteBuffer inputBuffer; + private int inputBufferAccessUnitCount; @Nullable private ByteBuffer outputBuffer; private byte[] preV21OutputBuffer; private int preV21OutputBufferOffset; @@ -298,7 +316,7 @@ public final class DefaultAudioSink implements AudioSink { */ public DefaultAudioSink( @Nullable AudioCapabilities audioCapabilities, AudioProcessor[] audioProcessors) { - this(audioCapabilities, audioProcessors, /* enableConvertHighResIntPcmToFloat= */ false); + this(audioCapabilities, audioProcessors, /* enableFloatOutput= */ false); } /** @@ -308,19 +326,16 @@ public final class DefaultAudioSink implements AudioSink { * default capabilities (no encoded audio passthrough support) should be assumed. * @param audioProcessors An array of {@link AudioProcessor}s that will process PCM audio before * output. May be empty. - * @param enableConvertHighResIntPcmToFloat Whether to enable conversion of high resolution - * integer PCM to 32-bit float for output, if possible. Functionality that uses 16-bit integer - * audio processing (for example, speed and pitch adjustment) will not be available when float - * output is in use. + * @param enableFloatOutput Whether to enable 32-bit float output. Where possible, 32-bit float + * output will be used if the input is 32-bit float, and also if the input is high resolution + * (24-bit or 32-bit) integer PCM. Audio processing (for example, speed adjustment) will not + * be available when float output is in use. */ public DefaultAudioSink( @Nullable AudioCapabilities audioCapabilities, AudioProcessor[] audioProcessors, - boolean enableConvertHighResIntPcmToFloat) { - this( - audioCapabilities, - new DefaultAudioProcessorChain(audioProcessors), - enableConvertHighResIntPcmToFloat); + boolean enableFloatOutput) { + this(audioCapabilities, new DefaultAudioProcessorChain(audioProcessors), enableFloatOutput); } /** @@ -331,18 +346,18 @@ public final class DefaultAudioSink implements AudioSink { * default capabilities (no encoded audio passthrough support) should be assumed. * @param audioProcessorChain An {@link AudioProcessorChain} which is used to apply playback * parameters adjustments. The instance passed in must not be reused in other sinks. - * @param enableConvertHighResIntPcmToFloat Whether to enable conversion of high resolution - * integer PCM to 32-bit float for output, if possible. Functionality that uses 16-bit integer - * audio processing (for example, speed and pitch adjustment) will not be available when float - * output is in use. + * @param enableFloatOutput Whether to enable 32-bit float output. Where possible, 32-bit float + * output will be used if the input is 32-bit float, and also if the input is high resolution + * (24-bit or 32-bit) integer PCM. Audio processing (for example, speed adjustment) will not + * be available when float output is in use. */ public DefaultAudioSink( @Nullable AudioCapabilities audioCapabilities, AudioProcessorChain audioProcessorChain, - boolean enableConvertHighResIntPcmToFloat) { + boolean enableFloatOutput) { this.audioCapabilities = audioCapabilities; this.audioProcessorChain = Assertions.checkNotNull(audioProcessorChain); - this.enableConvertHighResIntPcmToFloat = enableConvertHighResIntPcmToFloat; + this.enableFloatOutput = enableFloatOutput; releasingConditionVariable = new ConditionVariable(true); audioTrackPositionTracker = new AudioTrackPositionTracker(new PositionTrackerListener()); channelMappingAudioProcessor = new ChannelMappingAudioProcessor(); @@ -357,15 +372,19 @@ public final class DefaultAudioSink implements AudioSink { toIntPcmAvailableAudioProcessors = toIntPcmAudioProcessors.toArray(new AudioProcessor[0]); toFloatPcmAvailableAudioProcessors = new AudioProcessor[] {new FloatResamplingAudioProcessor()}; volume = 1.0f; - 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; + mediaPositionParameters = + new MediaPositionParameters( + DEFAULT_PLAYBACK_SPEED, + DEFAULT_SKIP_SILENCE, + /* mediaTimeUs= */ 0, + /* audioTrackPositionUs= */ 0); drainingAudioProcessorIndex = C.INDEX_UNSET; activeAudioProcessors = new AudioProcessor[0]; outputBuffers = new ByteBuffer[0]; - playbackParametersCheckpoints = new ArrayDeque<>(); + mediaPositionParametersCheckpoints = new ArrayDeque<>(); } // AudioSink implementation. @@ -393,12 +412,12 @@ public final class DefaultAudioSink implements AudioSink { @Override public long getCurrentPositionUs(boolean sourceEnded) { - if (!isInitialized() || startMediaTimeState == START_NOT_SET) { + if (!isInitialized()) { return CURRENT_POSITION_NOT_SET; } long positionUs = audioTrackPositionTracker.getCurrentPositionUs(sourceEnded); positionUs = Math.min(positionUs, configuration.framesToDurationUs(getWrittenFrames())); - return startMediaTimeUs + applySkipping(applySpeedup(positionUs)); + return applySkipping(applyMediaPositionParameters(positionUs)); } @Override @@ -421,37 +440,34 @@ public final class DefaultAudioSink implements AudioSink { } boolean isInputPcm = Util.isEncodingLinearPcm(inputEncoding); - boolean processingEnabled = isInputPcm && inputEncoding != C.ENCODING_PCM_FLOAT; + boolean processingEnabled = isInputPcm; int sampleRate = inputSampleRate; int channelCount = inputChannelCount; @C.Encoding int encoding = inputEncoding; - boolean shouldConvertHighResIntPcmToFloat = - enableConvertHighResIntPcmToFloat + boolean useFloatOutput = + enableFloatOutput && supportsOutput(inputChannelCount, C.ENCODING_PCM_FLOAT) - && Util.isEncodingHighResolutionIntegerPcm(inputEncoding); + && Util.isEncodingHighResolutionPcm(inputEncoding); AudioProcessor[] availableAudioProcessors = - shouldConvertHighResIntPcmToFloat - ? toFloatPcmAvailableAudioProcessors - : toIntPcmAvailableAudioProcessors; + useFloatOutput ? toFloatPcmAvailableAudioProcessors : toIntPcmAvailableAudioProcessors; if (processingEnabled) { trimmingAudioProcessor.setTrimFrameCount(trimStartFrames, trimEndFrames); channelMappingAudioProcessor.setChannelMap(outputChannels); - AudioProcessor.AudioFormat inputAudioFormat = + AudioProcessor.AudioFormat outputFormat = new AudioProcessor.AudioFormat(sampleRate, channelCount, encoding); - AudioProcessor.AudioFormat outputAudioFormat = inputAudioFormat; for (AudioProcessor audioProcessor : availableAudioProcessors) { try { - outputAudioFormat = audioProcessor.configure(inputAudioFormat); + AudioProcessor.AudioFormat nextFormat = audioProcessor.configure(outputFormat); + if (audioProcessor.isActive()) { + outputFormat = nextFormat; + } } catch (UnhandledAudioFormatException e) { throw new ConfigurationException(e); } - if (audioProcessor.isActive()) { - inputAudioFormat = outputAudioFormat; - } } - sampleRate = outputAudioFormat.sampleRate; - channelCount = outputAudioFormat.channelCount; - encoding = outputAudioFormat.encoding; + sampleRate = outputFormat.sampleRate; + channelCount = outputFormat.channelCount; + encoding = outputFormat.encoding; } int outputChannelConfig = getChannelConfig(channelCount, isInputPcm); @@ -463,7 +479,7 @@ public final class DefaultAudioSink implements AudioSink { isInputPcm ? Util.getPcmFrameSize(inputEncoding, inputChannelCount) : C.LENGTH_UNSET; int outputPcmFrameSize = isInputPcm ? Util.getPcmFrameSize(encoding, channelCount) : C.LENGTH_UNSET; - boolean canApplyPlaybackParameters = processingEnabled && !shouldConvertHighResIntPcmToFloat; + boolean canApplyPlaybackParameters = processingEnabled && !useFloatOutput; Configuration pendingConfiguration = new Configuration( isInputPcm, @@ -540,7 +556,10 @@ public final class DefaultAudioSink implements AudioSink { } } - applyPlaybackParameters(playbackParameters, presentationTimeUs); + startMediaTimeUs = Math.max(0, presentationTimeUs); + startMediaTimeUsNeedsSync = false; + + applyPlaybackSpeedAndSkipSilence(presentationTimeUs); audioTrackPositionTracker.setAudioTrack( audioTrack, @@ -567,19 +586,18 @@ public final class DefaultAudioSink implements AudioSink { @Override public void handleDiscontinuity() { // Force resynchronization after a skipped buffer. - if (startMediaTimeState == START_IN_SYNC) { - startMediaTimeState = START_NEED_SYNC; - } + startMediaTimeUsNeedsSync = true; } @Override @SuppressWarnings("ReferenceEquality") - public boolean handleBuffer(ByteBuffer buffer, long presentationTimeUs) + public boolean handleBuffer( + ByteBuffer buffer, long presentationTimeUs, int encodedAccessUnitCount) throws InitializationException, WriteException { Assertions.checkArgument(inputBuffer == null || buffer == inputBuffer); if (pendingConfiguration != null) { - if (!drainAudioProcessorsToEndOfStream()) { + if (!drainToEndOfStream()) { // There's still pending data in audio processors to write to the track. return false; } else if (!pendingConfiguration.canReuseAudioTrack(configuration)) { @@ -595,7 +613,7 @@ public final class DefaultAudioSink implements AudioSink { pendingConfiguration = null; } // Re-apply playback parameters. - applyPlaybackParameters(playbackParameters, presentationTimeUs); + applyPlaybackSpeedAndSkipSilence(presentationTimeUs); } if (!isInitialized()) { @@ -628,60 +646,63 @@ public final class DefaultAudioSink implements AudioSink { } } - if (afterDrainPlaybackParameters != null) { - if (!drainAudioProcessorsToEndOfStream()) { + if (afterDrainParameters != null) { + if (!drainToEndOfStream()) { // Don't process any more input until draining completes. return false; } - PlaybackParameters newPlaybackParameters = afterDrainPlaybackParameters; - afterDrainPlaybackParameters = null; - applyPlaybackParameters(newPlaybackParameters, presentationTimeUs); + applyPlaybackSpeedAndSkipSilence(presentationTimeUs); + afterDrainParameters = null; } - if (startMediaTimeState == START_NOT_SET) { - startMediaTimeUs = Math.max(0, presentationTimeUs); - startMediaTimeState = START_IN_SYNC; - } else { - // Sanity check that presentationTimeUs is consistent with the expected value. - long expectedPresentationTimeUs = - startMediaTimeUs - + configuration.inputFramesToDurationUs( - getSubmittedFrames() - trimmingAudioProcessor.getTrimmedFrameCount()); - if (startMediaTimeState == START_IN_SYNC - && Math.abs(expectedPresentationTimeUs - presentationTimeUs) > 200000) { - Log.e(TAG, "Discontinuity detected [expected " + expectedPresentationTimeUs + ", got " - + presentationTimeUs + "]"); - startMediaTimeState = START_NEED_SYNC; + // Sanity check that presentationTimeUs is consistent with the expected value. + long expectedPresentationTimeUs = + startMediaTimeUs + + configuration.inputFramesToDurationUs( + getSubmittedFrames() - trimmingAudioProcessor.getTrimmedFrameCount()); + if (!startMediaTimeUsNeedsSync + && Math.abs(expectedPresentationTimeUs - presentationTimeUs) > 200000) { + Log.e( + TAG, + "Discontinuity detected [expected " + + expectedPresentationTimeUs + + ", got " + + presentationTimeUs + + "]"); + startMediaTimeUsNeedsSync = true; + } + if (startMediaTimeUsNeedsSync) { + if (!drainToEndOfStream()) { + // Don't update timing until pending AudioProcessor buffers are completely drained. + return false; } - if (startMediaTimeState == START_NEED_SYNC) { - // Adjust startMediaTimeUs to be consistent with the current buffer's start time and the - // number of bytes submitted. - long adjustmentUs = presentationTimeUs - expectedPresentationTimeUs; - startMediaTimeUs += adjustmentUs; - startMediaTimeState = START_IN_SYNC; - if (listener != null && adjustmentUs != 0) { - listener.onPositionDiscontinuity(); - } + // Adjust startMediaTimeUs to be consistent with the current buffer's start time and the + // number of bytes submitted. + long adjustmentUs = presentationTimeUs - expectedPresentationTimeUs; + startMediaTimeUs += adjustmentUs; + startMediaTimeUsNeedsSync = false; + // Re-apply playback parameters because the startMediaTimeUs changed. + applyPlaybackSpeedAndSkipSilence(presentationTimeUs); + if (listener != null && adjustmentUs != 0) { + listener.onPositionDiscontinuity(); } } if (configuration.isInputPcm) { submittedPcmBytes += buffer.remaining(); } else { - submittedEncodedFrames += framesPerEncodedSample; + submittedEncodedFrames += framesPerEncodedSample * encodedAccessUnitCount; } inputBuffer = buffer; + inputBufferAccessUnitCount = encodedAccessUnitCount; } - if (configuration.processingEnabled) { - processBuffers(presentationTimeUs); - } else { - writeBuffer(inputBuffer, presentationTimeUs); - } + processBuffers(presentationTimeUs); if (!inputBuffer.hasRemaining()) { inputBuffer = null; + inputBufferAccessUnitCount = 0; return true; } @@ -776,7 +797,10 @@ public final class DefaultAudioSink implements AudioSink { } if (bytesWritten == bytesRemaining) { if (!configuration.isInputPcm) { - writtenEncodedFrames += framesPerEncodedSample; + // When playing non-PCM, the inputBuffer is never processed, thus the last inputBuffer + // must be the current input buffer. + Assertions.checkState(buffer == inputBuffer); + writtenEncodedFrames += framesPerEncodedSample * inputBufferAccessUnitCount; } outputBuffer = null; } @@ -784,13 +808,13 @@ public final class DefaultAudioSink implements AudioSink { @Override public void playToEndOfStream() throws WriteException { - if (!handledEndOfStream && isInitialized() && drainAudioProcessorsToEndOfStream()) { + if (!handledEndOfStream && isInitialized() && drainToEndOfStream()) { playPendingData(); handledEndOfStream = true; } } - private boolean drainAudioProcessorsToEndOfStream() throws WriteException { + private boolean drainToEndOfStream() throws WriteException { boolean audioProcessorNeedsEndOfStream = false; if (drainingAudioProcessorIndex == C.INDEX_UNSET) { drainingAudioProcessorIndex = @@ -831,34 +855,44 @@ public final class DefaultAudioSink implements AudioSink { return isInitialized() && audioTrackPositionTracker.hasPendingData(getWrittenFrames()); } + /** + * @deprecated Use {@link #setPlaybackSpeed(float)} and {@link #setSkipSilenceEnabled(boolean)} + * instead. + */ + @SuppressWarnings("deprecation") + @Deprecated @Override public void setPlaybackParameters(PlaybackParameters playbackParameters) { - if (configuration != null && !configuration.canApplyPlaybackParameters) { - this.playbackParameters = PlaybackParameters.DEFAULT; - return; - } - PlaybackParameters lastSetPlaybackParameters = getPlaybackParameters(); - if (!playbackParameters.equals(lastSetPlaybackParameters)) { - if (isInitialized()) { - // Drain the audio processors so we can determine the frame position at which the new - // parameters apply. - afterDrainPlaybackParameters = playbackParameters; - } else { - // Update the playback parameters now. They will be applied to the audio processors during - // initialization. - this.playbackParameters = playbackParameters; - } - } + setPlaybackSpeedAndSkipSilence(playbackParameters.speed, getSkipSilenceEnabled()); + } + + /** @deprecated Use {@link #getPlaybackSpeed()} and {@link #getSkipSilenceEnabled()} instead. */ + @SuppressWarnings("deprecation") + @Deprecated + @Override + public PlaybackParameters getPlaybackParameters() { + MediaPositionParameters mediaPositionParameters = getMediaPositionParameters(); + return new PlaybackParameters(mediaPositionParameters.playbackSpeed); } @Override - public PlaybackParameters getPlaybackParameters() { - // Mask the already set parameters. - return afterDrainPlaybackParameters != null - ? afterDrainPlaybackParameters - : !playbackParametersCheckpoints.isEmpty() - ? playbackParametersCheckpoints.getLast().playbackParameters - : playbackParameters; + public void setPlaybackSpeed(float playbackSpeed) { + setPlaybackSpeedAndSkipSilence(playbackSpeed, getSkipSilenceEnabled()); + } + + @Override + public float getPlaybackSpeed() { + return getMediaPositionParameters().playbackSpeed; + } + + @Override + public void setSkipSilenceEnabled(boolean skipSilenceEnabled) { + setPlaybackSpeedAndSkipSilence(getPlaybackSpeed(), skipSilenceEnabled); + } + + @Override + public boolean getSkipSilenceEnabled() { + return getMediaPositionParameters().skipSilence; } @Override @@ -954,25 +988,25 @@ public final class DefaultAudioSink implements AudioSink { writtenPcmBytes = 0; writtenEncodedFrames = 0; framesPerEncodedSample = 0; - if (afterDrainPlaybackParameters != null) { - playbackParameters = afterDrainPlaybackParameters; - afterDrainPlaybackParameters = null; - } else if (!playbackParametersCheckpoints.isEmpty()) { - playbackParameters = playbackParametersCheckpoints.getLast().playbackParameters; - } - playbackParametersCheckpoints.clear(); - playbackParametersOffsetUs = 0; - playbackParametersPositionUs = 0; + mediaPositionParameters = + new MediaPositionParameters( + getPlaybackSpeed(), + getSkipSilenceEnabled(), + /* mediaTimeUs= */ 0, + /* audioTrackPositionUs= */ 0); + startMediaTimeUs = 0; + afterDrainParameters = null; + mediaPositionParametersCheckpoints.clear(); trimmingAudioProcessor.resetTrimmedFrameCount(); flushAudioProcessors(); inputBuffer = null; + inputBufferAccessUnitCount = 0; outputBuffer = null; stoppedAudioTrack = false; handledEndOfStream = false; drainingAudioProcessorIndex = C.INDEX_UNSET; avSyncHeader = null; bytesUntilNextAvSync = 0; - startMediaTimeState = START_NOT_SET; if (audioTrackPositionTracker.isPlaying()) { audioTrack.pause(); } @@ -985,7 +1019,7 @@ public final class DefaultAudioSink implements AudioSink { } audioTrackPositionTracker.reset(); releasingConditionVariable.close(); - new Thread() { + new Thread("ExoPlayer:AudioTrackReleaseThread") { @Override public void run() { try { @@ -1013,9 +1047,9 @@ public final class DefaultAudioSink implements AudioSink { playing = false; } - /** - * Releases {@link #keepSessionIdAudioTrack} asynchronously, if it is non-{@code null}. - */ + // Internal methods. + + /** Releases {@link #keepSessionIdAudioTrack} asynchronously, if it is non-{@code null}. */ private void releaseKeepSessionIdAudioTrack() { if (keepSessionIdAudioTrack == null) { return; @@ -1032,47 +1066,85 @@ public final class DefaultAudioSink implements AudioSink { }.start(); } - private void applyPlaybackParameters( - PlaybackParameters playbackParameters, long presentationTimeUs) { - PlaybackParameters newPlaybackParameters = - configuration.canApplyPlaybackParameters - ? audioProcessorChain.applyPlaybackParameters(playbackParameters) - : PlaybackParameters.DEFAULT; - // Store the position and corresponding media time from which the parameters will apply. - playbackParametersCheckpoints.add( - new PlaybackParametersCheckpoint( - newPlaybackParameters, - /* mediaTimeUs= */ Math.max(0, presentationTimeUs), - /* positionUs= */ configuration.framesToDurationUs(getWrittenFrames()))); - setupAudioProcessors(); + private void setPlaybackSpeedAndSkipSilence(float playbackSpeed, boolean skipSilence) { + MediaPositionParameters currentMediaPositionParameters = getMediaPositionParameters(); + if (playbackSpeed != currentMediaPositionParameters.playbackSpeed + || skipSilence != currentMediaPositionParameters.skipSilence) { + MediaPositionParameters mediaPositionParameters = + new MediaPositionParameters( + playbackSpeed, + skipSilence, + /* mediaTimeUs= */ C.TIME_UNSET, + /* audioTrackPositionUs= */ C.TIME_UNSET); + if (isInitialized()) { + // Drain the audio processors so we can determine the frame position at which the new + // parameters apply. + this.afterDrainParameters = mediaPositionParameters; + } else { + // Update the audio processor chain parameters now. They will be applied to the audio + // processors during initialization. + this.mediaPositionParameters = mediaPositionParameters; + } + } } - private long applySpeedup(long positionUs) { - @Nullable PlaybackParametersCheckpoint checkpoint = null; - while (!playbackParametersCheckpoints.isEmpty() - && positionUs >= playbackParametersCheckpoints.getFirst().positionUs) { - checkpoint = playbackParametersCheckpoints.remove(); + private MediaPositionParameters getMediaPositionParameters() { + // Mask the already set parameters. + return afterDrainParameters != null + ? afterDrainParameters + : !mediaPositionParametersCheckpoints.isEmpty() + ? mediaPositionParametersCheckpoints.getLast() + : mediaPositionParameters; + } + + private void applyPlaybackSpeedAndSkipSilence(long presentationTimeUs) { + float playbackSpeed = + configuration.canApplyPlaybackParameters + ? audioProcessorChain.applyPlaybackSpeed(getPlaybackSpeed()) + : DEFAULT_PLAYBACK_SPEED; + boolean skipSilenceEnabled = + configuration.canApplyPlaybackParameters + ? audioProcessorChain.applySkipSilenceEnabled(getSkipSilenceEnabled()) + : DEFAULT_SKIP_SILENCE; + mediaPositionParametersCheckpoints.add( + new MediaPositionParameters( + playbackSpeed, + skipSilenceEnabled, + /* mediaTimeUs= */ Math.max(0, presentationTimeUs), + /* audioTrackPositionUs= */ configuration.framesToDurationUs(getWrittenFrames()))); + setupAudioProcessors(); + if (listener != null) { + listener.onSkipSilenceEnabledChanged(skipSilenceEnabled); } - if (checkpoint != null) { - // We are playing (or about to play) media with the new playback parameters, so update them. - playbackParameters = checkpoint.playbackParameters; - playbackParametersPositionUs = checkpoint.positionUs; - playbackParametersOffsetUs = checkpoint.mediaTimeUs - startMediaTimeUs; + } + + /** + * Applies and updates media position parameters. + * + * @param positionUs The current audio track position, in microseconds. + * @return The current media time, in microseconds. + */ + private long applyMediaPositionParameters(long positionUs) { + while (!mediaPositionParametersCheckpoints.isEmpty() + && positionUs >= mediaPositionParametersCheckpoints.getFirst().audioTrackPositionUs) { + // We are playing (or about to play) media with the new parameters, so update them. + mediaPositionParameters = mediaPositionParametersCheckpoints.remove(); } - if (playbackParameters.speed == 1f) { - return positionUs + playbackParametersOffsetUs - playbackParametersPositionUs; + long playoutDurationSinceLastCheckpoint = + positionUs - mediaPositionParameters.audioTrackPositionUs; + if (mediaPositionParameters.playbackSpeed != 1f) { + if (mediaPositionParametersCheckpoints.isEmpty()) { + playoutDurationSinceLastCheckpoint = + audioProcessorChain.getMediaDuration(playoutDurationSinceLastCheckpoint); + } else { + // Playing data at a previous playback speed, so fall back to multiplying by the speed. + playoutDurationSinceLastCheckpoint = + Util.getMediaDurationForPlayoutDuration( + playoutDurationSinceLastCheckpoint, mediaPositionParameters.playbackSpeed); + } } - - if (playbackParametersCheckpoints.isEmpty()) { - return playbackParametersOffsetUs - + audioProcessorChain.getMediaDuration(positionUs - playbackParametersPositionUs); - } - - // We are playing data at a previous playback speed, so fall back to multiplying by the speed. - return playbackParametersOffsetUs - + Util.getMediaDurationForPlayoutDuration( - positionUs - playbackParametersPositionUs, playbackParameters.speed); + return mediaPositionParameters.mediaTimeUs + playoutDurationSinceLastCheckpoint; } private long applySkipping(long positionUs) { @@ -1129,28 +1201,38 @@ public final class DefaultAudioSink implements AudioSink { private static int getMaximumEncodedRateBytesPerSecond(@C.Encoding int encoding) { switch (encoding) { + case C.ENCODING_MP3: + return MpegAudioUtil.MAX_RATE_BYTES_PER_SECOND; + case C.ENCODING_AAC_LC: + return AacUtil.AAC_LC_MAX_RATE_BYTES_PER_SECOND; + case C.ENCODING_AAC_HE_V1: + return AacUtil.AAC_HE_V1_MAX_RATE_BYTES_PER_SECOND; + case C.ENCODING_AAC_HE_V2: + return AacUtil.AAC_HE_V2_MAX_RATE_BYTES_PER_SECOND; + case C.ENCODING_AAC_XHE: + return AacUtil.AAC_XHE_MAX_RATE_BYTES_PER_SECOND; + case C.ENCODING_AAC_ELD: + return AacUtil.AAC_ELD_MAX_RATE_BYTES_PER_SECOND; case C.ENCODING_AC3: - return 640 * 1000 / 8; + return Ac3Util.AC3_MAX_RATE_BYTES_PER_SECOND; case C.ENCODING_E_AC3: case C.ENCODING_E_AC3_JOC: - return 6144 * 1000 / 8; + return Ac3Util.E_AC3_MAX_RATE_BYTES_PER_SECOND; case C.ENCODING_AC4: - return 2688 * 1000 / 8; + return Ac4Util.MAX_RATE_BYTES_PER_SECOND; case C.ENCODING_DTS: - // DTS allows an 'open' bitrate, but we assume the maximum listed value: 1536 kbit/s. - return 1536 * 1000 / 8; + return DtsUtil.DTS_MAX_RATE_BYTES_PER_SECOND; case C.ENCODING_DTS_HD: - return 18000 * 1000 / 8; + return DtsUtil.DTS_HD_MAX_RATE_BYTES_PER_SECOND; case C.ENCODING_DOLBY_TRUEHD: - return 24500 * 1000 / 8; - case C.ENCODING_INVALID: + return Ac3Util.TRUEHD_MAX_RATE_BYTES_PER_SECOND; case C.ENCODING_PCM_16BIT: + case C.ENCODING_PCM_16BIT_BIG_ENDIAN: 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 C.ENCODING_INVALID: case Format.NO_VALUE: default: throw new IllegalArgumentException(); @@ -1158,33 +1240,55 @@ public final class DefaultAudioSink implements AudioSink { } private static int getFramesPerEncodedSample(@C.Encoding int encoding, ByteBuffer buffer) { - if (encoding == C.ENCODING_DTS || encoding == C.ENCODING_DTS_HD) { - return DtsUtil.parseDtsAudioSampleCount(buffer); - } else if (encoding == C.ENCODING_AC3) { - return Ac3Util.getAc3SyncframeAudioSampleCount(); - } else if (encoding == C.ENCODING_E_AC3 || encoding == C.ENCODING_E_AC3_JOC) { - return Ac3Util.parseEAc3SyncframeAudioSampleCount(buffer); - } else if (encoding == C.ENCODING_AC4) { - return Ac4Util.parseAc4SyncframeAudioSampleCount(buffer); - } else if (encoding == C.ENCODING_DOLBY_TRUEHD) { - int syncframeOffset = Ac3Util.findTrueHdSyncframeOffset(buffer); - return syncframeOffset == C.INDEX_UNSET - ? 0 - : (Ac3Util.parseTrueHdSyncframeAudioSampleCount(buffer, syncframeOffset) - * Ac3Util.TRUEHD_RECHUNK_SAMPLE_COUNT); - } else { - throw new IllegalStateException("Unexpected audio encoding: " + encoding); + switch (encoding) { + case C.ENCODING_MP3: + int headerDataInBigEndian = Util.getBigEndianInt(buffer, buffer.position()); + return MpegAudioUtil.parseMpegAudioFrameSampleCount(headerDataInBigEndian); + case C.ENCODING_AAC_LC: + return AacUtil.AAC_LC_AUDIO_SAMPLE_COUNT; + case C.ENCODING_AAC_HE_V1: + case C.ENCODING_AAC_HE_V2: + return AacUtil.AAC_HE_AUDIO_SAMPLE_COUNT; + case C.ENCODING_AAC_XHE: + return AacUtil.AAC_XHE_AUDIO_SAMPLE_COUNT; + case C.ENCODING_AAC_ELD: + return AacUtil.AAC_LD_AUDIO_SAMPLE_COUNT; + case C.ENCODING_DTS: + case C.ENCODING_DTS_HD: + return DtsUtil.parseDtsAudioSampleCount(buffer); + case C.ENCODING_AC3: + case C.ENCODING_E_AC3: + case C.ENCODING_E_AC3_JOC: + return Ac3Util.parseAc3SyncframeAudioSampleCount(buffer); + case C.ENCODING_AC4: + return Ac4Util.parseAc4SyncframeAudioSampleCount(buffer); + case C.ENCODING_DOLBY_TRUEHD: + int syncframeOffset = Ac3Util.findTrueHdSyncframeOffset(buffer); + return syncframeOffset == C.INDEX_UNSET + ? 0 + : (Ac3Util.parseTrueHdSyncframeAudioSampleCount(buffer, syncframeOffset) + * Ac3Util.TRUEHD_RECHUNK_SAMPLE_COUNT); + case C.ENCODING_PCM_16BIT: + case C.ENCODING_PCM_16BIT_BIG_ENDIAN: + case C.ENCODING_PCM_24BIT: + case C.ENCODING_PCM_32BIT: + case C.ENCODING_PCM_8BIT: + case C.ENCODING_PCM_FLOAT: + case C.ENCODING_INVALID: + case Format.NO_VALUE: + default: + throw new IllegalStateException("Unexpected audio encoding: " + encoding); } } - @TargetApi(21) + @RequiresApi(21) private static int writeNonBlockingV21(AudioTrack audioTrack, ByteBuffer buffer, int size) { return audioTrack.write(buffer, size, WRITE_NON_BLOCKING); } - @TargetApi(21) - private int writeNonBlockingWithAvSyncV21(AudioTrack audioTrack, ByteBuffer buffer, int size, - long presentationTimeUs) { + @RequiresApi(21) + private int writeNonBlockingWithAvSyncV21( + AudioTrack audioTrack, ByteBuffer buffer, int size, long presentationTimeUs) { if (Util.SDK_INT >= 26) { // The underlying platform AudioTrack writes AV sync headers directly. return audioTrack.write(buffer, size, WRITE_NON_BLOCKING, presentationTimeUs * 1000); @@ -1220,7 +1324,7 @@ public final class DefaultAudioSink implements AudioSink { return result; } - @TargetApi(21) + @RequiresApi(21) private static void setVolumeInternalV21(AudioTrack audioTrack, float volume) { audioTrack.setVolume(volume); } @@ -1238,20 +1342,25 @@ public final class DefaultAudioSink implements AudioSink { } } - /** Stores playback parameters with the position and media time at which they apply. */ - private static final class PlaybackParametersCheckpoint { + /** Stores parameters used to calculate the current media position. */ + private static final class MediaPositionParameters { - private final PlaybackParameters playbackParameters; - private final long mediaTimeUs; - private final long positionUs; + /** The playback speed. */ + public final float playbackSpeed; + /** Whether to skip silences. */ + public final boolean skipSilence; + /** The media time from which the playback parameters apply, in microseconds. */ + public final long mediaTimeUs; + /** The audio track position from which the playback parameters apply, in microseconds. */ + public final long audioTrackPositionUs; - private PlaybackParametersCheckpoint(PlaybackParameters playbackParameters, long mediaTimeUs, - long positionUs) { - this.playbackParameters = playbackParameters; + private MediaPositionParameters( + float playbackSpeed, boolean skipSilence, long mediaTimeUs, long audioTrackPositionUs) { + this.playbackSpeed = playbackSpeed; + this.skipSilence = skipSilence; this.mediaTimeUs = mediaTimeUs; - this.positionUs = positionUs; + this.audioTrackPositionUs = audioTrackPositionUs; } - } private final class PositionTrackerListener implements AudioTrackPositionTracker.Listener { @@ -1422,7 +1531,7 @@ public final class DefaultAudioSink implements AudioSink { return audioTrack; } - @TargetApi(21) + @RequiresApi(21) private AudioTrack createAudioTrackV21( boolean tunneling, AudioAttributes audioAttributes, int audioSessionId) { android.media.AudioAttributes attributes; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/FloatResamplingAudioProcessor.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/FloatResamplingAudioProcessor.java index a75e675e6e..ca6b4f3f13 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/FloatResamplingAudioProcessor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/FloatResamplingAudioProcessor.java @@ -16,13 +16,19 @@ package com.google.android.exoplayer2.audio; import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.util.Util; import java.nio.ByteBuffer; /** - * An {@link AudioProcessor} that converts 24-bit and 32-bit integer PCM audio to 32-bit float PCM - * audio. + * An {@link AudioProcessor} that converts high resolution PCM audio to 32-bit float. The following + * encodings are supported as input: + * + *

          + *
        • {@link C#ENCODING_PCM_24BIT} + *
        • {@link C#ENCODING_PCM_32BIT} + *
        • {@link C#ENCODING_PCM_FLOAT} ({@link #isActive()} will return {@code false}) + *
        */ /* package */ final class FloatResamplingAudioProcessor extends BaseAudioProcessor { @@ -32,10 +38,11 @@ import java.nio.ByteBuffer; @Override public AudioFormat onConfigure(AudioFormat inputAudioFormat) throws UnhandledAudioFormatException { - if (!Util.isEncodingHighResolutionIntegerPcm(inputAudioFormat.encoding)) { + @C.PcmEncoding int encoding = inputAudioFormat.encoding; + if (!Util.isEncodingHighResolutionPcm(encoding)) { throw new UnhandledAudioFormatException(inputAudioFormat); } - return Util.isEncodingHighResolutionIntegerPcm(inputAudioFormat.encoding) + return encoding != C.ENCODING_PCM_FLOAT ? new AudioFormat( inputAudioFormat.sampleRate, inputAudioFormat.channelCount, C.ENCODING_PCM_FLOAT) : AudioFormat.NOT_SET; @@ -43,31 +50,42 @@ import java.nio.ByteBuffer; @Override public void queueInput(ByteBuffer inputBuffer) { - Assertions.checkState(Util.isEncodingHighResolutionIntegerPcm(inputAudioFormat.encoding)); - boolean isInput32Bit = inputAudioFormat.encoding == C.ENCODING_PCM_32BIT; int position = inputBuffer.position(); int limit = inputBuffer.limit(); int size = limit - position; - int resampledSize = isInput32Bit ? size : (size / 3) * 4; - ByteBuffer buffer = replaceOutputBuffer(resampledSize); - if (isInput32Bit) { - for (int i = position; i < limit; i += 4) { - int pcm32BitInteger = - (inputBuffer.get(i) & 0xFF) - | ((inputBuffer.get(i + 1) & 0xFF) << 8) - | ((inputBuffer.get(i + 2) & 0xFF) << 16) - | ((inputBuffer.get(i + 3) & 0xFF) << 24); - writePcm32BitFloat(pcm32BitInteger, buffer); - } - } else { // Input is 24-bit PCM. - for (int i = position; i < limit; i += 3) { - int pcm32BitInteger = - ((inputBuffer.get(i) & 0xFF) << 8) - | ((inputBuffer.get(i + 1) & 0xFF) << 16) - | ((inputBuffer.get(i + 2) & 0xFF) << 24); - writePcm32BitFloat(pcm32BitInteger, buffer); - } + ByteBuffer buffer; + switch (inputAudioFormat.encoding) { + case C.ENCODING_PCM_24BIT: + buffer = replaceOutputBuffer((size / 3) * 4); + for (int i = position; i < limit; i += 3) { + int pcm32BitInteger = + ((inputBuffer.get(i) & 0xFF) << 8) + | ((inputBuffer.get(i + 1) & 0xFF) << 16) + | ((inputBuffer.get(i + 2) & 0xFF) << 24); + writePcm32BitFloat(pcm32BitInteger, buffer); + } + break; + case C.ENCODING_PCM_32BIT: + buffer = replaceOutputBuffer(size); + for (int i = position; i < limit; i += 4) { + int pcm32BitInteger = + (inputBuffer.get(i) & 0xFF) + | ((inputBuffer.get(i + 1) & 0xFF) << 8) + | ((inputBuffer.get(i + 2) & 0xFF) << 16) + | ((inputBuffer.get(i + 3) & 0xFF) << 24); + writePcm32BitFloat(pcm32BitInteger, buffer); + } + break; + case C.ENCODING_PCM_8BIT: + case C.ENCODING_PCM_16BIT: + case C.ENCODING_PCM_16BIT_BIG_ENDIAN: + case C.ENCODING_PCM_FLOAT: + case C.ENCODING_INVALID: + case Format.NO_VALUE: + default: + // Never happens. + throw new IllegalStateException(); } inputBuffer.position(inputBuffer.limit()); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/ForwardingAudioSink.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/ForwardingAudioSink.java new file mode 100644 index 0000000000..7ab1cdade4 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/ForwardingAudioSink.java @@ -0,0 +1,181 @@ +/* + * Copyright (C) 2020 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 androidx.annotation.Nullable; +import com.google.android.exoplayer2.PlaybackParameters; +import java.nio.ByteBuffer; + +/** An overridable {@link AudioSink} implementation forwarding all methods to another sink. */ +public class ForwardingAudioSink implements AudioSink { + + private final AudioSink sink; + + public ForwardingAudioSink(AudioSink sink) { + this.sink = sink; + } + + @Override + public void setListener(Listener listener) { + sink.setListener(listener); + } + + @Override + public boolean supportsOutput(int channelCount, int encoding) { + return sink.supportsOutput(channelCount, encoding); + } + + @Override + public long getCurrentPositionUs(boolean sourceEnded) { + return sink.getCurrentPositionUs(sourceEnded); + } + + @Override + public void configure( + int inputEncoding, + int inputChannelCount, + int inputSampleRate, + int specifiedBufferSize, + @Nullable int[] outputChannels, + int trimStartFrames, + int trimEndFrames) + throws ConfigurationException { + sink.configure( + inputEncoding, + inputChannelCount, + inputSampleRate, + specifiedBufferSize, + outputChannels, + trimStartFrames, + trimEndFrames); + } + + @Override + public void play() { + sink.play(); + } + + @Override + public void handleDiscontinuity() { + sink.handleDiscontinuity(); + } + + @Override + public boolean handleBuffer( + ByteBuffer buffer, long presentationTimeUs, int encodedAccessUnitCount) + throws InitializationException, WriteException { + return sink.handleBuffer(buffer, presentationTimeUs, encodedAccessUnitCount); + } + + @Override + public void playToEndOfStream() throws WriteException { + sink.playToEndOfStream(); + } + + @Override + public boolean isEnded() { + return sink.isEnded(); + } + + @Override + public boolean hasPendingData() { + return sink.hasPendingData(); + } + + /** + * @deprecated Use {@link #setPlaybackSpeed(float)} and {@link #setSkipSilenceEnabled(boolean)} + * instead. + */ + @SuppressWarnings("deprecation") + @Deprecated + @Override + public void setPlaybackParameters(PlaybackParameters playbackParameters) { + sink.setPlaybackParameters(playbackParameters); + } + + /** @deprecated Use {@link #getPlaybackSpeed()} and {@link #getSkipSilenceEnabled()} instead. */ + @SuppressWarnings("deprecation") + @Deprecated + @Override + public PlaybackParameters getPlaybackParameters() { + return sink.getPlaybackParameters(); + } + + @Override + public void setPlaybackSpeed(float playbackSpeed) { + sink.setPlaybackSpeed(playbackSpeed); + } + + @Override + public float getPlaybackSpeed() { + return sink.getPlaybackSpeed(); + } + + @Override + public void setSkipSilenceEnabled(boolean skipSilenceEnabled) { + sink.setSkipSilenceEnabled(skipSilenceEnabled); + } + + @Override + public boolean getSkipSilenceEnabled() { + return sink.getSkipSilenceEnabled(); + } + + @Override + public void setAudioAttributes(AudioAttributes audioAttributes) { + sink.setAudioAttributes(audioAttributes); + } + + @Override + public void setAudioSessionId(int audioSessionId) { + sink.setAudioSessionId(audioSessionId); + } + + @Override + public void setAuxEffectInfo(AuxEffectInfo auxEffectInfo) { + sink.setAuxEffectInfo(auxEffectInfo); + } + + @Override + public void enableTunnelingV21(int tunnelingAudioSessionId) { + sink.enableTunnelingV21(tunnelingAudioSessionId); + } + + @Override + public void disableTunneling() { + sink.disableTunneling(); + } + + @Override + public void setVolume(float volume) { + sink.setVolume(volume); + } + + @Override + public void pause() { + sink.pause(); + } + + @Override + public void flush() { + sink.flush(); + } + + @Override + public void reset() { + sink.reset(); + } +} 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 d5927196c0..10b727bb3c 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 @@ -22,27 +22,22 @@ import android.media.MediaCrypto; import android.media.MediaFormat; import android.media.audiofx.Virtualizer; import android.os.Handler; -import androidx.annotation.CallSuper; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.FormatHolder; -import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.PlayerMessage.Target; +import com.google.android.exoplayer2.RendererCapabilities; import com.google.android.exoplayer2.audio.AudioRendererEventListener.EventDispatcher; import com.google.android.exoplayer2.decoder.DecoderInputBuffer; -import com.google.android.exoplayer2.drm.DrmSessionManager; -import com.google.android.exoplayer2.drm.FrameworkMediaCrypto; import com.google.android.exoplayer2.mediacodec.MediaCodecInfo; import com.google.android.exoplayer2.mediacodec.MediaCodecRenderer; import com.google.android.exoplayer2.mediacodec.MediaCodecSelector; import com.google.android.exoplayer2.mediacodec.MediaCodecUtil; import com.google.android.exoplayer2.mediacodec.MediaCodecUtil.DecoderQueryException; import com.google.android.exoplayer2.mediacodec.MediaFormatUtil; -import com.google.android.exoplayer2.source.MediaSource; -import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.MediaClock; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.Util; @@ -58,86 +53,52 @@ import java.util.List; * on the playback thread: * *
          - *
        • Message with type {@link C#MSG_SET_VOLUME} to set the volume. The message payload should be + *
        • Message with type {@link #MSG_SET_VOLUME} to set the volume. The message payload should be * a {@link Float} with 0 being silence and 1 being unity gain. - *
        • Message with type {@link C#MSG_SET_AUDIO_ATTRIBUTES} to set the audio attributes. The + *
        • Message with type {@link #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 + *
        • Message with type {@link #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. + *
        • Message with type {@link #MSG_SET_SKIP_SILENCE_ENABLED} to enable or disable skipping + * silences. The message payload should be a {@link Boolean}. + *
        • Message with type {@link #MSG_SET_AUDIO_SESSION_ID} to set the audio session ID. The + * message payload should be a session ID {@link Integer} that will be attached to the * underlying audio track. *
        */ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements MediaClock { - /** - * Maximum number of tracked pending stream change times. Generally there is zero or one pending - * stream change. We track more to allow for pending changes that have fewer samples than the - * codec latency. - */ - private static final int MAX_PENDING_STREAM_CHANGE_COUNT = 10; - private static final String TAG = "MediaCodecAudioRenderer"; + /** + * Custom key used to indicate bits per sample by some decoders on Vivo devices. For example + * OMX.vivo.alac.decoder on the Vivo Z1 Pro. + */ + private static final String VIVO_BITS_PER_SAMPLE_KEY = "v-bits-per-sample"; private final Context context; private final EventDispatcher eventDispatcher; private final AudioSink audioSink; - private final long[] pendingStreamChangeTimesUs; private int codecMaxInputSize; private boolean passthroughEnabled; private boolean codecNeedsDiscardChannelsWorkaround; private boolean codecNeedsEosBufferTimestampWorkaround; private android.media.MediaFormat passthroughMediaFormat; - private @C.Encoding int pcmEncoding; - private int channelCount; - private int encoderDelay; - private int encoderPadding; + @Nullable private Format inputFormat; private long currentPositionUs; private boolean allowFirstBufferPositionDiscontinuity; private boolean allowPositionDiscontinuity; - private long lastInputTimeUs; - private int pendingStreamChangeCount; /** * @param context A context. * @param mediaCodecSelector A decoder selector. */ - @SuppressWarnings("deprecation") public MediaCodecAudioRenderer(Context context, MediaCodecSelector mediaCodecSelector) { this( context, mediaCodecSelector, - /* drmSessionManager= */ null, - /* playClearSamplesWithoutKeys= */ false); - } - - /** - * @param context A context. - * @param mediaCodecSelector A decoder selector. - * @param drmSessionManager For use with encrypted content. May be null if support for encrypted - * content is not required. - * @param playClearSamplesWithoutKeys Encrypted media may contain clear (un-encrypted) regions. - * For example a media file may start with a short clear region so as to allow playback to - * 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. - * @deprecated Use {@link #MediaCodecAudioRenderer(Context, MediaCodecSelector, boolean, Handler, - * AudioRendererEventListener, AudioSink)} instead, and pass DRM-related parameters to the - * {@link MediaSource} factories. - */ - @Deprecated - @SuppressWarnings("deprecation") - public MediaCodecAudioRenderer( - Context context, - MediaCodecSelector mediaCodecSelector, - @Nullable DrmSessionManager drmSessionManager, - boolean playClearSamplesWithoutKeys) { - this( - context, - mediaCodecSelector, - drmSessionManager, - playClearSamplesWithoutKeys, /* eventHandler= */ null, /* eventListener= */ null); } @@ -149,7 +110,6 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media * null if delivery of events is not required. * @param eventListener A listener of events. May be null if delivery of events is not required. */ - @SuppressWarnings("deprecation") public MediaCodecAudioRenderer( Context context, MediaCodecSelector mediaCodecSelector, @@ -158,43 +118,6 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media this( context, mediaCodecSelector, - /* drmSessionManager= */ null, - /* playClearSamplesWithoutKeys= */ false, - eventHandler, - eventListener); - } - - /** - * @param context A context. - * @param mediaCodecSelector A decoder selector. - * @param drmSessionManager For use with encrypted content. May be null if support for encrypted - * content is not required. - * @param playClearSamplesWithoutKeys Encrypted media may contain clear (un-encrypted) regions. - * For example a media file may start with a short clear region so as to allow playback to - * 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 eventHandler A handler to use when delivering events to {@code eventListener}. 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. - * @deprecated Use {@link #MediaCodecAudioRenderer(Context, MediaCodecSelector, boolean, Handler, - * AudioRendererEventListener, AudioSink)} instead, and pass DRM-related parameters to the - * {@link MediaSource} factories. - */ - @Deprecated - @SuppressWarnings("deprecation") - public MediaCodecAudioRenderer( - Context context, - MediaCodecSelector mediaCodecSelector, - @Nullable DrmSessionManager drmSessionManager, - boolean playClearSamplesWithoutKeys, - @Nullable Handler eventHandler, - @Nullable AudioRendererEventListener eventListener) { - this( - context, - mediaCodecSelector, - drmSessionManager, - playClearSamplesWithoutKeys, eventHandler, eventListener, (AudioCapabilities) null); @@ -203,13 +126,6 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media /** * @param context A context. * @param mediaCodecSelector A decoder selector. - * @param drmSessionManager For use with encrypted content. May be null if support for encrypted - * content is not required. - * @param playClearSamplesWithoutKeys Encrypted media may contain clear (un-encrypted) regions. - * For example a media file may start with a short clear region so as to allow playback to - * 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 eventHandler A handler to use when delivering events to {@code eventListener}. 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. @@ -217,17 +133,10 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media * default capabilities (no encoded audio passthrough support) should be assumed. * @param audioProcessors Optional {@link AudioProcessor}s that will process PCM audio before * output. - * @deprecated Use {@link #MediaCodecAudioRenderer(Context, MediaCodecSelector, boolean, Handler, - * AudioRendererEventListener, AudioSink)} instead, and pass DRM-related parameters to the - * {@link MediaSource} factories. */ - @Deprecated - @SuppressWarnings("deprecation") public MediaCodecAudioRenderer( Context context, MediaCodecSelector mediaCodecSelector, - @Nullable DrmSessionManager drmSessionManager, - boolean playClearSamplesWithoutKeys, @Nullable Handler eventHandler, @Nullable AudioRendererEventListener eventListener, @Nullable AudioCapabilities audioCapabilities, @@ -235,8 +144,6 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media this( context, mediaCodecSelector, - drmSessionManager, - playClearSamplesWithoutKeys, eventHandler, eventListener, new DefaultAudioSink(audioCapabilities, audioProcessors)); @@ -245,36 +152,20 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media /** * @param context A context. * @param mediaCodecSelector A decoder selector. - * @param drmSessionManager For use with encrypted content. May be null if support for encrypted - * content is not required. - * @param playClearSamplesWithoutKeys Encrypted media may contain clear (un-encrypted) regions. - * For example a media file may start with a short clear region so as to allow playback to - * 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 eventHandler A handler to use when delivering events to {@code eventListener}. 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 audioSink The sink to which audio will be output. - * @deprecated Use {@link #MediaCodecAudioRenderer(Context, MediaCodecSelector, boolean, Handler, - * AudioRendererEventListener, AudioSink)} instead, and pass DRM-related parameters to the - * {@link MediaSource} factories. */ - @Deprecated - @SuppressWarnings("deprecation") public MediaCodecAudioRenderer( Context context, MediaCodecSelector mediaCodecSelector, - @Nullable DrmSessionManager drmSessionManager, - boolean playClearSamplesWithoutKeys, @Nullable Handler eventHandler, @Nullable AudioRendererEventListener eventListener, AudioSink audioSink) { this( context, mediaCodecSelector, - drmSessionManager, - playClearSamplesWithoutKeys, /* enableDecoderFallback= */ false, eventHandler, eventListener, @@ -292,7 +183,6 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media * @param eventListener A listener of events. May be null if delivery of events is not required. * @param audioSink The sink to which audio will be output. */ - @SuppressWarnings("deprecation") public MediaCodecAudioRenderer( Context context, MediaCodecSelector mediaCodecSelector, @@ -300,107 +190,64 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media @Nullable Handler eventHandler, @Nullable AudioRendererEventListener eventListener, AudioSink audioSink) { - this( - context, - mediaCodecSelector, - /* drmSessionManager= */ null, - /* playClearSamplesWithoutKeys= */ false, - enableDecoderFallback, - eventHandler, - eventListener, - audioSink); - } - - /** - * @param context A context. - * @param mediaCodecSelector A decoder selector. - * @param drmSessionManager For use with encrypted content. May be null if support for encrypted - * content is not required. - * @param playClearSamplesWithoutKeys Encrypted media may contain clear (un-encrypted) regions. - * For example a media file may start with a short clear region so as to allow playback to - * 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 enableDecoderFallback Whether to enable fallback to lower-priority decoders if decoder - * initialization fails. This may result in using a decoder that is slower/less efficient than - * the primary decoder. - * @param eventHandler A handler to use when delivering events to {@code eventListener}. 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 audioSink The sink to which audio will be output. - * @deprecated Use {@link #MediaCodecAudioRenderer(Context, MediaCodecSelector, boolean, Handler, - * AudioRendererEventListener, AudioSink)} instead, and pass DRM-related parameters to the - * {@link MediaSource} factories. - */ - @Deprecated - public MediaCodecAudioRenderer( - Context context, - MediaCodecSelector mediaCodecSelector, - @Nullable DrmSessionManager drmSessionManager, - boolean playClearSamplesWithoutKeys, - boolean enableDecoderFallback, - @Nullable Handler eventHandler, - @Nullable AudioRendererEventListener eventListener, - AudioSink audioSink) { super( C.TRACK_TYPE_AUDIO, mediaCodecSelector, - drmSessionManager, - playClearSamplesWithoutKeys, enableDecoderFallback, /* assumedMinimumCodecOperatingRate= */ 44100); this.context = context.getApplicationContext(); this.audioSink = audioSink; - lastInputTimeUs = C.TIME_UNSET; - pendingStreamChangeTimesUs = new long[MAX_PENDING_STREAM_CHANGE_COUNT]; eventDispatcher = new EventDispatcher(eventHandler, eventListener); audioSink.setListener(new AudioSinkListener()); } @Override - protected int supportsFormat( - MediaCodecSelector mediaCodecSelector, - @Nullable DrmSessionManager drmSessionManager, - Format format) + public String getName() { + return TAG; + } + + @Override + @Capabilities + protected int supportsFormat(MediaCodecSelector mediaCodecSelector, Format format) throws DecoderQueryException { String mimeType = format.sampleMimeType; if (!MimeTypes.isAudio(mimeType)) { - return FORMAT_UNSUPPORTED_TYPE; + return RendererCapabilities.create(FORMAT_UNSUPPORTED_TYPE); } + @TunnelingSupport int tunnelingSupport = Util.SDK_INT >= 21 ? TUNNELING_SUPPORTED : TUNNELING_NOT_SUPPORTED; - boolean supportsFormatDrm = - format.drmInitData == null - || FrameworkMediaCrypto.class.equals(format.exoMediaCryptoType) - || (format.exoMediaCryptoType == null - && supportsFormatDrm(drmSessionManager, format.drmInitData)); + boolean supportsFormatDrm = supportsFormatDrm(format); if (supportsFormatDrm - && allowPassthrough(format.channelCount, mimeType) - && mediaCodecSelector.getPassthroughDecoderInfo() != null) { - return ADAPTIVE_NOT_SEAMLESS | tunnelingSupport | FORMAT_HANDLED; + && usePassthrough(format.channelCount, mimeType) + // A Passthrough decoder is only needed to decode the DRM encryption. + && (format.drmInitData == null || MediaCodecUtil.getPassthroughDecoderInfo() != null)) { + return RendererCapabilities.create(FORMAT_HANDLED, ADAPTIVE_NOT_SEAMLESS, tunnelingSupport); } if ((MimeTypes.AUDIO_RAW.equals(mimeType) && !audioSink.supportsOutput(format.channelCount, format.pcmEncoding)) || !audioSink.supportsOutput(format.channelCount, C.ENCODING_PCM_16BIT)) { // Assume the decoder outputs 16-bit PCM, unless the input is raw. - return FORMAT_UNSUPPORTED_SUBTYPE; + return RendererCapabilities.create(FORMAT_UNSUPPORTED_SUBTYPE); } List decoderInfos = getDecoderInfos(mediaCodecSelector, format, /* requiresSecureDecoder= */ false); if (decoderInfos.isEmpty()) { - return FORMAT_UNSUPPORTED_SUBTYPE; + return RendererCapabilities.create(FORMAT_UNSUPPORTED_SUBTYPE); } if (!supportsFormatDrm) { - return FORMAT_UNSUPPORTED_DRM; + return RendererCapabilities.create(FORMAT_UNSUPPORTED_DRM); } // Check capabilities for the first decoder in the list, which takes priority. MediaCodecInfo decoderInfo = decoderInfos.get(0); boolean isFormatSupported = decoderInfo.isFormatSupported(format); + @AdaptiveSupport int adaptiveSupport = isFormatSupported && decoderInfo.isSeamlessAdaptationSupported(format) ? ADAPTIVE_SEAMLESS : ADAPTIVE_NOT_SEAMLESS; + @FormatSupport int formatSupport = isFormatSupported ? FORMAT_HANDLED : FORMAT_EXCEEDS_CAPABILITIES; - return adaptiveSupport | tunnelingSupport | formatSupport; + return RendererCapabilities.create(formatSupport, adaptiveSupport, tunnelingSupport); } @Override @@ -411,11 +258,10 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media if (mimeType == null) { return Collections.emptyList(); } - if (allowPassthrough(format.channelCount, mimeType)) { - @Nullable - MediaCodecInfo passthroughDecoderInfo = mediaCodecSelector.getPassthroughDecoderInfo(); - if (passthroughDecoderInfo != null) { - return Collections.singletonList(passthroughDecoderInfo); + if (usePassthrough(format.channelCount, mimeType)) { + @Nullable MediaCodecInfo codecInfo = MediaCodecUtil.getPassthroughDecoderInfo(); + if (codecInfo != null) { + return Collections.singletonList(codecInfo); } } List decoderInfos = @@ -433,17 +279,8 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media return Collections.unmodifiableList(decoderInfos); } - /** - * Returns whether encoded audio passthrough should be used for playing back the input format. - * This implementation returns true if the {@link AudioSink} indicates that encoded audio output - * is supported. - * - * @param channelCount The number of channels in the input media, or {@link Format#NO_VALUE} if - * not known. - * @param mimeType The type of input media. - * @return Whether passthrough playback is supported. - */ - protected boolean allowPassthrough(int channelCount, String mimeType) { + @Override + protected boolean usePassthrough(int channelCount, String mimeType) { return getPassthroughEncoding(channelCount, mimeType) != C.ENCODING_INVALID; } @@ -457,10 +294,11 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media codecMaxInputSize = getCodecMaxInputSize(codecInfo, format, getStreamFormats()); codecNeedsDiscardChannelsWorkaround = codecNeedsDiscardChannelsWorkaround(codecInfo.name); codecNeedsEosBufferTimestampWorkaround = codecNeedsEosBufferTimestampWorkaround(codecInfo.name); - passthroughEnabled = codecInfo.passthrough; - String codecMimeType = passthroughEnabled ? MimeTypes.AUDIO_RAW : codecInfo.codecMimeType; + passthroughEnabled = + MimeTypes.AUDIO_RAW.equals(codecInfo.mimeType) + && !MimeTypes.AUDIO_RAW.equals(format.sampleMimeType); MediaFormat mediaFormat = - getMediaFormat(format, codecMimeType, codecMaxInputSize, codecOperatingRate); + getMediaFormat(format, codecInfo.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. @@ -518,6 +356,7 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media } @Override + @Nullable public MediaClock getMediaClock() { return this; } @@ -546,15 +385,8 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media @Override protected void onInputFormatChanged(FormatHolder formatHolder) throws ExoPlaybackException { super.onInputFormatChanged(formatHolder); - Format newFormat = formatHolder.format; - eventDispatcher.inputFormatChanged(newFormat); - // If the input format is anything other than PCM then we assume that the audio decoder will - // output 16-bit PCM. - pcmEncoding = MimeTypes.AUDIO_RAW.equals(newFormat.sampleMimeType) ? newFormat.pcmEncoding - : C.ENCODING_PCM_16BIT; - channelCount = newFormat.channelCount; - encoderDelay = newFormat.encoderDelay; - encoderPadding = newFormat.encoderPadding; + inputFormat = formatHolder.format; + eventDispatcher.inputFormatChanged(inputFormat); } @Override @@ -570,26 +402,32 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media mediaFormat.getString(MediaFormat.KEY_MIME)); } else { mediaFormat = outputMediaFormat; - encoding = pcmEncoding; + if (outputMediaFormat.containsKey(VIVO_BITS_PER_SAMPLE_KEY)) { + encoding = Util.getPcmEncoding(outputMediaFormat.getInteger(VIVO_BITS_PER_SAMPLE_KEY)); + } else { + encoding = getPcmEncoding(inputFormat); + } } int channelCount = mediaFormat.getInteger(MediaFormat.KEY_CHANNEL_COUNT); int sampleRate = mediaFormat.getInteger(MediaFormat.KEY_SAMPLE_RATE); int[] channelMap; - if (codecNeedsDiscardChannelsWorkaround && channelCount == 6 && this.channelCount < 6) { - channelMap = new int[this.channelCount]; - for (int i = 0; i < this.channelCount; i++) { + if (codecNeedsDiscardChannelsWorkaround && channelCount == 6 && inputFormat.channelCount < 6) { + channelMap = new int[inputFormat.channelCount]; + for (int i = 0; i < inputFormat.channelCount; i++) { channelMap[i] = i; } } else { channelMap = null; } + configureAudioSink(encoding, channelCount, sampleRate, channelMap); + } - try { - audioSink.configure(encoding, channelCount, sampleRate, 0, channelMap, encoderDelay, - encoderPadding); - } catch (AudioSink.ConfigurationException e) { - throw ExoPlaybackException.createForRenderer(e, getIndex()); - } + @Override + protected void onOutputPassthroughFormatChanged(Format outputFormat) throws ExoPlaybackException { + @C.Encoding + int encoding = getPassthroughEncoding(outputFormat.channelCount, outputFormat.sampleMimeType); + configureAudioSink( + encoding, outputFormat.channelCount, outputFormat.sampleRate, /* channelMap= */ null); } /** @@ -598,6 +436,10 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media */ @C.Encoding protected int getPassthroughEncoding(int channelCount, String mimeType) { + if (MimeTypes.AUDIO_RAW.equals(mimeType)) { + // PCM passthrough is not supported. + return C.ENCODING_INVALID; + } if (MimeTypes.AUDIO_E_AC3_JOC.equals(mimeType)) { // E-AC3 JOC is object-based so the output channel count is arbitrary. if (audioSink.supportsOutput(/* channelCount= */ Format.NO_VALUE, C.ENCODING_E_AC3_JOC)) { @@ -621,30 +463,32 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media * order to spatialize the audio channels. For this use case, any {@link Virtualizer} instances * should be released in {@link #onDisabled()} (if not before). * - * @see AudioSink.Listener#onAudioSessionId(int) + *

        See {@link AudioSink.Listener#onAudioSessionId(int)}. */ protected void onAudioSessionId(int audioSessionId) { // Do nothing. } - /** - * @see AudioSink.Listener#onPositionDiscontinuity() - */ + /** See {@link AudioSink.Listener#onPositionDiscontinuity()}. */ protected void onAudioTrackPositionDiscontinuity() { // Do nothing. } - /** - * @see AudioSink.Listener#onUnderrun(int, long, long) - */ - protected void onAudioTrackUnderrun(int bufferSize, long bufferSizeMs, - long elapsedSinceLastFeedMs) { + /** See {@link AudioSink.Listener#onUnderrun(int, long, long)}. */ + protected void onAudioTrackUnderrun( + int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) { + // Do nothing. + } + + /** See {@link AudioSink.Listener#onSkipSilenceEnabledChanged(boolean)}. */ + protected void onAudioTrackSkipSilenceEnabledChanged(boolean skipSilenceEnabled) { // Do nothing. } @Override - protected void onEnabled(boolean joining) throws ExoPlaybackException { - super.onEnabled(joining); + protected void onEnabled(boolean joining, boolean mayRenderStartOfStream) + throws ExoPlaybackException { + super.onEnabled(joining, mayRenderStartOfStream); eventDispatcher.enabled(decoderCounters); int tunnelingAudioSessionId = getConfiguration().tunnelingAudioSessionId; if (tunnelingAudioSessionId != C.AUDIO_SESSION_ID_UNSET) { @@ -654,22 +498,6 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media } } - @Override - protected void onStreamChanged(Format[] formats, long offsetUs) throws ExoPlaybackException { - super.onStreamChanged(formats, offsetUs); - if (lastInputTimeUs != C.TIME_UNSET) { - if (pendingStreamChangeCount == pendingStreamChangeTimesUs.length) { - Log.w( - TAG, - "Too many stream changes, so dropping change at " - + pendingStreamChangeTimesUs[pendingStreamChangeCount - 1]); - } else { - pendingStreamChangeCount++; - } - pendingStreamChangeTimesUs[pendingStreamChangeCount - 1] = lastInputTimeUs; - } - } - @Override protected void onPositionReset(long positionUs, boolean joining) throws ExoPlaybackException { super.onPositionReset(positionUs, joining); @@ -677,8 +505,6 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media currentPositionUs = positionUs; allowFirstBufferPositionDiscontinuity = true; allowPositionDiscontinuity = true; - lastInputTimeUs = C.TIME_UNSET; - pendingStreamChangeCount = 0; } @Override @@ -697,8 +523,6 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media @Override protected void onDisabled() { try { - lastInputTimeUs = C.TIME_UNSET; - pendingStreamChangeCount = 0; audioSink.flush(); } finally { try { @@ -737,13 +561,13 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media } @Override - public void setPlaybackParameters(PlaybackParameters playbackParameters) { - audioSink.setPlaybackParameters(playbackParameters); + public void setPlaybackSpeed(float playbackSpeed) { + audioSink.setPlaybackSpeed(playbackSpeed); } @Override - public PlaybackParameters getPlaybackParameters() { - return audioSink.getPlaybackParameters(); + public float getPlaybackSpeed() { + return audioSink.getPlaybackSpeed(); } @Override @@ -757,42 +581,34 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media } allowFirstBufferPositionDiscontinuity = false; } - lastInputTimeUs = Math.max(buffer.timeUs, lastInputTimeUs); } - @CallSuper @Override - protected void onProcessedOutputBuffer(long presentationTimeUs) { - while (pendingStreamChangeCount != 0 && presentationTimeUs >= pendingStreamChangeTimesUs[0]) { - audioSink.handleDiscontinuity(); - pendingStreamChangeCount--; - System.arraycopy( - pendingStreamChangeTimesUs, - /* srcPos= */ 1, - pendingStreamChangeTimesUs, - /* destPos= */ 0, - pendingStreamChangeCount); - } + protected void onProcessedStreamChange() { + super.onProcessedStreamChange(); + audioSink.handleDiscontinuity(); } @Override protected boolean processOutputBuffer( long positionUs, long elapsedRealtimeUs, - MediaCodec codec, + @Nullable MediaCodec codec, ByteBuffer buffer, int bufferIndex, int bufferFlags, + int sampleCount, long bufferPresentationTimeUs, boolean isDecodeOnlyBuffer, boolean isLastBuffer, Format format) throws ExoPlaybackException { - if (codecNeedsEosBufferTimestampWorkaround + if (codec != null + && codecNeedsEosBufferTimestampWorkaround && bufferPresentationTimeUs == 0 && (bufferFlags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0 - && lastInputTimeUs != C.TIME_UNSET) { - bufferPresentationTimeUs = lastInputTimeUs; + && getLargestQueuedPresentationTimeUs() != C.TIME_UNSET) { + bufferPresentationTimeUs = getLargestQueuedPresentationTimeUs(); } if (passthroughEnabled && (bufferFlags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) { @@ -802,21 +618,30 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media } if (isDecodeOnlyBuffer) { - codec.releaseOutputBuffer(bufferIndex, false); + if (codec != null) { + codec.releaseOutputBuffer(bufferIndex, false); + } decoderCounters.skippedOutputBufferCount++; audioSink.handleDiscontinuity(); return true; } + boolean fullyConsumed; try { - if (audioSink.handleBuffer(buffer, bufferPresentationTimeUs)) { - codec.releaseOutputBuffer(bufferIndex, false); - decoderCounters.renderedOutputBufferCount++; - return true; - } + fullyConsumed = audioSink.handleBuffer(buffer, bufferPresentationTimeUs, sampleCount); } catch (AudioSink.InitializationException | AudioSink.WriteException e) { - throw ExoPlaybackException.createForRenderer(e, getIndex()); + // TODO(internal: b/145658993) Use outputFormat instead. + throw createRendererException(e, inputFormat); } + + if (fullyConsumed) { + if (codec != null) { + codec.releaseOutputBuffer(bufferIndex, false); + } + decoderCounters.renderedOutputBufferCount++; + return true; + } + return false; } @@ -825,24 +650,31 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media try { audioSink.playToEndOfStream(); } catch (AudioSink.WriteException e) { - throw ExoPlaybackException.createForRenderer(e, getIndex()); + // TODO(internal: b/145658993) Use outputFormat instead. + throw createRendererException(e, inputFormat); } } @Override public void handleMessage(int messageType, @Nullable Object message) throws ExoPlaybackException { switch (messageType) { - case C.MSG_SET_VOLUME: + case MSG_SET_VOLUME: audioSink.setVolume((Float) message); break; - case C.MSG_SET_AUDIO_ATTRIBUTES: + case MSG_SET_AUDIO_ATTRIBUTES: AudioAttributes audioAttributes = (AudioAttributes) message; audioSink.setAudioAttributes(audioAttributes); break; - case C.MSG_SET_AUX_EFFECT_INFO: + case MSG_SET_AUX_EFFECT_INFO: AuxEffectInfo auxEffectInfo = (AuxEffectInfo) message; audioSink.setAuxEffectInfo(auxEffectInfo); break; + case MSG_SET_SKIP_SILENCE_ENABLED: + audioSink.setSkipSilenceEnabled((Boolean) message); + break; + case MSG_SET_AUDIO_SESSION_ID: + audioSink.setAudioSessionId((Integer) message); + break; default: super.handleMessage(messageType, message); break; @@ -933,6 +765,24 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media return mediaFormat; } + private void configureAudioSink( + int encoding, int channelCount, int sampleRate, @Nullable int[] channelMap) + throws ExoPlaybackException { + try { + audioSink.configure( + encoding, + channelCount, + sampleRate, + /* specifiedBufferSize= */ 0, + channelMap, + inputFormat.encoderDelay, + inputFormat.encoderPadding); + } catch (AudioSink.ConfigurationException e) { + // TODO(internal: b/145658993) Use outputFormat instead. + throw createRendererException(e, inputFormat); + } + } + private void updateCurrentPosition() { long newCurrentPositionUs = audioSink.getCurrentPositionUs(isEnded()); if (newCurrentPositionUs != AudioSink.CURRENT_POSITION_NOT_SET) { @@ -987,6 +837,15 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media || Util.DEVICE.startsWith("ms01")); } + @C.Encoding + private static int getPcmEncoding(Format format) { + // If the format is anything other than PCM then we assume that the audio decoder will output + // 16-bit PCM. + return MimeTypes.AUDIO_RAW.equals(format.sampleMimeType) + ? format.pcmEncoding + : C.ENCODING_PCM_16BIT; + } + private final class AudioSinkListener implements AudioSink.Listener { @Override @@ -1008,6 +867,10 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media onAudioTrackUnderrun(bufferSize, bufferSizeMs, elapsedSinceLastFeedMs); } + @Override + public void onSkipSilenceEnabledChanged(boolean skipSilenceEnabled) { + eventDispatcher.skipSilenceEnabledChanged(skipSilenceEnabled); + onAudioTrackSkipSilenceEnabledChanged(skipSilenceEnabled); + } } - } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/ResamplingAudioProcessor.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/ResamplingAudioProcessor.java index 1bfa1897c8..883f5bcb92 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/ResamplingAudioProcessor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/ResamplingAudioProcessor.java @@ -20,8 +20,17 @@ import com.google.android.exoplayer2.Format; import java.nio.ByteBuffer; /** - * An {@link AudioProcessor} that converts 8-bit, 24-bit and 32-bit integer PCM audio to 16-bit - * integer PCM audio. + * An {@link AudioProcessor} that converts different PCM audio encodings to 16-bit integer PCM. The + * following encodings are supported as input: + * + *

          + *
        • {@link C#ENCODING_PCM_8BIT} + *
        • {@link C#ENCODING_PCM_16BIT} ({@link #isActive()} will return {@code false}) + *
        • {@link C#ENCODING_PCM_16BIT_BIG_ENDIAN} + *
        • {@link C#ENCODING_PCM_24BIT} + *
        • {@link C#ENCODING_PCM_32BIT} + *
        • {@link C#ENCODING_PCM_FLOAT} + *
        */ /* package */ final class ResamplingAudioProcessor extends BaseAudioProcessor { @@ -29,8 +38,12 @@ import java.nio.ByteBuffer; public AudioFormat onConfigure(AudioFormat inputAudioFormat) throws UnhandledAudioFormatException { @C.PcmEncoding int encoding = inputAudioFormat.encoding; - if (encoding != C.ENCODING_PCM_8BIT && encoding != C.ENCODING_PCM_16BIT - && encoding != C.ENCODING_PCM_24BIT && encoding != C.ENCODING_PCM_32BIT) { + if (encoding != C.ENCODING_PCM_8BIT + && encoding != C.ENCODING_PCM_16BIT + && encoding != C.ENCODING_PCM_16BIT_BIG_ENDIAN + && encoding != C.ENCODING_PCM_24BIT + && encoding != C.ENCODING_PCM_32BIT + && encoding != C.ENCODING_PCM_FLOAT) { throw new UnhandledAudioFormatException(inputAudioFormat); } return encoding != C.ENCODING_PCM_16BIT @@ -50,16 +63,17 @@ import java.nio.ByteBuffer; case C.ENCODING_PCM_8BIT: resampledSize = size * 2; break; + case C.ENCODING_PCM_16BIT_BIG_ENDIAN: + resampledSize = size; + break; case C.ENCODING_PCM_24BIT: resampledSize = (size / 3) * 2; break; case C.ENCODING_PCM_32BIT: + case C.ENCODING_PCM_FLOAT: resampledSize = size / 2; break; case C.ENCODING_PCM_16BIT: - case C.ENCODING_PCM_FLOAT: - case C.ENCODING_PCM_A_LAW: - case C.ENCODING_PCM_MU_LAW: case C.ENCODING_INVALID: case Format.NO_VALUE: default: @@ -70,30 +84,43 @@ import java.nio.ByteBuffer; ByteBuffer buffer = replaceOutputBuffer(resampledSize); switch (inputAudioFormat.encoding) { case C.ENCODING_PCM_8BIT: - // 8->16 bit resampling. Shift each byte from [0, 256) to [-128, 128) and scale up. + // 8 -> 16 bit resampling. Shift each byte from [0, 256) to [-128, 128) and scale up. for (int i = position; i < limit; i++) { buffer.put((byte) 0); buffer.put((byte) ((inputBuffer.get(i) & 0xFF) - 128)); } break; + case C.ENCODING_PCM_16BIT_BIG_ENDIAN: + // Big endian to little endian resampling. Swap the byte order. + for (int i = position; i < limit; i += 2) { + buffer.put(inputBuffer.get(i + 1)); + buffer.put(inputBuffer.get(i)); + } + break; case C.ENCODING_PCM_24BIT: - // 24->16 bit resampling. Drop the least significant byte. + // 24 -> 16 bit resampling. Drop the least significant byte. for (int i = position; i < limit; i += 3) { buffer.put(inputBuffer.get(i + 1)); buffer.put(inputBuffer.get(i + 2)); } break; case C.ENCODING_PCM_32BIT: - // 32->16 bit resampling. Drop the two least significant bytes. + // 32 -> 16 bit resampling. Drop the two least significant bytes. for (int i = position; i < limit; i += 4) { buffer.put(inputBuffer.get(i + 2)); buffer.put(inputBuffer.get(i + 3)); } break; - case C.ENCODING_PCM_16BIT: case C.ENCODING_PCM_FLOAT: - case C.ENCODING_PCM_A_LAW: - case C.ENCODING_PCM_MU_LAW: + // 32 bit floating point -> 16 bit resampling. Floating point values are in the range + // [-1.0, 1.0], so need to be scaled by Short.MAX_VALUE. + for (int i = position; i < limit; i += 4) { + short value = (short) (inputBuffer.getFloat(i) * Short.MAX_VALUE); + buffer.put((byte) (value & 0xFF)); + buffer.put((byte) ((value >> 8) & 0xFF)); + } + break; + case C.ENCODING_PCM_16BIT: case C.ENCODING_INVALID: case Format.NO_VALUE: default: 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 2a98d2fb25..454007194f 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 @@ -17,11 +17,13 @@ package com.google.android.exoplayer2.audio; import androidx.annotation.IntDef; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Util; import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.nio.ByteBuffer; +import java.nio.ByteOrder; /** * An {@link AudioProcessor} that skips silence in the input stream. Input and output are 16-bit @@ -39,19 +41,9 @@ public final class SilenceSkippingAudioProcessor extends BaseAudioProcessor { * not exceed {@link #MINIMUM_SILENCE_DURATION_US}. */ 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 - * significant byte, for efficiency. - */ + /** The absolute level below which an individual PCM sample is classified as silent. */ private static final short SILENCE_THRESHOLD_LEVEL = 1024; - /** - * Threshold for classifying an individual PCM sample as silent based on its more significant - * byte. This is {@link #SILENCE_THRESHOLD_LEVEL} divided by 256 with rounding. - */ - private static final byte SILENCE_THRESHOLD_LEVEL_MSB = (SILENCE_THRESHOLD_LEVEL + 128) >> 8; - /** Trimming states. */ @Documented @Retention(RetentionPolicy.SOURCE) @@ -325,9 +317,10 @@ public final class SilenceSkippingAudioProcessor extends BaseAudioProcessor { * classified as a noisy frame, or the limit of the buffer if no such frame exists. */ private int findNoisePosition(ByteBuffer buffer) { + Assertions.checkArgument(buffer.order() == ByteOrder.LITTLE_ENDIAN); // The input is in ByteOrder.nativeOrder(), which is little endian on Android. - for (int i = buffer.position() + 1; i < buffer.limit(); i += 2) { - if (Math.abs(buffer.get(i)) > SILENCE_THRESHOLD_LEVEL_MSB) { + for (int i = buffer.position(); i < buffer.limit(); i += 2) { + if (Math.abs(buffer.getShort(i)) > SILENCE_THRESHOLD_LEVEL) { // Round to the start of the frame. return bytesPerFrame * (i / bytesPerFrame); } @@ -340,9 +333,10 @@ public final class SilenceSkippingAudioProcessor extends BaseAudioProcessor { * from the byte position to the limit are classified as silent. */ private int findNoiseLimit(ByteBuffer buffer) { + Assertions.checkArgument(buffer.order() == ByteOrder.LITTLE_ENDIAN); // The input is in ByteOrder.nativeOrder(), which is little endian on Android. - for (int i = buffer.limit() - 1; i >= buffer.position(); i -= 2) { - if (Math.abs(buffer.get(i)) > SILENCE_THRESHOLD_LEVEL_MSB) { + for (int i = buffer.limit() - 2; i >= buffer.position(); i -= 2) { + if (Math.abs(buffer.getShort(i)) > SILENCE_THRESHOLD_LEVEL) { // Return the start of the next frame. return bytesPerFrame * (i / bytesPerFrame) + bytesPerFrame; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/Sonic.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/Sonic.java index 6cd46bb705..50e424003d 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/Sonic.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/Sonic.java @@ -35,7 +35,6 @@ import java.util.Arrays; private final int inputSampleRateHz; private final int channelCount; private final float speed; - private final float pitch; private final float rate; private final int minPeriod; private final int maxPeriod; @@ -62,15 +61,12 @@ import java.util.Arrays; * @param inputSampleRateHz The sample rate of input audio, in hertz. * @param channelCount The number of channels in the input audio. * @param speed The speedup factor for output audio. - * @param pitch The pitch factor for output audio. * @param outputSampleRateHz The sample rate for output audio, in hertz. */ - public Sonic( - int inputSampleRateHz, int channelCount, float speed, float pitch, int outputSampleRateHz) { + public Sonic(int inputSampleRateHz, int channelCount, float speed, int outputSampleRateHz) { this.inputSampleRateHz = inputSampleRateHz; this.channelCount = channelCount; this.speed = speed; - this.pitch = pitch; rate = (float) inputSampleRateHz / outputSampleRateHz; minPeriod = inputSampleRateHz / MAXIMUM_PITCH; maxPeriod = inputSampleRateHz / MINIMUM_PITCH; @@ -120,10 +116,8 @@ import java.util.Arrays; */ public void queueEndOfStream() { int remainingFrameCount = inputFrameCount; - float s = speed / pitch; - float r = rate * pitch; int expectedOutputFrames = - outputFrameCount + (int) ((remainingFrameCount / s + pitchFrameCount) / r + 0.5f); + outputFrameCount + (int) ((remainingFrameCount / speed + pitchFrameCount) / rate + 0.5f); // Add enough silence to flush both input and pitch buffers. inputBuffer = @@ -468,16 +462,14 @@ import java.util.Arrays; private void processStreamInput() { // Resample as many pitch periods as we have buffered on the input. int originalOutputFrameCount = outputFrameCount; - float s = speed / pitch; - float r = rate * pitch; - if (s > 1.00001 || s < 0.99999) { - changeSpeed(s); + if (speed > 1.00001 || speed < 0.99999) { + changeSpeed(speed); } else { copyToOutput(inputBuffer, 0, inputFrameCount); inputFrameCount = 0; } - if (r != 1.0f) { - adjustRate(r, originalOutputFrameCount); + if (rate != 1.0f) { + adjustRate(rate, originalOutputFrameCount); } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/SonicAudioProcessor.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/SonicAudioProcessor.java index b9a59cd620..48075bac50 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/SonicAudioProcessor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/SonicAudioProcessor.java @@ -37,14 +37,6 @@ public final class SonicAudioProcessor implements AudioProcessor { * The minimum allowed playback speed in {@link #setSpeed(float)}. */ public static final float MINIMUM_SPEED = 0.1f; - /** - * The maximum allowed pitch in {@link #setPitch(float)}. - */ - public static final float MAXIMUM_PITCH = 8.0f; - /** - * The minimum allowed pitch in {@link #setPitch(float)}. - */ - public static final float MINIMUM_PITCH = 0.1f; /** * Indicates that the output sample rate should be the same as the input. */ @@ -63,7 +55,6 @@ public final class SonicAudioProcessor implements AudioProcessor { private int pendingOutputSampleRate; private float speed; - private float pitch; private AudioFormat pendingInputAudioFormat; private AudioFormat pendingOutputAudioFormat; @@ -84,7 +75,6 @@ public final class SonicAudioProcessor implements AudioProcessor { */ public SonicAudioProcessor() { speed = 1f; - pitch = 1f; pendingInputAudioFormat = AudioFormat.NOT_SET; pendingOutputAudioFormat = AudioFormat.NOT_SET; inputAudioFormat = AudioFormat.NOT_SET; @@ -112,23 +102,6 @@ public final class SonicAudioProcessor implements AudioProcessor { return speed; } - /** - * Sets the playback pitch. This method may only be called after draining data through the - * processor. The value returned by {@link #isActive()} may change, and the processor must be - * {@link #flush() flushed} before queueing more data. - * - * @param pitch The requested new pitch. - * @return The actual new pitch. - */ - public float setPitch(float pitch) { - pitch = Util.constrainValue(pitch, MINIMUM_PITCH, MAXIMUM_PITCH); - if (this.pitch != pitch) { - this.pitch = pitch; - pendingSonicRecreation = true; - } - return pitch; - } - /** * Sets the sample rate for output audio, in Hertz. Pass {@link #SAMPLE_RATE_NO_CHANGE} to output * audio at the same sample rate as the input. After calling this method, call {@link @@ -182,7 +155,6 @@ public final class SonicAudioProcessor implements AudioProcessor { public boolean isActive() { return pendingOutputAudioFormat.sampleRate != Format.NO_VALUE && (Math.abs(speed - 1f) >= CLOSE_THRESHOLD - || Math.abs(pitch - 1f) >= CLOSE_THRESHOLD || pendingOutputAudioFormat.sampleRate != pendingInputAudioFormat.sampleRate); } @@ -243,7 +215,6 @@ public final class SonicAudioProcessor implements AudioProcessor { inputAudioFormat.sampleRate, inputAudioFormat.channelCount, speed, - pitch, outputAudioFormat.sampleRate); } else if (sonic != null) { sonic.flush(); @@ -258,7 +229,6 @@ public final class SonicAudioProcessor implements AudioProcessor { @Override public void reset() { speed = 1f; - pitch = 1f; pendingInputAudioFormat = AudioFormat.NOT_SET; pendingOutputAudioFormat = AudioFormat.NOT_SET; inputAudioFormat = AudioFormat.NOT_SET; 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 index 8f39dd1d85..a9afa47198 100644 --- 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 @@ -79,6 +79,11 @@ public final class TeeAudioProcessor extends BaseAudioProcessor { replaceOutputBuffer(remaining).put(inputBuffer).flip(); } + @Override + protected void onFlush() { + flushSinkIfActive(); + } + @Override protected void onQueueEndOfStream() { flushSinkIfActive(); @@ -176,7 +181,7 @@ public final class TeeAudioProcessor extends BaseAudioProcessor { // Write the rest of the header as little endian data. scratchByteBuffer.clear(); scratchByteBuffer.putInt(16); - scratchByteBuffer.putShort((short) WavUtil.getTypeForEncoding(encoding)); + scratchByteBuffer.putShort((short) WavUtil.getTypeForPcmEncoding(encoding)); scratchByteBuffer.putShort((short) channelCount); scratchByteBuffer.putInt(sampleRateHz); int bytesPerSample = Util.getPcmFrameSize(encoding, channelCount); @@ -201,7 +206,7 @@ public final class TeeAudioProcessor extends BaseAudioProcessor { } private void reset() throws IOException { - RandomAccessFile randomAccessFile = this.randomAccessFile; + @Nullable RandomAccessFile randomAccessFile = this.randomAccessFile; if (randomAccessFile == null) { return; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/TrimmingAudioProcessor.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/TrimmingAudioProcessor.java index 9437e4ac26..f630c267e6 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/TrimmingAudioProcessor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/TrimmingAudioProcessor.java @@ -155,17 +155,20 @@ import java.nio.ByteBuffer; @Override protected void onFlush() { if (reconfigurationPending) { + // Flushing activates the new configuration, so prepare to trim bytes from the start/end. reconfigurationPending = false; endBuffer = new byte[trimEndFrames * inputAudioFormat.bytesPerFrame]; pendingTrimStartBytes = trimStartFrames * inputAudioFormat.bytesPerFrame; - } else { - // Audio processors are flushed after initial configuration, so we leave the pending trim - // start byte count unmodified if the processor was just configured. Otherwise we (possibly - // incorrectly) assume that this is a seek to a non-zero position. We should instead check the - // timestamp of the first input buffer queued after flushing to decide whether to trim (see - // also [Internal: b/77292509]). - pendingTrimStartBytes = 0; } + + // TODO(internal b/77292509): Flushing occurs to activate a configuration (handled above) but + // also when seeking within a stream. This implementation currently doesn't handle seek to start + // (where we need to trim at the start again), nor seeks to non-zero positions before start + // trimming has occurred (where we should set pendingTrimStartBytes to zero). These cases can be + // fixed by trimming in queueInput based on timestamp, once that information is available. + + // Any data in the end buffer should no longer be output if we are playing from a different + // position, so discard it and refill the buffer using new input. endBufferSize = 0; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/database/VersionTable.java b/library/core/src/main/java/com/google/android/exoplayer2/database/VersionTable.java index be367d2f22..f1d269ddbf 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/database/VersionTable.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/database/VersionTable.java @@ -40,6 +40,8 @@ public final class VersionTable { public static final int FEATURE_CACHE_CONTENT_METADATA = 1; /** Version of tables used for cache file metadata. */ public static final int FEATURE_CACHE_FILE_METADATA = 2; + /** Version of tables used from external features. */ + public static final int FEATURE_EXTERNAL = 1000; private static final String TABLE_NAME = DatabaseProvider.TABLE_PREFIX + "Versions"; @@ -67,7 +69,12 @@ public final class VersionTable { @Documented @Retention(RetentionPolicy.SOURCE) - @IntDef({FEATURE_OFFLINE, FEATURE_CACHE_CONTENT_METADATA, FEATURE_CACHE_FILE_METADATA}) + @IntDef({ + FEATURE_OFFLINE, + FEATURE_CACHE_CONTENT_METADATA, + FEATURE_CACHE_FILE_METADATA, + FEATURE_EXTERNAL + }) private @interface Feature {} private VersionTable() {} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/decoder/DecoderCounters.java b/library/core/src/main/java/com/google/android/exoplayer2/decoder/DecoderCounters.java index 8409bab558..5de4fcb126 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/decoder/DecoderCounters.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/decoder/DecoderCounters.java @@ -73,6 +73,23 @@ public final class DecoderCounters { * dropped from the source to advance to the keyframe. */ public int droppedToKeyframeCount; + /** + * The sum of video frame processing offset samples in microseconds. + * + *

        Video frame processing offset measures how early a video frame was processed by a video + * renderer compared to the player's current position. + * + *

        Note: Use {@link #addVideoFrameProcessingOffsetSample(long)} to update this field instead of + * updating it directly. + */ + public long totalVideoFrameProcessingOffsetUs; + /** + * The number of video frame processing offset samples added. + * + *

        Note: Use {@link #addVideoFrameProcessingOffsetSample(long)} to update this field instead of + * updating it directly. + */ + public int videoFrameProcessingOffsetCount; /** * Should be called to ensure counter values are made visible across threads. The playback thread @@ -100,6 +117,25 @@ public final class DecoderCounters { maxConsecutiveDroppedBufferCount = Math.max(maxConsecutiveDroppedBufferCount, other.maxConsecutiveDroppedBufferCount); droppedToKeyframeCount += other.droppedToKeyframeCount; + + addVideoFrameProcessingOffsetSamples( + other.totalVideoFrameProcessingOffsetUs, other.videoFrameProcessingOffsetCount); } + /** + * Adds a video frame processing offset sample to {@link #totalVideoFrameProcessingOffsetUs} and + * increases {@link #videoFrameProcessingOffsetCount} by one. + * + *

        Convenience method to ensure both fields are updated when adding a sample. + * + * @param sampleUs The sample in microseconds. + */ + public void addVideoFrameProcessingOffsetSample(long sampleUs) { + addVideoFrameProcessingOffsetSamples(sampleUs, /* count= */ 1); + } + + private void addVideoFrameProcessingOffsetSamples(long sampleUs, int count) { + totalVideoFrameProcessingOffsetUs += sampleUs; + videoFrameProcessingOffsetCount += count; + } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioDecoderException.java b/library/core/src/main/java/com/google/android/exoplayer2/decoder/DecoderException.java similarity index 79% rename from library/core/src/main/java/com/google/android/exoplayer2/audio/AudioDecoderException.java rename to library/core/src/main/java/com/google/android/exoplayer2/decoder/DecoderException.java index ac4f632d62..c07e646f09 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioDecoderException.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/decoder/DecoderException.java @@ -13,13 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.google.android.exoplayer2.audio; +package com.google.android.exoplayer2.decoder; -/** Thrown when an audio decoder error occurs. */ -public class AudioDecoderException extends Exception { +/** Thrown when a {@link Decoder} error occurs. */ +public class DecoderException extends Exception { /** @param message The detail message for this exception. */ - public AudioDecoderException(String message) { + public DecoderException(String message) { super(message); } @@ -28,8 +28,7 @@ public class AudioDecoderException extends Exception { * @param cause the cause (which is saved for later retrieval by the {@link #getCause()} method). * A null value is permitted, and indicates that the cause is nonexistent or unknown. */ - public AudioDecoderException(String message, Throwable cause) { + public DecoderException(String message, Throwable cause) { super(message, cause); } - } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/decoder/OutputBuffer.java b/library/core/src/main/java/com/google/android/exoplayer2/decoder/OutputBuffer.java index 730ce15ed4..ca431bf77e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/decoder/OutputBuffer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/decoder/OutputBuffer.java @@ -20,6 +20,17 @@ package com.google.android.exoplayer2.decoder; */ public abstract class OutputBuffer extends Buffer { + /** Buffer owner. */ + public interface Owner { + + /** + * Releases the buffer. + * + * @param outputBuffer Output buffer. + */ + void releaseOutputBuffer(S outputBuffer); + } + /** * The presentation timestamp for the buffer, in microseconds. */ @@ -34,5 +45,4 @@ public abstract class OutputBuffer extends Buffer { * Releases the output buffer for reuse. Must be called when the buffer is no longer needed. */ public abstract void release(); - } 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 03aabecb0e..8f660c4c24 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 @@ -21,7 +21,10 @@ import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.util.Assertions; import java.util.ArrayDeque; -/** Base class for {@link Decoder}s that use their own decode thread. */ +/** + * Base class for {@link Decoder}s that use their own decode thread and decode each input buffer + * immediately into a corresponding output buffer. + */ @SuppressWarnings("UngroupedOverloads") public abstract class SimpleDecoder< I extends DecoderInputBuffer, O extends OutputBuffer, E extends Exception> @@ -62,12 +65,13 @@ public abstract class SimpleDecoder< for (int i = 0; i < availableOutputBufferCount; i++) { availableOutputBuffers[i] = createOutputBuffer(); } - decodeThread = new Thread() { - @Override - public void run() { - SimpleDecoder.this.run(); - } - }; + decodeThread = + new Thread("ExoPlayer:SimpleDecoder") { + @Override + public void run() { + SimpleDecoder.this.run(); + } + }; decodeThread.start(); } @@ -149,6 +153,7 @@ public abstract class SimpleDecoder< while (!queuedOutputBuffers.isEmpty()) { queuedOutputBuffers.removeFirst().release(); } + exception = null; } } @@ -225,6 +230,7 @@ public abstract class SimpleDecoder< if (inputBuffer.isDecodeOnly()) { outputBuffer.addFlag(C.BUFFER_FLAG_DECODE_ONLY); } + @Nullable E exception; try { exception = decode(inputBuffer, outputBuffer, resetDecoder); } catch (RuntimeException e) { @@ -238,8 +244,9 @@ public abstract class SimpleDecoder< exception = createUnexpectedDecodeException(e); } if (exception != null) { - // Memory barrier to ensure that the decoder exception is visible from the playback thread. - synchronized (lock) {} + synchronized (lock) { + this.exception = exception; + } return false; } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/decoder/SimpleOutputBuffer.java b/library/core/src/main/java/com/google/android/exoplayer2/decoder/SimpleOutputBuffer.java index 84cffc1145..22cff021de 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/decoder/SimpleOutputBuffer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/decoder/SimpleOutputBuffer.java @@ -24,11 +24,11 @@ import java.nio.ByteOrder; */ public class SimpleOutputBuffer extends OutputBuffer { - private final SimpleDecoder owner; + private final Owner owner; @Nullable public ByteBuffer data; - public SimpleOutputBuffer(SimpleDecoder owner) { + public SimpleOutputBuffer(Owner owner) { this.owner = owner; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/device/DeviceInfo.java b/library/core/src/main/java/com/google/android/exoplayer2/device/DeviceInfo.java new file mode 100644 index 0000000000..43c37028ea --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/device/DeviceInfo.java @@ -0,0 +1,79 @@ +/* + * Copyright 2020 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.device; + +import androidx.annotation.IntDef; +import androidx.annotation.Nullable; +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** Information about the playback device. */ +public final class DeviceInfo { + + /** Types of playback. One of {@link #PLAYBACK_TYPE_LOCAL} or {@link #PLAYBACK_TYPE_REMOTE}. */ + @Documented + @Retention(RetentionPolicy.SOURCE) + @Target({ElementType.TYPE_PARAMETER, ElementType.TYPE_USE}) + @IntDef({ + PLAYBACK_TYPE_LOCAL, + PLAYBACK_TYPE_REMOTE, + }) + public @interface PlaybackType {} + /** Playback happens on the local device (e.g. phone). */ + public static final int PLAYBACK_TYPE_LOCAL = 0; + /** Playback happens outside of the device (e.g. a cast device). */ + public static final int PLAYBACK_TYPE_REMOTE = 1; + + /** The type of playback. */ + public final @PlaybackType int playbackType; + /** The minimum volume that the device supports. */ + public final int minVolume; + /** The maximum volume that the device supports. */ + public final int maxVolume; + + /** Creates device information. */ + public DeviceInfo(@PlaybackType int playbackType, int minVolume, int maxVolume) { + this.playbackType = playbackType; + this.minVolume = minVolume; + this.maxVolume = maxVolume; + } + + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof DeviceInfo)) { + return false; + } + DeviceInfo other = (DeviceInfo) obj; + return playbackType == other.playbackType + && minVolume == other.minVolume + && maxVolume == other.maxVolume; + } + + @Override + public int hashCode() { + int result = 17; + result = 31 * result + playbackType; + result = 31 * result + minVolume; + result = 31 * result + maxVolume; + return result; + } +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/device/DeviceListener.java b/library/core/src/main/java/com/google/android/exoplayer2/device/DeviceListener.java new file mode 100644 index 0000000000..3d35c6ad54 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/device/DeviceListener.java @@ -0,0 +1,28 @@ +/* + * Copyright 2020 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.device; + +import com.google.android.exoplayer2.Player; + +/** A listener for changes of {@link Player.DeviceComponent}. */ +public interface DeviceListener { + + /** Called when the device information changes. */ + default void onDeviceInfoChanged(DeviceInfo deviceInfo) {} + + /** Called when the device volume or mute state changes. */ + default void onDeviceVolumeChanged(int volume, boolean muted) {} +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/device/package-info.java b/library/core/src/main/java/com/google/android/exoplayer2/device/package-info.java new file mode 100644 index 0000000000..400a2e1b50 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/device/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright 2020 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. + */ +@NonNullApi +package com.google.android.exoplayer2.device; + +import com.google.android.exoplayer2.util.NonNullApi; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSession.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSession.java index 0d93ec7c62..ad8a5c9854 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSession.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSession.java @@ -16,7 +16,6 @@ package com.google.android.exoplayer2.drm; import android.annotation.SuppressLint; -import android.annotation.TargetApi; import android.media.NotProvisionedException; import android.os.Handler; import android.os.HandlerThread; @@ -25,14 +24,16 @@ import android.os.Message; import android.os.SystemClock; import android.util.Pair; import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.drm.DrmInitData.SchemeData; import com.google.android.exoplayer2.drm.ExoMediaDrm.KeyRequest; import com.google.android.exoplayer2.drm.ExoMediaDrm.ProvisionRequest; import com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy; import com.google.android.exoplayer2.util.Assertions; -import com.google.android.exoplayer2.util.EventDispatcher; +import com.google.android.exoplayer2.util.CopyOnWriteMultiset; import com.google.android.exoplayer2.util.Log; +import com.google.android.exoplayer2.util.MediaSourceEventDispatcher; import com.google.android.exoplayer2.util.Util; import java.io.IOException; import java.util.Arrays; @@ -41,14 +42,13 @@ import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.UUID; -import org.checkerframework.checker.nullness.compatqual.NullableType; import org.checkerframework.checker.nullness.qual.EnsuresNonNullIf; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.checkerframework.checker.nullness.qual.RequiresNonNull; /** A {@link DrmSession} that supports playbacks using {@link ExoMediaDrm}. */ -@TargetApi(18) -/* package */ class DefaultDrmSession implements DrmSession { +@RequiresApi(18) +/* package */ class DefaultDrmSession implements DrmSession { /** Thrown when an unexpected exception or error is thrown during provisioning or key requests. */ public static final class UnexpectedDrmSessionException extends IOException { @@ -59,7 +59,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; } /** Manages provisioning requests. */ - public interface ProvisioningManager { + public interface ProvisioningManager { /** * Called when a session requires provisioning. The manager may call {@link @@ -69,7 +69,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; * * @param session The session. */ - void provisionRequired(DefaultDrmSession session); + void provisionRequired(DefaultDrmSession session); /** * Called by a session when it fails to perform a provisioning operation. @@ -83,14 +83,14 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; } /** Callback to be notified when the session is released. */ - public interface ReleaseCallback { + public interface ReleaseCallback { /** * Called immediately after releasing session resources. * * @param session The session. */ - void onSessionReleased(DefaultDrmSession session); + void onSessionReleased(DefaultDrmSession session); } private static final String TAG = "DefaultDrmSession"; @@ -102,14 +102,14 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; /** The DRM scheme datas, or null if this session uses offline keys. */ @Nullable public final List schemeDatas; - private final ExoMediaDrm mediaDrm; - private final ProvisioningManager provisioningManager; - private final ReleaseCallback releaseCallback; + private final ExoMediaDrm mediaDrm; + private final ProvisioningManager provisioningManager; + private final ReleaseCallback releaseCallback; private final @DefaultDrmSessionManager.Mode int mode; private final boolean playClearSamplesWithoutKeys; private final boolean isPlaceholderSession; private final HashMap keyRequestParameters; - private final EventDispatcher eventDispatcher; + private final CopyOnWriteMultiset eventDispatchers; private final LoadErrorHandlingPolicy loadErrorHandlingPolicy; /* package */ final MediaDrmCallback callback; @@ -120,9 +120,9 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; private int referenceCount; @Nullable private HandlerThread requestHandlerThread; @Nullable private RequestHandler requestHandler; - @Nullable private T mediaCrypto; + @Nullable private ExoMediaCrypto mediaCrypto; @Nullable private DrmSessionException lastException; - private byte @NullableType [] sessionId; + @Nullable private byte[] sessionId; private byte @MonotonicNonNull [] offlineLicenseKeySetId; @Nullable private KeyRequest currentKeyRequest; @@ -144,15 +144,14 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; * @param keyRequestParameters Key request parameters. * @param callback The media DRM callback. * @param playbackLooper The playback looper. - * @param eventDispatcher The dispatcher for DRM session manager events. * @param loadErrorHandlingPolicy The {@link LoadErrorHandlingPolicy} for key and provisioning * requests. */ public DefaultDrmSession( UUID uuid, - ExoMediaDrm mediaDrm, - ProvisioningManager provisioningManager, - ReleaseCallback releaseCallback, + ExoMediaDrm mediaDrm, + ProvisioningManager provisioningManager, + ReleaseCallback releaseCallback, @Nullable List schemeDatas, @DefaultDrmSessionManager.Mode int mode, boolean playClearSamplesWithoutKeys, @@ -161,7 +160,6 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; HashMap keyRequestParameters, MediaDrmCallback callback, Looper playbackLooper, - EventDispatcher eventDispatcher, LoadErrorHandlingPolicy loadErrorHandlingPolicy) { if (mode == DefaultDrmSessionManager.MODE_QUERY || mode == DefaultDrmSessionManager.MODE_RELEASE) { @@ -182,7 +180,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; } this.keyRequestParameters = keyRequestParameters; this.callback = callback; - this.eventDispatcher = eventDispatcher; + this.eventDispatchers = new CopyOnWriteMultiset<>(); this.loadErrorHandlingPolicy = loadErrorHandlingPolicy; state = STATE_OPENING; responseHandler = new ResponseHandler(playbackLooper); @@ -242,7 +240,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; } @Override - public final @Nullable T getMediaCrypto() { + public final @Nullable ExoMediaCrypto getMediaCrypto() { return mediaCrypto; } @@ -259,21 +257,32 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; } @Override - public void acquire() { + public void acquire(@Nullable MediaSourceEventDispatcher eventDispatcher) { Assertions.checkState(referenceCount >= 0); + if (eventDispatcher != null) { + eventDispatchers.add(eventDispatcher); + } if (++referenceCount == 1) { Assertions.checkState(state == STATE_OPENING); - requestHandlerThread = new HandlerThread("DrmRequestHandler"); + requestHandlerThread = new HandlerThread("ExoPlayer:DrmRequestHandler"); requestHandlerThread.start(); requestHandler = new RequestHandler(requestHandlerThread.getLooper()); if (openInternal(true)) { doLicense(true); } + } else { + // TODO: Add a parameter to onDrmSessionAcquired to indicate whether the session is being + // re-used or not. + if (eventDispatcher != null) { + eventDispatcher.dispatch( + (listener, windowIndex, mediaPeriodId) -> listener.onDrmSessionAcquired(), + DrmSessionEventListener.class); + } } } @Override - public void release() { + public void release(@Nullable MediaSourceEventDispatcher eventDispatcher) { if (--referenceCount == 0) { // Assigning null to various non-null variables for clean-up. state = STATE_RELEASED; @@ -289,10 +298,13 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; if (sessionId != null) { mediaDrm.closeSession(sessionId); sessionId = null; - eventDispatcher.dispatch(DefaultDrmSessionEventListener::onDrmSessionReleased); } releaseCallback.onSessionReleased(this); } + dispatchEvent((listener, windowIndex, mediaPeriodId) -> listener.onDrmSessionReleased()); + if (eventDispatcher != null) { + eventDispatchers.remove(eventDispatcher); + } } // Internal methods. @@ -314,7 +326,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; try { sessionId = mediaDrm.openSession(); mediaCrypto = mediaDrm.createMediaCrypto(sessionId); - eventDispatcher.dispatch(DefaultDrmSessionEventListener::onDrmSessionAcquired); + dispatchEvent((listener, windowIndex, mediaPeriodId) -> listener.onDrmSessionAcquired()); state = STATE_OPENED; Assertions.checkNotNull(sessionId); return true; @@ -378,7 +390,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; onError(new KeysExpiredException()); } else { state = STATE_OPENED_WITH_KEYS; - eventDispatcher.dispatch(DefaultDrmSessionEventListener::onDrmKeysRestored); + dispatchEvent((listener, windowIndex, mediaPeriodId) -> listener.onDrmKeysRestored()); } } break; @@ -448,7 +460,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; byte[] responseData = (byte[]) response; if (mode == DefaultDrmSessionManager.MODE_RELEASE) { mediaDrm.provideKeyResponse(Util.castNonNull(offlineLicenseKeySetId), responseData); - eventDispatcher.dispatch(DefaultDrmSessionEventListener::onDrmKeysRestored); + dispatchEvent((listener, windowIndex, mediaPeriodId) -> listener.onDrmKeysRestored()); } else { byte[] keySetId = mediaDrm.provideKeyResponse(sessionId, responseData); if ((mode == DefaultDrmSessionManager.MODE_DOWNLOAD @@ -459,7 +471,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; offlineLicenseKeySetId = keySetId; } state = STATE_OPENED_WITH_KEYS; - eventDispatcher.dispatch(DefaultDrmSessionEventListener::onDrmKeysLoaded); + dispatchEvent((listener, windowIndex, mediaPeriodId) -> listener.onDrmKeysLoaded()); } } catch (Exception e) { onKeysError(e); @@ -483,7 +495,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; private void onError(final Exception e) { lastException = new DrmSessionException(e); - eventDispatcher.dispatch(listener -> listener.onDrmSessionManagerError(e)); + dispatchEvent((listener, windowIndex, mediaPeriodId) -> listener.onDrmSessionManagerError(e)); if (state != STATE_OPENED_WITH_KEYS) { state = STATE_ERROR; } @@ -495,6 +507,13 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; return state == STATE_OPENED || state == STATE_OPENED_WITH_KEYS; } + private void dispatchEvent( + MediaSourceEventDispatcher.EventWithPeriodId event) { + for (MediaSourceEventDispatcher eventDispatcher : eventDispatchers.elementSet()) { + eventDispatcher.dispatch(event, DrmSessionEventListener.class); + } + } + // Internal classes. @SuppressLint("HandlerLeak") 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 1c27d745de..dbfde1cc9a 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 @@ -16,12 +16,12 @@ package com.google.android.exoplayer2.drm; import android.annotation.SuppressLint; -import android.annotation.TargetApi; import android.os.Handler; import android.os.Looper; import android.os.Message; import androidx.annotation.IntDef; import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.drm.DrmInitData.SchemeData; import com.google.android.exoplayer2.drm.DrmSession.DrmSessionException; @@ -29,8 +29,8 @@ import com.google.android.exoplayer2.drm.ExoMediaDrm.OnEventListener; import com.google.android.exoplayer2.upstream.DefaultLoadErrorHandlingPolicy; import com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy; import com.google.android.exoplayer2.util.Assertions; -import com.google.android.exoplayer2.util.EventDispatcher; import com.google.android.exoplayer2.util.Log; +import com.google.android.exoplayer2.util.MediaSourceEventDispatcher; import com.google.android.exoplayer2.util.Util; import java.lang.annotation.Documented; import java.lang.annotation.Retention; @@ -43,8 +43,8 @@ import java.util.Map; import java.util.UUID; /** A {@link DrmSessionManager} that supports playbacks using {@link ExoMediaDrm}. */ -@TargetApi(18) -public class DefaultDrmSessionManager implements DrmSessionManager { +@RequiresApi(18) +public class DefaultDrmSessionManager implements DrmSessionManager { /** * Builder for {@link DefaultDrmSessionManager} instances. @@ -55,7 +55,7 @@ public class DefaultDrmSessionManager implements DrmSe private final HashMap keyRequestParameters; private UUID uuid; - private ExoMediaDrm.Provider exoMediaDrmProvider; + private ExoMediaDrm.Provider exoMediaDrmProvider; private boolean multiSession; private int[] useDrmSessionsForClearContentTrackTypes; private boolean playClearSamplesWithoutKeys; @@ -76,27 +76,29 @@ public class DefaultDrmSessionManager implements DrmSe * DefaultLoadErrorHandlingPolicy}. *

      */ - @SuppressWarnings("unchecked") public Builder() { keyRequestParameters = new HashMap<>(); uuid = C.WIDEVINE_UUID; - exoMediaDrmProvider = (ExoMediaDrm.Provider) FrameworkMediaDrm.DEFAULT_PROVIDER; + exoMediaDrmProvider = FrameworkMediaDrm.DEFAULT_PROVIDER; loadErrorHandlingPolicy = new DefaultLoadErrorHandlingPolicy(); useDrmSessionsForClearContentTrackTypes = new int[0]; } /** * Sets the key request parameters to pass as the last argument to {@link - * ExoMediaDrm#getKeyRequest(byte[], List, int, HashMap)}. + * ExoMediaDrm#getKeyRequest(byte[], List, int, HashMap)}. May be null if not parameters need to + * be passed. * *

      Custom data for PlayReady should be set under {@link #PLAYREADY_CUSTOM_DATA_KEY}. * * @param keyRequestParameters A map with parameters. * @return This builder. */ - public Builder setKeyRequestParameters(Map keyRequestParameters) { + public Builder setKeyRequestParameters(@Nullable Map keyRequestParameters) { this.keyRequestParameters.clear(); - this.keyRequestParameters.putAll(Assertions.checkNotNull(keyRequestParameters)); + if (keyRequestParameters != null) { + this.keyRequestParameters.putAll(keyRequestParameters); + } return this; } @@ -107,7 +109,6 @@ public class DefaultDrmSessionManager implements DrmSe * @param exoMediaDrmProvider The {@link ExoMediaDrm.Provider}. * @return This builder. */ - @SuppressWarnings({"rawtypes", "unchecked"}) public Builder setUuidAndExoMediaDrmProvider( UUID uuid, ExoMediaDrm.Provider exoMediaDrmProvider) { this.uuid = Assertions.checkNotNull(uuid); @@ -180,8 +181,8 @@ public class DefaultDrmSessionManager implements DrmSe } /** Builds a {@link DefaultDrmSessionManager} instance. */ - public DefaultDrmSessionManager build(MediaDrmCallback mediaDrmCallback) { - return new DefaultDrmSessionManager<>( + public DefaultDrmSessionManager build(MediaDrmCallback mediaDrmCallback) { + return new DefaultDrmSessionManager( uuid, exoMediaDrmProvider, mediaDrmCallback, @@ -235,23 +236,22 @@ public class DefaultDrmSessionManager implements DrmSe private static final String TAG = "DefaultDrmSessionMgr"; private final UUID uuid; - private final ExoMediaDrm.Provider exoMediaDrmProvider; + private final ExoMediaDrm.Provider exoMediaDrmProvider; private final MediaDrmCallback callback; private final HashMap keyRequestParameters; - private final EventDispatcher eventDispatcher; private final boolean multiSession; private final int[] useDrmSessionsForClearContentTrackTypes; private final boolean playClearSamplesWithoutKeys; private final ProvisioningManagerImpl provisioningManagerImpl; private final LoadErrorHandlingPolicy loadErrorHandlingPolicy; - private final List> sessions; - private final List> provisioningSessions; + private final List sessions; + private final List provisioningSessions; private int prepareCallsCount; - @Nullable private ExoMediaDrm exoMediaDrm; - @Nullable private DefaultDrmSession placeholderDrmSession; - @Nullable private DefaultDrmSession noMultiSessionDrmSession; + @Nullable private ExoMediaDrm exoMediaDrm; + @Nullable private DefaultDrmSession placeholderDrmSession; + @Nullable private DefaultDrmSession noMultiSessionDrmSession; @Nullable private Looper playbackLooper; private int mode; @Nullable private byte[] offlineLicenseKeySetId; @@ -270,7 +270,7 @@ public class DefaultDrmSessionManager implements DrmSe @Deprecated public DefaultDrmSessionManager( UUID uuid, - ExoMediaDrm exoMediaDrm, + ExoMediaDrm exoMediaDrm, MediaDrmCallback callback, @Nullable HashMap keyRequestParameters) { this( @@ -295,7 +295,7 @@ public class DefaultDrmSessionManager implements DrmSe @Deprecated public DefaultDrmSessionManager( UUID uuid, - ExoMediaDrm exoMediaDrm, + ExoMediaDrm exoMediaDrm, MediaDrmCallback callback, @Nullable HashMap keyRequestParameters, boolean multiSession) { @@ -323,14 +323,14 @@ public class DefaultDrmSessionManager implements DrmSe @Deprecated public DefaultDrmSessionManager( UUID uuid, - ExoMediaDrm exoMediaDrm, + ExoMediaDrm exoMediaDrm, MediaDrmCallback callback, @Nullable HashMap keyRequestParameters, boolean multiSession, int initialDrmRequestRetryCount) { this( uuid, - new ExoMediaDrm.AppManagedProvider<>(exoMediaDrm), + new ExoMediaDrm.AppManagedProvider(exoMediaDrm), callback, keyRequestParameters == null ? new HashMap<>() : keyRequestParameters, multiSession, @@ -339,11 +339,9 @@ public class DefaultDrmSessionManager implements DrmSe new DefaultLoadErrorHandlingPolicy(initialDrmRequestRetryCount)); } - // the constructor does not initialize fields: offlineLicenseKeySetId - @SuppressWarnings("nullness:initialization.fields.uninitialized") private DefaultDrmSessionManager( UUID uuid, - ExoMediaDrm.Provider exoMediaDrmProvider, + ExoMediaDrm.Provider exoMediaDrmProvider, MediaDrmCallback callback, HashMap keyRequestParameters, boolean multiSession, @@ -356,7 +354,6 @@ public class DefaultDrmSessionManager implements DrmSe this.exoMediaDrmProvider = exoMediaDrmProvider; this.callback = callback; this.keyRequestParameters = keyRequestParameters; - this.eventDispatcher = new EventDispatcher<>(); this.multiSession = multiSession; this.useDrmSessionsForClearContentTrackTypes = useDrmSessionsForClearContentTrackTypes; this.playClearSamplesWithoutKeys = playClearSamplesWithoutKeys; @@ -367,29 +364,10 @@ public class DefaultDrmSessionManager implements DrmSe provisioningSessions = new ArrayList<>(); } - /** - * Adds a {@link DefaultDrmSessionEventListener} to listen to drm session events. - * - * @param handler A handler to use when delivering events to {@code eventListener}. - * @param eventListener A listener of events. - */ - public final void addListener(Handler handler, DefaultDrmSessionEventListener eventListener) { - eventDispatcher.addListener(handler, eventListener); - } - - /** - * Removes a {@link DefaultDrmSessionEventListener} from the list of drm session event listeners. - * - * @param eventListener The listener to remove. - */ - public final void removeListener(DefaultDrmSessionEventListener eventListener) { - eventDispatcher.removeListener(eventListener); - } - /** * Sets the mode, which determines the role of sessions acquired from the instance. This must be - * called before {@link #acquireSession(Looper, DrmInitData)} or {@link - * #acquirePlaceholderSession} is called. + * called before {@link #acquireSession(Looper, MediaSourceEventDispatcher, DrmInitData)} or + * {@link #acquirePlaceholderSession} is called. * *

      By default, the mode is {@link #MODE_PLAYBACK} and a streaming license is requested when * required. @@ -472,9 +450,9 @@ public class DefaultDrmSessionManager implements DrmSe @Override @Nullable - public DrmSession acquirePlaceholderSession(Looper playbackLooper, int trackType) { + public DrmSession acquirePlaceholderSession(Looper playbackLooper, int trackType) { assertExpectedPlaybackLooper(playbackLooper); - ExoMediaDrm exoMediaDrm = Assertions.checkNotNull(this.exoMediaDrm); + ExoMediaDrm exoMediaDrm = Assertions.checkNotNull(this.exoMediaDrm); boolean avoidPlaceholderDrmSessions = FrameworkMediaCrypto.class.equals(exoMediaDrm.getExoMediaCryptoType()) && FrameworkMediaCrypto.WORKAROUND_DEVICE_NEEDS_KEYS_TO_CONFIGURE_CODEC; @@ -486,18 +464,21 @@ public class DefaultDrmSessionManager implements DrmSe } maybeCreateMediaDrmHandler(playbackLooper); if (placeholderDrmSession == null) { - DefaultDrmSession placeholderDrmSession = + DefaultDrmSession placeholderDrmSession = createNewDefaultSession( /* schemeDatas= */ Collections.emptyList(), /* isPlaceholderSession= */ true); sessions.add(placeholderDrmSession); this.placeholderDrmSession = placeholderDrmSession; } - placeholderDrmSession.acquire(); + placeholderDrmSession.acquire(/* eventDispatcher= */ null); return placeholderDrmSession; } @Override - public DrmSession acquireSession(Looper playbackLooper, DrmInitData drmInitData) { + public DrmSession acquireSession( + Looper playbackLooper, + @Nullable MediaSourceEventDispatcher eventDispatcher, + DrmInitData drmInitData) { assertExpectedPlaybackLooper(playbackLooper); maybeCreateMediaDrmHandler(playbackLooper); @@ -506,18 +487,22 @@ public class DefaultDrmSessionManager implements DrmSe schemeDatas = getSchemeDatas(drmInitData, uuid, false); if (schemeDatas.isEmpty()) { final MissingSchemeDataException error = new MissingSchemeDataException(uuid); - eventDispatcher.dispatch(listener -> listener.onDrmSessionManagerError(error)); - return new ErrorStateDrmSession<>(new DrmSessionException(error)); + if (eventDispatcher != null) { + eventDispatcher.dispatch( + (listener, windowIndex, mediaPeriodId) -> listener.onDrmSessionManagerError(error), + DrmSessionEventListener.class); + } + return new ErrorStateDrmSession(new DrmSessionException(error)); } } - @Nullable DefaultDrmSession session; + @Nullable DefaultDrmSession session; if (!multiSession) { session = noMultiSessionDrmSession; } else { // Only use an existing session if it has matching init data. session = null; - for (DefaultDrmSession existingSession : sessions) { + for (DefaultDrmSession existingSession : sessions) { if (Util.areEqual(existingSession.schemeDatas, schemeDatas)) { session = existingSession; break; @@ -533,13 +518,13 @@ public class DefaultDrmSessionManager implements DrmSe } sessions.add(session); } - session.acquire(); + session.acquire(eventDispatcher); return session; } @Override @Nullable - public Class getExoMediaCryptoType(DrmInitData drmInitData) { + public Class getExoMediaCryptoType(DrmInitData drmInitData) { return canAcquireSession(drmInitData) ? Assertions.checkNotNull(exoMediaDrm).getExoMediaCryptoType() : null; @@ -558,12 +543,12 @@ public class DefaultDrmSessionManager implements DrmSe } } - private DefaultDrmSession createNewDefaultSession( + private DefaultDrmSession createNewDefaultSession( @Nullable List schemeDatas, boolean isPlaceholderSession) { Assertions.checkNotNull(exoMediaDrm); // Placeholder sessions should always play clear samples without keys. boolean playClearSamplesWithoutKeys = this.playClearSamplesWithoutKeys | isPlaceholderSession; - return new DefaultDrmSession<>( + return new DefaultDrmSession( uuid, exoMediaDrm, /* provisioningManager= */ provisioningManagerImpl, @@ -576,11 +561,10 @@ public class DefaultDrmSessionManager implements DrmSe keyRequestParameters, callback, Assertions.checkNotNull(playbackLooper), - eventDispatcher, loadErrorHandlingPolicy); } - private void onSessionReleased(DefaultDrmSession drmSession) { + private void onSessionReleased(DefaultDrmSession drmSession) { sessions.remove(drmSession); if (placeholderDrmSession == drmSession) { placeholderDrmSession = null; @@ -636,7 +620,7 @@ public class DefaultDrmSessionManager implements DrmSe // The event is not associated with any particular session. return; } - for (DefaultDrmSession session : sessions) { + for (DefaultDrmSession session : sessions) { if (session.hasSessionId(sessionId)) { session.onMediaDrmEvent(msg.what); return; @@ -645,9 +629,9 @@ public class DefaultDrmSessionManager implements DrmSe } } - private class ProvisioningManagerImpl implements DefaultDrmSession.ProvisioningManager { + private class ProvisioningManagerImpl implements DefaultDrmSession.ProvisioningManager { @Override - public void provisionRequired(DefaultDrmSession session) { + public void provisionRequired(DefaultDrmSession session) { if (provisioningSessions.contains(session)) { // The session has already requested provisioning. return; @@ -661,7 +645,7 @@ public class DefaultDrmSessionManager implements DrmSe @Override public void onProvisionCompleted() { - for (DefaultDrmSession session : provisioningSessions) { + for (DefaultDrmSession session : provisioningSessions) { session.onProvisionCompleted(); } provisioningSessions.clear(); @@ -669,22 +653,18 @@ public class DefaultDrmSessionManager implements DrmSe @Override public void onProvisionError(Exception error) { - for (DefaultDrmSession session : provisioningSessions) { + for (DefaultDrmSession session : provisioningSessions) { session.onProvisionError(error); } provisioningSessions.clear(); } } - private class MediaDrmEventListener implements OnEventListener { + private class MediaDrmEventListener implements OnEventListener { @Override public void onEvent( - ExoMediaDrm md, - @Nullable byte[] sessionId, - int event, - int extra, - @Nullable byte[] data) { + ExoMediaDrm md, @Nullable byte[] sessionId, int event, int extra, @Nullable byte[] data) { Assertions.checkNotNull(mediaDrmHandler).obtainMessage(event, sessionId).sendToTarget(); } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmSession.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmSession.java index 35358f04f7..3f2aae7b30 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmSession.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmSession.java @@ -18,33 +18,35 @@ package com.google.android.exoplayer2.drm; import android.media.MediaDrm; import androidx.annotation.IntDef; import androidx.annotation.Nullable; +import com.google.android.exoplayer2.util.MediaSourceEventDispatcher; import java.io.IOException; import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.Map; -/** - * A DRM session. - */ -public interface DrmSession { +/** A DRM session. */ +public interface DrmSession { /** - * Invokes {@code newSession's} {@link #acquire()} and {@code previousSession's} {@link - * #release()} in that order. Null arguments are ignored. Does nothing if {@code previousSession} + * Acquires {@code newSession} then releases {@code previousSession}. + * + *

      Invokes {@code newSession's} {@link #acquire(MediaSourceEventDispatcher)} and {@code + * previousSession's} {@link #release(MediaSourceEventDispatcher)} in that order (passing {@code + * eventDispatcher = null}). Null arguments are ignored. Does nothing if {@code previousSession} * and {@code newSession} are the same session. */ - static void replaceSession( - @Nullable DrmSession previousSession, @Nullable DrmSession newSession) { + static void replaceSession( + @Nullable DrmSession previousSession, @Nullable DrmSession newSession) { if (previousSession == newSession) { // Do nothing. return; } if (newSession != null) { - newSession.acquire(); + newSession.acquire(/* eventDispatcher= */ null); } if (previousSession != null) { - previousSession.release(); + previousSession.release(/* eventDispatcher= */ null); } } @@ -102,11 +104,11 @@ public interface DrmSession { DrmSessionException getError(); /** - * Returns a {@link ExoMediaCrypto} for the open session, or null if called before the session has - * been opened or after it's been released. + * Returns an {@link ExoMediaCrypto} for the open session, or null if called before the session + * has been opened or after it's been released. */ @Nullable - T getMediaCrypto(); + ExoMediaCrypto getMediaCrypto(); /** * Returns a map describing the key status for the session, or null if called before the session @@ -132,13 +134,20 @@ public interface DrmSession { /** * Increments the reference count. When the caller no longer needs to use the instance, it must - * call {@link #release()} to decrement the reference count. + * call {@link #release(MediaSourceEventDispatcher)} to decrement the reference count. + * + * @param eventDispatcher The {@link MediaSourceEventDispatcher} used to route DRM-related events + * dispatched from this session, or null if no event handling is needed. */ - void acquire(); + void acquire(@Nullable MediaSourceEventDispatcher eventDispatcher); /** * Decrements the reference count. If the reference count drops to 0 underlying resources are * released, and the instance cannot be re-used. + * + * @param eventDispatcher The {@link MediaSourceEventDispatcher} to disconnect when the session is + * released (the same instance (possibly null) that was passed by the caller to {@link + * #acquire(MediaSourceEventDispatcher)}). */ - void release(); + void release(@Nullable MediaSourceEventDispatcher eventDispatcher); } 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/DrmSessionEventListener.java similarity index 94% rename from library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionEventListener.java rename to library/core/src/main/java/com/google/android/exoplayer2/drm/DrmSessionEventListener.java index 297f26bb71..dd306d952f 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/DrmSessionEventListener.java @@ -17,8 +17,8 @@ package com.google.android.exoplayer2.drm; import com.google.android.exoplayer2.Player; -/** Listener of {@link DefaultDrmSessionManager} events. */ -public interface DefaultDrmSessionEventListener { +/** Listener of {@link DrmSessionManager} events. */ +public interface DrmSessionEventListener { /** Called each time a drm session is acquired. */ default void onDrmSessionAcquired() {} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmSessionManager.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmSessionManager.java index 146c5d704d..0283470765 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmSessionManager.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmSessionManager.java @@ -19,21 +19,19 @@ import android.os.Looper; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.drm.DrmInitData.SchemeData; +import com.google.android.exoplayer2.util.MediaSourceEventDispatcher; -/** - * Manages a DRM session. - */ -public interface DrmSessionManager { +/** Manages a DRM session. */ +public interface DrmSessionManager { /** Returns {@link #DUMMY}. */ - @SuppressWarnings("unchecked") - static DrmSessionManager getDummyDrmSessionManager() { - return (DrmSessionManager) DUMMY; + static DrmSessionManager getDummyDrmSessionManager() { + return DUMMY; } /** {@link DrmSessionManager} that supports no DRM schemes. */ - DrmSessionManager DUMMY = - new DrmSessionManager() { + DrmSessionManager DUMMY = + new DrmSessionManager() { @Override public boolean canAcquireSession(DrmInitData drmInitData) { @@ -41,9 +39,11 @@ public interface DrmSessionManager { } @Override - public DrmSession acquireSession( - Looper playbackLooper, DrmInitData drmInitData) { - return new ErrorStateDrmSession<>( + public DrmSession acquireSession( + Looper playbackLooper, + @Nullable MediaSourceEventDispatcher eventDispatcher, + DrmInitData drmInitData) { + return new ErrorStateDrmSession( new DrmSession.DrmSessionException( new UnsupportedDrmException(UnsupportedDrmException.REASON_UNSUPPORTED_SCHEME))); } @@ -83,7 +83,7 @@ public interface DrmSessionManager { /** * Returns a {@link DrmSession} that does not execute key requests, with an incremented reference * count. When the caller no longer needs to use the instance, it must call {@link - * DrmSession#release()} to decrement the reference count. + * DrmSession#release(MediaSourceEventDispatcher)} to decrement the reference count. * *

      Placeholder {@link DrmSession DrmSessions} may be used to configure secure decoders for * playback of clear content periods. This can reduce the cost of transitioning between clear and @@ -96,21 +96,26 @@ public interface DrmSessionManager { * placeholder sessions. */ @Nullable - default DrmSession acquirePlaceholderSession(Looper playbackLooper, int trackType) { + default DrmSession acquirePlaceholderSession(Looper playbackLooper, int trackType) { return null; } /** * Returns a {@link DrmSession} for the specified {@link DrmInitData}, with an incremented * reference count. When the caller no longer needs to use the instance, it must call {@link - * DrmSession#release()} to decrement the reference count. + * DrmSession#release(MediaSourceEventDispatcher)} to decrement the reference count. * * @param playbackLooper The looper associated with the media playback thread. + * @param eventDispatcher The {@link MediaSourceEventDispatcher} used to distribute events, and + * passed on to {@link DrmSession#acquire(MediaSourceEventDispatcher)}. * @param drmInitData DRM initialization data. All contained {@link SchemeData}s must contain * non-null {@link SchemeData#data}. * @return The DRM session. */ - DrmSession acquireSession(Looper playbackLooper, DrmInitData drmInitData); + DrmSession acquireSession( + Looper playbackLooper, + @Nullable MediaSourceEventDispatcher eventDispatcher, + DrmInitData drmInitData); /** * Returns the {@link ExoMediaCrypto} type returned by sessions acquired using the given {@link diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/DummyExoMediaDrm.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/DummyExoMediaDrm.java index b619d9486f..d8311f6701 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/DummyExoMediaDrm.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/DummyExoMediaDrm.java @@ -26,21 +26,25 @@ import java.util.Map; /** An {@link ExoMediaDrm} that does not support any protection schemes. */ @RequiresApi(18) -public final class DummyExoMediaDrm implements ExoMediaDrm { +public final class DummyExoMediaDrm implements ExoMediaDrm { /** Returns a new instance. */ - @SuppressWarnings("unchecked") - public static DummyExoMediaDrm getInstance() { - return (DummyExoMediaDrm) new DummyExoMediaDrm<>(); + public static DummyExoMediaDrm getInstance() { + return new DummyExoMediaDrm(); } @Override - public void setOnEventListener(OnEventListener listener) { + public void setOnEventListener(@Nullable OnEventListener listener) { // Do nothing. } @Override - public void setOnKeyStatusChangeListener(OnKeyStatusChangeListener listener) { + public void setOnKeyStatusChangeListener(@Nullable OnKeyStatusChangeListener listener) { + // Do nothing. + } + + @Override + public void setOnExpirationUpdateListener(@Nullable OnExpirationUpdateListener listener) { // Do nothing. } @@ -64,8 +68,8 @@ public final class DummyExoMediaDrm implements ExoMedi throw new IllegalStateException(); } - @Nullable @Override + @Nullable public byte[] provideKeyResponse(byte[] scope, byte[] response) { // Should not be invoked. No session should exist. throw new IllegalStateException(); @@ -132,14 +136,14 @@ public final class DummyExoMediaDrm implements ExoMedi } @Override - public T createMediaCrypto(byte[] sessionId) { + public ExoMediaCrypto createMediaCrypto(byte[] sessionId) { // Should not be invoked. No session should exist. throw new IllegalStateException(); } @Override @Nullable - public Class getExoMediaCryptoType() { + public Class getExoMediaCryptoType() { // No ExoMediaCrypto type is supported. return null; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/ErrorStateDrmSession.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/ErrorStateDrmSession.java index 0028e47987..ff0a861f4b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/ErrorStateDrmSession.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/ErrorStateDrmSession.java @@ -17,10 +17,11 @@ package com.google.android.exoplayer2.drm; import androidx.annotation.Nullable; import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.MediaSourceEventDispatcher; import java.util.Map; /** A {@link DrmSession} that's in a terminal error state. */ -public final class ErrorStateDrmSession implements DrmSession { +public final class ErrorStateDrmSession implements DrmSession { private final DrmSessionException error; @@ -46,7 +47,7 @@ public final class ErrorStateDrmSession implements Drm @Override @Nullable - public T getMediaCrypto() { + public ExoMediaCrypto getMediaCrypto() { return null; } @@ -63,12 +64,12 @@ public final class ErrorStateDrmSession implements Drm } @Override - public void acquire() { + public void acquire(@Nullable MediaSourceEventDispatcher eventDispatcher) { // Do nothing. } @Override - public void release() { + public void release(@Nullable MediaSourceEventDispatcher eventDispatcher) { // Do nothing. } } 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 b6ee644842..957945fa2a 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 @@ -42,17 +42,17 @@ import java.util.UUID; * new instance does not normally need to call {@link #acquire()}, and must call {@link #release()} * when the instance is no longer required. */ -public interface ExoMediaDrm { +public interface ExoMediaDrm { /** {@link ExoMediaDrm} instances provider. */ - interface Provider { + interface Provider { /** * Returns an {@link ExoMediaDrm} instance with an incremented reference count. When the caller * no longer needs to use the instance, it must call {@link ExoMediaDrm#release()} to decrement * the reference count. */ - ExoMediaDrm acquireExoMediaDrm(UUID uuid); + ExoMediaDrm acquireExoMediaDrm(UUID uuid); } /** @@ -62,17 +62,17 @@ public interface ExoMediaDrm { * instance, and remains responsible for calling {@link ExoMediaDrm#release()} on the instance * when it's no longer being used. */ - final class AppManagedProvider implements Provider { + final class AppManagedProvider implements Provider { - private final ExoMediaDrm exoMediaDrm; + private final ExoMediaDrm exoMediaDrm; /** Creates an instance that provides the given {@link ExoMediaDrm}. */ - public AppManagedProvider(ExoMediaDrm exoMediaDrm) { + public AppManagedProvider(ExoMediaDrm exoMediaDrm) { this.exoMediaDrm = exoMediaDrm; } @Override - public ExoMediaDrm acquireExoMediaDrm(UUID uuid) { + public ExoMediaDrm acquireExoMediaDrm(UUID uuid) { exoMediaDrm.acquire(); return exoMediaDrm; } @@ -108,10 +108,8 @@ public interface ExoMediaDrm { @SuppressWarnings("InlinedApi") int KEY_TYPE_RELEASE = MediaDrm.KEY_TYPE_RELEASE; - /** - * @see android.media.MediaDrm.OnEventListener - */ - interface OnEventListener { + /** @see android.media.MediaDrm.OnEventListener */ + interface OnEventListener { /** * Called when an event occurs that requires the app to be notified * @@ -122,17 +120,15 @@ public interface ExoMediaDrm { * @param data Optional byte array of data that may be associated with the event. */ void onEvent( - ExoMediaDrm mediaDrm, + ExoMediaDrm mediaDrm, @Nullable byte[] sessionId, int event, int extra, @Nullable byte[] data); } - /** - * @see android.media.MediaDrm.OnKeyStatusChangeListener - */ - interface OnKeyStatusChangeListener { + /** @see android.media.MediaDrm.OnKeyStatusChangeListener */ + interface OnKeyStatusChangeListener { /** * Called when the keys in a session change status, such as when the license is renewed or * expires. @@ -143,12 +139,28 @@ public interface ExoMediaDrm { * @param hasNewUsableKey Whether a new key became usable. */ void onKeyStatusChange( - ExoMediaDrm mediaDrm, + ExoMediaDrm mediaDrm, byte[] sessionId, List exoKeyInformation, boolean hasNewUsableKey); } + /** @see android.media.MediaDrm.OnExpirationUpdateListener */ + interface OnExpirationUpdateListener { + + /** + * Called when a session expiration update occurs, to inform the app about the change in + * expiration time + * + * @param mediaDrm The {@link ExoMediaDrm} object on which the event occurred. + * @param sessionId The DRM session ID on which the event occurred + * @param expirationTimeMs The new expiration time for the keys in the session. The time is in + * milliseconds, relative to the Unix epoch. A time of 0 indicates that the keys never + * expire. + */ + void onExpirationUpdate(ExoMediaDrm mediaDrm, byte[] sessionId, long expirationTimeMs); + } + /** @see android.media.MediaDrm.KeyStatus */ final class KeyStatus { @@ -213,18 +225,42 @@ public interface ExoMediaDrm { } /** + * Sets the listener for DRM events. + * + *

      This is an optional method, and some implementations may only support it on certain Android + * API levels. + * + * @param listener The listener to receive events, or {@code null} to stop receiving events. + * @throws UnsupportedOperationException if the implementation doesn't support this method. * @see MediaDrm#setOnEventListener(MediaDrm.OnEventListener) */ - void setOnEventListener(OnEventListener listener); + void setOnEventListener(@Nullable OnEventListener listener); /** + * Sets the listener for key status change events. + * + *

      This is an optional method, and some implementations may only support it on certain Android + * API levels. + * + * @param listener The listener to receive events, or {@code null} to stop receiving events. + * @throws UnsupportedOperationException if the implementation doesn't support this method. * @see MediaDrm#setOnKeyStatusChangeListener(MediaDrm.OnKeyStatusChangeListener, Handler) */ - void setOnKeyStatusChangeListener(OnKeyStatusChangeListener listener); + void setOnKeyStatusChangeListener(@Nullable OnKeyStatusChangeListener listener); /** - * @see MediaDrm#openSession() + * Sets the listener for session expiration events. + * + *

      This is an optional method, and some implementations may only support it on certain Android + * API levels. + * + * @param listener The listener to receive events, or {@code null} to stop receiving events. + * @throws UnsupportedOperationException if the implementation doesn't support this method. + * @see MediaDrm#setOnExpirationUpdateListener(MediaDrm.OnExpirationUpdateListener, Handler) */ + void setOnExpirationUpdateListener(@Nullable OnExpirationUpdateListener listener); + + /** @see MediaDrm#openSession() */ byte[] openSession() throws MediaDrmException; /** @@ -331,12 +367,12 @@ public interface ExoMediaDrm { * @return An object extends {@link ExoMediaCrypto}, using opaque crypto scheme specific data. * @throws MediaCryptoException If the instance can't be created. */ - T createMediaCrypto(byte[] sessionId) throws MediaCryptoException; + ExoMediaCrypto createMediaCrypto(byte[] sessionId) throws MediaCryptoException; /** * Returns the {@link ExoMediaCrypto} type created by {@link #createMediaCrypto(byte[])}, or null * if this instance cannot create any {@link ExoMediaCrypto} instances. */ @Nullable - Class getExoMediaCryptoType(); + Class getExoMediaCryptoType(); } 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 8ac92b093c..2227738ed5 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 @@ -47,7 +47,7 @@ import java.util.UUID; /** An {@link ExoMediaDrm} implementation that wraps the framework {@link MediaDrm}. */ @TargetApi(23) @RequiresApi(18) -public final class FrameworkMediaDrm implements ExoMediaDrm { +public final class FrameworkMediaDrm implements ExoMediaDrm { private static final String TAG = "FrameworkMediaDrm"; @@ -56,13 +56,13 @@ public final class FrameworkMediaDrm implements ExoMediaDrm DEFAULT_PROVIDER = + public static final Provider DEFAULT_PROVIDER = uuid -> { try { return newInstance(uuid); } catch (UnsupportedDrmException e) { Log.e(TAG, "Failed to instantiate a FrameworkMediaDrm for uuid: " + uuid + "."); - return new DummyExoMediaDrm<>(); + return new DummyExoMediaDrm(); } }; @@ -106,8 +106,7 @@ public final class FrameworkMediaDrm implements ExoMediaDrm listener) { + public void setOnEventListener(@Nullable ExoMediaDrm.OnEventListener listener) { mediaDrm.setOnEventListener( listener == null ? null @@ -115,9 +114,16 @@ public final class FrameworkMediaDrm implements ExoMediaDrm listener) { + @Nullable ExoMediaDrm.OnKeyStatusChangeListener listener) { if (Util.SDK_INT < 23) { throw new UnsupportedOperationException(); } @@ -133,7 +139,29 @@ public final class FrameworkMediaDrm implements ExoMediaDrm { + listener.onExpirationUpdate(FrameworkMediaDrm.this, sessionId, expirationTimeMs); + }, + /* handler= */ null); } @Override @@ -179,8 +207,8 @@ public final class FrameworkMediaDrm implements ExoMediaDrm requestProperties) throws IOException { HttpDataSource dataSource = dataSourceFactory.createDataSource(); - if (requestProperties != null) { - for (Map.Entry requestProperty : requestProperties.entrySet()) { - dataSource.setRequestProperty(requestProperty.getKey(), requestProperty.getValue()); - } - } - int manualRedirectCount = 0; while (true) { DataSpec dataSpec = - new DataSpec( - Uri.parse(url), - DataSpec.HTTP_METHOD_POST, - httpBody, - /* absoluteStreamPosition= */ 0, - /* position= */ 0, - /* length= */ C.LENGTH_UNSET, - /* key= */ null, - DataSpec.FLAG_ALLOW_GZIP); + new DataSpec.Builder() + .setUri(url) + .setHttpRequestHeaders( + requestProperties != null ? requestProperties : Collections.emptyMap()) + .setHttpMethod(DataSpec.HTTP_METHOD_POST) + .setHttpBody(httpBody) + .setFlags(DataSpec.FLAG_ALLOW_GZIP) + .build(); DataSourceInputStream inputStream = new DataSourceInputStream(dataSource, dataSpec); try { return Util.toByteArray(inputStream); @@ -170,7 +159,7 @@ public final class HttpMediaDrmCallback implements MediaDrmCallback { boolean manuallyRedirect = (e.responseCode == 307 || e.responseCode == 308) && manualRedirectCount++ < MAX_MANUAL_REDIRECTS; - String redirectUrl = manuallyRedirect ? getRedirectUrl(e) : null; + @Nullable String redirectUrl = manuallyRedirect ? getRedirectUrl(e) : null; if (redirectUrl == null) { throw e; } @@ -184,12 +173,11 @@ public final class HttpMediaDrmCallback implements MediaDrmCallback { private static @Nullable String getRedirectUrl(InvalidResponseCodeException exception) { Map> headerFields = exception.headerFields; if (headerFields != null) { - List locationHeaders = headerFields.get("Location"); + @Nullable List locationHeaders = headerFields.get("Location"); if (locationHeaders != null && !locationHeaders.isEmpty()) { return locationHeaders.get(0); } } return null; } - } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java index 93a7585f89..6092f3911f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java @@ -15,7 +15,6 @@ */ package com.google.android.exoplayer2.drm; -import android.annotation.TargetApi; import android.media.MediaDrm; import android.os.ConditionVariable; import android.os.Handler; @@ -23,26 +22,24 @@ import android.os.HandlerThread; import android.util.Pair; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; -import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.drm.DefaultDrmSessionManager.Mode; import com.google.android.exoplayer2.drm.DrmSession.DrmSessionException; import com.google.android.exoplayer2.upstream.HttpDataSource; -import com.google.android.exoplayer2.upstream.HttpDataSource.Factory; import com.google.android.exoplayer2.util.Assertions; -import java.util.Collections; +import com.google.android.exoplayer2.util.MediaSourceEventDispatcher; import java.util.Map; import java.util.UUID; /** Helper class to download, renew and release offline licenses. */ -@TargetApi(18) @RequiresApi(18) -public final class OfflineLicenseHelper { +public final class OfflineLicenseHelper { private static final DrmInitData DUMMY_DRM_INIT_DATA = new DrmInitData(); private final ConditionVariable conditionVariable; - private final DefaultDrmSessionManager drmSessionManager; + private final DefaultDrmSessionManager drmSessionManager; private final HandlerThread handlerThread; + private final MediaSourceEventDispatcher eventDispatcher; /** * Instantiates a new instance which uses Widevine CDM. Call {@link #release()} when the instance @@ -51,14 +48,19 @@ public final class OfflineLicenseHelper { * @param defaultLicenseUrl The default license URL. Used for key requests that do not specify * their own license URL. * @param httpDataSourceFactory A factory from which to obtain {@link HttpDataSource} instances. + * @param eventDispatcher A {@link MediaSourceEventDispatcher} used to distribute DRM-related + * events. * @return A new instance which uses Widevine CDM. - * @throws UnsupportedDrmException If the Widevine DRM scheme is unsupported or cannot be - * instantiated. */ - public static OfflineLicenseHelper newWidevineInstance( - String defaultLicenseUrl, Factory httpDataSourceFactory) - throws UnsupportedDrmException { - return newWidevineInstance(defaultLicenseUrl, false, httpDataSourceFactory, null); + public static OfflineLicenseHelper newWidevineInstance( + String defaultLicenseUrl, + HttpDataSource.Factory httpDataSourceFactory, + MediaSourceEventDispatcher eventDispatcher) { + return newWidevineInstance( + defaultLicenseUrl, + /* forceDefaultLicenseUrl= */ false, + httpDataSourceFactory, + eventDispatcher); } /** @@ -70,15 +72,21 @@ public final class OfflineLicenseHelper { * @param forceDefaultLicenseUrl Whether to use {@code defaultLicenseUrl} for key requests that * include their own license URL. * @param httpDataSourceFactory A factory from which to obtain {@link HttpDataSource} instances. + * @param eventDispatcher A {@link MediaSourceEventDispatcher} used to distribute DRM-related + * events. * @return A new instance which uses Widevine CDM. - * @throws UnsupportedDrmException If the Widevine DRM scheme is unsupported or cannot be - * instantiated. */ - public static OfflineLicenseHelper newWidevineInstance( - String defaultLicenseUrl, boolean forceDefaultLicenseUrl, Factory httpDataSourceFactory) - throws UnsupportedDrmException { - return newWidevineInstance(defaultLicenseUrl, forceDefaultLicenseUrl, httpDataSourceFactory, - null); + public static OfflineLicenseHelper newWidevineInstance( + String defaultLicenseUrl, + boolean forceDefaultLicenseUrl, + HttpDataSource.Factory httpDataSourceFactory, + MediaSourceEventDispatcher eventDispatcher) { + return newWidevineInstance( + defaultLicenseUrl, + forceDefaultLicenseUrl, + httpDataSourceFactory, + /* optionalKeyRequestParameters= */ null, + eventDispatcher); } /** @@ -91,45 +99,62 @@ public final class OfflineLicenseHelper { * include their own license URL. * @param optionalKeyRequestParameters An optional map of parameters to pass as the last argument * to {@link MediaDrm#getKeyRequest}. May be null. + * @param eventDispatcher A {@link MediaSourceEventDispatcher} used to distribute DRM-related + * events. * @return A new instance which uses Widevine CDM. - * @throws UnsupportedDrmException If the Widevine DRM scheme is unsupported or cannot be - * instantiated. * @see DefaultDrmSessionManager.Builder */ - public static OfflineLicenseHelper newWidevineInstance( + public static OfflineLicenseHelper newWidevineInstance( String defaultLicenseUrl, boolean forceDefaultLicenseUrl, - Factory httpDataSourceFactory, - @Nullable Map optionalKeyRequestParameters) - throws UnsupportedDrmException { - return new OfflineLicenseHelper<>( - C.WIDEVINE_UUID, - FrameworkMediaDrm.DEFAULT_PROVIDER, - new HttpMediaDrmCallback(defaultLicenseUrl, forceDefaultLicenseUrl, httpDataSourceFactory), - optionalKeyRequestParameters); + HttpDataSource.Factory httpDataSourceFactory, + @Nullable Map optionalKeyRequestParameters, + MediaSourceEventDispatcher eventDispatcher) { + return new OfflineLicenseHelper( + new DefaultDrmSessionManager.Builder() + .setKeyRequestParameters(optionalKeyRequestParameters) + .build( + new HttpMediaDrmCallback( + defaultLicenseUrl, forceDefaultLicenseUrl, httpDataSourceFactory)), + eventDispatcher); + } + + /** + * @deprecated Use {@link #OfflineLicenseHelper(DefaultDrmSessionManager, + * MediaSourceEventDispatcher)} instead. + */ + @Deprecated + public OfflineLicenseHelper( + UUID uuid, + ExoMediaDrm.Provider mediaDrmProvider, + MediaDrmCallback callback, + @Nullable Map optionalKeyRequestParameters, + MediaSourceEventDispatcher eventDispatcher) { + this( + new DefaultDrmSessionManager.Builder() + .setUuidAndExoMediaDrmProvider(uuid, mediaDrmProvider) + .setKeyRequestParameters(optionalKeyRequestParameters) + .build(callback), + eventDispatcher); } /** * Constructs an instance. Call {@link #release()} when the instance is no longer required. * - * @param uuid The UUID of the drm scheme. - * @param mediaDrmProvider A {@link ExoMediaDrm.Provider}. - * @param callback Performs key and provisioning requests. - * @param optionalKeyRequestParameters An optional map of parameters to pass as the last argument - * to {@link MediaDrm#getKeyRequest}. May be null. - * @see DefaultDrmSessionManager.Builder + * @param defaultDrmSessionManager The {@link DefaultDrmSessionManager} used to download licenses. + * @param eventDispatcher A {@link MediaSourceEventDispatcher} used to distribute DRM-related + * events. */ - @SuppressWarnings("unchecked") public OfflineLicenseHelper( - UUID uuid, - ExoMediaDrm.Provider mediaDrmProvider, - MediaDrmCallback callback, - @Nullable Map optionalKeyRequestParameters) { - handlerThread = new HandlerThread("OfflineLicenseHelper"); + DefaultDrmSessionManager defaultDrmSessionManager, + MediaSourceEventDispatcher eventDispatcher) { + this.drmSessionManager = defaultDrmSessionManager; + this.eventDispatcher = eventDispatcher; + handlerThread = new HandlerThread("ExoPlayer:OfflineLicenseHelper"); handlerThread.start(); conditionVariable = new ConditionVariable(); - DefaultDrmSessionEventListener eventListener = - new DefaultDrmSessionEventListener() { + DrmSessionEventListener eventListener = + new DrmSessionEventListener() { @Override public void onDrmKeysLoaded() { conditionVariable.open(); @@ -150,16 +175,8 @@ public final class OfflineLicenseHelper { conditionVariable.open(); } }; - if (optionalKeyRequestParameters == null) { - optionalKeyRequestParameters = Collections.emptyMap(); - } - drmSessionManager = - (DefaultDrmSessionManager) - new DefaultDrmSessionManager.Builder() - .setUuidAndExoMediaDrmProvider(uuid, mediaDrmProvider) - .setKeyRequestParameters(optionalKeyRequestParameters) - .build(callback); - drmSessionManager.addListener(new Handler(handlerThread.getLooper()), eventListener); + eventDispatcher.addEventListener( + new Handler(handlerThread.getLooper()), eventListener, DrmSessionEventListener.class); } /** @@ -212,13 +229,13 @@ public final class OfflineLicenseHelper { throws DrmSessionException { Assertions.checkNotNull(offlineLicenseKeySetId); drmSessionManager.prepare(); - DrmSession drmSession = + DrmSession drmSession = openBlockingKeyRequest( DefaultDrmSessionManager.MODE_QUERY, offlineLicenseKeySetId, DUMMY_DRM_INIT_DATA); DrmSessionException error = drmSession.getError(); Pair licenseDurationRemainingSec = WidevineUtil.getLicenseDurationRemainingSec(drmSession); - drmSession.release(); + drmSession.release(eventDispatcher); drmSessionManager.release(); if (error != null) { if (error.getCause() instanceof KeysExpiredException) { @@ -240,11 +257,11 @@ public final class OfflineLicenseHelper { @Mode int licenseMode, @Nullable byte[] offlineLicenseKeySetId, DrmInitData drmInitData) throws DrmSessionException { drmSessionManager.prepare(); - DrmSession drmSession = openBlockingKeyRequest(licenseMode, offlineLicenseKeySetId, - drmInitData); + DrmSession drmSession = + openBlockingKeyRequest(licenseMode, offlineLicenseKeySetId, drmInitData); DrmSessionException error = drmSession.getError(); byte[] keySetId = drmSession.getOfflineLicenseKeySetId(); - drmSession.release(); + drmSession.release(eventDispatcher); drmSessionManager.release(); if (error != null) { throw error; @@ -252,12 +269,12 @@ public final class OfflineLicenseHelper { return Assertions.checkNotNull(keySetId); } - private DrmSession openBlockingKeyRequest( + private DrmSession openBlockingKeyRequest( @Mode int licenseMode, @Nullable byte[] offlineLicenseKeySetId, DrmInitData drmInitData) { drmSessionManager.setMode(licenseMode, offlineLicenseKeySetId); conditionVariable.close(); - DrmSession drmSession = drmSessionManager.acquireSession(handlerThread.getLooper(), - drmInitData); + DrmSession drmSession = + drmSessionManager.acquireSession(handlerThread.getLooper(), eventDispatcher, drmInitData); // Block current thread until key loading is finished conditionVariable.block(); return drmSession; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/WidevineUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/WidevineUtil.java index 004f873a33..5f60ad690e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/WidevineUtil.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/WidevineUtil.java @@ -39,8 +39,7 @@ public final class WidevineUtil { * @return A {@link Pair} consisting of the remaining license and playback durations in seconds, * or null if called before the session has been opened or after it's been released. */ - public static @Nullable Pair getLicenseDurationRemainingSec( - DrmSession drmSession) { + public static @Nullable Pair getLicenseDurationRemainingSec(DrmSession drmSession) { Map keyStatus = drmSession.queryKeyStatus(); if (keyStatus == null) { return null; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/FlacFrameReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/FlacFrameReader.java deleted file mode 100644 index d8d6b8b500..0000000000 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/FlacFrameReader.java +++ /dev/null @@ -1,261 +0,0 @@ -/* - * Copyright (C) 2019 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.util.FlacStreamMetadata; -import com.google.android.exoplayer2.util.ParsableByteArray; -import com.google.android.exoplayer2.util.Util; - -/** Reads and peeks FLAC frame elements. */ -public final class FlacFrameReader { - - /** Holds a frame block size. */ - public static final class BlockSizeHolder { - /** The block size in samples. */ - public int blockSizeSamples; - } - - /** - * Checks whether the given FLAC frame header is valid and, if so, reads it and writes the frame - * block size in {@code blockSizeHolder}. - * - *

      If the header is valid, the position of {@code scratch} is moved to the byte following it. - * Otherwise, there is no guarantee on the position. - * - * @param scratch The array to read the data from, whose position must correspond to the frame - * header. - * @param flacStreamMetadata The stream metadata. - * @param frameStartMarker The frame start marker of the stream. - * @param blockSizeHolder The holder used to contain the block size. - * @return Whether the frame header is valid. - */ - public static boolean checkAndReadFrameHeader( - ParsableByteArray scratch, - FlacStreamMetadata flacStreamMetadata, - int frameStartMarker, - BlockSizeHolder blockSizeHolder) { - int frameStartPosition = scratch.getPosition(); - - long frameHeaderBytes = scratch.readUnsignedInt(); - if (frameHeaderBytes >>> 16 != frameStartMarker) { - return false; - } - - int blockSizeKey = (int) (frameHeaderBytes >> 12 & 0xF); - int sampleRateKey = (int) (frameHeaderBytes >> 8 & 0xF); - int channelAssignmentKey = (int) (frameHeaderBytes >> 4 & 0xF); - int bitsPerSampleKey = (int) (frameHeaderBytes >> 1 & 0x7); - boolean reservedBit = (frameHeaderBytes & 1) == 1; - return checkChannelAssignment(channelAssignmentKey, flacStreamMetadata) - && checkBitsPerSample(bitsPerSampleKey, flacStreamMetadata) - && !reservedBit - && checkAndReadUtf8Data(scratch) - && checkAndReadBlockSizeSamples(scratch, flacStreamMetadata, blockSizeKey, blockSizeHolder) - && checkAndReadSampleRate(scratch, flacStreamMetadata, sampleRateKey) - && checkAndReadCrc(scratch, frameStartPosition); - } - - /** - * Returns the block size of the given frame. - * - *

      If no exception is thrown, the position of {@code scratch} is left unchanged. Otherwise, - * there is no guarantee on the position. - * - * @param scratch The array to get the data from, whose position must correspond to the start of a - * frame. - * @return The block size in samples, or -1 if the block size is invalid. - */ - public static int getFrameBlockSizeSamples(ParsableByteArray scratch) { - int blockSizeKey = (scratch.data[2] & 0xFF) >> 4; - if (blockSizeKey < 6 || blockSizeKey > 7) { - return readFrameBlockSizeSamplesFromKey(scratch, blockSizeKey); - } - scratch.skipBytes(4); - scratch.readUtf8EncodedLong(); - int blockSizeSamples = readFrameBlockSizeSamplesFromKey(scratch, blockSizeKey); - scratch.setPosition(0); - return blockSizeSamples; - } - - /** - * Reads the given block size. - * - * @param scratch The array to read the data from, whose position must correspond to the block - * size bits. - * @param blockSizeKey The key in the block size lookup table. - * @return The block size in samples. - */ - public static int readFrameBlockSizeSamplesFromKey(ParsableByteArray scratch, int blockSizeKey) { - switch (blockSizeKey) { - case 1: - return 192; - case 2: - case 3: - case 4: - case 5: - return 576 << (blockSizeKey - 2); - case 6: - return scratch.readUnsignedByte() + 1; - case 7: - return scratch.readUnsignedShort() + 1; - case 8: - case 9: - case 10: - case 11: - case 12: - case 13: - case 14: - case 15: - return 256 << (blockSizeKey - 8); - default: - return -1; - } - } - - /** - * Checks whether the given channel assignment is valid. - * - * @param channelAssignmentKey The channel assignment lookup key. - * @param flacStreamMetadata The stream metadata. - * @return Whether the channel assignment is valid. - */ - private static boolean checkChannelAssignment( - int channelAssignmentKey, FlacStreamMetadata flacStreamMetadata) { - if (channelAssignmentKey <= 7) { - return channelAssignmentKey == flacStreamMetadata.channels - 1; - } else if (channelAssignmentKey <= 10) { - return flacStreamMetadata.channels == 2; - } else { - return false; - } - } - - /** - * Checks whether the given number of bits per sample is valid. - * - * @param bitsPerSampleKey The bits per sample lookup key. - * @param flacStreamMetadata The stream metadata. - * @return Whether the number of bits per sample is valid. - */ - private static boolean checkBitsPerSample( - int bitsPerSampleKey, FlacStreamMetadata flacStreamMetadata) { - if (bitsPerSampleKey == 0) { - return true; - } - return bitsPerSampleKey == flacStreamMetadata.bitsPerSampleLookupKey; - } - - /** - * Checks whether the given UTF-8 data is valid and, if so, reads it. - * - *

      If the UTF-8 data is valid, the position of {@code scratch} is moved to the byte following - * it. Otherwise, there is no guarantee on the position. - * - * @param scratch The array to read the data from, whose position must correspond to the UTF-8 - * data. - * @return Whether the UTF-8 data is valid. - */ - private static boolean checkAndReadUtf8Data(ParsableByteArray scratch) { - try { - scratch.readUtf8EncodedLong(); - } catch (NumberFormatException e) { - return false; - } - return true; - } - - /** - * Checks whether the given frame block size key and block size bits are valid and, if so, reads - * the block size bits and writes the block size in {@code blockSizeHolder}. - * - *

      If the block size is valid, the position of {@code scratch} is moved to the byte following - * the block size bits. Otherwise, there is no guarantee on the position. - * - * @param scratch The array to read the data from, whose position must correspond to the block - * size bits. - * @param flacStreamMetadata The stream metadata. - * @param blockSizeKey The key in the block size lookup table. - * @param blockSizeHolder The holder used to contain the block size. - * @return Whether the block size is valid. - */ - private static boolean checkAndReadBlockSizeSamples( - ParsableByteArray scratch, - FlacStreamMetadata flacStreamMetadata, - int blockSizeKey, - BlockSizeHolder blockSizeHolder) { - int blockSizeSamples = readFrameBlockSizeSamplesFromKey(scratch, blockSizeKey); - if (blockSizeSamples == -1 || blockSizeSamples > flacStreamMetadata.maxBlockSizeSamples) { - return false; - } - blockSizeHolder.blockSizeSamples = blockSizeSamples; - return true; - } - - /** - * Checks whether the given sample rate key and sample rate bits are valid and, if so, reads the - * sample rate bits. - * - *

      If the sample rate is valid, the position of {@code scratch} is moved to the byte following - * the sample rate bits. Otherwise, there is no guarantee on the position. - * - * @param scratch The array to read the data from, whose position must indicate the sample rate - * bits. - * @param flacStreamMetadata The stream metadata. - * @param sampleRateKey The key in the sample rate lookup table. - * @return Whether the sample rate is valid. - */ - private static boolean checkAndReadSampleRate( - ParsableByteArray scratch, FlacStreamMetadata flacStreamMetadata, int sampleRateKey) { - int expectedSampleRate = flacStreamMetadata.sampleRate; - if (sampleRateKey == 0) { - return true; - } else if (sampleRateKey <= 11) { - return sampleRateKey == flacStreamMetadata.sampleRateLookupKey; - } else if (sampleRateKey == 12) { - return scratch.readUnsignedByte() * 1000 == expectedSampleRate; - } else if (sampleRateKey <= 14) { - int sampleRate = scratch.readUnsignedShort(); - if (sampleRateKey == 14) { - sampleRate *= 10; - } - return sampleRate == expectedSampleRate; - } else { - return false; - } - } - - /** - * Checks whether the given CRC is valid and, if so, reads it. - * - *

      If the CRC is valid, the position of {@code scratch} is moved to the byte following it. - * Otherwise, there is no guarantee on the position. - * - *

      The {@code scratch} array must contain the whole frame header. - * - * @param scratch The array to read the data from, whose position must indicate the CRC. - * @param frameStartPosition The frame start offset in {@code scratch}. - * @return Whether the CRC is valid. - */ - private static boolean checkAndReadCrc(ParsableByteArray scratch, int frameStartPosition) { - int crc = scratch.readUnsignedByte(); - int frameEndPosition = scratch.getPosition(); - int expectedCrc = - Util.crc8(scratch.data, frameStartPosition, frameEndPosition - 1, /* initialValue= */ 0); - return crc == expectedCrc; - } - - private FlacFrameReader() {} -} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/MpegAudioHeader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/MpegAudioHeader.java deleted file mode 100644 index 8412b738bb..0000000000 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/MpegAudioHeader.java +++ /dev/null @@ -1,227 +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.extractor; - -import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.util.MimeTypes; -import org.checkerframework.checker.nullness.qual.Nullable; - -/** - * An MPEG audio frame header. - */ -public final class MpegAudioHeader { - - /** - * Theoretical maximum frame size for an MPEG audio stream, which occurs when playing a Layer 2 - * MPEG 2.5 audio stream at 16 kb/s (with padding). The size is 1152 sample/frame * - * 160000 bit/s / (8000 sample/s * 8 bit/byte) + 1 padding byte/frame = 2881 byte/frame. - * The next power of two size is 4 KiB. - */ - public static final int MAX_FRAME_SIZE_BYTES = 4096; - - private static final String[] MIME_TYPE_BY_LAYER = - new String[] {MimeTypes.AUDIO_MPEG_L1, MimeTypes.AUDIO_MPEG_L2, MimeTypes.AUDIO_MPEG}; - private static final int[] SAMPLING_RATE_V1 = {44100, 48000, 32000}; - private static final int[] BITRATE_V1_L1 = { - 32000, 64000, 96000, 128000, 160000, 192000, 224000, 256000, 288000, 320000, 352000, 384000, - 416000, 448000 - }; - private static final int[] BITRATE_V2_L1 = { - 32000, 48000, 56000, 64000, 80000, 96000, 112000, 128000, 144000, 160000, 176000, 192000, - 224000, 256000 - }; - private static final int[] BITRATE_V1_L2 = { - 32000, 48000, 56000, 64000, 80000, 96000, 112000, 128000, 160000, 192000, 224000, 256000, - 320000, 384000 - }; - private static final int[] BITRATE_V1_L3 = { - 32000, 40000, 48000, 56000, 64000, 80000, 96000, 112000, 128000, 160000, 192000, 224000, 256000, - 320000 - }; - private static final int[] BITRATE_V2 = { - 8000, 16000, 24000, 32000, 40000, 48000, 56000, 64000, 80000, 96000, 112000, 128000, 144000, - 160000 - }; - - /** - * Returns the size of the frame associated with {@code header}, or {@link C#LENGTH_UNSET} if it - * is invalid. - */ - public static int getFrameSize(int header) { - if ((header & 0xFFE00000) != 0xFFE00000) { - return C.LENGTH_UNSET; - } - - int version = (header >>> 19) & 3; - if (version == 1) { - return C.LENGTH_UNSET; - } - - int layer = (header >>> 17) & 3; - if (layer == 0) { - return C.LENGTH_UNSET; - } - - int bitrateIndex = (header >>> 12) & 15; - if (bitrateIndex == 0 || bitrateIndex == 0xF) { - // Disallow "free" bitrate. - return C.LENGTH_UNSET; - } - - int samplingRateIndex = (header >>> 10) & 3; - if (samplingRateIndex == 3) { - return C.LENGTH_UNSET; - } - - int samplingRate = SAMPLING_RATE_V1[samplingRateIndex]; - if (version == 2) { - // Version 2 - samplingRate /= 2; - } else if (version == 0) { - // Version 2.5 - samplingRate /= 4; - } - - int bitrate; - int padding = (header >>> 9) & 1; - if (layer == 3) { - // Layer I (layer == 3) - bitrate = version == 3 ? BITRATE_V1_L1[bitrateIndex - 1] : BITRATE_V2_L1[bitrateIndex - 1]; - return (12 * bitrate / samplingRate + padding) * 4; - } else { - // Layer II (layer == 2) or III (layer == 1) - if (version == 3) { - bitrate = layer == 2 ? BITRATE_V1_L2[bitrateIndex - 1] : BITRATE_V1_L3[bitrateIndex - 1]; - } else { - // Version 2 or 2.5. - bitrate = BITRATE_V2[bitrateIndex - 1]; - } - } - - if (version == 3) { - // Version 1 - return 144 * bitrate / samplingRate + padding; - } else { - // Version 2 or 2.5 - return (layer == 1 ? 72 : 144) * bitrate / samplingRate + padding; - } - } - - /** - * Parses {@code headerData}, populating {@code header} with the parsed data. - * - * @param headerData Header data to parse. - * @param header Header to populate with data from {@code headerData}. - * @return True if the header was populated. False otherwise, indicating that {@code headerData} - * is not a valid MPEG audio header. - */ - public static boolean populateHeader(int headerData, MpegAudioHeader header) { - if ((headerData & 0xFFE00000) != 0xFFE00000) { - return false; - } - - int version = (headerData >>> 19) & 3; - if (version == 1) { - return false; - } - - int layer = (headerData >>> 17) & 3; - if (layer == 0) { - return false; - } - - int bitrateIndex = (headerData >>> 12) & 15; - if (bitrateIndex == 0 || bitrateIndex == 0xF) { - // Disallow "free" bitrate. - return false; - } - - int samplingRateIndex = (headerData >>> 10) & 3; - if (samplingRateIndex == 3) { - return false; - } - - int sampleRate = SAMPLING_RATE_V1[samplingRateIndex]; - if (version == 2) { - // Version 2 - sampleRate /= 2; - } else if (version == 0) { - // Version 2.5 - sampleRate /= 4; - } - - int padding = (headerData >>> 9) & 1; - int bitrate; - int frameSize; - int samplesPerFrame; - if (layer == 3) { - // Layer I (layer == 3) - bitrate = version == 3 ? BITRATE_V1_L1[bitrateIndex - 1] : BITRATE_V2_L1[bitrateIndex - 1]; - frameSize = (12 * bitrate / sampleRate + padding) * 4; - samplesPerFrame = 384; - } else { - // Layer II (layer == 2) or III (layer == 1) - if (version == 3) { - // Version 1 - bitrate = layer == 2 ? BITRATE_V1_L2[bitrateIndex - 1] : BITRATE_V1_L3[bitrateIndex - 1]; - samplesPerFrame = 1152; - frameSize = 144 * bitrate / sampleRate + padding; - } else { - // Version 2 or 2.5. - bitrate = BITRATE_V2[bitrateIndex - 1]; - samplesPerFrame = layer == 1 ? 576 : 1152; - frameSize = (layer == 1 ? 72 : 144) * bitrate / sampleRate + padding; - } - } - - String mimeType = MIME_TYPE_BY_LAYER[3 - layer]; - int channels = ((headerData >> 6) & 3) == 3 ? 1 : 2; - header.setValues(version, mimeType, frameSize, sampleRate, channels, bitrate, samplesPerFrame); - return true; - } - - /** MPEG audio header version. */ - public int version; - /** The mime type. */ - @Nullable public String mimeType; - /** Size of the frame associated with this header, in bytes. */ - public int frameSize; - /** Sample rate in samples per second. */ - public int sampleRate; - /** Number of audio channels in the frame. */ - public int channels; - /** Bitrate of the frame in bit/s. */ - public int bitrate; - /** Number of samples stored in the frame. */ - public int samplesPerFrame; - - private void setValues( - int version, - String mimeType, - int frameSize, - int sampleRate, - int channels, - int bitrate, - int samplesPerFrame) { - this.version = version; - this.mimeType = mimeType; - this.frameSize = frameSize; - this.sampleRate = sampleRate; - this.channels = channels; - this.bitrate = bitrate; - this.samplesPerFrame = samplesPerFrame; - } -} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/flac/FlacExtractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/flac/FlacExtractor.java deleted file mode 100644 index 0f67153e61..0000000000 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/flac/FlacExtractor.java +++ /dev/null @@ -1,332 +0,0 @@ -/* - * Copyright (C) 2019 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.flac; - -import static com.google.android.exoplayer2.util.Util.castNonNull; - -import androidx.annotation.IntDef; -import androidx.annotation.Nullable; -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.ExtractorOutput; -import com.google.android.exoplayer2.extractor.ExtractorsFactory; -import com.google.android.exoplayer2.extractor.FlacFrameReader; -import com.google.android.exoplayer2.extractor.FlacFrameReader.BlockSizeHolder; -import com.google.android.exoplayer2.extractor.FlacMetadataReader; -import com.google.android.exoplayer2.extractor.FlacMetadataReader.FirstFrameMetadata; -import com.google.android.exoplayer2.extractor.PositionHolder; -import com.google.android.exoplayer2.extractor.SeekMap; -import com.google.android.exoplayer2.extractor.TrackOutput; -import com.google.android.exoplayer2.metadata.Metadata; -import com.google.android.exoplayer2.util.Assertions; -import com.google.android.exoplayer2.util.FlacConstants; -import com.google.android.exoplayer2.util.FlacStreamMetadata; -import com.google.android.exoplayer2.util.ParsableByteArray; -import java.io.IOException; -import java.lang.annotation.Documented; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import org.checkerframework.checker.nullness.qual.MonotonicNonNull; - -// TODO: implement seeking. -// TODO: support live streams. -/** - * Extracts data from FLAC container format. - * - *

      The format specification can be found at https://xiph.org/flac/format.html. - */ -public final class FlacExtractor implements Extractor { - - /** Factory for {@link FlacExtractor} instances. */ - public static final ExtractorsFactory FACTORY = () -> new Extractor[] {new FlacExtractor()}; - - /** - * Flags controlling the behavior of the extractor. Possible flag value is {@link - * #FLAG_DISABLE_ID3_METADATA}. - */ - @Documented - @Retention(RetentionPolicy.SOURCE) - @IntDef( - flag = true, - value = {FLAG_DISABLE_ID3_METADATA}) - public @interface Flags {} - - /** - * Flag to disable parsing of ID3 metadata. Can be set to save memory if ID3 metadata is not - * required. - */ - public static final int FLAG_DISABLE_ID3_METADATA = 1; - - /** Parser state. */ - @Documented - @Retention(RetentionPolicy.SOURCE) - @IntDef({ - STATE_READ_ID3_METADATA, - STATE_GET_STREAM_MARKER_AND_INFO_BLOCK_BYTES, - STATE_READ_STREAM_MARKER, - STATE_READ_METADATA_BLOCKS, - STATE_GET_FIRST_FRAME_METADATA, - STATE_READ_FRAMES - }) - private @interface State {} - - private static final int STATE_READ_ID3_METADATA = 0; - private static final int STATE_GET_STREAM_MARKER_AND_INFO_BLOCK_BYTES = 1; - private static final int STATE_READ_STREAM_MARKER = 2; - private static final int STATE_READ_METADATA_BLOCKS = 3; - private static final int STATE_GET_FIRST_FRAME_METADATA = 4; - private static final int STATE_READ_FRAMES = 5; - - /** Arbitrary scratch length of 32KB, which is ~170ms of 16-bit stereo PCM audio at 48KHz. */ - private static final int SCRATCH_LENGTH = 32 * 1024; - - /** Value of an unknown block size. */ - private static final int BLOCK_SIZE_UNKNOWN = -1; - - private final byte[] streamMarkerAndInfoBlock; - private final ParsableByteArray scratch; - private final boolean id3MetadataDisabled; - - private final BlockSizeHolder blockSizeHolder; - - @MonotonicNonNull private ExtractorOutput extractorOutput; - @MonotonicNonNull private TrackOutput trackOutput; - - private @State int state; - @Nullable private Metadata id3Metadata; - @MonotonicNonNull private FlacStreamMetadata flacStreamMetadata; - private int minFrameSize; - private int frameStartMarker; - private int currentFrameBlockSizeSamples; - private int currentFrameBytesWritten; - private long totalSamplesWritten; - - /** Constructs an instance with {@code flags = 0}. */ - public FlacExtractor() { - this(/* flags= */ 0); - } - - /** - * Constructs an instance. - * - * @param flags Flags that control the extractor's behavior. Possible flags are described by - * {@link Flags}. - */ - public FlacExtractor(int flags) { - streamMarkerAndInfoBlock = - new byte[FlacConstants.STREAM_MARKER_SIZE + FlacConstants.STREAM_INFO_BLOCK_SIZE]; - scratch = new ParsableByteArray(SCRATCH_LENGTH); - blockSizeHolder = new BlockSizeHolder(); - id3MetadataDisabled = (flags & FLAG_DISABLE_ID3_METADATA) != 0; - } - - @Override - public boolean sniff(ExtractorInput input) throws IOException, InterruptedException { - FlacMetadataReader.peekId3Metadata(input, /* parseData= */ false); - return FlacMetadataReader.checkAndPeekStreamMarker(input); - } - - @Override - public void init(ExtractorOutput output) { - extractorOutput = output; - trackOutput = output.track(/* id= */ 0, C.TRACK_TYPE_AUDIO); - output.endTracks(); - } - - @Override - public int read(ExtractorInput input, PositionHolder seekPosition) - throws IOException, InterruptedException { - switch (state) { - case STATE_READ_ID3_METADATA: - readId3Metadata(input); - return Extractor.RESULT_CONTINUE; - case STATE_GET_STREAM_MARKER_AND_INFO_BLOCK_BYTES: - getStreamMarkerAndInfoBlockBytes(input); - return Extractor.RESULT_CONTINUE; - case STATE_READ_STREAM_MARKER: - readStreamMarker(input); - return Extractor.RESULT_CONTINUE; - case STATE_READ_METADATA_BLOCKS: - readMetadataBlocks(input); - return Extractor.RESULT_CONTINUE; - case STATE_GET_FIRST_FRAME_METADATA: - getFirstFrameMetadata(input); - return Extractor.RESULT_CONTINUE; - case STATE_READ_FRAMES: - return readFrames(input); - default: - throw new IllegalStateException(); - } - } - - @Override - public void seek(long position, long timeUs) { - state = STATE_READ_ID3_METADATA; - currentFrameBytesWritten = 0; - totalSamplesWritten = 0; - scratch.reset(); - } - - @Override - public void release() { - // Do nothing. - } - - // Private methods. - - private void readId3Metadata(ExtractorInput input) throws IOException, InterruptedException { - id3Metadata = FlacMetadataReader.readId3Metadata(input, /* parseData= */ !id3MetadataDisabled); - state = STATE_GET_STREAM_MARKER_AND_INFO_BLOCK_BYTES; - } - - private void getStreamMarkerAndInfoBlockBytes(ExtractorInput input) - throws IOException, InterruptedException { - input.peekFully(streamMarkerAndInfoBlock, 0, streamMarkerAndInfoBlock.length); - input.resetPeekPosition(); - state = STATE_READ_STREAM_MARKER; - } - - private void readStreamMarker(ExtractorInput input) throws IOException, InterruptedException { - FlacMetadataReader.readStreamMarker(input); - state = STATE_READ_METADATA_BLOCKS; - } - - private void readMetadataBlocks(ExtractorInput input) throws IOException, InterruptedException { - boolean isLastMetadataBlock = false; - FlacMetadataReader.FlacStreamMetadataHolder metadataHolder = - new FlacMetadataReader.FlacStreamMetadataHolder(flacStreamMetadata); - while (!isLastMetadataBlock) { - isLastMetadataBlock = FlacMetadataReader.readMetadataBlock(input, metadataHolder); - // Save the current metadata in case an exception occurs. - flacStreamMetadata = castNonNull(metadataHolder.flacStreamMetadata); - } - - Assertions.checkNotNull(flacStreamMetadata); - minFrameSize = Math.max(flacStreamMetadata.minFrameSize, FlacConstants.MIN_FRAME_HEADER_SIZE); - castNonNull(trackOutput) - .format(flacStreamMetadata.getFormat(streamMarkerAndInfoBlock, id3Metadata)); - castNonNull(extractorOutput) - .seekMap(new SeekMap.Unseekable(flacStreamMetadata.getDurationUs())); - - state = STATE_GET_FIRST_FRAME_METADATA; - } - - private void getFirstFrameMetadata(ExtractorInput input) - throws IOException, InterruptedException { - FirstFrameMetadata firstFrameMetadata = FlacMetadataReader.getFirstFrameMetadata(input); - frameStartMarker = firstFrameMetadata.frameStartMarker; - currentFrameBlockSizeSamples = firstFrameMetadata.blockSizeSamples; - - state = STATE_READ_FRAMES; - } - - private int readFrames(ExtractorInput input) throws IOException, InterruptedException { - Assertions.checkNotNull(trackOutput); - Assertions.checkNotNull(flacStreamMetadata); - - // Copy more bytes into the scratch. - int currentLimit = scratch.limit(); - int bytesRead = - input.read( - scratch.data, /* offset= */ currentLimit, /* length= */ SCRATCH_LENGTH - currentLimit); - boolean foundEndOfInput = bytesRead == C.RESULT_END_OF_INPUT; - if (!foundEndOfInput) { - scratch.setLimit(currentLimit + bytesRead); - } else if (scratch.bytesLeft() == 0) { - return C.RESULT_END_OF_INPUT; - } - - // Search for a frame. - int positionBeforeFindingAFrame = scratch.getPosition(); - - // Skip frame search on the bytes within the minimum frame size. - if (currentFrameBytesWritten < minFrameSize) { - scratch.skipBytes(Math.min(minFrameSize, scratch.bytesLeft())); - } - - int nextFrameBlockSizeSamples = findFrame(scratch, foundEndOfInput); - int numberOfFrameBytes = scratch.getPosition() - positionBeforeFindingAFrame; - scratch.setPosition(positionBeforeFindingAFrame); - trackOutput.sampleData(scratch, numberOfFrameBytes); - currentFrameBytesWritten += numberOfFrameBytes; - - // Frame found. - if (nextFrameBlockSizeSamples != BLOCK_SIZE_UNKNOWN || foundEndOfInput) { - long timeUs = getTimeUs(totalSamplesWritten, flacStreamMetadata.sampleRate); - trackOutput.sampleMetadata( - timeUs, - C.BUFFER_FLAG_KEY_FRAME, - currentFrameBytesWritten, - /* offset= */ 0, - /* encryptionData= */ null); - totalSamplesWritten += currentFrameBlockSizeSamples; - currentFrameBytesWritten = 0; - currentFrameBlockSizeSamples = nextFrameBlockSizeSamples; - } - - if (scratch.bytesLeft() < FlacConstants.MAX_FRAME_HEADER_SIZE) { - // The next frame header may not fit in the rest of the scratch, so put the trailing bytes at - // the start of the scratch, and reset the position and limit. - System.arraycopy( - scratch.data, scratch.getPosition(), scratch.data, /* destPos= */ 0, scratch.bytesLeft()); - scratch.reset(scratch.bytesLeft()); - } - - return Extractor.RESULT_CONTINUE; - } - - /** - * Searches for the start of a frame in {@code scratch}. - * - *

        - *
      • If the search is successful, the position is set to the start of the found frame. - *
      • Otherwise, the position is set to the first unsearched byte. - *
      - * - * @param scratch The array to be searched. - * @param foundEndOfInput If the end of input was met when filling in the {@code scratch}. - * @return The block size of the frame found, or {@code BLOCK_SIZE_UNKNOWN} if the search was not - * successful. - */ - private int findFrame(ParsableByteArray scratch, boolean foundEndOfInput) { - Assertions.checkNotNull(flacStreamMetadata); - - int frameOffset = scratch.getPosition(); - while (frameOffset <= scratch.limit() - FlacConstants.MAX_FRAME_HEADER_SIZE) { - scratch.setPosition(frameOffset); - if (FlacFrameReader.checkAndReadFrameHeader( - scratch, flacStreamMetadata, frameStartMarker, blockSizeHolder)) { - scratch.setPosition(frameOffset); - return blockSizeHolder.blockSizeSamples; - } - frameOffset++; - } - - if (foundEndOfInput) { - // Reached the end of the file. Assume it's the end of the frame. - scratch.setPosition(scratch.limit()); - } else { - scratch.setPosition(frameOffset); - } - - return BLOCK_SIZE_UNKNOWN; - } - - private long getTimeUs(long numSamples, int sampleRate) { - return numSamples * C.MICROS_PER_SECOND / sampleRate; - } -} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/FlacReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/FlacReader.java deleted file mode 100644 index 152d803da7..0000000000 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/FlacReader.java +++ /dev/null @@ -1,208 +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.extractor.ogg; - -import com.google.android.exoplayer2.extractor.ExtractorInput; -import com.google.android.exoplayer2.extractor.SeekMap; -import com.google.android.exoplayer2.extractor.SeekPoint; -import com.google.android.exoplayer2.util.FlacStreamMetadata; -import com.google.android.exoplayer2.util.ParsableByteArray; -import com.google.android.exoplayer2.util.Util; -import java.io.IOException; -import java.util.Arrays; - -/** - * {@link StreamReader} to extract Flac data out of Ogg byte stream. - */ -/* package */ final class FlacReader extends StreamReader { - - private static final byte AUDIO_PACKET_TYPE = (byte) 0xFF; - private static final byte SEEKTABLE_PACKET_TYPE = 0x03; - - private static final int FRAME_HEADER_SAMPLE_NUMBER_OFFSET = 4; - - private FlacStreamMetadata streamMetadata; - private FlacOggSeeker flacOggSeeker; - - public static boolean verifyBitstreamType(ParsableByteArray data) { - return data.bytesLeft() >= 5 && data.readUnsignedByte() == 0x7F && // packet type - data.readUnsignedInt() == 0x464C4143; // ASCII signature "FLAC" - } - - @Override - protected void reset(boolean headerData) { - super.reset(headerData); - if (headerData) { - streamMetadata = null; - flacOggSeeker = null; - } - } - - private static boolean isAudioPacket(byte[] data) { - return data[0] == AUDIO_PACKET_TYPE; - } - - @Override - protected long preparePayload(ParsableByteArray packet) { - if (!isAudioPacket(packet.data)) { - return -1; - } - return getFlacFrameBlockSize(packet); - } - - @Override - protected boolean readHeaders(ParsableByteArray packet, long position, SetupData setupData) { - byte[] data = packet.data; - if (streamMetadata == null) { - streamMetadata = new FlacStreamMetadata(data, 17); - byte[] metadata = Arrays.copyOfRange(data, 9, packet.limit()); - setupData.format = streamMetadata.getFormat(metadata, /* id3Metadata= */ null); - } else if ((data[0] & 0x7F) == SEEKTABLE_PACKET_TYPE) { - flacOggSeeker = new FlacOggSeeker(); - flacOggSeeker.parseSeekTable(packet); - } else if (isAudioPacket(data)) { - if (flacOggSeeker != null) { - flacOggSeeker.setFirstFrameOffset(position); - setupData.oggSeeker = flacOggSeeker; - } - return false; - } - return true; - } - - private int getFlacFrameBlockSize(ParsableByteArray packet) { - int blockSizeCode = (packet.data[2] & 0xFF) >> 4; - switch (blockSizeCode) { - case 1: - return 192; - case 2: - case 3: - case 4: - case 5: - return 576 << (blockSizeCode - 2); - case 6: - case 7: - // skip the sample number - packet.skipBytes(FRAME_HEADER_SAMPLE_NUMBER_OFFSET); - packet.readUtf8EncodedLong(); - int value = blockSizeCode == 6 ? packet.readUnsignedByte() : packet.readUnsignedShort(); - packet.setPosition(0); - return value + 1; - case 8: - case 9: - case 10: - case 11: - case 12: - case 13: - case 14: - case 15: - return 256 << (blockSizeCode - 8); - default: - return -1; - } - } - - private class FlacOggSeeker implements OggSeeker, SeekMap { - - private static final int METADATA_LENGTH_OFFSET = 1; - private static final int SEEK_POINT_SIZE = 18; - - private long[] seekPointGranules; - private long[] seekPointOffsets; - private long firstFrameOffset; - private long pendingSeekGranule; - - public FlacOggSeeker() { - firstFrameOffset = -1; - pendingSeekGranule = -1; - } - - public void setFirstFrameOffset(long firstFrameOffset) { - this.firstFrameOffset = firstFrameOffset; - } - - /** - * Parses a FLAC file seek table metadata structure and initializes internal fields. - * - * @param data A {@link ParsableByteArray} including whole seek table metadata block. Its - * position should be set to the beginning of the block. - * @see FLAC format - * METADATA_BLOCK_SEEKTABLE - */ - public void parseSeekTable(ParsableByteArray data) { - data.skipBytes(METADATA_LENGTH_OFFSET); - int length = data.readUnsignedInt24(); - int numberOfSeekPoints = length / SEEK_POINT_SIZE; - seekPointGranules = new long[numberOfSeekPoints]; - seekPointOffsets = new long[numberOfSeekPoints]; - for (int i = 0; i < numberOfSeekPoints; i++) { - seekPointGranules[i] = data.readLong(); - seekPointOffsets[i] = data.readLong(); - data.skipBytes(2); // Skip "Number of samples in the target frame." - } - } - - @Override - public long read(ExtractorInput input) throws IOException, InterruptedException { - if (pendingSeekGranule >= 0) { - long result = -(pendingSeekGranule + 2); - pendingSeekGranule = -1; - return result; - } - return -1; - } - - @Override - public void startSeek(long targetGranule) { - int index = Util.binarySearchFloor(seekPointGranules, targetGranule, true, true); - pendingSeekGranule = seekPointGranules[index]; - } - - @Override - public SeekMap createSeekMap() { - return this; - } - - @Override - public boolean isSeekable() { - return true; - } - - @Override - public SeekPoints getSeekPoints(long timeUs) { - long granule = convertTimeToGranule(timeUs); - int index = Util.binarySearchFloor(seekPointGranules, granule, true, true); - long seekTimeUs = convertGranuleToTime(seekPointGranules[index]); - long seekPosition = firstFrameOffset + seekPointOffsets[index]; - SeekPoint seekPoint = new SeekPoint(seekTimeUs, seekPosition); - if (seekTimeUs >= timeUs || index == seekPointGranules.length - 1) { - return new SeekPoints(seekPoint); - } else { - long secondSeekTimeUs = convertGranuleToTime(seekPointGranules[index + 1]); - long secondSeekPosition = firstFrameOffset + seekPointOffsets[index + 1]; - SeekPoint secondSeekPoint = new SeekPoint(secondSeekTimeUs, secondSeekPosition); - return new SeekPoints(seekPoint, secondSeekPoint); - } - } - - @Override - public long getDurationUs() { - return streamMetadata.getDurationUs(); - } - - } - -} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/SpliceInfoSectionReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/SpliceInfoSectionReader.java deleted file mode 100644 index 27838d4c25..0000000000 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/SpliceInfoSectionReader.java +++ /dev/null @@ -1,62 +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.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.util.MimeTypes; -import com.google.android.exoplayer2.util.ParsableByteArray; -import com.google.android.exoplayer2.util.TimestampAdjuster; - -/** - * Parses splice info sections as defined by SCTE35. - */ -public final class SpliceInfoSectionReader implements SectionPayloadReader { - - private TimestampAdjuster timestampAdjuster; - private TrackOutput output; - private boolean formatDeclared; - - @Override - public void init(TimestampAdjuster timestampAdjuster, ExtractorOutput extractorOutput, - TsPayloadReader.TrackIdGenerator idGenerator) { - this.timestampAdjuster = timestampAdjuster; - idGenerator.generateNewId(); - output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_METADATA); - output.format(Format.createSampleFormat(idGenerator.getFormatId(), MimeTypes.APPLICATION_SCTE35, - null, Format.NO_VALUE, null)); - } - - @Override - public void consume(ParsableByteArray sectionData) { - if (!formatDeclared) { - if (timestampAdjuster.getTimestampOffsetUs() == C.TIME_UNSET) { - // There is not enough information to initialize the timestamp adjuster. - return; - } - output.format(Format.createSampleFormat(null, MimeTypes.APPLICATION_SCTE35, - timestampAdjuster.getTimestampOffsetUs())); - formatDeclared = true; - } - int sampleSize = sectionData.bytesLeft(); - output.sampleData(sectionData, sampleSize); - output.sampleMetadata(timestampAdjuster.getLastAdjustedTimestampUs(), C.BUFFER_FLAG_KEY_FRAME, - sampleSize, 0, null); - } - -} 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 deleted file mode 100644 index 91097c9e5b..0000000000 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavExtractor.java +++ /dev/null @@ -1,120 +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.extractor.wav; - -import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.Format; -import com.google.android.exoplayer2.ParserException; -import com.google.android.exoplayer2.extractor.Extractor; -import com.google.android.exoplayer2.extractor.ExtractorInput; -import com.google.android.exoplayer2.extractor.ExtractorOutput; -import com.google.android.exoplayer2.extractor.ExtractorsFactory; -import com.google.android.exoplayer2.extractor.PositionHolder; -import com.google.android.exoplayer2.extractor.TrackOutput; -import com.google.android.exoplayer2.util.Assertions; -import com.google.android.exoplayer2.util.MimeTypes; -import java.io.IOException; - -/** - * Extracts data from WAV byte streams. - */ -public final class WavExtractor implements Extractor { - - /** 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; - - private ExtractorOutput extractorOutput; - private TrackOutput trackOutput; - private WavHeader wavHeader; - private int bytesPerFrame; - private int pendingBytes; - - @Override - public boolean sniff(ExtractorInput input) throws IOException, InterruptedException { - return WavHeaderReader.peek(input) != null; - } - - @Override - public void init(ExtractorOutput output) { - extractorOutput = output; - trackOutput = output.track(0, C.TRACK_TYPE_AUDIO); - wavHeader = null; - output.endTracks(); - } - - @Override - public void seek(long position, long timeUs) { - pendingBytes = 0; - } - - @Override - public void release() { - // Do nothing - } - - @Override - public int read(ExtractorInput input, PositionHolder seekPosition) - throws IOException, InterruptedException { - if (wavHeader == null) { - wavHeader = WavHeaderReader.peek(input); - if (wavHeader == null) { - // Should only happen if the media wasn't sniffed. - throw new ParserException("Unsupported or unrecognized wav header."); - } - Format format = Format.createAudioSampleFormat(null, MimeTypes.AUDIO_RAW, null, - wavHeader.getBitrate(), MAX_INPUT_SIZE, wavHeader.getNumChannels(), - wavHeader.getSampleRateHz(), wavHeader.getEncoding(), null, null, 0, null); - trackOutput.format(format); - bytesPerFrame = wavHeader.getBytesPerFrame(); - } - - if (!wavHeader.hasDataBounds()) { - WavHeaderReader.skipToData(input, wavHeader); - extractorOutput.seekMap(wavHeader); - } else if (input.getPosition() == 0) { - input.skipFully(wavHeader.getDataStartPosition()); - } - - long dataEndPosition = wavHeader.getDataEndPosition(); - Assertions.checkState(dataEndPosition != C.POSITION_UNSET); - - long bytesLeft = dataEndPosition - input.getPosition(); - if (bytesLeft <= 0) { - return Extractor.RESULT_END_OF_INPUT; - } - - int maxBytesToRead = (int) Math.min(MAX_INPUT_SIZE - pendingBytes, bytesLeft); - int bytesAppended = trackOutput.sampleData(input, maxBytesToRead, true); - if (bytesAppended != RESULT_END_OF_INPUT) { - pendingBytes += bytesAppended; - } - - // Samples must consist of a whole number of frames. - int pendingFrames = pendingBytes / bytesPerFrame; - if (pendingFrames > 0) { - long timeUs = wavHeader.getTimeUs(input.getPosition() - pendingBytes); - int size = pendingFrames * bytesPerFrame; - pendingBytes -= size; - trackOutput.sampleMetadata(timeUs, C.BUFFER_FLAG_KEY_FRAME, size, pendingBytes, null); - } - - return bytesAppended == RESULT_END_OF_INPUT ? RESULT_END_OF_INPUT : RESULT_CONTINUE; - } - -} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavHeader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavHeader.java deleted file mode 100644 index 228151339a..0000000000 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavHeader.java +++ /dev/null @@ -1,165 +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.extractor.wav; - -import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.extractor.SeekMap; -import com.google.android.exoplayer2.extractor.SeekPoint; -import com.google.android.exoplayer2.util.Util; - -/** Header for a WAV file. */ -/* package */ final class WavHeader implements SeekMap { - - /** Number of audio channels. */ - private final int numChannels; - /** Sample rate in Hertz. */ - private final int sampleRateHz; - /** Average bytes per second for the sample data. */ - private final int averageBytesPerSecond; - /** Alignment for frames of audio data; should equal {@code numChannels * bitsPerSample / 8}. */ - private final int blockAlignment; - /** Bits per sample for the audio data. */ - private final int bitsPerSample; - /** The PCM encoding. */ - @C.PcmEncoding private final int encoding; - - /** Position of the start of the sample data, in bytes. */ - private int dataStartPosition; - /** Position of the end of the sample data (exclusive), in bytes. */ - private long dataEndPosition; - - public WavHeader( - int numChannels, - int sampleRateHz, - int averageBytesPerSecond, - int blockAlignment, - int bitsPerSample, - @C.PcmEncoding int encoding) { - this.numChannels = numChannels; - this.sampleRateHz = sampleRateHz; - this.averageBytesPerSecond = averageBytesPerSecond; - this.blockAlignment = blockAlignment; - this.bitsPerSample = bitsPerSample; - this.encoding = encoding; - dataStartPosition = C.POSITION_UNSET; - dataEndPosition = C.POSITION_UNSET; - } - - // Data bounds. - - /** - * Sets the data start position and size in bytes of sample data in this WAV. - * - * @param dataStartPosition The position of the start of the sample data, in bytes. - * @param dataEndPosition The position of the end of the sample data (exclusive), in bytes. - */ - public void setDataBounds(int dataStartPosition, long dataEndPosition) { - this.dataStartPosition = dataStartPosition; - this.dataEndPosition = dataEndPosition; - } - - /** - * Returns the position of the start of the sample data, in bytes, or {@link C#POSITION_UNSET} if - * the data bounds have not been set. - */ - public int getDataStartPosition() { - return dataStartPosition; - } - - /** - * Returns the position of the end of the sample data (exclusive), in bytes, or {@link - * C#POSITION_UNSET} if the data bounds have not been set. - */ - public long getDataEndPosition() { - return dataEndPosition; - } - - /** Returns whether the data start position and size have been set. */ - public boolean hasDataBounds() { - return dataStartPosition != C.POSITION_UNSET; - } - - // SeekMap implementation. - - @Override - public boolean isSeekable() { - return true; - } - - @Override - public long getDurationUs() { - long numFrames = (dataEndPosition - dataStartPosition) / blockAlignment; - return (numFrames * C.MICROS_PER_SECOND) / sampleRateHz; - } - - @Override - public SeekPoints getSeekPoints(long timeUs) { - long dataSize = dataEndPosition - dataStartPosition; - long positionOffset = (timeUs * averageBytesPerSecond) / C.MICROS_PER_SECOND; - // Constrain to nearest preceding frame offset. - positionOffset = (positionOffset / blockAlignment) * blockAlignment; - positionOffset = Util.constrainValue(positionOffset, 0, dataSize - blockAlignment); - long seekPosition = dataStartPosition + positionOffset; - long seekTimeUs = getTimeUs(seekPosition); - SeekPoint seekPoint = new SeekPoint(seekTimeUs, seekPosition); - if (seekTimeUs >= timeUs || positionOffset == dataSize - blockAlignment) { - return new SeekPoints(seekPoint); - } else { - long secondSeekPosition = seekPosition + blockAlignment; - long secondSeekTimeUs = getTimeUs(secondSeekPosition); - SeekPoint secondSeekPoint = new SeekPoint(secondSeekTimeUs, secondSeekPosition); - return new SeekPoints(seekPoint, secondSeekPoint); - } - } - - // Misc getters. - - /** - * Returns the time in microseconds for the given position in bytes. - * - * @param position The position in bytes. - */ - public long getTimeUs(long position) { - long positionOffset = Math.max(0, position - dataStartPosition); - return (positionOffset * C.MICROS_PER_SECOND) / averageBytesPerSecond; - } - - /** Returns the bytes per frame of this WAV. */ - public int getBytesPerFrame() { - return blockAlignment; - } - - /** Returns the bitrate of this WAV. */ - public int getBitrate() { - return sampleRateHz * bitsPerSample * numChannels; - } - - /** Returns the sample rate in Hertz of this WAV. */ - public int getSampleRateHz() { - return sampleRateHz; - } - - /** Returns the number of audio channels in this WAV. */ - public int getNumChannels() { - return numChannels; - } - - /** Returns the PCM encoding. **/ - public @C.PcmEncoding int getEncoding() { - return encoding; - } - -} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/AsynchronousMediaCodecAdapter.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/AsynchronousMediaCodecAdapter.java new file mode 100644 index 0000000000..040ef340ed --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/AsynchronousMediaCodecAdapter.java @@ -0,0 +1,159 @@ +/* + * Copyright (C) 2019 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.mediacodec; + +import android.media.MediaCodec; +import android.media.MediaFormat; +import android.os.Handler; +import android.os.Looper; +import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; +import androidx.annotation.VisibleForTesting; +import com.google.android.exoplayer2.decoder.CryptoInfo; +import com.google.android.exoplayer2.util.Assertions; + +/** + * A {@link MediaCodecAdapter} that operates the {@link MediaCodec} in asynchronous mode. + * + *

      The AsynchronousMediaCodecAdapter routes callbacks to the current thread's {@link Looper} + * obtained via {@link Looper#myLooper()} + */ +@RequiresApi(21) +/* package */ final class AsynchronousMediaCodecAdapter implements MediaCodecAdapter { + private final MediaCodecAsyncCallback mediaCodecAsyncCallback; + private final Handler handler; + private final MediaCodec codec; + @Nullable private IllegalStateException internalException; + private boolean flushing; + private Runnable codecStartRunnable; + + /** + * Create a new {@code AsynchronousMediaCodecAdapter}. + * + * @param codec The {@link MediaCodec} to wrap. + */ + public AsynchronousMediaCodecAdapter(MediaCodec codec) { + this(codec, Assertions.checkNotNull(Looper.myLooper())); + } + + @VisibleForTesting + /* package */ AsynchronousMediaCodecAdapter(MediaCodec codec, Looper looper) { + mediaCodecAsyncCallback = new MediaCodecAsyncCallback(); + handler = new Handler(looper); + this.codec = codec; + this.codec.setCallback(mediaCodecAsyncCallback); + codecStartRunnable = codec::start; + } + + @Override + public void start() { + codecStartRunnable.run(); + } + + @Override + public void queueInputBuffer( + int index, int offset, int size, long presentationTimeUs, int flags) { + codec.queueInputBuffer(index, offset, size, presentationTimeUs, flags); + } + + @Override + public void queueSecureInputBuffer( + int index, int offset, CryptoInfo info, long presentationTimeUs, int flags) { + codec.queueSecureInputBuffer( + index, offset, info.getFrameworkCryptoInfo(), presentationTimeUs, flags); + } + + @Override + public int dequeueInputBufferIndex() { + if (flushing) { + return MediaCodec.INFO_TRY_AGAIN_LATER; + } else { + maybeThrowException(); + return mediaCodecAsyncCallback.dequeueInputBufferIndex(); + } + } + + @Override + public int dequeueOutputBufferIndex(MediaCodec.BufferInfo bufferInfo) { + if (flushing) { + return MediaCodec.INFO_TRY_AGAIN_LATER; + } else { + maybeThrowException(); + return mediaCodecAsyncCallback.dequeueOutputBufferIndex(bufferInfo); + } + } + + @Override + public MediaFormat getOutputFormat() { + return mediaCodecAsyncCallback.getOutputFormat(); + } + + @Override + public void flush() { + clearPendingFlushState(); + flushing = true; + codec.flush(); + handler.post(this::onCompleteFlush); + } + + @Override + public void shutdown() { + clearPendingFlushState(); + } + + @VisibleForTesting + /* package */ MediaCodec.Callback getMediaCodecCallback() { + return mediaCodecAsyncCallback; + } + + private void onCompleteFlush() { + flushing = false; + mediaCodecAsyncCallback.flush(); + try { + codecStartRunnable.run(); + } catch (IllegalStateException e) { + // Catch IllegalStateException directly so that we don't have to wrap it. + internalException = e; + } catch (Exception e) { + internalException = new IllegalStateException(e); + } + } + + @VisibleForTesting + /* package */ void setCodecStartRunnable(Runnable codecStartRunnable) { + this.codecStartRunnable = codecStartRunnable; + } + + private void maybeThrowException() throws IllegalStateException { + maybeThrowInternalException(); + mediaCodecAsyncCallback.maybeThrowMediaCodecException(); + } + + private void maybeThrowInternalException() { + if (internalException != null) { + IllegalStateException e = internalException; + internalException = null; + throw e; + } + } + + /** Clear state related to pending flush events. */ + private void clearPendingFlushState() { + handler.removeCallbacksAndMessages(null); + internalException = null; + } +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/AsynchronousMediaCodecBufferEnqueuer.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/AsynchronousMediaCodecBufferEnqueuer.java new file mode 100644 index 0000000000..428d64d0b1 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/AsynchronousMediaCodecBufferEnqueuer.java @@ -0,0 +1,354 @@ +/* + * Copyright (C) 2020 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.mediacodec; + +import android.media.MediaCodec; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Message; +import androidx.annotation.GuardedBy; +import androidx.annotation.RequiresApi; +import androidx.annotation.VisibleForTesting; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.decoder.CryptoInfo; +import com.google.android.exoplayer2.util.ConditionVariable; +import com.google.android.exoplayer2.util.Util; +import java.util.ArrayDeque; +import java.util.Arrays; +import java.util.concurrent.atomic.AtomicReference; +import org.checkerframework.checker.nullness.compatqual.NullableType; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; + +/** + * A {@link MediaCodecInputBufferEnqueuer} that defers queueing operations on a background thread. + * + *

      The implementation of this class assumes that its public methods will be called from the same + * thread. + */ +@RequiresApi(23) +class AsynchronousMediaCodecBufferEnqueuer implements MediaCodecInputBufferEnqueuer { + + private static final int MSG_QUEUE_INPUT_BUFFER = 0; + private static final int MSG_QUEUE_SECURE_INPUT_BUFFER = 1; + private static final int MSG_FLUSH = 2; + + @GuardedBy("MESSAGE_PARAMS_INSTANCE_POOL") + private static final ArrayDeque MESSAGE_PARAMS_INSTANCE_POOL = new ArrayDeque<>(); + + private static final Object QUEUE_SECURE_LOCK = new Object(); + + private final MediaCodec codec; + private final HandlerThread handlerThread; + private @MonotonicNonNull Handler handler; + private final AtomicReference<@NullableType RuntimeException> pendingRuntimeException; + private final ConditionVariable conditionVariable; + private final boolean needsSynchronizationWorkaround; + private boolean started; + + /** + * Creates a new instance that submits input buffers on the specified {@link MediaCodec}. + * + * @param codec The {@link MediaCodec} to submit input buffers to. + * @param trackType The type of stream (used for debug logs). + */ + public AsynchronousMediaCodecBufferEnqueuer(MediaCodec codec, int trackType) { + this( + codec, + new HandlerThread(createThreadLabel(trackType)), + /* conditionVariable= */ new ConditionVariable()); + } + + @VisibleForTesting + /* package */ AsynchronousMediaCodecBufferEnqueuer( + MediaCodec codec, HandlerThread handlerThread, ConditionVariable conditionVariable) { + this.codec = codec; + this.handlerThread = handlerThread; + this.conditionVariable = conditionVariable; + pendingRuntimeException = new AtomicReference<>(); + needsSynchronizationWorkaround = needsSynchronizationWorkaround(); + } + + @Override + public void start() { + if (!started) { + handlerThread.start(); + handler = + new Handler(handlerThread.getLooper()) { + @Override + public void handleMessage(Message msg) { + doHandleMessage(msg); + } + }; + started = true; + } + } + + @Override + public void queueInputBuffer( + int index, int offset, int size, long presentationTimeUs, int flags) { + maybeThrowException(); + MessageParams messageParams = getMessageParams(); + messageParams.setQueueParams(index, offset, size, presentationTimeUs, flags); + Message message = + Util.castNonNull(handler).obtainMessage(MSG_QUEUE_INPUT_BUFFER, messageParams); + message.sendToTarget(); + } + + @Override + public void queueSecureInputBuffer( + int index, int offset, CryptoInfo info, long presentationTimeUs, int flags) { + maybeThrowException(); + MessageParams messageParams = getMessageParams(); + messageParams.setQueueParams(index, offset, /* size= */ 0, presentationTimeUs, flags); + copy(info, messageParams.cryptoInfo); + Message message = + Util.castNonNull(handler).obtainMessage(MSG_QUEUE_SECURE_INPUT_BUFFER, messageParams); + message.sendToTarget(); + } + + @Override + public void flush() { + if (started) { + try { + flushHandlerThread(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + // The playback thread should not be interrupted. Raising this as an + // IllegalStateException. + throw new IllegalStateException(e); + } + } + } + + @Override + public void shutdown() { + if (started) { + flush(); + handlerThread.quit(); + } + started = false; + } + + private void doHandleMessage(Message msg) { + MessageParams params = null; + switch (msg.what) { + case MSG_QUEUE_INPUT_BUFFER: + params = (MessageParams) msg.obj; + doQueueInputBuffer( + params.index, params.offset, params.size, params.presentationTimeUs, params.flags); + break; + case MSG_QUEUE_SECURE_INPUT_BUFFER: + params = (MessageParams) msg.obj; + doQueueSecureInputBuffer( + params.index, + params.offset, + params.cryptoInfo, + params.presentationTimeUs, + params.flags); + break; + case MSG_FLUSH: + conditionVariable.open(); + break; + default: + setPendingRuntimeException(new IllegalStateException(String.valueOf(msg.what))); + } + if (params != null) { + recycleMessageParams(params); + } + } + + private void maybeThrowException() { + RuntimeException exception = pendingRuntimeException.getAndSet(null); + if (exception != null) { + throw exception; + } + } + + /** + * Empties all tasks enqueued on the {@link #handlerThread} via the {@link #handler}. This method + * blocks until the {@link #handlerThread} is idle. + */ + private void flushHandlerThread() throws InterruptedException { + Handler handler = Util.castNonNull(this.handler); + handler.removeCallbacksAndMessages(null); + conditionVariable.close(); + handler.obtainMessage(MSG_FLUSH).sendToTarget(); + conditionVariable.block(); + // Check if any exceptions happened during the last queueing action. + maybeThrowException(); + } + + // Called from the handler thread + + @VisibleForTesting + /* package */ void setPendingRuntimeException(RuntimeException exception) { + pendingRuntimeException.set(exception); + } + + private void doQueueInputBuffer( + int index, int offset, int size, long presentationTimeUs, int flag) { + try { + codec.queueInputBuffer(index, offset, size, presentationTimeUs, flag); + } catch (RuntimeException e) { + setPendingRuntimeException(e); + } + } + + private void doQueueSecureInputBuffer( + int index, int offset, MediaCodec.CryptoInfo info, long presentationTimeUs, int flags) { + try { + if (needsSynchronizationWorkaround) { + synchronized (QUEUE_SECURE_LOCK) { + codec.queueSecureInputBuffer(index, offset, info, presentationTimeUs, flags); + } + } else { + codec.queueSecureInputBuffer(index, offset, info, presentationTimeUs, flags); + } + } catch (RuntimeException e) { + setPendingRuntimeException(e); + } + } + + @VisibleForTesting + /* package */ static int getInstancePoolSize() { + synchronized (MESSAGE_PARAMS_INSTANCE_POOL) { + return MESSAGE_PARAMS_INSTANCE_POOL.size(); + } + } + + private static MessageParams getMessageParams() { + synchronized (MESSAGE_PARAMS_INSTANCE_POOL) { + if (MESSAGE_PARAMS_INSTANCE_POOL.isEmpty()) { + return new MessageParams(); + } else { + return MESSAGE_PARAMS_INSTANCE_POOL.removeFirst(); + } + } + } + + private static void recycleMessageParams(MessageParams params) { + synchronized (MESSAGE_PARAMS_INSTANCE_POOL) { + MESSAGE_PARAMS_INSTANCE_POOL.add(params); + } + } + + /** Parameters for queue input buffer and queue secure input buffer tasks. */ + private static class MessageParams { + public int index; + public int offset; + public int size; + public final MediaCodec.CryptoInfo cryptoInfo; + public long presentationTimeUs; + public int flags; + + MessageParams() { + cryptoInfo = new MediaCodec.CryptoInfo(); + } + + /** Convenience method for setting the queueing parameters. */ + public void setQueueParams( + int index, int offset, int size, long presentationTimeUs, int flags) { + this.index = index; + this.offset = offset; + this.size = size; + this.presentationTimeUs = presentationTimeUs; + this.flags = flags; + } + } + + /** + * Returns whether this device needs the synchronization workaround when queueing secure input + * buffers (see [Internal: b/149908061]). + */ + private static boolean needsSynchronizationWorkaround() { + String manufacturer = Util.toLowerInvariant(Util.MANUFACTURER); + return manufacturer.contains("samsung") || manufacturer.contains("motorola"); + } + + private static String createThreadLabel(int trackType) { + StringBuilder labelBuilder = new StringBuilder("ExoPlayer:MediaCodecBufferEnqueuer:"); + if (trackType == C.TRACK_TYPE_AUDIO) { + labelBuilder.append("Audio"); + } else if (trackType == C.TRACK_TYPE_VIDEO) { + labelBuilder.append("Video"); + } else { + labelBuilder.append("Unknown(").append(trackType).append(")"); + } + return labelBuilder.toString(); + } + + /** Performs a deep copy of {@code cryptoInfo} to {@code frameworkCryptoInfo}. */ + private static void copy( + CryptoInfo cryptoInfo, android.media.MediaCodec.CryptoInfo frameworkCryptoInfo) { + // Update frameworkCryptoInfo fields directly because CryptoInfo.set performs an unnecessary + // object allocation on Android N. + frameworkCryptoInfo.numSubSamples = cryptoInfo.numSubSamples; + frameworkCryptoInfo.numBytesOfClearData = + copy(cryptoInfo.numBytesOfClearData, frameworkCryptoInfo.numBytesOfClearData); + frameworkCryptoInfo.numBytesOfEncryptedData = + copy(cryptoInfo.numBytesOfEncryptedData, frameworkCryptoInfo.numBytesOfEncryptedData); + frameworkCryptoInfo.key = copy(cryptoInfo.key, frameworkCryptoInfo.key); + frameworkCryptoInfo.iv = copy(cryptoInfo.iv, frameworkCryptoInfo.iv); + frameworkCryptoInfo.mode = cryptoInfo.mode; + if (Util.SDK_INT >= 24) { + android.media.MediaCodec.CryptoInfo.Pattern pattern = + new android.media.MediaCodec.CryptoInfo.Pattern( + cryptoInfo.encryptedBlocks, cryptoInfo.clearBlocks); + frameworkCryptoInfo.setPattern(pattern); + } + } + + /** + * Copies {@code src}, reusing {@code dst} if it's at least as long as {@code src}. + * + * @param src The source array. + * @param dst The destination array, which will be reused if it's at least as long as {@code src}. + * @return The copy, which may be {@code dst} if it was reused. + */ + private static int[] copy(int[] src, int[] dst) { + if (src == null) { + return dst; + } + + if (dst == null || dst.length < src.length) { + return Arrays.copyOf(src, src.length); + } else { + System.arraycopy(src, 0, dst, 0, src.length); + return dst; + } + } + + /** + * Copies {@code src}, reusing {@code dst} if it's at least as long as {@code src}. + * + * @param src The source array. + * @param dst The destination array, which will be reused if it's at least as long as {@code src}. + * @return The copy, which may be {@code dst} if it was reused. + */ + private static byte[] copy(byte[] src, byte[] dst) { + if (src == null) { + return dst; + } + + if (dst == null || dst.length < src.length) { + return Arrays.copyOf(src, src.length); + } else { + System.arraycopy(src, 0, dst, 0, src.length); + return dst; + } + } +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/BatchBuffer.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/BatchBuffer.java new file mode 100644 index 0000000000..3c40fe02d4 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/BatchBuffer.java @@ -0,0 +1,186 @@ +/* + * Copyright (C) 2020 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.mediacodec; + +import androidx.annotation.IntRange; +import androidx.annotation.Nullable; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.decoder.DecoderInputBuffer; +import com.google.android.exoplayer2.util.Assertions; +import java.nio.ByteBuffer; + +/** Buffer that stores multiple encoded access units to allow batch processing. */ +/* package */ final class BatchBuffer extends DecoderInputBuffer { + /** Arbitrary limit to the number of access unit in a full batch buffer. */ + public static final int DEFAULT_BATCH_SIZE_ACCESS_UNITS = 32; + /** + * Arbitrary limit to the memory used by a full batch buffer to avoid using too much memory for + * very high bitrate. Equivalent of 75s of mp3 at highest bitrate (320kb/s) and 30s of AAC LC at + * highest bitrate (800kb/s). That limit is ignored for the first access unit to avoid stalling + * stream with huge access units. + */ + private static final int BATCH_SIZE_BYTES = 3 * 1000 * 1024; + + private final DecoderInputBuffer nextAccessUnitBuffer; + private boolean hasPendingAccessUnit; + + private long firstAccessUnitTimeUs; + private int accessUnitCount; + private int maxAccessUnitCount; + + public BatchBuffer() { + super(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_DIRECT); + nextAccessUnitBuffer = + new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_DIRECT); + clear(); + } + + /** Sets the maximum number of access units the buffer can contain before being full. */ + public void setMaxAccessUnitCount(@IntRange(from = 1) int maxAccessUnitCount) { + Assertions.checkArgument(maxAccessUnitCount > 0); + this.maxAccessUnitCount = maxAccessUnitCount; + } + + /** Gets the maximum number of access units the buffer can contain before being full. */ + public int getMaxAccessUnitCount() { + return maxAccessUnitCount; + } + + /** Resets the state of this object to what it was after construction. */ + @Override + public void clear() { + flush(); + maxAccessUnitCount = DEFAULT_BATCH_SIZE_ACCESS_UNITS; + } + + /** Clear all access units from the BatchBuffer to empty it. */ + public void flush() { + clearMainBuffer(); + nextAccessUnitBuffer.clear(); + hasPendingAccessUnit = false; + } + + /** Clears the state of the batch buffer to be ready to receive a new sequence of access units. */ + public void batchWasConsumed() { + clearMainBuffer(); + if (hasPendingAccessUnit) { + putAccessUnit(nextAccessUnitBuffer); + hasPendingAccessUnit = false; + } + } + + /** + * Gets the buffer to fill-out that will then be append to the batch buffer with {@link + * #commitNextAccessUnit()}. + */ + public DecoderInputBuffer getNextAccessUnitBuffer() { + return nextAccessUnitBuffer; + } + + /** Gets the timestamp of the first access unit in the buffer. */ + public long getFirstAccessUnitTimeUs() { + return firstAccessUnitTimeUs; + } + + /** Gets the timestamp of the last access unit in the buffer. */ + public long getLastAccessUnitTimeUs() { + return timeUs; + } + + /** Gets the number of access units contained in this batch buffer. */ + public int getAccessUnitCount() { + return accessUnitCount; + } + + /** If the buffer contains no access units. */ + public boolean isEmpty() { + return accessUnitCount == 0; + } + + /** If more access units should be added to the batch buffer. */ + public boolean isFull() { + return accessUnitCount >= maxAccessUnitCount + || (data != null && data.position() >= BATCH_SIZE_BYTES) + || hasPendingAccessUnit; + } + + /** + * Appends the staged access unit in this batch buffer. + * + * @throws IllegalStateException If calling this method on a full or end of stream batch buffer. + * @throws IllegalArgumentException If the {@code accessUnit} is encrypted or has + * supplementalData, as batching of those state has not been implemented. + */ + public void commitNextAccessUnit() { + DecoderInputBuffer accessUnit = nextAccessUnitBuffer; + Assertions.checkState(!isFull() && !isEndOfStream()); + Assertions.checkArgument(!accessUnit.isEncrypted() && !accessUnit.hasSupplementalData()); + if (!canBatch(accessUnit)) { + hasPendingAccessUnit = true; // Delay the putAccessUnit until the batch buffer is empty. + return; + } + putAccessUnit(accessUnit); + } + + private boolean canBatch(DecoderInputBuffer accessUnit) { + if (isEmpty()) { + return true; // Batching with an empty batch must always succeed or the stream will stall. + } + if (accessUnit.isDecodeOnly() != isDecodeOnly()) { + return false; // Decode only and non decode only access units can not be batched together. + } + + @Nullable ByteBuffer accessUnitData = accessUnit.data; + if (accessUnitData != null + && this.data != null + && this.data.position() + accessUnitData.limit() >= BATCH_SIZE_BYTES) { + return false; // The batch buffer does not have the capacity to add this access unit. + } + return true; + } + + private void putAccessUnit(DecoderInputBuffer accessUnit) { + @Nullable ByteBuffer accessUnitData = accessUnit.data; + if (accessUnitData != null) { + accessUnit.flip(); + ensureSpaceForWrite(accessUnitData.remaining()); + this.data.put(accessUnitData); + } + + if (accessUnit.isEndOfStream()) { + setFlags(C.BUFFER_FLAG_END_OF_STREAM); + } + if (accessUnit.isDecodeOnly()) { + setFlags(C.BUFFER_FLAG_DECODE_ONLY); + } + if (accessUnit.isKeyFrame()) { + setFlags(C.BUFFER_FLAG_KEY_FRAME); + } + accessUnitCount++; + timeUs = accessUnit.timeUs; + if (accessUnitCount == 1) { // First read of the buffer + firstAccessUnitTimeUs = timeUs; + } + accessUnit.clear(); + } + + private void clearMainBuffer() { + super.clear(); + accessUnitCount = 0; + firstAccessUnitTimeUs = C.TIME_UNSET; + timeUs = C.TIME_UNSET; + } +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/DedicatedThreadAsyncMediaCodecAdapter.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/DedicatedThreadAsyncMediaCodecAdapter.java new file mode 100644 index 0000000000..88e3f56daa --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/DedicatedThreadAsyncMediaCodecAdapter.java @@ -0,0 +1,270 @@ +/* + * Copyright (C) 2019 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.mediacodec; + +import android.media.MediaCodec; +import android.media.MediaFormat; +import android.os.Handler; +import android.os.HandlerThread; +import androidx.annotation.IntDef; +import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; +import androidx.annotation.VisibleForTesting; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.decoder.CryptoInfo; +import com.google.android.exoplayer2.util.Util; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; + +/** + * A {@link MediaCodecAdapter} that operates the underlying {@link MediaCodec} in asynchronous mode + * and routes {@link MediaCodec.Callback} callbacks on a dedicated thread that is managed + * internally. + * + *

      This adapter supports queueing input buffers asynchronously. + */ +@RequiresApi(23) +/* package */ final class DedicatedThreadAsyncMediaCodecAdapter extends MediaCodec.Callback + implements MediaCodecAdapter { + + @Documented + @Retention(RetentionPolicy.SOURCE) + @IntDef({STATE_CREATED, STATE_STARTED, STATE_SHUT_DOWN}) + private @interface State {} + + private static final int STATE_CREATED = 0; + private static final int STATE_STARTED = 1; + private static final int STATE_SHUT_DOWN = 2; + + private final MediaCodecAsyncCallback mediaCodecAsyncCallback; + private final MediaCodec codec; + private final HandlerThread handlerThread; + private @MonotonicNonNull Handler handler; + private long pendingFlushCount; + private @State int state; + private Runnable codecStartRunnable; + private final MediaCodecInputBufferEnqueuer bufferEnqueuer; + @Nullable private IllegalStateException internalException; + + /** + * Creates an instance that wraps the specified {@link MediaCodec}. Instances created with this + * constructor will queue input buffers to the {@link MediaCodec} synchronously. + * + * @param codec The {@link MediaCodec} to wrap. + * @param trackType One of {@link C#TRACK_TYPE_AUDIO} or {@link C#TRACK_TYPE_VIDEO}. Used for + * labelling the internal thread accordingly. + */ + /* package */ DedicatedThreadAsyncMediaCodecAdapter(MediaCodec codec, int trackType) { + this( + codec, + /* enableAsynchronousQueueing= */ false, + trackType, + new HandlerThread(createThreadLabel(trackType))); + } + + /** + * Creates an instance that wraps the specified {@link MediaCodec}. + * + * @param codec The {@link MediaCodec} to wrap. + * @param enableAsynchronousQueueing Whether input buffers will be queued asynchronously. + * @param trackType One of {@link C#TRACK_TYPE_AUDIO} or {@link C#TRACK_TYPE_VIDEO}. Used for + * labelling the internal thread accordingly. + */ + /* package */ DedicatedThreadAsyncMediaCodecAdapter( + MediaCodec codec, boolean enableAsynchronousQueueing, int trackType) { + this( + codec, + enableAsynchronousQueueing, + trackType, + new HandlerThread(createThreadLabel(trackType))); + } + + @VisibleForTesting + /* package */ DedicatedThreadAsyncMediaCodecAdapter( + MediaCodec codec, + boolean enableAsynchronousQueueing, + int trackType, + HandlerThread handlerThread) { + mediaCodecAsyncCallback = new MediaCodecAsyncCallback(); + this.codec = codec; + this.handlerThread = handlerThread; + state = STATE_CREATED; + codecStartRunnable = codec::start; + if (enableAsynchronousQueueing) { + bufferEnqueuer = new AsynchronousMediaCodecBufferEnqueuer(codec, trackType); + } else { + bufferEnqueuer = new SynchronousMediaCodecBufferEnqueuer(this.codec); + } + } + + @Override + public synchronized void start() { + handlerThread.start(); + handler = new Handler(handlerThread.getLooper()); + codec.setCallback(this, handler); + bufferEnqueuer.start(); + codecStartRunnable.run(); + state = STATE_STARTED; + } + + @Override + public void queueInputBuffer( + int index, int offset, int size, long presentationTimeUs, int flags) { + // This method does not need to be synchronized because it does not interact with the + // mediaCodecAsyncCallback. + bufferEnqueuer.queueInputBuffer(index, offset, size, presentationTimeUs, flags); + } + + @Override + public void queueSecureInputBuffer( + int index, int offset, CryptoInfo info, long presentationTimeUs, int flags) { + // This method does not need to be synchronized because it does not interact with the + // mediaCodecAsyncCallback. + bufferEnqueuer.queueSecureInputBuffer(index, offset, info, presentationTimeUs, flags); + } + + @Override + public synchronized int dequeueInputBufferIndex() { + if (isFlushing()) { + return MediaCodec.INFO_TRY_AGAIN_LATER; + } else { + maybeThrowException(); + return mediaCodecAsyncCallback.dequeueInputBufferIndex(); + } + } + + @Override + public synchronized int dequeueOutputBufferIndex(MediaCodec.BufferInfo bufferInfo) { + if (isFlushing()) { + return MediaCodec.INFO_TRY_AGAIN_LATER; + } else { + maybeThrowException(); + return mediaCodecAsyncCallback.dequeueOutputBufferIndex(bufferInfo); + } + } + + @Override + public synchronized MediaFormat getOutputFormat() { + return mediaCodecAsyncCallback.getOutputFormat(); + } + + @Override + public synchronized void flush() { + bufferEnqueuer.flush(); + codec.flush(); + ++pendingFlushCount; + Util.castNonNull(handler).post(this::onFlushCompleted); + } + + @Override + public synchronized void shutdown() { + if (state == STATE_STARTED) { + bufferEnqueuer.shutdown(); + handlerThread.quit(); + mediaCodecAsyncCallback.flush(); + } + state = STATE_SHUT_DOWN; + } + + @Override + public synchronized void onInputBufferAvailable(MediaCodec codec, int index) { + mediaCodecAsyncCallback.onInputBufferAvailable(codec, index); + } + + @Override + public synchronized void onOutputBufferAvailable( + MediaCodec codec, int index, MediaCodec.BufferInfo info) { + mediaCodecAsyncCallback.onOutputBufferAvailable(codec, index, info); + } + + @Override + public synchronized void onError(MediaCodec codec, MediaCodec.CodecException e) { + mediaCodecAsyncCallback.onError(codec, e); + } + + @Override + public synchronized void onOutputFormatChanged(MediaCodec codec, MediaFormat format) { + mediaCodecAsyncCallback.onOutputFormatChanged(codec, format); + } + + @VisibleForTesting + /* package */ void onMediaCodecError(IllegalStateException e) { + mediaCodecAsyncCallback.onMediaCodecError(e); + } + + @VisibleForTesting + /* package */ void setCodecStartRunnable(Runnable codecStartRunnable) { + this.codecStartRunnable = codecStartRunnable; + } + + private synchronized void onFlushCompleted() { + if (state != STATE_STARTED) { + // The adapter has been shutdown. + return; + } + + --pendingFlushCount; + if (pendingFlushCount > 0) { + // Another flush() has been called. + return; + } else if (pendingFlushCount < 0) { + // This should never happen. + internalException = new IllegalStateException(); + return; + } + + mediaCodecAsyncCallback.flush(); + try { + codecStartRunnable.run(); + } catch (IllegalStateException e) { + internalException = e; + } catch (Exception e) { + internalException = new IllegalStateException(e); + } + } + + private synchronized boolean isFlushing() { + return pendingFlushCount > 0; + } + + private synchronized void maybeThrowException() { + maybeThrowInternalException(); + mediaCodecAsyncCallback.maybeThrowMediaCodecException(); + } + + private synchronized void maybeThrowInternalException() { + if (internalException != null) { + IllegalStateException e = internalException; + internalException = null; + throw e; + } + } + + private static String createThreadLabel(int trackType) { + StringBuilder labelBuilder = new StringBuilder("ExoPlayer:MediaCodecAsyncAdapter:"); + if (trackType == C.TRACK_TYPE_AUDIO) { + labelBuilder.append("Audio"); + } else if (trackType == C.TRACK_TYPE_VIDEO) { + labelBuilder.append("Video"); + } else { + labelBuilder.append("Unknown(").append(trackType).append(")"); + } + return labelBuilder.toString(); + } +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecAdapter.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecAdapter.java new file mode 100644 index 0000000000..1be850c899 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecAdapter.java @@ -0,0 +1,112 @@ +/* + * Copyright (C) 2019 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.mediacodec; + +import android.media.MediaCodec; +import android.media.MediaFormat; +import com.google.android.exoplayer2.decoder.CryptoInfo; + +/** + * Abstracts {@link MediaCodec} operations. + * + *

      {@code MediaCodecAdapter} offers a common interface to interact with a {@link MediaCodec} + * regardless of the {@link + * com.google.android.exoplayer2.mediacodec.MediaCodecRenderer.MediaCodecOperationMode} the {@link + * MediaCodec} is operating in. + * + * @see com.google.android.exoplayer2.mediacodec.MediaCodecRenderer.MediaCodecOperationMode + */ +/* package */ interface MediaCodecAdapter { + + /** + * Starts this instance. + * + * @see MediaCodec#start(). + */ + void start(); + + /** + * Returns the next available input buffer index from the underlying {@link MediaCodec} or {@link + * MediaCodec#INFO_TRY_AGAIN_LATER} if no such buffer exists. + * + * @throws IllegalStateException If the underlying {@link MediaCodec} raised an error. + */ + int dequeueInputBufferIndex(); + + /** + * Returns the next available output buffer index from the underlying {@link MediaCodec}. If the + * next available output is a MediaFormat change, it will return {@link + * MediaCodec#INFO_OUTPUT_FORMAT_CHANGED} and you should call {@link #getOutputFormat()} to get + * the format. If there is no available output, this method will return {@link + * MediaCodec#INFO_TRY_AGAIN_LATER}. + * + * @throws IllegalStateException If the underlying {@link MediaCodec} raised an error. + */ + int dequeueOutputBufferIndex(MediaCodec.BufferInfo bufferInfo); + + /** + * Gets the {@link MediaFormat} that was output from the {@link MediaCodec}. + * + *

      Call this method if a previous call to {@link #dequeueOutputBufferIndex} returned {@link + * MediaCodec#INFO_OUTPUT_FORMAT_CHANGED}. + */ + MediaFormat getOutputFormat(); + + /** + * Submit an input buffer for decoding. + * + *

      The {@code index} must be an input buffer index that has been obtained from a previous call + * to {@link #dequeueInputBufferIndex()}. + * + * @see MediaCodec#queueInputBuffer + */ + void queueInputBuffer(int index, int offset, int size, long presentationTimeUs, int flags); + + /** + * Submit an input buffer that is potentially encrypted for decoding. + * + *

      The {@code index} must be an input buffer index that has been obtained from a previous call + * to {@link #dequeueInputBufferIndex()}. + * + *

      Note: This method behaves as {@link MediaCodec#queueSecureInputBuffer} with the difference + * that {@code info} is of type {@link CryptoInfo} and not {@link + * android.media.MediaCodec.CryptoInfo}. + * + * @see MediaCodec#queueSecureInputBuffer + */ + void queueSecureInputBuffer( + int index, int offset, CryptoInfo info, long presentationTimeUs, int flags); + + /** + * Flushes the {@code MediaCodecAdapter}. + * + *

      Note: {@link #flush()} should also call any {@link MediaCodec} methods needed to flush the + * {@link MediaCodec}, i.e., {@link MediaCodec#flush()} and optionally {@link + * MediaCodec#start()}, if the {@link MediaCodec} operates in asynchronous mode. + */ + void flush(); + + /** + * Shutdown the {@code MediaCodecAdapter}. + * + *

      Note: This method does not release the underlying {@link MediaCodec}. Make sure to call + * {@link #shutdown()} before stopping or releasing the underlying {@link MediaCodec} to ensure + * the adapter is fully shutdown before the {@link MediaCodec} stops executing. Otherwise, there + * is a risk the adapter might interact with a stopped or released {@link MediaCodec}. + */ + void shutdown(); +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecAsyncCallback.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecAsyncCallback.java index dc4bcbbf38..5be6031356 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecAsyncCallback.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecAsyncCallback.java @@ -17,7 +17,6 @@ package com.google.android.exoplayer2.mediacodec; import android.media.MediaCodec; import android.media.MediaFormat; -import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import androidx.annotation.VisibleForTesting; @@ -120,25 +119,24 @@ import java.util.ArrayDeque; } @Override - public void onInputBufferAvailable(@NonNull MediaCodec mediaCodec, int i) { + public void onInputBufferAvailable(MediaCodec mediaCodec, int i) { availableInputBuffers.add(i); } @Override public void onOutputBufferAvailable( - @NonNull MediaCodec mediaCodec, int i, @NonNull MediaCodec.BufferInfo bufferInfo) { + MediaCodec mediaCodec, int i, MediaCodec.BufferInfo bufferInfo) { availableOutputBuffers.add(i); bufferInfos.add(bufferInfo); } @Override - public void onError(@NonNull MediaCodec mediaCodec, @NonNull MediaCodec.CodecException e) { + public void onError(MediaCodec mediaCodec, MediaCodec.CodecException e) { onMediaCodecError(e); } @Override - public void onOutputFormatChanged( - @NonNull MediaCodec mediaCodec, @NonNull MediaFormat mediaFormat) { + public void onOutputFormatChanged(MediaCodec mediaCodec, MediaFormat mediaFormat) { availableOutputBuffers.add(MediaCodec.INFO_OUTPUT_FORMAT_CHANGED); formats.add(mediaFormat); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecDecoderException.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecDecoderException.java new file mode 100644 index 0000000000..524f8568f7 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecDecoderException.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2020 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.mediacodec; + +import android.media.MediaCodec; +import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; +import com.google.android.exoplayer2.decoder.DecoderException; +import com.google.android.exoplayer2.util.Util; + +/** Thrown when a failure occurs in a {@link MediaCodec} decoder. */ +public class MediaCodecDecoderException extends DecoderException { + + /** The {@link MediaCodecInfo} of the decoder that failed. Null if unknown. */ + @Nullable public final MediaCodecInfo codecInfo; + + /** An optional developer-readable diagnostic information string. May be null. */ + @Nullable public final String diagnosticInfo; + + public MediaCodecDecoderException(Throwable cause, @Nullable MediaCodecInfo codecInfo) { + super("Decoder failed: " + (codecInfo == null ? null : codecInfo.name), cause); + this.codecInfo = codecInfo; + diagnosticInfo = Util.SDK_INT >= 21 ? getDiagnosticInfoV21(cause) : null; + } + + @RequiresApi(21) + @Nullable + private static String getDiagnosticInfoV21(Throwable cause) { + if (cause instanceof MediaCodec.CodecException) { + return ((MediaCodec.CodecException) cause).getDiagnosticInfo(); + } + return null; + } +} 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 64517feec9..736f941152 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 @@ -15,7 +15,6 @@ */ package com.google.android.exoplayer2.mediacodec; -import android.annotation.TargetApi; import android.graphics.Point; import android.media.MediaCodec; import android.media.MediaCodecInfo.AudioCapabilities; @@ -24,6 +23,7 @@ import android.media.MediaCodecInfo.CodecProfileLevel; import android.media.MediaCodecInfo.VideoCapabilities; import android.util.Pair; import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Log; @@ -50,15 +50,14 @@ public final class MediaCodecInfo { */ public final String name; - /** The MIME type handled by the codec, or {@code null} if this is a passthrough codec. */ - @Nullable public final String mimeType; + /** The MIME type handled by the codec. */ + public final String mimeType; /** - * The MIME type that the codec uses for media of type {@link #mimeType}, or {@code null} if this - * is a passthrough codec. Equal to {@link #mimeType} unless the codec is known to use a - * non-standard MIME type alias. + * The MIME type that the codec uses for media of type {@link #mimeType}. Equal to {@link + * #mimeType} unless the codec is known to use a non-standard MIME type alias. */ - @Nullable public final String codecMimeType; + public final String codecMimeType; /** * The capabilities of the decoder, like the profiles/levels it supports, or {@code null} if not @@ -90,9 +89,6 @@ public final class MediaCodecInfo { */ public final boolean secure; - /** Whether this instance describes a passthrough codec. */ - public final boolean passthrough; - /** * Whether the codec is hardware accelerated. * @@ -122,26 +118,6 @@ public final class MediaCodecInfo { private final boolean isVideo; - /** - * Creates an instance representing an audio passthrough decoder. - * - * @param name The name of the {@link MediaCodec}. - * @return The created instance. - */ - public static MediaCodecInfo newPassthroughInstance(String name) { - return new MediaCodecInfo( - name, - /* mimeType= */ null, - /* codecMimeType= */ null, - /* capabilities= */ null, - /* passthrough= */ true, - /* hardwareAccelerated= */ false, - /* softwareOnly= */ true, - /* vendor= */ false, - /* forceDisableAdaptive= */ false, - /* forceSecure= */ false); - } - /** * Creates an instance. * @@ -173,7 +149,6 @@ public final class MediaCodecInfo { mimeType, codecMimeType, capabilities, - /* passthrough= */ false, hardwareAccelerated, softwareOnly, vendor, @@ -183,10 +158,9 @@ public final class MediaCodecInfo { private MediaCodecInfo( String name, - @Nullable String mimeType, - @Nullable String codecMimeType, + String mimeType, + String codecMimeType, @Nullable CodecCapabilities capabilities, - boolean passthrough, boolean hardwareAccelerated, boolean softwareOnly, boolean vendor, @@ -196,7 +170,6 @@ public final class MediaCodecInfo { this.mimeType = mimeType; this.codecMimeType = codecMimeType; this.capabilities = capabilities; - this.passthrough = passthrough; this.hardwareAccelerated = hardwareAccelerated; this.softwareOnly = softwareOnly; this.vendor = vendor; @@ -229,9 +202,10 @@ public final class MediaCodecInfo { * @see CodecCapabilities#getMaxSupportedInstances() */ public int getMaxSupportedInstances() { - return (Util.SDK_INT < 23 || capabilities == null) - ? MAX_SUPPORTED_INSTANCES_UNKNOWN - : getMaxSupportedInstancesV23(capabilities); + if (Util.SDK_INT < 23 || capabilities == null) { + return MAX_SUPPORTED_INSTANCES_UNKNOWN; + } + return getMaxSupportedInstancesV23(capabilities); } /** @@ -393,7 +367,7 @@ public final class MediaCodecInfo { * Format#NO_VALUE} or any value less than or equal to 0. * @return Whether the decoder supports video with the given width, height and frame rate. */ - @TargetApi(21) + @RequiresApi(21) public boolean isVideoSizeAndRateSupportedV21(int width, int height, double frameRate) { if (capabilities == null) { logNoSupport("sizeAndRate.caps"); @@ -419,8 +393,8 @@ public final class MediaCodecInfo { /** * Returns the smallest video size greater than or equal to a specified size that also satisfies * the {@link MediaCodec}'s width and height alignment requirements. - *

      - * Must not be called if the device SDK version is less than 21. + * + *

      Must not be called if the device SDK version is less than 21. * * @param width Width in pixels. * @param height Height in pixels. @@ -428,7 +402,7 @@ public final class MediaCodecInfo { * the {@link MediaCodec}'s width and height alignment requirements, or null if not a video * codec. */ - @TargetApi(21) + @RequiresApi(21) public Point alignVideoSizeV21(int width, int height) { if (capabilities == null) { return null; @@ -442,13 +416,13 @@ public final class MediaCodecInfo { /** * Whether the decoder supports audio with a given sample rate. - *

      - * Must not be called if the device SDK version is less than 21. + * + *

      Must not be called if the device SDK version is less than 21. * * @param sampleRate The sample rate in Hz. * @return Whether the decoder supports audio with the given sample rate. */ - @TargetApi(21) + @RequiresApi(21) public boolean isAudioSampleRateSupportedV21(int sampleRate) { if (capabilities == null) { logNoSupport("sampleRate.caps"); @@ -468,13 +442,13 @@ public final class MediaCodecInfo { /** * Whether the decoder supports audio with a given channel count. - *

      - * Must not be called if the device SDK version is less than 21. + * + *

      Must not be called if the device SDK version is less than 21. * * @param channelCount The channel count. * @return Whether the decoder supports audio with the given channel count. */ - @TargetApi(21) + @RequiresApi(21) public boolean isAudioChannelCountSupportedV21(int channelCount) { if (capabilities == null) { logNoSupport("channelCount.caps"); @@ -542,7 +516,7 @@ public final class MediaCodecInfo { return Util.SDK_INT >= 19 && isAdaptiveV19(capabilities); } - @TargetApi(19) + @RequiresApi(19) private static boolean isAdaptiveV19(CodecCapabilities capabilities) { return capabilities.isFeatureSupported(CodecCapabilities.FEATURE_AdaptivePlayback); } @@ -551,7 +525,7 @@ public final class MediaCodecInfo { return Util.SDK_INT >= 21 && isTunnelingV21(capabilities); } - @TargetApi(21) + @RequiresApi(21) private static boolean isTunnelingV21(CodecCapabilities capabilities) { return capabilities.isFeatureSupported(CodecCapabilities.FEATURE_TunneledPlayback); } @@ -560,20 +534,22 @@ public final class MediaCodecInfo { return Util.SDK_INT >= 21 && isSecureV21(capabilities); } - @TargetApi(21) + @RequiresApi(21) private static boolean isSecureV21(CodecCapabilities capabilities) { return capabilities.isFeatureSupported(CodecCapabilities.FEATURE_SecurePlayback); } - @TargetApi(21) - private static boolean areSizeAndRateSupportedV21(VideoCapabilities capabilities, int width, - int height, double frameRate) { + @RequiresApi(21) + private static boolean areSizeAndRateSupportedV21( + VideoCapabilities capabilities, int width, int height, double frameRate) { // Don't ever fail due to alignment. See: https://github.com/google/ExoPlayer/issues/6551. Point alignedSize = alignVideoSizeV21(capabilities, width, height); width = alignedSize.x; height = alignedSize.y; - if (frameRate == Format.NO_VALUE || frameRate <= 0) { + // VideoCapabilities.areSizeAndRateSupported incorrectly returns false if frameRate < 1 on some + // versions of Android, so we only check the size in this case [Internal ref: b/153940404]. + if (frameRate == Format.NO_VALUE || frameRate < 1) { return capabilities.isSizeSupported(width, height); } else { // The signaled frame rate may be slightly higher than the actual frame rate, so we take the @@ -584,7 +560,7 @@ public final class MediaCodecInfo { } } - @TargetApi(21) + @RequiresApi(21) private static Point alignVideoSizeV21(VideoCapabilities capabilities, int width, int height) { int widthAlignment = capabilities.getWidthAlignment(); int heightAlignment = capabilities.getHeightAlignment(); @@ -593,7 +569,7 @@ public final class MediaCodecInfo { Util.ceilDivide(height, heightAlignment) * heightAlignment); } - @TargetApi(23) + @RequiresApi(23) private static int getMaxSupportedInstancesV23(CodecCapabilities capabilities) { return capabilities.getMaxSupportedInstances(); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecInputBufferEnqueuer.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecInputBufferEnqueuer.java new file mode 100644 index 0000000000..34a1ccc6ba --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecInputBufferEnqueuer.java @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2020 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.mediacodec; + +import android.media.MediaCodec; +import com.google.android.exoplayer2.decoder.CryptoInfo; + +/** Abstracts operations to enqueue input buffer on a {@link android.media.MediaCodec}. */ +interface MediaCodecInputBufferEnqueuer { + + /** + * Starts this instance. + * + *

      Call this method after creating an instance. + */ + void start(); + + /** + * Submits an input buffer for decoding. + * + * @see android.media.MediaCodec#queueInputBuffer + */ + void queueInputBuffer(int index, int offset, int size, long presentationTimeUs, int flags); + + /** + * Submits an input buffer that potentially contains encrypted data for decoding. + * + *

      Note: This method behaves as {@link MediaCodec#queueSecureInputBuffer} with the difference + * that {@code info} is of type {@link CryptoInfo} and not {@link + * android.media.MediaCodec.CryptoInfo}. + * + * @see android.media.MediaCodec#queueSecureInputBuffer + */ + void queueSecureInputBuffer( + int index, int offset, CryptoInfo info, long presentationTimeUs, int flags); + + /** Flushes the instance. */ + void flush(); + + /** Shut down the instance. Make sure to call this method to release its internal resources. */ + void shutdown(); +} 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 78eda88b4b..611631038a 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 @@ -23,9 +23,8 @@ import android.media.MediaCrypto; import android.media.MediaCryptoException; import android.media.MediaFormat; import android.os.Bundle; -import android.os.Handler; -import android.os.Looper; import android.os.SystemClock; +import androidx.annotation.CallSuper; import androidx.annotation.CheckResult; import androidx.annotation.IntDef; import androidx.annotation.Nullable; @@ -35,23 +34,28 @@ 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.decoder.CryptoInfo; import com.google.android.exoplayer2.decoder.DecoderCounters; import com.google.android.exoplayer2.decoder.DecoderInputBuffer; import com.google.android.exoplayer2.drm.DrmSession; import com.google.android.exoplayer2.drm.DrmSession.DrmSessionException; -import com.google.android.exoplayer2.drm.DrmSessionManager; +import com.google.android.exoplayer2.drm.ExoMediaCrypto; import com.google.android.exoplayer2.drm.FrameworkMediaCrypto; import com.google.android.exoplayer2.mediacodec.MediaCodecUtil.DecoderQueryException; import com.google.android.exoplayer2.source.MediaPeriod; +import com.google.android.exoplayer2.source.SampleStream; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Log; +import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.NalUnitUtil; import com.google.android.exoplayer2.util.TimedValueQueue; import com.google.android.exoplayer2.util.TraceUtil; import com.google.android.exoplayer2.util.Util; import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; import java.nio.ByteBuffer; import java.util.ArrayDeque; import java.util.ArrayList; @@ -63,8 +67,60 @@ import java.util.List; public abstract class MediaCodecRenderer extends BaseRenderer { /** - * Thrown when a failure occurs instantiating a decoder. + * The modes to operate the {@link MediaCodec}. + * + *

      Allowed values: + * + *

        + *
      • {@link #OPERATION_MODE_SYNCHRONOUS} + *
      • {@link #OPERATION_MODE_ASYNCHRONOUS_PLAYBACK_THREAD} + *
      • {@link #OPERATION_MODE_ASYNCHRONOUS_DEDICATED_THREAD} + *
      • {@link #OPERATION_MODE_ASYNCHRONOUS_DEDICATED_THREAD_MULTI_LOCK} + *
      */ + @Documented + @Retention(RetentionPolicy.SOURCE) + @Target({ElementType.TYPE_PARAMETER, ElementType.TYPE_USE}) + @IntDef({ + OPERATION_MODE_SYNCHRONOUS, + OPERATION_MODE_ASYNCHRONOUS_PLAYBACK_THREAD, + OPERATION_MODE_ASYNCHRONOUS_DEDICATED_THREAD, + OPERATION_MODE_ASYNCHRONOUS_DEDICATED_THREAD_MULTI_LOCK, + OPERATION_MODE_ASYNCHRONOUS_DEDICATED_THREAD_ASYNCHRONOUS_QUEUEING, + OPERATION_MODE_ASYNCHRONOUS_DEDICATED_THREAD_MULTI_LOCK_ASYNCHRONOUS_QUEUEING + }) + public @interface MediaCodecOperationMode {} + + /** Operates the {@link MediaCodec} in synchronous mode. */ + public static final int OPERATION_MODE_SYNCHRONOUS = 0; + /** + * Operates the {@link MediaCodec} in asynchronous mode and routes {@link MediaCodec.Callback} + * callbacks to the playback thread. + */ + public static final int OPERATION_MODE_ASYNCHRONOUS_PLAYBACK_THREAD = 1; + /** + * Operates the {@link MediaCodec} in asynchronous mode and routes {@link MediaCodec.Callback} + * callbacks to a dedicated thread. + */ + public static final int OPERATION_MODE_ASYNCHRONOUS_DEDICATED_THREAD = 2; + /** + * Operates the {@link MediaCodec} in asynchronous mode and routes {@link MediaCodec.Callback} + * callbacks to a dedicated thread. Uses granular locking for input and output buffers. + */ + public static final int OPERATION_MODE_ASYNCHRONOUS_DEDICATED_THREAD_MULTI_LOCK = 3; + /** + * Same as {@link #OPERATION_MODE_ASYNCHRONOUS_DEDICATED_THREAD}, and offloads queueing to another + * thread. + */ + public static final int OPERATION_MODE_ASYNCHRONOUS_DEDICATED_THREAD_ASYNCHRONOUS_QUEUEING = 4; + /** + * Same as {@link #OPERATION_MODE_ASYNCHRONOUS_DEDICATED_THREAD_MULTI_LOCK}, and offloads queueing + * to another thread. + */ + public static final int + OPERATION_MODE_ASYNCHRONOUS_DEDICATED_THREAD_MULTI_LOCK_ASYNCHRONOUS_QUEUEING = 5; + + /** Thrown when a failure occurs instantiating a decoder. */ public static class DecoderInitializationException extends Exception { private static final int CUSTOM_ERROR_CODE_BASE = -50000; @@ -97,8 +153,8 @@ public abstract class MediaCodecRenderer extends BaseRenderer { */ @Nullable public final DecoderInitializationException fallbackDecoderInitializationException; - public DecoderInitializationException(Format format, Throwable cause, - boolean secureDecoderRequired, int errorCode) { + public DecoderInitializationException( + Format format, @Nullable Throwable cause, boolean secureDecoderRequired, int errorCode) { this( "Decoder init failed: [" + errorCode + "], " + format, cause, @@ -111,7 +167,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { public DecoderInitializationException( Format format, - Throwable cause, + @Nullable Throwable cause, boolean secureDecoderRequired, MediaCodecInfo mediaCodecInfo) { this( @@ -126,7 +182,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { private DecoderInitializationException( String message, - Throwable cause, + @Nullable Throwable cause, String mimeType, boolean secureDecoderRequired, @Nullable MediaCodecInfo mediaCodecInfo, @@ -153,8 +209,9 @@ public abstract class MediaCodecRenderer extends BaseRenderer { fallbackException); } - @TargetApi(21) - private static String getDiagnosticInfoV21(Throwable cause) { + @RequiresApi(21) + @Nullable + private static String getDiagnosticInfoV21(@Nullable Throwable cause) { if (cause instanceof CodecException) { return ((CodecException) cause).getDiagnosticInfo(); } @@ -169,30 +226,6 @@ public abstract class MediaCodecRenderer extends BaseRenderer { } } - /** Thrown when a failure occurs in the decoder. */ - public static class DecoderException extends Exception { - - /** The {@link MediaCodecInfo} of the decoder that failed. Null if unknown. */ - @Nullable public final MediaCodecInfo codecInfo; - - /** An optional developer-readable diagnostic information string. May be null. */ - @Nullable public final String diagnosticInfo; - - public DecoderException(Throwable cause, @Nullable MediaCodecInfo codecInfo) { - super("Decoder failed: " + (codecInfo == null ? null : codecInfo.name), cause); - this.codecInfo = codecInfo; - diagnosticInfo = Util.SDK_INT >= 21 ? getDiagnosticInfoV21(cause) : null; - } - - @TargetApi(21) - private static String getDiagnosticInfoV21(Throwable cause) { - if (cause instanceof CodecException) { - return ((CodecException) cause).getDiagnosticInfo(); - } - return null; - } - } - /** Indicates no codec operating rate should be set. */ protected static final float CODEC_OPERATING_RATE_UNSET = -1; @@ -208,6 +241,10 @@ public abstract class MediaCodecRenderer extends BaseRenderer { */ private static final long MAX_CODEC_HOTSWAP_TIME_MS = 1000; + // 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; + /** * The possible return values for {@link #canKeepCodec(MediaCodec, MediaCodecInfo, Format, * Format)}. @@ -293,50 +330,6 @@ public abstract class MediaCodecRenderer extends BaseRenderer { }) private @interface AdaptationWorkaroundMode {} - /** - * Abstracts {@link MediaCodec} operations that differ whether a {@link MediaCodec} is used in - * synchronous or asynchronous mode. - */ - private interface MediaCodecAdapter { - - /** - * Returns the next available input buffer index from the underlying {@link MediaCodec} or - * {@link MediaCodec#INFO_TRY_AGAIN_LATER} if no such buffer exists. - * - * @throws {@link IllegalStateException} if the underling {@link MediaCodec} raised an error. - */ - int dequeueInputBufferIndex(); - - /** - * Returns the next available output buffer index from the underlying {@link MediaCodec}. If the - * next available output is a MediaFormat change, it will return {@link - * MediaCodec#INFO_OUTPUT_FORMAT_CHANGED} and you should call {@link #getOutputFormat()} to get - * the format. If there is no available output, this method will return {@link - * MediaCodec#INFO_TRY_AGAIN_LATER}. - * - * @throws {@link IllegalStateException} if the underling {@link MediaCodec} raised an error. - */ - int dequeueOutputBufferIndex(MediaCodec.BufferInfo bufferInfo); - - /** - * Gets the {@link MediaFormat} that was output from the {@link MediaCodec}. - * - *

      Call this method if a previous call to {@link #dequeueOutputBufferIndex} returned {@link - * MediaCodec#INFO_OUTPUT_FORMAT_CHANGED}. - */ - MediaFormat getOutputFormat(); - - /** Flushes the {@code MediaCodecAdapter}. */ - void flush(); - - /** - * Shutdown the {@code MediaCodecAdapter}. - * - *

      Note: it does not release the underlying codec. - */ - void shutdown(); - } - /** * The adaptation workaround is never used. */ @@ -365,24 +358,25 @@ public abstract class MediaCodecRenderer extends BaseRenderer { private static final int ADAPTATION_WORKAROUND_SLICE_WIDTH_HEIGHT = 32; private final MediaCodecSelector mediaCodecSelector; - @Nullable private final DrmSessionManager drmSessionManager; - private final boolean playClearSamplesWithoutKeys; private final boolean enableDecoderFallback; private final float assumedMinimumCodecOperatingRate; private final DecoderInputBuffer buffer; private final DecoderInputBuffer flagsOnlyBuffer; + private final BatchBuffer passthroughBatchBuffer; private final TimedValueQueue formatQueue; private final ArrayList decodeOnlyPresentationTimestamps; private final MediaCodec.BufferInfo outputBufferInfo; + private final long[] pendingOutputStreamOffsetsUs; + private final long[] pendingOutputStreamSwitchTimesUs; @Nullable private Format inputFormat; - private Format outputFormat; - @Nullable private DrmSession codecDrmSession; - @Nullable private DrmSession sourceDrmSession; + @Nullable private Format outputFormat; + @Nullable private DrmSession codecDrmSession; + @Nullable private DrmSession sourceDrmSession; @Nullable private MediaCrypto mediaCrypto; private boolean mediaCryptoRequiresSecureDecoder; private long renderTimeLimitMs; - private float rendererOperatingRate; + private float operatingRate; @Nullable private MediaCodec codec; @Nullable private MediaCodecAdapter codecAdapter; @Nullable private Format codecFormat; @@ -394,6 +388,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { private boolean codecNeedsReconfigureWorkaround; private boolean codecNeedsDiscardToSpsWorkaround; private boolean codecNeedsFlushWorkaround; + private boolean codecNeedsSosFlushWorkaround; private boolean codecNeedsEosFlushWorkaround; private boolean codecNeedsEosOutputExceptionWorkaround; private boolean codecNeedsMonoChannelCountWorkaround; @@ -408,37 +403,33 @@ public abstract class MediaCodecRenderer extends BaseRenderer { private ByteBuffer outputBuffer; private boolean isDecodeOnlyOutputBuffer; private boolean isLastOutputBuffer; + private boolean passthroughEnabled; + private boolean passthroughDrainAndReinitialize; private boolean codecReconfigured; @ReconfigurationState private int codecReconfigurationState; @DrainState private int codecDrainState; @DrainAction private int codecDrainAction; private boolean codecReceivedBuffers; private boolean codecReceivedEos; - private long lastBufferInStreamPresentationTimeUs; + private boolean codecHasOutputMediaFormat; private long largestQueuedPresentationTimeUs; + private long lastBufferInStreamPresentationTimeUs; private boolean inputStreamEnded; private boolean outputStreamEnded; private boolean waitingForKeys; private boolean waitingForFirstSyncSample; private boolean waitingForFirstSampleInFormat; - private boolean skipMediaCodecStopOnRelease; private boolean pendingOutputEndOfStream; - - private boolean useMediaCodecInAsyncMode; - + @MediaCodecOperationMode private int mediaCodecOperationMode; protected DecoderCounters decoderCounters; + private long outputStreamOffsetUs; + private int pendingOutputStreamOffsetCount; + private boolean receivedOutputMediaFormatChange; /** * @param trackType The track type that the renderer handles. One of the {@code C.TRACK_TYPE_*} * constants defined in {@link C}. * @param mediaCodecSelector A decoder selector. - * @param drmSessionManager For use with encrypted media. May be null if support for encrypted - * media is not required. - * @param playClearSamplesWithoutKeys Encrypted media may contain clear (un-encrypted) regions. - * For example a media file may start with a short clear region so as to allow playback to - * 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 enableDecoderFallback Whether to enable fallback to lower-priority decoders if decoder * initialization fails. This may result in using a decoder that is less efficient or slower * than the primary decoder. @@ -449,14 +440,10 @@ public abstract class MediaCodecRenderer extends BaseRenderer { public MediaCodecRenderer( int trackType, MediaCodecSelector mediaCodecSelector, - @Nullable DrmSessionManager drmSessionManager, - boolean playClearSamplesWithoutKeys, boolean enableDecoderFallback, float assumedMinimumCodecOperatingRate) { super(trackType); this.mediaCodecSelector = Assertions.checkNotNull(mediaCodecSelector); - this.drmSessionManager = drmSessionManager; - this.playClearSamplesWithoutKeys = playClearSamplesWithoutKeys; this.enableDecoderFallback = enableDecoderFallback; this.assumedMinimumCodecOperatingRate = assumedMinimumCodecOperatingRate; buffer = new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_DISABLED); @@ -464,12 +451,14 @@ public abstract class MediaCodecRenderer extends BaseRenderer { formatQueue = new TimedValueQueue<>(); decodeOnlyPresentationTimestamps = new ArrayList<>(); outputBufferInfo = new MediaCodec.BufferInfo(); - codecReconfigurationState = RECONFIGURATION_STATE_NONE; - codecDrainState = DRAIN_STATE_NONE; - codecDrainAction = DRAIN_ACTION_NONE; - codecOperatingRate = CODEC_OPERATING_RATE_UNSET; - rendererOperatingRate = 1f; + operatingRate = 1f; renderTimeLimitMs = C.TIME_UNSET; + mediaCodecOperationMode = OPERATION_MODE_SYNCHRONOUS; + pendingOutputStreamOffsetsUs = new long[MAX_PENDING_OUTPUT_STREAM_OFFSET_COUNT]; + pendingOutputStreamSwitchTimesUs = new long[MAX_PENDING_OUTPUT_STREAM_OFFSET_COUNT]; + outputStreamOffsetUs = C.TIME_UNSET; + passthroughBatchBuffer = new BatchBuffer(); + resetCodecStateForRelease(); } /** @@ -487,61 +476,69 @@ public abstract class MediaCodecRenderer extends BaseRenderer { } /** - * Skip calling {@link MediaCodec#stop()} when the underlying MediaCodec is going to be released. - * - *

      By default, when the MediaCodecRenderer is releasing the underlying {@link MediaCodec}, it - * first calls {@link MediaCodec#stop()} and then calls {@link MediaCodec#release()}. If this - * feature is enabled, the MediaCodecRenderer will skip the call to {@link MediaCodec#stop()}. + * Set the mode of operation of the underlying {@link MediaCodec}. * *

      This method is experimental, and will be renamed or removed in a future release. It should * only be called before the renderer is used. * - * @param enabled enable or disable the feature. + * @param mode The mode of the MediaCodec. The supported modes are: + *

        + *
      • {@link #OPERATION_MODE_SYNCHRONOUS}: The {@link MediaCodec} will operate in + * synchronous mode. + *
      • {@link #OPERATION_MODE_ASYNCHRONOUS_PLAYBACK_THREAD}: The {@link MediaCodec} will + * operate in asynchronous mode and {@link MediaCodec.Callback} callbacks will be routed + * to the playback thread. This mode requires API level ≥ 21; if the API level is + * ≤ 20, the operation mode will be set to {@link + * MediaCodecRenderer#OPERATION_MODE_SYNCHRONOUS}. + *
      • {@link #OPERATION_MODE_ASYNCHRONOUS_DEDICATED_THREAD}: The {@link MediaCodec} will + * operate in asynchronous mode and {@link MediaCodec.Callback} callbacks will be routed + * to a dedicated thread. This mode requires API level ≥ 23; if the API level is ≤ + * 22, the operation mode will be set to {@link #OPERATION_MODE_SYNCHRONOUS}. + *
      • {@link #OPERATION_MODE_ASYNCHRONOUS_DEDICATED_THREAD_MULTI_LOCK}: Same as {@link + * #OPERATION_MODE_ASYNCHRONOUS_DEDICATED_THREAD} and, in addition, input buffers will + * submitted to the {@link MediaCodec} in a separate thread. + *
      • {@link #OPERATION_MODE_ASYNCHRONOUS_DEDICATED_THREAD_ASYNCHRONOUS_QUEUEING}: Same as + * {@link #OPERATION_MODE_ASYNCHRONOUS_DEDICATED_THREAD} and, in addition, input buffers + * will be submitted to the {@link MediaCodec} in a separate thread. + *
      • {@link + * #OPERATION_MODE_ASYNCHRONOUS_DEDICATED_THREAD_MULTI_LOCK_ASYNCHRONOUS_QUEUEING}: Same + * as {@link #OPERATION_MODE_ASYNCHRONOUS_DEDICATED_THREAD_MULTI_LOCK} and, in addition, + * input buffers will be submitted to the {@link MediaCodec} in a separate thread. + *
      + * By default, the operation mode is set to {@link + * MediaCodecRenderer#OPERATION_MODE_SYNCHRONOUS}. */ - public void experimental_setSkipMediaCodecStopOnRelease(boolean enabled) { - skipMediaCodecStopOnRelease = enabled; - } - - /** - * Use the underlying {@link MediaCodec} in asynchronous mode to obtain available input and output - * buffers. - * - *

      This method is experimental, and will be renamed or removed in a future release. It should - * only be called before the renderer is used. - * - * @param enabled enable of disable the feature. - */ - public void experimental_setUseMediaCodecInAsyncMode(boolean enabled) { - useMediaCodecInAsyncMode = enabled; + public void experimental_setMediaCodecOperationMode(@MediaCodecOperationMode int mode) { + mediaCodecOperationMode = mode; } @Override + @AdaptiveSupport public final int supportsMixedMimeTypeAdaptation() { return ADAPTIVE_NOT_SEAMLESS; } @Override + @Capabilities public final int supportsFormat(Format format) throws ExoPlaybackException { try { - return supportsFormat(mediaCodecSelector, drmSessionManager, format); + return supportsFormat(mediaCodecSelector, format); } catch (DecoderQueryException e) { - throw ExoPlaybackException.createForRenderer(e, getIndex()); + throw createRendererException(e, format); } } /** - * Returns the extent to which the renderer is capable of supporting a given {@link Format}. + * Returns the {@link Capabilities} for the given {@link Format}. * * @param mediaCodecSelector The decoder selector. - * @param drmSessionManager The renderer's {@link DrmSessionManager}. * @param format The {@link Format}. - * @return The extent to which the renderer is capable of supporting the given format. See {@link - * #supportsFormat(Format)} for more detail. + * @return The {@link Capabilities} for this {@link Format}. * @throws DecoderQueryException If there was an error querying decoders. */ + @Capabilities protected abstract int supportsFormat( MediaCodecSelector mediaCodecSelector, - @Nullable DrmSessionManager drmSessionManager, Format format) throws DecoderQueryException; @@ -575,9 +572,15 @@ public abstract class MediaCodecRenderer extends BaseRenderer { @Nullable MediaCrypto crypto, float codecOperatingRate); - protected final void maybeInitCodec() throws ExoPlaybackException { - if (codec != null || inputFormat == null) { - // We have a codec already, or we don't have a format with which to instantiate one. + protected final void maybeInitCodecOrPassthrough() throws ExoPlaybackException { + if (codec != null || passthroughEnabled || inputFormat == null) { + // We have a codec or using passthrough, or don't have a format to decide how to render. + return; + } + + if (inputFormat.drmInitData == null + && usePassthrough(inputFormat.channelCount, inputFormat.sampleMimeType)) { + initPassthrough(inputFormat); return; } @@ -586,9 +589,10 @@ public abstract class MediaCodecRenderer extends BaseRenderer { String mimeType = inputFormat.sampleMimeType; if (codecDrmSession != null) { if (mediaCrypto == null) { - FrameworkMediaCrypto sessionMediaCrypto = codecDrmSession.getMediaCrypto(); + @Nullable + FrameworkMediaCrypto sessionMediaCrypto = getFrameworkMediaCrypto(codecDrmSession); if (sessionMediaCrypto == null) { - DrmSessionException drmError = codecDrmSession.getError(); + @Nullable DrmSessionException drmError = codecDrmSession.getError(); if (drmError != null) { // Continue for now. We may be able to avoid failure if the session recovers, or if a // new input format causes the session to be replaced before it's used. @@ -600,7 +604,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { try { mediaCrypto = new MediaCrypto(sessionMediaCrypto.uuid, sessionMediaCrypto.sessionId); } catch (MediaCryptoException e) { - throw ExoPlaybackException.createForRenderer(e, getIndex()); + throw createRendererException(e, inputFormat); } mediaCryptoRequiresSecureDecoder = !sessionMediaCrypto.forceAllowInsecureDecoderComponents @@ -610,7 +614,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { if (FrameworkMediaCrypto.WORKAROUND_DEVICE_NEEDS_KEYS_TO_CONFIGURE_CODEC) { @DrmSession.State int drmSessionState = codecDrmSession.getState(); if (drmSessionState == DrmSession.STATE_ERROR) { - throw ExoPlaybackException.createForRenderer(codecDrmSession.getError(), getIndex()); + throw createRendererException(codecDrmSession.getError(), inputFormat); } else if (drmSessionState != DrmSession.STATE_OPENED_WITH_KEYS) { // Wait for keys. return; @@ -621,10 +625,22 @@ public abstract class MediaCodecRenderer extends BaseRenderer { try { maybeInitCodecWithFallback(mediaCrypto, mediaCryptoRequiresSecureDecoder); } catch (DecoderInitializationException e) { - throw ExoPlaybackException.createForRenderer(e, getIndex()); + throw createRendererException(e, inputFormat); } } + /** + * Returns whether encoded passthrough should be used for playing back the input format. + * + * @param channelCount The number of channels in the input media, or {@link Format#NO_VALUE} if + * not known. + * @param mimeType The type of input media. + * @return Whether passthrough playback is supported. + */ + protected boolean usePassthrough(int channelCount, String mimeType) { + return false; + } + protected boolean shouldInitCodec(MediaCodecInfo codecInfo) { return true; } @@ -643,39 +659,86 @@ public abstract class MediaCodecRenderer extends BaseRenderer { * method if they are taking over responsibility for output format propagation (e.g., when using * video tunneling). */ - protected final @Nullable Format updateOutputFormatForTime(long presentationTimeUs) { - Format format = formatQueue.pollFloor(presentationTimeUs); + protected final void updateOutputFormatForTime(long presentationTimeUs) { + @Nullable Format format = formatQueue.pollFloor(presentationTimeUs); if (format != null) { outputFormat = format; + onOutputFormatChanged(outputFormat); + } else if (receivedOutputMediaFormatChange && outputFormat != null) { + // No Format change with the MediaFormat change, so we need to update based on the existing + // Format. + configureOutput(outputFormat); } - return format; + + receivedOutputMediaFormatChange = false; } + @Nullable + protected final Format getCurrentOutputFormat() { + return outputFormat; + } + + @Nullable protected final MediaCodec getCodec() { return codec; } - protected final @Nullable MediaCodecInfo getCodecInfo() { + @Nullable + protected final MediaCodecInfo getCodecInfo() { return codecInfo; } @Override - protected void onEnabled(boolean joining) throws ExoPlaybackException { + protected void onEnabled(boolean joining, boolean mayRenderStartOfStream) + throws ExoPlaybackException { decoderCounters = new DecoderCounters(); } + @Override + protected void onStreamChanged(Format[] formats, long offsetUs) throws ExoPlaybackException { + if (outputStreamOffsetUs == C.TIME_UNSET) { + outputStreamOffsetUs = offsetUs; + } else { + if (pendingOutputStreamOffsetCount == pendingOutputStreamOffsetsUs.length) { + Log.w( + TAG, + "Too many stream changes, so dropping offset: " + + pendingOutputStreamOffsetsUs[pendingOutputStreamOffsetCount - 1]); + } else { + pendingOutputStreamOffsetCount++; + } + pendingOutputStreamOffsetsUs[pendingOutputStreamOffsetCount - 1] = offsetUs; + pendingOutputStreamSwitchTimesUs[pendingOutputStreamOffsetCount - 1] = + largestQueuedPresentationTimeUs; + } + } + @Override protected void onPositionReset(long positionUs, boolean joining) throws ExoPlaybackException { inputStreamEnded = false; outputStreamEnded = false; pendingOutputEndOfStream = false; - flushOrReinitializeCodec(); + if (passthroughEnabled) { + passthroughBatchBuffer.flush(); + } else { + flushOrReinitializeCodec(); + } + // If there is a format change on the input side still pending propagation to the output, we + // need to queue a format next time a buffer is read. This is because we may not read a new + // input format after the position reset. + if (formatQueue.size() > 0) { + waitingForFirstSampleInFormat = true; + } formatQueue.clear(); + if (pendingOutputStreamOffsetCount != 0) { + outputStreamOffsetUs = pendingOutputStreamOffsetsUs[pendingOutputStreamOffsetCount - 1]; + pendingOutputStreamOffsetCount = 0; + } } @Override - public final void setOperatingRate(float operatingRate) throws ExoPlaybackException { - rendererOperatingRate = operatingRate; + public void setOperatingRate(float operatingRate) throws ExoPlaybackException { + this.operatingRate = operatingRate; if (codec != null && codecDrainAction != DRAIN_ACTION_REINITIALIZE && getState() != STATE_DISABLED) { @@ -686,6 +749,8 @@ public abstract class MediaCodecRenderer extends BaseRenderer { @Override protected void onDisabled() { inputFormat = null; + outputStreamOffsetUs = C.TIME_UNSET; + pendingOutputStreamOffsetCount = 0; if (sourceDrmSession != null || codecDrmSession != null) { // TODO: Do something better with this case. onReset(); @@ -697,49 +762,39 @@ public abstract class MediaCodecRenderer extends BaseRenderer { @Override protected void onReset() { try { + disablePassthrough(); releaseCodec(); } finally { setSourceDrmSession(null); } } + private void disablePassthrough() { + passthroughDrainAndReinitialize = false; + passthroughBatchBuffer.clear(); + passthroughEnabled = false; + } + protected void releaseCodec() { - availableCodecInfos = null; - codecInfo = null; - codecFormat = null; - resetInputBuffer(); - resetOutputBuffer(); - resetCodecBuffers(); - waitingForKeys = false; - codecHotswapDeadlineMs = C.TIME_UNSET; - decodeOnlyPresentationTimestamps.clear(); - largestQueuedPresentationTimeUs = C.TIME_UNSET; - lastBufferInStreamPresentationTimeUs = C.TIME_UNSET; try { + if (codecAdapter != null) { + codecAdapter.shutdown(); + } if (codec != null) { decoderCounters.decoderReleaseCount++; - try { - if (!skipMediaCodecStopOnRelease) { - codec.stop(); - } - } finally { - codec.release(); - } + codec.release(); } } finally { codec = null; - if (codecAdapter != null) { - codecAdapter.shutdown(); - codecAdapter = null; - } + codecAdapter = null; try { if (mediaCrypto != null) { mediaCrypto.release(); } } finally { mediaCrypto = null; - mediaCryptoRequiresSecureDecoder = false; setCodecDrmSession(null); + resetCodecStateForRelease(); } } } @@ -770,27 +825,30 @@ public abstract class MediaCodecRenderer extends BaseRenderer { return; } // We have a format. - maybeInitCodec(); - if (codec != null) { - long drainStartTimeMs = SystemClock.elapsedRealtime(); + maybeInitCodecOrPassthrough(); + if (passthroughEnabled) { + TraceUtil.beginSection("renderPassthrough"); + while (renderPassthrough(positionUs, elapsedRealtimeUs)) {} + TraceUtil.endSection(); + } else if (codec != null) { + long renderStartTimeMs = SystemClock.elapsedRealtime(); TraceUtil.beginSection("drainAndFeed"); - while (drainOutputBuffer(positionUs, elapsedRealtimeUs)) {} - while (feedInputBuffer() && shouldContinueFeeding(drainStartTimeMs)) {} + while (drainOutputBuffer(positionUs, elapsedRealtimeUs) + && shouldContinueRendering(renderStartTimeMs)) {} + while (feedInputBuffer() && shouldContinueRendering(renderStartTimeMs)) {} TraceUtil.endSection(); } else { decoderCounters.skippedInputBufferCount += skipSource(positionUs); // We need to read any format changes despite not having a codec so that drmSession can be // updated, and so that we have the most recent format should the codec be initialized. We - // may - // also reach the end of the stream. Note that readSource will not read a sample into a + // may also reach the end of the stream. Note that readSource will not read a sample into a // flags-only buffer. readToFlagsOnlyBuffer(/* requireFormat= */ false); } decoderCounters.ensureUpdated(); } catch (IllegalStateException e) { if (isMediaCodecException(e)) { - throw ExoPlaybackException.createForRenderer( - createDecoderException(e, getCodecInfo()), getIndex()); + throw createRendererException(e, inputFormat); } throw e; } @@ -801,7 +859,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { * This method is a no-op if the codec is {@code null}. * *

      The implementation of this method calls {@link #flushOrReleaseCodec()}, and {@link - * #maybeInitCodec()} if the codec needs to be re-instantiated. + * #maybeInitCodecOrPassthrough()} if the codec needs to be re-instantiated. * * @return Whether the codec was released and reinitialized, rather than being flushed. * @throws ExoPlaybackException If an error occurs re-instantiating the codec. @@ -809,7 +867,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { protected final boolean flushOrReinitializeCodec() throws ExoPlaybackException { boolean released = flushOrReleaseCodec(); if (released) { - maybeInitCodec(); + maybeInitCodecOrPassthrough(); } return released; } @@ -826,12 +884,22 @@ public abstract class MediaCodecRenderer extends BaseRenderer { } if (codecDrainAction == DRAIN_ACTION_REINITIALIZE || codecNeedsFlushWorkaround + || (codecNeedsSosFlushWorkaround && !codecHasOutputMediaFormat) || (codecNeedsEosFlushWorkaround && codecReceivedEos)) { releaseCodec(); return true; } + try { + codecAdapter.flush(); + } finally { + resetCodecStateForFlush(); + } + return false; + } - codecAdapter.flush(); + /** Resets the renderer internal state after a codec flush. */ + @CallSuper + protected void resetCodecStateForFlush() { resetInputBuffer(); resetOutputBuffer(); codecHotswapDeadlineMs = C.TIME_UNSET; @@ -842,7 +910,6 @@ public abstract class MediaCodecRenderer extends BaseRenderer { shouldSkipAdaptationWorkaroundOutputBuffer = false; isDecodeOnlyOutputBuffer = false; isLastOutputBuffer = false; - waitingForKeys = false; decodeOnlyPresentationTimestamps.clear(); largestQueuedPresentationTimeUs = C.TIME_UNSET; @@ -854,18 +921,48 @@ public abstract class MediaCodecRenderer extends BaseRenderer { // guarantee that it's processed. codecReconfigurationState = codecReconfigured ? RECONFIGURATION_STATE_WRITE_PENDING : RECONFIGURATION_STATE_NONE; - return false; } - protected DecoderException createDecoderException( + /** + * Resets the renderer internal state after a codec release. + * + *

      Note that this only needs to reset state variables that are changed in addition to those + * already changed in {@link #resetCodecStateForFlush()}. + */ + @CallSuper + protected void resetCodecStateForRelease() { + resetCodecStateForFlush(); + + availableCodecInfos = null; + codecInfo = null; + codecFormat = null; + codecHasOutputMediaFormat = false; + codecOperatingRate = CODEC_OPERATING_RATE_UNSET; + codecAdaptationWorkaroundMode = ADAPTATION_WORKAROUND_MODE_NEVER; + codecNeedsReconfigureWorkaround = false; + codecNeedsDiscardToSpsWorkaround = false; + codecNeedsFlushWorkaround = false; + codecNeedsSosFlushWorkaround = false; + codecNeedsEosFlushWorkaround = false; + codecNeedsEosOutputExceptionWorkaround = false; + codecNeedsMonoChannelCountWorkaround = false; + codecNeedsEosPropagation = false; + codecReconfigured = false; + codecReconfigurationState = RECONFIGURATION_STATE_NONE; + resetCodecBuffers(); + mediaCryptoRequiresSecureDecoder = false; + } + + protected MediaCodecDecoderException createDecoderException( Throwable cause, @Nullable MediaCodecInfo codecInfo) { - return new DecoderException(cause, codecInfo); + return new MediaCodecDecoderException(cause, codecInfo); } /** Reads into {@link #flagsOnlyBuffer} and returns whether a {@link Format} was read. */ private boolean readToFlagsOnlyBuffer(boolean requireFormat) throws ExoPlaybackException { FormatHolder formatHolder = getFormatHolder(); flagsOnlyBuffer.clear(); + @SampleStream.ReadDataResult int result = readSource(formatHolder, flagsOnlyBuffer, requireFormat); if (result == C.RESULT_FORMAT_READ) { onInputFormatChanged(formatHolder); @@ -963,6 +1060,26 @@ public abstract class MediaCodecRenderer extends BaseRenderer { return codecInfos; } + /** + * Configures passthrough where no codec is used. Called instead of {@link + * #configureCodec(MediaCodecInfo, MediaCodec, Format, MediaCrypto, float)} when no codec is used + * in passthrough. + */ + private void initPassthrough(Format format) { + disablePassthrough(); // In case of transition between 2 passthrough formats. + + String mimeType = format.sampleMimeType; + if (!MimeTypes.AUDIO_AAC.equals(mimeType) + && !MimeTypes.AUDIO_MPEG.equals(mimeType) + && !MimeTypes.AUDIO_OPUS.equals(mimeType)) { + // TODO(b/154746451): Batching provokes frame drops in non offload passthrough. + passthroughBatchBuffer.setMaxAccessUnitCount(1); + } else { + passthroughBatchBuffer.setMaxAccessUnitCount(BatchBuffer.DEFAULT_BATCH_SIZE_ACCESS_UNITS); + } + passthroughEnabled = true; + } + private void initCodec(MediaCodecInfo codecInfo, MediaCrypto crypto) throws Exception { long codecInitializingTimestamp; long codecInitializedTimestamp; @@ -972,7 +1089,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { float codecOperatingRate = Util.SDK_INT < 23 ? CODEC_OPERATING_RATE_UNSET - : getCodecOperatingRateV23(rendererOperatingRate, inputFormat, getStreamFormats()); + : getCodecOperatingRateV23(operatingRate, inputFormat, getStreamFormats()); if (codecOperatingRate <= assumedMinimumCodecOperatingRate) { codecOperatingRate = CODEC_OPERATING_RATE_UNSET; } @@ -982,10 +1099,29 @@ public abstract class MediaCodecRenderer extends BaseRenderer { codecInitializingTimestamp = SystemClock.elapsedRealtime(); TraceUtil.beginSection("createCodec:" + codecName); codec = MediaCodec.createByCodecName(codecName); - if (useMediaCodecInAsyncMode && Util.SDK_INT >= 21) { + if (mediaCodecOperationMode == OPERATION_MODE_ASYNCHRONOUS_PLAYBACK_THREAD + && Util.SDK_INT >= 21) { codecAdapter = new AsynchronousMediaCodecAdapter(codec); + } else if (mediaCodecOperationMode == OPERATION_MODE_ASYNCHRONOUS_DEDICATED_THREAD + && Util.SDK_INT >= 23) { + codecAdapter = new DedicatedThreadAsyncMediaCodecAdapter(codec, getTrackType()); + } else if (mediaCodecOperationMode == OPERATION_MODE_ASYNCHRONOUS_DEDICATED_THREAD_MULTI_LOCK + && Util.SDK_INT >= 23) { + codecAdapter = new MultiLockAsyncMediaCodecAdapter(codec, getTrackType()); + } else if (mediaCodecOperationMode + == OPERATION_MODE_ASYNCHRONOUS_DEDICATED_THREAD_ASYNCHRONOUS_QUEUEING + && Util.SDK_INT >= 23) { + codecAdapter = + new DedicatedThreadAsyncMediaCodecAdapter( + codec, /* enableAsynchronousQueueing= */ true, getTrackType()); + } else if (mediaCodecOperationMode + == OPERATION_MODE_ASYNCHRONOUS_DEDICATED_THREAD_MULTI_LOCK_ASYNCHRONOUS_QUEUEING + && Util.SDK_INT >= 23) { + codecAdapter = + new MultiLockAsyncMediaCodecAdapter( + codec, /* enableAsynchronousQueueing= */ true, getTrackType()); } else { - codecAdapter = new SynchronousMediaCodecAdapter(codec, getDequeueOutputBufferTimeoutUs()); + codecAdapter = new SynchronousMediaCodecAdapter(codec); } TraceUtil.endSection(); @@ -993,7 +1129,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { configureCodec(codecInfo, codec, inputFormat, crypto, codecOperatingRate); TraceUtil.endSection(); TraceUtil.beginSection("startCodec"); - codec.start(); + codecAdapter.start(); TraceUtil.endSection(); codecInitializedTimestamp = SystemClock.elapsedRealtime(); getCodecBuffers(codec); @@ -1017,39 +1153,25 @@ public abstract class MediaCodecRenderer extends BaseRenderer { codecNeedsReconfigureWorkaround = codecNeedsReconfigureWorkaround(codecName); codecNeedsDiscardToSpsWorkaround = codecNeedsDiscardToSpsWorkaround(codecName, codecFormat); codecNeedsFlushWorkaround = codecNeedsFlushWorkaround(codecName); + codecNeedsSosFlushWorkaround = codecNeedsSosFlushWorkaround(codecName); codecNeedsEosFlushWorkaround = codecNeedsEosFlushWorkaround(codecName); codecNeedsEosOutputExceptionWorkaround = codecNeedsEosOutputExceptionWorkaround(codecName); codecNeedsMonoChannelCountWorkaround = codecNeedsMonoChannelCountWorkaround(codecName, codecFormat); codecNeedsEosPropagation = codecNeedsEosPropagationWorkaround(codecInfo) || getCodecNeedsEosPropagation(); - - resetInputBuffer(); - resetOutputBuffer(); - codecHotswapDeadlineMs = - getState() == STATE_STARTED - ? (SystemClock.elapsedRealtime() + MAX_CODEC_HOTSWAP_TIME_MS) - : C.TIME_UNSET; - codecReconfigured = false; - codecReconfigurationState = RECONFIGURATION_STATE_NONE; - codecReceivedEos = false; - codecReceivedBuffers = false; - codecDrainState = DRAIN_STATE_NONE; - codecDrainAction = DRAIN_ACTION_NONE; - codecNeedsAdaptationWorkaroundBuffer = false; - shouldSkipAdaptationWorkaroundOutputBuffer = false; - isDecodeOnlyOutputBuffer = false; - isLastOutputBuffer = false; - waitingForFirstSyncSample = true; + if (getState() == STATE_STARTED) { + codecHotswapDeadlineMs = SystemClock.elapsedRealtime() + MAX_CODEC_HOTSWAP_TIME_MS; + } decoderCounters.decoderInitCount++; long elapsed = codecInitializedTimestamp - codecInitializingTimestamp; onCodecInitialized(codecName, codecInitializedTimestamp, elapsed); } - private boolean shouldContinueFeeding(long drainStartTimeMs) { + private boolean shouldContinueRendering(long renderStartTimeMs) { return renderTimeLimitMs == C.TIME_UNSET - || SystemClock.elapsedRealtime() - drainStartTimeMs < renderTimeLimitMs; + || SystemClock.elapsedRealtime() - renderStartTimeMs < renderTimeLimitMs; } private void getCodecBuffers(MediaCodec codec) { @@ -1096,12 +1218,12 @@ public abstract class MediaCodecRenderer extends BaseRenderer { outputBuffer = null; } - private void setSourceDrmSession(@Nullable DrmSession session) { + private void setSourceDrmSession(@Nullable DrmSession session) { DrmSession.replaceSession(sourceDrmSession, session); sourceDrmSession = session; } - private void setCodecDrmSession(@Nullable DrmSession session) { + private void setCodecDrmSession(@Nullable DrmSession session) { DrmSession.replaceSession(codecDrmSession, session); codecDrmSession = session; } @@ -1131,7 +1253,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { // Do nothing. } else { codecReceivedEos = true; - codec.queueInputBuffer(inputIndex, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM); + codecAdapter.queueInputBuffer(inputIndex, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM); resetInputBuffer(); } codecDrainState = DRAIN_STATE_WAIT_END_OF_STREAM; @@ -1141,13 +1263,13 @@ public abstract class MediaCodecRenderer extends BaseRenderer { if (codecNeedsAdaptationWorkaroundBuffer) { codecNeedsAdaptationWorkaroundBuffer = false; buffer.data.put(ADAPTATION_WORKAROUND_BUFFER); - codec.queueInputBuffer(inputIndex, 0, ADAPTATION_WORKAROUND_BUFFER.length, 0, 0); + codecAdapter.queueInputBuffer(inputIndex, 0, ADAPTATION_WORKAROUND_BUFFER.length, 0, 0); resetInputBuffer(); codecReceivedBuffers = true; return true; } - int result; + @SampleStream.ReadDataResult int result; FormatHolder formatHolder = getFormatHolder(); int adaptiveReconfigurationBytes = 0; if (waitingForKeys) { @@ -1205,11 +1327,11 @@ public abstract class MediaCodecRenderer extends BaseRenderer { // Do nothing. } else { codecReceivedEos = true; - codec.queueInputBuffer(inputIndex, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM); + codecAdapter.queueInputBuffer(inputIndex, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM); resetInputBuffer(); } } catch (CryptoException e) { - throw ExoPlaybackException.createForRenderer(e, getIndex()); + throw createRendererException(e, inputFormat); } return false; } @@ -1254,31 +1376,30 @@ public abstract class MediaCodecRenderer extends BaseRenderer { onQueueInputBuffer(buffer); if (bufferEncrypted) { - MediaCodec.CryptoInfo cryptoInfo = getFrameworkCryptoInfo(buffer, - adaptiveReconfigurationBytes); - codec.queueSecureInputBuffer(inputIndex, 0, cryptoInfo, presentationTimeUs, 0); + CryptoInfo cryptoInfo = buffer.cryptoInfo; + cryptoInfo.increaseClearDataFirstSubSampleBy(adaptiveReconfigurationBytes); + codecAdapter.queueSecureInputBuffer(inputIndex, 0, cryptoInfo, presentationTimeUs, 0); } else { - codec.queueInputBuffer(inputIndex, 0, buffer.data.limit(), presentationTimeUs, 0); + codecAdapter.queueInputBuffer(inputIndex, 0, buffer.data.limit(), presentationTimeUs, 0); } resetInputBuffer(); codecReceivedBuffers = true; codecReconfigurationState = RECONFIGURATION_STATE_NONE; decoderCounters.inputBufferCount++; } catch (CryptoException e) { - throw ExoPlaybackException.createForRenderer(e, getIndex()); + throw createRendererException(e, inputFormat); } return true; } private boolean shouldWaitForKeys(boolean bufferEncrypted) throws ExoPlaybackException { if (codecDrmSession == null - || (!bufferEncrypted - && (playClearSamplesWithoutKeys || codecDrmSession.playClearSamplesWithoutKeys()))) { + || (!bufferEncrypted && codecDrmSession.playClearSamplesWithoutKeys())) { return false; } @DrmSession.State int drmSessionState = codecDrmSession.getState(); if (drmSessionState == DrmSession.STATE_ERROR) { - throw ExoPlaybackException.createForRenderer(codecDrmSession.getError(), getIndex()); + throw createRendererException(codecDrmSession.getError(), inputFormat); } return drmSessionState != DrmSession.STATE_OPENED_WITH_KEYS; } @@ -1304,29 +1425,31 @@ public abstract class MediaCodecRenderer extends BaseRenderer { * @param formatHolder A {@link FormatHolder} that holds the new {@link Format}. * @throws ExoPlaybackException If an error occurs re-initializing the {@link MediaCodec}. */ - @SuppressWarnings("unchecked") protected void onInputFormatChanged(FormatHolder formatHolder) throws ExoPlaybackException { waitingForFirstSampleInFormat = true; Format newFormat = Assertions.checkNotNull(formatHolder.format); - if (formatHolder.includesDrmSession) { - setSourceDrmSession((DrmSession) formatHolder.drmSession); - } else { - sourceDrmSession = - getUpdatedSourceDrmSession(inputFormat, newFormat, drmSessionManager, sourceDrmSession); - } + setSourceDrmSession(formatHolder.drmSession); inputFormat = newFormat; + if (passthroughEnabled) { + passthroughDrainAndReinitialize = true; + return; // Need to drain passthrough first. + } + if (codec == null) { - maybeInitCodec(); + maybeInitCodecOrPassthrough(); return; } - // We have an existing codec that we may need to reconfigure or re-initialize. If the existing - // codec instance is being kept then its operating rate may need to be updated. + // We have an existing codec that we may need to reconfigure or re-initialize or release it to + // switch to passthrough. If the existing codec instance is being kept then its operating rate + // may need to be updated. if ((sourceDrmSession == null && codecDrmSession != null) || (sourceDrmSession != null && codecDrmSession == null) - || (sourceDrmSession != null && !codecInfo.secure) + || (sourceDrmSession != codecDrmSession + && !codecInfo.secure + && maybeRequiresSecureDecoder(sourceDrmSession, newFormat)) || (Util.SDK_INT < 23 && sourceDrmSession != codecDrmSession)) { // We might need to switch between the clear and protected output paths, or we're using DRM // prior to API level 23 where the codec needs to be re-initialized to switch to the new DRM @@ -1392,6 +1515,41 @@ public abstract class MediaCodecRenderer extends BaseRenderer { // Do nothing. } + /** + * Called when the output {@link Format} changes. + * + *

      The default implementation is a no-op. + * + * @param outputFormat The new output {@link Format}. + */ + protected void onOutputFormatChanged(Format outputFormat) { + // Do nothing. + } + + /** + * Configures the renderer output based on a {@link Format}. + * + *

      The default implementation is a no-op. + * + * @param outputFormat The format to configure the output with. + */ + protected void configureOutput(Format outputFormat) { + // Do nothing. + } + + /** + * Called when the output {@link Format} changes in passthrough. + * + *

      The default implementation is a no-op. + * + * @param outputFormat The new output {@link MediaFormat}. + * @throws ExoPlaybackException Thrown if an error occurs handling the new output media format. + */ + // TODO(b/154849417): merge with {@link #onOutputFormatChanged(Format)}. + protected void onOutputPassthroughFormatChanged(Format outputFormat) throws ExoPlaybackException { + // Do nothing. + } + /** * Handles supplemental data associated with an input buffer. * @@ -1418,12 +1576,33 @@ public abstract class MediaCodecRenderer extends BaseRenderer { /** * Called when an output buffer is successfully processed. - *

      - * The default implementation is a no-op. * * @param presentationTimeUs The timestamp associated with the output buffer. */ + @CallSuper protected void onProcessedOutputBuffer(long presentationTimeUs) { + while (pendingOutputStreamOffsetCount != 0 + && presentationTimeUs >= pendingOutputStreamSwitchTimesUs[0]) { + outputStreamOffsetUs = pendingOutputStreamOffsetsUs[0]; + pendingOutputStreamOffsetCount--; + System.arraycopy( + pendingOutputStreamOffsetsUs, + /* srcPos= */ 1, + pendingOutputStreamOffsetsUs, + /* destPos= */ 0, + pendingOutputStreamOffsetCount); + System.arraycopy( + pendingOutputStreamSwitchTimesUs, + /* srcPos= */ 1, + pendingOutputStreamSwitchTimesUs, + /* destPos= */ 0, + pendingOutputStreamOffsetCount); + onProcessedStreamChange(); + } + } + + /** Called after the last output buffer before a stream change has been processed. */ + protected void onProcessedStreamChange() { // Do nothing. } @@ -1459,13 +1638,9 @@ public abstract class MediaCodecRenderer extends BaseRenderer { && SystemClock.elapsedRealtime() < codecHotswapDeadlineMs)); } - /** - * Returns the maximum time to block whilst waiting for a decoded output buffer. - * - * @return The maximum time to block, in microseconds. - */ - protected long getDequeueOutputBufferTimeoutUs() { - return 0; + /** Returns the renderer operating rate, as set by {@link #setOperatingRate}. */ + protected float getOperatingRate() { + return operatingRate; } /** @@ -1496,7 +1671,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { } float newCodecOperatingRate = - getCodecOperatingRateV23(rendererOperatingRate, codecFormat, getStreamFormats()); + getCodecOperatingRateV23(operatingRate, codecFormat, getStreamFormats()); if (codecOperatingRate == newCodecOperatingRate) { // No change. } else if (newCodecOperatingRate == CODEC_OPERATING_RATE_UNSET) { @@ -1585,6 +1760,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { if (outputIndex < 0) { if (outputIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED /* (-2) */) { processOutputMediaFormat(); + receivedOutputMediaFormatChange = true; return true; } else if (outputIndex == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED /* (-3) */) { processOutputBuffersChanged(); @@ -1635,6 +1811,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { outputBuffer, outputIndex, outputBufferInfo.flags, + /* sampleCount= */ 1, outputBufferInfo.presentationTimeUs, isDecodeOnlyOutputBuffer, isLastOutputBuffer, @@ -1656,6 +1833,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { outputBuffer, outputIndex, outputBufferInfo.flags, + /* sampleCount= */ 1, outputBufferInfo.presentationTimeUs, isDecodeOnlyOutputBuffer, isLastOutputBuffer, @@ -1677,6 +1855,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { /** Processes a new output {@link MediaFormat}. */ private void processOutputMediaFormat() throws ExoPlaybackException { + codecHasOutputMediaFormat = true; MediaFormat mediaFormat = codecAdapter.getOutputFormat(); if (codecAdaptationWorkaroundMode != ADAPTATION_WORKAROUND_MODE_NEVER && mediaFormat.getInteger(MediaFormat.KEY_WIDTH) == ADAPTATION_WORKAROUND_SLICE_WIDTH_HEIGHT @@ -1719,10 +1898,12 @@ public abstract class MediaCodecRenderer extends BaseRenderer { * iteration of the rendering loop. * @param elapsedRealtimeUs {@link SystemClock#elapsedRealtime()} in microseconds, measured at the * start of the current iteration of the rendering loop. - * @param codec The {@link MediaCodec} instance. + * @param codec The {@link MediaCodec} instance, or null in passthrough mode. * @param buffer The output buffer to process. * @param bufferIndex The index of the output buffer. * @param bufferFlags The flags attached to the output buffer. + * @param sampleCount The number of samples extracted from the sample queue in the buffer. This + * allows handling multiple samples as a batch for efficiency. * @param bufferPresentationTimeUs The presentation time of the output buffer in microseconds. * @param isDecodeOnlyBuffer Whether the buffer was marked with {@link C#BUFFER_FLAG_DECODE_ONLY} * by the source. @@ -1734,10 +1915,11 @@ public abstract class MediaCodecRenderer extends BaseRenderer { protected abstract boolean processOutputBuffer( long positionUs, long elapsedRealtimeUs, - MediaCodec codec, + @Nullable MediaCodec codec, ByteBuffer buffer, int bufferIndex, int bufferFlags, + int sampleCount, long bufferPresentationTimeUs, boolean isDecodeOnlyBuffer, boolean isLastBuffer, @@ -1760,6 +1942,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { * * @throws ExoPlaybackException If an error occurs processing the signal. */ + @TargetApi(23) // codecDrainAction == DRAIN_ACTION_UPDATE_DRM_SESSION implies SDK_INT >= 23. private void processEndOfStream() throws ExoPlaybackException { switch (codecDrainAction) { case DRAIN_ACTION_REINITIALIZE: @@ -1787,14 +1970,82 @@ public abstract class MediaCodecRenderer extends BaseRenderer { pendingOutputEndOfStream = true; } - private void reinitializeCodec() throws ExoPlaybackException { - releaseCodec(); - maybeInitCodec(); + /** Returns the largest queued input presentation time, in microseconds. */ + protected final long getLargestQueuedPresentationTimeUs() { + return largestQueuedPresentationTimeUs; } - @TargetApi(23) + /** + * Returns the offset that should be subtracted from {@code bufferPresentationTimeUs} in {@link + * #processOutputBuffer(long, long, MediaCodec, ByteBuffer, int, int, int, long, boolean, boolean, + * Format)} to get the playback position with respect to the media. + */ + protected final long getOutputStreamOffsetUs() { + return outputStreamOffsetUs; + } + + /** Returns whether this renderer supports the given {@link Format Format's} DRM scheme. */ + protected static boolean supportsFormatDrm(Format format) { + return format.drmInitData == null + || FrameworkMediaCrypto.class.equals(format.exoMediaCryptoType); + } + + /** + * Returns whether a {@link DrmSession} may require a secure decoder for a given {@link Format}. + * + * @param drmSession The {@link DrmSession}. + * @param format The {@link Format}. + * @return Whether a secure decoder may be required. + */ + private boolean maybeRequiresSecureDecoder(DrmSession drmSession, Format format) + throws ExoPlaybackException { + // MediaCrypto type is checked during track selection. + @Nullable FrameworkMediaCrypto sessionMediaCrypto = getFrameworkMediaCrypto(drmSession); + if (sessionMediaCrypto == null) { + // We'd only expect this to happen if the CDM from which the pending session is obtained needs + // provisioning. This is unlikely to happen (it probably requires a switch from one DRM scheme + // to another, where the new CDM hasn't been used before and needs provisioning). Assume that + // a secure decoder may be required. + return true; + } + if (sessionMediaCrypto.forceAllowInsecureDecoderComponents) { + return false; + } + MediaCrypto mediaCrypto; + try { + mediaCrypto = new MediaCrypto(sessionMediaCrypto.uuid, sessionMediaCrypto.sessionId); + } catch (MediaCryptoException e) { + // This shouldn't happen, but if it does then assume that a secure decoder may be required. + return true; + } + try { + return mediaCrypto.requiresSecureDecoderComponent(format.sampleMimeType); + } finally { + mediaCrypto.release(); + } + } + + private void reinitializeCodec() throws ExoPlaybackException { + releaseCodec(); + maybeInitCodecOrPassthrough(); + } + + private boolean isDecodeOnlyBuffer(long presentationTimeUs) { + // We avoid using decodeOnlyPresentationTimestamps.remove(presentationTimeUs) because it would + // box presentationTimeUs, creating a Long object that would need to be garbage collected. + int size = decodeOnlyPresentationTimestamps.size(); + for (int i = 0; i < size; i++) { + if (decodeOnlyPresentationTimestamps.get(i) == presentationTimeUs) { + decodeOnlyPresentationTimestamps.remove(i); + return true; + } + } + return false; + } + + @RequiresApi(23) private void updateDrmSessionOrReinitializeCodecV23() throws ExoPlaybackException { - FrameworkMediaCrypto sessionMediaCrypto = sourceDrmSession.getMediaCrypto(); + @Nullable FrameworkMediaCrypto sessionMediaCrypto = getFrameworkMediaCrypto(sourceDrmSession); if (sessionMediaCrypto == null) { // We'd only expect this to happen if the CDM from which the pending session is obtained needs // provisioning. This is unlikely to happen (it probably requires a switch from one DRM scheme @@ -1821,42 +2072,137 @@ public abstract class MediaCodecRenderer extends BaseRenderer { try { mediaCrypto.setMediaDrmSession(sessionMediaCrypto.sessionId); } catch (MediaCryptoException e) { - throw ExoPlaybackException.createForRenderer(e, getIndex()); + throw createRendererException(e, inputFormat); } setCodecDrmSession(sourceDrmSession); codecDrainState = DRAIN_STATE_NONE; codecDrainAction = DRAIN_ACTION_NONE; } - private boolean isDecodeOnlyBuffer(long presentationTimeUs) { - // We avoid using decodeOnlyPresentationTimestamps.remove(presentationTimeUs) because it would - // box presentationTimeUs, creating a Long object that would need to be garbage collected. - int size = decodeOnlyPresentationTimestamps.size(); - for (int i = 0; i < size; i++) { - if (decodeOnlyPresentationTimestamps.get(i) == presentationTimeUs) { - decodeOnlyPresentationTimestamps.remove(i); - return true; + @Nullable + private FrameworkMediaCrypto getFrameworkMediaCrypto(DrmSession drmSession) + throws ExoPlaybackException { + @Nullable ExoMediaCrypto mediaCrypto = drmSession.getMediaCrypto(); + if (mediaCrypto != null && !(mediaCrypto instanceof FrameworkMediaCrypto)) { + // This should not happen if the track went through a supportsFormatDrm() check, during track + // selection. + throw createRendererException( + new IllegalArgumentException("Expecting FrameworkMediaCrypto but found: " + mediaCrypto), + inputFormat); + } + return (FrameworkMediaCrypto) mediaCrypto; + } + + /** + * Processes any pending batch of buffers without using a decoder, and drains a new batch of + * buffers from the source. + * + * @param positionUs The current media time in microseconds, measured at the start of the current + * iteration of the rendering loop. + * @param elapsedRealtimeUs {@link SystemClock#elapsedRealtime()} in microseconds, measured at the + * start of the current iteration of the rendering loop. + * @return If more buffers are ready to be rendered. + * @throws ExoPlaybackException If an error occurred while processing a buffer or handling a + * format change. + */ + private boolean renderPassthrough(long positionUs, long elapsedRealtimeUs) + throws ExoPlaybackException { + BatchBuffer batchBuffer = passthroughBatchBuffer; + + // Let's process the pending buffer if any. + Assertions.checkState(!outputStreamEnded); + if (!batchBuffer.isEmpty()) { // Optimisation: Do not process buffer if empty. + if (processOutputBuffer( + positionUs, + elapsedRealtimeUs, + /* codec= */ null, + batchBuffer.data, + outputIndex, + /* bufferFlags= */ 0, + batchBuffer.getAccessUnitCount(), + batchBuffer.getFirstAccessUnitTimeUs(), + batchBuffer.isDecodeOnly(), + batchBuffer.isEndOfStream(), + outputFormat)) { + // Buffer completely processed + onProcessedOutputBuffer(batchBuffer.getLastAccessUnitTimeUs()); + } else { + return false; // Could not process buffer, let's try later. + } + } + if (batchBuffer.isEndOfStream()) { + outputStreamEnded = true; + return false; + } + batchBuffer.batchWasConsumed(); + + if (passthroughDrainAndReinitialize) { + if (!batchBuffer.isEmpty()) { + return true; // Drain the batch buffer before propagating the format change. + } + disablePassthrough(); // The new format might not be supported in passthrough. + passthroughDrainAndReinitialize = false; + maybeInitCodecOrPassthrough(); + if (!passthroughEnabled) { + return false; // The new format is not supported in passthrough. + } + } + + // Now refill the empty buffer for the next iteration. + Assertions.checkState(!inputStreamEnded); + FormatHolder formatHolder = getFormatHolder(); + boolean formatChange = readBatchFromSource(formatHolder, batchBuffer); + + if (!batchBuffer.isEmpty() && waitingForFirstSampleInFormat) { + // This is the first buffer in a new format, the output format must be updated. + outputFormat = Assertions.checkNotNull(inputFormat); + onOutputPassthroughFormatChanged(outputFormat); + waitingForFirstSampleInFormat = false; + } + + if (formatChange) { + onInputFormatChanged(formatHolder); + } + + if (batchBuffer.isEndOfStream()) { + inputStreamEnded = true; + } + + if (batchBuffer.isEmpty()) { + return false; // The buffer could not be filled, there is nothing more to do. + } + batchBuffer.flip(); // Buffer at least partially full, it can now be processed. + return true; + } + + /** + * Fills the buffer with multiple access unit from the source. Has otherwise the same semantic as + * {@link #readSource(FormatHolder, DecoderInputBuffer, boolean)}. Will stop early on format + * change, EOS or source starvation. + * + * @return If the format has changed. + */ + private boolean readBatchFromSource(FormatHolder formatHolder, BatchBuffer batchBuffer) { + while (!batchBuffer.isFull() && !batchBuffer.isEndOfStream()) { + @SampleStream.ReadDataResult + int result = + readSource( + formatHolder, batchBuffer.getNextAccessUnitBuffer(), /* formatRequired= */ false); + switch (result) { + case C.RESULT_FORMAT_READ: + return true; + case C.RESULT_NOTHING_READ: + return false; + case C.RESULT_BUFFER_READ: + batchBuffer.commitNextAccessUnit(); + break; + default: + throw new IllegalStateException(); // Unsupported result } } return false; } - private static MediaCodec.CryptoInfo getFrameworkCryptoInfo( - DecoderInputBuffer buffer, int adaptiveReconfigurationBytes) { - MediaCodec.CryptoInfo cryptoInfo = buffer.cryptoInfo.getFrameworkCryptoInfo(); - 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 static boolean isMediaCodecException(IllegalStateException error) { if (Util.SDK_INT >= 21 && isMediaCodecExceptionV21(error)) { return true; @@ -1865,7 +2211,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { return stackTrace.length > 0 && stackTrace[0].getClassName().equals("android.media.MediaCodec"); } - @TargetApi(21) + @RequiresApi(21) private static boolean isMediaCodecExceptionV21(IllegalStateException error) { return error instanceof MediaCodec.CodecException; } @@ -2022,123 +2368,20 @@ public abstract class MediaCodecRenderer extends BaseRenderer { && "OMX.MTK.AUDIO.DECODER.MP3".equals(name); } - @RequiresApi(21) - private static class AsynchronousMediaCodecAdapter implements MediaCodecAdapter { - - private MediaCodecAsyncCallback mediaCodecAsyncCallback; - private final Handler handler; - private final MediaCodec codec; - @Nullable private IllegalStateException internalException; - private boolean flushing; - - public AsynchronousMediaCodecAdapter(MediaCodec codec) { - mediaCodecAsyncCallback = new MediaCodecAsyncCallback(); - handler = new Handler(Looper.myLooper()); - this.codec = codec; - this.codec.setCallback(mediaCodecAsyncCallback); - } - - @Override - public int dequeueInputBufferIndex() { - if (flushing) { - return MediaCodec.INFO_TRY_AGAIN_LATER; - } else { - maybeThrowException(); - return mediaCodecAsyncCallback.dequeueInputBufferIndex(); - } - } - - @Override - public int dequeueOutputBufferIndex(MediaCodec.BufferInfo bufferInfo) { - if (flushing) { - return MediaCodec.INFO_TRY_AGAIN_LATER; - } else { - maybeThrowException(); - return mediaCodecAsyncCallback.dequeueOutputBufferIndex(bufferInfo); - } - } - - @Override - public MediaFormat getOutputFormat() { - return mediaCodecAsyncCallback.getOutputFormat(); - } - - @Override - public void flush() { - clearPendingFlushState(); - flushing = true; - codec.flush(); - handler.post(this::onCompleteFlush); - } - - @Override - public void shutdown() { - clearPendingFlushState(); - } - - private void onCompleteFlush() { - flushing = false; - mediaCodecAsyncCallback.flush(); - try { - codec.start(); - } catch (IllegalStateException e) { - // Catch IllegalStateException directly so that we don't have to wrap it - internalException = e; - } catch (Exception e) { - internalException = new IllegalStateException(e); - } - } - - private void maybeThrowException() throws IllegalStateException { - maybeThrowInternalException(); - mediaCodecAsyncCallback.maybeThrowMediaCodecException(); - } - - private void maybeThrowInternalException() { - if (internalException != null) { - IllegalStateException e = internalException; - internalException = null; - throw e; - } - } - - /** Clear state related to pending flush events. */ - private void clearPendingFlushState() { - handler.removeCallbacksAndMessages(null); - internalException = null; - } - } - - private static class SynchronousMediaCodecAdapter implements MediaCodecAdapter { - private final MediaCodec codec; - private final long dequeueOutputBufferTimeoutMs; - - public SynchronousMediaCodecAdapter(MediaCodec mediaCodec, long dequeueOutputBufferTimeoutMs) { - this.codec = mediaCodec; - this.dequeueOutputBufferTimeoutMs = dequeueOutputBufferTimeoutMs; - } - - @Override - public int dequeueInputBufferIndex() { - return codec.dequeueInputBuffer(0); - } - - @Override - public int dequeueOutputBufferIndex(MediaCodec.BufferInfo bufferInfo) { - return codec.dequeueOutputBuffer(bufferInfo, dequeueOutputBufferTimeoutMs); - } - - @Override - public MediaFormat getOutputFormat() { - return codec.getOutputFormat(); - } - - @Override - public void flush() { - codec.flush(); - } - - @Override - public void shutdown() {} + /** + * Returns whether the decoder is known to behave incorrectly if flushed prior to having output a + * {@link MediaFormat}. + * + *

      If true is returned, the renderer will work around the issue by instantiating a new decoder + * when this case occurs. + * + *

      See [Internal: b/141097367]. + * + * @param name The name of the decoder. + * @return True if the decoder is known to behave incorrectly if flushed prior to having output a + * {@link MediaFormat}. False otherwise. + */ + private static boolean codecNeedsSosFlushWorkaround(String name) { + return Util.SDK_INT == 29 && "c2.android.aac.decoder".equals(name); } } 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 10ff81147e..7f39dced61 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,6 @@ package com.google.android.exoplayer2.mediacodec; import android.media.MediaCodec; -import androidx.annotation.Nullable; import com.google.android.exoplayer2.mediacodec.MediaCodecUtil.DecoderQueryException; import java.util.List; @@ -29,22 +28,7 @@ public interface MediaCodecSelector { * Default implementation of {@link MediaCodecSelector}, which returns the preferred decoder for * the given format. */ - MediaCodecSelector DEFAULT = - new MediaCodecSelector() { - @Override - public List getDecoderInfos( - String mimeType, boolean requiresSecureDecoder, boolean requiresTunnelingDecoder) - throws DecoderQueryException { - return MediaCodecUtil.getDecoderInfos( - mimeType, requiresSecureDecoder, requiresTunnelingDecoder); - } - - @Override - @Nullable - public MediaCodecInfo getPassthroughDecoderInfo() throws DecoderQueryException { - return MediaCodecUtil.getPassthroughDecoderInfo(); - } - }; + MediaCodecSelector DEFAULT = MediaCodecUtil::getDecoderInfos; /** * Returns a list of decoders that can decode media in the specified MIME type, in priority order. @@ -59,13 +43,4 @@ public interface MediaCodecSelector { List getDecoderInfos( String mimeType, boolean requiresSecureDecoder, boolean requiresTunnelingDecoder) throws DecoderQueryException; - - /** - * Selects a decoder to instantiate for audio passthrough. - * - * @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 9adb6bc7bc..db68fb3e89 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 @@ -16,7 +16,6 @@ package com.google.android.exoplayer2.mediacodec; import android.annotation.SuppressLint; -import android.annotation.TargetApi; import android.media.MediaCodecInfo.CodecCapabilities; import android.media.MediaCodecInfo.CodecProfileLevel; import android.media.MediaCodecList; @@ -25,6 +24,7 @@ import android.util.Pair; import android.util.SparseIntArray; import androidx.annotation.CheckResult; import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.util.Log; @@ -123,10 +123,7 @@ public final class MediaCodecUtil { */ @Nullable public static MediaCodecInfo getPassthroughDecoderInfo() throws DecoderQueryException { - @Nullable - MediaCodecInfo decoderInfo = - getDecoderInfo(MimeTypes.AUDIO_RAW, /* secure= */ false, /* tunneling= */ false); - return decoderInfo == null ? null : MediaCodecInfo.newPassthroughInstance(decoderInfo.name); + return getDecoderInfo(MimeTypes.AUDIO_RAW, /* secure= */ false, /* tunneling= */ false); } /** @@ -289,9 +286,16 @@ public final class MediaCodecUtil { // Note: MediaCodecList is sorted by the framework such that the best decoders come first. for (int i = 0; i < numberOfCodecs; i++) { android.media.MediaCodecInfo codecInfo = mediaCodecList.getCodecInfoAt(i); + if (isAlias(codecInfo)) { + // Skip aliases of other codecs, since they will also be listed under their canonical + // names. + continue; + } String name = codecInfo.getName(); - @Nullable - String codecMimeType = getCodecMimeType(codecInfo, name, secureDecodersExplicit, mimeType); + if (!isCodecUsableDecoder(codecInfo, name, secureDecodersExplicit, mimeType)) { + continue; + } + @Nullable String codecMimeType = getCodecMimeType(codecInfo, name, mimeType); if (codecMimeType == null) { continue; } @@ -373,7 +377,6 @@ public final class MediaCodecUtil { * * @param info The codec information. * @param name The name of the codec - * @param secureDecodersExplicit Whether secure decoders were explicitly listed, if present. * @param mimeType The MIME type. * @return The codec's supported MIME type for media of type {@code mimeType}, or {@code null} if * the codec can't be used. If non-null, the returned type will be equal to {@code mimeType} @@ -383,12 +386,7 @@ public final class MediaCodecUtil { private static String getCodecMimeType( android.media.MediaCodecInfo info, String name, - boolean secureDecodersExplicit, String mimeType) { - if (!isCodecUsableDecoder(info, name, secureDecodersExplicit, mimeType)) { - return null; - } - String[] supportedTypes = info.getSupportedTypes(); for (String supportedType : supportedTypes) { if (supportedType.equalsIgnoreCase(mimeType)) { @@ -566,7 +564,9 @@ public final class MediaCodecUtil { } return 0; }); - } else if (Util.SDK_INT < 21 && decoderInfos.size() > 1) { + } + + if (Util.SDK_INT < 21 && decoderInfos.size() > 1) { String firstCodecName = decoderInfos.get(0).name; if ("OMX.SEC.mp3.dec".equals(firstCodecName) || "OMX.SEC.MP3.Decoder".equals(firstCodecName) @@ -578,6 +578,24 @@ public final class MediaCodecUtil { sortByScore(decoderInfos, decoderInfo -> decoderInfo.name.startsWith("OMX.google") ? 1 : 0); } } + + if (Util.SDK_INT < 30 && decoderInfos.size() > 1) { + String firstCodecName = decoderInfos.get(0).name; + // Prefer anything other than OMX.qti.audio.decoder.flac on older devices. See [Internal + // ref: b/147278539] and [Internal ref: b/147354613]. + if ("OMX.qti.audio.decoder.flac".equals(firstCodecName)) { + decoderInfos.add(decoderInfos.remove(0)); + } + } + } + + private static boolean isAlias(android.media.MediaCodecInfo info) { + return Util.SDK_INT >= 29 && isAliasV29(info); + } + + @RequiresApi(29) + private static boolean isAliasV29(android.media.MediaCodecInfo info) { + return info.isAlias(); } /** @@ -593,7 +611,7 @@ public final class MediaCodecUtil { return !isSoftwareOnly(codecInfo); } - @TargetApi(29) + @RequiresApi(29) private static boolean isHardwareAcceleratedV29(android.media.MediaCodecInfo codecInfo) { return codecInfo.isHardwareAccelerated(); } @@ -619,7 +637,7 @@ public final class MediaCodecUtil { || (!codecName.startsWith("omx.") && !codecName.startsWith("c2.")); } - @TargetApi(29) + @RequiresApi(29) private static boolean isSoftwareOnlyV29(android.media.MediaCodecInfo codecInfo) { return codecInfo.isSoftwareOnly(); } @@ -638,7 +656,7 @@ public final class MediaCodecUtil { && !codecName.startsWith("c2.google."); } - @TargetApi(29) + @RequiresApi(29) private static boolean isVendorV29(android.media.MediaCodecInfo codecInfo) { return codecInfo.isVendor(); } @@ -936,15 +954,13 @@ public final class MediaCodecUtil { boolean isFeatureRequired(String feature, String mimeType, CodecCapabilities capabilities); } - @TargetApi(21) + @RequiresApi(21) private static final class MediaCodecListCompatV21 implements MediaCodecListCompat { private final int codecKind; @Nullable private android.media.MediaCodecInfo[] mediaCodecInfos; - // the constructor does not initialize fields: mediaCodecInfos - @SuppressWarnings("nullness:initialization.fields.uninitialized") public MediaCodecListCompatV21(boolean includeSecure, boolean includeTunneling) { codecKind = includeSecure || includeTunneling @@ -958,8 +974,6 @@ public final class MediaCodecUtil { return mediaCodecInfos.length; } - // incompatible types in return. - @SuppressWarnings("nullness:return.type.incompatible") @Override public android.media.MediaCodecInfo getCodecInfoAt(int index) { ensureMediaCodecInfosInitialized(); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MultiLockAsyncMediaCodecAdapter.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MultiLockAsyncMediaCodecAdapter.java new file mode 100644 index 0000000000..d51f985ed7 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MultiLockAsyncMediaCodecAdapter.java @@ -0,0 +1,385 @@ +/* + * Copyright (C) 2019 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.mediacodec; + +import android.media.MediaCodec; +import android.media.MediaFormat; +import android.os.Handler; +import android.os.HandlerThread; +import androidx.annotation.GuardedBy; +import androidx.annotation.IntDef; +import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; +import androidx.annotation.VisibleForTesting; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.decoder.CryptoInfo; +import com.google.android.exoplayer2.util.IntArrayQueue; +import com.google.android.exoplayer2.util.Util; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.ArrayDeque; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; + +/** + * A {@link MediaCodecAdapter} that operates the underlying {@link MediaCodec} in asynchronous mode + * and routes {@link MediaCodec.Callback} callbacks on a dedicated thread that is managed + * internally. + * + *

      The main difference of this class compared to the {@link + * DedicatedThreadAsyncMediaCodecAdapter} is that its internal implementation applies finer-grained + * locking. The {@link DedicatedThreadAsyncMediaCodecAdapter} uses a single lock to synchronize + * access, whereas this class uses a different lock to access the available input and available + * output buffer indexes returned from the {@link MediaCodec}. This class assumes that the {@link + * MediaCodecAdapter} methods will be accessed by the playback thread and the {@link + * MediaCodec.Callback} methods will be accessed by the internal thread. This class is + * NOT generally thread-safe in the sense that its public methods cannot be called + * by any thread. + */ +@RequiresApi(23) +/* package */ final class MultiLockAsyncMediaCodecAdapter extends MediaCodec.Callback + implements MediaCodecAdapter { + + @Documented + @Retention(RetentionPolicy.SOURCE) + @IntDef({STATE_CREATED, STATE_STARTED, STATE_SHUT_DOWN}) + private @interface State {} + + private static final int STATE_CREATED = 0; + private static final int STATE_STARTED = 1; + private static final int STATE_SHUT_DOWN = 2; + + private final MediaCodec codec; + private final Object inputBufferLock; + private final Object outputBufferLock; + private final Object objectStateLock; + + @GuardedBy("inputBufferLock") + private final IntArrayQueue availableInputBuffers; + + @GuardedBy("outputBufferLock") + private final IntArrayQueue availableOutputBuffers; + + @GuardedBy("outputBufferLock") + private final ArrayDeque bufferInfos; + + @GuardedBy("outputBufferLock") + private final ArrayDeque formats; + + @GuardedBy("objectStateLock") + private @MonotonicNonNull MediaFormat currentFormat; + + @GuardedBy("objectStateLock") + private long pendingFlush; + + @GuardedBy("objectStateLock") + @Nullable + private IllegalStateException codecException; + + private final HandlerThread handlerThread; + private @MonotonicNonNull Handler handler; + private Runnable codecStartRunnable; + private final MediaCodecInputBufferEnqueuer bufferEnqueuer; + + @GuardedBy("objectStateLock") + @State + private int state; + + /** + * Creates a new instance that wraps the specified {@link MediaCodec}. An instance created with + * this constructor will queue input buffers synchronously. + * + * @param codec The {@link MediaCodec} to wrap. + * @param trackType One of {@link C#TRACK_TYPE_AUDIO} or {@link C#TRACK_TYPE_VIDEO}. Used for + * labelling the internal thread accordingly. + */ + /* package */ MultiLockAsyncMediaCodecAdapter(MediaCodec codec, int trackType) { + this( + codec, + /* enableAsynchronousQueueing= */ false, + trackType, + new HandlerThread(createThreadLabel(trackType))); + } + + /** + * Creates a new instance that wraps the specified {@link MediaCodec}. + * + * @param codec The {@link MediaCodec} to wrap. + * @param enableAsynchronousQueueing Whether input buffers will be queued asynchronously. + * @param trackType One of {@link C#TRACK_TYPE_AUDIO} or {@link C#TRACK_TYPE_VIDEO}. Used for + * labelling the internal thread accordingly. + */ + /* package */ MultiLockAsyncMediaCodecAdapter( + MediaCodec codec, boolean enableAsynchronousQueueing, int trackType) { + this( + codec, + enableAsynchronousQueueing, + trackType, + new HandlerThread(createThreadLabel(trackType))); + } + + @VisibleForTesting + /* package */ MultiLockAsyncMediaCodecAdapter( + MediaCodec codec, + boolean enableAsynchronousQueueing, + int trackType, + HandlerThread handlerThread) { + this.codec = codec; + inputBufferLock = new Object(); + outputBufferLock = new Object(); + objectStateLock = new Object(); + availableInputBuffers = new IntArrayQueue(); + availableOutputBuffers = new IntArrayQueue(); + bufferInfos = new ArrayDeque<>(); + formats = new ArrayDeque<>(); + codecException = null; + this.handlerThread = handlerThread; + codecStartRunnable = codec::start; + if (enableAsynchronousQueueing) { + bufferEnqueuer = new AsynchronousMediaCodecBufferEnqueuer(codec, trackType); + } else { + bufferEnqueuer = new SynchronousMediaCodecBufferEnqueuer(codec); + } + state = STATE_CREATED; + } + + @Override + public void start() { + synchronized (objectStateLock) { + handlerThread.start(); + handler = new Handler(handlerThread.getLooper()); + codec.setCallback(this, handler); + bufferEnqueuer.start(); + codecStartRunnable.run(); + state = STATE_STARTED; + } + } + + @Override + public int dequeueInputBufferIndex() { + synchronized (objectStateLock) { + if (isFlushing()) { + return MediaCodec.INFO_TRY_AGAIN_LATER; + } else { + maybeThrowException(); + return dequeueAvailableInputBufferIndex(); + } + } + } + + @Override + public int dequeueOutputBufferIndex(MediaCodec.BufferInfo bufferInfo) { + synchronized (objectStateLock) { + if (isFlushing()) { + return MediaCodec.INFO_TRY_AGAIN_LATER; + } else { + maybeThrowException(); + return dequeueAvailableOutputBufferIndex(bufferInfo); + } + } + } + + @Override + public MediaFormat getOutputFormat() { + synchronized (objectStateLock) { + if (currentFormat == null) { + throw new IllegalStateException(); + } + + return currentFormat; + } + } + + @Override + public void queueInputBuffer( + int index, int offset, int size, long presentationTimeUs, int flags) { + // This method does not need to be synchronized because it is not interacting with + // MediaCodec.Callback and dequeueing buffers operations. + bufferEnqueuer.queueInputBuffer(index, offset, size, presentationTimeUs, flags); + } + + @Override + public void queueSecureInputBuffer( + int index, int offset, CryptoInfo info, long presentationTimeUs, int flags) { + // This method does not need to be synchronized because it is not interacting with + // MediaCodec.Callback and dequeueing buffers operations. + bufferEnqueuer.queueSecureInputBuffer(index, offset, info, presentationTimeUs, flags); + } + + @Override + public void flush() { + synchronized (objectStateLock) { + bufferEnqueuer.flush(); + codec.flush(); + pendingFlush++; + Util.castNonNull(handler).post(this::onFlushComplete); + } + } + + @Override + public void shutdown() { + synchronized (objectStateLock) { + if (state == STATE_STARTED) { + bufferEnqueuer.shutdown(); + handlerThread.quit(); + } + state = STATE_SHUT_DOWN; + } + } + + @VisibleForTesting + /* package */ void setCodecStartRunnable(Runnable codecStartRunnable) { + this.codecStartRunnable = codecStartRunnable; + } + + private int dequeueAvailableInputBufferIndex() { + synchronized (inputBufferLock) { + return availableInputBuffers.isEmpty() + ? MediaCodec.INFO_TRY_AGAIN_LATER + : availableInputBuffers.remove(); + } + } + + @GuardedBy("objectStateLock") + private int dequeueAvailableOutputBufferIndex(MediaCodec.BufferInfo bufferInfo) { + int bufferIndex; + synchronized (outputBufferLock) { + if (availableOutputBuffers.isEmpty()) { + bufferIndex = MediaCodec.INFO_TRY_AGAIN_LATER; + } else { + bufferIndex = availableOutputBuffers.remove(); + if (bufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) { + currentFormat = formats.remove(); + } else if (bufferIndex >= 0) { + MediaCodec.BufferInfo outBufferInfo = bufferInfos.remove(); + bufferInfo.set( + outBufferInfo.offset, + outBufferInfo.size, + outBufferInfo.presentationTimeUs, + outBufferInfo.flags); + } + } + } + return bufferIndex; + } + + @GuardedBy("objectStateLock") + private boolean isFlushing() { + return pendingFlush > 0; + } + + @GuardedBy("objectStateLock") + private void maybeThrowException() { + @Nullable IllegalStateException exception = codecException; + if (exception != null) { + codecException = null; + throw exception; + } + } + + // Called by the internal thread. + + @Override + public void onInputBufferAvailable(MediaCodec codec, int index) { + synchronized (inputBufferLock) { + availableInputBuffers.add(index); + } + } + + @Override + public void onOutputBufferAvailable(MediaCodec codec, int index, MediaCodec.BufferInfo info) { + synchronized (outputBufferLock) { + availableOutputBuffers.add(index); + bufferInfos.add(info); + } + } + + @Override + public void onError(MediaCodec codec, MediaCodec.CodecException e) { + onMediaCodecError(e); + } + + @Override + public void onOutputFormatChanged(MediaCodec codec, MediaFormat format) { + synchronized (outputBufferLock) { + availableOutputBuffers.add(MediaCodec.INFO_OUTPUT_FORMAT_CHANGED); + formats.add(format); + } + } + + @VisibleForTesting + /* package */ void onMediaCodecError(IllegalStateException e) { + synchronized (objectStateLock) { + codecException = e; + } + } + + private void onFlushComplete() { + synchronized (objectStateLock) { + if (state == STATE_SHUT_DOWN) { + return; + } + + --pendingFlush; + if (pendingFlush > 0) { + // Another flush() has been called. + return; + } else if (pendingFlush < 0) { + // This should never happen. + codecException = new IllegalStateException(); + return; + } + + clearAvailableInput(); + clearAvailableOutput(); + codecException = null; + try { + codecStartRunnable.run(); + } catch (IllegalStateException e) { + codecException = e; + } catch (Exception e) { + codecException = new IllegalStateException(e); + } + } + } + + private void clearAvailableInput() { + synchronized (inputBufferLock) { + availableInputBuffers.clear(); + } + } + + private void clearAvailableOutput() { + synchronized (outputBufferLock) { + availableOutputBuffers.clear(); + bufferInfos.clear(); + formats.clear(); + } + } + + private static String createThreadLabel(int trackType) { + StringBuilder labelBuilder = new StringBuilder("ExoPlayer:MediaCodecAsyncAdapter:"); + if (trackType == C.TRACK_TYPE_AUDIO) { + labelBuilder.append("Audio"); + } else if (trackType == C.TRACK_TYPE_VIDEO) { + labelBuilder.append("Video"); + } else { + labelBuilder.append("Unknown(").append(trackType).append(")"); + } + return labelBuilder.toString(); + } +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/SynchronousMediaCodecAdapter.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/SynchronousMediaCodecAdapter.java new file mode 100644 index 0000000000..f50b49e602 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/SynchronousMediaCodecAdapter.java @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2019 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.mediacodec; + +import android.media.MediaCodec; +import android.media.MediaFormat; +import com.google.android.exoplayer2.decoder.CryptoInfo; + +/** + * A {@link MediaCodecAdapter} that operates the underlying {@link MediaCodec} in synchronous mode. + */ +/* package */ final class SynchronousMediaCodecAdapter implements MediaCodecAdapter { + + private final MediaCodec codec; + + public SynchronousMediaCodecAdapter(MediaCodec mediaCodec) { + this.codec = mediaCodec; + } + + @Override + public void start() { + codec.start(); + } + + @Override + public int dequeueInputBufferIndex() { + return codec.dequeueInputBuffer(0); + } + + @Override + public int dequeueOutputBufferIndex(MediaCodec.BufferInfo bufferInfo) { + return codec.dequeueOutputBuffer(bufferInfo, 0); + } + + @Override + public MediaFormat getOutputFormat() { + return codec.getOutputFormat(); + } + + @Override + public void queueInputBuffer( + int index, int offset, int size, long presentationTimeUs, int flags) { + codec.queueInputBuffer(index, offset, size, presentationTimeUs, flags); + } + + @Override + public void queueSecureInputBuffer( + int index, int offset, CryptoInfo info, long presentationTimeUs, int flags) { + codec.queueSecureInputBuffer( + index, offset, info.getFrameworkCryptoInfo(), presentationTimeUs, flags); + } + + @Override + public void flush() { + codec.flush(); + } + + @Override + public void shutdown() {} +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/SynchronousMediaCodecBufferEnqueuer.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/SynchronousMediaCodecBufferEnqueuer.java new file mode 100644 index 0000000000..f16748f8fc --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/SynchronousMediaCodecBufferEnqueuer.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2020 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.mediacodec; + +import android.media.MediaCodec; +import com.google.android.exoplayer2.decoder.CryptoInfo; + +/** + * A {@link MediaCodecInputBufferEnqueuer} that forwards queueing methods directly to {@link + * MediaCodec}. + */ +class SynchronousMediaCodecBufferEnqueuer implements MediaCodecInputBufferEnqueuer { + private final MediaCodec codec; + + /** + * Creates an instance that queues input buffers on the specified {@link MediaCodec}. + * + * @param codec The {@link MediaCodec} to submit input buffers to. + */ + SynchronousMediaCodecBufferEnqueuer(MediaCodec codec) { + this.codec = codec; + } + + @Override + public void start() {} + + @Override + public void queueInputBuffer( + int index, int offset, int size, long presentationTimeUs, int flags) { + codec.queueInputBuffer(index, offset, size, presentationTimeUs, flags); + } + + @Override + public void queueSecureInputBuffer( + int index, int offset, CryptoInfo info, long presentationTimeUs, int flags) { + codec.queueSecureInputBuffer( + index, offset, info.getFrameworkCryptoInfo(), presentationTimeUs, flags); + } + + @Override + public void flush() {} + + @Override + public void shutdown() {} +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/MetadataDecoderFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/MetadataDecoderFactory.java index 0b653830a3..fae53a5d09 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/MetadataDecoderFactory.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/MetadataDecoderFactory.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.metadata; import androidx.annotation.Nullable; import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.metadata.dvbsi.AppInfoTableDecoder; import com.google.android.exoplayer2.metadata.emsg.EventMessageDecoder; import com.google.android.exoplayer2.metadata.icy.IcyDecoder; import com.google.android.exoplayer2.metadata.id3.Id3Decoder; @@ -67,7 +68,8 @@ public interface MetadataDecoderFactory { return MimeTypes.APPLICATION_ID3.equals(mimeType) || MimeTypes.APPLICATION_EMSG.equals(mimeType) || MimeTypes.APPLICATION_SCTE35.equals(mimeType) - || MimeTypes.APPLICATION_ICY.equals(mimeType); + || MimeTypes.APPLICATION_ICY.equals(mimeType) + || MimeTypes.APPLICATION_AIT.equals(mimeType); } @Override @@ -83,6 +85,8 @@ public interface MetadataDecoderFactory { return new SpliceInfoDecoder(); case MimeTypes.APPLICATION_ICY: return new IcyDecoder(); + case MimeTypes.APPLICATION_AIT: + return new AppInfoTableDecoder(); default: break; } 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 d738a8662e..238d515caf 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 @@ -15,6 +15,8 @@ */ package com.google.android.exoplayer2.metadata; +import static com.google.android.exoplayer2.util.Util.castNonNull; + import android.os.Handler; import android.os.Handler.Callback; import android.os.Looper; @@ -22,20 +24,23 @@ import android.os.Message; import androidx.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.RendererCapabilities; +import com.google.android.exoplayer2.source.SampleStream; 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.List; +import org.checkerframework.checker.nullness.compatqual.NullableType; /** * A renderer for metadata. */ public final class MetadataRenderer extends BaseRenderer implements Callback { + private static final String TAG = "MetadataRenderer"; private static final int MSG_INVOKE_RENDERER = 0; // TODO: Holding multiple pending metadata objects is temporary mitigation against // https://github.com/google/ExoPlayer/issues/1874. It should be removed once this issue has been @@ -46,12 +51,12 @@ public final class MetadataRenderer extends BaseRenderer implements Callback { private final MetadataOutput output; @Nullable private final Handler outputHandler; private final MetadataInputBuffer buffer; - private final Metadata[] pendingMetadata; + private final @NullableType Metadata[] pendingMetadata; private final long[] pendingMetadataTimestamps; private int pendingMetadataIndex; private int pendingMetadataCount; - private MetadataDecoder decoder; + @Nullable private MetadataDecoder decoder; private boolean inputStreamEnded; private long subsampleOffsetUs; @@ -89,16 +94,23 @@ public final class MetadataRenderer extends BaseRenderer implements Callback { } @Override + public String getName() { + return TAG; + } + + @Override + @Capabilities public int supportsFormat(Format format) { if (decoderFactory.supportsFormat(format)) { - return supportsFormatDrm(null, format.drmInitData) ? FORMAT_HANDLED : FORMAT_UNSUPPORTED_DRM; + return RendererCapabilities.create( + format.drmInitData == null ? FORMAT_HANDLED : FORMAT_UNSUPPORTED_DRM); } else { - return FORMAT_UNSUPPORTED_TYPE; + return RendererCapabilities.create(FORMAT_UNSUPPORTED_TYPE); } } @Override - protected void onStreamChanged(Format[] formats, long offsetUs) throws ExoPlaybackException { + protected void onStreamChanged(Format[] formats, long offsetUs) { decoder = decoderFactory.createDecoder(formats[0]); } @@ -109,11 +121,11 @@ public final class MetadataRenderer extends BaseRenderer implements Callback { } @Override - public void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException { + public void render(long positionUs, long elapsedRealtimeUs) { if (!inputStreamEnded && pendingMetadataCount < MAX_PENDING_METADATA_COUNT) { buffer.clear(); FormatHolder formatHolder = getFormatHolder(); - int result = readSource(formatHolder, buffer, false); + @SampleStream.ReadDataResult int result = readSource(formatHolder, buffer, false); if (result == C.RESULT_BUFFER_READ) { if (buffer.isEndOfStream()) { inputStreamEnded = true; @@ -124,7 +136,7 @@ public final class MetadataRenderer extends BaseRenderer implements Callback { } else { buffer.subsampleOffsetUs = subsampleOffsetUs; buffer.flip(); - Metadata metadata = decoder.decode(buffer); + @Nullable Metadata metadata = castNonNull(decoder).decode(buffer); if (metadata != null) { List entries = new ArrayList<>(metadata.length()); decodeWrappedMetadata(metadata, entries); @@ -139,12 +151,13 @@ public final class MetadataRenderer extends BaseRenderer implements Callback { } } } else if (result == C.RESULT_FORMAT_READ) { - subsampleOffsetUs = formatHolder.format.subsampleOffsetUs; + subsampleOffsetUs = Assertions.checkNotNull(formatHolder.format).subsampleOffsetUs; } } if (pendingMetadataCount > 0 && pendingMetadataTimestamps[pendingMetadataIndex] <= positionUs) { - invokeRenderer(pendingMetadata[pendingMetadataIndex]); + Metadata metadata = castNonNull(pendingMetadata[pendingMetadataIndex]); + invokeRenderer(metadata); pendingMetadata[pendingMetadataIndex] = null; pendingMetadataIndex = (pendingMetadataIndex + 1) % MAX_PENDING_METADATA_COUNT; pendingMetadataCount--; @@ -158,7 +171,7 @@ public final class MetadataRenderer extends BaseRenderer implements Callback { */ private void decodeWrappedMetadata(Metadata metadata, List decodedEntries) { for (int i = 0; i < metadata.length(); i++) { - Format wrappedMetadataFormat = metadata.get(i).getWrappedMetadataFormat(); + @Nullable Format wrappedMetadataFormat = metadata.get(i).getWrappedMetadataFormat(); if (wrappedMetadataFormat != null && decoderFactory.supportsFormat(wrappedMetadataFormat)) { MetadataDecoder wrappedMetadataDecoder = decoderFactory.createDecoder(wrappedMetadataFormat); @@ -167,7 +180,7 @@ public final class MetadataRenderer extends BaseRenderer implements Callback { Assertions.checkNotNull(metadata.get(i).getWrappedMetadataBytes()); buffer.clear(); buffer.ensureSpaceForWrite(wrappedMetadataBytes.length); - buffer.data.put(wrappedMetadataBytes); + castNonNull(buffer.data).put(wrappedMetadataBytes); buffer.flip(); @Nullable Metadata innerMetadata = wrappedMetadataDecoder.decode(buffer); if (innerMetadata != null) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/dvbsi/AppInfoTable.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/dvbsi/AppInfoTable.java new file mode 100644 index 0000000000..cdfb15f15b --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/dvbsi/AppInfoTable.java @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2020 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.dvbsi; + +import android.os.Parcel; +import android.os.Parcelable; +import com.google.android.exoplayer2.metadata.Metadata; +import com.google.android.exoplayer2.util.Assertions; + +/** + * A representation of a DVB Application Information Table (AIT). + * + *

      For more info on the AIT see section 5.3.4 of the + * DVB ETSI TS 102 809 v1.1.1 spec. + */ +public final class AppInfoTable implements Metadata.Entry { + /** + * The application shall be started when the service is selected, unless the application is + * already running. + */ + public static final int CONTROL_CODE_AUTOSTART = 0x01; + /** + * The application is allowed to run while the service is selected, however it shall not start + * automatically when the service becomes selected. + */ + public static final int CONTROL_CODE_PRESENT = 0x02; + + public final int controlCode; + public final String url; + + public AppInfoTable(int controlCode, String url) { + this.controlCode = controlCode; + this.url = url; + } + + @Override + public String toString() { + return "Ait(controlCode=" + controlCode + ",url=" + url + ")"; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel parcel, int i) { + parcel.writeString(url); + parcel.writeInt(controlCode); + } + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + @Override + public AppInfoTable createFromParcel(Parcel in) { + String url = Assertions.checkNotNull(in.readString()); + int controlCode = in.readInt(); + return new AppInfoTable(controlCode, url); + } + + @Override + public AppInfoTable[] newArray(int size) { + return new AppInfoTable[size]; + } + }; +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/dvbsi/AppInfoTableDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/dvbsi/AppInfoTableDecoder.java new file mode 100644 index 0000000000..f533b97d13 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/dvbsi/AppInfoTableDecoder.java @@ -0,0 +1,141 @@ +/* + * Copyright (C) 2020 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.dvbsi; + +import androidx.annotation.Nullable; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.metadata.Metadata; +import com.google.android.exoplayer2.metadata.MetadataDecoder; +import com.google.android.exoplayer2.metadata.MetadataInputBuffer; +import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.ParsableBitArray; +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.util.ArrayList; + +/** + * Decoder for the DVB Application Information Table (AIT). + * + *

      For more info on the AIT see section 5.3.4 of the + * DVB ETSI TS 102 809 v1.1.1 spec. + */ +public final class AppInfoTableDecoder implements MetadataDecoder { + + /** See section 5.3.6. */ + private static final int DESCRIPTOR_TRANSPORT_PROTOCOL = 0x02; + /** See section 5.3.7. */ + private static final int DESCRIPTOR_SIMPLE_APPLICATION_LOCATION = 0x15; + + /** See table 29 in section 5.3.6. */ + private static final int TRANSPORT_PROTOCOL_HTTP = 3; + + /** See table 16 in section 5.3.4.6. */ + public static final int APPLICATION_INFORMATION_TABLE_ID = 0x74; + + @Override + @Nullable + public Metadata decode(MetadataInputBuffer inputBuffer) { + ByteBuffer buffer = Assertions.checkNotNull(inputBuffer.data); + Assertions.checkArgument( + buffer.position() == 0 && buffer.hasArray() && buffer.arrayOffset() == 0); + int tableId = buffer.get(); + return tableId == APPLICATION_INFORMATION_TABLE_ID + ? parseAit(new ParsableBitArray(buffer.array(), buffer.limit())) + : null; + } + + @Nullable + private static Metadata parseAit(ParsableBitArray sectionData) { + // tableId, section_syntax_indication, reserved_future_use, reserved + sectionData.skipBits(12); + int sectionLength = sectionData.readBits(12); + int endOfSection = sectionData.getBytePosition() + sectionLength - 4 /* Ignore leading CRC */; + + // test_application_flag, application_type, reserved, version_number, current_next_indicator, + // section_number, last_section_number, reserved_future_use + sectionData.skipBits(44); + + int commonDescriptorsLength = sectionData.readBits(12); + + // Since we currently only keep URL and control code, which are unique per application, + // there is no useful information in common descriptor. + sectionData.skipBytes(commonDescriptorsLength); + + // reserved_future_use, application_loop_length + sectionData.skipBits(16); + + ArrayList appInfoTables = new ArrayList<>(); + while (sectionData.getBytePosition() < endOfSection) { + @Nullable String urlBase = null; + @Nullable String urlExtension = null; + + // application_identifier + sectionData.skipBits(48); + + int controlCode = sectionData.readBits(8); + + // reserved_future_use + sectionData.skipBits(4); + + int applicationDescriptorsLoopLength = sectionData.readBits(12); + int positionOfNextApplication = + sectionData.getBytePosition() + applicationDescriptorsLoopLength; + while (sectionData.getBytePosition() < positionOfNextApplication) { + int descriptorTag = sectionData.readBits(8); + int descriptorLength = sectionData.readBits(8); + int positionOfNextDescriptor = sectionData.getBytePosition() + descriptorLength; + + if (descriptorTag == DESCRIPTOR_TRANSPORT_PROTOCOL) { + // See section 5.3.6. + int protocolId = sectionData.readBits(16); + // label + sectionData.skipBits(8); + + if (protocolId == TRANSPORT_PROTOCOL_HTTP) { + // See section 5.3.6.2. + while (sectionData.getBytePosition() < positionOfNextDescriptor) { + int urlBaseLength = sectionData.readBits(8); + urlBase = sectionData.readBytesAsString(urlBaseLength, Charset.forName(C.ASCII_NAME)); + + int extensionCount = sectionData.readBits(8); + for (int urlExtensionIndex = 0; + urlExtensionIndex < extensionCount; + urlExtensionIndex++) { + int urlExtensionLength = sectionData.readBits(8); + sectionData.skipBytes(urlExtensionLength); + } + } + } + } else if (descriptorTag == DESCRIPTOR_SIMPLE_APPLICATION_LOCATION) { + // See section 5.3.7. + urlExtension = + sectionData.readBytesAsString(descriptorLength, Charset.forName(C.ASCII_NAME)); + } + + sectionData.setPosition(positionOfNextDescriptor * 8); + } + + sectionData.setPosition(positionOfNextApplication * 8); + + if (urlBase != null && urlExtension != null) { + appInfoTables.add(new AppInfoTable(controlCode, urlBase + urlExtension)); + } + } + + return appInfoTables.isEmpty() ? null : new Metadata(appInfoTables); + } +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/dvbsi/package-info.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/dvbsi/package-info.java new file mode 100644 index 0000000000..33efd262fe --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/dvbsi/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (C) 2020 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. + * + */ +@NonNullApi +package com.google.android.exoplayer2.metadata.dvbsi; + +import com.google.android.exoplayer2.util.NonNullApi; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/icy/IcyDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/icy/IcyDecoder.java index 13d6b485b3..cd3c1dfb63 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/icy/IcyDecoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/icy/IcyDecoder.java @@ -15,53 +15,92 @@ */ package com.google.android.exoplayer2.metadata.icy; -import androidx.annotation.VisibleForTesting; +import androidx.annotation.Nullable; +import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.MetadataDecoder; import com.google.android.exoplayer2.metadata.MetadataInputBuffer; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Util; import java.nio.ByteBuffer; +import java.nio.charset.CharacterCodingException; +import java.nio.charset.Charset; +import java.nio.charset.CharsetDecoder; import java.util.regex.Matcher; import java.util.regex.Pattern; /** Decodes ICY stream information. */ public final class IcyDecoder implements MetadataDecoder { - private static final String TAG = "IcyDecoder"; - private static final Pattern METADATA_ELEMENT = Pattern.compile("(.+?)='(.*?)';", Pattern.DOTALL); private static final String STREAM_KEY_NAME = "streamtitle"; private static final String STREAM_KEY_URL = "streamurl"; - @Override - @SuppressWarnings("ByteBufferBackingArray") - public Metadata decode(MetadataInputBuffer inputBuffer) { - ByteBuffer buffer = Assertions.checkNotNull(inputBuffer.data); - byte[] data = buffer.array(); - int length = buffer.limit(); - return decode(Util.fromUtf8Bytes(data, 0, length)); + private final CharsetDecoder utf8Decoder; + private final CharsetDecoder iso88591Decoder; + + public IcyDecoder() { + utf8Decoder = Charset.forName(C.UTF8_NAME).newDecoder(); + iso88591Decoder = Charset.forName(C.ISO88591_NAME).newDecoder(); } - @VisibleForTesting - /* package */ Metadata decode(String metadata) { - String name = null; - String url = null; + @Override + public Metadata decode(MetadataInputBuffer inputBuffer) { + ByteBuffer buffer = Assertions.checkNotNull(inputBuffer.data); + Assertions.checkArgument( + buffer.position() == 0 && buffer.hasArray() && buffer.arrayOffset() == 0); + @Nullable String icyString = decodeToString(buffer); + byte[] icyBytes = new byte[buffer.limit()]; + buffer.get(icyBytes); + + if (icyString == null) { + return new Metadata(new IcyInfo(icyBytes, /* title= */ null, /* url= */ null)); + } + + @Nullable String name = null; + @Nullable String url = null; int index = 0; - Matcher matcher = METADATA_ELEMENT.matcher(metadata); + Matcher matcher = METADATA_ELEMENT.matcher(icyString); while (matcher.find(index)) { - String key = Util.toLowerInvariant(matcher.group(1)); - String value = matcher.group(2); - switch (key) { - case STREAM_KEY_NAME: - name = value; - break; - case STREAM_KEY_URL: - url = value; - break; + @Nullable String key = Util.toLowerInvariant(matcher.group(1)); + @Nullable String value = matcher.group(2); + if (key != null) { + switch (key) { + case STREAM_KEY_NAME: + name = value; + break; + case STREAM_KEY_URL: + url = value; + break; + default: + break; + } } index = matcher.end(); } - return new Metadata(new IcyInfo(metadata, name, url)); + return new Metadata(new IcyInfo(icyBytes, name, url)); + } + + // The ICY spec doesn't specify a character encoding, and there's no way to communicate one + // either. So try decoding UTF-8 first, then fall back to ISO-8859-1. + // https://github.com/google/ExoPlayer/issues/6753 + @Nullable + private String decodeToString(ByteBuffer data) { + try { + return utf8Decoder.decode(data).toString(); + } catch (CharacterCodingException e) { + // Fall through to try ISO-8859-1 decoding. + } finally { + utf8Decoder.reset(); + data.rewind(); + } + try { + return iso88591Decoder.decode(data).toString(); + } catch (CharacterCodingException e) { + return null; + } finally { + iso88591Decoder.reset(); + data.rewind(); + } } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/icy/IcyInfo.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/icy/IcyInfo.java index 1198d1af8b..1a3ed2ea6d 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/icy/IcyInfo.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/icy/IcyInfo.java @@ -20,34 +20,34 @@ import android.os.Parcelable; import androidx.annotation.Nullable; import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.util.Assertions; -import com.google.android.exoplayer2.util.Util; +import java.util.Arrays; /** ICY in-stream information. */ public final class IcyInfo implements Metadata.Entry { - /** The complete metadata string used to construct this IcyInfo. */ - public final String rawMetadata; - /** The stream title if present, or {@code null}. */ + /** The complete metadata bytes used to construct this IcyInfo. */ + public final byte[] rawMetadata; + /** The stream title if present and decodable, or {@code null}. */ @Nullable public final String title; - /** The stream URL if present, or {@code null}. */ + /** The stream URL if present and decodable, or {@code null}. */ @Nullable public final String url; /** - * Construct a new IcyInfo from the source metadata string, and optionally a StreamTitle and - * StreamUrl that have been extracted. + * Construct a new IcyInfo from the source metadata, and optionally a StreamTitle and StreamUrl + * that have been extracted. * * @param rawMetadata See {@link #rawMetadata}. * @param title See {@link #title}. * @param url See {@link #url}. */ - public IcyInfo(String rawMetadata, @Nullable String title, @Nullable String url) { + public IcyInfo(byte[] rawMetadata, @Nullable String title, @Nullable String url) { this.rawMetadata = rawMetadata; this.title = title; this.url = url; } /* package */ IcyInfo(Parcel in) { - rawMetadata = Assertions.checkNotNull(in.readString()); + rawMetadata = Assertions.checkNotNull(in.createByteArray()); title = in.readString(); url = in.readString(); } @@ -62,26 +62,26 @@ public final class IcyInfo implements Metadata.Entry { } IcyInfo other = (IcyInfo) obj; // title & url are derived from rawMetadata, so no need to include them in the comparison. - return Util.areEqual(rawMetadata, other.rawMetadata); + return Arrays.equals(rawMetadata, other.rawMetadata); } @Override public int hashCode() { // title & url are derived from rawMetadata, so no need to include them in the hash. - return rawMetadata.hashCode(); + return Arrays.hashCode(rawMetadata); } @Override public String toString() { return String.format( - "ICY: title=\"%s\", url=\"%s\", rawMetadata=\"%s\"", title, url, rawMetadata); + "ICY: title=\"%s\", url=\"%s\", rawMetadata.length=\"%s\"", title, url, rawMetadata.length); } // Parcelable implementation. @Override public void writeToParcel(Parcel dest, int flags) { - dest.writeString(rawMetadata); + dest.writeByteArray(rawMetadata); dest.writeString(title); dest.writeString(url); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/icy/package-info.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/icy/package-info.java new file mode 100644 index 0000000000..2a2d0c7fc2 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/icy/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2019 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. + */ +@NonNullApi +package com.google.android.exoplayer2.metadata.icy; + +import com.google.android.exoplayer2.util.NonNullApi; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/scte35/PrivateCommand.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/scte35/PrivateCommand.java index 4334fa99cb..44850b720f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/scte35/PrivateCommand.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/scte35/PrivateCommand.java @@ -18,6 +18,7 @@ package com.google.android.exoplayer2.metadata.scte35; import android.os.Parcel; import android.os.Parcelable; import com.google.android.exoplayer2.util.ParsableByteArray; +import com.google.android.exoplayer2.util.Util; /** * Represents a private command as defined in SCTE35, Section 9.3.6. @@ -46,8 +47,7 @@ public final class PrivateCommand extends SpliceCommand { private PrivateCommand(Parcel in) { ptsAdjustment = in.readLong(); identifier = in.readLong(); - commandBytes = new byte[in.readInt()]; - in.readByteArray(commandBytes); + commandBytes = Util.castNonNull(in.createByteArray()); } /* package */ static PrivateCommand parseFromSection(ParsableByteArray sectionData, @@ -64,7 +64,6 @@ public final class PrivateCommand extends SpliceCommand { public void writeToParcel(Parcel dest, int flags) { dest.writeLong(ptsAdjustment); dest.writeLong(identifier); - dest.writeInt(commandBytes.length); dest.writeByteArray(commandBytes); } 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 1153f918fc..647e1296a9 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 @@ -15,13 +15,16 @@ */ package com.google.android.exoplayer2.metadata.scte35; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.MetadataDecoder; import com.google.android.exoplayer2.metadata.MetadataInputBuffer; +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.TimestampAdjuster; import java.nio.ByteBuffer; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /** * Decodes splice info sections and produces splice commands. @@ -37,16 +40,19 @@ public final class SpliceInfoDecoder implements MetadataDecoder { private final ParsableByteArray sectionData; private final ParsableBitArray sectionHeader; - private TimestampAdjuster timestampAdjuster; + private @MonotonicNonNull TimestampAdjuster timestampAdjuster; public SpliceInfoDecoder() { sectionData = new ParsableByteArray(); sectionHeader = new ParsableBitArray(); } - @SuppressWarnings("ByteBufferBackingArray") @Override public Metadata decode(MetadataInputBuffer inputBuffer) { + ByteBuffer buffer = Assertions.checkNotNull(inputBuffer.data); + Assertions.checkArgument( + buffer.position() == 0 && buffer.hasArray() && buffer.arrayOffset() == 0); + // Internal timestamps adjustment. if (timestampAdjuster == null || inputBuffer.subsampleOffsetUs != timestampAdjuster.getTimestampOffsetUs()) { @@ -54,7 +60,6 @@ public final class SpliceInfoDecoder implements MetadataDecoder { timestampAdjuster.adjustSampleTimestamp(inputBuffer.timeUs - inputBuffer.subsampleOffsetUs); } - ByteBuffer buffer = inputBuffer.data; byte[] data = buffer.array(); int size = buffer.limit(); sectionData.reset(data, size); @@ -68,7 +73,7 @@ public final class SpliceInfoDecoder implements MetadataDecoder { sectionHeader.skipBits(20); int spliceCommandLength = sectionHeader.readBits(12); int spliceCommandType = sectionHeader.readBits(8); - SpliceCommand command = null; + @Nullable SpliceCommand command = null; // Go to the start of the command by skipping all fields up to command_type. sectionData.skipBytes(14); switch (spliceCommandType) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/scte35/package-info.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/scte35/package-info.java new file mode 100644 index 0000000000..0c4448f4d3 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/scte35/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2019 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. + */ +@NonNullApi +package com.google.android.exoplayer2.metadata.scte35; + +import com.google.android.exoplayer2.util.NonNullApi; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DefaultDownloadIndex.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DefaultDownloadIndex.java index 7ed1eb095f..4437fccd16 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/DefaultDownloadIndex.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DefaultDownloadIndex.java @@ -26,6 +26,8 @@ import androidx.annotation.VisibleForTesting; import com.google.android.exoplayer2.database.DatabaseIOException; import com.google.android.exoplayer2.database.DatabaseProvider; import com.google.android.exoplayer2.database.VersionTable; +import com.google.android.exoplayer2.offline.Download.FailureReason; +import com.google.android.exoplayer2.offline.Download.State; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Util; import java.util.ArrayList; @@ -239,6 +241,9 @@ public final class DefaultDownloadIndex implements WritableDownloadIndex { try { ContentValues values = new ContentValues(); values.put(COLUMN_STATE, Download.STATE_REMOVING); + // Only downloads in STATE_FAILED are allowed a failure reason, so we need to clear it here in + // case we're moving downloads from STATE_FAILED to STATE_REMOVING. + values.put(COLUMN_FAILURE_REASON, Download.FAILURE_REASON_NONE); SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase(); writableDatabase.update(tableName, values, /* whereClause= */ null, /* whereArgs= */ null); } catch (SQLException e) { @@ -302,8 +307,6 @@ public final class DefaultDownloadIndex implements WritableDownloadIndex { } } - // incompatible types in argument. - @SuppressWarnings("nullness:argument.type.incompatible") private Cursor getCursor(String selection, @Nullable String[] selectionArgs) throws DatabaseIOException { try { @@ -351,14 +354,22 @@ public final class DefaultDownloadIndex implements WritableDownloadIndex { DownloadProgress downloadProgress = new DownloadProgress(); downloadProgress.bytesDownloaded = cursor.getLong(COLUMN_INDEX_BYTES_DOWNLOADED); downloadProgress.percentDownloaded = cursor.getFloat(COLUMN_INDEX_PERCENT_DOWNLOADED); + @State int state = cursor.getInt(COLUMN_INDEX_STATE); + // It's possible the database contains failure reasons for non-failed downloads, which is + // invalid. Clear them here. See https://github.com/google/ExoPlayer/issues/6785. + @FailureReason + int failureReason = + state == Download.STATE_FAILED + ? cursor.getInt(COLUMN_INDEX_FAILURE_REASON) + : Download.FAILURE_REASON_NONE; return new Download( request, - /* state= */ cursor.getInt(COLUMN_INDEX_STATE), + state, /* startTimeMs= */ cursor.getLong(COLUMN_INDEX_START_TIME_MS), /* updateTimeMs= */ cursor.getLong(COLUMN_INDEX_UPDATE_TIME_MS), /* contentLength= */ cursor.getLong(COLUMN_INDEX_CONTENT_LENGTH), /* stopReason= */ cursor.getInt(COLUMN_INDEX_STOP_REASON), - /* failureReason= */ cursor.getInt(COLUMN_INDEX_FAILURE_REASON), + failureReason, downloadProgress); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DefaultDownloaderFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DefaultDownloaderFactory.java index d8126d4736..0b7434c339 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/DefaultDownloaderFactory.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DefaultDownloaderFactory.java @@ -17,8 +17,10 @@ package com.google.android.exoplayer2.offline; import android.net.Uri; import androidx.annotation.Nullable; +import com.google.android.exoplayer2.upstream.cache.CacheDataSource; import java.lang.reflect.Constructor; import java.util.List; +import java.util.concurrent.Executor; /** * Default {@link DownloaderFactory}, supporting creation of progressive, DASH, HLS and @@ -32,7 +34,7 @@ public class DefaultDownloaderFactory implements DownloaderFactory { @Nullable private static final Constructor SS_DOWNLOADER_CONSTRUCTOR; static { - Constructor dashDownloaderConstructor = null; + @Nullable Constructor dashDownloaderConstructor = null; try { // LINT.IfChange dashDownloaderConstructor = @@ -43,7 +45,7 @@ public class DefaultDownloaderFactory implements DownloaderFactory { // Expected if the app was built without the DASH module. } DASH_DOWNLOADER_CONSTRUCTOR = dashDownloaderConstructor; - Constructor hlsDownloaderConstructor = null; + @Nullable Constructor hlsDownloaderConstructor = null; try { // LINT.IfChange hlsDownloaderConstructor = @@ -54,7 +56,7 @@ public class DefaultDownloaderFactory implements DownloaderFactory { // Expected if the app was built without the HLS module. } HLS_DOWNLOADER_CONSTRUCTOR = hlsDownloaderConstructor; - Constructor ssDownloaderConstructor = null; + @Nullable Constructor ssDownloaderConstructor = null; try { // LINT.IfChange ssDownloaderConstructor = @@ -68,11 +70,32 @@ public class DefaultDownloaderFactory implements DownloaderFactory { SS_DOWNLOADER_CONSTRUCTOR = ssDownloaderConstructor; } - private final DownloaderConstructorHelper downloaderConstructorHelper; + private final CacheDataSource.Factory cacheDataSourceFactory; + private final Executor executor; - /** @param downloaderConstructorHelper A helper for instantiating downloaders. */ - public DefaultDownloaderFactory(DownloaderConstructorHelper downloaderConstructorHelper) { - this.downloaderConstructorHelper = downloaderConstructorHelper; + /** + * Creates an instance. + * + * @param cacheDataSourceFactory A {@link CacheDataSource.Factory} for the cache into which + * downloads will be written. + */ + public DefaultDownloaderFactory(CacheDataSource.Factory cacheDataSourceFactory) { + this(cacheDataSourceFactory, Runnable::run); + } + + /** + * Creates an instance. + * + * @param cacheDataSourceFactory A {@link CacheDataSource.Factory} for the cache into which + * downloads will be written. + * @param executor An {@link Executor} used to make requests for media being downloaded. Providing + * an {@link Executor} that uses multiple threads will speed up download tasks that can be + * split into smaller parts for parallel execution. + */ + public DefaultDownloaderFactory( + CacheDataSource.Factory cacheDataSourceFactory, Executor executor) { + this.cacheDataSourceFactory = cacheDataSourceFactory; + this.executor = executor; } @Override @@ -80,7 +103,7 @@ public class DefaultDownloaderFactory implements DownloaderFactory { switch (request.type) { case DownloadRequest.TYPE_PROGRESSIVE: return new ProgressiveDownloader( - request.uri, request.customCacheKey, downloaderConstructorHelper); + request.uri, request.customCacheKey, cacheDataSourceFactory, executor); case DownloadRequest.TYPE_DASH: return createDownloader(request, DASH_DOWNLOADER_CONSTRUCTOR); case DownloadRequest.TYPE_HLS: @@ -98,7 +121,8 @@ public class DefaultDownloaderFactory implements DownloaderFactory { throw new IllegalStateException("Module missing for: " + request.type); } try { - return constructor.newInstance(request.uri, request.streamKeys, downloaderConstructorHelper); + return constructor.newInstance( + request.uri, request.streamKeys, cacheDataSourceFactory, executor); } catch (Exception e) { throw new RuntimeException("Failed to instantiate downloader for: " + request.type, e); } @@ -109,7 +133,7 @@ public class DefaultDownloaderFactory implements DownloaderFactory { try { return clazz .asSubclass(Downloader.class) - .getConstructor(Uri.class, List.class, DownloaderConstructorHelper.class); + .getConstructor(Uri.class, List.class, CacheDataSource.Factory.class, Executor.class); } catch (NoSuchMethodException e) { // The downloader is present, but the expected constructor is missing. throw new RuntimeException("Downloader constructor missing", e); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/Download.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/Download.java index 97dff8394e..da46120b29 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/Download.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/Download.java @@ -130,9 +130,9 @@ public final class Download { @FailureReason int failureReason, DownloadProgress progress) { Assertions.checkNotNull(progress); - Assertions.checkState((failureReason == FAILURE_REASON_NONE) == (state != STATE_FAILED)); + Assertions.checkArgument((failureReason == FAILURE_REASON_NONE) == (state != STATE_FAILED)); if (stopReason != 0) { - Assertions.checkState(state != STATE_DOWNLOADING && state != STATE_QUEUED); + Assertions.checkArgument(state != STATE_DOWNLOADING && state != STATE_QUEUED); } this.request = request; this.state = state; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadHelper.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadHelper.java index c585c79b76..8e50d70020 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadHelper.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadHelper.java @@ -24,11 +24,12 @@ import android.util.SparseIntArray; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlaybackException; +import com.google.android.exoplayer2.Renderer; import com.google.android.exoplayer2.RendererCapabilities; import com.google.android.exoplayer2.RenderersFactory; import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.audio.AudioRendererEventListener; import com.google.android.exoplayer2.drm.DrmSessionManager; -import com.google.android.exoplayer2.drm.FrameworkMediaCrypto; import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; @@ -54,6 +55,7 @@ import com.google.android.exoplayer2.upstream.DefaultAllocator; import com.google.android.exoplayer2.upstream.TransferListener; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Util; +import com.google.android.exoplayer2.video.VideoRendererEventListener; import java.io.IOException; import java.lang.reflect.Constructor; import java.util.ArrayList; @@ -154,6 +156,28 @@ public final class DownloadHelper { private static final Constructor HLS_FACTORY_CONSTRUCTOR = getConstructor("com.google.android.exoplayer2.source.hls.HlsMediaSource$Factory"); + /** + * Extracts renderer capabilities for the renderers created by the provided renderers factory. + * + * @param renderersFactory A {@link RenderersFactory}. + * @return The {@link RendererCapabilities} for each renderer created by the {@code + * renderersFactory}. + */ + public static RendererCapabilities[] getRendererCapabilities(RenderersFactory renderersFactory) { + Renderer[] renderers = + renderersFactory.createRenderers( + Util.createHandler(), + new VideoRendererEventListener() {}, + new AudioRendererEventListener() {}, + (cues) -> {}, + (metadata) -> {}); + RendererCapabilities[] capabilities = new RendererCapabilities[renderers.length]; + for (int i = 0; i < renderers.length; i++) { + capabilities[i] = renderers[i].getCapabilities(); + } + return capabilities; + } + /** @deprecated Use {@link #forProgressive(Context, Uri)} */ @Deprecated @SuppressWarnings("deprecation") @@ -245,8 +269,8 @@ public final class DownloadHelper { * @param dataSourceFactory A {@link DataSource.Factory} used to load the manifest. * @param renderersFactory A {@link RenderersFactory} creating the renderers for which tracks are * selected. - * @param drmSessionManager An optional {@link DrmSessionManager} used by the renderers created by - * {@code renderersFactory}. + * @param drmSessionManager An optional {@link DrmSessionManager}. Used to help determine which + * tracks can be selected. * @param trackSelectorParameters {@link DefaultTrackSelector.Parameters} for selecting tracks for * downloading. * @return A {@link DownloadHelper} for DASH streams. @@ -256,16 +280,20 @@ public final class DownloadHelper { Uri uri, DataSource.Factory dataSourceFactory, RenderersFactory renderersFactory, - @Nullable DrmSessionManager drmSessionManager, + @Nullable DrmSessionManager drmSessionManager, DefaultTrackSelector.Parameters trackSelectorParameters) { return new DownloadHelper( DownloadRequest.TYPE_DASH, uri, /* cacheKey= */ null, createMediaSourceInternal( - DASH_FACTORY_CONSTRUCTOR, uri, dataSourceFactory, /* streamKeys= */ null), + DASH_FACTORY_CONSTRUCTOR, + uri, + dataSourceFactory, + drmSessionManager, + /* streamKeys= */ null), trackSelectorParameters, - Util.getRendererCapabilities(renderersFactory, drmSessionManager)); + getRendererCapabilities(renderersFactory)); } /** @deprecated Use {@link #forHls(Context, Uri, Factory, RenderersFactory)} */ @@ -311,8 +339,8 @@ public final class DownloadHelper { * @param dataSourceFactory A {@link DataSource.Factory} used to load the playlist. * @param renderersFactory A {@link RenderersFactory} creating the renderers for which tracks are * selected. - * @param drmSessionManager An optional {@link DrmSessionManager} used by the renderers created by - * {@code renderersFactory}. + * @param drmSessionManager An optional {@link DrmSessionManager}. Used to help determine which + * tracks can be selected. * @param trackSelectorParameters {@link DefaultTrackSelector.Parameters} for selecting tracks for * downloading. * @return A {@link DownloadHelper} for HLS streams. @@ -322,16 +350,20 @@ public final class DownloadHelper { Uri uri, DataSource.Factory dataSourceFactory, RenderersFactory renderersFactory, - @Nullable DrmSessionManager drmSessionManager, + @Nullable DrmSessionManager drmSessionManager, DefaultTrackSelector.Parameters trackSelectorParameters) { return new DownloadHelper( DownloadRequest.TYPE_HLS, uri, /* cacheKey= */ null, createMediaSourceInternal( - HLS_FACTORY_CONSTRUCTOR, uri, dataSourceFactory, /* streamKeys= */ null), + HLS_FACTORY_CONSTRUCTOR, + uri, + dataSourceFactory, + drmSessionManager, + /* streamKeys= */ null), trackSelectorParameters, - Util.getRendererCapabilities(renderersFactory, drmSessionManager)); + getRendererCapabilities(renderersFactory)); } /** @deprecated Use {@link #forSmoothStreaming(Context, Uri, Factory, RenderersFactory)} */ @@ -377,8 +409,8 @@ public final class DownloadHelper { * @param dataSourceFactory A {@link DataSource.Factory} used to load the manifest. * @param renderersFactory A {@link RenderersFactory} creating the renderers for which tracks are * selected. - * @param drmSessionManager An optional {@link DrmSessionManager} used by the renderers created by - * {@code renderersFactory}. + * @param drmSessionManager An optional {@link DrmSessionManager}. Used to help determine which + * tracks can be selected. * @param trackSelectorParameters {@link DefaultTrackSelector.Parameters} for selecting tracks for * downloading. * @return A {@link DownloadHelper} for SmoothStreaming streams. @@ -388,28 +420,45 @@ public final class DownloadHelper { Uri uri, DataSource.Factory dataSourceFactory, RenderersFactory renderersFactory, - @Nullable DrmSessionManager drmSessionManager, + @Nullable DrmSessionManager drmSessionManager, DefaultTrackSelector.Parameters trackSelectorParameters) { return new DownloadHelper( DownloadRequest.TYPE_SS, uri, /* cacheKey= */ null, createMediaSourceInternal( - SS_FACTORY_CONSTRUCTOR, uri, dataSourceFactory, /* streamKeys= */ null), + SS_FACTORY_CONSTRUCTOR, + uri, + dataSourceFactory, + drmSessionManager, + /* streamKeys= */ null), trackSelectorParameters, - Util.getRendererCapabilities(renderersFactory, drmSessionManager)); + getRendererCapabilities(renderersFactory)); } /** - * Utility method to create a MediaSource which only contains the tracks defined in {@code + * Equivalent to {@link #createMediaSource(DownloadRequest, Factory, DrmSessionManager) + * createMediaSource(downloadRequest, dataSourceFactory, null)}. + */ + public static MediaSource createMediaSource( + DownloadRequest downloadRequest, DataSource.Factory dataSourceFactory) { + return createMediaSource(downloadRequest, dataSourceFactory, /* drmSessionManager= */ null); + } + + /** + * Utility method to create a {@link MediaSource} that only exposes the tracks defined in {@code * downloadRequest}. * * @param downloadRequest A {@link DownloadRequest}. * @param dataSourceFactory A factory for {@link DataSource}s to read the media. - * @return A MediaSource which only contains the tracks defined in {@code downloadRequest}. + * @param drmSessionManager An optional {@link DrmSessionManager} to be passed to the {@link + * MediaSource}. + * @return A {@link MediaSource} that only exposes the tracks defined in {@code downloadRequest}. */ public static MediaSource createMediaSource( - DownloadRequest downloadRequest, DataSource.Factory dataSourceFactory) { + DownloadRequest downloadRequest, + DataSource.Factory dataSourceFactory, + @Nullable DrmSessionManager drmSessionManager) { @Nullable Constructor constructor; switch (downloadRequest.type) { case DownloadRequest.TYPE_DASH: @@ -423,12 +472,17 @@ public final class DownloadHelper { break; case DownloadRequest.TYPE_PROGRESSIVE: return new ProgressiveMediaSource.Factory(dataSourceFactory) + .setCustomCacheKey(downloadRequest.customCacheKey) .createMediaSource(downloadRequest.uri); default: throw new IllegalStateException("Unsupported type: " + downloadRequest.type); } return createMediaSourceInternal( - constructor, downloadRequest.uri, dataSourceFactory, downloadRequest.streamKeys); + constructor, + downloadRequest.uri, + dataSourceFactory, + drmSessionManager, + downloadRequest.streamKeys); } private final String downloadType; @@ -888,12 +942,16 @@ public final class DownloadHelper { @Nullable Constructor constructor, Uri uri, Factory dataSourceFactory, + @Nullable DrmSessionManager drmSessionManager, @Nullable List streamKeys) { if (constructor == null) { throw new IllegalStateException("Module missing to create media source."); } try { MediaSourceFactory factory = constructor.newInstance(dataSourceFactory); + if (drmSessionManager != null) { + factory.setDrmSessionManager(drmSessionManager); + } if (streamKeys != null) { factory.setStreamKeys(streamKeys); } @@ -935,7 +993,7 @@ public final class DownloadHelper { @SuppressWarnings("methodref.receiver.bound.invalid") Handler downloadThreadHandler = Util.createHandler(this::handleDownloadHelperCallbackMessage); this.downloadHelperHandler = downloadThreadHandler; - mediaSourceThread = new HandlerThread("DownloadHelper"); + mediaSourceThread = new HandlerThread("ExoPlayer:DownloadHelper"); mediaSourceThread.start(); mediaSourceHandler = Util.createHandler(mediaSourceThread.getLooper(), /* callback= */ this); mediaSourceHandler.sendEmptyMessage(MESSAGE_PREPARE_SOURCE); @@ -1097,8 +1155,8 @@ public final class DownloadHelper { return C.SELECTION_REASON_UNKNOWN; } - @Nullable @Override + @Nullable public Object getSelectionData() { return null; } @@ -1121,8 +1179,8 @@ public final class DownloadHelper { return 0; } - @Nullable @Override + @Nullable public TransferListener getTransferListener() { return null; } 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 c3cf0bdc24..32931d9f32 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 @@ -40,6 +40,7 @@ import com.google.android.exoplayer2.scheduler.RequirementsWatcher; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSource.Factory; import com.google.android.exoplayer2.upstream.cache.Cache; +import com.google.android.exoplayer2.upstream.cache.CacheDataSource; import com.google.android.exoplayer2.upstream.cache.CacheEvictor; import com.google.android.exoplayer2.upstream.cache.NoOpCacheEvictor; import com.google.android.exoplayer2.util.Assertions; @@ -61,7 +62,9 @@ import java.util.concurrent.CopyOnWriteArraySet; * *

      A download manager instance must be accessed only from the thread that created it, unless that * thread does not have a {@link Looper}. In that case, it must be accessed only from the - * application's main thread. Registered listeners will be called on the same thread. + * application's main thread. Registered listeners will be called on the same thread. In all cases + * the `Looper` of the thread from which the manager must be accessed can be queried using {@link + * #getApplicationLooper()}. */ public final class DownloadManager { @@ -75,6 +78,16 @@ public final class DownloadManager { */ default void onInitialized(DownloadManager downloadManager) {} + /** + * Called when downloads are ({@link #pauseDownloads() paused} or {@link #resumeDownloads() + * resumed}. + * + * @param downloadManager The reporting instance. + * @param downloadsPaused Whether downloads are currently paused. + */ + default void onDownloadsPausedChanged( + DownloadManager downloadManager, boolean downloadsPaused) {} + /** * Called when the state of a download changes. * @@ -110,6 +123,19 @@ public final class DownloadManager { DownloadManager downloadManager, Requirements requirements, @Requirements.RequirementFlags int notMetRequirements) {} + + /** + * Called when there is a change in whether this manager has one or more downloads that are not + * progressing for the sole reason that the {@link #getRequirements() Requirements} are not met. + * See {@link #isWaitingForRequirements()} for more information. + * + * @param downloadManager The reporting instance. + * @param waitingForRequirements Whether this manager has one or more downloads that are not + * progressing for the sole reason that the {@link #getRequirements() Requirements} are not + * met. + */ + default void onWaitingForRequirementsChanged( + DownloadManager downloadManager, boolean waitingForRequirements) {} } /** The default maximum number of parallel downloads. */ @@ -143,7 +169,7 @@ public final class DownloadManager { private final Context context; private final WritableDownloadIndex downloadIndex; - private final Handler mainHandler; + private final Handler applicationHandler; private final InternalHandler internalHandler; private final RequirementsWatcher.Listener requirementsListener; private final CopyOnWriteArraySet listeners; @@ -155,6 +181,7 @@ public final class DownloadManager { private int maxParallelDownloads; private int minRetryCount; private int notMetRequirements; + private boolean waitingForRequirements; private List downloads; private RequirementsWatcher requirementsWatcher; @@ -173,7 +200,10 @@ public final class DownloadManager { this( context, new DefaultDownloadIndex(databaseProvider), - new DefaultDownloaderFactory(new DownloaderConstructorHelper(cache, upstreamFactory))); + new DefaultDownloaderFactory( + new CacheDataSource.Factory() + .setCache(cache) + .setUpstreamDataSourceFactory(upstreamFactory))); } /** @@ -196,8 +226,8 @@ public final class DownloadManager { @SuppressWarnings("methodref.receiver.bound.invalid") Handler mainHandler = Util.createHandler(this::handleMainMessage); - this.mainHandler = mainHandler; - HandlerThread internalThread = new HandlerThread("DownloadManager file i/o"); + this.applicationHandler = mainHandler; + HandlerThread internalThread = new HandlerThread("ExoPlayer:DownloadManager"); internalThread.start(); internalHandler = new InternalHandler( @@ -222,6 +252,14 @@ public final class DownloadManager { .sendToTarget(); } + /** + * Returns the {@link Looper} associated with the application thread that's used to access the + * manager, and on which the manager will call its {@link Listener Listeners}. + */ + public Looper getApplicationLooper() { + return applicationHandler.getLooper(); + } + /** Returns whether the manager has completed initialization. */ public boolean isInitialized() { return initialized; @@ -238,17 +276,16 @@ public final class DownloadManager { /** * Returns whether this manager has one or more downloads that are not progressing for the sole - * reason that the {@link #getRequirements() Requirements} are not met. + * reason that the {@link #getRequirements() Requirements} are not met. This is true if: + * + *

        + *
      • The {@link #getRequirements() Requirements} are not met. + *
      • The downloads are not paused (i.e. {@link #getDownloadsPaused()} is {@code false}). + *
      • There are downloads in the {@link Download#STATE_QUEUED queued state}. + *
      */ public boolean isWaitingForRequirements() { - if (!downloadsPaused && notMetRequirements != 0) { - for (int i = 0; i < downloads.size(); i++) { - if (downloads.get(i).state == STATE_QUEUED) { - return true; - } - } - } - return false; + return waitingForRequirements; } /** @@ -281,7 +318,7 @@ public final class DownloadManager { */ @Requirements.RequirementFlags public int getNotMetRequirements() { - return getRequirements().getNotMetRequirements(context); + return notMetRequirements; } /** @@ -374,29 +411,15 @@ public final class DownloadManager { * {@link Download#stopReason stopReasons}. */ public void resumeDownloads() { - if (!downloadsPaused) { - return; - } - downloadsPaused = false; - pendingMessages++; - internalHandler - .obtainMessage(MSG_SET_DOWNLOADS_PAUSED, /* downloadsPaused */ 0, /* unused */ 0) - .sendToTarget(); + setDownloadsPaused(/* downloadsPaused= */ false); } /** - * Pauses downloads. Downloads that would otherwise be making progress transition to {@link + * Pauses downloads. Downloads that would otherwise be making progress will transition to {@link * Download#STATE_QUEUED}. */ public void pauseDownloads() { - if (downloadsPaused) { - return; - } - downloadsPaused = true; - pendingMessages++; - internalHandler - .obtainMessage(MSG_SET_DOWNLOADS_PAUSED, /* downloadsPaused */ 1, /* unused */ 0) - .sendToTarget(); + setDownloadsPaused(/* downloadsPaused= */ true); } /** @@ -475,12 +498,32 @@ public final class DownloadManager { // Restore the interrupted status. Thread.currentThread().interrupt(); } - mainHandler.removeCallbacksAndMessages(/* token= */ null); + applicationHandler.removeCallbacksAndMessages(/* token= */ null); // Reset state. downloads = Collections.emptyList(); pendingMessages = 0; activeTaskCount = 0; initialized = false; + notMetRequirements = 0; + waitingForRequirements = false; + } + } + + private void setDownloadsPaused(boolean downloadsPaused) { + if (this.downloadsPaused == downloadsPaused) { + return; + } + this.downloadsPaused = downloadsPaused; + pendingMessages++; + internalHandler + .obtainMessage(MSG_SET_DOWNLOADS_PAUSED, downloadsPaused ? 1 : 0, /* unused */ 0) + .sendToTarget(); + boolean waitingForRequirementsChanged = updateWaitingForRequirements(); + for (Listener listener : listeners) { + listener.onDownloadsPausedChanged(this, downloadsPaused); + } + if (waitingForRequirementsChanged) { + notifyWaitingForRequirementsChanged(); } } @@ -488,17 +531,41 @@ public final class DownloadManager { RequirementsWatcher requirementsWatcher, @Requirements.RequirementFlags int notMetRequirements) { Requirements requirements = requirementsWatcher.getRequirements(); + if (this.notMetRequirements != notMetRequirements) { + this.notMetRequirements = notMetRequirements; + pendingMessages++; + internalHandler + .obtainMessage(MSG_SET_NOT_MET_REQUIREMENTS, notMetRequirements, /* unused */ 0) + .sendToTarget(); + } + boolean waitingForRequirementsChanged = updateWaitingForRequirements(); for (Listener listener : listeners) { listener.onRequirementsStateChanged(this, requirements, notMetRequirements); } - if (this.notMetRequirements == notMetRequirements) { - return; + if (waitingForRequirementsChanged) { + notifyWaitingForRequirementsChanged(); + } + } + + private boolean updateWaitingForRequirements() { + boolean waitingForRequirements = false; + if (!downloadsPaused && notMetRequirements != 0) { + for (int i = 0; i < downloads.size(); i++) { + if (downloads.get(i).state == STATE_QUEUED) { + waitingForRequirements = true; + break; + } + } + } + boolean waitingForRequirementsChanged = this.waitingForRequirements != waitingForRequirements; + this.waitingForRequirements = waitingForRequirements; + return waitingForRequirementsChanged; + } + + private void notifyWaitingForRequirementsChanged() { + for (Listener listener : listeners) { + listener.onWaitingForRequirementsChanged(this, waitingForRequirements); } - this.notMetRequirements = notMetRequirements; - pendingMessages++; - internalHandler - .obtainMessage(MSG_SET_NOT_MET_REQUIREMENTS, notMetRequirements, /* unused */ 0) - .sendToTarget(); } // Main thread message handling. @@ -528,14 +595,19 @@ public final class DownloadManager { private void onInitialized(List downloads) { initialized = true; this.downloads = Collections.unmodifiableList(downloads); + boolean waitingForRequirementsChanged = updateWaitingForRequirements(); for (Listener listener : listeners) { listener.onInitialized(DownloadManager.this); } + if (waitingForRequirementsChanged) { + notifyWaitingForRequirementsChanged(); + } } private void onDownloadUpdate(DownloadUpdate update) { downloads = Collections.unmodifiableList(update.downloads); Download updatedDownload = update.download; + boolean waitingForRequirementsChanged = updateWaitingForRequirements(); if (update.isRemove) { for (Listener listener : listeners) { listener.onDownloadRemoved(this, updatedDownload); @@ -545,6 +617,9 @@ public final class DownloadManager { listener.onDownloadChanged(this, updatedDownload); } } + if (waitingForRequirementsChanged) { + notifyWaitingForRequirementsChanged(); + } } private void onMessageProcessed(int processedMessageCount, int activeTaskCount) { @@ -669,7 +744,7 @@ public final class DownloadManager { break; case MSG_CONTENT_LENGTH_CHANGED: task = (Task) message.obj; - onContentLengthChanged(task); + onContentLengthChanged(task, Util.toLong(message.arg1, message.arg2)); return; // No need to post back to mainHandler. case MSG_UPDATE_PROGRESS: updateProgress(); @@ -944,7 +1019,7 @@ public final class DownloadManager { // Cancel the downloading task. activeTask.cancel(/* released= */ false); } - // The activeTask is either a remove task, or a downloading task that we just cancelled. In + // The activeTask is either a remove task, or a downloading task that we just canceled. In // the latter case we need to wait for the task to stop before we start a remove task. return; } @@ -965,9 +1040,8 @@ public final class DownloadManager { // Task event processing. - private void onContentLengthChanged(Task task) { + private void onContentLengthChanged(Task task, long contentLength) { String downloadId = task.request.id; - long contentLength = task.contentLength; Download download = Assertions.checkNotNull(getDownload(downloadId, /* loadFromIndex= */ false)); if (contentLength == download.contentLength || contentLength == C.LENGTH_UNSET) { @@ -1210,7 +1284,6 @@ public final class DownloadManager { if (!isCanceled) { isCanceled = true; downloader.cancel(); - interrupt(); } } @@ -1260,7 +1333,13 @@ public final class DownloadManager { this.contentLength = contentLength; @Nullable Handler internalHandler = this.internalHandler; if (internalHandler != null) { - internalHandler.obtainMessage(MSG_CONTENT_LENGTH_CHANGED, this).sendToTarget(); + internalHandler + .obtainMessage( + MSG_CONTENT_LENGTH_CHANGED, + (int) (contentLength >> 32), + (int) contentLength, + this) + .sendToTarget(); } } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadProgress.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadProgress.java index 9d946daa28..ba226e60b2 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadProgress.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadProgress.java @@ -21,8 +21,8 @@ import com.google.android.exoplayer2.C; public class DownloadProgress { /** The number of bytes that have been downloaded. */ - public long bytesDownloaded; + public volatile long bytesDownloaded; /** The percentage that has been downloaded, or {@link C#PERCENTAGE_UNSET} if unknown. */ - public float percentDownloaded; + public volatile float percentDownloaded; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadRequest.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadRequest.java index 7ff43ceacd..988b908140 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadRequest.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadRequest.java @@ -100,8 +100,7 @@ public final class DownloadRequest implements Parcelable { } streamKeys = Collections.unmodifiableList(mutableStreamKeys); customCacheKey = in.readString(); - data = new byte[in.readInt()]; - in.readByteArray(data); + data = castNonNull(in.createByteArray()); } /** @@ -194,7 +193,6 @@ public final class DownloadRequest implements Parcelable { dest.writeParcelable(streamKeys.get(i), /* parcelableFlags= */ 0); } dest.writeString(customCacheKey); - dest.writeInt(data.length); dest.writeByteArray(data); } 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 db10517b67..0ee9a83260 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 @@ -34,6 +34,7 @@ import com.google.android.exoplayer2.util.NotificationUtil; import com.google.android.exoplayer2.util.Util; import java.util.HashMap; import java.util.List; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /** A {@link Service} for downloading media. */ public abstract class DownloadService extends Service { @@ -166,20 +167,22 @@ public abstract class DownloadService extends Service { private static final String TAG = "DownloadService"; - // Keep DownloadManagerListeners for each DownloadService as long as there are downloads (and the - // process is running). This allows DownloadService to restart when there's no scheduler. + // Keep a DownloadManagerHelper for each DownloadService as long as the process is running. The + // helper is needed to restart the DownloadService when there's no scheduler. Even when there is a + // scheduler, the DownloadManagerHelper is typically able to restart the DownloadService faster. private static final HashMap, DownloadManagerHelper> - downloadManagerListeners = new HashMap<>(); + downloadManagerHelpers = new HashMap<>(); @Nullable private final ForegroundNotificationUpdater foregroundNotificationUpdater; @Nullable private final String channelId; @StringRes private final int channelNameResourceId; @StringRes private final int channelDescriptionResourceId; - @Nullable private DownloadManager downloadManager; + private @MonotonicNonNull DownloadManager downloadManager; private int lastStartId; private boolean startedInForeground; private boolean taskRemoved; + private boolean isStopped; private boolean isDestroyed; /** @@ -575,30 +578,33 @@ public abstract class DownloadService extends Service { NotificationUtil.IMPORTANCE_LOW); } Class clazz = getClass(); - DownloadManagerHelper downloadManagerHelper = downloadManagerListeners.get(clazz); + @Nullable DownloadManagerHelper downloadManagerHelper = downloadManagerHelpers.get(clazz); if (downloadManagerHelper == null) { - DownloadManager downloadManager = getDownloadManager(); + boolean foregroundAllowed = foregroundNotificationUpdater != null; + @Nullable Scheduler scheduler = foregroundAllowed ? getScheduler() : null; + downloadManager = getDownloadManager(); downloadManager.resumeDownloads(); downloadManagerHelper = new DownloadManagerHelper( - getApplicationContext(), downloadManager, getScheduler(), clazz); - downloadManagerListeners.put(clazz, downloadManagerHelper); + getApplicationContext(), downloadManager, foregroundAllowed, scheduler, clazz); + downloadManagerHelpers.put(clazz, downloadManagerHelper); + } else { + downloadManager = downloadManagerHelper.downloadManager; } - downloadManager = downloadManagerHelper.downloadManager; downloadManagerHelper.attachService(this); } @Override - public int onStartCommand(Intent intent, int flags, int startId) { + public int onStartCommand(@Nullable Intent intent, int flags, int startId) { lastStartId = startId; taskRemoved = false; @Nullable String intentAction = null; @Nullable String contentId = null; if (intent != null) { intentAction = intent.getAction(); + contentId = intent.getStringExtra(KEY_CONTENT_ID); startedInForeground |= intent.getBooleanExtra(KEY_FOREGROUND, false) || ACTION_RESTART.equals(intentAction); - contentId = intent.getStringExtra(KEY_CONTENT_ID); } // intentAction is null if the service is restarted or no action is specified. if (intentAction == null) { @@ -611,7 +617,9 @@ public abstract class DownloadService extends Service { // Do nothing. break; case ACTION_ADD_DOWNLOAD: - @Nullable DownloadRequest downloadRequest = intent.getParcelableExtra(KEY_DOWNLOAD_REQUEST); + @Nullable + DownloadRequest downloadRequest = + Assertions.checkNotNull(intent).getParcelableExtra(KEY_DOWNLOAD_REQUEST); if (downloadRequest == null) { Log.e(TAG, "Ignored ADD_DOWNLOAD: Missing " + KEY_DOWNLOAD_REQUEST + " extra"); } else { @@ -636,7 +644,7 @@ public abstract class DownloadService extends Service { downloadManager.pauseDownloads(); break; case ACTION_SET_STOP_REASON: - if (!intent.hasExtra(KEY_STOP_REASON)) { + if (!Assertions.checkNotNull(intent).hasExtra(KEY_STOP_REASON)) { Log.e(TAG, "Ignored SET_STOP_REASON: Missing " + KEY_STOP_REASON + " extra"); } else { int stopReason = intent.getIntExtra(KEY_STOP_REASON, /* defaultValue= */ 0); @@ -644,7 +652,9 @@ public abstract class DownloadService extends Service { } break; case ACTION_SET_REQUIREMENTS: - @Nullable Requirements requirements = intent.getParcelableExtra(KEY_REQUIREMENTS); + @Nullable + Requirements requirements = + Assertions.checkNotNull(intent).getParcelableExtra(KEY_REQUIREMENTS); if (requirements == null) { Log.e(TAG, "Ignored SET_REQUIREMENTS: Missing " + KEY_REQUIREMENTS + " extra"); } else { @@ -656,6 +666,12 @@ public abstract class DownloadService extends Service { break; } + if (Util.SDK_INT >= 26 && startedInForeground && foregroundNotificationUpdater != null) { + // From API level 26, services started in the foreground are required to show a notification. + foregroundNotificationUpdater.showNotificationIfNotAlready(); + } + + isStopped = false; if (downloadManager.isIdle()) { stop(); } @@ -671,9 +687,8 @@ public abstract class DownloadService extends Service { public void onDestroy() { isDestroyed = true; DownloadManagerHelper downloadManagerHelper = - Assertions.checkNotNull(downloadManagerListeners.get(getClass())); - boolean unschedule = !downloadManagerHelper.downloadManager.isWaitingForRequirements(); - downloadManagerHelper.detachService(this, unschedule); + Assertions.checkNotNull(downloadManagerHelpers.get(getClass())); + downloadManagerHelper.detachService(this); if (foregroundNotificationUpdater != null) { foregroundNotificationUpdater.stopPeriodicUpdates(); } @@ -682,8 +697,8 @@ public abstract class DownloadService extends Service { /** * Throws {@link UnsupportedOperationException} because this service is not designed to be bound. */ - @Nullable @Override + @Nullable public final IBinder onBind(Intent intent) { throw new UnsupportedOperationException(); } @@ -698,21 +713,21 @@ public abstract class DownloadService extends Service { * Returns a {@link Scheduler} to restart the service when requirements allowing downloads to take * place are met. If {@code null}, the service will only be restarted if the process is still in * memory when the requirements are met. + * + *

      This method is not called for services whose {@code foregroundNotificationId} is set to + * {@link #FOREGROUND_NOTIFICATION_ID_NONE}. Such services will only be restarted if the process + * is still in memory and considered non-idle, meaning that it's either in the foreground or was + * backgrounded within the last few minutes. */ - protected abstract @Nullable Scheduler getScheduler(); + @Nullable + protected abstract Scheduler getScheduler(); /** - * Returns a notification to be displayed when this service running in the foreground. This method - * is called when there is a download state change and periodically while there are active - * downloads. The periodic update interval can be set using {@link #DownloadService(int, long)}. - * - *

      On API level 26 and above, this method may also be called just before the service stops, - * with an empty {@code downloads} array. The returned notification is used to satisfy system - * requirements for foreground services. + * Returns a notification to be displayed when this service running in the foreground. * *

      Download services that do not wish to run in the foreground should be created by setting the * {@code foregroundNotificationId} constructor argument to {@link - * #FOREGROUND_NOTIFICATION_ID_NONE}. This method will not be called in this case, meaning it can + * #FOREGROUND_NOTIFICATION_ID_NONE}. This method is not called for such services, meaning it can * be implemented to throw {@link UnsupportedOperationException}. * * @param downloads The current downloads. @@ -731,29 +746,52 @@ public abstract class DownloadService extends Service { } /** - * Called when the state of a download changes. The default implementation is a no-op. - * - * @param download The new state of the download. + * @deprecated Some state change events may not be delivered to this method. Instead, use {@link + * DownloadManager#addListener(DownloadManager.Listener)} to register a listener directly to + * the {@link DownloadManager} that you return through {@link #getDownloadManager()}. */ + @Deprecated protected void onDownloadChanged(Download download) { // Do nothing. } /** - * Called when a download is removed. The default implementation is a no-op. - * - * @param download The last state of the download before it was removed. + * @deprecated Some download removal events may not be delivered to this method. Instead, use + * {@link DownloadManager#addListener(DownloadManager.Listener)} to register a listener + * directly to the {@link DownloadManager} that you return through {@link + * #getDownloadManager()}. */ + @Deprecated protected void onDownloadRemoved(Download download) { // Do nothing. } + /** + * Called after the service is created, once the downloads are known. + * + * @param downloads The current downloads. + */ + private void notifyDownloads(List downloads) { + if (foregroundNotificationUpdater != null) { + for (int i = 0; i < downloads.size(); i++) { + if (needsStartedService(downloads.get(i).state)) { + foregroundNotificationUpdater.startPeriodicUpdates(); + break; + } + } + } + } + + /** + * Called when the state of a download changes. + * + * @param download The state of the download. + */ + @SuppressWarnings("deprecation") private void notifyDownloadChanged(Download download) { onDownloadChanged(download); if (foregroundNotificationUpdater != null) { - if (download.state == Download.STATE_DOWNLOADING - || download.state == Download.STATE_REMOVING - || download.state == Download.STATE_RESTARTING) { + if (needsStartedService(download.state)) { foregroundNotificationUpdater.startPeriodicUpdates(); } else { foregroundNotificationUpdater.invalidate(); @@ -761,6 +799,12 @@ public abstract class DownloadService extends Service { } } + /** + * Called when a download is removed. + * + * @param download The last state of the download before it was removed. + */ + @SuppressWarnings("deprecation") private void notifyDownloadRemoved(Download download) { onDownloadRemoved(download); if (foregroundNotificationUpdater != null) { @@ -768,21 +812,29 @@ public abstract class DownloadService extends Service { } } + /** Returns whether the service is stopped. */ + private boolean isStopped() { + return isStopped; + } + 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(); + isStopped = true; } else { - stopSelfResult(lastStartId); + isStopped |= stopSelfResult(lastStartId); } } + private static boolean needsStartedService(@Download.State int state) { + return state == Download.STATE_DOWNLOADING + || state == Download.STATE_REMOVING + || state == Download.STATE_RESTARTING; + } + private static Intent getIntent( Context context, Class clazz, String action, boolean foreground) { return getIntent(context, clazz, action).putExtra(KEY_FOREGROUND, foreground); @@ -853,6 +905,7 @@ public abstract class DownloadService extends Service { private final Context context; private final DownloadManager downloadManager; + private final boolean foregroundAllowed; @Nullable private final Scheduler scheduler; private final Class serviceClass; @Nullable private DownloadService downloadService; @@ -860,38 +913,63 @@ public abstract class DownloadService extends Service { private DownloadManagerHelper( Context context, DownloadManager downloadManager, + boolean foregroundAllowed, @Nullable Scheduler scheduler, Class serviceClass) { this.context = context; this.downloadManager = downloadManager; + this.foregroundAllowed = foregroundAllowed; this.scheduler = scheduler; this.serviceClass = serviceClass; downloadManager.addListener(this); - if (scheduler != null) { - Requirements requirements = downloadManager.getRequirements(); - setSchedulerEnabled( - scheduler, /* enabled= */ !requirements.checkRequirements(context), requirements); - } + updateScheduler(); } public void attachService(DownloadService downloadService) { Assertions.checkState(this.downloadService == null); this.downloadService = downloadService; + if (downloadManager.isInitialized()) { + // The call to DownloadService.notifyDownloads is posted to avoid it being called directly + // from DownloadService.onCreate. This is a good idea because it may in turn call + // DownloadService.getForegroundNotification, and concrete subclass implementations may + // not anticipate the possibility of this method being called before their onCreate + // implementation has finished executing. + Util.createHandler() + .postAtFrontOfQueue( + () -> downloadService.notifyDownloads(downloadManager.getCurrentDownloads())); + } } - public void detachService(DownloadService downloadService, boolean unschedule) { + public void detachService(DownloadService downloadService) { Assertions.checkState(this.downloadService == downloadService); this.downloadService = null; - if (scheduler != null && unschedule) { + if (scheduler != null && !downloadManager.isWaitingForRequirements()) { scheduler.cancel(); } } + // DownloadManager.Listener implementation. + + @Override + public void onInitialized(DownloadManager downloadManager) { + if (downloadService != null) { + downloadService.notifyDownloads(downloadManager.getCurrentDownloads()); + } + } + @Override public void onDownloadChanged(DownloadManager downloadManager, Download download) { if (downloadService != null) { downloadService.notifyDownloadChanged(download); } + if (serviceMayNeedRestart() && needsStartedService(download.state)) { + // This shouldn't happen unless (a) application code is changing the downloads by calling + // the DownloadManager directly rather than sending actions through the service, or (b) if + // the service is background only and a previous attempt to start it was prevented. Try and + // restart the service to robust against such cases. + Log.w(TAG, "DownloadService wasn't running. Restarting."); + restartService(); + } } @Override @@ -909,35 +987,62 @@ public abstract class DownloadService extends Service { } @Override - public void onRequirementsStateChanged( - DownloadManager downloadManager, - Requirements requirements, - @Requirements.RequirementFlags int notMetRequirements) { - boolean requirementsMet = notMetRequirements == 0; - if (downloadService == null && requirementsMet) { + public void onWaitingForRequirementsChanged( + DownloadManager downloadManager, boolean waitingForRequirements) { + if (!waitingForRequirements + && !downloadManager.getDownloadsPaused() + && serviceMayNeedRestart()) { + // We're no longer waiting for requirements and downloads aren't paused, meaning the manager + // will be able to resume downloads that are currently queued. If there exist queued + // downloads then we should ensure the service is started. + List downloads = downloadManager.getCurrentDownloads(); + for (int i = 0; i < downloads.size(); i++) { + if (downloads.get(i).state == Download.STATE_QUEUED) { + restartService(); + break; + } + } + } + updateScheduler(); + } + + // Internal methods. + + private boolean serviceMayNeedRestart() { + return downloadService == null || downloadService.isStopped(); + } + + private void restartService() { + if (foregroundAllowed) { + Intent intent = getIntent(context, serviceClass, DownloadService.ACTION_RESTART); + Util.startForegroundService(context, intent); + } else { + // The service is background only. Use ACTION_INIT rather than ACTION_RESTART because + // ACTION_RESTART is handled as though KEY_FOREGROUND is set to true. try { Intent intent = getIntent(context, serviceClass, DownloadService.ACTION_INIT); context.startService(intent); } catch (IllegalStateException e) { - /* startService fails if the app is in the background then don't stop the scheduler. */ - return; + // The process is classed as idle by the platform. Starting a background service is not + // allowed in this state. + Log.w(TAG, "Failed to restart DownloadService (process is idle)."); } } - if (scheduler != null) { - setSchedulerEnabled(scheduler, /* enabled= */ !requirementsMet, requirements); - } } - private void setSchedulerEnabled( - Scheduler scheduler, boolean enabled, Requirements requirements) { - if (!enabled) { - scheduler.cancel(); - } else { + private void updateScheduler() { + if (scheduler == null) { + return; + } + if (downloadManager.isWaitingForRequirements()) { String servicePackage = context.getPackageName(); + Requirements requirements = downloadManager.getRequirements(); boolean success = scheduler.schedule(requirements, servicePackage, ACTION_RESTART); if (!success) { Log.e(TAG, "Scheduling downloads failed."); } + } else { + scheduler.cancel(); } } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/Downloader.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/Downloader.java index fa10d5842b..98079bf200 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/Downloader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/Downloader.java @@ -28,6 +28,10 @@ public interface Downloader { /** * Called when progress is made during a download operation. * + *

      May be called directly from {@link #download}, or from any other thread used by the + * downloader. In all cases, {@link #download} is guaranteed not to return until after the last + * call to {@link #onProgress} has finished executing. + * * @param contentLength The length of the content in bytes, or {@link C#LENGTH_UNSET} if * unknown. * @param bytesDownloaded The number of bytes that have been downloaded. @@ -42,19 +46,13 @@ public interface Downloader { * * @param progressListener A listener to receive progress updates, or {@code null}. * @throws DownloadException Thrown if the content cannot be downloaded. - * @throws InterruptedException If the thread has been interrupted. - * @throws IOException Thrown when there is an io error while downloading. + * @throws IOException If the download did not complete successfully. */ - void download(@Nullable ProgressListener progressListener) - throws InterruptedException, IOException; + void download(@Nullable ProgressListener progressListener) throws IOException; /** Cancels the download operation and prevents future download operations from running. */ void cancel(); - /** - * Removes the content. - * - * @throws InterruptedException Thrown if the thread was interrupted. - */ - void remove() throws InterruptedException; + /** Removes the content. */ + void remove(); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloaderConstructorHelper.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloaderConstructorHelper.java deleted file mode 100644 index 0d53b3cde0..0000000000 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloaderConstructorHelper.java +++ /dev/null @@ -1,170 +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.offline; - -import androidx.annotation.Nullable; -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.DummyDataSource; -import com.google.android.exoplayer2.upstream.FileDataSource; -import com.google.android.exoplayer2.upstream.PriorityDataSourceFactory; -import com.google.android.exoplayer2.upstream.cache.Cache; -import com.google.android.exoplayer2.upstream.cache.CacheDataSink; -import com.google.android.exoplayer2.upstream.cache.CacheDataSinkFactory; -import com.google.android.exoplayer2.upstream.cache.CacheDataSource; -import com.google.android.exoplayer2.upstream.cache.CacheDataSourceFactory; -import com.google.android.exoplayer2.upstream.cache.CacheKeyFactory; -import com.google.android.exoplayer2.upstream.cache.CacheUtil; -import com.google.android.exoplayer2.util.PriorityTaskManager; - -/** A helper class that holds necessary parameters for {@link Downloader} construction. */ -public final class DownloaderConstructorHelper { - - private final Cache cache; - @Nullable private final CacheKeyFactory cacheKeyFactory; - @Nullable private final PriorityTaskManager priorityTaskManager; - private final CacheDataSourceFactory onlineCacheDataSourceFactory; - private final CacheDataSourceFactory offlineCacheDataSourceFactory; - - /** - * @param cache Cache instance to be used to store downloaded data. - * @param upstreamFactory A {@link DataSource.Factory} for creating {@link DataSource}s for - * downloading data. - */ - public DownloaderConstructorHelper(Cache cache, DataSource.Factory upstreamFactory) { - this( - cache, - upstreamFactory, - /* cacheReadDataSourceFactory= */ null, - /* cacheWriteDataSinkFactory= */ null, - /* priorityTaskManager= */ null); - } - - /** - * @param cache Cache instance to be used to store downloaded data. - * @param upstreamFactory A {@link DataSource.Factory} for creating {@link DataSource}s for - * downloading data. - * @param cacheReadDataSourceFactory A {@link DataSource.Factory} for creating {@link DataSource}s - * for reading data from the cache. If null then a {@link FileDataSource.Factory} will be - * used. - * @param cacheWriteDataSinkFactory A {@link DataSink.Factory} for creating {@link DataSource}s - * for writing data to the cache. If null then a {@link CacheDataSinkFactory} will be used. - * @param priorityTaskManager A {@link PriorityTaskManager} to use when downloading. If non-null, - * downloaders will register as tasks with priority {@link C#PRIORITY_DOWNLOAD} whilst - * downloading. - */ - public DownloaderConstructorHelper( - Cache cache, - DataSource.Factory upstreamFactory, - @Nullable DataSource.Factory cacheReadDataSourceFactory, - @Nullable DataSink.Factory cacheWriteDataSinkFactory, - @Nullable PriorityTaskManager priorityTaskManager) { - this( - cache, - upstreamFactory, - cacheReadDataSourceFactory, - cacheWriteDataSinkFactory, - priorityTaskManager, - /* cacheKeyFactory= */ null); - } - - /** - * @param cache Cache instance to be used to store downloaded data. - * @param upstreamFactory A {@link DataSource.Factory} for creating {@link DataSource}s for - * downloading data. - * @param cacheReadDataSourceFactory A {@link DataSource.Factory} for creating {@link DataSource}s - * for reading data from the cache. If null then a {@link FileDataSource.Factory} will be - * used. - * @param cacheWriteDataSinkFactory A {@link DataSink.Factory} for creating {@link DataSource}s - * for writing data to the cache. If null then a {@link CacheDataSinkFactory} will be used. - * @param priorityTaskManager A {@link PriorityTaskManager} to use when downloading. If non-null, - * downloaders will register as tasks with priority {@link C#PRIORITY_DOWNLOAD} whilst - * downloading. - * @param cacheKeyFactory An optional factory for cache keys. - */ - public DownloaderConstructorHelper( - Cache cache, - DataSource.Factory upstreamFactory, - @Nullable DataSource.Factory cacheReadDataSourceFactory, - @Nullable DataSink.Factory cacheWriteDataSinkFactory, - @Nullable PriorityTaskManager priorityTaskManager, - @Nullable CacheKeyFactory cacheKeyFactory) { - if (priorityTaskManager != null) { - upstreamFactory = - new PriorityDataSourceFactory(upstreamFactory, priorityTaskManager, C.PRIORITY_DOWNLOAD); - } - DataSource.Factory readDataSourceFactory = - cacheReadDataSourceFactory != null - ? cacheReadDataSourceFactory - : new FileDataSource.Factory(); - if (cacheWriteDataSinkFactory == null) { - cacheWriteDataSinkFactory = - new CacheDataSinkFactory(cache, CacheDataSink.DEFAULT_FRAGMENT_SIZE); - } - onlineCacheDataSourceFactory = - new CacheDataSourceFactory( - cache, - upstreamFactory, - readDataSourceFactory, - cacheWriteDataSinkFactory, - CacheDataSource.FLAG_BLOCK_ON_CACHE, - /* eventListener= */ null, - cacheKeyFactory); - offlineCacheDataSourceFactory = - new CacheDataSourceFactory( - cache, - DummyDataSource.FACTORY, - readDataSourceFactory, - null, - CacheDataSource.FLAG_BLOCK_ON_CACHE, - /* eventListener= */ null, - cacheKeyFactory); - this.cache = cache; - this.priorityTaskManager = priorityTaskManager; - this.cacheKeyFactory = cacheKeyFactory; - } - - /** Returns the {@link Cache} instance. */ - public Cache getCache() { - return cache; - } - - /** Returns the {@link CacheKeyFactory}. */ - public CacheKeyFactory getCacheKeyFactory() { - return cacheKeyFactory != null ? cacheKeyFactory : CacheUtil.DEFAULT_CACHE_KEY_FACTORY; - } - - /** Returns a {@link PriorityTaskManager} instance. */ - public PriorityTaskManager getPriorityTaskManager() { - // Return a dummy PriorityTaskManager if none is provided. Create a new PriorityTaskManager - // each time so clients don't affect each other over the dummy PriorityTaskManager instance. - return priorityTaskManager != null ? priorityTaskManager : new PriorityTaskManager(); - } - - /** Returns a new {@link CacheDataSource} instance. */ - public CacheDataSource createCacheDataSource() { - return onlineCacheDataSourceFactory.createDataSource(); - } - - /** - * Returns a new {@link CacheDataSource} instance which accesses cache read-only and throws an - * exception on cache miss. - */ - public CacheDataSource createOfflineCacheDataSource() { - return offlineCacheDataSourceFactory.createDataSource(); - } -} 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 a73258272c..6ad186b575 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 @@ -19,85 +19,98 @@ import android.net.Uri; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.upstream.DataSpec; -import com.google.android.exoplayer2.upstream.cache.Cache; import com.google.android.exoplayer2.upstream.cache.CacheDataSource; -import com.google.android.exoplayer2.upstream.cache.CacheKeyFactory; import com.google.android.exoplayer2.upstream.cache.CacheUtil; import com.google.android.exoplayer2.util.PriorityTaskManager; import java.io.IOException; +import java.util.concurrent.Executor; import java.util.concurrent.atomic.AtomicBoolean; -/** - * A downloader for progressive media streams. - * - *

      The downloader attempts to download the entire media bytes referenced by a {@link Uri} into a - * cache as defined by {@link DownloaderConstructorHelper}. Callers can use the constructor to - * specify a custom cache key for the downloaded bytes. - * - *

      The downloader will avoid downloading already-downloaded media bytes. - */ +/** A downloader for progressive media streams. */ public final class ProgressiveDownloader implements Downloader { private static final int BUFFER_SIZE_BYTES = 128 * 1024; private final DataSpec dataSpec; - private final Cache cache; private final CacheDataSource dataSource; - private final CacheKeyFactory cacheKeyFactory; - private final PriorityTaskManager priorityTaskManager; private final AtomicBoolean isCanceled; + @Nullable private volatile Thread downloadThread; + /** * @param uri Uri of the data to be downloaded. * @param customCacheKey A custom key that uniquely identifies the original stream. Used for cache * indexing. May be null. - * @param constructorHelper A {@link DownloaderConstructorHelper} instance. + * @param cacheDataSourceFactory A {@link CacheDataSource.Factory} for the cache into which the + * download will be written. */ public ProgressiveDownloader( - Uri uri, @Nullable String customCacheKey, DownloaderConstructorHelper constructorHelper) { - this.dataSpec = - new DataSpec( - uri, - /* absoluteStreamPosition= */ 0, - C.LENGTH_UNSET, - customCacheKey, - /* flags= */ DataSpec.FLAG_ALLOW_CACHE_FRAGMENTATION); - this.cache = constructorHelper.getCache(); - this.dataSource = constructorHelper.createCacheDataSource(); - this.cacheKeyFactory = constructorHelper.getCacheKeyFactory(); - this.priorityTaskManager = constructorHelper.getPriorityTaskManager(); + Uri uri, @Nullable String customCacheKey, CacheDataSource.Factory cacheDataSourceFactory) { + this(uri, customCacheKey, cacheDataSourceFactory, Runnable::run); + } + + /** + * @param uri Uri of the data to be downloaded. + * @param customCacheKey A custom key that uniquely identifies the original stream. Used for cache + * indexing. May be null. + * @param cacheDataSourceFactory A {@link CacheDataSource.Factory} for the cache into which the + * download will be written. + * @param executor An {@link Executor} used to make requests for the media being downloaded. In + * the future, providing an {@link Executor} that uses multiple threads may speed up the + * download by allowing parts of it to be executed in parallel. + */ + public ProgressiveDownloader( + Uri uri, + @Nullable String customCacheKey, + CacheDataSource.Factory cacheDataSourceFactory, + Executor executor) { + dataSpec = + new DataSpec.Builder() + .setUri(uri) + .setKey(customCacheKey) + .setFlags(DataSpec.FLAG_ALLOW_CACHE_FRAGMENTATION) + .build(); + dataSource = cacheDataSourceFactory.createDataSourceForDownloading(); isCanceled = new AtomicBoolean(); } @Override - public void download(@Nullable ProgressListener progressListener) - throws InterruptedException, IOException { - priorityTaskManager.add(C.PRIORITY_DOWNLOAD); + public void download(@Nullable ProgressListener progressListener) throws IOException { + downloadThread = Thread.currentThread(); + if (isCanceled.get()) { + return; + } + @Nullable PriorityTaskManager priorityTaskManager = dataSource.getUpstreamPriorityTaskManager(); + if (priorityTaskManager != null) { + priorityTaskManager.add(C.PRIORITY_DOWNLOAD); + } try { CacheUtil.cache( - dataSpec, - cache, - cacheKeyFactory, dataSource, - new byte[BUFFER_SIZE_BYTES], - priorityTaskManager, - C.PRIORITY_DOWNLOAD, + dataSpec, progressListener == null ? null : new ProgressForwarder(progressListener), isCanceled, - /* enableEOFException= */ true); + /* enableEOFException= */ true, + /* temporaryBuffer= */ new byte[BUFFER_SIZE_BYTES]); } finally { - priorityTaskManager.remove(C.PRIORITY_DOWNLOAD); + if (priorityTaskManager != null) { + priorityTaskManager.remove(C.PRIORITY_DOWNLOAD); + } } } @Override public void cancel() { isCanceled.set(true); + @Nullable Thread downloadThread = this.downloadThread; + if (downloadThread != null) { + downloadThread.interrupt(); + } } @Override public void remove() { - CacheUtil.remove(dataSpec, cache, cacheKeyFactory); + CacheUtil.remove(dataSpec, dataSource.getCache(), dataSource.getCacheKeyFactory()); } private static final class ProgressForwarder implements CacheUtil.ProgressListener { 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 5155685999..601945c69d 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 @@ -21,6 +21,8 @@ import androidx.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.ParsingLoadable; +import com.google.android.exoplayer2.upstream.ParsingLoadable.Parser; import com.google.android.exoplayer2.upstream.cache.Cache; import com.google.android.exoplayer2.upstream.cache.CacheDataSource; import com.google.android.exoplayer2.upstream.cache.CacheKeyFactory; @@ -33,6 +35,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; +import java.util.concurrent.Executor; import java.util.concurrent.atomic.AtomicBoolean; /** @@ -67,44 +70,56 @@ public abstract class SegmentDownloader> impleme private static final long MAX_MERGED_SEGMENT_START_TIME_DIFF_US = 20 * C.MICROS_PER_SECOND; private final DataSpec manifestDataSpec; - private final Cache cache; - private final CacheDataSource dataSource; - private final CacheDataSource offlineDataSource; - private final CacheKeyFactory cacheKeyFactory; - private final PriorityTaskManager priorityTaskManager; + private final Parser manifestParser; private final ArrayList streamKeys; + private final CacheDataSource.Factory cacheDataSourceFactory; + private final Executor executor; private final AtomicBoolean isCanceled; + @Nullable private volatile Thread downloadThread; + /** * @param manifestUri The {@link Uri} of the manifest to be downloaded. + * @param manifestParser A parser for the manifest. * @param streamKeys Keys defining which streams in the manifest should be selected for download. * If empty, all streams are downloaded. - * @param constructorHelper A {@link DownloaderConstructorHelper} instance. + * @param cacheDataSourceFactory A {@link CacheDataSource.Factory} for the cache into which the + * download will be written. + * @param executor An {@link Executor} used to make requests for the media being downloaded. + * Providing an {@link Executor} that uses multiple threads will speed up the download by + * allowing parts of it to be executed in parallel. */ public SegmentDownloader( - Uri manifestUri, List streamKeys, DownloaderConstructorHelper constructorHelper) { + Uri manifestUri, + Parser manifestParser, + List streamKeys, + CacheDataSource.Factory cacheDataSourceFactory, + Executor executor) { this.manifestDataSpec = getCompressibleDataSpec(manifestUri); + this.manifestParser = manifestParser; this.streamKeys = new ArrayList<>(streamKeys); - this.cache = constructorHelper.getCache(); - this.dataSource = constructorHelper.createCacheDataSource(); - this.offlineDataSource = constructorHelper.createOfflineCacheDataSource(); - this.cacheKeyFactory = constructorHelper.getCacheKeyFactory(); - this.priorityTaskManager = constructorHelper.getPriorityTaskManager(); + this.cacheDataSourceFactory = cacheDataSourceFactory; + this.executor = executor; isCanceled = new AtomicBoolean(); } - /** - * Downloads the selected streams in the media. If multiple streams are selected, they are - * downloaded in sync with one another. - * - * @throws IOException Thrown when there is an error downloading. - * @throws InterruptedException If the thread has been interrupted. - */ @Override - public final void download(@Nullable ProgressListener progressListener) - throws IOException, InterruptedException { - priorityTaskManager.add(C.PRIORITY_DOWNLOAD); + public final void download(@Nullable ProgressListener progressListener) throws IOException { + downloadThread = Thread.currentThread(); + if (isCanceled.get()) { + return; + } + @Nullable + PriorityTaskManager priorityTaskManager = + cacheDataSourceFactory.getUpstreamPriorityTaskManager(); + if (priorityTaskManager != null) { + priorityTaskManager.add(C.PRIORITY_DOWNLOAD); + } try { + Cache cache = Assertions.checkNotNull(cacheDataSourceFactory.getCache()); + CacheKeyFactory cacheKeyFactory = cacheDataSourceFactory.getCacheKeyFactory(); + CacheDataSource dataSource = cacheDataSourceFactory.createDataSourceForDownloading(); + // Get the manifest and all of the segments. M manifest = getManifest(dataSource, manifestDataSpec); if (!streamKeys.isEmpty()) { @@ -141,70 +156,77 @@ public abstract class SegmentDownloader> impleme } // Download the segments. - @Nullable ProgressNotifier progressNotifier = null; - if (progressListener != null) { - progressNotifier = - new ProgressNotifier( - progressListener, - contentLength, - totalSegments, - bytesDownloaded, - segmentsDownloaded); - } - byte[] buffer = new byte[BUFFER_SIZE_BYTES]; + @Nullable + ProgressNotifier progressNotifier = + progressListener != null + ? new ProgressNotifier( + progressListener, + contentLength, + totalSegments, + bytesDownloaded, + segmentsDownloaded) + : null; + byte[] temporaryBuffer = new byte[BUFFER_SIZE_BYTES]; for (int i = 0; i < segments.size(); i++) { CacheUtil.cache( - segments.get(i).dataSpec, - cache, - cacheKeyFactory, dataSource, - buffer, - priorityTaskManager, - C.PRIORITY_DOWNLOAD, + segments.get(i).dataSpec, progressNotifier, isCanceled, - true); + /* enableEOFException= */ true, + temporaryBuffer); if (progressNotifier != null) { progressNotifier.onSegmentDownloaded(); } } } finally { - priorityTaskManager.remove(C.PRIORITY_DOWNLOAD); + if (priorityTaskManager != null) { + priorityTaskManager.remove(C.PRIORITY_DOWNLOAD); + } } } @Override public void cancel() { isCanceled.set(true); + @Nullable Thread downloadThread = this.downloadThread; + if (downloadThread != null) { + downloadThread.interrupt(); + } } @Override - public final void remove() throws InterruptedException { + public final void remove() { + Cache cache = Assertions.checkNotNull(cacheDataSourceFactory.getCache()); + CacheKeyFactory cacheKeyFactory = cacheDataSourceFactory.getCacheKeyFactory(); + CacheDataSource dataSource = cacheDataSourceFactory.createDataSourceForRemovingDownload(); try { - M manifest = getManifest(offlineDataSource, manifestDataSpec); - List segments = getSegments(offlineDataSource, manifest, true); + M manifest = getManifest(dataSource, manifestDataSpec); + List segments = getSegments(dataSource, manifest, true); for (int i = 0; i < segments.size(); i++) { - removeDataSpec(segments.get(i).dataSpec); + CacheUtil.remove(segments.get(i).dataSpec, cache, cacheKeyFactory); } } catch (IOException e) { // Ignore exceptions when removing. } finally { // Always attempt to remove the manifest. - removeDataSpec(manifestDataSpec); + CacheUtil.remove(manifestDataSpec, cache, cacheKeyFactory); } } // Internal methods. /** - * Loads and parses the manifest. + * Loads and parses a manifest. * * @param dataSource The {@link DataSource} through which to load. * @param dataSpec The manifest {@link DataSpec}. * @return The manifest. * @throws IOException If an error occurs reading data. */ - protected abstract M getManifest(DataSource dataSource, DataSpec dataSpec) throws IOException; + protected final M getManifest(DataSource dataSource, DataSpec dataSpec) throws IOException { + return ParsingLoadable.load(dataSource, manifestParser, dataSpec, C.DATA_TYPE_MANIFEST); + } /** * Returns a list of all downloadable {@link Segment}s for a given manifest. @@ -215,25 +237,14 @@ public abstract class SegmentDownloader> impleme * segments from being listed. If true then a partial segment list will be returned. If false * an {@link IOException} will be thrown. * @return The list of downloadable {@link Segment}s. - * @throws InterruptedException Thrown if the thread was interrupted. * @throws IOException Thrown if {@code allowPartialIndex} is false and a load error occurs, or if * the media is not in a form that allows for its segments to be listed. */ protected abstract List getSegments( - DataSource dataSource, M manifest, boolean allowIncompleteList) - throws InterruptedException, IOException; - - private void removeDataSpec(DataSpec dataSpec) { - CacheUtil.remove(dataSpec, cache, cacheKeyFactory); - } + DataSource dataSource, M manifest, boolean allowIncompleteList) throws IOException; protected static DataSpec getCompressibleDataSpec(Uri uri) { - return new DataSpec( - uri, - /* absoluteStreamPosition= */ 0, - /* length= */ C.LENGTH_UNSET, - /* key= */ null, - /* flags= */ DataSpec.FLAG_ALLOW_GZIP); + return new DataSpec.Builder().setUri(uri).setFlags(DataSpec.FLAG_ALLOW_GZIP).build(); } private static void mergeSegments(List segments, CacheKeyFactory keyFactory) { @@ -267,7 +278,7 @@ public abstract class SegmentDownloader> impleme private static boolean canMergeSegments(DataSpec dataSpec1, DataSpec dataSpec2) { return dataSpec1.uri.equals(dataSpec2.uri) && dataSpec1.length != C.LENGTH_UNSET - && (dataSpec1.absoluteStreamPosition + dataSpec1.length == dataSpec2.absoluteStreamPosition) + && (dataSpec1.position + dataSpec1.length == dataSpec2.position) && Util.areEqual(dataSpec1.key, dataSpec2.key) && dataSpec1.flags == dataSpec2.flags && dataSpec1.httpMethod == dataSpec2.httpMethod diff --git a/library/core/src/main/java/com/google/android/exoplayer2/scheduler/PlatformScheduler.java b/library/core/src/main/java/com/google/android/exoplayer2/scheduler/PlatformScheduler.java index 752239c991..c4861abdf3 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/scheduler/PlatformScheduler.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/scheduler/PlatformScheduler.java @@ -15,7 +15,6 @@ */ package com.google.android.exoplayer2.scheduler; -import android.annotation.TargetApi; import android.app.job.JobInfo; import android.app.job.JobParameters; import android.app.job.JobScheduler; @@ -24,6 +23,7 @@ import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.os.PersistableBundle; +import androidx.annotation.RequiresApi; import androidx.annotation.RequiresPermission; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Log; @@ -42,7 +42,7 @@ import com.google.android.exoplayer2.util.Util; * android:exported="true"/> * } */ -@TargetApi(21) +@RequiresApi(21) public final class PlatformScheduler implements Scheduler { private static final boolean DEBUG = false; @@ -64,6 +64,7 @@ public final class PlatformScheduler implements Scheduler { */ @RequiresPermission(android.Manifest.permission.RECEIVE_BOOT_COMPLETED) public PlatformScheduler(Context context, int jobId) { + context = context.getApplicationContext(); this.jobId = jobId; jobServiceComponentName = new ComponentName(context, PlatformSchedulerService.class); jobScheduler = (JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/scheduler/Requirements.java b/library/core/src/main/java/com/google/android/exoplayer2/scheduler/Requirements.java index 35f8e37dcf..8919a26720 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/scheduler/Requirements.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/scheduler/Requirements.java @@ -163,9 +163,10 @@ public final class Requirements implements Parcelable { } private static boolean isInternetConnectivityValidated(ConnectivityManager connectivityManager) { - if (Util.SDK_INT < 23) { - // TODO Check internet connectivity using http://clients3.google.com/generate_204 on API - // levels prior to 23. + // It's possible to query NetworkCapabilities from API level 23, but RequirementsWatcher only + // fires an event to update its Requirements when NetworkCapabilities change from API level 24. + // Since Requirements won't be updated, we assume connectivity is validated on API level 23. + if (Util.SDK_INT < 24) { return true; } Network activeNetwork = connectivityManager.getActiveNetwork(); @@ -174,10 +175,8 @@ public final class Requirements implements Parcelable { } NetworkCapabilities networkCapabilities = connectivityManager.getNetworkCapabilities(activeNetwork); - boolean validated = - networkCapabilities == null - || !networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED); - return !validated; + return networkCapabilities != null + && networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED); } @Override 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 0d9b8261d9..797b7f7170 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 @@ -15,7 +15,6 @@ */ package com.google.android.exoplayer2.scheduler; -import android.annotation.TargetApi; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; @@ -23,7 +22,6 @@ import android.content.IntentFilter; import android.net.ConnectivityManager; import android.net.Network; import android.net.NetworkCapabilities; -import android.net.NetworkRequest; import android.os.Handler; import android.os.Looper; import android.os.PowerManager; @@ -62,7 +60,7 @@ public final class RequirementsWatcher { @Nullable private DeviceStatusChangeReceiver receiver; @Requirements.RequirementFlags private int notMetRequirements; - @Nullable private CapabilityValidatedCallback networkCallback; + @Nullable private NetworkCallback networkCallback; /** * @param context Any context. @@ -88,8 +86,8 @@ public final class RequirementsWatcher { IntentFilter filter = new IntentFilter(); if (requirements.isNetworkRequired()) { - if (Util.SDK_INT >= 23) { - registerNetworkCallbackV23(); + if (Util.SDK_INT >= 24) { + registerNetworkCallbackV24(); } else { filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION); } @@ -115,8 +113,8 @@ public final class RequirementsWatcher { public void stop() { context.unregisterReceiver(Assertions.checkNotNull(receiver)); receiver = null; - if (networkCallback != null) { - unregisterNetworkCallback(); + if (Util.SDK_INT >= 24 && networkCallback != null) { + unregisterNetworkCallbackV24(); } } @@ -125,26 +123,21 @@ public final class RequirementsWatcher { return requirements; } - @TargetApi(23) - private void registerNetworkCallbackV23() { + @RequiresApi(24) + private void registerNetworkCallbackV24() { ConnectivityManager connectivityManager = Assertions.checkNotNull( (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE)); - NetworkRequest request = - new NetworkRequest.Builder() - .addCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED) - .build(); - networkCallback = new CapabilityValidatedCallback(); - connectivityManager.registerNetworkCallback(request, networkCallback); + networkCallback = new NetworkCallback(); + connectivityManager.registerDefaultNetworkCallback(networkCallback); } - private void unregisterNetworkCallback() { - if (Util.SDK_INT >= 21) { - ConnectivityManager connectivityManager = - (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); - connectivityManager.unregisterNetworkCallback(Assertions.checkNotNull(networkCallback)); - networkCallback = null; - } + @RequiresApi(24) + private void unregisterNetworkCallbackV24() { + ConnectivityManager connectivityManager = + (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); + connectivityManager.unregisterNetworkCallback(Assertions.checkNotNull(networkCallback)); + networkCallback = null; } private void checkRequirements() { @@ -165,8 +158,11 @@ public final class RequirementsWatcher { } } - @RequiresApi(api = 21) - private final class CapabilityValidatedCallback extends ConnectivityManager.NetworkCallback { + @RequiresApi(24) + private final class NetworkCallback extends ConnectivityManager.NetworkCallback { + boolean receivedCapabilitiesChange; + boolean networkValidated; + @Override public void onAvailable(Network network) { onNetworkCallback(); @@ -177,6 +173,17 @@ public final class RequirementsWatcher { onNetworkCallback(); } + @Override + public void onCapabilitiesChanged(Network network, NetworkCapabilities networkCapabilities) { + boolean networkValidated = + networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED); + if (!receivedCapabilitiesChange || this.networkValidated != networkValidated) { + receivedCapabilitiesChange = true; + this.networkValidated = networkValidated; + onNetworkCallback(); + } + } + private void onNetworkCallback() { handler.post( () -> { 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 86e00e0a37..461b146b8d 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,8 +19,10 @@ import android.os.Handler; import android.os.Looper; import androidx.annotation.Nullable; import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.drm.DrmSessionEventListener; import com.google.android.exoplayer2.upstream.TransferListener; import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.MediaSourceEventDispatcher; import java.util.ArrayList; import java.util.HashSet; @@ -106,7 +108,7 @@ public abstract class BaseMediaSource implements MediaSource { */ protected final MediaSourceEventListener.EventDispatcher createEventDispatcher( MediaPeriodId mediaPeriodId, long mediaTimeOffsetMs) { - Assertions.checkArgument(mediaPeriodId != null); + Assertions.checkNotNull(mediaPeriodId); return eventDispatcher.withParameters(/* windowIndex= */ 0, mediaPeriodId, mediaTimeOffsetMs); } @@ -132,12 +134,44 @@ public abstract class BaseMediaSource implements MediaSource { @Override public final void addEventListener(Handler handler, MediaSourceEventListener eventListener) { - eventDispatcher.addEventListener(handler, eventListener); + addEventListenerInternal(handler, eventListener, MediaSourceEventListener.class); } @Override public final void removeEventListener(MediaSourceEventListener eventListener) { - eventDispatcher.removeEventListener(eventListener); + removeEventListenerInternal(eventListener, MediaSourceEventListener.class); + } + + @Override + public final void addDrmEventListener(Handler handler, DrmSessionEventListener eventListener) { + addEventListenerInternal(handler, eventListener, DrmSessionEventListener.class); + } + + @Override + public final void removeDrmEventListener(DrmSessionEventListener eventListener) { + removeEventListenerInternal(eventListener, DrmSessionEventListener.class); + } + + /** + * Adds a listener to the internal {@link MediaSourceEventDispatcher} with the provided type. + * + *

      NOTE: Read the caveats on {@link MediaSourceEventDispatcher#addEventListener(Handler, + * Object, Class)} when deciding what value to pass for {@code listenerClass}. + * + * @see MediaSourceEventDispatcher#addEventListener(Handler, Object, Class) + */ + protected final void addEventListenerInternal( + Handler handler, T eventListener, Class listenerClass) { + eventDispatcher.addEventListener(handler, eventListener, listenerClass); + } + + /** + * Removes a listener from the internal {@link MediaSourceEventDispatcher}. + * + * @see MediaSourceEventDispatcher#removeEventListener(Object, Class) + */ + protected final void removeEventListenerInternal(T eventListener, Class listenerClass) { + eventDispatcher.removeEventListener(eventListener, listenerClass); } @Override @@ -145,7 +179,7 @@ public abstract class BaseMediaSource implements MediaSource { MediaSourceCaller caller, @Nullable TransferListener mediaTransferListener) { Looper looper = Looper.myLooper(); Assertions.checkArgument(this.looper == null || this.looper == looper); - Timeline timeline = this.timeline; + @Nullable Timeline timeline = this.timeline; mediaSourceCallers.add(caller); if (this.looper == null) { this.looper = looper; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/BundledExtractorsAdapter.java b/library/core/src/main/java/com/google/android/exoplayer2/source/BundledExtractorsAdapter.java new file mode 100644 index 0000000000..f8764585aa --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/BundledExtractorsAdapter.java @@ -0,0 +1,118 @@ +/* + * Copyright (C) 2020 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; + +import android.net.Uri; +import androidx.annotation.Nullable; +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.ExtractorOutput; +import com.google.android.exoplayer2.extractor.PositionHolder; +import com.google.android.exoplayer2.extractor.mp3.Mp3Extractor; +import com.google.android.exoplayer2.upstream.DataReader; +import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.Util; +import java.io.EOFException; +import java.io.IOException; + +/** + * {@link ProgressiveMediaExtractor} built on top of {@link Extractor} instances, whose + * implementation classes are bundled in the app. + */ +/* package */ final class BundledExtractorsAdapter implements ProgressiveMediaExtractor { + + private final Extractor[] extractors; + + @Nullable private Extractor extractor; + @Nullable private ExtractorInput extractorInput; + + /** + * Creates a holder that will select an extractor and initialize it using the specified output. + * + * @param extractors One or more extractors to choose from. + */ + public BundledExtractorsAdapter(Extractor[] extractors) { + this.extractors = extractors; + } + + @Override + public void init( + DataReader dataReader, Uri uri, long position, long length, ExtractorOutput output) + throws IOException { + extractorInput = new DefaultExtractorInput(dataReader, position, length); + if (extractor != null) { + return; + } + if (extractors.length == 1) { + this.extractor = extractors[0]; + } else { + for (Extractor extractor : extractors) { + try { + if (extractor.sniff(extractorInput)) { + this.extractor = extractor; + break; + } + } catch (EOFException e) { + // Do nothing. + } finally { + extractorInput.resetPeekPosition(); + } + } + if (extractor == null) { + throw new UnrecognizedInputFormatException( + "None of the available extractors (" + + Util.getCommaDelimitedSimpleClassNames(extractors) + + ") could read the stream.", + Assertions.checkNotNull(uri)); + } + } + extractor.init(output); + } + + @Override + public void release() { + if (extractor != null) { + extractor.release(); + extractor = null; + } + extractorInput = null; + } + + @Override + public void disableSeekingOnMp3Streams() { + if (extractor instanceof Mp3Extractor) { + ((Mp3Extractor) extractor).disableSeeking(); + } + } + + @Override + public long getCurrentInputPosition() { + return extractorInput != null ? extractorInput.getPosition() : C.POSITION_UNSET; + } + + @Override + public void seek(long position, long seekTimeUs) { + Assertions.checkNotNull(extractor).seek(position, seekTimeUs); + } + + @Override + public int read(PositionHolder positionHolder) throws IOException { + return Assertions.checkNotNull(extractor) + .read(Assertions.checkNotNull(extractorInput), positionHolder); + } +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaPeriod.java index 8aafb9a0e5..c5484a8f45 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaPeriod.java @@ -174,7 +174,7 @@ public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callb @Override public long seekToUs(long positionUs) { pendingInitialDiscontinuityPositionUs = C.TIME_UNSET; - for (ClippingSampleStream sampleStream : sampleStreams) { + for (@Nullable ClippingSampleStream sampleStream : sampleStreams) { if (sampleStream != null) { sampleStream.clearSentEos(); } @@ -310,21 +310,27 @@ public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callb buffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM); return C.RESULT_BUFFER_READ; } - int result = childStream.readData(formatHolder, buffer, requireFormat); + @ReadDataResult int result = childStream.readData(formatHolder, buffer, requireFormat); if (result == C.RESULT_FORMAT_READ) { Format format = Assertions.checkNotNull(formatHolder.format); if (format.encoderDelay != 0 || format.encoderPadding != 0) { // Clear gapless playback metadata if the start/end points don't match the media. int encoderDelay = startUs != 0 ? 0 : format.encoderDelay; int encoderPadding = endUs != C.TIME_END_OF_SOURCE ? 0 : format.encoderPadding; - formatHolder.format = format.copyWithGaplessInfo(encoderDelay, encoderPadding); + formatHolder.format = + format + .buildUpon() + .setEncoderDelay(encoderDelay) + .setEncoderPadding(encoderPadding) + .build(); } return C.RESULT_FORMAT_READ; } if (endUs != C.TIME_END_OF_SOURCE && ((result == C.RESULT_BUFFER_READ && buffer.timeUs >= endUs) || (result == C.RESULT_NOTHING_READ - && getBufferedPositionUs() == C.TIME_END_OF_SOURCE))) { + && getBufferedPositionUs() == C.TIME_END_OF_SOURCE + && !buffer.waitingForKeys))) { buffer.clear(); buffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM); sentEos = true; 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 4780c075d5..d4ede3e59e 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 @@ -319,14 +319,14 @@ public final class ClippingMediaSource extends CompositeMediaSource { } Window window = timeline.getWindow(0, new Window()); startUs = Math.max(0, startUs); + if (!window.isPlaceholder && startUs != 0 && !window.isSeekable) { + throw new IllegalClippingException(IllegalClippingException.REASON_NOT_SEEKABLE_TO_START); + } long resolvedEndUs = endUs == C.TIME_END_OF_SOURCE ? window.durationUs : Math.max(0, endUs); if (window.durationUs != C.TIME_UNSET) { if (resolvedEndUs > window.durationUs) { resolvedEndUs = window.durationUs; } - if (startUs != 0 && !window.isSeekable) { - throw new IllegalClippingException(IllegalClippingException.REASON_NOT_SEEKABLE_TO_START); - } if (startUs > resolvedEndUs) { throw new IllegalClippingException(IllegalClippingException.REASON_START_EXCEEDS_END); } 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 424ee6299d..5cc75e8e0b 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 @@ -19,8 +19,10 @@ import android.os.Handler; import androidx.annotation.CallSuper; import androidx.annotation.Nullable; import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.drm.DrmSessionEventListener; import com.google.android.exoplayer2.upstream.TransferListener; import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.UnknownNull; import com.google.android.exoplayer2.util.Util; import java.io.IOException; import java.util.HashMap; @@ -91,7 +93,7 @@ public abstract class CompositeMediaSource extends BaseMediaSource { * @param timeline The timeline of the child source. */ protected abstract void onChildSourceInfoRefreshed( - T id, MediaSource mediaSource, Timeline timeline); + @UnknownNull T id, MediaSource mediaSource, Timeline timeline); /** * Prepares a child source. @@ -105,13 +107,14 @@ 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(final T id, MediaSource mediaSource) { + protected final void prepareChildSource(@UnknownNull T id, MediaSource mediaSource) { Assertions.checkArgument(!childSources.containsKey(id)); MediaSourceCaller caller = (source, timeline) -> onChildSourceInfoRefreshed(id, source, timeline); - MediaSourceEventListener eventListener = new ForwardingEventListener(id); + ForwardingEventListener eventListener = new ForwardingEventListener(id); childSources.put(id, new MediaSourceAndListener(mediaSource, caller, eventListener)); mediaSource.addEventListener(Assertions.checkNotNull(eventHandler), eventListener); + mediaSource.addDrmEventListener(Assertions.checkNotNull(eventHandler), eventListener); mediaSource.prepareSource(caller, mediaTransferListener); if (!isEnabled()) { mediaSource.disable(caller); @@ -123,7 +126,7 @@ public abstract class CompositeMediaSource extends BaseMediaSource { * * @param id The unique id used to prepare the child source. */ - protected final void enableChildSource(final T id) { + protected final void enableChildSource(@UnknownNull T id) { MediaSourceAndListener enabledChild = Assertions.checkNotNull(childSources.get(id)); enabledChild.mediaSource.enable(enabledChild.caller); } @@ -133,7 +136,7 @@ public abstract class CompositeMediaSource extends BaseMediaSource { * * @param id The unique id used to prepare the child source. */ - protected final void disableChildSource(final T id) { + protected final void disableChildSource(@UnknownNull T id) { MediaSourceAndListener disabledChild = Assertions.checkNotNull(childSources.get(id)); disabledChild.mediaSource.disable(disabledChild.caller); } @@ -143,7 +146,7 @@ public abstract class CompositeMediaSource extends BaseMediaSource { * * @param id The unique id used to prepare the child source. */ - protected final void releaseChildSource(T id) { + protected final void releaseChildSource(@UnknownNull T id) { MediaSourceAndListener removedChild = Assertions.checkNotNull(childSources.remove(id)); removedChild.mediaSource.releaseSource(removedChild.caller); removedChild.mediaSource.removeEventListener(removedChild.eventListener); @@ -157,7 +160,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(T id, int windowIndex) { + protected int getWindowIndexForChildWindowIndex(@UnknownNull T id, int windowIndex) { return windowIndex; } @@ -171,8 +174,9 @@ public abstract class CompositeMediaSource extends BaseMediaSource { * @return The corresponding {@link MediaPeriodId} in the composite source. Null if no * corresponding media period id can be determined. */ - protected @Nullable MediaPeriodId getMediaPeriodIdForChildMediaPeriodId( - T id, MediaPeriodId mediaPeriodId) { + @Nullable + protected MediaPeriodId getMediaPeriodIdForChildMediaPeriodId( + @UnknownNull T id, MediaPeriodId mediaPeriodId) { return mediaPeriodId; } @@ -184,7 +188,7 @@ public abstract class CompositeMediaSource extends BaseMediaSource { * @param mediaTimeMs A media time of the child source, in milliseconds. * @return The corresponding media time in the composite source, in milliseconds. */ - protected long getMediaTimeForChildMediaTime(@Nullable T id, long mediaTimeMs) { + protected long getMediaTimeForChildMediaTime(@UnknownNull T id, long mediaTimeMs) { return mediaTimeMs; } @@ -214,16 +218,19 @@ public abstract class CompositeMediaSource extends BaseMediaSource { } } - private final class ForwardingEventListener implements MediaSourceEventListener { + private final class ForwardingEventListener + implements MediaSourceEventListener, DrmSessionEventListener { - private final T id; + @UnknownNull private final T id; private EventDispatcher eventDispatcher; - public ForwardingEventListener(T id) { + public ForwardingEventListener(@UnknownNull T id) { this.eventDispatcher = createEventDispatcher(/* mediaPeriodId= */ null); this.id = id; } + // MediaSourceEventListener implementation + @Override public void onMediaPeriodCreated(int windowIndex, MediaPeriodId mediaPeriodId) { if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) { @@ -314,10 +321,54 @@ public abstract class CompositeMediaSource extends BaseMediaSource { } } + // DrmSessionEventListener implementation + + @Override + public void onDrmSessionAcquired() { + eventDispatcher.dispatch( + (listener, windowIndex, mediaPeriodId) -> listener.onDrmSessionAcquired(), + DrmSessionEventListener.class); + } + + @Override + public void onDrmKeysLoaded() { + eventDispatcher.dispatch( + (listener, windowIndex, mediaPeriodId) -> listener.onDrmKeysLoaded(), + DrmSessionEventListener.class); + } + + @Override + public void onDrmSessionManagerError(Exception error) { + eventDispatcher.dispatch( + (listener, windowIndex, mediaPeriodId) -> listener.onDrmSessionManagerError(error), + DrmSessionEventListener.class); + } + + @Override + public void onDrmKeysRestored() { + eventDispatcher.dispatch( + (listener, windowIndex, mediaPeriodId) -> listener.onDrmKeysRestored(), + DrmSessionEventListener.class); + } + + @Override + public void onDrmKeysRemoved() { + eventDispatcher.dispatch( + (listener, windowIndex, mediaPeriodId) -> listener.onDrmKeysRemoved(), + DrmSessionEventListener.class); + } + + @Override + public void onDrmSessionReleased() { + eventDispatcher.dispatch( + (listener, windowIndex, mediaPeriodId) -> listener.onDrmSessionReleased(), + DrmSessionEventListener.class); + } + /** Updates the event dispatcher and returns whether the event should be dispatched. */ private boolean maybeUpdateEventDispatcher( int childWindowIndex, @Nullable MediaPeriodId childMediaPeriodId) { - MediaPeriodId mediaPeriodId = null; + @Nullable MediaPeriodId mediaPeriodId = null; if (childMediaPeriodId != null) { mediaPeriodId = getMediaPeriodIdForChildMediaPeriodId(id, childMediaPeriodId); if (mediaPeriodId == null) { 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 c1ab78a9bc..8664c4367b 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 @@ -28,7 +28,6 @@ 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.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -468,7 +467,7 @@ public final class ConcatenatingMediaSource extends CompositeMediaSource(index, mediaSourceHolders, callbackAction)) @@ -581,9 +581,10 @@ public final class ConcatenatingMediaSource extends CompositeMediaSource(fromIndex, toIndex, callbackAction)) @@ -600,9 +601,10 @@ public final class ConcatenatingMediaSource extends CompositeMediaSource(currentIndex, newIndex, callbackAction)) @@ -616,7 +618,7 @@ public final class ConcatenatingMediaSource extends CompositeMediaSourceThis implementation delegates calls to {@link #createMediaSource(MediaItem)} to the following + * factories: + * + *

        + *
      • {@code DashMediaSource.Factory} if the item's {@link MediaItem.PlaybackProperties#uri uri} + * ends in '.mpd' or if its {@link MediaItem.PlaybackProperties#mimeType mimeType field} is + * explicitly set to {@link MimeTypes#APPLICATION_MPD} (Requires the exoplayer-dash module + * to be added to the app). + *
      • {@code HlsMediaSource.Factory} if the item's {@link MediaItem.PlaybackProperties#uri uri} + * ends in '.m3u8' or if its {@link MediaItem.PlaybackProperties#mimeType mimeType field} is + * explicitly set to {@link MimeTypes#APPLICATION_M3U8} (Requires the exoplayer-hls module to + * be added to the app). + *
      • {@code SsMediaSource.Factory} if the item's {@link MediaItem.PlaybackProperties#uri uri} + * ends in '.ism', '.ism/Manifest' or if its {@link MediaItem.PlaybackProperties#mimeType + * mimeType field} is explicitly set to {@link MimeTypes#APPLICATION_SS} (Requires the + * exoplayer-smoothstreaming module to be added to the app). + *
      • {@link ProgressiveMediaSource.Factory} serves as a fallback if the item's {@link + * MediaItem.PlaybackProperties#uri uri} doesn't match one of the above. It tries to infer the + * required extractor by using the {@link + * com.google.android.exoplayer2.extractor.DefaultExtractorsFactory}. An {@link + * UnrecognizedInputFormatException} is thrown if none of the available extractors can read + * the stream. + *
      + * + *

      DrmSessionManager creation for protected content

      + * + *

      For a media item with a valid {@link + * com.google.android.exoplayer2.MediaItem.DrmConfiguration}, a {@link DefaultDrmSessionManager} is + * created. The following setter can be used to optionally configure the creation: + * + *

        + *
      • {@link #setDrmHttpDataSourceFactory(HttpDataSource.Factory)}: Sets the data source factory + * to be used by the {@link HttpMediaDrmCallback} for network requests (default: {@link + * DefaultHttpDataSourceFactory}). + *
      + * + *

      For media items without a drm configuration {@link DrmSessionManager#DUMMY} is used. To use an + * alternative dummy, apps can pass a drm session manager to {@link + * #setDrmSessionManager(DrmSessionManager)} which will be used for all items without a drm + * configuration. + * + *

      Ad support for media items with ad tag uri

      + * + *

      For a media item with an ad tag uri an {@link AdSupportProvider} needs to be passed to the + * constructor {@link #DefaultMediaSourceFactory(Context, DataSource.Factory, AdSupportProvider)}. + */ +public final class DefaultMediaSourceFactory implements MediaSourceFactory { + + /** + * Provides {@link AdsLoader ads loaders} and an {@link AdsLoader.AdViewProvider} to created + * {@link AdsMediaSource AdsMediaSources}. + */ + public interface AdSupportProvider { + + /** + * Returns an {@link AdsLoader} for the given {@link Uri ad tag uri} or null if no ads loader is + * available for the given ad tag uri. + * + *

      This method is called for each media item for which a media source is created. + */ + @Nullable + AdsLoader getAdsLoader(Uri adTagUri); + + /** + * Returns an {@link AdsLoader.AdViewProvider} which is used to create {@link AdsMediaSource + * AdsMediaSources}. + */ + AdsLoader.AdViewProvider getAdViewProvider(); + } + + /** + * Creates a new instance with the given {@link Context}. + * + *

      This is functionally equivalent with calling {@code #newInstance(Context, + * DefaultDataSourceFactory)}. + * + * @param context The {@link Context}. + */ + public static DefaultMediaSourceFactory newInstance(Context context) { + return newInstance( + context, + new DefaultDataSourceFactory( + context, Util.getUserAgent(context, ExoPlayerLibraryInfo.VERSION_SLASHY))); + } + + /** + * Creates a new instance with the given {@link Context} and {@link DataSource.Factory}. + * + * @param context The {@link Context}. + * @param dataSourceFactory A {@link DataSource.Factory} to be used to create media sources. + */ + public static DefaultMediaSourceFactory newInstance( + Context context, DataSource.Factory dataSourceFactory) { + return new DefaultMediaSourceFactory(context, dataSourceFactory, /* adSupportProvider= */ null); + } + + private static final String TAG = "DefaultMediaSourceFactory"; + + private final DataSource.Factory dataSourceFactory; + @Nullable private final AdSupportProvider adSupportProvider; + private final SparseArray mediaSourceFactories; + @C.ContentType private final int[] supportedTypes; + private final String userAgent; + + private DrmSessionManager drmSessionManager; + private HttpDataSource.Factory drmHttpDataSourceFactory; + @Nullable private List streamKeys; + + /** + * Creates a new instance with the given {@link Context} and {@link DataSource.Factory}. + * + * @param context The {@link Context}. + * @param dataSourceFactory A {@link DataSource.Factory} to be used to create media sources. + * @param adSupportProvider An {@link AdSupportProvider} to get ads loaders and ad view providers + * to be used to create {@link AdsMediaSource AdsMediaSources}. + */ + public DefaultMediaSourceFactory( + Context context, + DataSource.Factory dataSourceFactory, + @Nullable AdSupportProvider adSupportProvider) { + this.dataSourceFactory = dataSourceFactory; + this.adSupportProvider = adSupportProvider; + drmSessionManager = DrmSessionManager.getDummyDrmSessionManager(); + userAgent = Util.getUserAgent(context, ExoPlayerLibraryInfo.VERSION_SLASHY); + drmHttpDataSourceFactory = new DefaultHttpDataSourceFactory(userAgent); + mediaSourceFactories = loadDelegates(dataSourceFactory); + supportedTypes = new int[mediaSourceFactories.size()]; + for (int i = 0; i < mediaSourceFactories.size(); i++) { + supportedTypes[i] = mediaSourceFactories.keyAt(i); + } + } + + /** + * Sets the {@link HttpDataSource.Factory} to be used for creating {@link HttpMediaDrmCallback + * HttpMediaDrmCallbacks} which executes key and provisioning requests over HTTP. If {@code null} + * is passed the {@link DefaultHttpDataSourceFactory} is used. + * + * @param drmHttpDataSourceFactory The HTTP data source factory or {@code null} to use {@link + * DefaultHttpDataSourceFactory}. + * @return This factory, for convenience. + */ + public DefaultMediaSourceFactory setDrmHttpDataSourceFactory( + @Nullable HttpDataSource.Factory drmHttpDataSourceFactory) { + this.drmHttpDataSourceFactory = + drmHttpDataSourceFactory != null + ? drmHttpDataSourceFactory + : new DefaultHttpDataSourceFactory(userAgent); + return this; + } + + @Override + public DefaultMediaSourceFactory setDrmSessionManager( + @Nullable DrmSessionManager drmSessionManager) { + this.drmSessionManager = + drmSessionManager != null + ? drmSessionManager + : DrmSessionManager.getDummyDrmSessionManager(); + return this; + } + + @Override + public DefaultMediaSourceFactory setLoadErrorHandlingPolicy( + @Nullable LoadErrorHandlingPolicy loadErrorHandlingPolicy) { + LoadErrorHandlingPolicy newLoadErrorHandlingPolicy = + loadErrorHandlingPolicy != null + ? loadErrorHandlingPolicy + : new DefaultLoadErrorHandlingPolicy(); + for (int i = 0; i < mediaSourceFactories.size(); i++) { + mediaSourceFactories.valueAt(i).setLoadErrorHandlingPolicy(newLoadErrorHandlingPolicy); + } + return this; + } + + /** + * @deprecated Use {@link MediaItem.Builder#setStreamKeys(List)} and {@link + * #createMediaSource(MediaItem)} instead. + */ + @SuppressWarnings("deprecation") + @Deprecated + @Override + public DefaultMediaSourceFactory setStreamKeys(@Nullable List streamKeys) { + this.streamKeys = streamKeys != null && !streamKeys.isEmpty() ? streamKeys : null; + return this; + } + + @Override + public int[] getSupportedTypes() { + return Arrays.copyOf(supportedTypes, supportedTypes.length); + } + + @SuppressWarnings("deprecation") + @Override + public MediaSource createMediaSource(MediaItem mediaItem) { + Assertions.checkNotNull(mediaItem.playbackProperties); + @C.ContentType + int type = + Util.inferContentTypeWithMimeType( + mediaItem.playbackProperties.uri, mediaItem.playbackProperties.mimeType); + @Nullable MediaSourceFactory mediaSourceFactory = mediaSourceFactories.get(type); + Assertions.checkNotNull( + mediaSourceFactory, "No suitable media source factory found for content type: " + type); + mediaSourceFactory.setDrmSessionManager(createDrmSessionManager(mediaItem)); + mediaSourceFactory.setStreamKeys( + !mediaItem.playbackProperties.streamKeys.isEmpty() + ? mediaItem.playbackProperties.streamKeys + : streamKeys); + + MediaSource mediaSource = mediaSourceFactory.createMediaSource(mediaItem); + + List subtitles = mediaItem.playbackProperties.subtitles; + if (!subtitles.isEmpty()) { + MediaSource[] mediaSources = new MediaSource[subtitles.size() + 1]; + mediaSources[0] = mediaSource; + SingleSampleMediaSource.Factory singleSampleSourceFactory = + new SingleSampleMediaSource.Factory(dataSourceFactory); + for (int i = 0; i < subtitles.size(); i++) { + MediaItem.Subtitle subtitle = subtitles.get(i); + Format subtitleFormat = + new Format.Builder() + .setSampleMimeType(subtitle.mimeType) + .setLanguage(subtitle.language) + .setSelectionFlags(subtitle.selectionFlags) + .build(); + mediaSources[i + 1] = + singleSampleSourceFactory.createMediaSource( + subtitle.uri, subtitleFormat, /* durationUs= */ C.TIME_UNSET); + } + mediaSource = new MergingMediaSource(mediaSources); + } + return maybeWrapWithAdsMediaSource(mediaItem, maybeClipMediaSource(mediaItem, mediaSource)); + } + + // internal methods + + private DrmSessionManager createDrmSessionManager(MediaItem mediaItem) { + Assertions.checkNotNull(mediaItem.playbackProperties); + if (mediaItem.playbackProperties.drmConfiguration == null + || mediaItem.playbackProperties.drmConfiguration.licenseUri == null + || Util.SDK_INT < 18) { + return drmSessionManager; + } + return new DefaultDrmSessionManager.Builder() + .setUuidAndExoMediaDrmProvider( + mediaItem.playbackProperties.drmConfiguration.uuid, FrameworkMediaDrm.DEFAULT_PROVIDER) + .setMultiSession(mediaItem.playbackProperties.drmConfiguration.multiSession) + .setPlayClearSamplesWithoutKeys( + mediaItem.playbackProperties.drmConfiguration.playClearContentWithoutKey) + .setUseDrmSessionsForClearContent( + Util.toArray(mediaItem.playbackProperties.drmConfiguration.sessionForClearTypes)) + .build(createHttpMediaDrmCallback(mediaItem.playbackProperties.drmConfiguration)); + } + + private MediaDrmCallback createHttpMediaDrmCallback(MediaItem.DrmConfiguration drmConfiguration) { + Assertions.checkNotNull(drmConfiguration.licenseUri); + HttpMediaDrmCallback drmCallback = + new HttpMediaDrmCallback(drmConfiguration.licenseUri.toString(), drmHttpDataSourceFactory); + for (Map.Entry entry : drmConfiguration.requestHeaders.entrySet()) { + drmCallback.setKeyRequestProperty(entry.getKey(), entry.getValue()); + } + return drmCallback; + } + + private static MediaSource maybeClipMediaSource(MediaItem mediaItem, MediaSource mediaSource) { + if (mediaItem.clippingProperties.startPositionMs == 0 + && mediaItem.clippingProperties.endPositionMs == C.TIME_END_OF_SOURCE + && !mediaItem.clippingProperties.relativeToDefaultPosition) { + return mediaSource; + } + return new ClippingMediaSource( + mediaSource, + C.msToUs(mediaItem.clippingProperties.startPositionMs), + C.msToUs(mediaItem.clippingProperties.endPositionMs), + /* enableInitialDiscontinuity= */ !mediaItem.clippingProperties.startsAtKeyFrame, + /* allowDynamicClippingUpdates= */ mediaItem.clippingProperties.relativeToLiveWindow, + mediaItem.clippingProperties.relativeToDefaultPosition); + } + + private MediaSource maybeWrapWithAdsMediaSource(MediaItem mediaItem, MediaSource mediaSource) { + Assertions.checkNotNull(mediaItem.playbackProperties); + if (mediaItem.playbackProperties.adTagUri == null) { + return mediaSource; + } + if (adSupportProvider == null) { + Log.w( + TAG, + "Playing media without ads. Pass an AdsSupportProvider to the constructor for supporting" + + " media items with an ad tag uri."); + return mediaSource; + } + AdsLoader adsLoader = adSupportProvider.getAdsLoader(mediaItem.playbackProperties.adTagUri); + if (adsLoader == null) { + Log.w( + TAG, + String.format( + "Playing media without ads. No AdsLoader for media item with mediaId '%s'.", + mediaItem.mediaId)); + return mediaSource; + } + return new AdsMediaSource( + mediaSource, + /* adMediaSourceFactory= */ this, + adsLoader, + adSupportProvider.getAdViewProvider()); + } + + private static SparseArray loadDelegates( + DataSource.Factory dataSourceFactory) { + SparseArray factories = new SparseArray<>(); + // LINT.IfChange + try { + Class factoryClazz = + Class.forName("com.google.android.exoplayer2.source.dash.DashMediaSource$Factory") + .asSubclass(MediaSourceFactory.class); + factories.put( + C.TYPE_DASH, + factoryClazz.getConstructor(DataSource.Factory.class).newInstance(dataSourceFactory)); + } catch (Exception e) { + // Expected if the app was built without the dash module. + } + try { + Class factoryClazz = + Class.forName( + "com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource$Factory") + .asSubclass(MediaSourceFactory.class); + factories.put( + C.TYPE_SS, + factoryClazz.getConstructor(DataSource.Factory.class).newInstance(dataSourceFactory)); + } catch (Exception e) { + // Expected if the app was built without the smoothstreaming module. + } + try { + Class factoryClazz = + Class.forName("com.google.android.exoplayer2.source.hls.HlsMediaSource$Factory") + .asSubclass(MediaSourceFactory.class); + factories.put( + C.TYPE_HLS, + factoryClazz.getConstructor(DataSource.Factory.class).newInstance(dataSourceFactory)); + } catch (Exception e) { + // Expected if the app was built without the hls module. + } + // LINT.ThenChange(../../../../../../../../proguard-rules.txt) + factories.put(C.TYPE_OTHER, new ProgressiveMediaSource.Factory(dataSourceFactory)); + return factories; + } +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/EmptySampleStream.java b/library/core/src/main/java/com/google/android/exoplayer2/source/EmptySampleStream.java index 299b816cc8..fe574cf597 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/EmptySampleStream.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/EmptySampleStream.java @@ -18,7 +18,6 @@ package com.google.android.exoplayer2.source; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.FormatHolder; import com.google.android.exoplayer2.decoder.DecoderInputBuffer; -import java.io.IOException; /** * An empty {@link SampleStream}. @@ -31,7 +30,7 @@ public final class EmptySampleStream implements SampleStream { } @Override - public void maybeThrowError() throws IOException { + public void maybeThrowError() { // Do nothing. } @@ -46,5 +45,4 @@ public final class EmptySampleStream implements SampleStream { public int skipData(long positionUs) { return 0; } - } 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 060027fee7..2e1c92067c 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 @@ -19,6 +19,7 @@ import android.net.Uri; import android.os.Handler; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.MediaItem; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.drm.DrmSessionManager; @@ -64,12 +65,11 @@ public final class ExtractorMediaSource extends CompositeMediaSource { private final DataSource.Factory dataSourceFactory; - @Nullable private ExtractorsFactory extractorsFactory; - @Nullable private String customCacheKey; - @Nullable private Object tag; + private ExtractorsFactory extractorsFactory; private LoadErrorHandlingPolicy loadErrorHandlingPolicy; private int continueLoadingCheckIntervalBytes; - private boolean isCreateCalled; + @Nullable private String customCacheKey; + @Nullable private Object tag; /** * Creates a new factory for {@link ExtractorMediaSource}s. @@ -78,6 +78,7 @@ public final class ExtractorMediaSource extends CompositeMediaSource { */ public Factory(DataSource.Factory dataSourceFactory) { this.dataSourceFactory = dataSourceFactory; + extractorsFactory = new DefaultExtractorsFactory(); loadErrorHandlingPolicy = new DefaultLoadErrorHandlingPolicy(); continueLoadingCheckIntervalBytes = DEFAULT_LOADING_CHECK_INTERVAL_BYTES; } @@ -90,11 +91,10 @@ public final class ExtractorMediaSource extends CompositeMediaSource { * possible formats are known, pass a factory that instantiates extractors for those * formats. * @return This factory, for convenience. - * @throws IllegalStateException If one of the {@code create} methods has already been called. */ - public Factory setExtractorsFactory(ExtractorsFactory extractorsFactory) { - Assertions.checkState(!isCreateCalled); - this.extractorsFactory = extractorsFactory; + public Factory setExtractorsFactory(@Nullable ExtractorsFactory extractorsFactory) { + this.extractorsFactory = + extractorsFactory != null ? extractorsFactory : new DefaultExtractorsFactory(); return this; } @@ -105,42 +105,23 @@ public final class ExtractorMediaSource extends CompositeMediaSource { * @param customCacheKey A custom key that uniquely identifies the original stream. Used for * cache indexing. * @return This factory, for convenience. - * @throws IllegalStateException If one of the {@code create} methods has already been called. */ - public Factory setCustomCacheKey(String customCacheKey) { - Assertions.checkState(!isCreateCalled); + public Factory setCustomCacheKey(@Nullable String customCacheKey) { this.customCacheKey = customCacheKey; return this; } /** - * Sets a tag for the media source which will be published in the {@link - * com.google.android.exoplayer2.Timeline} of the source as {@link - * com.google.android.exoplayer2.Timeline.Window#tag}. - * - * @param tag A tag for the media source. - * @return This factory, for convenience. - * @throws IllegalStateException If one of the {@code create} methods has already been called. + * @deprecated Use {@link MediaItem.Builder#setTag(Object)} and {@link + * #createMediaSource(MediaItem)} instead. */ - public Factory setTag(Object tag) { - Assertions.checkState(!isCreateCalled); + @Deprecated + public Factory setTag(@Nullable Object tag) { this.tag = tag; return this; } - /** - * 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 Use {@link #setLoadErrorHandlingPolicy(LoadErrorHandlingPolicy)} instead. */ @Deprecated public Factory setMinLoadableRetryCount(int minLoadableRetryCount) { return setLoadErrorHandlingPolicy(new DefaultLoadErrorHandlingPolicy(minLoadableRetryCount)); @@ -154,11 +135,14 @@ public final class ExtractorMediaSource extends CompositeMediaSource { * * @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; + @Override + public Factory setLoadErrorHandlingPolicy( + @Nullable LoadErrorHandlingPolicy loadErrorHandlingPolicy) { + this.loadErrorHandlingPolicy = + loadErrorHandlingPolicy != null + ? loadErrorHandlingPolicy + : new DefaultLoadErrorHandlingPolicy(); return this; } @@ -171,34 +155,45 @@ public final class ExtractorMediaSource extends CompositeMediaSource { * each invocation of {@link * MediaPeriod.Callback#onContinueLoadingRequested(SequenceableLoader)}. * @return This factory, for convenience. - * @throws IllegalStateException If one of the {@code create} methods has already been called. */ public Factory setContinueLoadingCheckIntervalBytes(int continueLoadingCheckIntervalBytes) { - Assertions.checkState(!isCreateCalled); this.continueLoadingCheckIntervalBytes = continueLoadingCheckIntervalBytes; return this; } + /** @deprecated Use {@link ProgressiveMediaSource.Factory#setDrmSessionManager} instead. */ + @Deprecated + @Override + public Factory setDrmSessionManager(@Nullable DrmSessionManager drmSessionManager) { + throw new UnsupportedOperationException(); + } + + /** @deprecated Use {@link #createMediaSource(MediaItem)} instead. */ + @SuppressWarnings("deprecation") + @Deprecated + @Override + public ExtractorMediaSource createMediaSource(Uri uri) { + return createMediaSource(new MediaItem.Builder().setUri(uri).build()); + } + /** * Returns a new {@link ExtractorMediaSource} using the current parameters. * - * @param uri The {@link Uri}. + * @param mediaItem The {@link MediaItem}. * @return The new {@link ExtractorMediaSource}. + * @throws NullPointerException if {@link MediaItem#playbackProperties} is {@code null}. */ @Override - public ExtractorMediaSource createMediaSource(Uri uri) { - isCreateCalled = true; - if (extractorsFactory == null) { - extractorsFactory = new DefaultExtractorsFactory(); - } + public ExtractorMediaSource createMediaSource(MediaItem mediaItem) { + Assertions.checkNotNull(mediaItem.playbackProperties); return new ExtractorMediaSource( - uri, + mediaItem.playbackProperties.uri, dataSourceFactory, extractorsFactory, loadErrorHandlingPolicy, customCacheKey, continueLoadingCheckIntervalBytes, - tag); + mediaItem.playbackProperties.tag != null ? mediaItem.playbackProperties.tag : tag); } /** diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/IcyDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/IcyDataSource.java index d097073960..84d2902c53 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/IcyDataSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/IcyDataSource.java @@ -71,7 +71,7 @@ import java.util.Map; } @Override - public long open(DataSpec dataSpec) throws IOException { + public long open(DataSpec dataSpec) { throw new UnsupportedOperationException(); } @@ -91,8 +91,8 @@ import java.util.Map; return bytesRead; } - @Nullable @Override + @Nullable public Uri getUri() { return upstream.getUri(); } @@ -103,7 +103,7 @@ import java.util.Map; } @Override - public void close() throws IOException { + public void close() { throw new UnsupportedOperationException(); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/LoadEventInfo.java b/library/core/src/main/java/com/google/android/exoplayer2/source/LoadEventInfo.java index ae61cf45b5..8ae7b02d43 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/LoadEventInfo.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/LoadEventInfo.java @@ -18,12 +18,24 @@ package com.google.android.exoplayer2.source; import android.net.Uri; import android.os.SystemClock; import com.google.android.exoplayer2.upstream.DataSpec; +import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.concurrent.atomic.AtomicLong; /** {@link MediaSource} load event information. */ public final class LoadEventInfo { + /** Used for the generation of unique ids. */ + private static final AtomicLong idSource = new AtomicLong(); + + /** Returns an non-negative identifier which is unique to the JVM instance. */ + public static long getNewId() { + return idSource.getAndIncrement(); + } + + /** Identifies the load task to which this event corresponds. */ + public final long loadTaskId; /** Defines the requested data. */ public final DataSpec dataSpec; /** @@ -41,28 +53,42 @@ public final class LoadEventInfo { /** The number of bytes that were loaded up to the event time. */ public final long bytesLoaded; + /** + * Equivalent to {@link #LoadEventInfo(long, DataSpec, Uri, Map, long, long, long) + * LoadEventInfo(loadTaskId, dataSpec, dataSpec.uri, Collections.emptyMap(), elapsedRealtimeMs, 0, + * 0)}. + */ + public LoadEventInfo(long loadTaskId, DataSpec dataSpec, long elapsedRealtimeMs) { + this( + loadTaskId, + dataSpec, + dataSpec.uri, + Collections.emptyMap(), + elapsedRealtimeMs, + /* loadDurationMs= */ 0, + /* bytesLoaded= */ 0); + } + /** * Creates load event info. * - * @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 responseHeaders The response headers associated with the load, or an empty map if - * unavailable. - * @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. For compressed - * network responses, this is the decompressed size. + * @param loadTaskId See {@link #loadTaskId}. + * @param dataSpec See {@link #dataSpec}. + * @param uri See {@link #uri}. + * @param responseHeaders See {@link #responseHeaders}. + * @param elapsedRealtimeMs See {@link #elapsedRealtimeMs}. + * @param loadDurationMs See {@link #loadDurationMs}. + * @param bytesLoaded See {@link #bytesLoaded}. */ public LoadEventInfo( + long loadTaskId, DataSpec dataSpec, Uri uri, Map> responseHeaders, long elapsedRealtimeMs, long loadDurationMs, long bytesLoaded) { + this.loadTaskId = loadTaskId; this.dataSpec = dataSpec; this.uri = uri; this.responseHeaders = responseHeaders; 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 8769a84d95..13f9758a73 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 @@ -71,8 +71,8 @@ public final class LoopingMediaSource extends CompositeMediaSource { return maskingMediaSource.getTag(); } - @Nullable @Override + @Nullable public Timeline getInitialTimeline() { return loopCount != Integer.MAX_VALUE ? new LoopingTimeline(maskingMediaSource.getTimeline(), loopCount) @@ -107,6 +107,7 @@ public final class LoopingMediaSource extends CompositeMediaSource { @Override public void releasePeriod(MediaPeriod mediaPeriod) { maskingMediaSource.releasePeriod(mediaPeriod); + @Nullable MediaPeriodId childMediaPeriodId = mediaPeriodToChildMediaPeriodId.remove(mediaPeriod); if (childMediaPeriodId != null) { childMediaPeriodIdToMediaPeriodId.remove(childMediaPeriodId); @@ -123,7 +124,8 @@ public final class LoopingMediaSource extends CompositeMediaSource { } @Override - protected @Nullable MediaPeriodId getMediaPeriodIdForChildMediaPeriodId( + @Nullable + protected MediaPeriodId getMediaPeriodIdForChildMediaPeriodId( Void id, MediaPeriodId mediaPeriodId) { return loopCount != Integer.MAX_VALUE ? childMediaPeriodIdToMediaPeriodId.get(mediaPeriodId) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/MaskingMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/MaskingMediaPeriod.java index 17ac6c0667..142527af7d 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/MaskingMediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/MaskingMediaPeriod.java @@ -91,8 +91,8 @@ public final class MaskingMediaPeriod implements MediaPeriod, MediaPeriod.Callba } /** - * Overrides the default prepare position at which to prepare the media period. This value is only - * used if called before {@link #createPeriod(MediaPeriodId)}. + * Overrides the default prepare position at which to prepare the media period. This method must + * be called before {@link #createPeriod(MediaPeriodId)}. * * @param preparePositionUs The default prepare position to use, in microseconds. */ @@ -100,6 +100,11 @@ public final class MaskingMediaPeriod implements MediaPeriod, MediaPeriod.Callba preparePositionOverrideUs = preparePositionUs; } + /** Returns the prepare position override set by {@link #overridePreparePositionUs(long)}. */ + public long getPreparePositionOverrideUs() { + return preparePositionOverrideUs; + } + /** * Calls {@link MediaSource#createPeriod(MediaPeriodId, Allocator, long)} on the wrapped source * then prepares it if {@link #prepare(Callback, long)} has been called. Call {@link diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/MaskingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/MaskingMediaSource.java index 031d50e7d2..35b3e1848e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/MaskingMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/MaskingMediaSource.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.source; import android.util.Pair; import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.Timeline.Window; @@ -25,7 +26,7 @@ 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.io.IOException; +import org.checkerframework.checker.nullness.qual.RequiresNonNull; /** * A {@link MediaSource} that masks the {@link Timeline} with a placeholder until the actual media @@ -58,7 +59,7 @@ public final class MaskingMediaSource extends CompositeMediaSource { this.useLazyPreparation = useLazyPreparation && mediaSource.isSingleWindow(); window = new Timeline.Window(); period = new Timeline.Period(); - Timeline initialTimeline = mediaSource.getInitialTimeline(); + @Nullable Timeline initialTimeline = mediaSource.getInitialTimeline(); if (initialTimeline != null) { timeline = MaskingTimeline.createWithRealTimeline( @@ -83,15 +84,15 @@ public final class MaskingMediaSource extends CompositeMediaSource { } } - @Nullable @Override + @Nullable public Object getTag() { return mediaSource.getTag(); } @Override @SuppressWarnings("MissingSuperCall") - public void maybeThrowSourceInfoRefreshError() throws IOException { + public void maybeThrowSourceInfoRefreshError() { // Do nothing. Source info refresh errors will be thrown when calling // MaskingMediaPeriod.maybeThrowPrepareError. } @@ -140,8 +141,14 @@ public final class MaskingMediaSource extends CompositeMediaSource { @Override protected synchronized void onChildSourceInfoRefreshed( Void id, MediaSource mediaSource, Timeline newTimeline) { + @Nullable MediaPeriodId idForMaskingPeriodPreparation = null; if (isPrepared) { timeline = timeline.cloneWithUpdatedTimeline(newTimeline); + if (unpreparedMaskingMediaPeriod != null) { + // Reset override in case the duration changed and we need to update our override. + setPreparePositionOverrideToUnpreparedMaskingPeriod( + unpreparedMaskingMediaPeriod.getPreparePositionOverrideUs()); + } } else if (newTimeline.isEmpty()) { timeline = hasRealTimeline @@ -181,19 +188,22 @@ public final class MaskingMediaSource extends CompositeMediaSource { : MaskingTimeline.createWithRealTimeline(newTimeline, windowUid, periodUid); if (unpreparedMaskingMediaPeriod != null) { MaskingMediaPeriod maskingPeriod = unpreparedMaskingMediaPeriod; - maskingPeriod.overridePreparePositionUs(periodPositionUs); - MediaPeriodId idInSource = + setPreparePositionOverrideToUnpreparedMaskingPeriod(periodPositionUs); + idForMaskingPeriodPreparation = maskingPeriod.id.copyWithPeriodUid(getInternalPeriodUid(maskingPeriod.id.periodUid)); - maskingPeriod.createPeriod(idInSource); } } hasRealTimeline = true; isPrepared = true; refreshSourceInfo(this.timeline); + if (idForMaskingPeriodPreparation != null) { + Assertions.checkNotNull(unpreparedMaskingMediaPeriod) + .createPeriod(idForMaskingPeriodPreparation); + } } - @Nullable @Override + @Nullable protected MediaPeriodId getMediaPeriodIdForChildMediaPeriodId( Void id, MediaPeriodId mediaPeriodId) { return mediaPeriodId.copyWithPeriodUid(getExternalPeriodUid(mediaPeriodId.periodUid)); @@ -221,6 +231,27 @@ public final class MaskingMediaSource extends CompositeMediaSource { : internalPeriodUid; } + @RequiresNonNull("unpreparedMaskingMediaPeriod") + private void setPreparePositionOverrideToUnpreparedMaskingPeriod(long preparePositionOverrideUs) { + MaskingMediaPeriod maskingPeriod = unpreparedMaskingMediaPeriod; + int maskingPeriodIndex = timeline.getIndexOfPeriod(maskingPeriod.id.periodUid); + if (maskingPeriodIndex == C.INDEX_UNSET) { + // The new timeline doesn't contain this period anymore. This can happen if the media source + // has multiple periods and removed the first period with a timeline update. Ignore the + // update, as the non-existing period will be released anyway as soon as the player receives + // this new timeline. + return; + } + long periodDurationUs = timeline.getPeriod(maskingPeriodIndex, period).durationUs; + if (periodDurationUs != C.TIME_UNSET) { + // Ensure the overridden position doesn't exceed the period duration. + if (preparePositionOverrideUs >= periodDurationUs) { + preparePositionOverrideUs = Math.max(0, periodDurationUs - 1); + } + } + maskingPeriod.overridePreparePositionUs(preparePositionOverrideUs); + } + /** * Timeline used as placeholder for an unprepared media source. After preparation, a * MaskingTimeline is used to keep the originally assigned dummy period ID. @@ -314,7 +345,8 @@ public final class MaskingMediaSource extends CompositeMediaSource { } /** Dummy placeholder timeline with one dynamic window with a period of indeterminate duration. */ - private static final class DummyTimeline extends Timeline { + @VisibleForTesting + public static final class DummyTimeline extends Timeline { @Nullable private final Object tag; @@ -329,12 +361,13 @@ public final class MaskingMediaSource extends CompositeMediaSource { @Override public Window getWindow(int windowIndex, Window window, long defaultPositionProjectionUs) { - return window.set( + window.set( Window.SINGLE_WINDOW_UID, tag, /* manifest= */ null, /* presentationStartTimeMs= */ C.TIME_UNSET, /* windowStartTimeMs= */ C.TIME_UNSET, + /* elapsedRealtimeEpochOffsetMs= */ C.TIME_UNSET, /* isSeekable= */ false, // Dynamic window to indicate pending timeline updates. /* isDynamic= */ true, @@ -344,6 +377,8 @@ public final class MaskingMediaSource extends CompositeMediaSource { /* firstPeriodIndex= */ 0, /* lastPeriodIndex= */ 0, /* positionInFirstPeriodUs= */ 0); + window.isPlaceholder = true; + return window; } @Override diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaLoadData.java b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaLoadData.java index 7d9d5e5969..0de79e9219 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaLoadData.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaLoadData.java @@ -55,22 +55,28 @@ public final class MediaLoadData { */ public final long mediaEndTimeMs; + /** Creates an instance with the given {@link #dataType}. */ + public MediaLoadData(int dataType) { + this( + dataType, + /* trackType= */ C.TRACK_TYPE_UNKNOWN, + /* trackFormat= */ null, + /* trackSelectionReason= */ C.SELECTION_REASON_UNKNOWN, + /* trackSelectionData= */ null, + /* mediaStartTimeMs= */ C.TIME_UNSET, + /* mediaEndTimeMs= */ C.TIME_UNSET); + } + /** * Creates media load data. * - * @param dataType One of the {@link C} {@code DATA_TYPE_*} constants defining the type of data. - * @param trackType One of the {@link C} {@code TRACK_TYPE_*} constants if the data corresponds to - * media of a specific type. {@link C#TRACK_TYPE_UNKNOWN} otherwise. - * @param trackFormat The format of the track to which the data belongs. Null if the data does not - * belong to a track. - * @param trackSelectionReason One of the {@link C} {@code SELECTION_REASON_*} constants if the - * data belongs to a track. {@link C#SELECTION_REASON_UNKNOWN} otherwise. - * @param trackSelectionData Optional data associated with the selection of the track to which the - * data belongs. Null if the data does not belong to a track. - * @param mediaStartTimeMs The start time of the media, or {@link C#TIME_UNSET} if the data does - * not belong to a specific media period. - * @param mediaEndTimeMs The end time of the media, or {@link C#TIME_UNSET} if the data does not - * belong to a specific media period or the end time is unknown. + * @param dataType See {@link #dataType}. + * @param trackType See {@link #trackType}. + * @param trackFormat See {@link #trackFormat}. + * @param trackSelectionReason See {@link #trackSelectionReason}. + * @param trackSelectionData See {@link #trackSelectionData}. + * @param mediaStartTimeMs See {@link #mediaStartTimeMs}. + * @param mediaEndTimeMs See {@link #mediaEndTimeMs}. */ public MediaLoadData( int dataType, 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 f6dd4d79a4..479db2adc2 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 @@ -19,6 +19,7 @@ import android.os.Handler; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.drm.DrmSessionEventListener; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.TransferListener; import java.io.IOException; @@ -228,6 +229,23 @@ public interface MediaSource { */ void removeEventListener(MediaSourceEventListener eventListener); + /** + * Adds a {@link DrmSessionEventListener} to the list of listeners which are notified of DRM + * events for this media source. + * + * @param handler A handler on the which listener events will be posted. + * @param eventListener The listener to be added. + */ + void addDrmEventListener(Handler handler, DrmSessionEventListener eventListener); + + /** + * Removes a {@link DrmSessionEventListener} from the list of listeners which are notified of DRM + * events for this media source. + * + * @param eventListener The listener to be removed. + */ + void removeDrmEventListener(DrmSessionEventListener eventListener); + /** * Returns the initial dummy timeline that is returned immediately when the real timeline is not * yet known, or null to let the player create an initial timeline. 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 8123245024..7c9dc34b4f 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,22 +15,15 @@ */ package com.google.android.exoplayer2.source; -import android.net.Uri; -import android.os.Handler; -import android.os.Looper; -import androidx.annotation.CheckResult; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; -import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.CopyOnWriteMultiset; +import com.google.android.exoplayer2.util.MediaSourceEventDispatcher; import java.io.IOException; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.concurrent.CopyOnWriteArrayList; /** Interface for callbacks to be notified of {@link MediaSource} events. */ public interface MediaSourceEventListener { @@ -167,132 +160,65 @@ public interface MediaSourceEventListener { default void onDownstreamFormatChanged( int windowIndex, @Nullable MediaPeriodId mediaPeriodId, MediaLoadData mediaLoadData) {} - /** Dispatches events to {@link MediaSourceEventListener}s. */ - final class EventDispatcher { + /** @deprecated Use {@link MediaSourceEventDispatcher} directly instead. */ + @Deprecated + final class EventDispatcher extends MediaSourceEventDispatcher { - /** The timeline window index reported with the events. */ - public final int windowIndex; - /** The {@link MediaPeriodId} reported with the events. */ - @Nullable public final MediaPeriodId mediaPeriodId; - - private final CopyOnWriteArrayList listenerAndHandlers; - private final long mediaTimeOffsetMs; - - /** Creates an event dispatcher. */ public EventDispatcher() { - this( - /* listenerAndHandlers= */ new CopyOnWriteArrayList<>(), - /* windowIndex= */ 0, - /* mediaPeriodId= */ null, - /* mediaTimeOffsetMs= */ 0); + super(); } private EventDispatcher( - CopyOnWriteArrayList listenerAndHandlers, + CopyOnWriteMultiset listeners, int windowIndex, @Nullable MediaPeriodId mediaPeriodId, long mediaTimeOffsetMs) { - this.listenerAndHandlers = listenerAndHandlers; - this.windowIndex = windowIndex; - this.mediaPeriodId = mediaPeriodId; - this.mediaTimeOffsetMs = mediaTimeOffsetMs; + super(listeners, windowIndex, mediaPeriodId, mediaTimeOffsetMs); } - /** - * Creates a view of the event dispatcher with pre-configured window index, media period id, and - * media time offset. - * - * @param windowIndex The timeline window index to be reported with the events. - * @param mediaPeriodId The {@link MediaPeriodId} to be reported with the events. - * @param mediaTimeOffsetMs The offset to be added to all media times, in milliseconds. - * @return A view of the event dispatcher with the pre-configured parameters. - */ - @CheckResult + @Override public EventDispatcher withParameters( int windowIndex, @Nullable MediaPeriodId mediaPeriodId, long mediaTimeOffsetMs) { - return new EventDispatcher( - listenerAndHandlers, windowIndex, mediaPeriodId, mediaTimeOffsetMs); + return new EventDispatcher(listenerInfos, windowIndex, mediaPeriodId, mediaTimeOffsetMs); } - /** - * Adds a listener to the event dispatcher. - * - * @param handler A handler on the which listener events will be posted. - * @param eventListener The listener to be added. - */ - public void addEventListener(Handler handler, MediaSourceEventListener eventListener) { - Assertions.checkArgument(handler != null && eventListener != null); - listenerAndHandlers.add(new ListenerAndHandler(handler, eventListener)); - } - - /** - * Removes a listener from the event dispatcher. - * - * @param eventListener The listener to be removed. - */ - public void removeEventListener(MediaSourceEventListener eventListener) { - for (ListenerAndHandler listenerAndHandler : listenerAndHandlers) { - if (listenerAndHandler.listener == eventListener) { - listenerAndHandlers.remove(listenerAndHandler); - } - } - } - - /** Dispatches {@link #onMediaPeriodCreated(int, MediaPeriodId)}. */ public void mediaPeriodCreated() { - MediaPeriodId mediaPeriodId = Assertions.checkNotNull(this.mediaPeriodId); - for (ListenerAndHandler listenerAndHandler : listenerAndHandlers) { - final MediaSourceEventListener listener = listenerAndHandler.listener; - postOrRun( - listenerAndHandler.handler, - () -> listener.onMediaPeriodCreated(windowIndex, mediaPeriodId)); - } + dispatch( + (listener, windowIndex, mediaPeriodId) -> + listener.onMediaPeriodCreated(windowIndex, Assertions.checkNotNull(mediaPeriodId)), + MediaSourceEventListener.class); } - /** Dispatches {@link #onMediaPeriodReleased(int, MediaPeriodId)}. */ public void mediaPeriodReleased() { - MediaPeriodId mediaPeriodId = Assertions.checkNotNull(this.mediaPeriodId); - for (ListenerAndHandler listenerAndHandler : listenerAndHandlers) { - final MediaSourceEventListener listener = listenerAndHandler.listener; - postOrRun( - listenerAndHandler.handler, - () -> listener.onMediaPeriodReleased(windowIndex, mediaPeriodId)); - } + dispatch( + (listener, windowIndex, mediaPeriodId) -> + listener.onMediaPeriodReleased(windowIndex, Assertions.checkNotNull(mediaPeriodId)), + MediaSourceEventListener.class); } - /** Dispatches {@link #onLoadStarted(int, MediaPeriodId, LoadEventInfo, MediaLoadData)}. */ - public void loadStarted(DataSpec dataSpec, int dataType, long elapsedRealtimeMs) { + public void loadStarted(LoadEventInfo loadEventInfo, int dataType) { loadStarted( - dataSpec, + loadEventInfo, dataType, - C.TRACK_TYPE_UNKNOWN, - null, - C.SELECTION_REASON_UNKNOWN, - null, - C.TIME_UNSET, - C.TIME_UNSET, - elapsedRealtimeMs); + /* trackType= */ C.TRACK_TYPE_UNKNOWN, + /* trackFormat= */ null, + /* trackSelectionReason= */ C.SELECTION_REASON_UNKNOWN, + /* trackSelectionData= */ null, + /* mediaStartTimeUs= */ C.TIME_UNSET, + /* mediaEndTimeUs= */ C.TIME_UNSET); } - /** Dispatches {@link #onLoadStarted(int, MediaPeriodId, LoadEventInfo, MediaLoadData)}. */ public void loadStarted( - DataSpec dataSpec, + LoadEventInfo loadEventInfo, int dataType, int trackType, @Nullable Format trackFormat, int trackSelectionReason, @Nullable Object trackSelectionData, long mediaStartTimeUs, - long mediaEndTimeUs, - long elapsedRealtimeMs) { + long mediaEndTimeUs) { loadStarted( - new LoadEventInfo( - dataSpec, - dataSpec.uri, - /* responseHeaders= */ Collections.emptyMap(), - elapsedRealtimeMs, - /* loadDurationMs= */ 0, - /* bytesLoaded= */ 0), + loadEventInfo, new MediaLoadData( dataType, trackType, @@ -303,59 +229,36 @@ public interface MediaSourceEventListener { adjustMediaTime(mediaEndTimeUs))); } - /** Dispatches {@link #onLoadStarted(int, MediaPeriodId, LoadEventInfo, MediaLoadData)}. */ public void loadStarted(LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData) { - for (ListenerAndHandler listenerAndHandler : listenerAndHandlers) { - final MediaSourceEventListener listener = listenerAndHandler.listener; - postOrRun( - listenerAndHandler.handler, - () -> listener.onLoadStarted(windowIndex, mediaPeriodId, loadEventInfo, mediaLoadData)); - } + dispatch( + (listener, windowIndex, mediaPeriodId) -> + listener.onLoadStarted(windowIndex, mediaPeriodId, loadEventInfo, mediaLoadData), + MediaSourceEventListener.class); } - /** Dispatches {@link #onLoadCompleted(int, MediaPeriodId, LoadEventInfo, MediaLoadData)}. */ - public void loadCompleted( - DataSpec dataSpec, - Uri uri, - Map> responseHeaders, - int dataType, - long elapsedRealtimeMs, - long loadDurationMs, - long bytesLoaded) { + public void loadCompleted(LoadEventInfo loadEventInfo, int dataType) { loadCompleted( - dataSpec, - uri, - responseHeaders, + loadEventInfo, dataType, - C.TRACK_TYPE_UNKNOWN, - null, - C.SELECTION_REASON_UNKNOWN, - null, - C.TIME_UNSET, - C.TIME_UNSET, - elapsedRealtimeMs, - loadDurationMs, - bytesLoaded); + /* trackType= */ C.TRACK_TYPE_UNKNOWN, + /* trackFormat= */ null, + /* trackSelectionReason= */ C.SELECTION_REASON_UNKNOWN, + /* trackSelectionData= */ null, + /* mediaStartTimeUs= */ C.TIME_UNSET, + /* mediaEndTimeUs= */ C.TIME_UNSET); } - /** Dispatches {@link #onLoadCompleted(int, MediaPeriodId, LoadEventInfo, MediaLoadData)}. */ public void loadCompleted( - DataSpec dataSpec, - Uri uri, - Map> responseHeaders, + LoadEventInfo loadEventInfo, int dataType, int trackType, @Nullable Format trackFormat, int trackSelectionReason, @Nullable Object trackSelectionData, long mediaStartTimeUs, - long mediaEndTimeUs, - long elapsedRealtimeMs, - long loadDurationMs, - long bytesLoaded) { + long mediaEndTimeUs) { loadCompleted( - new LoadEventInfo( - dataSpec, uri, responseHeaders, elapsedRealtimeMs, loadDurationMs, bytesLoaded), + loadEventInfo, new MediaLoadData( dataType, trackType, @@ -366,60 +269,36 @@ public interface MediaSourceEventListener { adjustMediaTime(mediaEndTimeUs))); } - /** Dispatches {@link #onLoadCompleted(int, MediaPeriodId, LoadEventInfo, MediaLoadData)}. */ public void loadCompleted(LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData) { - for (ListenerAndHandler listenerAndHandler : listenerAndHandlers) { - final MediaSourceEventListener listener = listenerAndHandler.listener; - postOrRun( - listenerAndHandler.handler, - () -> - listener.onLoadCompleted(windowIndex, mediaPeriodId, loadEventInfo, mediaLoadData)); - } + dispatch( + (listener, windowIndex, mediaPeriodId) -> + listener.onLoadCompleted(windowIndex, mediaPeriodId, loadEventInfo, mediaLoadData), + MediaSourceEventListener.class); } - /** Dispatches {@link #onLoadCanceled(int, MediaPeriodId, LoadEventInfo, MediaLoadData)}. */ - public void loadCanceled( - DataSpec dataSpec, - Uri uri, - Map> responseHeaders, - int dataType, - long elapsedRealtimeMs, - long loadDurationMs, - long bytesLoaded) { + public void loadCanceled(LoadEventInfo loadEventInfo, int dataType) { loadCanceled( - dataSpec, - uri, - responseHeaders, + loadEventInfo, dataType, - C.TRACK_TYPE_UNKNOWN, - null, - C.SELECTION_REASON_UNKNOWN, - null, - C.TIME_UNSET, - C.TIME_UNSET, - elapsedRealtimeMs, - loadDurationMs, - bytesLoaded); + /* trackType= */ C.TRACK_TYPE_UNKNOWN, + /* trackFormat= */ null, + /* trackSelectionReason= */ C.SELECTION_REASON_UNKNOWN, + /* trackSelectionData= */ null, + /* mediaStartTimeUs= */ C.TIME_UNSET, + /* mediaEndTimeUs= */ C.TIME_UNSET); } - /** Dispatches {@link #onLoadCanceled(int, MediaPeriodId, LoadEventInfo, MediaLoadData)}. */ public void loadCanceled( - DataSpec dataSpec, - Uri uri, - Map> responseHeaders, + LoadEventInfo loadEventInfo, int dataType, int trackType, @Nullable Format trackFormat, int trackSelectionReason, @Nullable Object trackSelectionData, long mediaStartTimeUs, - long mediaEndTimeUs, - long elapsedRealtimeMs, - long loadDurationMs, - long bytesLoaded) { + long mediaEndTimeUs) { loadCanceled( - new LoadEventInfo( - dataSpec, uri, responseHeaders, elapsedRealtimeMs, loadDurationMs, bytesLoaded), + loadEventInfo, new MediaLoadData( dataType, trackType, @@ -430,57 +309,30 @@ public interface MediaSourceEventListener { adjustMediaTime(mediaEndTimeUs))); } - /** Dispatches {@link #onLoadCanceled(int, MediaPeriodId, LoadEventInfo, MediaLoadData)}. */ public void loadCanceled(LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData) { - for (ListenerAndHandler listenerAndHandler : listenerAndHandlers) { - MediaSourceEventListener listener = listenerAndHandler.listener; - postOrRun( - listenerAndHandler.handler, - () -> - listener.onLoadCanceled(windowIndex, mediaPeriodId, loadEventInfo, mediaLoadData)); - } + dispatch( + (listener, windowIndex, mediaPeriodId) -> + listener.onLoadCanceled(windowIndex, mediaPeriodId, loadEventInfo, mediaLoadData), + MediaSourceEventListener.class); } - /** - * Dispatches {@link #onLoadError(int, MediaPeriodId, LoadEventInfo, MediaLoadData, IOException, - * boolean)}. - */ public void loadError( - DataSpec dataSpec, - Uri uri, - Map> responseHeaders, - int dataType, - long elapsedRealtimeMs, - long loadDurationMs, - long bytesLoaded, - IOException error, - boolean wasCanceled) { + LoadEventInfo loadEventInfo, int dataType, IOException error, boolean wasCanceled) { loadError( - dataSpec, - uri, - responseHeaders, + loadEventInfo, dataType, - C.TRACK_TYPE_UNKNOWN, - null, - C.SELECTION_REASON_UNKNOWN, - null, - C.TIME_UNSET, - C.TIME_UNSET, - elapsedRealtimeMs, - loadDurationMs, - bytesLoaded, + /* trackType= */ C.TRACK_TYPE_UNKNOWN, + /* trackFormat= */ null, + /* trackSelectionReason= */ C.SELECTION_REASON_UNKNOWN, + /* trackSelectionData= */ null, + /* mediaStartTimeUs= */ C.TIME_UNSET, + /* mediaEndTimeUs= */ C.TIME_UNSET, error, wasCanceled); } - /** - * Dispatches {@link #onLoadError(int, MediaPeriodId, LoadEventInfo, MediaLoadData, IOException, - * boolean)}. - */ public void loadError( - DataSpec dataSpec, - Uri uri, - Map> responseHeaders, + LoadEventInfo loadEventInfo, int dataType, int trackType, @Nullable Format trackFormat, @@ -488,14 +340,10 @@ public interface MediaSourceEventListener { @Nullable Object trackSelectionData, long mediaStartTimeUs, long mediaEndTimeUs, - long elapsedRealtimeMs, - long loadDurationMs, - long bytesLoaded, IOException error, boolean wasCanceled) { loadError( - new LoadEventInfo( - dataSpec, uri, responseHeaders, elapsedRealtimeMs, loadDurationMs, bytesLoaded), + loadEventInfo, new MediaLoadData( dataType, trackType, @@ -508,37 +356,25 @@ public interface MediaSourceEventListener { wasCanceled); } - /** - * Dispatches {@link #onLoadError(int, MediaPeriodId, LoadEventInfo, MediaLoadData, IOException, - * boolean)}. - */ public void loadError( LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData, IOException error, boolean wasCanceled) { - for (ListenerAndHandler listenerAndHandler : listenerAndHandlers) { - final MediaSourceEventListener listener = listenerAndHandler.listener; - postOrRun( - listenerAndHandler.handler, - () -> - listener.onLoadError( - windowIndex, mediaPeriodId, loadEventInfo, mediaLoadData, error, wasCanceled)); - } + dispatch( + (listener, windowIndex, mediaPeriodId) -> + listener.onLoadError( + windowIndex, mediaPeriodId, loadEventInfo, mediaLoadData, error, wasCanceled), + MediaSourceEventListener.class); } - /** Dispatches {@link #onReadingStarted(int, MediaPeriodId)}. */ public void readingStarted() { - MediaPeriodId mediaPeriodId = Assertions.checkNotNull(this.mediaPeriodId); - for (ListenerAndHandler listenerAndHandler : listenerAndHandlers) { - final MediaSourceEventListener listener = listenerAndHandler.listener; - postOrRun( - listenerAndHandler.handler, - () -> listener.onReadingStarted(windowIndex, mediaPeriodId)); - } + dispatch( + (listener, windowIndex, mediaPeriodId) -> + listener.onReadingStarted(windowIndex, Assertions.checkNotNull(mediaPeriodId)), + MediaSourceEventListener.class); } - /** Dispatches {@link #onUpstreamDiscarded(int, MediaPeriodId, MediaLoadData)}. */ public void upstreamDiscarded(int trackType, long mediaStartTimeUs, long mediaEndTimeUs) { upstreamDiscarded( new MediaLoadData( @@ -551,18 +387,14 @@ public interface MediaSourceEventListener { adjustMediaTime(mediaEndTimeUs))); } - /** Dispatches {@link #onUpstreamDiscarded(int, MediaPeriodId, MediaLoadData)}. */ public void upstreamDiscarded(MediaLoadData mediaLoadData) { - MediaPeriodId mediaPeriodId = Assertions.checkNotNull(this.mediaPeriodId); - for (ListenerAndHandler listenerAndHandler : listenerAndHandlers) { - final MediaSourceEventListener listener = listenerAndHandler.listener; - postOrRun( - listenerAndHandler.handler, - () -> listener.onUpstreamDiscarded(windowIndex, mediaPeriodId, mediaLoadData)); - } + dispatch( + (listener, windowIndex, mediaPeriodId) -> + listener.onUpstreamDiscarded( + windowIndex, Assertions.checkNotNull(mediaPeriodId), mediaLoadData), + MediaSourceEventListener.class); } - /** Dispatches {@link #onDownstreamFormatChanged(int, MediaPeriodId, MediaLoadData)}. */ public void downstreamFormatChanged( int trackType, @Nullable Format trackFormat, @@ -580,38 +412,15 @@ public interface MediaSourceEventListener { /* mediaEndTimeMs= */ C.TIME_UNSET)); } - /** Dispatches {@link #onDownstreamFormatChanged(int, MediaPeriodId, MediaLoadData)}. */ public void downstreamFormatChanged(MediaLoadData mediaLoadData) { - for (ListenerAndHandler listenerAndHandler : listenerAndHandlers) { - final MediaSourceEventListener listener = listenerAndHandler.listener; - postOrRun( - listenerAndHandler.handler, - () -> listener.onDownstreamFormatChanged(windowIndex, mediaPeriodId, mediaLoadData)); - } + dispatch( + (listener, windowIndex, mediaPeriodId) -> + listener.onDownstreamFormatChanged(windowIndex, mediaPeriodId, mediaLoadData), + MediaSourceEventListener.class); } private long adjustMediaTime(long mediaTimeUs) { - long mediaTimeMs = C.usToMs(mediaTimeUs); - return mediaTimeMs == C.TIME_UNSET ? C.TIME_UNSET : mediaTimeOffsetMs + mediaTimeMs; - } - - private void postOrRun(Handler handler, Runnable runnable) { - if (handler.getLooper() == Looper.myLooper()) { - runnable.run(); - } else { - handler.post(runnable); - } - } - - private static final class ListenerAndHandler { - - public final Handler handler; - public final MediaSourceEventListener listener; - - public ListenerAndHandler(Handler handler, MediaSourceEventListener listener) { - this.handler = handler; - this.listener = listener; - } + return adjustMediaTime(mediaTimeUs, mediaTimeOffsetMs); } } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSourceFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSourceFactory.java index c53abd1235..e1c52c097b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSourceFactory.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSourceFactory.java @@ -16,31 +16,40 @@ package com.google.android.exoplayer2.source; import android.net.Uri; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.MediaItem; +import com.google.android.exoplayer2.drm.DrmSession; +import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.offline.StreamKey; +import com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy; import java.util.List; /** Factory for creating {@link MediaSource}s from URIs. */ public interface MediaSourceFactory { - /** - * Sets a list of {@link StreamKey StreamKeys} by which the manifest is filtered. - * - * @param streamKeys A list of {@link StreamKey StreamKeys}. - * @return This factory, for convenience. - * @throws IllegalStateException If {@link #createMediaSource(Uri)} has already been called. - */ - default MediaSourceFactory setStreamKeys(List streamKeys) { + /** @deprecated Use {@link MediaItem.PlaybackProperties#streamKeys} instead. */ + @Deprecated + default MediaSourceFactory setStreamKeys(@Nullable List streamKeys) { return this; } /** - * Creates a new {@link MediaSource} with the specified {@code uri}. + * Sets the {@link DrmSessionManager} to use for acquiring {@link DrmSession DrmSessions}. * - * @param uri The URI to play. - * @return The new {@link MediaSource media source}. + * @param drmSessionManager The {@link DrmSessionManager}. + * @return This factory, for convenience. */ - MediaSource createMediaSource(Uri uri); + MediaSourceFactory setDrmSessionManager(@Nullable DrmSessionManager drmSessionManager); + + /** + * Sets an optional {@link LoadErrorHandlingPolicy}. + * + * @param loadErrorHandlingPolicy A {@link LoadErrorHandlingPolicy}. + * @return This factory, for convenience. + */ + MediaSourceFactory setLoadErrorHandlingPolicy( + @Nullable LoadErrorHandlingPolicy loadErrorHandlingPolicy); /** * Returns the {@link C.ContentType content types} supported by media sources created by this @@ -48,4 +57,18 @@ public interface MediaSourceFactory { */ @C.ContentType int[] getSupportedTypes(); + + /** + * Creates a new {@link MediaSource} with the specified {@link MediaItem}. + * + * @param mediaItem The media item to play. + * @return The new {@link MediaSource media source}. + */ + MediaSource createMediaSource(MediaItem mediaItem); + + /** @deprecated Use {@link #createMediaSource(MediaItem)} instead. */ + @Deprecated + default MediaSource createMediaSource(Uri uri) { + return createMediaSource(MediaItem.fromUri(uri)); + } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaPeriod.java index afa25d6fce..0c5af44816 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaPeriod.java @@ -17,22 +17,26 @@ package com.google.android.exoplayer2.source; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.FormatHolder; import com.google.android.exoplayer2.SeekParameters; +import com.google.android.exoplayer2.decoder.DecoderInputBuffer; +import com.google.android.exoplayer2.offline.StreamKey; import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.util.Assertions; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.IdentityHashMap; +import java.util.List; import org.checkerframework.checker.nullness.compatqual.NullableType; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /** * Merges multiple {@link MediaPeriod}s. */ /* package */ final class MergingMediaPeriod implements MediaPeriod, MediaPeriod.Callback { - public final MediaPeriod[] periods; - + private final MediaPeriod[] periods; private final IdentityHashMap streamPeriodIndices; private final CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory; private final ArrayList childrenPendingPreparation; @@ -42,7 +46,9 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; private MediaPeriod[] enabledPeriods; private SequenceableLoader compositeSequenceableLoader; - public MergingMediaPeriod(CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory, + public MergingMediaPeriod( + CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory, + long[] periodTimeOffsetsUs, MediaPeriod... periods) { this.compositeSequenceableLoaderFactory = compositeSequenceableLoaderFactory; this.periods = periods; @@ -51,6 +57,22 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; compositeSequenceableLoaderFactory.createCompositeSequenceableLoader(); streamPeriodIndices = new IdentityHashMap<>(); enabledPeriods = new MediaPeriod[0]; + for (int i = 0; i < periods.length; i++) { + if (periodTimeOffsetsUs[i] != 0) { + this.periods[i] = new TimeOffsetMediaPeriod(periods[i], periodTimeOffsetsUs[i]); + } + } + } + + /** + * Returns the child period passed to {@link + * #MergingMediaPeriod(CompositeSequenceableLoaderFactory, long[], MediaPeriod...)} at the + * specified index. + */ + public MediaPeriod getChildPeriod(int index) { + return periods[index] instanceof TimeOffsetMediaPeriod + ? ((TimeOffsetMediaPeriod) periods[index]).mediaPeriod + : periods[index]; } @Override @@ -85,8 +107,8 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; int[] streamChildIndices = new int[selections.length]; int[] selectionChildIndices = new int[selections.length]; for (int i = 0; i < selections.length; i++) { - streamChildIndices[i] = streams[i] == null ? C.INDEX_UNSET - : streamPeriodIndices.get(streams[i]); + Integer streamChildIndex = streams[i] == null ? null : streamPeriodIndices.get(streams[i]); + streamChildIndices[i] = streamChildIndex == null ? C.INDEX_UNSET : streamChildIndex; selectionChildIndices[i] = C.INDEX_UNSET; if (selections[i] != null) { TrackGroup trackGroup = selections[i].getTrackGroup(); @@ -136,8 +158,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; // Copy the new streams back into the streams array. System.arraycopy(newStreams, 0, streams, 0, newStreams.length); // Update the local state. - enabledPeriods = new MediaPeriod[enabledPeriodsList.size()]; - enabledPeriodsList.toArray(enabledPeriods); + enabledPeriods = enabledPeriodsList.toArray(new MediaPeriod[0]); compositeSequenceableLoader = compositeSequenceableLoaderFactory.createCompositeSequenceableLoader(enabledPeriods); return positionUs; @@ -181,23 +202,32 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; @Override public long readDiscontinuity() { - long positionUs = periods[0].readDiscontinuity(); - // Periods other than the first one are not allowed to report discontinuities. - for (int i = 1; i < periods.length; i++) { - if (periods[i].readDiscontinuity() != C.TIME_UNSET) { - throw new IllegalStateException("Child reported discontinuity."); - } - } - // It must be possible to seek enabled periods to the new position, if there is one. - if (positionUs != C.TIME_UNSET) { - for (MediaPeriod enabledPeriod : enabledPeriods) { - if (enabledPeriod != periods[0] - && enabledPeriod.seekToUs(positionUs) != positionUs) { + long discontinuityUs = C.TIME_UNSET; + for (MediaPeriod period : enabledPeriods) { + long otherDiscontinuityUs = period.readDiscontinuity(); + if (otherDiscontinuityUs != C.TIME_UNSET) { + if (discontinuityUs == C.TIME_UNSET) { + discontinuityUs = otherDiscontinuityUs; + // First reported discontinuity. Seek all previous periods to the new position. + for (MediaPeriod previousPeriod : enabledPeriods) { + if (previousPeriod == period) { + break; + } + if (previousPeriod.seekToUs(discontinuityUs) != discontinuityUs) { + throw new IllegalStateException("Unexpected child seekToUs result."); + } + } + } else if (otherDiscontinuityUs != discontinuityUs) { + throw new IllegalStateException("Conflicting discontinuities."); + } + } else if (discontinuityUs != C.TIME_UNSET) { + // We already have a discontinuity, seek this period to the new position. + if (period.seekToUs(discontinuityUs) != discontinuityUs) { throw new IllegalStateException("Unexpected child seekToUs result."); } } } - return positionUs; + return discontinuityUs; } @Override @@ -253,4 +283,173 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; Assertions.checkNotNull(callback).onContinueLoadingRequested(this); } + private static final class TimeOffsetMediaPeriod implements MediaPeriod, MediaPeriod.Callback { + + private final MediaPeriod mediaPeriod; + private final long timeOffsetUs; + + private @MonotonicNonNull Callback callback; + + public TimeOffsetMediaPeriod(MediaPeriod mediaPeriod, long timeOffsetUs) { + this.mediaPeriod = mediaPeriod; + this.timeOffsetUs = timeOffsetUs; + } + + @Override + public void prepare(Callback callback, long positionUs) { + this.callback = callback; + mediaPeriod.prepare(/* callback= */ this, positionUs - timeOffsetUs); + } + + @Override + public void maybeThrowPrepareError() throws IOException { + mediaPeriod.maybeThrowPrepareError(); + } + + @Override + public TrackGroupArray getTrackGroups() { + return mediaPeriod.getTrackGroups(); + } + + @Override + public List getStreamKeys(List trackSelections) { + return mediaPeriod.getStreamKeys(trackSelections); + } + + @Override + public long selectTracks( + @NullableType TrackSelection[] selections, + boolean[] mayRetainStreamFlags, + @NullableType SampleStream[] streams, + boolean[] streamResetFlags, + long positionUs) { + @NullableType SampleStream[] childStreams = new SampleStream[streams.length]; + for (int i = 0; i < streams.length; i++) { + TimeOffsetSampleStream sampleStream = (TimeOffsetSampleStream) streams[i]; + childStreams[i] = sampleStream != null ? sampleStream.getChildStream() : null; + } + long startPositionUs = + mediaPeriod.selectTracks( + selections, + mayRetainStreamFlags, + childStreams, + streamResetFlags, + positionUs - timeOffsetUs); + for (int i = 0; i < streams.length; i++) { + @Nullable SampleStream childStream = childStreams[i]; + if (childStream == null) { + streams[i] = null; + } else if (streams[i] == null + || ((TimeOffsetSampleStream) streams[i]).getChildStream() != childStream) { + streams[i] = new TimeOffsetSampleStream(childStream, timeOffsetUs); + } + } + return startPositionUs + timeOffsetUs; + } + + @Override + public void discardBuffer(long positionUs, boolean toKeyframe) { + mediaPeriod.discardBuffer(positionUs - timeOffsetUs, toKeyframe); + } + + @Override + public long readDiscontinuity() { + long discontinuityPositionUs = mediaPeriod.readDiscontinuity(); + return discontinuityPositionUs == C.TIME_UNSET + ? C.TIME_UNSET + : discontinuityPositionUs + timeOffsetUs; + } + + @Override + public long seekToUs(long positionUs) { + return mediaPeriod.seekToUs(positionUs - timeOffsetUs) + timeOffsetUs; + } + + @Override + public long getAdjustedSeekPositionUs(long positionUs, SeekParameters seekParameters) { + return mediaPeriod.getAdjustedSeekPositionUs(positionUs - timeOffsetUs, seekParameters) + + timeOffsetUs; + } + + @Override + public long getBufferedPositionUs() { + long bufferedPositionUs = mediaPeriod.getBufferedPositionUs(); + return bufferedPositionUs == C.TIME_END_OF_SOURCE + ? C.TIME_END_OF_SOURCE + : bufferedPositionUs + timeOffsetUs; + } + + @Override + public long getNextLoadPositionUs() { + long nextLoadPositionUs = mediaPeriod.getNextLoadPositionUs(); + return nextLoadPositionUs == C.TIME_END_OF_SOURCE + ? C.TIME_END_OF_SOURCE + : nextLoadPositionUs + timeOffsetUs; + } + + @Override + public boolean continueLoading(long positionUs) { + return mediaPeriod.continueLoading(positionUs - timeOffsetUs); + } + + @Override + public boolean isLoading() { + return mediaPeriod.isLoading(); + } + + @Override + public void reevaluateBuffer(long positionUs) { + mediaPeriod.reevaluateBuffer(positionUs - timeOffsetUs); + } + + @Override + public void onPrepared(MediaPeriod mediaPeriod) { + Assertions.checkNotNull(callback).onPrepared(/* mediaPeriod= */ this); + } + + @Override + public void onContinueLoadingRequested(MediaPeriod source) { + Assertions.checkNotNull(callback).onContinueLoadingRequested(/* source= */ this); + } + } + + private static final class TimeOffsetSampleStream implements SampleStream { + + private final SampleStream sampleStream; + private final long timeOffsetUs; + + public TimeOffsetSampleStream(SampleStream sampleStream, long timeOffsetUs) { + this.sampleStream = sampleStream; + this.timeOffsetUs = timeOffsetUs; + } + + public SampleStream getChildStream() { + return sampleStream; + } + + @Override + public boolean isReady() { + return sampleStream.isReady(); + } + + @Override + public void maybeThrowError() throws IOException { + sampleStream.maybeThrowError(); + } + + @Override + public int readData( + FormatHolder formatHolder, DecoderInputBuffer buffer, boolean formatRequired) { + int readResult = sampleStream.readData(formatHolder, buffer, formatRequired); + if (readResult == C.RESULT_BUFFER_READ) { + buffer.timeUs = Math.max(0, buffer.timeUs + timeOffsetUs); + } + return readResult; + } + + @Override + public int skipData(long positionUs) { + return sampleStream.skipData(positionUs - timeOffsetUs); + } + } } 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 dd7675f3d4..d69c037a5a 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 @@ -66,34 +66,59 @@ public final class MergingMediaSource extends CompositeMediaSource { private static final int PERIOD_COUNT_UNSET = -1; + private final boolean adjustPeriodTimeOffsets; private final MediaSource[] mediaSources; private final Timeline[] timelines; private final ArrayList pendingTimelineSources; private final CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory; private int periodCount; + private long[][] periodTimeOffsetsUs; @Nullable private IllegalMergeException mergeError; /** + * Creates a merging media source. + * + *

      Offsets between the timestamps in the media sources will not be adjusted. + * * @param mediaSources The {@link MediaSource}s to merge. */ public MergingMediaSource(MediaSource... mediaSources) { - this(new DefaultCompositeSequenceableLoaderFactory(), mediaSources); + this(/* adjustPeriodTimeOffsets= */ false, mediaSources); } /** - * @param compositeSequenceableLoaderFactory A factory to create composite - * {@link SequenceableLoader}s for when this media source loads data from multiple streams - * (video, audio etc...). + * Creates a merging media source. + * + * @param adjustPeriodTimeOffsets Whether to adjust timestamps of the merged media sources to all + * start at the same time. * @param mediaSources The {@link MediaSource}s to merge. */ - public MergingMediaSource(CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory, + public MergingMediaSource(boolean adjustPeriodTimeOffsets, MediaSource... mediaSources) { + this(adjustPeriodTimeOffsets, new DefaultCompositeSequenceableLoaderFactory(), mediaSources); + } + + /** + * Creates a merging media source. + * + * @param adjustPeriodTimeOffsets Whether to adjust timestamps of the merged media sources to all + * start at the same time. + * @param compositeSequenceableLoaderFactory A factory to create composite {@link + * SequenceableLoader}s for when this media source loads data from multiple streams (video, + * audio etc...). + * @param mediaSources The {@link MediaSource}s to merge. + */ + public MergingMediaSource( + boolean adjustPeriodTimeOffsets, + CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory, MediaSource... mediaSources) { + this.adjustPeriodTimeOffsets = adjustPeriodTimeOffsets; this.mediaSources = mediaSources; this.compositeSequenceableLoaderFactory = compositeSequenceableLoaderFactory; pendingTimelineSources = new ArrayList<>(Arrays.asList(mediaSources)); periodCount = PERIOD_COUNT_UNSET; timelines = new Timeline[mediaSources.length]; + periodTimeOffsetsUs = new long[0][]; } @Override @@ -125,16 +150,19 @@ public final class MergingMediaSource extends CompositeMediaSource { for (int i = 0; i < periods.length; i++) { MediaPeriodId childMediaPeriodId = id.copyWithPeriodUid(timelines[i].getUidOfPeriod(periodIndex)); - periods[i] = mediaSources[i].createPeriod(childMediaPeriodId, allocator, startPositionUs); + periods[i] = + mediaSources[i].createPeriod( + childMediaPeriodId, allocator, startPositionUs - periodTimeOffsetsUs[periodIndex][i]); } - return new MergingMediaPeriod(compositeSequenceableLoaderFactory, periods); + return new MergingMediaPeriod( + compositeSequenceableLoaderFactory, periodTimeOffsetsUs[periodIndex], periods); } @Override public void releasePeriod(MediaPeriod mediaPeriod) { MergingMediaPeriod mergingPeriod = (MergingMediaPeriod) mediaPeriod; for (int i = 0; i < mediaSources.length; i++) { - mediaSources[i].releasePeriod(mergingPeriod.periods[i]); + mediaSources[i].releasePeriod(mergingPeriod.getChildPeriod(i)); } } @@ -151,15 +179,24 @@ public final class MergingMediaSource extends CompositeMediaSource { @Override protected void onChildSourceInfoRefreshed( Integer id, MediaSource mediaSource, Timeline timeline) { - if (mergeError == null) { - mergeError = checkTimelineMerges(timeline); - } if (mergeError != null) { return; } + if (periodCount == PERIOD_COUNT_UNSET) { + periodCount = timeline.getPeriodCount(); + } else if (timeline.getPeriodCount() != periodCount) { + mergeError = new IllegalMergeException(IllegalMergeException.REASON_PERIOD_COUNT_MISMATCH); + return; + } + if (periodTimeOffsetsUs.length == 0) { + periodTimeOffsetsUs = new long[periodCount][timelines.length]; + } pendingTimelineSources.remove(mediaSource); timelines[id] = timeline; if (pendingTimelineSources.isEmpty()) { + if (adjustPeriodTimeOffsets) { + computePeriodTimeOffsets(); + } refreshSourceInfo(timelines[0]); } } @@ -171,14 +208,17 @@ public final class MergingMediaSource extends CompositeMediaSource { return id == 0 ? mediaPeriodId : null; } - @Nullable - private IllegalMergeException checkTimelineMerges(Timeline timeline) { - if (periodCount == PERIOD_COUNT_UNSET) { - periodCount = timeline.getPeriodCount(); - } else if (timeline.getPeriodCount() != periodCount) { - return new IllegalMergeException(IllegalMergeException.REASON_PERIOD_COUNT_MISMATCH); + private void computePeriodTimeOffsets() { + Timeline.Period period = new Timeline.Period(); + for (int periodIndex = 0; periodIndex < periodCount; periodIndex++) { + long primaryWindowOffsetUs = + -timelines[0].getPeriod(periodIndex, period).getPositionInWindowUs(); + for (int timelineIndex = 1; timelineIndex < timelines.length; timelineIndex++) { + long secondaryWindowOffsetUs = + -timelines[timelineIndex].getPeriod(periodIndex, period).getPositionInWindowUs(); + periodTimeOffsetsUs[periodIndex][timelineIndex] = + primaryWindowOffsetUs - secondaryWindowOffsetUs; + } } - return null; } - } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaExtractor.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaExtractor.java new file mode 100644 index 0000000000..6cc7c91232 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaExtractor.java @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2020 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; + +import android.net.Uri; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.extractor.Extractor; +import com.google.android.exoplayer2.extractor.ExtractorOutput; +import com.google.android.exoplayer2.extractor.PositionHolder; +import com.google.android.exoplayer2.upstream.DataReader; +import java.io.IOException; + +/** Extracts the contents of a container file from a progressive media stream. */ +/* package */ interface ProgressiveMediaExtractor { + + /** + * Initializes the underlying infrastructure for reading from the input. + * + * @param dataReader The {@link DataReader} from which data should be read. + * @param uri The {@link Uri} from which the media is obtained. + * @param position The initial position of the {@code dataReader} in the stream. + * @param length The length of the stream, or {@link C#LENGTH_UNSET} if length is unknown. + * @param output The {@link ExtractorOutput} that will be used to initialize the selected + * extractor. + * @throws UnrecognizedInputFormatException Thrown if the input format could not be detected. + * @throws IOException Thrown if the input could not be read. + */ + void init(DataReader dataReader, Uri uri, long position, long length, ExtractorOutput output) + throws IOException; + + /** Releases any held resources. */ + void release(); + + /** + * Disables seeking in MP3 streams. + * + *

      MP3 live streams commonly have seekable metadata, despite being unseekable. + */ + void disableSeekingOnMp3Streams(); + + /** + * Returns the current read position in the input stream, or {@link C#POSITION_UNSET} if no input + * is available. + */ + long getCurrentInputPosition(); + + /** + * Notifies the extracting infrastructure that a seek has occurred. + * + * @param position The byte offset in the stream from which data will be provided. + * @param seekTimeUs The seek time in microseconds. + */ + void seek(long position, long seekTimeUs); + + /** + * Extracts data starting at the current input stream position. + * + * @param positionHolder If {@link Extractor#RESULT_SEEK} is returned, this holder is updated to + * hold the position of the required data. + * @return One of the {@link Extractor}{@code .RESULT_*} values. + * @throws IOException If an error occurred reading from the input. + */ + int read(PositionHolder positionHolder) throws IOException; +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaPeriod.java index aa9aaf489f..32c96e14f7 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaPeriod.java @@ -25,16 +25,13 @@ import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.SeekParameters; import com.google.android.exoplayer2.decoder.DecoderInputBuffer; import com.google.android.exoplayer2.drm.DrmSessionManager; -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.ExtractorOutput; import com.google.android.exoplayer2.extractor.PositionHolder; import com.google.android.exoplayer2.extractor.SeekMap; import com.google.android.exoplayer2.extractor.SeekMap.SeekPoints; import com.google.android.exoplayer2.extractor.SeekMap.Unseekable; import com.google.android.exoplayer2.extractor.TrackOutput; -import com.google.android.exoplayer2.extractor.mp3.Mp3Extractor; import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.icy.IcyHeaders; import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; @@ -44,6 +41,7 @@ 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.LoadErrorHandlingPolicy; +import com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy.LoadErrorInfo; import com.google.android.exoplayer2.upstream.Loader; import com.google.android.exoplayer2.upstream.Loader.LoadErrorAction; import com.google.android.exoplayer2.upstream.Loader.Loadable; @@ -53,13 +51,15 @@ import com.google.android.exoplayer2.util.ConditionVariable; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.Util; -import java.io.EOFException; import java.io.IOException; +import java.io.InterruptedIOException; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.Map; import org.checkerframework.checker.nullness.compatqual.NullableType; +import org.checkerframework.checker.nullness.qual.EnsuresNonNull; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /** A {@link MediaPeriod} that extracts data using an {@link Extractor}. */ /* package */ final class ProgressiveMediaPeriod @@ -94,11 +94,11 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; private static final Map ICY_METADATA_HEADERS = createIcyMetadataHeaders(); private static final Format ICY_FORMAT = - Format.createSampleFormat("icy", MimeTypes.APPLICATION_ICY, Format.OFFSET_SAMPLE_RELATIVE); + new Format.Builder().setId("icy").setSampleMimeType(MimeTypes.APPLICATION_ICY).build(); private final Uri uri; private final DataSource dataSource; - private final DrmSessionManager drmSessionManager; + private final DrmSessionManager drmSessionManager; private final LoadErrorHandlingPolicy loadErrorHandlingPolicy; private final EventDispatcher eventDispatcher; private final Listener listener; @@ -106,31 +106,31 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; @Nullable private final String customCacheKey; private final long continueLoadingCheckIntervalBytes; private final Loader loader; - private final ExtractorHolder extractorHolder; + private final ProgressiveMediaExtractor progressiveMediaExtractor; private final ConditionVariable loadCondition; private final Runnable maybeFinishPrepareRunnable; private final Runnable onContinueLoadingRequestedRunnable; private final Handler handler; @Nullable private Callback callback; - @Nullable private SeekMap seekMap; @Nullable private IcyHeaders icyHeaders; private SampleQueue[] sampleQueues; private TrackId[] sampleQueueTrackIds; private boolean sampleQueuesBuilt; - private boolean prepared; - @Nullable private PreparedState preparedState; + private boolean prepared; private boolean haveAudioVideoTracks; + private @MonotonicNonNull TrackState trackState; + private @MonotonicNonNull SeekMap seekMap; + private long durationUs; + private boolean isLive; private int dataType; private boolean seenFirstTrackSelection; private boolean notifyDiscontinuity; private boolean notifiedReadingStarted; private int enabledTrackCount; - private long durationUs; private long length; - private boolean isLive; private long lastSeekPositionUs; private long pendingResetPositionUs; @@ -162,7 +162,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; Uri uri, DataSource dataSource, Extractor[] extractors, - DrmSessionManager drmSessionManager, + DrmSessionManager drmSessionManager, LoadErrorHandlingPolicy loadErrorHandlingPolicy, EventDispatcher eventDispatcher, Listener listener, @@ -179,7 +179,8 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; this.customCacheKey = customCacheKey; this.continueLoadingCheckIntervalBytes = continueLoadingCheckIntervalBytes; loader = new Loader("Loader:ProgressiveMediaPeriod"); - extractorHolder = new ExtractorHolder(extractors); + ProgressiveMediaExtractor progressiveMediaExtractor = new BundledExtractorsAdapter(extractors); + this.progressiveMediaExtractor = progressiveMediaExtractor; loadCondition = new ConditionVariable(); maybeFinishPrepareRunnable = this::maybeFinishPrepare; onContinueLoadingRequestedRunnable = @@ -219,7 +220,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; for (SampleQueue sampleQueue : sampleQueues) { sampleQueue.release(); } - extractorHolder.release(); + progressiveMediaExtractor.release(); } @Override @@ -239,7 +240,8 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; @Override public TrackGroupArray getTrackGroups() { - return getPreparedState().tracks; + assertPrepared(); + return trackState.tracks; } @Override @@ -249,9 +251,9 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; @NullableType SampleStream[] streams, boolean[] streamResetFlags, long positionUs) { - PreparedState preparedState = getPreparedState(); - TrackGroupArray tracks = preparedState.tracks; - boolean[] trackEnabledStates = preparedState.trackEnabledStates; + assertPrepared(); + TrackGroupArray tracks = trackState.tracks; + boolean[] trackEnabledStates = trackState.trackEnabledStates; int oldEnabledTrackCount = enabledTrackCount; // Deselect old tracks. for (int i = 0; i < selections.length; i++) { @@ -281,13 +283,13 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; // If there's still a chance of avoiding a seek, try and seek within the sample queue. if (!seekRequired) { SampleQueue sampleQueue = sampleQueues[track]; - sampleQueue.rewind(); - // A seek can be avoided if we're able to advance to the current playback position in the + // A seek can be avoided if we're able to seek to the current playback position in the // sample queue, or if we haven't read anything from the queue since the previous seek // (this case is common for sparse tracks such as metadata tracks). In all other cases a // seek is required. - seekRequired = sampleQueue.advanceTo(positionUs, true, true) == SampleQueue.ADVANCE_FAILED - && sampleQueue.getReadIndex() != 0; + seekRequired = + !sampleQueue.seekTo(positionUs, /* allowTimeBeyondBuffer= */ true) + && sampleQueue.getReadIndex() != 0; } } } @@ -320,10 +322,11 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; @Override public void discardBuffer(long positionUs, boolean toKeyframe) { + assertPrepared(); if (isPendingReset()) { return; } - boolean[] trackEnabledStates = getPreparedState().trackEnabledStates; + boolean[] trackEnabledStates = trackState.trackEnabledStates; int trackCount = sampleQueues.length; for (int i = 0; i < trackCount; i++) { sampleQueues[i].discardTo(positionUs, toKeyframe, trackEnabledStates[i]); @@ -377,7 +380,8 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; @Override public long getBufferedPositionUs() { - boolean[] trackIsAudioVideoFlags = getPreparedState().trackIsAudioVideoFlags; + assertPrepared(); + boolean[] trackIsAudioVideoFlags = trackState.trackIsAudioVideoFlags; if (loadingFinished) { return C.TIME_END_OF_SOURCE; } else if (isPendingReset()) { @@ -403,9 +407,8 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; @Override public long seekToUs(long positionUs) { - PreparedState preparedState = getPreparedState(); - SeekMap seekMap = preparedState.seekMap; - boolean[] trackIsAudioVideoFlags = preparedState.trackIsAudioVideoFlags; + assertPrepared(); + boolean[] trackIsAudioVideoFlags = trackState.trackIsAudioVideoFlags; // Treat all seeks into non-seekable media as being to t=0. positionUs = seekMap.isSeekable() ? positionUs : 0; @@ -440,14 +443,14 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; @Override public long getAdjustedSeekPositionUs(long positionUs, SeekParameters seekParameters) { - SeekMap seekMap = getPreparedState().seekMap; + assertPrepared(); if (!seekMap.isSeekable()) { // Treat all seeks into non-seekable media as being to t=0. return 0; } SeekPoints seekPoints = seekMap.getSeekPoints(positionUs); - return Util.resolveSeekPositionUs( - positionUs, seekParameters, seekPoints.first.timeUs, seekPoints.second.timeUs); + return seekParameters.resolveSeekPositionUs( + positionUs, seekPoints.first.timeUs, seekPoints.second.timeUs); } // SampleStream methods. @@ -493,10 +496,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; if (loadingFinished && positionUs > sampleQueue.getLargestQueuedTimestampUs()) { skipCount = sampleQueue.advanceToEnd(); } else { - skipCount = sampleQueue.advanceTo(positionUs, true, true); - if (skipCount == SampleQueue.ADVANCE_FAILED) { - skipCount = 0; - } + skipCount = sampleQueue.advanceTo(positionUs); } if (skipCount == 0) { maybeStartDeferredRetry(track); @@ -505,10 +505,10 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; } private void maybeNotifyDownstreamFormat(int track) { - PreparedState preparedState = getPreparedState(); - boolean[] trackNotifiedDownstreamFormats = preparedState.trackNotifiedDownstreamFormats; + assertPrepared(); + boolean[] trackNotifiedDownstreamFormats = trackState.trackNotifiedDownstreamFormats; if (!trackNotifiedDownstreamFormats[track]) { - Format trackFormat = preparedState.tracks.get(track).getFormat(/* index= */ 0); + Format trackFormat = trackState.tracks.get(track).getFormat(/* index= */ 0); eventDispatcher.downstreamFormatChanged( MimeTypes.getTrackType(trackFormat.sampleMimeType), trackFormat, @@ -520,7 +520,8 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; } private void maybeStartDeferredRetry(int track) { - boolean[] trackIsAudioVideoFlags = getPreparedState().trackIsAudioVideoFlags; + assertPrepared(); + boolean[] trackIsAudioVideoFlags = trackState.trackIsAudioVideoFlags; if (!pendingDeferredRetry || !trackIsAudioVideoFlags[track] || sampleQueues[track].isReady(/* loadingFinished= */ false)) { @@ -544,8 +545,8 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; // Loader.Callback implementation. @Override - public void onLoadCompleted(ExtractingLoadable loadable, long elapsedRealtimeMs, - long loadDurationMs) { + public void onLoadCompleted( + ExtractingLoadable loadable, long elapsedRealtimeMs, long loadDurationMs) { if (durationUs == C.TIME_UNSET && seekMap != null) { boolean isSeekable = seekMap.isSeekable(); long largestQueuedTimestampUs = getLargestQueuedTimestampUs(); @@ -553,42 +554,54 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; : largestQueuedTimestampUs + DEFAULT_LAST_SAMPLE_DURATION_US; listener.onSourceInfoRefreshed(durationUs, isSeekable, isLive); } + StatsDataSource dataSource = loadable.dataSource; + LoadEventInfo loadEventInfo = + new LoadEventInfo( + loadable.loadTaskId, + loadable.dataSpec, + dataSource.getLastOpenedUri(), + dataSource.getLastResponseHeaders(), + elapsedRealtimeMs, + loadDurationMs, + dataSource.getBytesRead()); + loadErrorHandlingPolicy.onLoadTaskConcluded(loadable.loadTaskId); eventDispatcher.loadCompleted( - loadable.dataSpec, - loadable.dataSource.getLastOpenedUri(), - loadable.dataSource.getLastResponseHeaders(), + loadEventInfo, C.DATA_TYPE_MEDIA, C.TRACK_TYPE_UNKNOWN, /* trackFormat= */ null, C.SELECTION_REASON_UNKNOWN, /* trackSelectionData= */ null, /* mediaStartTimeUs= */ loadable.seekTimeUs, - durationUs, - elapsedRealtimeMs, - loadDurationMs, - loadable.dataSource.getBytesRead()); + durationUs); copyLengthFromLoader(loadable); loadingFinished = true; Assertions.checkNotNull(callback).onContinueLoadingRequested(this); } @Override - public void onLoadCanceled(ExtractingLoadable loadable, long elapsedRealtimeMs, - long loadDurationMs, boolean released) { + public void onLoadCanceled( + ExtractingLoadable loadable, long elapsedRealtimeMs, long loadDurationMs, boolean released) { + StatsDataSource dataSource = loadable.dataSource; + LoadEventInfo loadEventInfo = + new LoadEventInfo( + loadable.loadTaskId, + loadable.dataSpec, + dataSource.getLastOpenedUri(), + dataSource.getLastResponseHeaders(), + elapsedRealtimeMs, + loadDurationMs, + dataSource.getBytesRead()); + loadErrorHandlingPolicy.onLoadTaskConcluded(loadable.loadTaskId); eventDispatcher.loadCanceled( - loadable.dataSpec, - loadable.dataSource.getLastOpenedUri(), - loadable.dataSource.getLastResponseHeaders(), + loadEventInfo, C.DATA_TYPE_MEDIA, C.TRACK_TYPE_UNKNOWN, /* trackFormat= */ null, C.SELECTION_REASON_UNKNOWN, /* trackSelectionData= */ null, /* mediaStartTimeUs= */ loadable.seekTimeUs, - durationUs, - elapsedRealtimeMs, - loadDurationMs, - loadable.dataSource.getBytesRead()); + durationUs); if (!released) { copyLengthFromLoader(loadable); for (SampleQueue sampleQueue : sampleQueues) { @@ -608,9 +621,29 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; IOException error, int errorCount) { copyLengthFromLoader(loadable); + StatsDataSource dataSource = loadable.dataSource; + LoadEventInfo loadEventInfo = + new LoadEventInfo( + loadable.loadTaskId, + loadable.dataSpec, + dataSource.getLastOpenedUri(), + dataSource.getLastResponseHeaders(), + elapsedRealtimeMs, + loadDurationMs, + dataSource.getBytesRead()); + MediaLoadData mediaLoadData = + new MediaLoadData( + C.DATA_TYPE_MEDIA, + C.TRACK_TYPE_UNKNOWN, + /* trackFormat= */ null, + C.SELECTION_REASON_UNKNOWN, + /* trackSelectionData= */ null, + /* mediaStartTimeMs= */ C.usToMs(loadable.seekTimeUs), + C.usToMs(durationUs)); LoadErrorAction loadErrorAction; long retryDelayMs = - loadErrorHandlingPolicy.getRetryDelayMsFor(dataType, loadDurationMs, error, errorCount); + loadErrorHandlingPolicy.getRetryDelayMsFor( + new LoadErrorInfo(loadEventInfo, mediaLoadData, error, errorCount)); if (retryDelayMs == C.TIME_UNSET) { loadErrorAction = Loader.DONT_RETRY_FATAL; } else /* the load should be retried */ { @@ -622,10 +655,9 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; : Loader.DONT_RETRY; } + boolean wasCanceled = !loadErrorAction.isRetry(); eventDispatcher.loadError( - loadable.dataSpec, - loadable.dataSource.getLastOpenedUri(), - loadable.dataSource.getLastResponseHeaders(), + loadEventInfo, C.DATA_TYPE_MEDIA, C.TRACK_TYPE_UNKNOWN, /* trackFormat= */ null, @@ -633,11 +665,11 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; /* trackSelectionData= */ null, /* mediaStartTimeUs= */ loadable.seekTimeUs, durationUs, - elapsedRealtimeMs, - loadDurationMs, - loadable.dataSource.getBytesRead(), error, - !loadErrorAction.isRetry()); + wasCanceled); + if (wasCanceled) { + loadErrorHandlingPolicy.onLoadTaskConcluded(loadable.loadTaskId); + } return loadErrorAction; } @@ -656,8 +688,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; @Override public void seekMap(SeekMap seekMap) { - this.seekMap = icyHeaders == null ? seekMap : new Unseekable(/* durationUs */ C.TIME_UNSET); - handler.post(maybeFinishPrepareRunnable); + handler.post(() -> setSeekMap(seekMap)); } // Icy metadata. Called by the loading thread. @@ -682,7 +713,12 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; return sampleQueues[i]; } } - SampleQueue trackOutput = new SampleQueue(allocator, drmSessionManager); + SampleQueue trackOutput = + new SampleQueue( + allocator, + /* playbackLooper= */ handler.getLooper(), + drmSessionManager, + eventDispatcher); trackOutput.setUpstreamFormatChangeListener(this); @NullableType TrackId[] sampleQueueTrackIds = Arrays.copyOf(this.sampleQueueTrackIds, trackCount + 1); @@ -694,8 +730,18 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; return trackOutput; } + private void setSeekMap(SeekMap seekMap) { + this.seekMap = icyHeaders == null ? seekMap : new Unseekable(/* durationUs= */ C.TIME_UNSET); + if (!prepared) { + maybeFinishPrepare(); + } + durationUs = seekMap.getDurationUs(); + isLive = length == C.LENGTH_UNSET && seekMap.getDurationUs() == C.TIME_UNSET; + dataType = isLive ? C.DATA_TYPE_MEDIA_PROGRESSIVE_LIVE : C.DATA_TYPE_MEDIA; + listener.onSourceInfoRefreshed(durationUs, seekMap.isSeekable(), isLive); + } + private void maybeFinishPrepare() { - SeekMap seekMap = this.seekMap; if (released || prepared || !sampleQueuesBuilt || seekMap == null) { return; } @@ -708,45 +754,40 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; int trackCount = sampleQueues.length; TrackGroup[] trackArray = new TrackGroup[trackCount]; boolean[] trackIsAudioVideoFlags = new boolean[trackCount]; - durationUs = seekMap.getDurationUs(); for (int i = 0; i < trackCount; i++) { - Format trackFormat = sampleQueues[i].getUpstreamFormat(); - String mimeType = trackFormat.sampleMimeType; + Format trackFormat = Assertions.checkNotNull(sampleQueues[i].getUpstreamFormat()); + @Nullable String mimeType = trackFormat.sampleMimeType; boolean isAudio = MimeTypes.isAudio(mimeType); boolean isAudioVideo = isAudio || MimeTypes.isVideo(mimeType); trackIsAudioVideoFlags[i] = isAudioVideo; haveAudioVideoTracks |= isAudioVideo; - IcyHeaders icyHeaders = this.icyHeaders; + @Nullable IcyHeaders icyHeaders = this.icyHeaders; if (icyHeaders != null) { if (isAudio || sampleQueueTrackIds[i].isIcyTrack) { - Metadata metadata = trackFormat.metadata; - trackFormat = - trackFormat.copyWithMetadata( - metadata == null - ? new Metadata(icyHeaders) - : metadata.copyWithAppendedEntries(icyHeaders)); + @Nullable Metadata metadata = trackFormat.metadata; + if (metadata == null) { + metadata = new Metadata(icyHeaders); + } else { + metadata = metadata.copyWithAppendedEntries(icyHeaders); + } + trackFormat = trackFormat.buildUpon().setMetadata(metadata).build(); } + // Update the track format with the bitrate from the ICY header only if it declares neither + // an average or peak bitrate of its own. if (isAudio - && trackFormat.bitrate == Format.NO_VALUE + && trackFormat.averageBitrate == Format.NO_VALUE + && trackFormat.peakBitrate == Format.NO_VALUE && icyHeaders.bitrate != Format.NO_VALUE) { - trackFormat = trackFormat.copyWithBitrate(icyHeaders.bitrate); + trackFormat = trackFormat.buildUpon().setAverageBitrate(icyHeaders.bitrate).build(); } } trackArray[i] = new TrackGroup(trackFormat); } - isLive = length == C.LENGTH_UNSET && seekMap.getDurationUs() == C.TIME_UNSET; - dataType = isLive ? C.DATA_TYPE_MEDIA_PROGRESSIVE_LIVE : C.DATA_TYPE_MEDIA; - preparedState = - new PreparedState(seekMap, new TrackGroupArray(trackArray), trackIsAudioVideoFlags); + trackState = new TrackState(new TrackGroupArray(trackArray), trackIsAudioVideoFlags); prepared = true; - listener.onSourceInfoRefreshed(durationUs, seekMap.isSeekable(), isLive); Assertions.checkNotNull(callback).onPrepared(this); } - private PreparedState getPreparedState() { - return Assertions.checkNotNull(preparedState); - } - private void copyLengthFromLoader(ExtractingLoadable loadable) { if (length == C.LENGTH_UNSET) { length = loadable.length; @@ -756,9 +797,8 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; private void startLoading() { ExtractingLoadable loadable = new ExtractingLoadable( - uri, dataSource, extractorHolder, /* extractorOutput= */ this, loadCondition); + uri, dataSource, progressiveMediaExtractor, /* extractorOutput= */ this, loadCondition); if (prepared) { - SeekMap seekMap = getPreparedState().seekMap; Assertions.checkState(isPendingReset()); if (durationUs != C.TIME_UNSET && pendingResetPositionUs > durationUs) { loadingFinished = true; @@ -766,23 +806,24 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; return; } loadable.setLoadPosition( - seekMap.getSeekPoints(pendingResetPositionUs).first.position, pendingResetPositionUs); + Assertions.checkNotNull(seekMap).getSeekPoints(pendingResetPositionUs).first.position, + pendingResetPositionUs); pendingResetPositionUs = C.TIME_UNSET; } extractedSamplesCountAtStartOfLoad = getExtractedSamplesCount(); long elapsedRealtimeMs = loader.startLoading( loadable, this, loadErrorHandlingPolicy.getMinimumLoadableRetryCount(dataType)); + DataSpec dataSpec = loadable.dataSpec; eventDispatcher.loadStarted( - loadable.dataSpec, + new LoadEventInfo(loadable.loadTaskId, dataSpec, elapsedRealtimeMs), C.DATA_TYPE_MEDIA, C.TRACK_TYPE_UNKNOWN, /* trackFormat= */ null, C.SELECTION_REASON_UNKNOWN, /* trackSelectionData= */ null, /* mediaStartTimeUs= */ loadable.seekTimeUs, - durationUs, - elapsedRealtimeMs); + durationUs); } /** @@ -840,9 +881,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; int trackCount = sampleQueues.length; for (int i = 0; i < trackCount; i++) { SampleQueue sampleQueue = sampleQueues[i]; - sampleQueue.rewind(); - boolean seekInsideQueue = sampleQueue.advanceTo(positionUs, true, false) - != SampleQueue.ADVANCE_FAILED; + boolean seekInsideQueue = sampleQueue.seekTo(positionUs, /* allowTimeBeyondBuffer= */ false); // If we have AV tracks then an in-buffer seek is successful if the seek into every AV queue // is successful. We ignore whether seeks within non-AV queues are successful in this case, as // they may be sparse or poorly interleaved. If we only have non-AV tracks then a seek is @@ -875,6 +914,13 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; return pendingResetPositionUs != C.TIME_UNSET; } + @EnsuresNonNull({"trackState", "seekMap"}) + private void assertPrepared() { + Assertions.checkState(prepared); + Assertions.checkNotNull(trackState); + Assertions.checkNotNull(seekMap); + } + private final class SampleStreamImpl implements SampleStream { private final int track; @@ -909,9 +955,10 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; /** Loads the media stream and extracts sample data from it. */ /* package */ final class ExtractingLoadable implements Loadable, IcyDataSource.Listener { + private final long loadTaskId; private final Uri uri; private final StatsDataSource dataSource; - private final ExtractorHolder extractorHolder; + private final ProgressiveMediaExtractor progressiveMediaExtractor; private final ExtractorOutput extractorOutput; private final ConditionVariable loadCondition; private final PositionHolder positionHolder; @@ -929,17 +976,18 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; public ExtractingLoadable( Uri uri, DataSource dataSource, - ExtractorHolder extractorHolder, + ProgressiveMediaExtractor progressiveMediaExtractor, ExtractorOutput extractorOutput, ConditionVariable loadCondition) { this.uri = uri; this.dataSource = new StatsDataSource(dataSource); - this.extractorHolder = extractorHolder; + this.progressiveMediaExtractor = progressiveMediaExtractor; this.extractorOutput = extractorOutput; this.loadCondition = loadCondition; this.positionHolder = new PositionHolder(); this.pendingExtractorSeek = true; this.length = C.LENGTH_UNSET; + loadTaskId = LoadEventInfo.getNewId(); dataSpec = buildDataSpec(/* position= */ 0); } @@ -951,10 +999,9 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; } @Override - public void load() throws IOException, InterruptedException { + public void load() throws IOException { int result = Extractor.RESULT_CONTINUE; while (result == Extractor.RESULT_CONTINUE && !loadCanceled) { - ExtractorInput input = null; try { long position = positionHolder.position; dataSpec = buildDataSpec(position); @@ -962,7 +1009,6 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; if (length != C.LENGTH_UNSET) { length += position; } - Uri uri = Assertions.checkNotNull(dataSource.getUri()); icyHeaders = IcyHeaders.parse(dataSource.getResponseHeaders()); DataSource extractorDataSource = dataSource; if (icyHeaders != null && icyHeaders.metadataInterval != C.LENGTH_UNSET) { @@ -970,23 +1016,27 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; icyTrackOutput = icyTrack(); icyTrackOutput.format(ICY_FORMAT); } - input = new DefaultExtractorInput(extractorDataSource, position, length); - Extractor extractor = extractorHolder.selectExtractor(input, extractorOutput, uri); + progressiveMediaExtractor.init( + extractorDataSource, uri, position, length, extractorOutput); - // MP3 live streams commonly have seekable metadata, despite being unseekable. - if (icyHeaders != null && extractor instanceof Mp3Extractor) { - ((Mp3Extractor) extractor).disableSeeking(); + if (icyHeaders != null) { + progressiveMediaExtractor.disableSeekingOnMp3Streams(); } if (pendingExtractorSeek) { - extractor.seek(position, seekTimeUs); + progressiveMediaExtractor.seek(position, seekTimeUs); pendingExtractorSeek = false; } while (result == Extractor.RESULT_CONTINUE && !loadCanceled) { - loadCondition.block(); - result = extractor.read(input, positionHolder); - if (input.getPosition() > position + continueLoadingCheckIntervalBytes) { - position = input.getPosition(); + try { + loadCondition.block(); + } catch (InterruptedException e) { + throw new InterruptedIOException(); + } + result = progressiveMediaExtractor.read(positionHolder); + long currentInputPosition = progressiveMediaExtractor.getCurrentInputPosition(); + if (currentInputPosition > position + continueLoadingCheckIntervalBytes) { + position = currentInputPosition; loadCondition.close(); handler.post(onContinueLoadingRequestedRunnable); } @@ -994,8 +1044,8 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; } finally { if (result == Extractor.RESULT_SEEK) { result = Extractor.RESULT_CONTINUE; - } else if (input != null) { - positionHolder.position = input.getPosition(); + } else if (progressiveMediaExtractor.getCurrentInputPosition() != C.POSITION_UNSET) { + positionHolder.position = progressiveMediaExtractor.getCurrentInputPosition(); } Util.closeQuietly(dataSource); } @@ -1023,13 +1073,14 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; private DataSpec buildDataSpec(long position) { // Disable caching if the content length cannot be resolved, since this is indicative of a // progressive live stream. - return new DataSpec( - uri, - position, - C.LENGTH_UNSET, - customCacheKey, - DataSpec.FLAG_DONT_CACHE_IF_LENGTH_UNKNOWN | DataSpec.FLAG_ALLOW_CACHE_FRAGMENTATION, - ICY_METADATA_HEADERS); + return new DataSpec.Builder() + .setUri(uri) + .setPosition(position) + .setKey(customCacheKey) + .setFlags( + DataSpec.FLAG_DONT_CACHE_IF_LENGTH_UNKNOWN | DataSpec.FLAG_ALLOW_CACHE_FRAGMENTATION) + .setHttpRequestHeaders(ICY_METADATA_HEADERS) + .build(); } private void setLoadPosition(long position, long timeUs) { @@ -1040,87 +1091,15 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; } } - /** Stores a list of extractors and a selected extractor when the format has been detected. */ - private static final class ExtractorHolder { + /** Stores track state. */ + private static final class TrackState { - private final Extractor[] extractors; - - @Nullable private 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. - */ - public ExtractorHolder(Extractor[] extractors) { - this.extractors = extractors; - } - - /** - * Returns an initialized extractor for reading {@code input}, and returns the same extractor on - * 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, ExtractorOutput output, Uri uri) - throws IOException, InterruptedException { - if (extractor != null) { - return extractor; - } - if (extractors.length == 1) { - this.extractor = extractors[0]; - } else { - for (Extractor extractor : extractors) { - try { - if (extractor.sniff(input)) { - this.extractor = extractor; - break; - } - } catch (EOFException e) { - // Do nothing. - } finally { - input.resetPeekPosition(); - } - } - if (extractor == null) { - throw new UnrecognizedInputFormatException( - "None of the available extractors (" - + Util.getCommaDelimitedSimpleClassNames(extractors) - + ") could read the stream.", - uri); - } - } - extractor.init(output); - return extractor; - } - - public void release() { - if (extractor != null) { - extractor.release(); - extractor = null; - } - } - } - - /** Stores state that is initialized when preparation completes. */ - private static final class PreparedState { - - public final SeekMap seekMap; public final TrackGroupArray tracks; public final boolean[] trackIsAudioVideoFlags; public final boolean[] trackEnabledStates; public final boolean[] trackNotifiedDownstreamFormats; - public PreparedState( - SeekMap seekMap, TrackGroupArray tracks, boolean[] trackIsAudioVideoFlags) { - this.seekMap = seekMap; + public TrackState(TrackGroupArray tracks, boolean[] trackIsAudioVideoFlags) { this.tracks = tracks; this.trackIsAudioVideoFlags = trackIsAudioVideoFlags; this.trackEnabledStates = new boolean[tracks.length]; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaSource.java index c88972da62..8885a716ba 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaSource.java @@ -18,6 +18,8 @@ package com.google.android.exoplayer2.source; import android.net.Uri; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.MediaItem; +import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.drm.DrmSession; import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory; @@ -29,7 +31,6 @@ 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; /** * Provides one period that loads data from a {@link Uri} and extracted using an {@link Extractor}. @@ -51,12 +52,11 @@ public final class ProgressiveMediaSource extends BaseMediaSource private final DataSource.Factory dataSourceFactory; private ExtractorsFactory extractorsFactory; - @Nullable private String customCacheKey; - @Nullable private Object tag; - private DrmSessionManager drmSessionManager; + private DrmSessionManager drmSessionManager; private LoadErrorHandlingPolicy loadErrorHandlingPolicy; private int continueLoadingCheckIntervalBytes; - private boolean isCreateCalled; + @Nullable private String customCacheKey; + @Nullable private Object tag; /** * Creates a new factory for {@link ProgressiveMediaSource}s, using the extractors provided by @@ -83,80 +83,50 @@ public final class ProgressiveMediaSource extends BaseMediaSource } /** - * Sets the factory for {@link Extractor}s to process the media stream. The default value is an - * instance of {@link DefaultExtractorsFactory}. - * - * @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. - * @return This factory, for convenience. - * @throws IllegalStateException If {@link #createMediaSource(Uri)} has already been called. * @deprecated Pass the {@link ExtractorsFactory} via {@link #Factory(DataSource.Factory, * ExtractorsFactory)}. This is necessary so that proguard can treat the default extractors * factory as unused. */ @Deprecated - public Factory setExtractorsFactory(ExtractorsFactory extractorsFactory) { - Assertions.checkState(!isCreateCalled); - this.extractorsFactory = extractorsFactory; + public Factory setExtractorsFactory(@Nullable ExtractorsFactory extractorsFactory) { + this.extractorsFactory = + extractorsFactory != null ? extractorsFactory : new DefaultExtractorsFactory(); return this; } /** - * Sets the custom key that uniquely identifies the original stream. Used for cache indexing. - * The default value is {@code null}. - * - * @param customCacheKey A custom key that uniquely identifies the original stream. Used for - * cache indexing. - * @return This factory, for convenience. - * @throws IllegalStateException If {@link #createMediaSource(Uri)} has already been called. + * @deprecated Use {@link MediaItem.Builder#setCustomCacheKey(String)} and {@link + * #createMediaSource(MediaItem)} instead. */ - public Factory setCustomCacheKey(String customCacheKey) { - Assertions.checkState(!isCreateCalled); + @Deprecated + public Factory setCustomCacheKey(@Nullable String customCacheKey) { this.customCacheKey = customCacheKey; return this; } /** - * Sets a tag for the media source which will be published in the {@link - * com.google.android.exoplayer2.Timeline} of the source as {@link - * com.google.android.exoplayer2.Timeline.Window#tag}. - * - * @param tag A tag for the media source. - * @return This factory, for convenience. - * @throws IllegalStateException If {@link #createMediaSource(Uri)} has already been called. + * @deprecated Use {@link MediaItem.Builder#setTag(Object)} and {@link + * #createMediaSource(MediaItem)} instead. */ - public Factory setTag(Object tag) { - Assertions.checkState(!isCreateCalled); + @Deprecated + public Factory setTag(@Nullable Object tag) { this.tag = tag; return this; } - /** - * Sets the {@link DrmSessionManager} to use for acquiring {@link DrmSession DrmSessions}. The - * default value is {@link DrmSessionManager#DUMMY}. - * - * @param drmSessionManager The {@link DrmSessionManager}. - * @return This factory, for convenience. - * @throws IllegalStateException If one of the {@code create} methods has already been called. - */ - public Factory setDrmSessionManager(DrmSessionManager drmSessionManager) { - Assertions.checkState(!isCreateCalled); - this.drmSessionManager = drmSessionManager; - return this; - } - /** * Sets the {@link LoadErrorHandlingPolicy}. The default value is created by calling {@link * DefaultLoadErrorHandlingPolicy#DefaultLoadErrorHandlingPolicy()}. * * @param loadErrorHandlingPolicy A {@link LoadErrorHandlingPolicy}. * @return This factory, for convenience. - * @throws IllegalStateException If {@link #createMediaSource(Uri)} has already been called. */ - public Factory setLoadErrorHandlingPolicy(LoadErrorHandlingPolicy loadErrorHandlingPolicy) { - Assertions.checkState(!isCreateCalled); - this.loadErrorHandlingPolicy = loadErrorHandlingPolicy; + public Factory setLoadErrorHandlingPolicy( + @Nullable LoadErrorHandlingPolicy loadErrorHandlingPolicy) { + this.loadErrorHandlingPolicy = + loadErrorHandlingPolicy != null + ? loadErrorHandlingPolicy + : new DefaultLoadErrorHandlingPolicy(); return this; } @@ -169,32 +139,57 @@ public final class ProgressiveMediaSource extends BaseMediaSource * each invocation of {@link * MediaPeriod.Callback#onContinueLoadingRequested(SequenceableLoader)}. * @return This factory, for convenience. - * @throws IllegalStateException If {@link #createMediaSource(Uri)} has already been called. */ public Factory setContinueLoadingCheckIntervalBytes(int continueLoadingCheckIntervalBytes) { - Assertions.checkState(!isCreateCalled); this.continueLoadingCheckIntervalBytes = continueLoadingCheckIntervalBytes; return this; } /** - * Returns a new {@link ProgressiveMediaSource} using the current parameters. + * Sets the {@link DrmSessionManager} to use for acquiring {@link DrmSession DrmSessions}. The + * default value is {@link DrmSessionManager#DUMMY}. * - * @param uri The {@link Uri}. - * @return The new {@link ProgressiveMediaSource}. + * @param drmSessionManager The {@link DrmSessionManager}. + * @return This factory, for convenience. */ @Override + public Factory setDrmSessionManager(@Nullable DrmSessionManager drmSessionManager) { + this.drmSessionManager = + drmSessionManager != null + ? drmSessionManager + : DrmSessionManager.getDummyDrmSessionManager(); + return this; + } + + /** @deprecated Use {@link #createMediaSource(MediaItem)} instead. */ + @SuppressWarnings("deprecation") + @Deprecated + @Override public ProgressiveMediaSource createMediaSource(Uri uri) { - isCreateCalled = true; + return createMediaSource(new MediaItem.Builder().setUri(uri).build()); + } + + /** + * Returns a new {@link ProgressiveMediaSource} using the current parameters. + * + * @param mediaItem The {@link MediaItem}. + * @return The new {@link ProgressiveMediaSource}. + * @throws NullPointerException if {@link MediaItem#playbackProperties} is {@code null}. + */ + @Override + public ProgressiveMediaSource createMediaSource(MediaItem mediaItem) { + Assertions.checkNotNull(mediaItem.playbackProperties); return new ProgressiveMediaSource( - uri, + mediaItem.playbackProperties.uri, dataSourceFactory, extractorsFactory, drmSessionManager, loadErrorHandlingPolicy, - customCacheKey, + mediaItem.playbackProperties.customCacheKey != null + ? mediaItem.playbackProperties.customCacheKey + : customCacheKey, continueLoadingCheckIntervalBytes, - tag); + mediaItem.playbackProperties.tag != null ? mediaItem.playbackProperties.tag : tag); } @Override @@ -212,12 +207,13 @@ public final class ProgressiveMediaSource extends BaseMediaSource private final Uri uri; private final DataSource.Factory dataSourceFactory; private final ExtractorsFactory extractorsFactory; - private final DrmSessionManager drmSessionManager; + private final DrmSessionManager drmSessionManager; private final LoadErrorHandlingPolicy loadableLoadErrorHandlingPolicy; @Nullable private final String customCacheKey; private final int continueLoadingCheckIntervalBytes; @Nullable private final Object tag; + private boolean timelineIsPlaceholder; private long timelineDurationUs; private boolean timelineIsSeekable; private boolean timelineIsLive; @@ -228,7 +224,7 @@ public final class ProgressiveMediaSource extends BaseMediaSource Uri uri, DataSource.Factory dataSourceFactory, ExtractorsFactory extractorsFactory, - DrmSessionManager drmSessionManager, + DrmSessionManager drmSessionManager, LoadErrorHandlingPolicy loadableLoadErrorHandlingPolicy, @Nullable String customCacheKey, int continueLoadingCheckIntervalBytes, @@ -240,6 +236,7 @@ public final class ProgressiveMediaSource extends BaseMediaSource this.loadableLoadErrorHandlingPolicy = loadableLoadErrorHandlingPolicy; this.customCacheKey = customCacheKey; this.continueLoadingCheckIntervalBytes = continueLoadingCheckIntervalBytes; + this.timelineIsPlaceholder = true; this.timelineDurationUs = C.TIME_UNSET; this.tag = tag; } @@ -254,11 +251,11 @@ public final class ProgressiveMediaSource extends BaseMediaSource protected void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) { transferListener = mediaTransferListener; drmSessionManager.prepare(); - notifySourceInfoRefreshed(timelineDurationUs, timelineIsSeekable, timelineIsLive); + notifySourceInfoRefreshed(); } @Override - public void maybeThrowSourceInfoRefreshError() throws IOException { + public void maybeThrowSourceInfoRefreshError() { // Do nothing. } @@ -297,30 +294,47 @@ public final class ProgressiveMediaSource extends BaseMediaSource public void onSourceInfoRefreshed(long durationUs, boolean isSeekable, boolean isLive) { // If we already have the duration from a previous source info refresh, use it. durationUs = durationUs == C.TIME_UNSET ? timelineDurationUs : durationUs; - if (timelineDurationUs == durationUs + if (!timelineIsPlaceholder + && timelineDurationUs == durationUs && timelineIsSeekable == isSeekable && timelineIsLive == isLive) { // Suppress no-op source info changes. return; } - notifySourceInfoRefreshed(durationUs, isSeekable, isLive); + timelineDurationUs = durationUs; + timelineIsSeekable = isSeekable; + timelineIsLive = isLive; + timelineIsPlaceholder = false; + notifySourceInfoRefreshed(); } // Internal methods. - private void notifySourceInfoRefreshed(long durationUs, boolean isSeekable, boolean isLive) { - timelineDurationUs = durationUs; - timelineIsSeekable = isSeekable; - timelineIsLive = isLive; + private void notifySourceInfoRefreshed() { // TODO: Split up isDynamic into multiple fields to indicate which values may change. Then // indicate that the duration may change until it's known. See [internal: b/69703223]. - refreshSourceInfo( + Timeline timeline = new SinglePeriodTimeline( timelineDurationUs, timelineIsSeekable, /* isDynamic= */ false, /* isLive= */ timelineIsLive, /* manifest= */ null, - tag)); + tag); + if (timelineIsPlaceholder) { + // TODO: Actually prepare the extractors during prepatation so that we don't need a + // placeholder. See https://github.com/google/ExoPlayer/issues/4727. + timeline = + new ForwardingTimeline(timeline) { + @Override + public Window getWindow( + int windowIndex, Window window, long defaultPositionProjectionUs) { + super.getWindow(windowIndex, window, defaultPositionProjectionUs); + window.isPlaceholder = true; + return window; + } + }; + } + refreshSourceInfo(timeline); } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SampleDataQueue.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SampleDataQueue.java new file mode 100644 index 0000000000..7fd95df34f --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SampleDataQueue.java @@ -0,0 +1,472 @@ +/* + * Copyright (C) 2019 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; + +import androidx.annotation.Nullable; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.decoder.CryptoInfo; +import com.google.android.exoplayer2.decoder.DecoderInputBuffer; +import com.google.android.exoplayer2.extractor.TrackOutput.CryptoData; +import com.google.android.exoplayer2.source.SampleQueue.SampleExtrasHolder; +import com.google.android.exoplayer2.upstream.Allocation; +import com.google.android.exoplayer2.upstream.Allocator; +import com.google.android.exoplayer2.upstream.DataReader; +import com.google.android.exoplayer2.util.ParsableByteArray; +import com.google.android.exoplayer2.util.Util; +import java.io.EOFException; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.Arrays; + +/** A queue of media sample data. */ +/* package */ class SampleDataQueue { + + private static final int INITIAL_SCRATCH_SIZE = 32; + + private final Allocator allocator; + private final int allocationLength; + private final ParsableByteArray scratch; + + // References into the linked list of allocations. + private AllocationNode firstAllocationNode; + private AllocationNode readAllocationNode; + private AllocationNode writeAllocationNode; + + // Accessed only by the loading thread (or the consuming thread when there is no loading thread). + private long totalBytesWritten; + + public SampleDataQueue(Allocator allocator) { + this.allocator = allocator; + allocationLength = allocator.getIndividualAllocationLength(); + scratch = new ParsableByteArray(INITIAL_SCRATCH_SIZE); + firstAllocationNode = new AllocationNode(/* startPosition= */ 0, allocationLength); + readAllocationNode = firstAllocationNode; + writeAllocationNode = firstAllocationNode; + } + + // Called by the consuming thread, but only when there is no loading thread. + + /** Clears all sample data. */ + public void reset() { + clearAllocationNodes(firstAllocationNode); + firstAllocationNode = new AllocationNode(0, allocationLength); + readAllocationNode = firstAllocationNode; + writeAllocationNode = firstAllocationNode; + totalBytesWritten = 0; + allocator.trim(); + } + + /** + * Discards sample data bytes from the write side of the queue. + * + * @param totalBytesWritten The reduced total number of bytes written after the samples have been + * discarded, or 0 if the queue is now empty. + */ + public void discardUpstreamSampleBytes(long totalBytesWritten) { + this.totalBytesWritten = totalBytesWritten; + if (this.totalBytesWritten == 0 + || this.totalBytesWritten == firstAllocationNode.startPosition) { + clearAllocationNodes(firstAllocationNode); + firstAllocationNode = new AllocationNode(this.totalBytesWritten, allocationLength); + readAllocationNode = firstAllocationNode; + writeAllocationNode = firstAllocationNode; + } else { + // Find the last node containing at least 1 byte of data that we need to keep. + AllocationNode lastNodeToKeep = firstAllocationNode; + while (this.totalBytesWritten > lastNodeToKeep.endPosition) { + lastNodeToKeep = lastNodeToKeep.next; + } + // Discard all subsequent nodes. + AllocationNode firstNodeToDiscard = lastNodeToKeep.next; + clearAllocationNodes(firstNodeToDiscard); + // Reset the successor of the last node to be an uninitialized node. + lastNodeToKeep.next = new AllocationNode(lastNodeToKeep.endPosition, allocationLength); + // Update writeAllocationNode and readAllocationNode as necessary. + writeAllocationNode = + this.totalBytesWritten == lastNodeToKeep.endPosition + ? lastNodeToKeep.next + : lastNodeToKeep; + if (readAllocationNode == firstNodeToDiscard) { + readAllocationNode = lastNodeToKeep.next; + } + } + } + + // Called by the consuming thread. + + /** Rewinds the read position to the first sample in the queue. */ + public void rewind() { + readAllocationNode = firstAllocationNode; + } + + /** + * Reads data from the rolling buffer to populate a decoder input buffer. + * + * @param buffer The buffer to populate. + * @param extrasHolder The extras holder whose offset should be read and subsequently adjusted. + */ + public void readToBuffer(DecoderInputBuffer buffer, SampleExtrasHolder extrasHolder) { + // Read encryption data if the sample is encrypted. + if (buffer.isEncrypted()) { + readEncryptionData(buffer, extrasHolder); + } + // Read sample data, extracting supplemental data into a separate buffer if needed. + if (buffer.hasSupplementalData()) { + // If there is supplemental data, the sample data is prefixed by its size. + scratch.reset(4); + readData(extrasHolder.offset, scratch.data, 4); + int sampleSize = scratch.readUnsignedIntToInt(); + extrasHolder.offset += 4; + extrasHolder.size -= 4; + + // Write the sample data. + buffer.ensureSpaceForWrite(sampleSize); + readData(extrasHolder.offset, buffer.data, sampleSize); + extrasHolder.offset += sampleSize; + extrasHolder.size -= sampleSize; + + // Write the remaining data as supplemental data. + buffer.resetSupplementalData(extrasHolder.size); + readData(extrasHolder.offset, buffer.supplementalData, extrasHolder.size); + } else { + // Write the sample data. + buffer.ensureSpaceForWrite(extrasHolder.size); + readData(extrasHolder.offset, buffer.data, extrasHolder.size); + } + } + + /** + * Advances the read position to the specified absolute position. + * + * @param absolutePosition The new absolute read position. May be {@link C#POSITION_UNSET}, in + * which case calling this method is a no-op. + */ + public void discardDownstreamTo(long absolutePosition) { + if (absolutePosition == C.POSITION_UNSET) { + return; + } + while (absolutePosition >= firstAllocationNode.endPosition) { + // Advance firstAllocationNode to the specified absolute position. Also clear nodes that are + // advanced past, and return their underlying allocations to the allocator. + allocator.release(firstAllocationNode.allocation); + firstAllocationNode = firstAllocationNode.clear(); + } + if (readAllocationNode.startPosition < firstAllocationNode.startPosition) { + // We discarded the node referenced by readAllocationNode. We need to advance it to the first + // remaining node. + readAllocationNode = firstAllocationNode; + } + } + + // Called by the loading thread. + + public long getTotalBytesWritten() { + return totalBytesWritten; + } + + public int sampleData(DataReader input, int length, boolean allowEndOfInput) throws IOException { + length = preAppend(length); + int bytesAppended = + input.read( + writeAllocationNode.allocation.data, + writeAllocationNode.translateOffset(totalBytesWritten), + length); + if (bytesAppended == C.RESULT_END_OF_INPUT) { + if (allowEndOfInput) { + return C.RESULT_END_OF_INPUT; + } + throw new EOFException(); + } + postAppend(bytesAppended); + return bytesAppended; + } + + public void sampleData(ParsableByteArray buffer, int length) { + while (length > 0) { + int bytesAppended = preAppend(length); + buffer.readBytes( + writeAllocationNode.allocation.data, + writeAllocationNode.translateOffset(totalBytesWritten), + bytesAppended); + length -= bytesAppended; + postAppend(bytesAppended); + } + } + + // Private methods. + + /** + * Reads encryption data for the current sample. + * + *

      The encryption data is written into {@link DecoderInputBuffer#cryptoInfo}, and {@link + * SampleExtrasHolder#size} is adjusted to subtract the number of bytes that were read. The same + * value is added to {@link SampleExtrasHolder#offset}. + * + * @param buffer The buffer into which the encryption data should be written. + * @param extrasHolder The extras holder whose offset should be read and subsequently adjusted. + */ + private void readEncryptionData(DecoderInputBuffer buffer, SampleExtrasHolder extrasHolder) { + long offset = extrasHolder.offset; + + // Read the signal byte. + scratch.reset(1); + readData(offset, scratch.data, 1); + offset++; + byte signalByte = scratch.data[0]; + boolean subsampleEncryption = (signalByte & 0x80) != 0; + int ivSize = signalByte & 0x7F; + + // Read the initialization vector. + CryptoInfo cryptoInfo = buffer.cryptoInfo; + if (cryptoInfo.iv == null) { + cryptoInfo.iv = new byte[16]; + } else { + // Zero out cryptoInfo.iv so that if ivSize < 16, the remaining bytes are correctly set to 0. + Arrays.fill(cryptoInfo.iv, (byte) 0); + } + readData(offset, cryptoInfo.iv, ivSize); + offset += ivSize; + + // Read the subsample count, if present. + int subsampleCount; + if (subsampleEncryption) { + scratch.reset(2); + readData(offset, scratch.data, 2); + offset += 2; + subsampleCount = scratch.readUnsignedShort(); + } else { + subsampleCount = 1; + } + + // Write the clear and encrypted subsample sizes. + @Nullable int[] clearDataSizes = cryptoInfo.numBytesOfClearData; + if (clearDataSizes == null || clearDataSizes.length < subsampleCount) { + clearDataSizes = new int[subsampleCount]; + } + @Nullable int[] encryptedDataSizes = cryptoInfo.numBytesOfEncryptedData; + if (encryptedDataSizes == null || encryptedDataSizes.length < subsampleCount) { + encryptedDataSizes = new int[subsampleCount]; + } + if (subsampleEncryption) { + int subsampleDataLength = 6 * subsampleCount; + scratch.reset(subsampleDataLength); + readData(offset, scratch.data, subsampleDataLength); + offset += subsampleDataLength; + scratch.setPosition(0); + for (int i = 0; i < subsampleCount; i++) { + clearDataSizes[i] = scratch.readUnsignedShort(); + encryptedDataSizes[i] = scratch.readUnsignedIntToInt(); + } + } else { + clearDataSizes[0] = 0; + encryptedDataSizes[0] = extrasHolder.size - (int) (offset - extrasHolder.offset); + } + + // Populate the cryptoInfo. + CryptoData cryptoData = Util.castNonNull(extrasHolder.cryptoData); + cryptoInfo.set( + subsampleCount, + clearDataSizes, + encryptedDataSizes, + cryptoData.encryptionKey, + cryptoInfo.iv, + cryptoData.cryptoMode, + cryptoData.encryptedBlocks, + cryptoData.clearBlocks); + + // Adjust the offset and size to take into account the bytes read. + int bytesRead = (int) (offset - extrasHolder.offset); + extrasHolder.offset += bytesRead; + extrasHolder.size -= bytesRead; + } + + /** + * Reads data from the front of the rolling buffer. + * + * @param absolutePosition The absolute position from which data should be read. + * @param target The buffer into which data should be written. + * @param length The number of bytes to read. + */ + private void readData(long absolutePosition, ByteBuffer target, int length) { + advanceReadTo(absolutePosition); + int remaining = length; + while (remaining > 0) { + int toCopy = Math.min(remaining, (int) (readAllocationNode.endPosition - absolutePosition)); + Allocation allocation = readAllocationNode.allocation; + target.put(allocation.data, readAllocationNode.translateOffset(absolutePosition), toCopy); + remaining -= toCopy; + absolutePosition += toCopy; + if (absolutePosition == readAllocationNode.endPosition) { + readAllocationNode = readAllocationNode.next; + } + } + } + + /** + * Reads data from the front of the rolling buffer. + * + * @param absolutePosition The absolute position from which data should be read. + * @param target The array into which data should be written. + * @param length The number of bytes to read. + */ + private void readData(long absolutePosition, byte[] target, int length) { + advanceReadTo(absolutePosition); + int remaining = length; + while (remaining > 0) { + int toCopy = Math.min(remaining, (int) (readAllocationNode.endPosition - absolutePosition)); + Allocation allocation = readAllocationNode.allocation; + System.arraycopy( + allocation.data, + readAllocationNode.translateOffset(absolutePosition), + target, + length - remaining, + toCopy); + remaining -= toCopy; + absolutePosition += toCopy; + if (absolutePosition == readAllocationNode.endPosition) { + readAllocationNode = readAllocationNode.next; + } + } + } + + /** + * Advances the read position to the specified absolute position. + * + * @param absolutePosition The position to which {@link #readAllocationNode} should be advanced. + */ + private void advanceReadTo(long absolutePosition) { + while (absolutePosition >= readAllocationNode.endPosition) { + readAllocationNode = readAllocationNode.next; + } + } + + /** + * Clears allocation nodes starting from {@code fromNode}. + * + * @param fromNode The node from which to clear. + */ + private void clearAllocationNodes(AllocationNode fromNode) { + if (!fromNode.wasInitialized) { + return; + } + // Bulk release allocations for performance (it's significantly faster when using + // DefaultAllocator because the allocator's lock only needs to be acquired and released once) + // [Internal: See b/29542039]. + int allocationCount = + (writeAllocationNode.wasInitialized ? 1 : 0) + + ((int) (writeAllocationNode.startPosition - fromNode.startPosition) + / allocationLength); + Allocation[] allocationsToRelease = new Allocation[allocationCount]; + AllocationNode currentNode = fromNode; + for (int i = 0; i < allocationsToRelease.length; i++) { + allocationsToRelease[i] = currentNode.allocation; + currentNode = currentNode.clear(); + } + allocator.release(allocationsToRelease); + } + + /** + * Called before writing sample data to {@link #writeAllocationNode}. May cause {@link + * #writeAllocationNode} to be initialized. + * + * @param length The number of bytes that the caller wishes to write. + * @return The number of bytes that the caller is permitted to write, which may be less than + * {@code length}. + */ + private int preAppend(int length) { + if (!writeAllocationNode.wasInitialized) { + writeAllocationNode.initialize( + allocator.allocate(), + new AllocationNode(writeAllocationNode.endPosition, allocationLength)); + } + return Math.min(length, (int) (writeAllocationNode.endPosition - totalBytesWritten)); + } + + /** + * Called after writing sample data. May cause {@link #writeAllocationNode} to be advanced. + * + * @param length The number of bytes that were written. + */ + private void postAppend(int length) { + totalBytesWritten += length; + if (totalBytesWritten == writeAllocationNode.endPosition) { + writeAllocationNode = writeAllocationNode.next; + } + } + + /** A node in a linked list of {@link Allocation}s held by the output. */ + private static final class AllocationNode { + + /** The absolute position of the start of the data (inclusive). */ + public final long startPosition; + /** The absolute position of the end of the data (exclusive). */ + public final long endPosition; + /** Whether the node has been initialized. Remains true after {@link #clear()}. */ + public boolean wasInitialized; + /** The {@link Allocation}, or {@code null} if the node is not initialized. */ + @Nullable public Allocation allocation; + /** + * The next {@link AllocationNode} in the list, or {@code null} if the node has not been + * initialized. Remains set after {@link #clear()}. + */ + @Nullable public AllocationNode next; + + /** + * @param startPosition See {@link #startPosition}. + * @param allocationLength The length of the {@link Allocation} with which this node will be + * initialized. + */ + public AllocationNode(long startPosition, int allocationLength) { + this.startPosition = startPosition; + this.endPosition = startPosition + allocationLength; + } + + /** + * Initializes the node. + * + * @param allocation The node's {@link Allocation}. + * @param next The next {@link AllocationNode}. + */ + public void initialize(Allocation allocation, AllocationNode next) { + this.allocation = allocation; + this.next = next; + wasInitialized = true; + } + + /** + * Gets the offset into the {@link #allocation}'s {@link Allocation#data} that corresponds to + * the specified absolute position. + * + * @param absolutePosition The absolute position. + * @return The corresponding offset into the allocation's data. + */ + public int translateOffset(long absolutePosition) { + return (int) (absolutePosition - startPosition) + allocation.offset; + } + + /** + * Clears {@link #allocation} and {@link #next}. + * + * @return The cleared next {@link AllocationNode}. + */ + public AllocationNode clear() { + allocation = null; + AllocationNode temp = next; + next = null; + return temp; + } + } +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SampleMetadataQueue.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SampleMetadataQueue.java deleted file mode 100644 index bb578ddec7..0000000000 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/SampleMetadataQueue.java +++ /dev/null @@ -1,721 +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; - -import android.os.Looper; -import androidx.annotation.Nullable; -import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.Format; -import com.google.android.exoplayer2.FormatHolder; -import com.google.android.exoplayer2.decoder.DecoderInputBuffer; -import com.google.android.exoplayer2.drm.DrmInitData; -import com.google.android.exoplayer2.drm.DrmSession; -import com.google.android.exoplayer2.drm.DrmSessionManager; -import com.google.android.exoplayer2.extractor.TrackOutput.CryptoData; -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; - -/** - * A queue of metadata describing the contents of a media buffer. - */ -/* package */ final class SampleMetadataQueue { - - /** - * A holder for sample metadata not held by {@link DecoderInputBuffer}. - */ - public static final class SampleExtrasHolder { - - public int size; - public long offset; - public CryptoData cryptoData; - - } - - private static final int SAMPLE_CAPACITY_INCREMENT = 1000; - - private final DrmSessionManager drmSessionManager; - - @Nullable private Format downstreamFormat; - @Nullable private DrmSession currentDrmSession; - - private int capacity; - private int[] sourceIds; - private long[] offsets; - private int[] sizes; - private int[] flags; - private long[] timesUs; - private CryptoData[] cryptoDatas; - private Format[] formats; - - private int length; - private int absoluteFirstIndex; - private int relativeFirstIndex; - private int readPosition; - - private long largestDiscardedTimestampUs; - private long largestQueuedTimestampUs; - private boolean isLastSampleQueued; - private boolean upstreamKeyframeRequired; - private boolean upstreamFormatRequired; - private Format upstreamFormat; - private Format upstreamCommittedFormat; - private int upstreamSourceId; - - public SampleMetadataQueue(DrmSessionManager drmSessionManager) { - this.drmSessionManager = drmSessionManager; - capacity = SAMPLE_CAPACITY_INCREMENT; - sourceIds = new int[capacity]; - offsets = new long[capacity]; - timesUs = new long[capacity]; - flags = new int[capacity]; - sizes = new int[capacity]; - cryptoDatas = new CryptoData[capacity]; - formats = new Format[capacity]; - largestDiscardedTimestampUs = Long.MIN_VALUE; - largestQueuedTimestampUs = Long.MIN_VALUE; - upstreamFormatRequired = true; - upstreamKeyframeRequired = true; - } - - // Called by the consuming thread, but only when there is no loading thread. - - /** - * Clears all sample metadata from the queue. - * - * @param resetUpstreamFormat Whether the upstream format should be cleared. If set to false, - * samples queued after the reset (and before a subsequent call to {@link #format(Format)}) - * are assumed to have the current upstream format. If set to true, {@link #format(Format)} - * must be called after the reset before any more samples can be queued. - */ - public void reset(boolean resetUpstreamFormat) { - length = 0; - absoluteFirstIndex = 0; - relativeFirstIndex = 0; - readPosition = 0; - upstreamKeyframeRequired = true; - largestDiscardedTimestampUs = Long.MIN_VALUE; - largestQueuedTimestampUs = Long.MIN_VALUE; - isLastSampleQueued = false; - upstreamCommittedFormat = null; - if (resetUpstreamFormat) { - upstreamFormat = null; - upstreamFormatRequired = true; - } - } - - /** - * Returns the current absolute write index. - */ - public int getWriteIndex() { - return absoluteFirstIndex + length; - } - - /** - * Discards samples from the write side of the queue. - * - * @param discardFromIndex The absolute index of the first sample to be discarded. - * @return The reduced total number of bytes written after the samples have been discarded, or 0 - * if the queue is now empty. - */ - public long discardUpstreamSamples(int discardFromIndex) { - int discardCount = getWriteIndex() - discardFromIndex; - Assertions.checkArgument(0 <= discardCount && discardCount <= (length - readPosition)); - length -= discardCount; - largestQueuedTimestampUs = Math.max(largestDiscardedTimestampUs, getLargestTimestamp(length)); - isLastSampleQueued = discardCount == 0 && isLastSampleQueued; - if (length == 0) { - return 0; - } else { - int relativeLastWriteIndex = getRelativeIndex(length - 1); - return offsets[relativeLastWriteIndex] + sizes[relativeLastWriteIndex]; - } - } - - public void sourceId(int sourceId) { - upstreamSourceId = sourceId; - } - - // Called by the consuming thread. - - /** - * Throws an error that's preventing data from being read. Does nothing if no such error exists. - * - * @throws IOException The underlying error. - */ - public void maybeThrowError() throws IOException { - // TODO: Avoid throwing if the DRM error is not preventing a read operation. - if (currentDrmSession != null && currentDrmSession.getState() == DrmSession.STATE_ERROR) { - throw Assertions.checkNotNull(currentDrmSession.getError()); - } - } - - /** Releases any owned {@link DrmSession} references. */ - public void releaseDrmSessionReferences() { - if (currentDrmSession != null) { - currentDrmSession.release(); - currentDrmSession = null; - // Clear downstream format to avoid violating the assumption that downstreamFormat.drmInitData - // != null implies currentSession != null - downstreamFormat = null; - } - } - - /** Returns the current absolute start index. */ - public int getFirstIndex() { - return absoluteFirstIndex; - } - - /** - * Returns the current absolute read index. - */ - public int getReadIndex() { - return absoluteFirstIndex + readPosition; - } - - /** - * Peeks the source id of the next sample to be read, or the current upstream source id if the - * queue is empty or if the read position is at the end of the queue. - * - * @return The source id. - */ - public synchronized int peekSourceId() { - int relativeReadIndex = getRelativeIndex(readPosition); - return hasNextSample() ? sourceIds[relativeReadIndex] : upstreamSourceId; - } - - /** - * Returns the upstream {@link Format} in which samples are being queued. - */ - public synchronized Format getUpstreamFormat() { - return upstreamFormatRequired ? null : upstreamFormat; - } - - /** - * Returns the largest sample timestamp that has been queued since the last call to - * {@link #reset(boolean)}. - *

      - * Samples that were discarded by calling {@link #discardUpstreamSamples(int)} are not - * considered as having been queued. Samples that were dequeued from the front of the queue are - * considered as having been queued. - * - * @return The largest sample timestamp that has been queued, or {@link Long#MIN_VALUE} if no - * samples have been queued. - */ - public synchronized long getLargestQueuedTimestampUs() { - return largestQueuedTimestampUs; - } - - /** - * Returns whether the last sample of the stream has knowingly been queued. A return value of - * {@code false} means that the last sample had not been queued or that it's unknown whether the - * last sample has been queued. - * - *

      Samples that were discarded by calling {@link #discardUpstreamSamples(int)} are not - * considered as having been queued. Samples that were dequeued from the front of the queue are - * considered as having been queued. - */ - public synchronized boolean isLastSampleQueued() { - return isLastSampleQueued; - } - - /** Returns the timestamp of the first sample, or {@link Long#MIN_VALUE} if the queue is empty. */ - public synchronized long getFirstTimestampUs() { - return length == 0 ? Long.MIN_VALUE : timesUs[relativeFirstIndex]; - } - - /** - * Rewinds the read position to the first sample retained in the queue. - */ - public synchronized void rewind() { - readPosition = 0; - } - - /** - * Returns whether there is data available for reading. - * - *

      Note: If the stream has ended then a buffer with the end of stream flag can always be read - * from {@link #read}. Hence an ended stream is always ready. - * - * @param loadingFinished Whether no more samples will be written to the sample queue. When true, - * this method returns true if the sample queue is empty, because an empty sample queue means - * the end of stream has been reached. When false, this method returns false if the sample - * queue is empty. - */ - public boolean isReady(boolean loadingFinished) { - if (!hasNextSample()) { - return loadingFinished - || isLastSampleQueued - || (upstreamFormat != null && upstreamFormat != downstreamFormat); - } - int relativeReadIndex = getRelativeIndex(readPosition); - if (formats[relativeReadIndex] != downstreamFormat) { - // A format can be read. - return true; - } - return mayReadSample(relativeReadIndex); - } - - /** - * Attempts to read from the queue. - * - * @param formatHolder A {@link FormatHolder} to populate in the case of reading a format. - * @param buffer A {@link DecoderInputBuffer} to populate in the case of reading a sample or the - * end of the stream. If a sample is read then the buffer is populated with information about - * the sample, but not its data. The size and absolute position of the data in the rolling - * buffer is stored in {@code extrasHolder}, along with an encryption id if present and the - * absolute position of the first byte that may still be required after the current sample has - * been read. If a {@link DecoderInputBuffer#isFlagsOnly() flags-only} buffer is passed, only - * the buffer flags may be populated by this method and the read position of the queue will - * not change. May be null if the caller requires that the format of the stream be read even - * if it's not changing. - * @param formatRequired Whether the caller requires that the format of the stream be read even if - * it's not changing. A sample will never be read if set to true, however it is still possible - * for the end of stream or nothing to be read. - * @param loadingFinished True if an empty queue should be considered the end of the stream. - * @param extrasHolder The holder into which extra sample information should be written. - * @return The result, which can be {@link C#RESULT_NOTHING_READ}, {@link C#RESULT_FORMAT_READ} or - * {@link C#RESULT_BUFFER_READ}. - */ - @SuppressWarnings("ReferenceEquality") - public synchronized int read( - FormatHolder formatHolder, - DecoderInputBuffer buffer, - boolean formatRequired, - boolean loadingFinished, - SampleExtrasHolder extrasHolder) { - if (!hasNextSample()) { - if (loadingFinished || isLastSampleQueued) { - buffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM); - return C.RESULT_BUFFER_READ; - } else if (upstreamFormat != null && (formatRequired || upstreamFormat != downstreamFormat)) { - onFormatResult(Assertions.checkNotNull(upstreamFormat), formatHolder); - return C.RESULT_FORMAT_READ; - } else { - return C.RESULT_NOTHING_READ; - } - } - - int relativeReadIndex = getRelativeIndex(readPosition); - if (formatRequired || formats[relativeReadIndex] != downstreamFormat) { - onFormatResult(formats[relativeReadIndex], formatHolder); - return C.RESULT_FORMAT_READ; - } - - if (!mayReadSample(relativeReadIndex)) { - return C.RESULT_NOTHING_READ; - } - - buffer.setFlags(flags[relativeReadIndex]); - buffer.timeUs = timesUs[relativeReadIndex]; - if (buffer.isFlagsOnly()) { - return C.RESULT_BUFFER_READ; - } - - extrasHolder.size = sizes[relativeReadIndex]; - extrasHolder.offset = offsets[relativeReadIndex]; - extrasHolder.cryptoData = cryptoDatas[relativeReadIndex]; - - readPosition++; - return C.RESULT_BUFFER_READ; - } - - /** - * Attempts to advance the read position to the sample before or at the specified time. - * - * @param timeUs The time to advance to. - * @param toKeyframe If true then attempts to advance to the keyframe before or at the specified - * time, rather than to any sample before or at that time. - * @param allowTimeBeyondBuffer Whether the operation can succeed if {@code timeUs} is beyond the - * end of the queue, by advancing the read position to the last sample (or keyframe) in the - * queue. - * @return The number of samples that were skipped if the operation was successful, which may be - * equal to 0, or {@link SampleQueue#ADVANCE_FAILED} if the operation was not successful. A - * successful advance is one in which the read position was unchanged or advanced, and is now - * at a sample meeting the specified criteria. - */ - public synchronized int advanceTo(long timeUs, boolean toKeyframe, - boolean allowTimeBeyondBuffer) { - int relativeReadIndex = getRelativeIndex(readPosition); - if (!hasNextSample() || timeUs < timesUs[relativeReadIndex] - || (timeUs > largestQueuedTimestampUs && !allowTimeBeyondBuffer)) { - return SampleQueue.ADVANCE_FAILED; - } - int offset = findSampleBefore(relativeReadIndex, length - readPosition, timeUs, toKeyframe); - if (offset == -1) { - return SampleQueue.ADVANCE_FAILED; - } - readPosition += offset; - return offset; - } - - /** - * Advances the read position to the end of the queue. - * - * @return The number of samples that were skipped. - */ - public synchronized int advanceToEnd() { - int skipCount = length - readPosition; - readPosition = length; - return skipCount; - } - - /** - * Attempts to set the read position to the specified sample index. - * - * @param sampleIndex The sample index. - * @return Whether the read position was set successfully. False is returned if the specified - * index is smaller than the index of the first sample in the queue, or larger than the index - * of the next sample that will be written. - */ - public synchronized boolean setReadPosition(int sampleIndex) { - if (absoluteFirstIndex <= sampleIndex && sampleIndex <= absoluteFirstIndex + length) { - readPosition = sampleIndex - absoluteFirstIndex; - return true; - } - return false; - } - - /** - * Discards up to but not including the sample immediately before or at the specified time. - * - * @param timeUs The time to discard up to. - * @param toKeyframe If true then discards samples up to the keyframe before or at the specified - * time, rather than just any sample before or at that time. - * @param stopAtReadPosition If true then samples are only discarded if they're before the read - * position. If false then samples at and beyond the read position may be discarded, in which - * case the read position is advanced to the first remaining sample. - * @return The corresponding offset up to which data should be discarded, or - * {@link C#POSITION_UNSET} if no discarding of data is necessary. - */ - public synchronized long discardTo(long timeUs, boolean toKeyframe, boolean stopAtReadPosition) { - if (length == 0 || timeUs < timesUs[relativeFirstIndex]) { - return C.POSITION_UNSET; - } - int searchLength = stopAtReadPosition && readPosition != length ? readPosition + 1 : length; - int discardCount = findSampleBefore(relativeFirstIndex, searchLength, timeUs, toKeyframe); - if (discardCount == -1) { - return C.POSITION_UNSET; - } - return discardSamples(discardCount); - } - - /** - * Discards samples up to but not including the read position. - * - * @return The corresponding offset up to which data should be discarded, or - * {@link C#POSITION_UNSET} if no discarding of data is necessary. - */ - public synchronized long discardToRead() { - if (readPosition == 0) { - return C.POSITION_UNSET; - } - return discardSamples(readPosition); - } - - /** - * Discards all samples in the queue. The read position is also advanced. - * - * @return The corresponding offset up to which data should be discarded, or - * {@link C#POSITION_UNSET} if no discarding of data is necessary. - */ - public synchronized long discardToEnd() { - if (length == 0) { - return C.POSITION_UNSET; - } - return discardSamples(length); - } - - // Called by the loading thread. - - public synchronized boolean format(Format format) { - if (format == null) { - upstreamFormatRequired = true; - return false; - } - upstreamFormatRequired = false; - if (Util.areEqual(format, upstreamFormat)) { - // The format is unchanged. If format and upstreamFormat are different objects, we keep the - // current upstreamFormat so we can detect format changes in read() using cheap referential - // equality. - return false; - } else if (Util.areEqual(format, upstreamCommittedFormat)) { - // The format has changed back to the format of the last committed sample. If they are - // different objects, we revert back to using upstreamCommittedFormat as the upstreamFormat so - // we can detect format changes in read() using cheap referential equality. - upstreamFormat = upstreamCommittedFormat; - return true; - } else { - upstreamFormat = format; - return true; - } - } - - public synchronized void commitSample(long timeUs, @C.BufferFlags int sampleFlags, long offset, - int size, CryptoData cryptoData) { - if (upstreamKeyframeRequired) { - if ((sampleFlags & C.BUFFER_FLAG_KEY_FRAME) == 0) { - return; - } - upstreamKeyframeRequired = false; - } - Assertions.checkState(!upstreamFormatRequired); - - isLastSampleQueued = (sampleFlags & C.BUFFER_FLAG_LAST_SAMPLE) != 0; - largestQueuedTimestampUs = Math.max(largestQueuedTimestampUs, timeUs); - - int relativeEndIndex = getRelativeIndex(length); - timesUs[relativeEndIndex] = timeUs; - offsets[relativeEndIndex] = offset; - sizes[relativeEndIndex] = size; - flags[relativeEndIndex] = sampleFlags; - cryptoDatas[relativeEndIndex] = cryptoData; - formats[relativeEndIndex] = upstreamFormat; - sourceIds[relativeEndIndex] = upstreamSourceId; - upstreamCommittedFormat = upstreamFormat; - - length++; - if (length == capacity) { - // Increase the capacity. - int newCapacity = capacity + SAMPLE_CAPACITY_INCREMENT; - int[] newSourceIds = new int[newCapacity]; - long[] newOffsets = new long[newCapacity]; - long[] newTimesUs = new long[newCapacity]; - int[] newFlags = new int[newCapacity]; - int[] newSizes = new int[newCapacity]; - CryptoData[] newCryptoDatas = new CryptoData[newCapacity]; - Format[] newFormats = new Format[newCapacity]; - int beforeWrap = capacity - relativeFirstIndex; - System.arraycopy(offsets, relativeFirstIndex, newOffsets, 0, beforeWrap); - System.arraycopy(timesUs, relativeFirstIndex, newTimesUs, 0, beforeWrap); - System.arraycopy(flags, relativeFirstIndex, newFlags, 0, beforeWrap); - System.arraycopy(sizes, relativeFirstIndex, newSizes, 0, beforeWrap); - System.arraycopy(cryptoDatas, relativeFirstIndex, newCryptoDatas, 0, beforeWrap); - System.arraycopy(formats, relativeFirstIndex, newFormats, 0, beforeWrap); - System.arraycopy(sourceIds, relativeFirstIndex, newSourceIds, 0, beforeWrap); - int afterWrap = relativeFirstIndex; - System.arraycopy(offsets, 0, newOffsets, beforeWrap, afterWrap); - System.arraycopy(timesUs, 0, newTimesUs, beforeWrap, afterWrap); - System.arraycopy(flags, 0, newFlags, beforeWrap, afterWrap); - System.arraycopy(sizes, 0, newSizes, beforeWrap, afterWrap); - System.arraycopy(cryptoDatas, 0, newCryptoDatas, beforeWrap, afterWrap); - System.arraycopy(formats, 0, newFormats, beforeWrap, afterWrap); - System.arraycopy(sourceIds, 0, newSourceIds, beforeWrap, afterWrap); - offsets = newOffsets; - timesUs = newTimesUs; - flags = newFlags; - sizes = newSizes; - cryptoDatas = newCryptoDatas; - formats = newFormats; - sourceIds = newSourceIds; - relativeFirstIndex = 0; - length = capacity; - capacity = newCapacity; - } - } - - /** - * Attempts to discard samples from the end of the queue to allow samples starting from the - * specified timestamp to be spliced in. Samples will not be discarded prior to the read position. - * - * @param timeUs The timestamp at which the splice occurs. - * @return Whether the splice was successful. - */ - public synchronized boolean attemptSplice(long timeUs) { - if (length == 0) { - return timeUs > largestDiscardedTimestampUs; - } - long largestReadTimestampUs = Math.max(largestDiscardedTimestampUs, - getLargestTimestamp(readPosition)); - if (largestReadTimestampUs >= timeUs) { - return false; - } - int retainCount = length; - int relativeSampleIndex = getRelativeIndex(length - 1); - while (retainCount > readPosition && timesUs[relativeSampleIndex] >= timeUs) { - retainCount--; - relativeSampleIndex--; - if (relativeSampleIndex == -1) { - relativeSampleIndex = capacity - 1; - } - } - discardUpstreamSamples(absoluteFirstIndex + retainCount); - return true; - } - - // Internal methods. - - private boolean hasNextSample() { - return readPosition != length; - } - - /** - * Sets the downstream format, performs DRM resource management, and populates the {@code - * outputFormatHolder}. - * - * @param newFormat The new downstream format. - * @param outputFormatHolder The output {@link FormatHolder}. - */ - private void onFormatResult(Format newFormat, FormatHolder outputFormatHolder) { - outputFormatHolder.format = newFormat; - boolean isFirstFormat = downstreamFormat == null; - DrmInitData oldDrmInitData = isFirstFormat ? null : downstreamFormat.drmInitData; - downstreamFormat = newFormat; - if (drmSessionManager == DrmSessionManager.DUMMY) { - // Avoid attempting to acquire a session using the dummy DRM session manager. It's likely that - // the media source creation has not yet been migrated and the renderer can acquire the - // session for the read DRM init data. - // TODO: Remove once renderers are migrated [Internal ref: b/122519809]. - return; - } - DrmInitData newDrmInitData = newFormat.drmInitData; - outputFormatHolder.includesDrmSession = true; - outputFormatHolder.drmSession = currentDrmSession; - if (!isFirstFormat && Util.areEqual(oldDrmInitData, newDrmInitData)) { - // Nothing to do. - return; - } - // Ensure we acquire the new session before releasing the previous one in case the same session - // is being used for both DrmInitData. - DrmSession previousSession = currentDrmSession; - Looper playbackLooper = Assertions.checkNotNull(Looper.myLooper()); - currentDrmSession = - newDrmInitData != null - ? drmSessionManager.acquireSession(playbackLooper, newDrmInitData) - : drmSessionManager.acquirePlaceholderSession( - playbackLooper, MimeTypes.getTrackType(newFormat.sampleMimeType)); - outputFormatHolder.drmSession = currentDrmSession; - - if (previousSession != null) { - previousSession.release(); - } - } - - /** - * Returns whether it's possible to read the next sample. - * - * @param relativeReadIndex The relative read index of the next sample. - * @return Whether it's possible to read the next sample. - */ - private boolean mayReadSample(int relativeReadIndex) { - if (drmSessionManager == DrmSessionManager.DUMMY) { - // TODO: Remove once renderers are migrated [Internal ref: b/122519809]. - // For protected content it's likely that the DrmSessionManager is still being injected into - // the renderers. We assume that the renderers will be able to acquire a DrmSession if needed. - return true; - } - return currentDrmSession == null - || currentDrmSession.getState() == DrmSession.STATE_OPENED_WITH_KEYS - || ((flags[relativeReadIndex] & C.BUFFER_FLAG_ENCRYPTED) == 0 - && currentDrmSession.playClearSamplesWithoutKeys()); - } - - /** - * Finds the sample in the specified range that's before or at the specified time. If {@code - * keyframe} is {@code true} then the sample is additionally required to be a keyframe. - * - * @param relativeStartIndex The relative index from which to start searching. - * @param length The length of the range being searched. - * @param timeUs The specified time. - * @param keyframe Whether only keyframes should be considered. - * @return The offset from {@code relativeFirstIndex} to the found sample, or -1 if no matching - * sample was found. - */ - private int findSampleBefore(int relativeStartIndex, int length, long timeUs, boolean keyframe) { - // This could be optimized to use a binary search, however in practice callers to this method - // normally pass times near to the start of the search region. Hence it's unclear whether - // switching to a binary search would yield any real benefit. - int sampleCountToTarget = -1; - int searchIndex = relativeStartIndex; - for (int i = 0; i < length && timesUs[searchIndex] <= timeUs; i++) { - if (!keyframe || (flags[searchIndex] & C.BUFFER_FLAG_KEY_FRAME) != 0) { - // We've found a suitable sample. - sampleCountToTarget = i; - } - searchIndex++; - if (searchIndex == capacity) { - searchIndex = 0; - } - } - return sampleCountToTarget; - } - - /** - * Discards the specified number of samples. - * - * @param discardCount The number of samples to discard. - * @return The corresponding offset up to which data should be discarded. - */ - private long discardSamples(int discardCount) { - largestDiscardedTimestampUs = Math.max(largestDiscardedTimestampUs, - getLargestTimestamp(discardCount)); - length -= discardCount; - absoluteFirstIndex += discardCount; - relativeFirstIndex += discardCount; - if (relativeFirstIndex >= capacity) { - relativeFirstIndex -= capacity; - } - readPosition -= discardCount; - if (readPosition < 0) { - readPosition = 0; - } - if (length == 0) { - int relativeLastDiscardIndex = (relativeFirstIndex == 0 ? capacity : relativeFirstIndex) - 1; - return offsets[relativeLastDiscardIndex] + sizes[relativeLastDiscardIndex]; - } else { - return offsets[relativeFirstIndex]; - } - } - - /** - * Finds the largest timestamp of any sample from the start of the queue up to the specified - * length, assuming that the timestamps prior to a keyframe are always less than the timestamp of - * the keyframe itself, and of subsequent frames. - * - * @param length The length of the range being searched. - * @return The largest timestamp, or {@link Long#MIN_VALUE} if {@code length == 0}. - */ - private long getLargestTimestamp(int length) { - if (length == 0) { - return Long.MIN_VALUE; - } - long largestTimestampUs = Long.MIN_VALUE; - int relativeSampleIndex = getRelativeIndex(length - 1); - for (int i = 0; i < length; i++) { - largestTimestampUs = Math.max(largestTimestampUs, timesUs[relativeSampleIndex]); - if ((flags[relativeSampleIndex] & C.BUFFER_FLAG_KEY_FRAME) != 0) { - break; - } - relativeSampleIndex--; - if (relativeSampleIndex == -1) { - relativeSampleIndex = capacity - 1; - } - } - return largestTimestampUs; - } - - /** - * Returns the relative index for a given offset from the start of the queue. - * - * @param offset The offset, which must be in the range [0, length]. - */ - private int getRelativeIndex(int offset) { - int relativeIndex = relativeFirstIndex + offset; - return relativeIndex < capacity ? relativeIndex : relativeIndex - capacity; - } - -} 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 1230b45fe4..3c08012cb8 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 @@ -1,5 +1,5 @@ /* - * Copyright (C) 2016 The Android Open Source Project + * Copyright (C) 2019 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. @@ -15,29 +15,32 @@ */ package com.google.android.exoplayer2.source; +import android.os.Looper; +import androidx.annotation.CallSuper; import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.FormatHolder; import com.google.android.exoplayer2.decoder.DecoderInputBuffer; +import com.google.android.exoplayer2.drm.DrmInitData; import com.google.android.exoplayer2.drm.DrmSession; import com.google.android.exoplayer2.drm.DrmSessionManager; -import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.extractor.TrackOutput; -import com.google.android.exoplayer2.source.SampleMetadataQueue.SampleExtrasHolder; -import com.google.android.exoplayer2.upstream.Allocation; import com.google.android.exoplayer2.upstream.Allocator; +import com.google.android.exoplayer2.upstream.DataReader; +import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.MediaSourceEventDispatcher; +import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.ParsableByteArray; -import java.io.EOFException; +import com.google.android.exoplayer2.util.Util; import java.io.IOException; -import java.nio.ByteBuffer; +import org.checkerframework.checker.nullness.compatqual.NullableType; /** A queue of media samples. */ public class SampleQueue implements TrackOutput { - /** - * A listener for changes to the upstream format. - */ + /** A listener for changes to the upstream format. */ public interface UpstreamFormatChangedListener { /** @@ -46,75 +49,121 @@ public class SampleQueue implements TrackOutput { * @param format The new upstream format. */ void onUpstreamFormatChanged(Format format); - } - public static final int ADVANCE_FAILED = -1; + @VisibleForTesting /* package */ static final int SAMPLE_CAPACITY_INCREMENT = 1000; - private static final int INITIAL_SCRATCH_SIZE = 32; - - private final Allocator allocator; - private final int allocationLength; - private final SampleMetadataQueue metadataQueue; + private final SampleDataQueue sampleDataQueue; private final SampleExtrasHolder extrasHolder; - private final ParsableByteArray scratch; + private final Looper playbackLooper; + private final DrmSessionManager drmSessionManager; + private final MediaSourceEventDispatcher eventDispatcher; + @Nullable private UpstreamFormatChangedListener upstreamFormatChangeListener; - // References into the linked list of allocations. - private AllocationNode firstAllocationNode; - private AllocationNode readAllocationNode; - private AllocationNode writeAllocationNode; + @Nullable private Format downstreamFormat; + @Nullable private DrmSession currentDrmSession; + + private int capacity; + private int[] sourceIds; + private long[] offsets; + private int[] sizes; + private int[] flags; + private long[] timesUs; + private @NullableType CryptoData[] cryptoDatas; + private Format[] formats; + + private int length; + private int absoluteFirstIndex; + private int relativeFirstIndex; + private int readPosition; + + private long largestDiscardedTimestampUs; + private long largestQueuedTimestampUs; + private boolean isLastSampleQueued; + private boolean upstreamKeyframeRequired; + private boolean upstreamFormatRequired; + private boolean upstreamFormatAdjustmentRequired; + @Nullable private Format unadjustedUpstreamFormat; + @Nullable private Format upstreamFormat; + @Nullable private Format upstreamCommittedFormat; + private int upstreamSourceId; - // Accessed only by the loading thread (or the consuming thread when there is no loading thread). - private boolean pendingFormatAdjustment; - private Format lastUnadjustedFormat; private long sampleOffsetUs; - private long totalBytesWritten; private boolean pendingSplice; - private UpstreamFormatChangedListener upstreamFormatChangeListener; /** * Creates a sample queue. * * @param allocator An {@link Allocator} from which allocations for sample data can be obtained. + * @param playbackLooper The looper associated with the media playback thread. * @param drmSessionManager The {@link DrmSessionManager} to obtain {@link DrmSession DrmSessions} * from. The created instance does not take ownership of this {@link DrmSessionManager}. + * @param eventDispatcher A {@link MediaSourceEventDispatcher} to notify of events related to this + * SampleQueue. */ - public SampleQueue(Allocator allocator, DrmSessionManager drmSessionManager) { - this.allocator = allocator; - allocationLength = allocator.getIndividualAllocationLength(); - metadataQueue = new SampleMetadataQueue(drmSessionManager); + public SampleQueue( + Allocator allocator, + Looper playbackLooper, + DrmSessionManager drmSessionManager, + MediaSourceEventDispatcher eventDispatcher) { + this.playbackLooper = playbackLooper; + this.drmSessionManager = drmSessionManager; + this.eventDispatcher = eventDispatcher; + sampleDataQueue = new SampleDataQueue(allocator); extrasHolder = new SampleExtrasHolder(); - scratch = new ParsableByteArray(INITIAL_SCRATCH_SIZE); - firstAllocationNode = new AllocationNode(0, allocationLength); - readAllocationNode = firstAllocationNode; - writeAllocationNode = firstAllocationNode; + capacity = SAMPLE_CAPACITY_INCREMENT; + sourceIds = new int[capacity]; + offsets = new long[capacity]; + timesUs = new long[capacity]; + flags = new int[capacity]; + sizes = new int[capacity]; + cryptoDatas = new CryptoData[capacity]; + formats = new Format[capacity]; + largestDiscardedTimestampUs = Long.MIN_VALUE; + largestQueuedTimestampUs = Long.MIN_VALUE; + upstreamFormatRequired = true; + upstreamKeyframeRequired = true; } - // Called by the consuming thread, but only when there is no loading thread. + // Called by the consuming thread when there is no loading thread. - /** - * Resets the output without clearing the upstream format. Equivalent to {@code reset(false)}. - */ - public void reset() { - reset(false); + /** Calls {@link #reset(boolean) reset(true)} and releases any resources owned by the queue. */ + @CallSuper + public void release() { + reset(/* resetUpstreamFormat= */ true); + releaseDrmSessionReferences(); + } + + /** Convenience method for {@code reset(false)}. */ + public final void reset() { + reset(/* resetUpstreamFormat= */ false); } /** - * Resets the output. + * Clears all samples from the queue. * * @param resetUpstreamFormat Whether the upstream format should be cleared. If set to false, * samples queued after the reset (and before a subsequent call to {@link #format(Format)}) * are assumed to have the current upstream format. If set to true, {@link #format(Format)} * must be called after the reset before any more samples can be queued. */ + @CallSuper public void reset(boolean resetUpstreamFormat) { - metadataQueue.reset(resetUpstreamFormat); - clearAllocationNodes(firstAllocationNode); - firstAllocationNode = new AllocationNode(0, allocationLength); - readAllocationNode = firstAllocationNode; - writeAllocationNode = firstAllocationNode; - totalBytesWritten = 0; - allocator.trim(); + sampleDataQueue.reset(); + length = 0; + absoluteFirstIndex = 0; + relativeFirstIndex = 0; + readPosition = 0; + upstreamKeyframeRequired = true; + largestDiscardedTimestampUs = Long.MIN_VALUE; + largestQueuedTimestampUs = Long.MIN_VALUE; + isLastSampleQueued = false; + upstreamCommittedFormat = null; + if (resetUpstreamFormat) { + unadjustedUpstreamFormat = null; + upstreamFormat = null; + upstreamFormatRequired = true; + } } /** @@ -122,22 +171,18 @@ public class SampleQueue implements TrackOutput { * * @param sourceId The source identifier. */ - public void sourceId(int sourceId) { - metadataQueue.sourceId(sourceId); + public final void sourceId(int sourceId) { + upstreamSourceId = sourceId; } - /** - * Indicates samples that are subsequently queued should be spliced into those already queued. - */ - public void splice() { + /** Indicates samples that are subsequently queued should be spliced into those already queued. */ + public final void splice() { pendingSplice = true; } - /** - * Returns the current absolute write index. - */ - public int getWriteIndex() { - return metadataQueue.getWriteIndex(); + /** Returns the current absolute write index. */ + public final int getWriteIndex() { + return absoluteFirstIndex + length; } /** @@ -146,56 +191,40 @@ public class SampleQueue implements TrackOutput { * @param discardFromIndex The absolute index of the first sample to be discarded. Must be in the * range [{@link #getReadIndex()}, {@link #getWriteIndex()}]. */ - public void discardUpstreamSamples(int discardFromIndex) { - totalBytesWritten = metadataQueue.discardUpstreamSamples(discardFromIndex); - if (totalBytesWritten == 0 || totalBytesWritten == firstAllocationNode.startPosition) { - clearAllocationNodes(firstAllocationNode); - firstAllocationNode = new AllocationNode(totalBytesWritten, allocationLength); - readAllocationNode = firstAllocationNode; - writeAllocationNode = firstAllocationNode; - } else { - // Find the last node containing at least 1 byte of data that we need to keep. - AllocationNode lastNodeToKeep = firstAllocationNode; - while (totalBytesWritten > lastNodeToKeep.endPosition) { - lastNodeToKeep = lastNodeToKeep.next; - } - // Discard all subsequent nodes. - AllocationNode firstNodeToDiscard = lastNodeToKeep.next; - clearAllocationNodes(firstNodeToDiscard); - // Reset the successor of the last node to be an uninitialized node. - lastNodeToKeep.next = new AllocationNode(lastNodeToKeep.endPosition, allocationLength); - // Update writeAllocationNode and readAllocationNode as necessary. - writeAllocationNode = totalBytesWritten == lastNodeToKeep.endPosition ? lastNodeToKeep.next - : lastNodeToKeep; - if (readAllocationNode == firstNodeToDiscard) { - readAllocationNode = lastNodeToKeep.next; - } - } + public final void discardUpstreamSamples(int discardFromIndex) { + sampleDataQueue.discardUpstreamSampleBytes(discardUpstreamSampleMetadata(discardFromIndex)); } // Called by the consuming thread. + /** Calls {@link #discardToEnd()} and releases any resources owned by the queue. */ + @CallSuper + public void preRelease() { + discardToEnd(); + releaseDrmSessionReferences(); + } + /** * Throws an error that's preventing data from being read. Does nothing if no such error exists. * * @throws IOException The underlying error. */ + @CallSuper public void maybeThrowError() throws IOException { - metadataQueue.maybeThrowError(); + // TODO: Avoid throwing if the DRM error is not preventing a read operation. + if (currentDrmSession != null && currentDrmSession.getState() == DrmSession.STATE_ERROR) { + throw Assertions.checkNotNull(currentDrmSession.getError()); + } } - /** - * Returns the absolute index of the first sample. - */ - public int getFirstIndex() { - return metadataQueue.getFirstIndex(); + /** Returns the current absolute start index. */ + public final int getFirstIndex() { + return absoluteFirstIndex; } - /** - * Returns the current absolute read index. - */ - public int getReadIndex() { - return metadataQueue.getReadIndex(); + /** Returns the current absolute read index. */ + public final int getReadIndex() { + return absoluteFirstIndex + readPosition; } /** @@ -204,129 +233,74 @@ public class SampleQueue implements TrackOutput { * * @return The source id. */ - public int peekSourceId() { - return metadataQueue.peekSourceId(); + public final synchronized int peekSourceId() { + int relativeReadIndex = getRelativeIndex(readPosition); + return hasNextSample() ? sourceIds[relativeReadIndex] : upstreamSourceId; } - /** - * Returns the upstream {@link Format} in which samples are being queued. - */ - public Format getUpstreamFormat() { - return metadataQueue.getUpstreamFormat(); + /** Returns the upstream {@link Format} in which samples are being queued. */ + @Nullable + public final synchronized Format getUpstreamFormat() { + return upstreamFormatRequired ? null : upstreamFormat; } /** * Returns the largest sample timestamp that has been queued since the last {@link #reset}. - *

      - * Samples that were discarded by calling {@link #discardUpstreamSamples(int)} are not + * + *

      Samples that were discarded by calling {@link #discardUpstreamSamples(int)} are not * considered as having been queued. Samples that were dequeued from the front of the queue are * considered as having been queued. * * @return The largest sample timestamp that has been queued, or {@link Long#MIN_VALUE} if no * samples have been queued. */ - public long getLargestQueuedTimestampUs() { - return metadataQueue.getLargestQueuedTimestampUs(); + public final synchronized long getLargestQueuedTimestampUs() { + return largestQueuedTimestampUs; } /** * Returns whether the last sample of the stream has knowingly been queued. A return value of * {@code false} means that the last sample had not been queued or that it's unknown whether the * last sample has been queued. + * + *

      Samples that were discarded by calling {@link #discardUpstreamSamples(int)} are not + * considered as having been queued. Samples that were dequeued from the front of the queue are + * considered as having been queued. */ - public boolean isLastSampleQueued() { - return metadataQueue.isLastSampleQueued(); + public final synchronized boolean isLastSampleQueued() { + return isLastSampleQueued; } /** Returns the timestamp of the first sample, or {@link Long#MIN_VALUE} if the queue is empty. */ - public long getFirstTimestampUs() { - return metadataQueue.getFirstTimestampUs(); + public final synchronized long getFirstTimestampUs() { + return length == 0 ? Long.MIN_VALUE : timesUs[relativeFirstIndex]; } /** - * Rewinds the read position to the first sample in the queue. - */ - public void rewind() { - metadataQueue.rewind(); - readAllocationNode = firstAllocationNode; - } - - /** - * Discards up to but not including the sample immediately before or at the specified time. + * Returns whether there is data available for reading. * - * @param timeUs The time to discard to. - * @param toKeyframe If true then discards samples up to the keyframe before or at the specified - * time, rather than any sample before or at that time. - * @param stopAtReadPosition If true then samples are only discarded if they're before the - * read position. If false then samples at and beyond the read position may be discarded, in - * which case the read position is advanced to the first remaining sample. - */ - public void discardTo(long timeUs, boolean toKeyframe, boolean stopAtReadPosition) { - discardDownstreamTo(metadataQueue.discardTo(timeUs, toKeyframe, stopAtReadPosition)); - } - - /** - * Discards up to but not including the read position. - */ - public void discardToRead() { - discardDownstreamTo(metadataQueue.discardToRead()); - } - - /** Calls {@link #discardToEnd()} and releases any owned {@link DrmSession} references. */ - public void preRelease() { - discardToEnd(); - metadataQueue.releaseDrmSessionReferences(); - } - - /** Calls {@link #reset()} and releases any owned {@link DrmSession} references. */ - public void release() { - reset(); - metadataQueue.releaseDrmSessionReferences(); - } - - /** - * Discards to the end of the queue. The read position is also advanced. - */ - public void discardToEnd() { - discardDownstreamTo(metadataQueue.discardToEnd()); - } - - /** - * Advances the read position to the end of the queue. + *

      Note: If the stream has ended then a buffer with the end of stream flag can always be read + * from {@link #read}. Hence an ended stream is always ready. * - * @return The number of samples that were skipped. + * @param loadingFinished Whether no more samples will be written to the sample queue. When true, + * this method returns true if the sample queue is empty, because an empty sample queue means + * the end of stream has been reached. When false, this method returns false if the sample + * queue is empty. */ - public int advanceToEnd() { - return metadataQueue.advanceToEnd(); - } - - /** - * Attempts to advance the read position to the sample before or at the specified time. - * - * @param timeUs The time to advance to. - * @param toKeyframe If true then attempts to advance to the keyframe before or at the specified - * time, rather than to any sample before or at that time. - * @param allowTimeBeyondBuffer Whether the operation can succeed if {@code timeUs} is beyond the - * end of the queue, by advancing the read position to the last sample (or keyframe). - * @return The number of samples that were skipped if the operation was successful, which may be - * equal to 0, or {@link #ADVANCE_FAILED} if the operation was not successful. A successful - * advance is one in which the read position was unchanged or advanced, and is now at a sample - * meeting the specified criteria. - */ - public int advanceTo(long timeUs, boolean toKeyframe, boolean allowTimeBeyondBuffer) { - return metadataQueue.advanceTo(timeUs, toKeyframe, allowTimeBeyondBuffer); - } - - /** - * Attempts to set the read position to the specified sample index. - * - * @param sampleIndex The sample index. - * @return Whether the read position was set successfully. False is returned if the specified - * index is smaller than the index of the first sample in the queue, or larger than the index - * of the next sample that will be written. - */ - public boolean setReadPosition(int sampleIndex) { - return metadataQueue.setReadPosition(sampleIndex); + @SuppressWarnings("ReferenceEquality") // See comments in setUpstreamFormat + @CallSuper + public synchronized boolean isReady(boolean loadingFinished) { + if (!hasNextSample()) { + return loadingFinished + || isLastSampleQueued + || (upstreamFormat != null && upstreamFormat != downstreamFormat); + } + int relativeReadIndex = getRelativeIndex(readPosition); + if (formats[relativeReadIndex] != downstreamFormat) { + // A format can be read. + return true; + } + return mayReadSample(relativeReadIndex); } /** @@ -356,7 +330,7 @@ public class SampleQueue implements TrackOutput { * @return The result, which can be {@link C#RESULT_NOTHING_READ}, {@link C#RESULT_FORMAT_READ} or * {@link C#RESULT_BUFFER_READ}. */ - @SuppressWarnings("ReferenceEquality") + @CallSuper public int read( FormatHolder formatHolder, DecoderInputBuffer buffer, @@ -364,284 +338,159 @@ public class SampleQueue implements TrackOutput { boolean loadingFinished, long decodeOnlyUntilUs) { int result = - metadataQueue.read(formatHolder, buffer, formatRequired, loadingFinished, extrasHolder); - if (result == C.RESULT_BUFFER_READ && !buffer.isEndOfStream()) { - if (buffer.timeUs < decodeOnlyUntilUs) { - buffer.addFlag(C.BUFFER_FLAG_DECODE_ONLY); - } - if (!buffer.isFlagsOnly()) { - readToBuffer(buffer, extrasHolder); - } + readSampleMetadata( + formatHolder, buffer, formatRequired, loadingFinished, decodeOnlyUntilUs, extrasHolder); + if (result == C.RESULT_BUFFER_READ && !buffer.isEndOfStream() && !buffer.isFlagsOnly()) { + sampleDataQueue.readToBuffer(buffer, extrasHolder); } return result; } /** - * Returns whether there is data available for reading. + * Attempts to seek the read position to the specified sample index. * - *

      Note: If the stream has ended then a buffer with the end of stream flag can always be read - * from {@link #read}. Hence an ended stream is always ready. - * - * @param loadingFinished Whether no more samples will be written to the sample queue. When true, - * this method returns true if the sample queue is empty, because an empty sample queue means - * the end of stream has been reached. When false, this method returns false if the sample - * queue is empty. + * @param sampleIndex The sample index. + * @return Whether the seek was successful. */ - public boolean isReady(boolean loadingFinished) { - return metadataQueue.isReady(loadingFinished); + public final synchronized boolean seekTo(int sampleIndex) { + rewind(); + if (sampleIndex < absoluteFirstIndex || sampleIndex > absoluteFirstIndex + length) { + return false; + } + readPosition = sampleIndex - absoluteFirstIndex; + return true; } /** - * Reads data from the rolling buffer to populate a decoder input buffer. + * Attempts to seek the read position to the keyframe before or at the specified time. * - * @param buffer The buffer to populate. - * @param extrasHolder The extras holder whose offset should be read and subsequently adjusted. + * @param timeUs The time to seek to. + * @param allowTimeBeyondBuffer Whether the operation can succeed if {@code timeUs} is beyond the + * end of the queue, by seeking to the last sample (or keyframe). + * @return Whether the seek was successful. */ - private void readToBuffer(DecoderInputBuffer buffer, SampleExtrasHolder extrasHolder) { - // Read encryption data if the sample is encrypted. - if (buffer.isEncrypted()) { - readEncryptionData(buffer, extrasHolder); + public final synchronized boolean seekTo(long timeUs, boolean allowTimeBeyondBuffer) { + rewind(); + int relativeReadIndex = getRelativeIndex(readPosition); + if (!hasNextSample() + || timeUs < timesUs[relativeReadIndex] + || (timeUs > largestQueuedTimestampUs && !allowTimeBeyondBuffer)) { + return false; } - // Read sample data, extracting supplemental data into a separate buffer if needed. - if (buffer.hasSupplementalData()) { - // If there is supplemental data, the sample data is prefixed by its size. - scratch.reset(4); - readData(extrasHolder.offset, scratch.data, 4); - int sampleSize = scratch.readUnsignedIntToInt(); - extrasHolder.offset += 4; - extrasHolder.size -= 4; - - // Write the sample data. - buffer.ensureSpaceForWrite(sampleSize); - readData(extrasHolder.offset, buffer.data, sampleSize); - extrasHolder.offset += sampleSize; - extrasHolder.size -= sampleSize; - - // Write the remaining data as supplemental data. - buffer.resetSupplementalData(extrasHolder.size); - readData(extrasHolder.offset, buffer.supplementalData, extrasHolder.size); - } else { - // Write the sample data. - buffer.ensureSpaceForWrite(extrasHolder.size); - readData(extrasHolder.offset, buffer.data, extrasHolder.size); + int offset = + findSampleBefore(relativeReadIndex, length - readPosition, timeUs, /* keyframe= */ true); + if (offset == -1) { + return false; } + readPosition += offset; + return true; } /** - * Reads encryption data for the current sample. + * Advances the read position to the keyframe before or at the specified time. * - *

      The encryption data is written into {@link DecoderInputBuffer#cryptoInfo}, and {@link - * SampleExtrasHolder#size} is adjusted to subtract the number of bytes that were read. The same - * value is added to {@link SampleExtrasHolder#offset}. - * - * @param buffer The buffer into which the encryption data should be written. - * @param extrasHolder The extras holder whose offset should be read and subsequently adjusted. + * @param timeUs The time to advance to. + * @return The number of samples that were skipped, which may be equal to 0. */ - private void readEncryptionData(DecoderInputBuffer buffer, SampleExtrasHolder extrasHolder) { - long offset = extrasHolder.offset; - - // Read the signal byte. - scratch.reset(1); - readData(offset, scratch.data, 1); - offset++; - byte signalByte = scratch.data[0]; - boolean subsampleEncryption = (signalByte & 0x80) != 0; - int ivSize = signalByte & 0x7F; - - // Read the initialization vector. - if (buffer.cryptoInfo.iv == null) { - buffer.cryptoInfo.iv = new byte[16]; + public final synchronized int advanceTo(long timeUs) { + int relativeReadIndex = getRelativeIndex(readPosition); + if (!hasNextSample() || timeUs < timesUs[relativeReadIndex]) { + return 0; } - readData(offset, buffer.cryptoInfo.iv, ivSize); - offset += ivSize; - - // Read the subsample count, if present. - int subsampleCount; - if (subsampleEncryption) { - scratch.reset(2); - readData(offset, scratch.data, 2); - offset += 2; - subsampleCount = scratch.readUnsignedShort(); - } else { - subsampleCount = 1; + int offset = + findSampleBefore(relativeReadIndex, length - readPosition, timeUs, /* keyframe= */ true); + if (offset == -1) { + return 0; } - - // Write the clear and encrypted subsample sizes. - int[] clearDataSizes = buffer.cryptoInfo.numBytesOfClearData; - if (clearDataSizes == null || clearDataSizes.length < subsampleCount) { - clearDataSizes = new int[subsampleCount]; - } - int[] encryptedDataSizes = buffer.cryptoInfo.numBytesOfEncryptedData; - if (encryptedDataSizes == null || encryptedDataSizes.length < subsampleCount) { - encryptedDataSizes = new int[subsampleCount]; - } - if (subsampleEncryption) { - int subsampleDataLength = 6 * subsampleCount; - scratch.reset(subsampleDataLength); - readData(offset, scratch.data, subsampleDataLength); - offset += subsampleDataLength; - scratch.setPosition(0); - for (int i = 0; i < subsampleCount; i++) { - clearDataSizes[i] = scratch.readUnsignedShort(); - encryptedDataSizes[i] = scratch.readUnsignedIntToInt(); - } - } else { - clearDataSizes[0] = 0; - encryptedDataSizes[0] = extrasHolder.size - (int) (offset - extrasHolder.offset); - } - - // Populate the cryptoInfo. - CryptoData cryptoData = extrasHolder.cryptoData; - buffer.cryptoInfo.set(subsampleCount, clearDataSizes, encryptedDataSizes, - cryptoData.encryptionKey, buffer.cryptoInfo.iv, cryptoData.cryptoMode, - cryptoData.encryptedBlocks, cryptoData.clearBlocks); - - // Adjust the offset and size to take into account the bytes read. - int bytesRead = (int) (offset - extrasHolder.offset); - extrasHolder.offset += bytesRead; - extrasHolder.size -= bytesRead; + readPosition += offset; + return offset; } /** - * Reads data from the front of the rolling buffer. + * Advances the read position to the end of the queue. * - * @param absolutePosition The absolute position from which data should be read. - * @param target The buffer into which data should be written. - * @param length The number of bytes to read. + * @return The number of samples that were skipped. */ - private void readData(long absolutePosition, ByteBuffer target, int length) { - advanceReadTo(absolutePosition); - int remaining = length; - while (remaining > 0) { - int toCopy = Math.min(remaining, (int) (readAllocationNode.endPosition - absolutePosition)); - Allocation allocation = readAllocationNode.allocation; - target.put(allocation.data, readAllocationNode.translateOffset(absolutePosition), toCopy); - remaining -= toCopy; - absolutePosition += toCopy; - if (absolutePosition == readAllocationNode.endPosition) { - readAllocationNode = readAllocationNode.next; - } - } + public final synchronized int advanceToEnd() { + int skipCount = length - readPosition; + readPosition = length; + return skipCount; } /** - * Reads data from the front of the rolling buffer. + * Discards up to but not including the sample immediately before or at the specified time. * - * @param absolutePosition The absolute position from which data should be read. - * @param target The array into which data should be written. - * @param length The number of bytes to read. + * @param timeUs The time to discard up to. + * @param toKeyframe If true then discards samples up to the keyframe before or at the specified + * time, rather than any sample before or at that time. + * @param stopAtReadPosition If true then samples are only discarded if they're before the read + * position. If false then samples at and beyond the read position may be discarded, in which + * case the read position is advanced to the first remaining sample. */ - private void readData(long absolutePosition, byte[] target, int length) { - advanceReadTo(absolutePosition); - int remaining = length; - while (remaining > 0) { - int toCopy = Math.min(remaining, (int) (readAllocationNode.endPosition - absolutePosition)); - Allocation allocation = readAllocationNode.allocation; - System.arraycopy(allocation.data, readAllocationNode.translateOffset(absolutePosition), - target, length - remaining, toCopy); - remaining -= toCopy; - absolutePosition += toCopy; - if (absolutePosition == readAllocationNode.endPosition) { - readAllocationNode = readAllocationNode.next; - } - } + public final void discardTo(long timeUs, boolean toKeyframe, boolean stopAtReadPosition) { + sampleDataQueue.discardDownstreamTo( + discardSampleMetadataTo(timeUs, toKeyframe, stopAtReadPosition)); } - /** - * Advances {@link #readAllocationNode} to the specified absolute position. - * - * @param absolutePosition The position to which {@link #readAllocationNode} should be advanced. - */ - private void advanceReadTo(long absolutePosition) { - while (absolutePosition >= readAllocationNode.endPosition) { - readAllocationNode = readAllocationNode.next; - } + /** Discards up to but not including the read position. */ + public final void discardToRead() { + sampleDataQueue.discardDownstreamTo(discardSampleMetadataToRead()); } - /** - * Advances {@link #firstAllocationNode} to the specified absolute position. - * {@link #readAllocationNode} is also advanced if necessary to avoid it falling behind - * {@link #firstAllocationNode}. Nodes that have been advanced past are cleared, and their - * underlying allocations are returned to the allocator. - * - * @param absolutePosition The position to which {@link #firstAllocationNode} should be advanced. - * May be {@link C#POSITION_UNSET}, in which case calling this method is a no-op. - */ - private void discardDownstreamTo(long absolutePosition) { - if (absolutePosition == C.POSITION_UNSET) { - return; - } - while (absolutePosition >= firstAllocationNode.endPosition) { - allocator.release(firstAllocationNode.allocation); - firstAllocationNode = firstAllocationNode.clear(); - } - // If we discarded the node referenced by readAllocationNode then we need to advance it to the - // first remaining node. - if (readAllocationNode.startPosition < firstAllocationNode.startPosition) { - readAllocationNode = firstAllocationNode; - } + /** Discards all samples in the queue and advances the read position. */ + public final void discardToEnd() { + sampleDataQueue.discardDownstreamTo(discardSampleMetadataToEnd()); } // Called by the loading thread. + /** + * Sets an offset that will be added to the timestamps (and sub-sample timestamps) of samples that + * are subsequently queued. + * + * @param sampleOffsetUs The timestamp offset in microseconds. + */ + public final void setSampleOffsetUs(long sampleOffsetUs) { + if (this.sampleOffsetUs != sampleOffsetUs) { + this.sampleOffsetUs = sampleOffsetUs; + invalidateUpstreamFormatAdjustment(); + } + } + /** * Sets a listener to be notified of changes to the upstream format. * * @param listener The listener. */ - public void setUpstreamFormatChangeListener(UpstreamFormatChangedListener listener) { + public final void setUpstreamFormatChangeListener( + @Nullable UpstreamFormatChangedListener listener) { upstreamFormatChangeListener = listener; } - /** - * Sets an offset that will be added to the timestamps (and sub-sample timestamps) of samples - * that are subsequently queued. - * - * @param sampleOffsetUs The timestamp offset in microseconds. - */ - public void setSampleOffsetUs(long sampleOffsetUs) { - if (this.sampleOffsetUs != sampleOffsetUs) { - this.sampleOffsetUs = sampleOffsetUs; - pendingFormatAdjustment = true; + // TrackOutput implementation. Called by the loading thread. + + @Override + public final void format(Format unadjustedUpstreamFormat) { + Format adjustedUpstreamFormat = getAdjustedUpstreamFormat(unadjustedUpstreamFormat); + upstreamFormatAdjustmentRequired = false; + this.unadjustedUpstreamFormat = unadjustedUpstreamFormat; + boolean upstreamFormatChanged = setUpstreamFormat(adjustedUpstreamFormat); + if (upstreamFormatChangeListener != null && upstreamFormatChanged) { + upstreamFormatChangeListener.onUpstreamFormatChanged(adjustedUpstreamFormat); } } @Override - public void format(Format format) { - Format adjustedFormat = getAdjustedSampleFormat(format, sampleOffsetUs); - boolean formatChanged = metadataQueue.format(adjustedFormat); - lastUnadjustedFormat = format; - pendingFormatAdjustment = false; - if (upstreamFormatChangeListener != null && formatChanged) { - upstreamFormatChangeListener.onUpstreamFormatChanged(adjustedFormat); - } + public final int sampleData( + DataReader input, int length, boolean allowEndOfInput, @SampleDataPart int sampleDataPart) + throws IOException { + return sampleDataQueue.sampleData(input, length, allowEndOfInput); } @Override - public int sampleData(ExtractorInput input, int length, boolean allowEndOfInput) - throws IOException, InterruptedException { - length = preAppend(length); - int bytesAppended = input.read(writeAllocationNode.allocation.data, - writeAllocationNode.translateOffset(totalBytesWritten), length); - if (bytesAppended == C.RESULT_END_OF_INPUT) { - if (allowEndOfInput) { - return C.RESULT_END_OF_INPUT; - } - throw new EOFException(); - } - postAppend(bytesAppended); - return bytesAppended; - } - - @Override - public void sampleData(ParsableByteArray buffer, int length) { - while (length > 0) { - int bytesAppended = preAppend(length); - buffer.readBytes(writeAllocationNode.allocation.data, - writeAllocationNode.translateOffset(totalBytesWritten), bytesAppended); - length -= bytesAppended; - postAppend(bytesAppended); - } + public final void sampleData( + ParsableByteArray buffer, int length, @SampleDataPart int sampleDataPart) { + sampleDataQueue.sampleData(buffer, length); } @Override @@ -651,160 +500,433 @@ public class SampleQueue implements TrackOutput { int size, int offset, @Nullable CryptoData cryptoData) { - if (pendingFormatAdjustment) { - format(lastUnadjustedFormat); + if (upstreamFormatAdjustmentRequired) { + format(Assertions.checkStateNotNull(unadjustedUpstreamFormat)); } timeUs += sampleOffsetUs; if (pendingSplice) { - if ((flags & C.BUFFER_FLAG_KEY_FRAME) == 0 || !metadataQueue.attemptSplice(timeUs)) { + if ((flags & C.BUFFER_FLAG_KEY_FRAME) == 0 || !attemptSplice(timeUs)) { return; } pendingSplice = false; } - long absoluteOffset = totalBytesWritten - size - offset; - metadataQueue.commitSample(timeUs, flags, absoluteOffset, size, cryptoData); + long absoluteOffset = sampleDataQueue.getTotalBytesWritten() - size - offset; + commitSample(timeUs, flags, absoluteOffset, size, cryptoData); } - // Private methods. - /** - * Clears allocation nodes starting from {@code fromNode}. - * - * @param fromNode The node from which to clear. + * Invalidates the last upstream format adjustment. {@link #getAdjustedUpstreamFormat(Format)} + * will be called to adjust the upstream {@link Format} again before the next sample is queued. */ - private void clearAllocationNodes(AllocationNode fromNode) { - if (!fromNode.wasInitialized) { - return; - } - // Bulk release allocations for performance (it's significantly faster when using - // DefaultAllocator because the allocator's lock only needs to be acquired and released once) - // [Internal: See b/29542039]. - int allocationCount = (writeAllocationNode.wasInitialized ? 1 : 0) - + ((int) (writeAllocationNode.startPosition - fromNode.startPosition) / allocationLength); - Allocation[] allocationsToRelease = new Allocation[allocationCount]; - AllocationNode currentNode = fromNode; - for (int i = 0; i < allocationsToRelease.length; i++) { - allocationsToRelease[i] = currentNode.allocation; - currentNode = currentNode.clear(); - } - allocator.release(allocationsToRelease); + protected final void invalidateUpstreamFormatAdjustment() { + upstreamFormatAdjustmentRequired = true; } /** - * Called before writing sample data to {@link #writeAllocationNode}. May cause - * {@link #writeAllocationNode} to be initialized. + * Adjusts the upstream {@link Format} (i.e., the {@link Format} that was most recently passed to + * {@link #format(Format)}). * - * @param length The number of bytes that the caller wishes to write. - * @return The number of bytes that the caller is permitted to write, which may be less than - * {@code length}. - */ - private int preAppend(int length) { - if (!writeAllocationNode.wasInitialized) { - writeAllocationNode.initialize(allocator.allocate(), - new AllocationNode(writeAllocationNode.endPosition, allocationLength)); - } - return Math.min(length, (int) (writeAllocationNode.endPosition - totalBytesWritten)); - } - - /** - * Called after writing sample data. May cause {@link #writeAllocationNode} to be advanced. - * - * @param length The number of bytes that were written. - */ - private void postAppend(int length) { - totalBytesWritten += length; - if (totalBytesWritten == writeAllocationNode.endPosition) { - writeAllocationNode = writeAllocationNode.next; - } - } - - /** - * Adjusts a {@link Format} to incorporate a sample offset into {@link Format#subsampleOffsetUs}. + *

      The default implementation incorporates the sample offset passed to {@link + * #setSampleOffsetUs(long)} into {@link Format#subsampleOffsetUs}. * * @param format The {@link Format} to adjust. - * @param sampleOffsetUs The offset to apply. * @return The adjusted {@link Format}. */ - private static Format getAdjustedSampleFormat(Format format, long sampleOffsetUs) { - if (format == null) { - return null; - } + @CallSuper + protected Format getAdjustedUpstreamFormat(Format format) { if (sampleOffsetUs != 0 && format.subsampleOffsetUs != Format.OFFSET_SAMPLE_RELATIVE) { - format = format.copyWithSubsampleOffsetUs(format.subsampleOffsetUs + sampleOffsetUs); + format = + format + .buildUpon() + .setSubsampleOffsetUs(format.subsampleOffsetUs + sampleOffsetUs) + .build(); } return format; } - /** A node in a linked list of {@link Allocation}s held by the output. */ - private static final class AllocationNode { - - /** - * The absolute position of the start of the data (inclusive). - */ - public final long startPosition; - /** - * The absolute position of the end of the data (exclusive). - */ - public final long endPosition; - /** - * Whether the node has been initialized. Remains true after {@link #clear()}. - */ - public boolean wasInitialized; - /** - * The {@link Allocation}, or {@code null} if the node is not initialized. - */ - @Nullable public Allocation allocation; - /** - * The next {@link AllocationNode} in the list, or {@code null} if the node has not been - * initialized. Remains set after {@link #clear()}. - */ - @Nullable public AllocationNode next; - - /** - * @param startPosition See {@link #startPosition}. - * @param allocationLength The length of the {@link Allocation} with which this node will be - * initialized. - */ - public AllocationNode(long startPosition, int allocationLength) { - this.startPosition = startPosition; - this.endPosition = startPosition + allocationLength; - } - - /** - * Initializes the node. - * - * @param allocation The node's {@link Allocation}. - * @param next The next {@link AllocationNode}. - */ - public void initialize(Allocation allocation, AllocationNode next) { - this.allocation = allocation; - this.next = next; - wasInitialized = true; - } - - /** - * Gets the offset into the {@link #allocation}'s {@link Allocation#data} that corresponds to - * the specified absolute position. - * - * @param absolutePosition The absolute position. - * @return The corresponding offset into the allocation's data. - */ - public int translateOffset(long absolutePosition) { - return (int) (absolutePosition - startPosition) + allocation.offset; - } - - /** - * Clears {@link #allocation} and {@link #next}. - * - * @return The cleared next {@link AllocationNode}. - */ - public AllocationNode clear() { - allocation = null; - AllocationNode temp = next; - next = null; - return temp; - } + // Internal methods. + /** Rewinds the read position to the first sample in the queue. */ + private synchronized void rewind() { + readPosition = 0; + sampleDataQueue.rewind(); } + @SuppressWarnings("ReferenceEquality") // See comments in setUpstreamFormat + private synchronized int readSampleMetadata( + FormatHolder formatHolder, + DecoderInputBuffer buffer, + boolean formatRequired, + boolean loadingFinished, + long decodeOnlyUntilUs, + SampleExtrasHolder extrasHolder) { + buffer.waitingForKeys = false; + // This is a temporary fix for https://github.com/google/ExoPlayer/issues/6155. + // TODO: Remove it and replace it with a fix that discards samples when writing to the queue. + boolean hasNextSample; + int relativeReadIndex = C.INDEX_UNSET; + while ((hasNextSample = hasNextSample())) { + relativeReadIndex = getRelativeIndex(readPosition); + long timeUs = timesUs[relativeReadIndex]; + if (timeUs < decodeOnlyUntilUs + && MimeTypes.allSamplesAreSyncSamples(formats[relativeReadIndex].sampleMimeType)) { + readPosition++; + } else { + break; + } + } + + if (!hasNextSample) { + if (loadingFinished || isLastSampleQueued) { + buffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM); + return C.RESULT_BUFFER_READ; + } else if (upstreamFormat != null && (formatRequired || upstreamFormat != downstreamFormat)) { + onFormatResult(Assertions.checkNotNull(upstreamFormat), formatHolder); + return C.RESULT_FORMAT_READ; + } else { + return C.RESULT_NOTHING_READ; + } + } + + if (formatRequired || formats[relativeReadIndex] != downstreamFormat) { + onFormatResult(formats[relativeReadIndex], formatHolder); + return C.RESULT_FORMAT_READ; + } + + if (!mayReadSample(relativeReadIndex)) { + buffer.waitingForKeys = true; + return C.RESULT_NOTHING_READ; + } + + buffer.setFlags(flags[relativeReadIndex]); + buffer.timeUs = timesUs[relativeReadIndex]; + if (buffer.timeUs < decodeOnlyUntilUs) { + buffer.addFlag(C.BUFFER_FLAG_DECODE_ONLY); + } + if (buffer.isFlagsOnly()) { + return C.RESULT_BUFFER_READ; + } + extrasHolder.size = sizes[relativeReadIndex]; + extrasHolder.offset = offsets[relativeReadIndex]; + extrasHolder.cryptoData = cryptoDatas[relativeReadIndex]; + + readPosition++; + return C.RESULT_BUFFER_READ; + } + + private synchronized boolean setUpstreamFormat(Format format) { + upstreamFormatRequired = false; + if (Util.areEqual(format, upstreamFormat)) { + // The format is unchanged. If format and upstreamFormat are different objects, we keep the + // current upstreamFormat so we can detect format changes on the read side using cheap + // referential quality. + return false; + } else if (Util.areEqual(format, upstreamCommittedFormat)) { + // The format has changed back to the format of the last committed sample. If they are + // different objects, we revert back to using upstreamCommittedFormat as the upstreamFormat + // so we can detect format changes on the read side using cheap referential equality. + upstreamFormat = upstreamCommittedFormat; + return true; + } else { + upstreamFormat = format; + return true; + } + } + + private synchronized long discardSampleMetadataTo( + long timeUs, boolean toKeyframe, boolean stopAtReadPosition) { + if (length == 0 || timeUs < timesUs[relativeFirstIndex]) { + return C.POSITION_UNSET; + } + int searchLength = stopAtReadPosition && readPosition != length ? readPosition + 1 : length; + int discardCount = findSampleBefore(relativeFirstIndex, searchLength, timeUs, toKeyframe); + if (discardCount == -1) { + return C.POSITION_UNSET; + } + return discardSamples(discardCount); + } + + public synchronized long discardSampleMetadataToRead() { + if (readPosition == 0) { + return C.POSITION_UNSET; + } + return discardSamples(readPosition); + } + + private synchronized long discardSampleMetadataToEnd() { + if (length == 0) { + return C.POSITION_UNSET; + } + return discardSamples(length); + } + + private void releaseDrmSessionReferences() { + if (currentDrmSession != null) { + currentDrmSession.release(eventDispatcher); + currentDrmSession = null; + // Clear downstream format to avoid violating the assumption that downstreamFormat.drmInitData + // != null implies currentSession != null + downstreamFormat = null; + } + } + + private synchronized void commitSample( + long timeUs, + @C.BufferFlags int sampleFlags, + long offset, + int size, + @Nullable CryptoData cryptoData) { + if (upstreamKeyframeRequired) { + if ((sampleFlags & C.BUFFER_FLAG_KEY_FRAME) == 0) { + return; + } + upstreamKeyframeRequired = false; + } + Assertions.checkState(!upstreamFormatRequired); + + isLastSampleQueued = (sampleFlags & C.BUFFER_FLAG_LAST_SAMPLE) != 0; + largestQueuedTimestampUs = Math.max(largestQueuedTimestampUs, timeUs); + + int relativeEndIndex = getRelativeIndex(length); + timesUs[relativeEndIndex] = timeUs; + offsets[relativeEndIndex] = offset; + sizes[relativeEndIndex] = size; + flags[relativeEndIndex] = sampleFlags; + cryptoDatas[relativeEndIndex] = cryptoData; + formats[relativeEndIndex] = upstreamFormat; + sourceIds[relativeEndIndex] = upstreamSourceId; + upstreamCommittedFormat = upstreamFormat; + + length++; + if (length == capacity) { + // Increase the capacity. + int newCapacity = capacity + SAMPLE_CAPACITY_INCREMENT; + int[] newSourceIds = new int[newCapacity]; + long[] newOffsets = new long[newCapacity]; + long[] newTimesUs = new long[newCapacity]; + int[] newFlags = new int[newCapacity]; + int[] newSizes = new int[newCapacity]; + CryptoData[] newCryptoDatas = new CryptoData[newCapacity]; + Format[] newFormats = new Format[newCapacity]; + int beforeWrap = capacity - relativeFirstIndex; + System.arraycopy(offsets, relativeFirstIndex, newOffsets, 0, beforeWrap); + System.arraycopy(timesUs, relativeFirstIndex, newTimesUs, 0, beforeWrap); + System.arraycopy(flags, relativeFirstIndex, newFlags, 0, beforeWrap); + System.arraycopy(sizes, relativeFirstIndex, newSizes, 0, beforeWrap); + System.arraycopy(cryptoDatas, relativeFirstIndex, newCryptoDatas, 0, beforeWrap); + System.arraycopy(formats, relativeFirstIndex, newFormats, 0, beforeWrap); + System.arraycopy(sourceIds, relativeFirstIndex, newSourceIds, 0, beforeWrap); + int afterWrap = relativeFirstIndex; + System.arraycopy(offsets, 0, newOffsets, beforeWrap, afterWrap); + System.arraycopy(timesUs, 0, newTimesUs, beforeWrap, afterWrap); + System.arraycopy(flags, 0, newFlags, beforeWrap, afterWrap); + System.arraycopy(sizes, 0, newSizes, beforeWrap, afterWrap); + System.arraycopy(cryptoDatas, 0, newCryptoDatas, beforeWrap, afterWrap); + System.arraycopy(formats, 0, newFormats, beforeWrap, afterWrap); + System.arraycopy(sourceIds, 0, newSourceIds, beforeWrap, afterWrap); + offsets = newOffsets; + timesUs = newTimesUs; + flags = newFlags; + sizes = newSizes; + cryptoDatas = newCryptoDatas; + formats = newFormats; + sourceIds = newSourceIds; + relativeFirstIndex = 0; + capacity = newCapacity; + } + } + + /** + * Attempts to discard samples from the end of the queue to allow samples starting from the + * specified timestamp to be spliced in. Samples will not be discarded prior to the read position. + * + * @param timeUs The timestamp at which the splice occurs. + * @return Whether the splice was successful. + */ + private synchronized boolean attemptSplice(long timeUs) { + if (length == 0) { + return timeUs > largestDiscardedTimestampUs; + } + long largestReadTimestampUs = + Math.max(largestDiscardedTimestampUs, getLargestTimestamp(readPosition)); + if (largestReadTimestampUs >= timeUs) { + return false; + } + int retainCount = length; + int relativeSampleIndex = getRelativeIndex(length - 1); + while (retainCount > readPosition && timesUs[relativeSampleIndex] >= timeUs) { + retainCount--; + relativeSampleIndex--; + if (relativeSampleIndex == -1) { + relativeSampleIndex = capacity - 1; + } + } + discardUpstreamSampleMetadata(absoluteFirstIndex + retainCount); + return true; + } + + private long discardUpstreamSampleMetadata(int discardFromIndex) { + int discardCount = getWriteIndex() - discardFromIndex; + Assertions.checkArgument(0 <= discardCount && discardCount <= (length - readPosition)); + length -= discardCount; + largestQueuedTimestampUs = Math.max(largestDiscardedTimestampUs, getLargestTimestamp(length)); + isLastSampleQueued = discardCount == 0 && isLastSampleQueued; + if (length != 0) { + int relativeLastWriteIndex = getRelativeIndex(length - 1); + return offsets[relativeLastWriteIndex] + sizes[relativeLastWriteIndex]; + } + return 0; + } + + private boolean hasNextSample() { + return readPosition != length; + } + + /** + * Sets the downstream format, performs DRM resource management, and populates the {@code + * outputFormatHolder}. + * + * @param newFormat The new downstream format. + * @param outputFormatHolder The output {@link FormatHolder}. + */ + private void onFormatResult(Format newFormat, FormatHolder outputFormatHolder) { + outputFormatHolder.format = newFormat; + boolean isFirstFormat = downstreamFormat == null; + @Nullable DrmInitData oldDrmInitData = isFirstFormat ? null : downstreamFormat.drmInitData; + downstreamFormat = newFormat; + @Nullable DrmInitData newDrmInitData = newFormat.drmInitData; + outputFormatHolder.drmSession = currentDrmSession; + if (!isFirstFormat && Util.areEqual(oldDrmInitData, newDrmInitData)) { + // Nothing to do. + return; + } + // Ensure we acquire the new session before releasing the previous one in case the same session + // is being used for both DrmInitData. + @Nullable DrmSession previousSession = currentDrmSession; + currentDrmSession = + newDrmInitData != null + ? drmSessionManager.acquireSession(playbackLooper, eventDispatcher, newDrmInitData) + : drmSessionManager.acquirePlaceholderSession( + playbackLooper, MimeTypes.getTrackType(newFormat.sampleMimeType)); + outputFormatHolder.drmSession = currentDrmSession; + + if (previousSession != null) { + previousSession.release(eventDispatcher); + } + } + + /** + * Returns whether it's possible to read the next sample. + * + * @param relativeReadIndex The relative read index of the next sample. + * @return Whether it's possible to read the next sample. + */ + private boolean mayReadSample(int relativeReadIndex) { + return currentDrmSession == null + || currentDrmSession.getState() == DrmSession.STATE_OPENED_WITH_KEYS + || ((flags[relativeReadIndex] & C.BUFFER_FLAG_ENCRYPTED) == 0 + && currentDrmSession.playClearSamplesWithoutKeys()); + } + + /** + * Finds the sample in the specified range that's before or at the specified time. If {@code + * keyframe} is {@code true} then the sample is additionally required to be a keyframe. + * + * @param relativeStartIndex The relative index from which to start searching. + * @param length The length of the range being searched. + * @param timeUs The specified time. + * @param keyframe Whether only keyframes should be considered. + * @return The offset from {@code relativeFirstIndex} to the found sample, or -1 if no matching + * sample was found. + */ + private int findSampleBefore(int relativeStartIndex, int length, long timeUs, boolean keyframe) { + // This could be optimized to use a binary search, however in practice callers to this method + // normally pass times near to the start of the search region. Hence it's unclear whether + // switching to a binary search would yield any real benefit. + int sampleCountToTarget = -1; + int searchIndex = relativeStartIndex; + for (int i = 0; i < length && timesUs[searchIndex] <= timeUs; i++) { + if (!keyframe || (flags[searchIndex] & C.BUFFER_FLAG_KEY_FRAME) != 0) { + // We've found a suitable sample. + sampleCountToTarget = i; + } + searchIndex++; + if (searchIndex == capacity) { + searchIndex = 0; + } + } + return sampleCountToTarget; + } + + /** + * Discards the specified number of samples. + * + * @param discardCount The number of samples to discard. + * @return The corresponding offset up to which data should be discarded. + */ + private long discardSamples(int discardCount) { + largestDiscardedTimestampUs = + Math.max(largestDiscardedTimestampUs, getLargestTimestamp(discardCount)); + length -= discardCount; + absoluteFirstIndex += discardCount; + relativeFirstIndex += discardCount; + if (relativeFirstIndex >= capacity) { + relativeFirstIndex -= capacity; + } + readPosition -= discardCount; + if (readPosition < 0) { + readPosition = 0; + } + if (length == 0) { + int relativeLastDiscardIndex = (relativeFirstIndex == 0 ? capacity : relativeFirstIndex) - 1; + return offsets[relativeLastDiscardIndex] + sizes[relativeLastDiscardIndex]; + } else { + return offsets[relativeFirstIndex]; + } + } + + /** + * Finds the largest timestamp of any sample from the start of the queue up to the specified + * length, assuming that the timestamps prior to a keyframe are always less than the timestamp of + * the keyframe itself, and of subsequent frames. + * + * @param length The length of the range being searched. + * @return The largest timestamp, or {@link Long#MIN_VALUE} if {@code length == 0}. + */ + private long getLargestTimestamp(int length) { + if (length == 0) { + return Long.MIN_VALUE; + } + long largestTimestampUs = Long.MIN_VALUE; + int relativeSampleIndex = getRelativeIndex(length - 1); + for (int i = 0; i < length; i++) { + largestTimestampUs = Math.max(largestTimestampUs, timesUs[relativeSampleIndex]); + if ((flags[relativeSampleIndex] & C.BUFFER_FLAG_KEY_FRAME) != 0) { + break; + } + relativeSampleIndex--; + if (relativeSampleIndex == -1) { + relativeSampleIndex = capacity - 1; + } + } + return largestTimestampUs; + } + + /** + * Returns the relative index for a given offset from the start of the queue. + * + * @param offset The offset, which must be in the range [0, length]. + */ + private int getRelativeIndex(int offset) { + int relativeIndex = relativeFirstIndex + offset; + return relativeIndex < capacity ? relativeIndex : relativeIndex - capacity; + } + + /** A holder for sample metadata not held by {@link DecoderInputBuffer}. */ + /* package */ static final class SampleExtrasHolder { + + public int size; + public long offset; + @Nullable public CryptoData cryptoData; + } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SampleStream.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SampleStream.java index 54293aa4c1..0c5e5045ef 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/SampleStream.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SampleStream.java @@ -15,16 +15,26 @@ */ package com.google.android.exoplayer2.source; +import androidx.annotation.IntDef; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.FormatHolder; import com.google.android.exoplayer2.decoder.DecoderInputBuffer; import java.io.IOException; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; /** * A stream of media samples (and associated format information). */ public interface SampleStream { + /** Return values of {@link #readData(FormatHolder, DecoderInputBuffer, boolean)}. */ + @Documented + @Retention(RetentionPolicy.SOURCE) + @IntDef({C.RESULT_NOTHING_READ, C.RESULT_FORMAT_READ, C.RESULT_BUFFER_READ}) + @interface ReadDataResult {} + /** * Returns whether data is available to be read. *

      @@ -62,9 +72,9 @@ public interface SampleStream { * @param formatRequired Whether the caller requires that the format of the stream be read even if * it's not changing. A sample will never be read if set to true, however it is still possible * for the end of stream or nothing to be read. - * @return The result, which can be {@link C#RESULT_NOTHING_READ}, {@link C#RESULT_FORMAT_READ} or - * {@link C#RESULT_BUFFER_READ}. + * @return The status of read, one of {@link ReadDataResult}. */ + @ReadDataResult int readData(FormatHolder formatHolder, DecoderInputBuffer buffer, boolean formatRequired); /** diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SilenceMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SilenceMediaSource.java index abaf33633e..f4fb376248 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/SilenceMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SilenceMediaSource.java @@ -33,27 +33,57 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; /** Media source with a single period consisting of silent raw audio of a given duration. */ public final class SilenceMediaSource extends BaseMediaSource { + /** Factory for {@link SilenceMediaSource SilenceMediaSources}. */ + public static final class Factory { + + private long durationUs; + @Nullable private Object tag; + + /** + * Sets the duration of the silent audio. + * + * @param durationUs The duration of silent audio to output, in microseconds. + * @return This factory, for convenience. + */ + public Factory setDurationUs(long durationUs) { + this.durationUs = durationUs; + return this; + } + + /** + * Sets a tag for the media source which will be published in the {@link + * com.google.android.exoplayer2.Timeline} of the source as {@link + * com.google.android.exoplayer2.Timeline.Window#tag}. + * + * @param tag A tag for the media source. + * @return This factory, for convenience. + */ + public Factory setTag(@Nullable Object tag) { + this.tag = tag; + return this; + } + + /** Creates a new {@link SilenceMediaSource}. */ + public SilenceMediaSource createMediaSource() { + return new SilenceMediaSource(durationUs, tag); + } + } + private static final int SAMPLE_RATE_HZ = 44100; - @C.PcmEncoding private static final int ENCODING = C.ENCODING_PCM_16BIT; + @C.PcmEncoding private static final int PCM_ENCODING = C.ENCODING_PCM_16BIT; private static final int CHANNEL_COUNT = 2; private static final Format FORMAT = - Format.createAudioSampleFormat( - /* id=*/ null, - MimeTypes.AUDIO_RAW, - /* codecs= */ null, - /* bitrate= */ Format.NO_VALUE, - /* maxInputSize= */ Format.NO_VALUE, - CHANNEL_COUNT, - SAMPLE_RATE_HZ, - ENCODING, - /* initializationData= */ null, - /* drmInitData= */ null, - /* selectionFlags= */ 0, - /* language= */ null); + new Format.Builder() + .setSampleMimeType(MimeTypes.AUDIO_RAW) + .setChannelCount(CHANNEL_COUNT) + .setSampleRate(SAMPLE_RATE_HZ) + .setPcmEncoding(PCM_ENCODING) + .build(); private static final byte[] SILENCE_SAMPLE = - new byte[Util.getPcmFrameSize(ENCODING, CHANNEL_COUNT) * 1024]; + new byte[Util.getPcmFrameSize(PCM_ENCODING, CHANNEL_COUNT) * 1024]; private final long durationUs; + @Nullable private final Object tag; /** * Creates a new media source providing silent audio of the given duration. @@ -61,15 +91,25 @@ public final class SilenceMediaSource extends BaseMediaSource { * @param durationUs The duration of silent audio to output, in microseconds. */ public SilenceMediaSource(long durationUs) { + this(durationUs, /* tag= */ null); + } + + private SilenceMediaSource(long durationUs, @Nullable Object tag) { Assertions.checkArgument(durationUs >= 0); this.durationUs = durationUs; + this.tag = tag; } @Override protected void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) { refreshSourceInfo( new SinglePeriodTimeline( - durationUs, /* isSeekable= */ true, /* isDynamic= */ false, /* isLive= */ false)); + durationUs, + /* isSeekable= */ true, + /* isDynamic= */ false, + /* isLive= */ false, + /* manifest= */ null, + tag)); } @Override @@ -243,11 +283,11 @@ public final class SilenceMediaSource extends BaseMediaSource { private static long getAudioByteCount(long durationUs) { long audioSampleCount = durationUs * SAMPLE_RATE_HZ / C.MICROS_PER_SECOND; - return Util.getPcmFrameSize(ENCODING, CHANNEL_COUNT) * audioSampleCount; + return Util.getPcmFrameSize(PCM_ENCODING, CHANNEL_COUNT) * audioSampleCount; } private static long getAudioPositionUs(long bytes) { - long audioSampleCount = bytes / Util.getPcmFrameSize(ENCODING, CHANNEL_COUNT); + long audioSampleCount = bytes / Util.getPcmFrameSize(PCM_ENCODING, CHANNEL_COUNT); return audioSampleCount * C.MICROS_PER_SECOND / SAMPLE_RATE_HZ; } } 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 45f64cacf2..5b47398dd5 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 @@ -29,6 +29,7 @@ public final class SinglePeriodTimeline extends Timeline { private final long presentationStartTimeMs; private final long windowStartTimeMs; + private final long elapsedRealtimeEpochOffsetMs; private final long periodDurationUs; private final long windowDurationUs; private final long windowPositionInPeriodUs; @@ -110,6 +111,7 @@ public final class SinglePeriodTimeline extends Timeline { this( /* presentationStartTimeMs= */ C.TIME_UNSET, /* windowStartTimeMs= */ C.TIME_UNSET, + /* elapsedRealtimeEpochOffsetMs= */ C.TIME_UNSET, periodDurationUs, windowDurationUs, windowPositionInPeriodUs, @@ -126,8 +128,12 @@ public final class SinglePeriodTimeline extends Timeline { * position in the period. * * @param presentationStartTimeMs The start time of the presentation in milliseconds since the - * epoch. - * @param windowStartTimeMs The window's start time in milliseconds since the epoch. + * epoch, or {@link C#TIME_UNSET} if unknown or not applicable. + * @param windowStartTimeMs The window's start time in milliseconds since the epoch, or {@link + * C#TIME_UNSET} if unknown or not applicable. + * @param elapsedRealtimeEpochOffsetMs The offset between {@link + * android.os.SystemClock#elapsedRealtime()} and the time since the Unix epoch according to + * the clock of the media origin server, or {@link C#TIME_UNSET} if unknown or not applicable. * @param periodDurationUs The duration of the period in microseconds. * @param windowDurationUs The duration of the window in microseconds. * @param windowPositionInPeriodUs The position of the start of the window in the period, in @@ -143,6 +149,7 @@ public final class SinglePeriodTimeline extends Timeline { public SinglePeriodTimeline( long presentationStartTimeMs, long windowStartTimeMs, + long elapsedRealtimeEpochOffsetMs, long periodDurationUs, long windowDurationUs, long windowPositionInPeriodUs, @@ -154,6 +161,7 @@ public final class SinglePeriodTimeline extends Timeline { @Nullable Object tag) { this.presentationStartTimeMs = presentationStartTimeMs; this.windowStartTimeMs = windowStartTimeMs; + this.elapsedRealtimeEpochOffsetMs = elapsedRealtimeEpochOffsetMs; this.periodDurationUs = periodDurationUs; this.windowDurationUs = windowDurationUs; this.windowPositionInPeriodUs = windowPositionInPeriodUs; @@ -192,13 +200,14 @@ public final class SinglePeriodTimeline extends Timeline { manifest, presentationStartTimeMs, windowStartTimeMs, + elapsedRealtimeEpochOffsetMs, isSeekable, isDynamic, isLive, windowDefaultStartPositionUs, windowDurationUs, - 0, - 0, + /* firstPeriodIndex= */ 0, + /* lastPeriodIndex= */ 0, windowPositionInPeriodUs); } 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 ca50c342b5..34d1bbd86c 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 @@ -26,6 +26,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.LoadErrorHandlingPolicy; +import com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy.LoadErrorInfo; import com.google.android.exoplayer2.upstream.Loader; import com.google.android.exoplayer2.upstream.Loader.LoadErrorAction; import com.google.android.exoplayer2.upstream.Loader.Loadable; @@ -104,7 +105,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; } @Override - public void maybeThrowPrepareError() throws IOException { + public void maybeThrowPrepareError() { // Do nothing. } @@ -154,21 +155,21 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; if (transferListener != null) { dataSource.addTransferListener(transferListener); } + SourceLoadable loadable = new SourceLoadable(dataSpec, dataSource); long elapsedRealtimeMs = loader.startLoading( - new SourceLoadable(dataSpec, dataSource), + loadable, /* callback= */ this, loadErrorHandlingPolicy.getMinimumLoadableRetryCount(C.DATA_TYPE_MEDIA)); eventDispatcher.loadStarted( - dataSpec, + new LoadEventInfo(loadable.loadTaskId, dataSpec, elapsedRealtimeMs), C.DATA_TYPE_MEDIA, C.TRACK_TYPE_UNKNOWN, format, C.SELECTION_REASON_UNKNOWN, /* trackSelectionData= */ null, /* mediaStartTimeUs= */ 0, - durationUs, - elapsedRealtimeMs); + durationUs); return true; } @@ -212,44 +213,56 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; // Loader.Callback implementation. @Override - public void onLoadCompleted(SourceLoadable loadable, long elapsedRealtimeMs, - long loadDurationMs) { + public void onLoadCompleted( + SourceLoadable loadable, long elapsedRealtimeMs, long loadDurationMs) { sampleSize = (int) loadable.dataSource.getBytesRead(); sampleData = Assertions.checkNotNull(loadable.sampleData); loadingFinished = true; + StatsDataSource dataSource = loadable.dataSource; + LoadEventInfo loadEventInfo = + new LoadEventInfo( + loadable.loadTaskId, + loadable.dataSpec, + dataSource.getLastOpenedUri(), + dataSource.getLastResponseHeaders(), + elapsedRealtimeMs, + loadDurationMs, + sampleSize); + loadErrorHandlingPolicy.onLoadTaskConcluded(loadable.loadTaskId); eventDispatcher.loadCompleted( - loadable.dataSpec, - loadable.dataSource.getLastOpenedUri(), - loadable.dataSource.getLastResponseHeaders(), + loadEventInfo, C.DATA_TYPE_MEDIA, C.TRACK_TYPE_UNKNOWN, format, C.SELECTION_REASON_UNKNOWN, /* trackSelectionData= */ null, /* mediaStartTimeUs= */ 0, - durationUs, - elapsedRealtimeMs, - loadDurationMs, - sampleSize); + durationUs); } @Override - public void onLoadCanceled(SourceLoadable loadable, long elapsedRealtimeMs, long loadDurationMs, - boolean released) { + public void onLoadCanceled( + SourceLoadable loadable, long elapsedRealtimeMs, long loadDurationMs, boolean released) { + StatsDataSource dataSource = loadable.dataSource; + LoadEventInfo loadEventInfo = + new LoadEventInfo( + loadable.loadTaskId, + loadable.dataSpec, + dataSource.getLastOpenedUri(), + dataSource.getLastResponseHeaders(), + elapsedRealtimeMs, + loadDurationMs, + dataSource.getBytesRead()); + loadErrorHandlingPolicy.onLoadTaskConcluded(loadable.loadTaskId); eventDispatcher.loadCanceled( - loadable.dataSpec, - loadable.dataSource.getLastOpenedUri(), - loadable.dataSource.getLastResponseHeaders(), + loadEventInfo, C.DATA_TYPE_MEDIA, C.TRACK_TYPE_UNKNOWN, /* trackFormat= */ null, C.SELECTION_REASON_UNKNOWN, /* trackSelectionData= */ null, /* mediaStartTimeUs= */ 0, - durationUs, - elapsedRealtimeMs, - loadDurationMs, - loadable.dataSource.getBytesRead()); + durationUs); } @Override @@ -259,9 +272,28 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; long loadDurationMs, IOException error, int errorCount) { + StatsDataSource dataSource = loadable.dataSource; + LoadEventInfo loadEventInfo = + new LoadEventInfo( + loadable.loadTaskId, + loadable.dataSpec, + dataSource.getLastOpenedUri(), + dataSource.getLastResponseHeaders(), + elapsedRealtimeMs, + loadDurationMs, + dataSource.getBytesRead()); + MediaLoadData mediaLoadData = + new MediaLoadData( + C.DATA_TYPE_MEDIA, + C.TRACK_TYPE_UNKNOWN, + format, + C.SELECTION_REASON_UNKNOWN, + /* trackSelectionData= */ null, + /* mediaStartTimeMs= */ 0, + C.usToMs(durationUs)); long retryDelay = loadErrorHandlingPolicy.getRetryDelayMsFor( - C.DATA_TYPE_MEDIA, loadDurationMs, error, errorCount); + new LoadErrorInfo(loadEventInfo, mediaLoadData, error, errorCount)); boolean errorCanBePropagated = retryDelay == C.TIME_UNSET || errorCount @@ -277,10 +309,9 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; ? Loader.createRetryAction(/* resetErrorCount= */ false, retryDelay) : Loader.DONT_RETRY_FATAL; } + boolean wasCanceled = !action.isRetry(); eventDispatcher.loadError( - loadable.dataSpec, - loadable.dataSource.getLastOpenedUri(), - loadable.dataSource.getLastResponseHeaders(), + loadEventInfo, C.DATA_TYPE_MEDIA, C.TRACK_TYPE_UNKNOWN, format, @@ -288,11 +319,11 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /* trackSelectionData= */ null, /* mediaStartTimeUs= */ 0, durationUs, - elapsedRealtimeMs, - loadDurationMs, - loadable.dataSource.getBytesRead(), error, - /* wasCanceled= */ !action.isRetry()); + wasCanceled); + if (wasCanceled) { + loadErrorHandlingPolicy.onLoadTaskConcluded(loadable.loadTaskId); + } return action; } @@ -377,15 +408,15 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /* package */ static final class SourceLoadable implements Loadable { + public final long loadTaskId; public final DataSpec dataSpec; private final StatsDataSource dataSource; @Nullable private byte[] sampleData; - // the constructor does not initialize fields: sampleData - @SuppressWarnings("nullness:initialization.fields.uninitialized") public SourceLoadable(DataSpec dataSpec, DataSource dataSource) { + this.loadTaskId = LoadEventInfo.getNewId(); this.dataSpec = dataSpec; this.dataSource = new StatsDataSource(dataSource); } @@ -396,7 +427,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; } @Override - public void load() throws IOException, InterruptedException { + public void load() throws IOException { // We always load from the beginning, so reset bytesRead to 0. dataSource.resetBytesRead(); try { @@ -417,7 +448,5 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; Util.closeQuietly(dataSource); } } - } - } 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 db1414942f..4365c8fda5 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 @@ -59,7 +59,6 @@ public final class SingleSampleMediaSource extends BaseMediaSource { private LoadErrorHandlingPolicy loadErrorHandlingPolicy; private boolean treatLoadErrorsAsEndOfStream; - private boolean isCreateCalled; @Nullable private Object tag; /** @@ -81,8 +80,7 @@ public final class SingleSampleMediaSource extends BaseMediaSource { * @return This factory, for convenience. * @throws IllegalStateException If one of the {@code create} methods has already been called. */ - public Factory setTag(Object tag) { - Assertions.checkState(!isCreateCalled); + public Factory setTag(@Nullable Object tag) { this.tag = tag; return this; } @@ -115,9 +113,12 @@ public final class SingleSampleMediaSource extends BaseMediaSource { * @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; + public Factory setLoadErrorHandlingPolicy( + @Nullable LoadErrorHandlingPolicy loadErrorHandlingPolicy) { + this.loadErrorHandlingPolicy = + loadErrorHandlingPolicy != null + ? loadErrorHandlingPolicy + : new DefaultLoadErrorHandlingPolicy(); return this; } @@ -132,7 +133,6 @@ public final class SingleSampleMediaSource extends BaseMediaSource { * @throws IllegalStateException If one of the {@code create} methods has already been called. */ public Factory setTreatLoadErrorsAsEndOfStream(boolean treatLoadErrorsAsEndOfStream) { - Assertions.checkState(!isCreateCalled); this.treatLoadErrorsAsEndOfStream = treatLoadErrorsAsEndOfStream; return this; } @@ -146,7 +146,6 @@ public final class SingleSampleMediaSource extends BaseMediaSource { * @return The new {@link SingleSampleMediaSource}. */ public SingleSampleMediaSource createMediaSource(Uri uri, Format format, long durationUs) { - isCreateCalled = true; return new SingleSampleMediaSource( uri, dataSourceFactory, @@ -257,8 +256,8 @@ public final class SingleSampleMediaSource extends BaseMediaSource { Format format, long durationUs, int minLoadableRetryCount, - Handler eventHandler, - EventListener eventListener, + @Nullable Handler eventHandler, + @Nullable EventListener eventListener, int eventSourceId, boolean treatLoadErrorsAsEndOfStream) { this( @@ -288,7 +287,7 @@ public final class SingleSampleMediaSource extends BaseMediaSource { this.loadErrorHandlingPolicy = loadErrorHandlingPolicy; this.treatLoadErrorsAsEndOfStream = treatLoadErrorsAsEndOfStream; this.tag = tag; - dataSpec = new DataSpec(uri, DataSpec.FLAG_ALLOW_GZIP); + dataSpec = new DataSpec.Builder().setUri(uri).setFlags(DataSpec.FLAG_ALLOW_GZIP).build(); timeline = new SinglePeriodTimeline( durationUs, @@ -314,7 +313,7 @@ public final class SingleSampleMediaSource extends BaseMediaSource { } @Override - public void maybeThrowSourceInfoRefreshError() throws IOException { + public void maybeThrowSourceInfoRefreshError() { // Do nothing. } 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 0a1628b3f9..dee63d819e 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 @@ -29,8 +29,7 @@ import java.util.Arrays; import org.checkerframework.checker.nullness.compatqual.NullableType; /** - * Represents ad group times relative to the start of the media and information on the state and - * URIs of ads within each ad group. + * Represents ad group times and information on the state and URIs of ads within each ad group. * *

      Instances are immutable. Call the {@code with*} methods to get new instances that have the * required changes. @@ -272,8 +271,9 @@ public final class AdPlaybackState { /** The number of ad groups. */ public final int adGroupCount; /** - * The times of ad groups, in microseconds. A final element with the value {@link - * C#TIME_END_OF_SOURCE} indicates a postroll ad. + * The times of ad groups, in microseconds, relative to the start of the {@link + * com.google.android.exoplayer2.Timeline.Period} they belong to. A final element with the value + * {@link C#TIME_END_OF_SOURCE} indicates a postroll ad. */ public final long[] adGroupTimesUs; /** The ad groups. */ @@ -286,8 +286,9 @@ public final class AdPlaybackState { /** * Creates a new ad playback state with the specified ad group times. * - * @param adGroupTimesUs The times of ad groups in microseconds. A final element with the value - * {@link C#TIME_END_OF_SOURCE} indicates that there is a postroll ad. + * @param adGroupTimesUs The times of ad groups in microseconds, relative to the start of the + * {@link com.google.android.exoplayer2.Timeline.Period} they belong to. A final element with + * the value {@link C#TIME_END_OF_SOURCE} indicates that there is a postroll ad. */ public AdPlaybackState(long... adGroupTimesUs) { int count = adGroupTimesUs.length; @@ -315,16 +316,18 @@ public final class AdPlaybackState { * unplayed. Returns {@link C#INDEX_UNSET} if the ad group at or before {@code positionUs} has no * ads remaining to be played, or if there is no such ad group. * - * @param positionUs The position at or before which to find an ad group, in microseconds, or - * {@link C#TIME_END_OF_SOURCE} for the end of the stream (in which case the index of any + * @param positionUs The period position at or before which to find an ad group, in microseconds, + * or {@link C#TIME_END_OF_SOURCE} for the end of the stream (in which case the index of any * unplayed postroll ad group will be returned). + * @param periodDurationUs The duration of the containing timeline period, in microseconds, or + * {@link C#TIME_UNSET} if not known. * @return The index of the ad group, or {@link C#INDEX_UNSET}. */ - public int getAdGroupIndexForPositionUs(long positionUs) { + public int getAdGroupIndexForPositionUs(long positionUs, long periodDurationUs) { // Use a linear search as the array elements may not be increasing due to TIME_END_OF_SOURCE. // In practice we expect there to be few ad groups so the search shouldn't be expensive. int index = adGroupTimesUs.length - 1; - while (index >= 0 && isPositionBeforeAdGroup(positionUs, index)) { + while (index >= 0 && isPositionBeforeAdGroup(positionUs, periodDurationUs, index)) { index--; } return index >= 0 && adGroups[index].hasUnplayedAds() ? index : C.INDEX_UNSET; @@ -334,11 +337,11 @@ public final class AdPlaybackState { * Returns the index of the next ad group after {@code positionUs} that has ads remaining to be * played. Returns {@link C#INDEX_UNSET} if there is no such ad group. * - * @param positionUs The position after which to find an ad group, in microseconds, or {@link - * C#TIME_END_OF_SOURCE} for the end of the stream (in which case there can be no ad group - * after the position). - * @param periodDurationUs The duration of the containing period in microseconds, or {@link - * C#TIME_UNSET} if not known. + * @param positionUs The period position after which to find an ad group, in microseconds, or + * {@link C#TIME_END_OF_SOURCE} for the end of the stream (in which case there can be no ad + * group after the position). + * @param periodDurationUs The duration of the containing timeline period, in microseconds, or + * {@link C#TIME_UNSET} if not known. * @return The index of the ad group, or {@link C#INDEX_UNSET}. */ public int getAdGroupIndexAfterPositionUs(long positionUs, long periodDurationUs) { @@ -425,7 +428,10 @@ public final class AdPlaybackState { return new AdPlaybackState(adGroupTimesUs, adGroups, adResumePositionUs, contentDurationUs); } - /** Returns an instance with the specified ad resume position, in microseconds. */ + /** + * Returns an instance with the specified ad resume position, in microseconds, relative to the + * start of the current ad. + */ @CheckResult public AdPlaybackState withAdResumePositionUs(long adResumePositionUs) { if (this.adResumePositionUs == adResumePositionUs) { @@ -471,14 +477,15 @@ public final class AdPlaybackState { return result; } - private boolean isPositionBeforeAdGroup(long positionUs, int adGroupIndex) { + private boolean isPositionBeforeAdGroup( + long positionUs, long periodDurationUs, int adGroupIndex) { if (positionUs == C.TIME_END_OF_SOURCE) { // The end of the content is at (but not before) any postroll ad, and after any other ads. return false; } long adGroupPositionUs = adGroupTimesUs[adGroupIndex]; if (adGroupPositionUs == C.TIME_END_OF_SOURCE) { - return contentDurationUs == C.TIME_UNSET || positionUs < contentDurationUs; + return periodDurationUs == C.TIME_UNSET || positionUs < periodDurationUs; } else { return positionUs < adGroupPositionUs; } 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 d1b5e84fb4..07a46f06a9 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 @@ -18,6 +18,7 @@ package com.google.android.exoplayer2.source.ads; import android.net.Uri; import android.os.Handler; import android.os.Looper; +import android.os.SystemClock; import androidx.annotation.IntDef; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; @@ -44,11 +45,9 @@ 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.HashMap; import java.util.List; -import java.util.Map; import org.checkerframework.checker.nullness.compatqual.NullableType; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /** * A {@link MediaSource} that inserts ads linearly with a provided content media source. This source @@ -129,15 +128,13 @@ public final class AdsMediaSource extends CompositeMediaSource { private final AdsLoader adsLoader; private final AdsLoader.AdViewProvider adViewProvider; private final Handler mainHandler; - private final Map> maskingMediaPeriodByAdMediaSource; private final Timeline.Period period; // Accessed on the player thread. @Nullable private ComponentListener componentListener; @Nullable private Timeline contentTimeline; @Nullable private AdPlaybackState adPlaybackState; - private @NullableType MediaSource[][] adGroupMediaSources; - private @NullableType Timeline[][] adGroupTimelines; + private @NullableType AdMediaSourceHolder[][] adMediaSourceHolders; /** * Constructs a new source that inserts ads linearly with the content specified by {@code @@ -179,10 +176,8 @@ public final class AdsMediaSource extends CompositeMediaSource { this.adsLoader = adsLoader; this.adViewProvider = adViewProvider; mainHandler = new Handler(Looper.getMainLooper()); - maskingMediaPeriodByAdMediaSource = new HashMap<>(); period = new Timeline.Period(); - adGroupMediaSources = new MediaSource[0][]; - adGroupTimelines = new Timeline[0][]; + adMediaSourceHolders = new AdMediaSourceHolder[0][]; adsLoader.setSupportedContentTypes(adMediaSourceFactory.getSupportedTypes()); } @@ -209,36 +204,21 @@ public final class AdsMediaSource extends CompositeMediaSource { int adIndexInAdGroup = id.adIndexInAdGroup; Uri adUri = Assertions.checkNotNull(adPlaybackState.adGroups[adGroupIndex].uris[adIndexInAdGroup]); - if (adGroupMediaSources[adGroupIndex].length <= adIndexInAdGroup) { + if (adMediaSourceHolders[adGroupIndex].length <= adIndexInAdGroup) { int adCount = adIndexInAdGroup + 1; - adGroupMediaSources[adGroupIndex] = - Arrays.copyOf(adGroupMediaSources[adGroupIndex], adCount); - adGroupTimelines[adGroupIndex] = Arrays.copyOf(adGroupTimelines[adGroupIndex], adCount); + adMediaSourceHolders[adGroupIndex] = + Arrays.copyOf(adMediaSourceHolders[adGroupIndex], adCount); } - MediaSource mediaSource = adGroupMediaSources[adGroupIndex][adIndexInAdGroup]; - if (mediaSource == null) { - mediaSource = adMediaSourceFactory.createMediaSource(adUri); - adGroupMediaSources[adGroupIndex][adIndexInAdGroup] = mediaSource; - maskingMediaPeriodByAdMediaSource.put(mediaSource, new ArrayList<>()); - prepareChildSource(id, mediaSource); + @Nullable + AdMediaSourceHolder adMediaSourceHolder = + adMediaSourceHolders[adGroupIndex][adIndexInAdGroup]; + if (adMediaSourceHolder == null) { + MediaSource adMediaSource = adMediaSourceFactory.createMediaSource(adUri); + adMediaSourceHolder = new AdMediaSourceHolder(adMediaSource); + adMediaSourceHolders[adGroupIndex][adIndexInAdGroup] = adMediaSourceHolder; + prepareChildSource(id, adMediaSource); } - MaskingMediaPeriod maskingMediaPeriod = - new MaskingMediaPeriod(mediaSource, id, allocator, startPositionUs); - maskingMediaPeriod.setPrepareErrorListener( - new AdPrepareErrorListener(adUri, adGroupIndex, adIndexInAdGroup)); - List mediaPeriods = maskingMediaPeriodByAdMediaSource.get(mediaSource); - if (mediaPeriods == null) { - Object periodUid = - Assertions.checkNotNull(adGroupTimelines[adGroupIndex][adIndexInAdGroup]) - .getUidOfPeriod(/* periodIndex= */ 0); - MediaPeriodId adSourceMediaPeriodId = new MediaPeriodId(periodUid, id.windowSequenceNumber); - maskingMediaPeriod.createPeriod(adSourceMediaPeriodId); - } else { - // Keep track of the masking media period so it can be populated with the real media period - // when the source's info becomes available. - mediaPeriods.add(maskingMediaPeriod); - } - return maskingMediaPeriod; + return adMediaSourceHolder.createMediaPeriod(adUri, id, allocator, startPositionUs); } else { MaskingMediaPeriod mediaPeriod = new MaskingMediaPeriod(contentMediaSource, id, allocator, startPositionUs); @@ -250,12 +230,18 @@ public final class AdsMediaSource extends CompositeMediaSource { @Override public void releasePeriod(MediaPeriod mediaPeriod) { MaskingMediaPeriod maskingMediaPeriod = (MaskingMediaPeriod) mediaPeriod; - List mediaPeriods = - maskingMediaPeriodByAdMediaSource.get(maskingMediaPeriod.mediaSource); - if (mediaPeriods != null) { - mediaPeriods.remove(maskingMediaPeriod); + MediaPeriodId id = maskingMediaPeriod.id; + if (id.isAd()) { + AdMediaSourceHolder adMediaSourceHolder = + Assertions.checkNotNull(adMediaSourceHolders[id.adGroupIndex][id.adIndexInAdGroup]); + adMediaSourceHolder.releaseMediaPeriod(maskingMediaPeriod); + if (adMediaSourceHolder.isInactive()) { + releaseChildSource(id); + adMediaSourceHolders[id.adGroupIndex][id.adIndexInAdGroup] = null; + } + } else { + maskingMediaPeriod.releasePeriod(); } - maskingMediaPeriod.releasePeriod(); } @Override @@ -263,11 +249,9 @@ public final class AdsMediaSource extends CompositeMediaSource { super.releaseSourceInternal(); Assertions.checkNotNull(componentListener).release(); componentListener = null; - maskingMediaPeriodByAdMediaSource.clear(); contentTimeline = null; adPlaybackState = null; - adGroupMediaSources = new MediaSource[0][]; - adGroupTimelines = new Timeline[0][]; + adMediaSourceHolders = new AdMediaSourceHolder[0][]; mainHandler.post(adsLoader::stop); } @@ -277,14 +261,17 @@ public final class AdsMediaSource extends CompositeMediaSource { if (mediaPeriodId.isAd()) { int adGroupIndex = mediaPeriodId.adGroupIndex; int adIndexInAdGroup = mediaPeriodId.adIndexInAdGroup; - onAdSourceInfoRefreshed(mediaSource, adGroupIndex, adIndexInAdGroup, timeline); + Assertions.checkNotNull(adMediaSourceHolders[adGroupIndex][adIndexInAdGroup]) + .handleSourceInfoRefresh(timeline); } else { - onContentSourceInfoRefreshed(timeline); + Assertions.checkArgument(timeline.getPeriodCount() == 1); + contentTimeline = timeline; } + maybeUpdateSourceInfo(); } @Override - protected @Nullable MediaPeriodId getMediaPeriodIdForChildMediaPeriodId( + protected MediaPeriodId getMediaPeriodIdForChildMediaPeriodId( MediaPeriodId childId, MediaPeriodId mediaPeriodId) { // 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. @@ -295,42 +282,17 @@ public final class AdsMediaSource extends CompositeMediaSource { private void onAdPlaybackState(AdPlaybackState adPlaybackState) { if (this.adPlaybackState == null) { - adGroupMediaSources = new MediaSource[adPlaybackState.adGroupCount][]; - Arrays.fill(adGroupMediaSources, new MediaSource[0]); - adGroupTimelines = new Timeline[adPlaybackState.adGroupCount][]; - Arrays.fill(adGroupTimelines, new Timeline[0]); + adMediaSourceHolders = new AdMediaSourceHolder[adPlaybackState.adGroupCount][]; + Arrays.fill(adMediaSourceHolders, new AdMediaSourceHolder[0]); } this.adPlaybackState = adPlaybackState; maybeUpdateSourceInfo(); } - private void onContentSourceInfoRefreshed(Timeline timeline) { - Assertions.checkArgument(timeline.getPeriodCount() == 1); - contentTimeline = timeline; - maybeUpdateSourceInfo(); - } - - private void onAdSourceInfoRefreshed(MediaSource mediaSource, int adGroupIndex, - int adIndexInAdGroup, Timeline timeline) { - Assertions.checkArgument(timeline.getPeriodCount() == 1); - adGroupTimelines[adGroupIndex][adIndexInAdGroup] = timeline; - List mediaPeriods = maskingMediaPeriodByAdMediaSource.remove(mediaSource); - if (mediaPeriods != null) { - Object periodUid = timeline.getUidOfPeriod(/* periodIndex= */ 0); - for (int i = 0; i < mediaPeriods.size(); i++) { - MaskingMediaPeriod mediaPeriod = mediaPeriods.get(i); - MediaPeriodId adSourceMediaPeriodId = - new MediaPeriodId(periodUid, mediaPeriod.id.windowSequenceNumber); - mediaPeriod.createPeriod(adSourceMediaPeriodId); - } - } - maybeUpdateSourceInfo(); - } - private void maybeUpdateSourceInfo() { - Timeline contentTimeline = this.contentTimeline; + @Nullable Timeline contentTimeline = this.contentTimeline; if (adPlaybackState != null && contentTimeline != null) { - adPlaybackState = adPlaybackState.withAdDurationsUs(getAdDurations(adGroupTimelines, period)); + adPlaybackState = adPlaybackState.withAdDurationsUs(getAdDurationsUs()); Timeline timeline = adPlaybackState.adGroupCount == 0 ? contentTimeline @@ -339,19 +301,16 @@ public final class AdsMediaSource extends CompositeMediaSource { } } - private static long[][] getAdDurations( - @NullableType Timeline[][] adTimelines, Timeline.Period period) { - long[][] adDurations = new long[adTimelines.length][]; - for (int i = 0; i < adTimelines.length; i++) { - adDurations[i] = new long[adTimelines[i].length]; - for (int j = 0; j < adTimelines[i].length; j++) { - adDurations[i][j] = - adTimelines[i][j] == null - ? C.TIME_UNSET - : adTimelines[i][j].getPeriod(/* periodIndex= */ 0, period).getDurationUs(); + private long[][] getAdDurationsUs() { + long[][] adDurationsUs = new long[adMediaSourceHolders.length][]; + for (int i = 0; i < adMediaSourceHolders.length; i++) { + adDurationsUs[i] = new long[adMediaSourceHolders[i].length]; + for (int j = 0; j < adMediaSourceHolders[i].length; j++) { + @Nullable AdMediaSourceHolder holder = adMediaSourceHolders[i][j]; + adDurationsUs[i][j] = holder == null ? C.TIME_UNSET : holder.getDurationUs(); } } - return adDurations; + return adDurationsUs; } /** Listener for component events. All methods are called on the main thread. */ @@ -396,13 +355,11 @@ public final class AdsMediaSource extends CompositeMediaSource { } createEventDispatcher(/* mediaPeriodId= */ null) .loadError( - dataSpec, - dataSpec.uri, - /* responseHeaders= */ Collections.emptyMap(), + new LoadEventInfo( + LoadEventInfo.getNewId(), + dataSpec, + /* elapsedRealtimeMs= */ SystemClock.elapsedRealtime()), C.DATA_TYPE_AD, - C.TRACK_TYPE_UNKNOWN, - /* loadDurationMs= */ 0, - /* bytesLoaded= */ 0, error, /* wasCanceled= */ true); } @@ -424,17 +381,72 @@ public final class AdsMediaSource extends CompositeMediaSource { public void onPrepareError(MediaPeriodId mediaPeriodId, final IOException exception) { createEventDispatcher(mediaPeriodId) .loadError( - new DataSpec(adUri), - adUri, - /* responseHeaders= */ Collections.emptyMap(), + new LoadEventInfo( + LoadEventInfo.getNewId(), + new DataSpec(adUri), + /* elapsedRealtimeMs= */ SystemClock.elapsedRealtime()), C.DATA_TYPE_AD, - C.TRACK_TYPE_UNKNOWN, - /* loadDurationMs= */ 0, - /* bytesLoaded= */ 0, AdLoadException.createForAd(exception), /* wasCanceled= */ true); mainHandler.post( () -> adsLoader.handlePrepareError(adGroupIndex, adIndexInAdGroup, exception)); } } + + private final class AdMediaSourceHolder { + + private final MediaSource adMediaSource; + private final List activeMediaPeriods; + + private @MonotonicNonNull Timeline timeline; + + public AdMediaSourceHolder(MediaSource adMediaSource) { + this.adMediaSource = adMediaSource; + activeMediaPeriods = new ArrayList<>(); + } + + public MediaPeriod createMediaPeriod( + Uri adUri, MediaPeriodId id, Allocator allocator, long startPositionUs) { + MaskingMediaPeriod maskingMediaPeriod = + new MaskingMediaPeriod(adMediaSource, id, allocator, startPositionUs); + maskingMediaPeriod.setPrepareErrorListener( + new AdPrepareErrorListener(adUri, id.adGroupIndex, id.adIndexInAdGroup)); + activeMediaPeriods.add(maskingMediaPeriod); + if (timeline != null) { + Object periodUid = timeline.getUidOfPeriod(/* periodIndex= */ 0); + MediaPeriodId adSourceMediaPeriodId = new MediaPeriodId(periodUid, id.windowSequenceNumber); + maskingMediaPeriod.createPeriod(adSourceMediaPeriodId); + } + return maskingMediaPeriod; + } + + public void handleSourceInfoRefresh(Timeline timeline) { + Assertions.checkArgument(timeline.getPeriodCount() == 1); + if (this.timeline == null) { + Object periodUid = timeline.getUidOfPeriod(/* periodIndex= */ 0); + for (int i = 0; i < activeMediaPeriods.size(); i++) { + MaskingMediaPeriod mediaPeriod = activeMediaPeriods.get(i); + MediaPeriodId adSourceMediaPeriodId = + new MediaPeriodId(periodUid, mediaPeriod.id.windowSequenceNumber); + mediaPeriod.createPeriod(adSourceMediaPeriodId); + } + } + this.timeline = timeline; + } + + public long getDurationUs() { + return timeline == null + ? C.TIME_UNSET + : timeline.getPeriod(/* periodIndex= */ 0, period).getDurationUs(); + } + + public void releaseMediaPeriod(MaskingMediaPeriod maskingMediaPeriod) { + activeMediaPeriods.remove(maskingMediaPeriod); + maskingMediaPeriod.releasePeriod(); + } + + public boolean isInactive() { + return activeMediaPeriods.isEmpty(); + } + } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/package-info.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/package-info.java new file mode 100644 index 0000000000..ee7c4ab024 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2019 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. + */ +@NonNullApi +package com.google.android.exoplayer2.source.ads; + +import com.google.android.exoplayer2.util.NonNullApi; 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 74d8ddad3d..b508c1da1d 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 @@ -20,6 +20,8 @@ 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.util.Assertions; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /** * A base implementation of {@link MediaChunk} that outputs to a {@link BaseMediaChunkOutput}. @@ -37,8 +39,8 @@ public abstract class BaseMediaChunk extends MediaChunk { */ public final long clippedEndTimeUs; - private BaseMediaChunkOutput output; - private int[] firstSampleIndices; + private @MonotonicNonNull BaseMediaChunkOutput output; + private int @MonotonicNonNull [] firstSampleIndices; /** * @param dataSource The source from which the data should be loaded. @@ -87,14 +89,14 @@ public abstract class BaseMediaChunk extends MediaChunk { * from this chunk. */ public final int getFirstSampleIndex(int trackIndex) { - return firstSampleIndices[trackIndex]; + return Assertions.checkStateNotNull(firstSampleIndices)[trackIndex]; } /** * Returns the output most recently passed to {@link #init(BaseMediaChunkOutput)}. */ protected final BaseMediaChunkOutput getOutput() { - return output; + return Assertions.checkStateNotNull(output); } } 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 0bcc46fcbf..50c37f8b31 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,7 +21,10 @@ import com.google.android.exoplayer2.source.SampleQueue; import com.google.android.exoplayer2.source.chunk.ChunkExtractorWrapper.TrackOutputProvider; import com.google.android.exoplayer2.util.Log; -/** An output for {@link BaseMediaChunk}s. */ +/** + * A {@link TrackOutputProvider} that provides {@link TrackOutput TrackOutputs} based on a + * predefined mapping from track type to output. + */ public final class BaseMediaChunkOutput implements TrackOutputProvider { private static final String TAG = "BaseMediaChunkOutput"; @@ -55,9 +58,7 @@ public final class BaseMediaChunkOutput implements TrackOutputProvider { public int[] getWriteIndices() { int[] writeIndices = new int[sampleQueues.length]; for (int i = 0; i < sampleQueues.length; i++) { - if (sampleQueues[i] != null) { - writeIndices[i] = sampleQueues[i].getWriteIndex(); - } + writeIndices[i] = sampleQueues[i].getWriteIndex(); } return writeIndices; } @@ -68,9 +69,7 @@ public final class BaseMediaChunkOutput implements TrackOutputProvider { */ public void setSampleOffsetUs(long sampleOffsetUs) { for (SampleQueue sampleQueue : sampleQueues) { - if (sampleQueue != null) { - sampleQueue.setSampleOffsetUs(sampleOffsetUs); - } + sampleQueue.setSampleOffsetUs(sampleOffsetUs); } } 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 a794f67fe2..2d2d74718b 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 @@ -19,6 +19,7 @@ import android.net.Uri; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.source.LoadEventInfo; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.upstream.Loader.Loadable; @@ -33,28 +34,26 @@ import java.util.Map; */ public abstract class Chunk implements Loadable { - /** - * The {@link DataSpec} that defines the data to be loaded. - */ + /** Identifies the load task for this loadable. */ + public final long loadTaskId; + /** The {@link DataSpec} that defines the data to be loaded. */ public final DataSpec dataSpec; /** * The type of the chunk. One of the {@code DATA_TYPE_*} constants defined in {@link C}. For * reporting only. */ public final int type; - /** - * The format of the track to which this chunk belongs, or null if the chunk does not belong to - * a track. - */ + /** The format of the track to which this chunk belongs. */ public final Format trackFormat; /** * One of the {@link C} {@code SELECTION_REASON_*} constants if the chunk belongs to a track. - * {@link C#SELECTION_REASON_UNKNOWN} if the chunk does not belong to a track. + * {@link C#SELECTION_REASON_UNKNOWN} if the chunk does not belong to a track, or if the selection + * reason is unknown. */ public final int trackSelectionReason; /** * Optional data associated with the selection of the track to which this chunk belongs. Null if - * the chunk does not belong to a track. + * the chunk does not belong to a track, or if there is no associated track selection data. */ @Nullable public final Object trackSelectionData; /** @@ -97,6 +96,7 @@ public abstract class Chunk implements Loadable { this.trackSelectionData = trackSelectionData; this.startTimeUs = startTimeUs; this.endTimeUs = endTimeUs; + loadTaskId = LoadEventInfo.getNewId(); } /** 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 c4c8647a55..f2362f2eb1 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,19 +15,22 @@ */ package com.google.android.exoplayer2.source.chunk; +import static com.google.android.exoplayer2.util.Util.castNonNull; + import android.util.SparseArray; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.extractor.DummyTrackOutput; import com.google.android.exoplayer2.extractor.Extractor; -import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.SeekMap; import com.google.android.exoplayer2.extractor.TrackOutput; +import com.google.android.exoplayer2.upstream.DataReader; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.ParsableByteArray; import java.io.IOException; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /** * An {@link Extractor} wrapper for loading chunks that contain a single primary track, and possibly @@ -63,10 +66,10 @@ public final class ChunkExtractorWrapper implements ExtractorOutput { private final SparseArray bindingTrackOutputs; private boolean extractorInitialized; - private TrackOutputProvider trackOutputProvider; + @Nullable private TrackOutputProvider trackOutputProvider; private long endTimeUs; - private SeekMap seekMap; - private Format[] sampleFormats; + private @MonotonicNonNull SeekMap seekMap; + private Format @MonotonicNonNull [] sampleFormats; /** * @param extractor The extractor to wrap. @@ -84,15 +87,19 @@ public final class ChunkExtractorWrapper implements ExtractorOutput { } /** - * Returns the {@link SeekMap} most recently output by the extractor, or null. + * Returns the {@link SeekMap} most recently output by the extractor, or null if the extractor has + * not output a {@link SeekMap}. */ + @Nullable public SeekMap getSeekMap() { return seekMap; } /** - * Returns the sample {@link Format}s most recently output by the extractor, or null. + * Returns the sample {@link Format}s for the tracks identified by the extractor, or null if the + * extractor has not finished identifying tracks. */ + @Nullable public Format[] getSampleFormats() { return sampleFormats; } @@ -146,7 +153,7 @@ public final class ChunkExtractorWrapper implements ExtractorOutput { public void endTracks() { Format[] sampleFormats = new Format[bindingTrackOutputs.size()]; for (int i = 0; i < bindingTrackOutputs.size(); i++) { - sampleFormats[i] = bindingTrackOutputs.valueAt(i).sampleFormat; + sampleFormats[i] = Assertions.checkStateNotNull(bindingTrackOutputs.valueAt(i).sampleFormat); } this.sampleFormats = sampleFormats; } @@ -162,21 +169,21 @@ public final class ChunkExtractorWrapper implements ExtractorOutput { private final int id; private final int type; - private final Format manifestFormat; + @Nullable private final Format manifestFormat; private final DummyTrackOutput dummyTrackOutput; - public Format sampleFormat; - private TrackOutput trackOutput; + public @MonotonicNonNull Format sampleFormat; + private @MonotonicNonNull TrackOutput trackOutput; private long endTimeUs; - public BindingTrackOutput(int id, int type, Format manifestFormat) { + public BindingTrackOutput(int id, int type, @Nullable Format manifestFormat) { this.id = id; this.type = type; this.manifestFormat = manifestFormat; dummyTrackOutput = new DummyTrackOutput(); } - public void bind(TrackOutputProvider trackOutputProvider, long endTimeUs) { + public void bind(@Nullable TrackOutputProvider trackOutputProvider, long endTimeUs) { if (trackOutputProvider == null) { trackOutput = dummyTrackOutput; return; @@ -190,31 +197,34 @@ public final class ChunkExtractorWrapper implements ExtractorOutput { @Override public void format(Format format) { - sampleFormat = manifestFormat != null ? format.copyWithManifestFormatInfo(manifestFormat) - : format; - trackOutput.format(sampleFormat); + sampleFormat = + manifestFormat != null ? format.withManifestFormatInfo(manifestFormat) : format; + castNonNull(trackOutput).format(sampleFormat); } @Override - public int sampleData(ExtractorInput input, int length, boolean allowEndOfInput) - throws IOException, InterruptedException { - return trackOutput.sampleData(input, length, allowEndOfInput); + public int sampleData( + DataReader input, int length, boolean allowEndOfInput, @SampleDataPart int sampleDataPart) + throws IOException { + return castNonNull(trackOutput).sampleData(input, length, allowEndOfInput); } @Override - public void sampleData(ParsableByteArray data, int length) { - trackOutput.sampleData(data, length); + public void sampleData(ParsableByteArray data, int length, @SampleDataPart int sampleDataPart) { + castNonNull(trackOutput).sampleData(data, length); } @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 (endTimeUs != C.TIME_UNSET && timeUs >= endTimeUs) { trackOutput = dummyTrackOutput; } - trackOutput.sampleMetadata(timeUs, flags, size, offset, cryptoData); + castNonNull(trackOutput).sampleMetadata(timeUs, flags, size, offset, cryptoData); } - } - } 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 32160904c6..c9a552a7cf 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 @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.source.chunk; +import android.os.Looper; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; @@ -23,12 +24,15 @@ import com.google.android.exoplayer2.SeekParameters; import com.google.android.exoplayer2.decoder.DecoderInputBuffer; import com.google.android.exoplayer2.drm.DrmSession; import com.google.android.exoplayer2.drm.DrmSessionManager; +import com.google.android.exoplayer2.source.LoadEventInfo; +import com.google.android.exoplayer2.source.MediaLoadData; import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; 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.LoadErrorHandlingPolicy; +import com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy.LoadErrorInfo; import com.google.android.exoplayer2.upstream.Loader; import com.google.android.exoplayer2.upstream.Loader.LoadErrorAction; import com.google.android.exoplayer2.util.Assertions; @@ -38,6 +42,7 @@ import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /** * A {@link SampleStream} that loads media in {@link Chunk}s, obtained from a {@link ChunkSource}. @@ -61,8 +66,8 @@ public class ChunkSampleStream implements SampleStream, S public final int primaryTrackType; - @Nullable private final int[] embeddedTrackTypes; - @Nullable private final Format[] embeddedTrackFormats; + private final int[] embeddedTrackTypes; + private final Format[] embeddedTrackFormats; private final boolean[] embeddedTracksSelected; private final T chunkSource; private final SequenceableLoader.Callback> callback; @@ -74,9 +79,9 @@ public class ChunkSampleStream implements SampleStream, S private final List readOnlyMediaChunks; private final SampleQueue primarySampleQueue; private final SampleQueue[] embeddedSampleQueues; - private final BaseMediaChunkOutput mediaChunkOutput; + private final BaseMediaChunkOutput chunkOutput; - private Format primaryDownstreamTrackFormat; + private @MonotonicNonNull Format primaryDownstreamTrackFormat; @Nullable private ReleaseCallback releaseCallback; private long pendingResetPositionUs; private long lastSeekPositionUs; @@ -109,12 +114,12 @@ public class ChunkSampleStream implements SampleStream, S Callback> callback, Allocator allocator, long positionUs, - DrmSessionManager drmSessionManager, + DrmSessionManager drmSessionManager, LoadErrorHandlingPolicy loadErrorHandlingPolicy, EventDispatcher eventDispatcher) { this.primaryTrackType = primaryTrackType; - this.embeddedTrackTypes = embeddedTrackTypes; - this.embeddedTrackFormats = embeddedTrackFormats; + this.embeddedTrackTypes = embeddedTrackTypes == null ? new int[0] : embeddedTrackTypes; + this.embeddedTrackFormats = embeddedTrackFormats == null ? new Format[0] : embeddedTrackFormats; this.chunkSource = chunkSource; this.callback = callback; this.eventDispatcher = eventDispatcher; @@ -124,25 +129,34 @@ public class ChunkSampleStream implements SampleStream, S mediaChunks = new ArrayList<>(); readOnlyMediaChunks = Collections.unmodifiableList(mediaChunks); - int embeddedTrackCount = embeddedTrackTypes == null ? 0 : embeddedTrackTypes.length; + int embeddedTrackCount = this.embeddedTrackTypes.length; embeddedSampleQueues = new SampleQueue[embeddedTrackCount]; embeddedTracksSelected = new boolean[embeddedTrackCount]; int[] trackTypes = new int[1 + embeddedTrackCount]; SampleQueue[] sampleQueues = new SampleQueue[1 + embeddedTrackCount]; - primarySampleQueue = new SampleQueue(allocator, drmSessionManager); + primarySampleQueue = + new SampleQueue( + allocator, + /* playbackLooper= */ Assertions.checkNotNull(Looper.myLooper()), + drmSessionManager, + eventDispatcher); trackTypes[0] = primaryTrackType; sampleQueues[0] = primarySampleQueue; for (int i = 0; i < embeddedTrackCount; i++) { SampleQueue sampleQueue = - new SampleQueue(allocator, DrmSessionManager.getDummyDrmSessionManager()); + new SampleQueue( + allocator, + /* playbackLooper= */ Assertions.checkNotNull(Looper.myLooper()), + DrmSessionManager.getDummyDrmSessionManager(), + eventDispatcher); embeddedSampleQueues[i] = sampleQueue; sampleQueues[i + 1] = sampleQueue; - trackTypes[i + 1] = embeddedTrackTypes[i]; + trackTypes[i + 1] = this.embeddedTrackTypes[i]; } - mediaChunkOutput = new BaseMediaChunkOutput(trackTypes, sampleQueues); + chunkOutput = new BaseMediaChunkOutput(trackTypes, sampleQueues); pendingResetPositionUs = positionUs; lastSeekPositionUs = positionUs; } @@ -185,8 +199,7 @@ public class ChunkSampleStream implements SampleStream, S if (embeddedTrackTypes[i] == trackType) { Assertions.checkState(!embeddedTracksSelected[i]); embeddedTracksSelected[i] = true; - embeddedSampleQueues[i].rewind(); - embeddedSampleQueues[i].advanceTo(positionUs, true, true); + embeddedSampleQueues[i].seekTo(positionUs, /* allowTimeBeyondBuffer= */ true); return new EmbeddedSampleStream(this, embeddedSampleQueues[i], i); } } @@ -251,7 +264,7 @@ public class ChunkSampleStream implements SampleStream, S } // Detect whether the seek is to the start of a chunk that's at least partially buffered. - BaseMediaChunk seekToMediaChunk = null; + @Nullable BaseMediaChunk seekToMediaChunk = null; for (int i = 0; i < mediaChunks.size(); i++) { BaseMediaChunk mediaChunk = mediaChunks.get(i); long mediaChunkStartTimeUs = mediaChunk.startTimeUs; @@ -266,21 +279,16 @@ public class ChunkSampleStream implements SampleStream, S // See if we can seek inside the primary sample queue. boolean seekInsideBuffer; - primarySampleQueue.rewind(); if (seekToMediaChunk != null) { // When seeking to the start of a chunk we use the index of the first sample in the chunk // rather than the seek position. This ensures we seek to the keyframe at the start of the // chunk even if the sample timestamps are slightly offset from the chunk start times. - seekInsideBuffer = - primarySampleQueue.setReadPosition(seekToMediaChunk.getFirstSampleIndex(0)); + seekInsideBuffer = primarySampleQueue.seekTo(seekToMediaChunk.getFirstSampleIndex(0)); decodeOnlyUntilPositionUs = 0; } else { seekInsideBuffer = - primarySampleQueue.advanceTo( - positionUs, - /* toKeyframe= */ true, - /* allowTimeBeyondBuffer= */ positionUs < getNextLoadPositionUs()) - != SampleQueue.ADVANCE_FAILED; + primarySampleQueue.seekTo( + positionUs, /* allowTimeBeyondBuffer= */ positionUs < getNextLoadPositionUs()); decodeOnlyUntilPositionUs = lastSeekPositionUs; } @@ -289,10 +297,9 @@ public class ChunkSampleStream implements SampleStream, S nextNotifyPrimaryFormatMediaChunkIndex = primarySampleIndexToMediaChunkIndex( primarySampleQueue.getReadIndex(), /* minChunkIndex= */ 0); - // Advance the embedded sample queues to the seek position. + // Seek the embedded sample queues. for (SampleQueue embeddedSampleQueue : embeddedSampleQueues) { - embeddedSampleQueue.rewind(); - embeddedSampleQueue.advanceTo(positionUs, true, false); + embeddedSampleQueue.seekTo(positionUs, /* allowTimeBeyondBuffer= */ true); } } else { // We can't seek inside the buffer, and so need to reset. @@ -389,10 +396,7 @@ public class ChunkSampleStream implements SampleStream, S if (loadingFinished && positionUs > primarySampleQueue.getLargestQueuedTimestampUs()) { skipCount = primarySampleQueue.advanceToEnd(); } else { - skipCount = primarySampleQueue.advanceTo(positionUs, true, true); - if (skipCount == SampleQueue.ADVANCE_FAILED) { - skipCount = 0; - } + skipCount = primarySampleQueue.advanceTo(positionUs); } maybeNotifyPrimaryTrackFormatChanged(); return skipCount; @@ -403,40 +407,50 @@ public class ChunkSampleStream implements SampleStream, S @Override public void onLoadCompleted(Chunk loadable, long elapsedRealtimeMs, long loadDurationMs) { chunkSource.onChunkLoadCompleted(loadable); + LoadEventInfo loadEventInfo = + new LoadEventInfo( + loadable.loadTaskId, + loadable.dataSpec, + loadable.getUri(), + loadable.getResponseHeaders(), + elapsedRealtimeMs, + loadDurationMs, + loadable.bytesLoaded()); + loadErrorHandlingPolicy.onLoadTaskConcluded(loadable.loadTaskId); eventDispatcher.loadCompleted( - loadable.dataSpec, - loadable.getUri(), - loadable.getResponseHeaders(), + loadEventInfo, loadable.type, primaryTrackType, loadable.trackFormat, loadable.trackSelectionReason, loadable.trackSelectionData, loadable.startTimeUs, - loadable.endTimeUs, - elapsedRealtimeMs, - loadDurationMs, - loadable.bytesLoaded()); + loadable.endTimeUs); callback.onContinueLoadingRequested(this); } @Override - public void onLoadCanceled(Chunk loadable, long elapsedRealtimeMs, long loadDurationMs, - boolean released) { + public void onLoadCanceled( + Chunk loadable, long elapsedRealtimeMs, long loadDurationMs, boolean released) { + LoadEventInfo loadEventInfo = + new LoadEventInfo( + loadable.loadTaskId, + loadable.dataSpec, + loadable.getUri(), + loadable.getResponseHeaders(), + elapsedRealtimeMs, + loadDurationMs, + loadable.bytesLoaded()); + loadErrorHandlingPolicy.onLoadTaskConcluded(loadable.loadTaskId); eventDispatcher.loadCanceled( - loadable.dataSpec, - loadable.getUri(), - loadable.getResponseHeaders(), + loadEventInfo, loadable.type, primaryTrackType, loadable.trackFormat, loadable.trackSelectionReason, loadable.trackSelectionData, loadable.startTimeUs, - loadable.endTimeUs, - elapsedRealtimeMs, - loadDurationMs, - loadable.bytesLoaded()); + loadable.endTimeUs); if (!released) { primarySampleQueue.reset(); for (SampleQueue embeddedSampleQueue : embeddedSampleQueues) { @@ -458,12 +472,32 @@ public class ChunkSampleStream implements SampleStream, S int lastChunkIndex = mediaChunks.size() - 1; boolean cancelable = bytesLoaded == 0 || !isMediaChunk || !haveReadFromMediaChunk(lastChunkIndex); + LoadEventInfo loadEventInfo = + new LoadEventInfo( + loadable.loadTaskId, + loadable.dataSpec, + loadable.getUri(), + loadable.getResponseHeaders(), + elapsedRealtimeMs, + loadDurationMs, + bytesLoaded); + MediaLoadData mediaLoadData = + new MediaLoadData( + loadable.type, + primaryTrackType, + loadable.trackFormat, + loadable.trackSelectionReason, + loadable.trackSelectionData, + C.usToMs(loadable.startTimeUs), + C.usToMs(loadable.endTimeUs)); + LoadErrorInfo loadErrorInfo = + new LoadErrorInfo(loadEventInfo, mediaLoadData, error, errorCount); + long blacklistDurationMs = cancelable - ? loadErrorHandlingPolicy.getBlacklistDurationMsFor( - loadable.type, loadDurationMs, error, errorCount) + ? loadErrorHandlingPolicy.getBlacklistDurationMsFor(loadErrorInfo) : C.TIME_UNSET; - LoadErrorAction loadErrorAction = null; + @Nullable LoadErrorAction loadErrorAction = null; if (chunkSource.onChunkLoadError(loadable, cancelable, error, blacklistDurationMs)) { if (cancelable) { loadErrorAction = Loader.DONT_RETRY; @@ -481,9 +515,7 @@ public class ChunkSampleStream implements SampleStream, S 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); + long retryDelayMs = loadErrorHandlingPolicy.getRetryDelayMsFor(loadErrorInfo); loadErrorAction = retryDelayMs != C.TIME_UNSET ? Loader.createRetryAction(/* resetErrorCount= */ false, retryDelayMs) @@ -492,9 +524,7 @@ public class ChunkSampleStream implements SampleStream, S boolean canceled = !loadErrorAction.isRetry(); eventDispatcher.loadError( - loadable.dataSpec, - loadable.getUri(), - loadable.getResponseHeaders(), + loadEventInfo, loadable.type, primaryTrackType, loadable.trackFormat, @@ -502,12 +532,10 @@ public class ChunkSampleStream implements SampleStream, S loadable.trackSelectionData, loadable.startTimeUs, loadable.endTimeUs, - elapsedRealtimeMs, - loadDurationMs, - bytesLoaded, error, canceled); if (canceled) { + loadErrorHandlingPolicy.onLoadTaskConcluded(loadable.loadTaskId); callback.onContinueLoadingRequested(this); } return loadErrorAction; @@ -533,7 +561,7 @@ public class ChunkSampleStream implements SampleStream, S } chunkSource.getNextChunk(positionUs, loadPositionUs, chunkQueue, nextChunkHolder); boolean endOfStream = nextChunkHolder.endOfStream; - Chunk loadable = nextChunkHolder.chunk; + @Nullable Chunk loadable = nextChunkHolder.chunk; nextChunkHolder.clear(); if (endOfStream) { @@ -554,22 +582,23 @@ public class ChunkSampleStream implements SampleStream, S decodeOnlyUntilPositionUs = resetToMediaChunk ? 0 : pendingResetPositionUs; pendingResetPositionUs = C.TIME_UNSET; } - mediaChunk.init(mediaChunkOutput); + mediaChunk.init(chunkOutput); mediaChunks.add(mediaChunk); + } else if (loadable instanceof InitializationChunk) { + ((InitializationChunk) loadable).init(chunkOutput); } long elapsedRealtimeMs = loader.startLoading( loadable, this, loadErrorHandlingPolicy.getMinimumLoadableRetryCount(loadable.type)); eventDispatcher.loadStarted( - loadable.dataSpec, + new LoadEventInfo(loadable.loadTaskId, loadable.dataSpec, elapsedRealtimeMs), loadable.type, primaryTrackType, loadable.trackFormat, loadable.trackSelectionReason, loadable.trackSelectionData, loadable.startTimeUs, - loadable.endTimeUs, - elapsedRealtimeMs); + loadable.endTimeUs); return true; } @@ -753,16 +782,13 @@ public class ChunkSampleStream implements SampleStream, S if (loadingFinished && positionUs > sampleQueue.getLargestQueuedTimestampUs()) { skipCount = sampleQueue.advanceToEnd(); } else { - skipCount = sampleQueue.advanceTo(positionUs, true, true); - if (skipCount == SampleQueue.ADVANCE_FAILED) { - skipCount = 0; - } + skipCount = sampleQueue.advanceTo(positionUs); } return skipCount; } @Override - public void maybeThrowError() throws IOException { + public void maybeThrowError() { // Do nothing. Errors will be thrown from the primary stream. } 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 ba7c0d0d5b..1b43af2084 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 @@ -15,12 +15,14 @@ */ package com.google.android.exoplayer2.source.chunk; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; 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.source.chunk.ChunkExtractorWrapper.TrackOutputProvider; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.util.Assertions; @@ -66,7 +68,7 @@ public class ContainerMediaChunk extends BaseMediaChunk { DataSpec dataSpec, Format trackFormat, int trackSelectionReason, - Object trackSelectionData, + @Nullable Object trackSelectionData, long startTimeUs, long endTimeUs, long clippedStartTimeUs, @@ -110,23 +112,22 @@ public class ContainerMediaChunk extends BaseMediaChunk { @SuppressWarnings("NonAtomicVolatileUpdate") @Override - public final void load() throws IOException, InterruptedException { - DataSpec loadDataSpec = dataSpec.subrange(nextLoadPosition); + public final void load() throws IOException { + if (nextLoadPosition == 0) { + // Configure the output and set it as the target for the extractor wrapper. + BaseMediaChunkOutput output = getOutput(); + output.setSampleOffsetUs(sampleOffsetUs); + extractorWrapper.init( + getTrackOutputProvider(output), + clippedStartTimeUs == C.TIME_UNSET ? C.TIME_UNSET : (clippedStartTimeUs - sampleOffsetUs), + clippedEndTimeUs == C.TIME_UNSET ? C.TIME_UNSET : (clippedEndTimeUs - sampleOffsetUs)); + } try { // Create and open the input. - ExtractorInput input = new DefaultExtractorInput(dataSource, - loadDataSpec.absoluteStreamPosition, dataSource.open(loadDataSpec)); - if (nextLoadPosition == 0) { - // Configure the output and set it as the target for the extractor wrapper. - BaseMediaChunkOutput output = getOutput(); - output.setSampleOffsetUs(sampleOffsetUs); - extractorWrapper.init( - getTrackOutputProvider(output), - clippedStartTimeUs == C.TIME_UNSET - ? C.TIME_UNSET - : (clippedStartTimeUs - sampleOffsetUs), - clippedEndTimeUs == C.TIME_UNSET ? C.TIME_UNSET : (clippedEndTimeUs - sampleOffsetUs)); - } + DataSpec loadDataSpec = dataSpec.subrange(nextLoadPosition); + ExtractorInput input = + new DefaultExtractorInput( + dataSource, loadDataSpec.position, dataSource.open(loadDataSpec)); // Load and decode the sample data. try { Extractor extractor = extractorWrapper.extractor; @@ -136,7 +137,7 @@ public class ContainerMediaChunk extends BaseMediaChunk { } Assertions.checkState(result != Extractor.RESULT_SEEK); } finally { - nextLoadPosition = input.getPosition() - dataSpec.absoluteStreamPosition; + nextLoadPosition = input.getPosition() - dataSpec.position; } } finally { Util.closeQuietly(dataSource); @@ -145,16 +146,13 @@ public class ContainerMediaChunk extends BaseMediaChunk { } /** - * Returns the {@link ChunkExtractorWrapper.TrackOutputProvider} to be used by the wrapped - * extractor. + * Returns the {@link TrackOutputProvider} to be used by the wrapped extractor. * * @param baseMediaChunkOutput The {@link BaseMediaChunkOutput} most recently passed to {@link * #init(BaseMediaChunkOutput)}. - * @return A {@link ChunkExtractorWrapper.TrackOutputProvider} to be used by the wrapped - * extractor. + * @return A {@link TrackOutputProvider} to be used by the wrapped extractor. */ - protected ChunkExtractorWrapper.TrackOutputProvider getTrackOutputProvider( - BaseMediaChunkOutput baseMediaChunkOutput) { + protected TrackOutputProvider getTrackOutputProvider(BaseMediaChunkOutput baseMediaChunkOutput) { return baseMediaChunkOutput; } } 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 f3bea8aeb5..6d97c1d92e 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 @@ -52,16 +52,16 @@ public abstract class DataChunk extends Chunk { Format trackFormat, int trackSelectionReason, @Nullable Object trackSelectionData, - byte[] data) { + @Nullable byte[] data) { super(dataSource, dataSpec, type, trackFormat, trackSelectionReason, trackSelectionData, C.TIME_UNSET, C.TIME_UNSET); - this.data = data; + this.data = data == null ? Util.EMPTY_BYTE_ARRAY : data; } /** * Returns the array in which the data is held. - *

      - * This method should be used for recycling the holder only, and not for reading the data. + * + *

      This method should be used for recycling the holder only, and not for reading the data. * * @return The array in which the data is held. */ @@ -77,7 +77,7 @@ public abstract class DataChunk extends Chunk { } @Override - public final void load() throws IOException, InterruptedException { + public final void load() throws IOException { try { dataSource.open(dataSpec); int limit = 0; @@ -108,9 +108,7 @@ public abstract class DataChunk extends Chunk { protected abstract void consume(byte[] data, int limit) throws IOException; private void maybeExpandData(int limit) { - if (data == null) { - data = new byte[READ_GRANULARITY]; - } else if (data.length < limit + READ_GRANULARITY) { + if (data.length < limit + READ_GRANULARITY) { // The new length is calculated as (data.length + READ_GRANULARITY) rather than // (limit + READ_GRANULARITY) in order to avoid small increments in the length. 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 37c70d5498..8b954af2f8 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 @@ -22,11 +22,13 @@ 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.source.chunk.ChunkExtractorWrapper.TrackOutputProvider; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Util; import java.io.IOException; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /** * A {@link Chunk} that uses an {@link Extractor} to decode initialization data for single track. @@ -37,6 +39,7 @@ public final class InitializationChunk extends Chunk { private final ChunkExtractorWrapper extractorWrapper; + private @MonotonicNonNull TrackOutputProvider trackOutputProvider; private long nextLoadPosition; private volatile boolean loadCanceled; @@ -60,6 +63,17 @@ public final class InitializationChunk extends Chunk { this.extractorWrapper = extractorWrapper; } + /** + * Initializes the chunk for loading, setting a {@link TrackOutputProvider} for track outputs to + * which formats will be written as they are loaded. + * + * @param trackOutputProvider The {@link TrackOutputProvider} for track outputs to which formats + * will be written as they are loaded. + */ + public void init(TrackOutputProvider trackOutputProvider) { + this.trackOutputProvider = trackOutputProvider; + } + // Loadable implementation. @Override @@ -69,18 +83,17 @@ public final class InitializationChunk extends Chunk { @SuppressWarnings("NonAtomicVolatileUpdate") @Override - public void load() throws IOException, InterruptedException { - DataSpec loadDataSpec = dataSpec.subrange(nextLoadPosition); + public void load() throws IOException { + if (nextLoadPosition == 0) { + extractorWrapper.init( + trackOutputProvider, /* startTimeUs= */ C.TIME_UNSET, /* endTimeUs= */ C.TIME_UNSET); + } try { // Create and open the input. - ExtractorInput input = new DefaultExtractorInput(dataSource, - loadDataSpec.absoluteStreamPosition, dataSource.open(loadDataSpec)); - if (nextLoadPosition == 0) { - extractorWrapper.init( - /* trackOutputProvider= */ null, - /* startTimeUs= */ C.TIME_UNSET, - /* endTimeUs= */ C.TIME_UNSET); - } + DataSpec loadDataSpec = dataSpec.subrange(nextLoadPosition); + ExtractorInput input = + new DefaultExtractorInput( + dataSource, loadDataSpec.position, dataSource.open(loadDataSpec)); // Load and decode the initialization data. try { Extractor extractor = extractorWrapper.extractor; @@ -90,11 +103,10 @@ public final class InitializationChunk extends Chunk { } Assertions.checkState(result != Extractor.RESULT_SEEK); } finally { - nextLoadPosition = input.getPosition() - dataSpec.absoluteStreamPosition; + nextLoadPosition = input.getPosition() - dataSpec.position; } } finally { Util.closeQuietly(dataSource); } } - } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/MediaChunkListIterator.java b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/MediaChunkListIterator.java deleted file mode 100644 index ca64e1affd..0000000000 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/MediaChunkListIterator.java +++ /dev/null @@ -1,61 +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.chunk; - -import com.google.android.exoplayer2.upstream.DataSpec; -import java.util.List; - -/** A {@link MediaChunkIterator} which iterates over a {@link List} of {@link MediaChunk}s. */ -public final class MediaChunkListIterator extends BaseMediaChunkIterator { - - private final List chunks; - private final boolean reverseOrder; - - /** - * Creates iterator. - * - * @param chunks The list of chunks to iterate over. - * @param reverseOrder Whether to iterate in reverse order. - */ - public MediaChunkListIterator(List chunks, boolean reverseOrder) { - super(0, chunks.size() - 1); - this.chunks = chunks; - this.reverseOrder = reverseOrder; - } - - @Override - public DataSpec getDataSpec() { - return getCurrentChunk().dataSpec; - } - - @Override - public long getChunkStartTimeUs() { - return getCurrentChunk().startTimeUs; - } - - @Override - public long getChunkEndTimeUs() { - return getCurrentChunk().endTimeUs; - } - - private MediaChunk getCurrentChunk() { - int index = (int) super.getCurrentIndex(); - if (reverseOrder) { - index = chunks.size() - 1 - index; - } - return chunks.get(index); - } -} 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 d53caf8e10..4e91e921d2 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 @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.source.chunk; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.extractor.DefaultExtractorInput; @@ -54,7 +55,7 @@ public final class SingleSampleMediaChunk extends BaseMediaChunk { DataSpec dataSpec, Format trackFormat, int trackSelectionReason, - Object trackSelectionData, + @Nullable Object trackSelectionData, long startTimeUs, long endTimeUs, long chunkIndex, @@ -90,20 +91,20 @@ public final class SingleSampleMediaChunk extends BaseMediaChunk { @SuppressWarnings("NonAtomicVolatileUpdate") @Override - public void load() throws IOException, InterruptedException { - DataSpec loadDataSpec = dataSpec.subrange(nextLoadPosition); + public void load() throws IOException { + BaseMediaChunkOutput output = getOutput(); + output.setSampleOffsetUs(0); + TrackOutput trackOutput = output.track(0, trackType); + trackOutput.format(sampleFormat); try { // Create and open the input. + DataSpec loadDataSpec = dataSpec.subrange(nextLoadPosition); long length = dataSource.open(loadDataSpec); if (length != C.LENGTH_UNSET) { length += nextLoadPosition; } ExtractorInput extractorInput = new DefaultExtractorInput(dataSource, nextLoadPosition, length); - BaseMediaChunkOutput output = getOutput(); - output.setSampleOffsetUs(0); - TrackOutput trackOutput = output.track(0, trackType); - trackOutput.format(sampleFormat); // Load the sample data. int result = 0; while (result != C.RESULT_END_OF_INPUT) { @@ -117,5 +118,4 @@ public final class SingleSampleMediaChunk extends BaseMediaChunk { } loadCompleted = true; } - } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/package-info.java b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/package-info.java new file mode 100644 index 0000000000..c57494dc1c --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2019 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. + */ +@NonNullApi +package com.google.android.exoplayer2.source.chunk; + +import com.google.android.exoplayer2.util.NonNullApi; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/package-info.java b/library/core/src/main/java/com/google/android/exoplayer2/source/package-info.java new file mode 100644 index 0000000000..adb05a46f2 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2019 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. + */ +@NonNullApi +package com.google.android.exoplayer2.source; + +import com.google.android.exoplayer2.util.NonNullApi; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/CaptionStyleCompat.java b/library/core/src/main/java/com/google/android/exoplayer2/text/CaptionStyleCompat.java index 51aec3638f..830185c0bd 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/CaptionStyleCompat.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/CaptionStyleCompat.java @@ -15,13 +15,13 @@ */ package com.google.android.exoplayer2.text; -import android.annotation.TargetApi; import android.graphics.Color; import android.graphics.Typeface; import android.view.accessibility.CaptioningManager; import android.view.accessibility.CaptioningManager.CaptionStyle; import androidx.annotation.IntDef; import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; import com.google.android.exoplayer2.util.Util; import java.lang.annotation.Documented; import java.lang.annotation.Retention; @@ -124,7 +124,7 @@ public final class CaptionStyleCompat { * @param captionStyle A {@link CaptionStyle}. * @return The equivalent {@link CaptionStyleCompat}. */ - @TargetApi(19) + @RequiresApi(19) public static CaptionStyleCompat createFromCaptionStyle( CaptioningManager.CaptionStyle captionStyle) { if (Util.SDK_INT >= 21) { @@ -159,7 +159,7 @@ public final class CaptionStyleCompat { this.typeface = typeface; } - @TargetApi(19) + @RequiresApi(19) @SuppressWarnings("ResourceType") private static CaptionStyleCompat createFromCaptionStyleV19( CaptioningManager.CaptionStyle captionStyle) { @@ -168,7 +168,7 @@ public final class CaptionStyleCompat { captionStyle.edgeType, captionStyle.edgeColor, captionStyle.getTypeface()); } - @TargetApi(21) + @RequiresApi(21) @SuppressWarnings("ResourceType") private static CaptionStyleCompat createFromCaptionStyleV21( CaptioningManager.CaptionStyle captionStyle) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/Cue.java b/library/core/src/main/java/com/google/android/exoplayer2/text/Cue.java index bd617ad626..3b84761c28 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/Cue.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/Cue.java @@ -17,17 +17,21 @@ package com.google.android.exoplayer2.text; import android.graphics.Bitmap; import android.graphics.Color; +import android.text.Layout; import android.text.Layout.Alignment; +import androidx.annotation.ColorInt; import androidx.annotation.IntDef; import androidx.annotation.Nullable; +import com.google.android.exoplayer2.util.Assertions; import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; -/** - * Contains information about a specific cue, including textual content and formatting data. - */ -public class Cue { +/** Contains information about a specific cue, including textual content and formatting data. */ +// This class shouldn't be sub-classed. If a subtitle format needs additional fields, either they +// should be generic enough to be added here, or the format-specific decoder should pass the +// information around in a sidecar object. +public final class Cue { /** The empty cue. */ public static final Cue EMPTY = new Cue(""); @@ -45,9 +49,7 @@ public class Cue { @IntDef({TYPE_UNSET, ANCHOR_TYPE_START, ANCHOR_TYPE_MIDDLE, ANCHOR_TYPE_END}) public @interface AnchorType {} - /** - * An unset anchor or line type value. - */ + /** An unset anchor, line, text size or vertical type value. */ public static final int TYPE_UNSET = Integer.MIN_VALUE; /** @@ -110,6 +112,25 @@ public class Cue { /** Text size is measured in number of pixels. */ public static final int TEXT_SIZE_TYPE_ABSOLUTE = 2; + /** + * The type of vertical layout for this cue, which may be unset (i.e. horizontal). One of {@link + * #TYPE_UNSET}, {@link #VERTICAL_TYPE_RL} or {@link #VERTICAL_TYPE_LR}. + */ + @Documented + @Retention(RetentionPolicy.SOURCE) + @IntDef({ + TYPE_UNSET, + VERTICAL_TYPE_RL, + VERTICAL_TYPE_LR, + }) + public @interface VerticalType {} + + /** Vertical right-to-left (e.g. for Japanese). */ + public static final int VERTICAL_TYPE_RL = 1; + + /** Vertical left-to-right (e.g. for Mongolian). */ + public static final int VERTICAL_TYPE_LR = 2; + /** * The cue text, or null if this is an image cue. Note the {@link CharSequence} may be decorated * with styling spans. @@ -124,37 +145,56 @@ public class Cue { /** * The position of the {@link #lineAnchor} of the cue box within the viewport in the direction - * orthogonal to the writing direction, or {@link #DIMEN_UNSET}. When set, the interpretation of - * the value depends on the value of {@link #lineType}. - *

      - * For horizontal text and {@link #lineType} equal to {@link #LINE_TYPE_FRACTION}, this is the - * fractional vertical position relative to the top of the viewport. + * orthogonal to the writing direction (determined by {@link #verticalType}), or {@link + * #DIMEN_UNSET}. When set, the interpretation of the value depends on the value of {@link + * #lineType}. + * + *

      The measurement direction depends on {@link #verticalType}: + * + *

        + *
      • For {@link #TYPE_UNSET} (i.e. horizontal), this is the vertical position relative to the + * top of the viewport. + *
      • For {@link #VERTICAL_TYPE_LR} this is the horizontal position relative to the left of the + * viewport. + *
      • For {@link #VERTICAL_TYPE_RL} this is the horizontal position relative to the right of + * the viewport. + *
      */ public final float line; /** * The type of the {@link #line} value. * - *

      {@link #LINE_TYPE_FRACTION} indicates that {@link #line} is a fractional position within the - * viewport. - * - *

      {@link #LINE_TYPE_NUMBER} indicates that {@link #line} is a line number, where the size of - * each line is taken to be the size of the first line of the cue. When {@link #line} is greater - * than or equal to 0 lines count from the start of the viewport, with 0 indicating zero offset - * from the start edge. When {@link #line} is negative lines count from the end of the viewport, - * with -1 indicating zero offset from the end edge. For horizontal text the line spacing is the - * height of the first line of the cue, and the start and end of the viewport are the top and - * bottom respectively. + *

        + *
      • {@link #LINE_TYPE_FRACTION} indicates that {@link #line} is a fractional position within + * the viewport. + *
      • {@link #LINE_TYPE_NUMBER} indicates that {@link #line} is a line number, where the size + * of each line is taken to be the size of the first line of the cue. + *
          + *
        • When {@link #line} is greater than or equal to 0 lines count from the start of the + * viewport, with 0 indicating zero offset from the start edge. When {@link #line} is + * negative lines count from the end of the viewport, with -1 indicating zero offset + * from the end edge. + *
        • For horizontal text the line spacing is the height of the first line of the cue, + * and the start and end of the viewport are the top and bottom respectively. + *
        + *
      * *

      Note that it's particularly important to consider the effect of {@link #lineAnchor} when - * using {@link #LINE_TYPE_NUMBER}. {@code (line == 0 && lineAnchor == ANCHOR_TYPE_START)} - * positions a (potentially multi-line) cue at the very top of the viewport. {@code (line == -1 && - * lineAnchor == ANCHOR_TYPE_END)} positions a (potentially multi-line) cue at the very bottom of - * the viewport. {@code (line == 0 && lineAnchor == ANCHOR_TYPE_END)} and {@code (line == -1 && - * lineAnchor == ANCHOR_TYPE_START)} position cues entirely outside of the viewport. {@code (line - * == 1 && lineAnchor == ANCHOR_TYPE_END)} positions a cue so that only the last line is visible - * at the top of the viewport. {@code (line == -2 && lineAnchor == ANCHOR_TYPE_START)} position a - * cue so that only its first line is visible at the bottom of the viewport. + * using {@link #LINE_TYPE_NUMBER}. + * + *

        + *
      • {@code (line == 0 && lineAnchor == ANCHOR_TYPE_START)} positions a (potentially + * multi-line) cue at the very start of the viewport. + *
      • {@code (line == -1 && lineAnchor == ANCHOR_TYPE_END)} positions a (potentially + * multi-line) cue at the very end of the viewport. + *
      • {@code (line == 0 && lineAnchor == ANCHOR_TYPE_END)} and {@code (line == -1 && lineAnchor + * == ANCHOR_TYPE_START)} position cues entirely outside of the viewport. + *
      • {@code (line == 1 && lineAnchor == ANCHOR_TYPE_END)} positions a cue so that only the + * last line is visible at the start of the viewport. + *
      • {@code (line == -2 && lineAnchor == ANCHOR_TYPE_START)} position a cue so that only its + * first line is visible at the end of the viewport. + *
      */ public final @LineType int lineType; @@ -171,10 +211,16 @@ public class Cue { /** * The fractional position of the {@link #positionAnchor} of the cue box within the viewport in * the direction orthogonal to {@link #line}, or {@link #DIMEN_UNSET}. - *

      - * For horizontal text, this is the horizontal position relative to the left of the viewport. Note - * that positioning is relative to the left of the viewport even in the case of right-to-left - * text. + * + *

      The measurement direction depends on {@link #verticalType}. + * + *

        + *
      • For {@link #TYPE_UNSET} (i.e. horizontal), this is the horizontal position relative to + * the left of the viewport. Note that positioning is relative to the left of the viewport + * even in the case of right-to-left text. + *
      • For {@link #VERTICAL_TYPE_LR} and {@link #VERTICAL_TYPE_RL} (i.e. vertical), this is the + * vertical position relative to the top of the viewport. + *
      */ public final float position; @@ -224,53 +270,19 @@ public class Cue { public final float textSize; /** - * Creates an image cue. - * - * @param bitmap See {@link #bitmap}. - * @param horizontalPosition The position of the horizontal anchor within the viewport, expressed - * as a fraction of the viewport width. - * @param horizontalPositionAnchor The horizontal anchor. One of {@link #ANCHOR_TYPE_START}, - * {@link #ANCHOR_TYPE_MIDDLE}, {@link #ANCHOR_TYPE_END} and {@link #TYPE_UNSET}. - * @param verticalPosition The position of the vertical anchor within the viewport, expressed as a - * fraction of the viewport height. - * @param verticalPositionAnchor The vertical anchor. One of {@link #ANCHOR_TYPE_START}, {@link - * #ANCHOR_TYPE_MIDDLE}, {@link #ANCHOR_TYPE_END} and {@link #TYPE_UNSET}. - * @param width The width of the cue as a fraction of the viewport width. - * @param height The height of the cue as a fraction of the viewport height, or {@link - * #DIMEN_UNSET} if the bitmap should be displayed at its natural height for the specified - * {@code width}. + * The vertical formatting of this Cue, or {@link #TYPE_UNSET} if the cue has no vertical setting + * (and so should be horizontal). */ - public Cue( - Bitmap bitmap, - float horizontalPosition, - @AnchorType int horizontalPositionAnchor, - float verticalPosition, - @AnchorType int verticalPositionAnchor, - float width, - float height) { - this( - /* text= */ null, - /* textAlignment= */ null, - bitmap, - verticalPosition, - /* lineType= */ LINE_TYPE_FRACTION, - verticalPositionAnchor, - horizontalPosition, - horizontalPositionAnchor, - /* textSizeType= */ TYPE_UNSET, - /* textSize= */ DIMEN_UNSET, - width, - height, - /* windowColorSet= */ false, - /* windowColor= */ Color.BLACK); - } + public final @VerticalType int verticalType; /** * Creates a text cue whose {@link #textAlignment} is null, whose type parameters are set to * {@link #TYPE_UNSET} and whose dimension parameters are set to {@link #DIMEN_UNSET}. * * @param text See {@link #text}. + * @deprecated Use {@link Builder}. */ + @Deprecated public Cue(CharSequence text) { this( text, @@ -294,7 +306,9 @@ public class Cue { * @param position See {@link #position}. * @param positionAnchor See {@link #positionAnchor}. * @param size See {@link #size}. + * @deprecated Use {@link Builder}. */ + @Deprecated public Cue( CharSequence text, @Nullable Alignment textAlignment, @@ -330,10 +344,12 @@ public class Cue { * @param size See {@link #size}. * @param textSizeType See {@link #textSizeType}. * @param textSize See {@link #textSize}. + * @deprecated Use {@link Builder}. */ + @Deprecated public Cue( CharSequence text, - Alignment textAlignment, + @Nullable Alignment textAlignment, float line, @LineType int lineType, @AnchorType int lineAnchor, @@ -356,7 +372,8 @@ public class Cue { size, /* bitmapHeight= */ DIMEN_UNSET, /* windowColorSet= */ false, - /* windowColor= */ Color.BLACK); + /* windowColor= */ Color.BLACK, + /* verticalType= */ TYPE_UNSET); } /** @@ -372,7 +389,9 @@ public class Cue { * @param size See {@link #size}. * @param windowColorSet See {@link #windowColorSet}. * @param windowColor See {@link #windowColor}. + * @deprecated Use {@link Builder}. */ + @Deprecated public Cue( CharSequence text, @Nullable Alignment textAlignment, @@ -398,7 +417,8 @@ public class Cue { size, /* bitmapHeight= */ DIMEN_UNSET, windowColorSet, - windowColor); + windowColor, + /* verticalType= */ TYPE_UNSET); } private Cue( @@ -415,7 +435,14 @@ public class Cue { float size, float bitmapHeight, boolean windowColorSet, - int windowColor) { + int windowColor, + @VerticalType int verticalType) { + // Exactly one of text or bitmap should be set. + if (text == null) { + Assertions.checkNotNull(bitmap); + } else { + Assertions.checkArgument(bitmap == null); + } this.text = text; this.textAlignment = textAlignment; this.bitmap = bitmap; @@ -430,6 +457,413 @@ public class Cue { this.windowColor = windowColor; this.textSizeType = textSizeType; this.textSize = textSize; + this.verticalType = verticalType; } + /** Returns a new {@link Cue.Builder} initialized with the same values as this Cue. */ + public Builder buildUpon() { + return new Cue.Builder(this); + } + + /** A builder for {@link Cue} objects. */ + public static final class Builder { + @Nullable private CharSequence text; + @Nullable private Bitmap bitmap; + @Nullable private Alignment textAlignment; + private float line; + @LineType private int lineType; + @AnchorType private int lineAnchor; + private float position; + @AnchorType private int positionAnchor; + @TextSizeType private int textSizeType; + private float textSize; + private float size; + private float bitmapHeight; + private boolean windowColorSet; + @ColorInt private int windowColor; + @VerticalType private int verticalType; + + public Builder() { + text = null; + bitmap = null; + textAlignment = null; + line = DIMEN_UNSET; + lineType = TYPE_UNSET; + lineAnchor = TYPE_UNSET; + position = DIMEN_UNSET; + positionAnchor = TYPE_UNSET; + textSizeType = TYPE_UNSET; + textSize = DIMEN_UNSET; + size = DIMEN_UNSET; + bitmapHeight = DIMEN_UNSET; + windowColorSet = false; + windowColor = Color.BLACK; + verticalType = TYPE_UNSET; + } + + private Builder(Cue cue) { + text = cue.text; + bitmap = cue.bitmap; + textAlignment = cue.textAlignment; + line = cue.line; + lineType = cue.lineType; + lineAnchor = cue.lineAnchor; + position = cue.position; + positionAnchor = cue.positionAnchor; + textSizeType = cue.textSizeType; + textSize = cue.textSize; + size = cue.size; + bitmapHeight = cue.bitmapHeight; + windowColorSet = cue.windowColorSet; + windowColor = cue.windowColor; + verticalType = cue.verticalType; + } + + /** + * Sets the cue text. + * + *

      Note that {@code text} may be decorated with styling spans. + * + * @see Cue#text + */ + public Builder setText(CharSequence text) { + this.text = text; + return this; + } + + /** + * Gets the cue text. + * + * @see Cue#text + */ + @Nullable + public CharSequence getText() { + return text; + } + + /** + * Sets the cue image. + * + * @see Cue#bitmap + */ + public Builder setBitmap(Bitmap bitmap) { + this.bitmap = bitmap; + return this; + } + + /** + * Gets the cue image. + * + * @see Cue#bitmap + */ + @Nullable + public Bitmap getBitmap() { + return bitmap; + } + + /** + * Sets the alignment of the cue text within the cue box. + * + *

      Passing null means the alignment is undefined. + * + * @see Cue#textAlignment + */ + public Builder setTextAlignment(@Nullable Layout.Alignment textAlignment) { + this.textAlignment = textAlignment; + return this; + } + + /** + * Gets the alignment of the cue text within the cue box, or null if the alignment is undefined. + * + * @see Cue#textAlignment + */ + @Nullable + public Alignment getTextAlignment() { + return textAlignment; + } + + /** + * Sets the position of the {@code lineAnchor} of the cue box within the viewport in the + * direction orthogonal to the writing direction. + * + *

      The interpretation of the {@code line} depends on the value of {@code lineType}. + * + *

        + *
      • {@link #LINE_TYPE_FRACTION} indicates that {@code line} is a fractional position within + * the viewport. + *
      • {@link #LINE_TYPE_NUMBER} indicates that {@code line} is a line number, where the size + * of each line is taken to be the size of the first line of the cue. + *
          + *
        • When {@code line} is greater than or equal to 0 lines count from the start of the + * viewport, with 0 indicating zero offset from the start edge. + *
        • When {@code line} is negative lines count from the end of the viewport, with -1 + * indicating zero offset from the end edge. + *
        • For horizontal text the line spacing is the height of the first line of the cue, + * and the start and end of the viewport are the top and bottom respectively. + *
        + *
      + * + *

      Note that it's particularly important to consider the effect of {@link #setLineAnchor(int) + * lineAnchor} when using {@link #LINE_TYPE_NUMBER}. + * + *

        + *
      • {@code (line == 0 && lineAnchor == ANCHOR_TYPE_START)} positions a (potentially + * multi-line) cue at the very start of the viewport. + *
      • {@code (line == -1 && lineAnchor == ANCHOR_TYPE_END)} positions a (potentially + * multi-line) cue at the very end of the viewport. + *
      • {@code (line == 0 && lineAnchor == ANCHOR_TYPE_END)} and {@code (line == -1 && + * lineAnchor == ANCHOR_TYPE_START)} position cues entirely outside of the viewport. + *
      • {@code (line == 1 && lineAnchor == ANCHOR_TYPE_END)} positions a cue so that only the + * last line is visible at the start of the viewport. + *
      • {@code (line == -2 && lineAnchor == ANCHOR_TYPE_START)} position a cue so that only its + * first line is visible at the end of the viewport. + *
      + * + * @see Cue#line + * @see Cue#lineType + */ + public Builder setLine(float line, @LineType int lineType) { + this.line = line; + this.lineType = lineType; + return this; + } + + /** + * Gets the position of the {@code lineAnchor} of the cue box within the viewport in the + * direction orthogonal to the writing direction. + * + * @see Cue#line + */ + public float getLine() { + return line; + } + + /** + * Gets the type of the value of {@link #getLine()}. + * + * @see Cue#lineType + */ + @LineType + public int getLineType() { + return lineType; + } + + /** + * Sets the cue box anchor positioned by {@link #setLine(float, int) line}. + * + *

      For the normal case of horizontal text, {@link #ANCHOR_TYPE_START}, {@link + * #ANCHOR_TYPE_MIDDLE} and {@link #ANCHOR_TYPE_END} correspond to the top, middle and bottom of + * the cue box respectively. + * + * @see Cue#lineAnchor + */ + public Builder setLineAnchor(@AnchorType int lineAnchor) { + this.lineAnchor = lineAnchor; + return this; + } + + /** + * Gets the cue box anchor positioned by {@link #setLine(float, int) line}. + * + * @see Cue#lineAnchor + */ + @AnchorType + public int getLineAnchor() { + return lineAnchor; + } + + /** + * Sets the fractional position of the {@link #setPositionAnchor(int) positionAnchor} of the cue + * box within the viewport in the direction orthogonal to {@link #setLine(float, int) line}. + * + *

      For horizontal text, this is the horizontal position relative to the left of the viewport. + * Note that positioning is relative to the left of the viewport even in the case of + * right-to-left text. + * + * @see Cue#position + */ + public Builder setPosition(float position) { + this.position = position; + return this; + } + + /** + * Gets the fractional position of the {@link #setPositionAnchor(int) positionAnchor} of the cue + * box within the viewport in the direction orthogonal to {@link #setLine(float, int) line}. + * + * @see Cue#position + */ + public float getPosition() { + return position; + } + + /** + * Sets the cue box anchor positioned by {@link #setPosition(float) position}. + * + *

      For the normal case of horizontal text, {@link #ANCHOR_TYPE_START}, {@link + * #ANCHOR_TYPE_MIDDLE} and {@link #ANCHOR_TYPE_END} correspond to the left, middle and right of + * the cue box respectively. + * + * @see Cue#positionAnchor + */ + public Builder setPositionAnchor(@AnchorType int positionAnchor) { + this.positionAnchor = positionAnchor; + return this; + } + + /** + * Gets the cue box anchor positioned by {@link #setPosition(float) position}. + * + * @see Cue#positionAnchor + */ + @AnchorType + public int getPositionAnchor() { + return positionAnchor; + } + + /** + * Sets the default text size and type for this cue's text. + * + * @see Cue#textSize + * @see Cue#textSizeType + */ + public Builder setTextSize(float textSize, @TextSizeType int textSizeType) { + this.textSize = textSize; + this.textSizeType = textSizeType; + return this; + } + + /** + * Gets the default text size type for this cue's text. + * + * @see Cue#textSizeType + */ + @TextSizeType + public int getTextSizeType() { + return textSizeType; + } + + /** + * Gets the default text size for this cue's text. + * + * @see Cue#textSize + */ + public float getTextSize() { + return textSize; + } + + /** + * Sets the size of the cue box in the writing direction specified as a fraction of the viewport + * size in that direction. + * + * @see Cue#size + */ + public Builder setSize(float size) { + this.size = size; + return this; + } + + /** + * Gets the size of the cue box in the writing direction specified as a fraction of the viewport + * size in that direction. + * + * @see Cue#size + */ + public float getSize() { + return size; + } + + /** + * Sets the bitmap height as a fraction of the viewport size. + * + * @see Cue#bitmapHeight + */ + public Builder setBitmapHeight(float bitmapHeight) { + this.bitmapHeight = bitmapHeight; + return this; + } + + /** + * Gets the bitmap height as a fraction of the viewport size. + * + * @see Cue#bitmapHeight + */ + public float getBitmapHeight() { + return bitmapHeight; + } + + /** + * Sets the fill color of the window. + * + *

      Also sets {@link Cue#windowColorSet} to true. + * + * @see Cue#windowColor + * @see Cue#windowColorSet + */ + public Builder setWindowColor(@ColorInt int windowColor) { + this.windowColor = windowColor; + this.windowColorSet = true; + return this; + } + + /** + * Returns true if the fill color of the window is set. + * + * @see Cue#windowColorSet + */ + public boolean isWindowColorSet() { + return windowColorSet; + } + + /** + * Gets the fill color of the window. + * + * @see Cue#windowColor + */ + @ColorInt + public int getWindowColor() { + return windowColor; + } + + /** + * Sets the vertical formatting for this Cue. + * + * @see Cue#verticalType + */ + public Builder setVerticalType(@VerticalType int verticalType) { + this.verticalType = verticalType; + return this; + } + + /** + * Gets the vertical formatting for this Cue. + * + * @see Cue#verticalType + */ + @VerticalType + public int getVerticalType() { + return verticalType; + } + + /** Build the cue. */ + public Cue build() { + return new Cue( + text, + textAlignment, + bitmap, + line, + lineType, + lineAnchor, + position, + positionAnchor, + textSizeType, + textSize, + size, + bitmapHeight, + windowColorSet, + windowColor, + verticalType); + } + } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/SimpleSubtitleDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/text/SimpleSubtitleDecoder.java index bd561afaf8..7987c8b5d6 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/SimpleSubtitleDecoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/SimpleSubtitleDecoder.java @@ -18,6 +18,7 @@ package com.google.android.exoplayer2.text; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.decoder.SimpleDecoder; +import com.google.android.exoplayer2.util.Assertions; import java.nio.ByteBuffer; /** @@ -29,9 +30,8 @@ public abstract class SimpleSubtitleDecoder extends private final String name; - /** - * @param name The name of the decoder. - */ + /** @param name The name of the decoder. */ + @SuppressWarnings("nullness:method.invocation.invalid") protected SimpleSubtitleDecoder(String name) { super(new SubtitleInputBuffer[2], new SubtitleOutputBuffer[2]); this.name = name; @@ -55,7 +55,7 @@ public abstract class SimpleSubtitleDecoder extends @Override protected final SubtitleOutputBuffer createOutputBuffer() { - return new SimpleSubtitleOutputBuffer(this); + return new SimpleSubtitleOutputBuffer(this::releaseOutputBuffer); } @Override @@ -63,18 +63,13 @@ public abstract class SimpleSubtitleDecoder extends return new SubtitleDecoderException("Unexpected decode error", error); } - @Override - protected final void releaseOutputBuffer(SubtitleOutputBuffer buffer) { - super.releaseOutputBuffer(buffer); - } - @SuppressWarnings("ByteBufferBackingArray") @Override @Nullable protected final SubtitleDecoderException decode( SubtitleInputBuffer inputBuffer, SubtitleOutputBuffer outputBuffer, boolean reset) { try { - ByteBuffer inputData = inputBuffer.data; + ByteBuffer inputData = Assertions.checkNotNull(inputBuffer.data); Subtitle subtitle = decode(inputData.array(), inputData.limit(), reset); outputBuffer.setContent(inputBuffer.timeUs, subtitle, inputBuffer.subsampleOffsetUs); // Clear BUFFER_FLAG_DECODE_ONLY (see [Internal: b/27893809]). diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/SimpleSubtitleOutputBuffer.java b/library/core/src/main/java/com/google/android/exoplayer2/text/SimpleSubtitleOutputBuffer.java index b2c25631f4..6807661c84 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/SimpleSubtitleOutputBuffer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/SimpleSubtitleOutputBuffer.java @@ -20,12 +20,10 @@ package com.google.android.exoplayer2.text; */ /* package */ final class SimpleSubtitleOutputBuffer extends SubtitleOutputBuffer { - private final SimpleSubtitleDecoder owner; + private final Owner owner; - /** - * @param owner The decoder that owns this buffer. - */ - public SimpleSubtitleOutputBuffer(SimpleSubtitleDecoder owner) { + /** @param owner The decoder that owns this buffer. */ + public SimpleSubtitleOutputBuffer(Owner owner) { super(); this.owner = owner; } 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 927ee8be5e..bd652c6586 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 @@ -108,7 +108,10 @@ public interface SubtitleDecoderFactory { return new Tx3gDecoder(format.initializationData); case MimeTypes.APPLICATION_CEA608: case MimeTypes.APPLICATION_MP4CEA608: - return new Cea608Decoder(mimeType, format.accessibilityChannel); + return new Cea608Decoder( + mimeType, + format.accessibilityChannel, + Cea608Decoder.MIN_DATA_CHANNEL_TIMEOUT_MS); case MimeTypes.APPLICATION_CEA708: return new Cea708Decoder(format.accessibilityChannel, format.initializationData); case MimeTypes.APPLICATION_DVBSUBS: diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/SubtitleOutputBuffer.java b/library/core/src/main/java/com/google/android/exoplayer2/text/SubtitleOutputBuffer.java index 1dcdecf95f..63b997a613 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/SubtitleOutputBuffer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/SubtitleOutputBuffer.java @@ -65,9 +65,6 @@ public abstract class SubtitleOutputBuffer extends OutputBuffer implements Subti return Assertions.checkNotNull(subtitle).getCues(timeUs - subsampleOffsetUs); } - @Override - public abstract void release(); - @Override public void clear() { super.clear(); 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 1622d68d99..b8b4d7de6e 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 @@ -23,10 +23,12 @@ import androidx.annotation.IntDef; import androidx.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.RendererCapabilities; +import com.google.android.exoplayer2.source.SampleStream; import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.Util; import java.lang.annotation.Documented; @@ -44,6 +46,8 @@ import java.util.List; */ public final class TextRenderer extends BaseRenderer implements Callback { + private static final String TAG = "TextRenderer"; + @Documented @Retention(RetentionPolicy.SOURCE) @IntDef({ @@ -79,11 +83,11 @@ public final class TextRenderer extends BaseRenderer implements Callback { private boolean inputStreamEnded; private boolean outputStreamEnded; @ReplacementState private int decoderReplacementState; - private Format streamFormat; - private SubtitleDecoder decoder; - private SubtitleInputBuffer nextInputBuffer; - private SubtitleOutputBuffer subtitle; - private SubtitleOutputBuffer nextSubtitle; + @Nullable private Format streamFormat; + @Nullable private SubtitleDecoder decoder; + @Nullable private SubtitleInputBuffer nextInputBuffer; + @Nullable private SubtitleOutputBuffer subtitle; + @Nullable private SubtitleOutputBuffer nextSubtitle; private int nextSubtitleEventIndex; /** @@ -118,18 +122,25 @@ public final class TextRenderer extends BaseRenderer implements Callback { } @Override + public String getName() { + return TAG; + } + + @Override + @Capabilities public int supportsFormat(Format format) { if (decoderFactory.supportsFormat(format)) { - return supportsFormatDrm(null, format.drmInitData) ? FORMAT_HANDLED : FORMAT_UNSUPPORTED_DRM; + return RendererCapabilities.create( + format.drmInitData == null ? FORMAT_HANDLED : FORMAT_UNSUPPORTED_DRM); } else if (MimeTypes.isText(format.sampleMimeType)) { - return FORMAT_UNSUPPORTED_SUBTYPE; + return RendererCapabilities.create(FORMAT_UNSUPPORTED_SUBTYPE); } else { - return FORMAT_UNSUPPORTED_TYPE; + return RendererCapabilities.create(FORMAT_UNSUPPORTED_TYPE); } } @Override - protected void onStreamChanged(Format[] formats, long offsetUs) throws ExoPlaybackException { + protected void onStreamChanged(Format[] formats, long offsetUs) { streamFormat = formats[0]; if (decoder != null) { decoderReplacementState = REPLACEMENT_STATE_SIGNAL_END_OF_STREAM; @@ -140,19 +151,13 @@ public final class TextRenderer extends BaseRenderer implements Callback { @Override protected void onPositionReset(long positionUs, boolean joining) { - clearOutput(); inputStreamEnded = false; outputStreamEnded = false; - if (decoderReplacementState != REPLACEMENT_STATE_NONE) { - replaceDecoder(); - } else { - releaseBuffers(); - decoder.flush(); - } + resetOutputAndDecoder(); } @Override - public void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException { + public void render(long positionUs, long elapsedRealtimeUs) { if (outputStreamEnded) { return; } @@ -162,7 +167,8 @@ public final class TextRenderer extends BaseRenderer implements Callback { try { nextSubtitle = decoder.dequeueOutputBuffer(); } catch (SubtitleDecoderException e) { - throw ExoPlaybackException.createForRenderer(e, getIndex()); + handleDecoderError(e); + return; } } @@ -229,7 +235,7 @@ public final class TextRenderer extends BaseRenderer implements Callback { return; } // Try and read the next subtitle from the source. - int result = readSource(formatHolder, nextInputBuffer, false); + @SampleStream.ReadDataResult int result = readSource(formatHolder, nextInputBuffer, false); if (result == C.RESULT_BUFFER_READ) { if (nextInputBuffer.isEndOfStream()) { inputStreamEnded = true; @@ -244,7 +250,8 @@ public final class TextRenderer extends BaseRenderer implements Callback { } } } catch (SubtitleDecoderException e) { - throw ExoPlaybackException.createForRenderer(e, getIndex()); + handleDecoderError(e); + return; } } @@ -326,4 +333,24 @@ public final class TextRenderer extends BaseRenderer implements Callback { output.onCues(cues); } + /** + * Called when {@link #decoder} throws an exception, so it can be logged and playback can + * continue. + * + *

      Logs {@code e} and resets state to allow decoding the next sample. + */ + private void handleDecoderError(SubtitleDecoderException e) { + Log.e(TAG, "Subtitle decoding failed. streamFormat=" + streamFormat, e); + resetOutputAndDecoder(); + } + + private void resetOutputAndDecoder() { + clearOutput(); + if (decoderReplacementState != REPLACEMENT_STATE_NONE) { + replaceDecoder(); + } else { + releaseBuffers(); + decoder.flush(); + } + } } 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 5a14063aa1..75e86c4113 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 @@ -24,23 +24,34 @@ import android.text.Spanned; import android.text.style.ForegroundColorSpan; import android.text.style.StyleSpan; import android.text.style.UnderlineSpan; +import androidx.annotation.Nullable; 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; import com.google.android.exoplayer2.text.SubtitleDecoder; +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 com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.ParsableByteArray; +import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import org.checkerframework.checker.nullness.compatqual.NullableType; -/** - * A {@link SubtitleDecoder} for CEA-608 (also known as "line 21 captions" and "EIA-608"). - */ +/** A {@link SubtitleDecoder} for CEA-608 (also known as "line 21 captions" and "EIA-608"). */ public final class Cea608Decoder extends CeaDecoder { + /** + * The minimum value for the {@code validDataChannelTimeoutMs} constructor parameter permitted by + * ANSI/CTA-608-E R-2014 Annex C.9. + */ + public static final long MIN_DATA_CHANNEL_TIMEOUT_MS = 16_000; + private static final String TAG = "Cea608Decoder"; private static final int CC_VALID_FLAG = 0x04; @@ -233,11 +244,12 @@ public final class Cea608Decoder extends CeaDecoder { private final int packetLength; private final int selectedField; private final int selectedChannel; + private final long validDataChannelTimeoutUs; private final ArrayList cueBuilders; private CueBuilder currentCueBuilder; - private List cues; - private List lastCues; + @Nullable private List cues; + @Nullable private List lastCues; private int captionMode; private int captionRowCount; @@ -253,11 +265,26 @@ public final class Cea608Decoder extends CeaDecoder { // service bytes and drops the rest. private boolean isInCaptionService; - public Cea608Decoder(String mimeType, int accessibilityChannel) { + private long lastCueUpdateUs; + + /** + * Constructs an instance. + * + * @param mimeType The MIME type of the CEA-608 data. + * @param accessibilityChannel The Accessibility channel, or {@link + * com.google.android.exoplayer2.Format#NO_VALUE} if unknown. + * @param validDataChannelTimeoutMs The timeout (in milliseconds) permitted by ANSI/CTA-608-E + * R-2014 Annex C.9 to clear "stuck" captions where no removal control code is received. The + * timeout should be at least {@link #MIN_DATA_CHANNEL_TIMEOUT_MS} or {@link C#TIME_UNSET} for + * no timeout. + */ + public Cea608Decoder(String mimeType, int accessibilityChannel, long validDataChannelTimeoutMs) { ccData = new ParsableByteArray(); cueBuilders = new ArrayList<>(); currentCueBuilder = new CueBuilder(CC_MODE_UNKNOWN, DEFAULT_CAPTIONS_ROW_COUNT); currentChannel = NTSC_CC_CHANNEL_1; + this.validDataChannelTimeoutUs = + validDataChannelTimeoutMs > 0 ? validDataChannelTimeoutMs * 1000 : C.TIME_UNSET; packetLength = MimeTypes.APPLICATION_MP4CEA608.equals(mimeType) ? 2 : 3; switch (accessibilityChannel) { case 1: @@ -285,6 +312,7 @@ public final class Cea608Decoder extends CeaDecoder { setCaptionMode(CC_MODE_UNKNOWN); resetCueBuilders(); isInCaptionService = true; + lastCueUpdateUs = C.TIME_UNSET; } @Override @@ -306,6 +334,7 @@ public final class Cea608Decoder extends CeaDecoder { repeatableControlCc2 = 0; currentChannel = NTSC_CC_CHANNEL_1; isInCaptionService = true; + lastCueUpdateUs = C.TIME_UNSET; } @Override @@ -313,6 +342,26 @@ public final class Cea608Decoder extends CeaDecoder { // Do nothing } + @Nullable + @Override + public SubtitleOutputBuffer dequeueOutputBuffer() throws SubtitleDecoderException { + SubtitleOutputBuffer outputBuffer = super.dequeueOutputBuffer(); + if (outputBuffer != null) { + return outputBuffer; + } + if (shouldClearStuckCaptions()) { + outputBuffer = getAvailableOutputBuffer(); + if (outputBuffer != null) { + cues = Collections.emptyList(); + lastCueUpdateUs = C.TIME_UNSET; + Subtitle subtitle = createSubtitle(); + outputBuffer.setContent(getPositionUs(), subtitle, Format.OFFSET_SAMPLE_RELATIVE); + return outputBuffer; + } + } + return null; + } + @Override protected boolean isNewSubtitleDataAvailable() { return cues != lastCues; @@ -321,13 +370,14 @@ public final class Cea608Decoder extends CeaDecoder { @Override protected Subtitle createSubtitle() { lastCues = cues; - return new CeaSubtitle(cues); + return new CeaSubtitle(Assertions.checkNotNull(cues)); } @SuppressWarnings("ByteBufferBackingArray") @Override protected void decode(SubtitleInputBuffer inputBuffer) { - ccData.reset(inputBuffer.data.array(), inputBuffer.data.limit()); + ByteBuffer subtitleData = Assertions.checkNotNull(inputBuffer.data); + ccData.reset(subtitleData.array(), subtitleData.limit()); boolean captionDataProcessed = false; while (ccData.bytesLeft() >= packetLength) { byte ccHeader = packetLength == 2 ? CC_IMPLICIT_DATA_HEADER @@ -418,6 +468,7 @@ public final class Cea608Decoder extends CeaDecoder { if (captionDataProcessed) { if (captionMode == CC_MODE_ROLL_UP || captionMode == CC_MODE_PAINT_ON) { cues = getDisplayCues(); + lastCueUpdateUs = getPositionUs(); } } } @@ -572,9 +623,9 @@ public final class Cea608Decoder extends CeaDecoder { // preference, then middle alignment, then end alignment. @Cue.AnchorType int positionAnchor = Cue.ANCHOR_TYPE_END; int cueBuilderCount = cueBuilders.size(); - List cueBuilderCues = new ArrayList<>(cueBuilderCount); + List<@NullableType Cue> cueBuilderCues = new ArrayList<>(cueBuilderCount); for (int i = 0; i < cueBuilderCount; i++) { - Cue cue = cueBuilders.get(i).build(/* forcedPositionAnchor= */ Cue.TYPE_UNSET); + @Nullable Cue cue = cueBuilders.get(i).build(/* forcedPositionAnchor= */ Cue.TYPE_UNSET); cueBuilderCues.add(cue); if (cue != null) { positionAnchor = Math.min(positionAnchor, cue.positionAnchor); @@ -584,10 +635,11 @@ public final class Cea608Decoder extends CeaDecoder { // Skip null cues and rebuild any that don't have the preferred alignment. List displayCues = new ArrayList<>(cueBuilderCount); for (int i = 0; i < cueBuilderCount; i++) { - Cue cue = cueBuilderCues.get(i); + @Nullable Cue cue = cueBuilderCues.get(i); if (cue != null) { if (cue.positionAnchor != positionAnchor) { - cue = cueBuilders.get(i).build(positionAnchor); + // The last time we built this cue it was non-null, it will be non-null this time too. + cue = Assertions.checkNotNull(cueBuilders.get(i).build(positionAnchor)); } displayCues.add(cue); } @@ -745,7 +797,7 @@ public final class Cea608Decoder extends CeaDecoder { return (cc1 & 0xF7) == 0x14; } - private static class CueBuilder { + private static final class CueBuilder { // 608 captions define a 15 row by 32 column screen grid. These constants convert from 608 // positions to normalized screen position. @@ -767,7 +819,7 @@ public final class Cea608Decoder extends CeaDecoder { rolledUpCaptions = new ArrayList<>(); captionStringBuilder = new StringBuilder(); reset(captionMode); - setCaptionRowCount(captionRowCount); + this.captionRowCount = captionRowCount; } public void reset(int captionMode) { @@ -829,6 +881,7 @@ public final class Cea608Decoder extends CeaDecoder { } } + @Nullable public Cue build(@Cue.AnchorType int forcedPositionAnchor) { SpannableStringBuilder cueString = new SpannableStringBuilder(); // Add any rolled up captions, separated by new lines. @@ -1011,4 +1064,12 @@ public final class Cea608Decoder extends CeaDecoder { } + /** See ANSI/CTA-608-E R-2014 Annex C.9 for Caption Erase Logic. */ + private boolean shouldClearStuckCaptions() { + if (validDataChannelTimeoutUs == C.TIME_UNSET || lastCueUpdateUs == C.TIME_UNSET) { + return false; + } + long elapsedUs = getPositionUs() - lastCueUpdateUs; + return elapsedUs >= validDataChannelTimeoutUs; + } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/cea/Cea708Cue.java b/library/core/src/main/java/com/google/android/exoplayer2/text/cea/Cea708Cue.java deleted file mode 100644 index fc1f0e2bdc..0000000000 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/cea/Cea708Cue.java +++ /dev/null @@ -1,68 +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.text.cea; - -import android.text.Layout.Alignment; -import androidx.annotation.NonNull; -import com.google.android.exoplayer2.text.Cue; - -/** - * A {@link Cue} for CEA-708. - */ -/* package */ final class Cea708Cue extends Cue implements Comparable { - - /** - * An unset priority. - */ - public static final int PRIORITY_UNSET = -1; - - /** - * The priority of the cue box. - */ - public final int priority; - - /** - * @param text See {@link #text}. - * @param textAlignment See {@link #textAlignment}. - * @param line See {@link #line}. - * @param lineType See {@link #lineType}. - * @param lineAnchor See {@link #lineAnchor}. - * @param position See {@link #position}. - * @param positionAnchor See {@link #positionAnchor}. - * @param size See {@link #size}. - * @param windowColorSet See {@link #windowColorSet}. - * @param windowColor See {@link #windowColor}. - * @param priority See (@link #priority}. - */ - public Cea708Cue(CharSequence text, Alignment textAlignment, float line, @LineType int lineType, - @AnchorType int lineAnchor, float position, @AnchorType int positionAnchor, float size, - boolean windowColorSet, int windowColor, int priority) { - super(text, textAlignment, line, lineType, lineAnchor, position, positionAnchor, size, - windowColorSet, windowColor); - this.priority = priority; - } - - @Override - public int compareTo(@NonNull Cea708Cue other) { - if (other.priority < priority) { - return -1; - } else if (other.priority > priority) { - return 1; - } - return 0; - } - -} 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 b3be88b851..182fe7a2fe 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 @@ -25,6 +25,7 @@ import android.text.style.BackgroundColorSpan; import android.text.style.ForegroundColorSpan; import android.text.style.StyleSpan; import android.text.style.UnderlineSpan; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.text.Cue; @@ -33,12 +34,15 @@ import com.google.android.exoplayer2.text.Subtitle; import com.google.android.exoplayer2.text.SubtitleDecoder; import com.google.android.exoplayer2.text.SubtitleInputBuffer; import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.CodecSpecificDataUtil; import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.ParsableBitArray; import com.google.android.exoplayer2.util.ParsableByteArray; +import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import org.checkerframework.checker.nullness.qual.RequiresNonNull; /** * A {@link SubtitleDecoder} for CEA-708 (also known as "EIA-708"). @@ -141,29 +145,34 @@ public final class Cea708Decoder extends CeaDecoder { private final ParsableByteArray ccData; private final ParsableBitArray serviceBlockPacket; + // TODO: Use isWideAspectRatio in decoding. + @SuppressWarnings({"unused", "FieldCanBeLocal"}) + private final boolean isWideAspectRatio; private final int selectedServiceNumber; - private final CueBuilder[] cueBuilders; + private final CueInfoBuilder[] cueInfoBuilders; - private CueBuilder currentCueBuilder; - private List cues; - private List lastCues; + private CueInfoBuilder currentCueInfoBuilder; + @Nullable private List cues; + @Nullable private List lastCues; - private DtvCcPacket currentDtvCcPacket; + @Nullable private DtvCcPacket currentDtvCcPacket; private int currentWindow; - public Cea708Decoder(int accessibilityChannel, List initializationData) { + public Cea708Decoder(int accessibilityChannel, @Nullable List initializationData) { ccData = new ParsableByteArray(); serviceBlockPacket = new ParsableBitArray(); selectedServiceNumber = accessibilityChannel == Format.NO_VALUE ? 1 : accessibilityChannel; + isWideAspectRatio = + initializationData != null + && CodecSpecificDataUtil.parseCea708InitializationData(initializationData); - cueBuilders = new CueBuilder[NUM_WINDOWS]; + cueInfoBuilders = new CueInfoBuilder[NUM_WINDOWS]; for (int i = 0; i < NUM_WINDOWS; i++) { - cueBuilders[i] = new CueBuilder(); + cueInfoBuilders[i] = new CueInfoBuilder(); } - currentCueBuilder = cueBuilders[0]; - resetCueBuilders(); + currentCueInfoBuilder = cueInfoBuilders[0]; } @Override @@ -177,7 +186,7 @@ public final class Cea708Decoder extends CeaDecoder { cues = null; lastCues = null; currentWindow = 0; - currentCueBuilder = cueBuilders[currentWindow]; + currentCueInfoBuilder = cueInfoBuilders[currentWindow]; resetCueBuilders(); currentDtvCcPacket = null; } @@ -190,15 +199,16 @@ public final class Cea708Decoder extends CeaDecoder { @Override protected Subtitle createSubtitle() { lastCues = cues; - return new CeaSubtitle(cues); + return new CeaSubtitle(Assertions.checkNotNull(cues)); } @Override protected void decode(SubtitleInputBuffer inputBuffer) { // Subtitle input buffers are non-direct and the position is zero, so calling array() is safe. + ByteBuffer subtitleData = Assertions.checkNotNull(inputBuffer.data); @SuppressWarnings("ByteBufferBackingArray") - byte[] inputBufferData = inputBuffer.data.array(); - ccData.reset(inputBufferData, inputBuffer.data.limit()); + byte[] inputBufferData = subtitleData.array(); + ccData.reset(inputBufferData, subtitleData.limit()); while (ccData.bytesLeft() >= 3) { int ccTypeAndValid = (ccData.readUnsignedByte() & 0x07); @@ -257,6 +267,7 @@ public final class Cea708Decoder extends CeaDecoder { currentDtvCcPacket = null; } + @RequiresNonNull("currentDtvCcPacket") private void processCurrentPacket() { if (currentDtvCcPacket.currentIndex != (currentDtvCcPacket.packetSize * 2 - 1)) { Log.w(TAG, "DtvCcPacket ended prematurely; size is " + (currentDtvCcPacket.packetSize * 2 - 1) @@ -346,13 +357,13 @@ public final class Cea708Decoder extends CeaDecoder { cues = getDisplayCues(); break; case COMMAND_BS: - currentCueBuilder.backspace(); + currentCueInfoBuilder.backspace(); break; case COMMAND_FF: resetCueBuilders(); break; case COMMAND_CR: - currentCueBuilder.append('\n'); + currentCueInfoBuilder.append('\n'); break; case COMMAND_HCR: // TODO: Add support for this command. @@ -384,42 +395,42 @@ public final class Cea708Decoder extends CeaDecoder { window = (command - COMMAND_CW0); if (currentWindow != window) { currentWindow = window; - currentCueBuilder = cueBuilders[window]; + currentCueInfoBuilder = cueInfoBuilders[window]; } break; case COMMAND_CLW: for (int i = 1; i <= NUM_WINDOWS; i++) { if (serviceBlockPacket.readBit()) { - cueBuilders[NUM_WINDOWS - i].clear(); + cueInfoBuilders[NUM_WINDOWS - i].clear(); } } break; case COMMAND_DSW: for (int i = 1; i <= NUM_WINDOWS; i++) { if (serviceBlockPacket.readBit()) { - cueBuilders[NUM_WINDOWS - i].setVisibility(true); + cueInfoBuilders[NUM_WINDOWS - i].setVisibility(true); } } break; case COMMAND_HDW: for (int i = 1; i <= NUM_WINDOWS; i++) { if (serviceBlockPacket.readBit()) { - cueBuilders[NUM_WINDOWS - i].setVisibility(false); + cueInfoBuilders[NUM_WINDOWS - i].setVisibility(false); } } break; case COMMAND_TGW: for (int i = 1; i <= NUM_WINDOWS; i++) { if (serviceBlockPacket.readBit()) { - CueBuilder cueBuilder = cueBuilders[NUM_WINDOWS - i]; - cueBuilder.setVisibility(!cueBuilder.isVisible()); + CueInfoBuilder cueInfoBuilder = cueInfoBuilders[NUM_WINDOWS - i]; + cueInfoBuilder.setVisibility(!cueInfoBuilder.isVisible()); } } break; case COMMAND_DLW: for (int i = 1; i <= NUM_WINDOWS; i++) { if (serviceBlockPacket.readBit()) { - cueBuilders[NUM_WINDOWS - i].reset(); + cueInfoBuilders[NUM_WINDOWS - i].reset(); } } break; @@ -434,7 +445,7 @@ public final class Cea708Decoder extends CeaDecoder { resetCueBuilders(); break; case COMMAND_SPA: - if (!currentCueBuilder.isDefined()) { + if (!currentCueInfoBuilder.isDefined()) { // ignore this command if the current window/cue isn't defined serviceBlockPacket.skipBits(16); } else { @@ -442,7 +453,7 @@ public final class Cea708Decoder extends CeaDecoder { } break; case COMMAND_SPC: - if (!currentCueBuilder.isDefined()) { + if (!currentCueInfoBuilder.isDefined()) { // ignore this command if the current window/cue isn't defined serviceBlockPacket.skipBits(24); } else { @@ -450,7 +461,7 @@ public final class Cea708Decoder extends CeaDecoder { } break; case COMMAND_SPL: - if (!currentCueBuilder.isDefined()) { + if (!currentCueInfoBuilder.isDefined()) { // ignore this command if the current window/cue isn't defined serviceBlockPacket.skipBits(16); } else { @@ -458,7 +469,7 @@ public final class Cea708Decoder extends CeaDecoder { } break; case COMMAND_SWA: - if (!currentCueBuilder.isDefined()) { + if (!currentCueInfoBuilder.isDefined()) { // ignore this command if the current window/cue isn't defined serviceBlockPacket.skipBits(32); } else { @@ -478,7 +489,7 @@ public final class Cea708Decoder extends CeaDecoder { // We also set the current window to the newly defined window. if (currentWindow != window) { currentWindow = window; - currentCueBuilder = cueBuilders[window]; + currentCueInfoBuilder = cueInfoBuilders[window]; } break; default: @@ -517,95 +528,95 @@ public final class Cea708Decoder extends CeaDecoder { private void handleG0Character(int characterCode) { if (characterCode == CHARACTER_MN) { - currentCueBuilder.append('\u266B'); + currentCueInfoBuilder.append('\u266B'); } else { - currentCueBuilder.append((char) (characterCode & 0xFF)); + currentCueInfoBuilder.append((char) (characterCode & 0xFF)); } } private void handleG1Character(int characterCode) { - currentCueBuilder.append((char) (characterCode & 0xFF)); + currentCueInfoBuilder.append((char) (characterCode & 0xFF)); } private void handleG2Character(int characterCode) { switch (characterCode) { case CHARACTER_TSP: - currentCueBuilder.append('\u0020'); + currentCueInfoBuilder.append('\u0020'); break; case CHARACTER_NBTSP: - currentCueBuilder.append('\u00A0'); + currentCueInfoBuilder.append('\u00A0'); break; case CHARACTER_ELLIPSIS: - currentCueBuilder.append('\u2026'); + currentCueInfoBuilder.append('\u2026'); break; case CHARACTER_BIG_CARONS: - currentCueBuilder.append('\u0160'); + currentCueInfoBuilder.append('\u0160'); break; case CHARACTER_BIG_OE: - currentCueBuilder.append('\u0152'); + currentCueInfoBuilder.append('\u0152'); break; case CHARACTER_SOLID_BLOCK: - currentCueBuilder.append('\u2588'); + currentCueInfoBuilder.append('\u2588'); break; case CHARACTER_OPEN_SINGLE_QUOTE: - currentCueBuilder.append('\u2018'); + currentCueInfoBuilder.append('\u2018'); break; case CHARACTER_CLOSE_SINGLE_QUOTE: - currentCueBuilder.append('\u2019'); + currentCueInfoBuilder.append('\u2019'); break; case CHARACTER_OPEN_DOUBLE_QUOTE: - currentCueBuilder.append('\u201C'); + currentCueInfoBuilder.append('\u201C'); break; case CHARACTER_CLOSE_DOUBLE_QUOTE: - currentCueBuilder.append('\u201D'); + currentCueInfoBuilder.append('\u201D'); break; case CHARACTER_BOLD_BULLET: - currentCueBuilder.append('\u2022'); + currentCueInfoBuilder.append('\u2022'); break; case CHARACTER_TM: - currentCueBuilder.append('\u2122'); + currentCueInfoBuilder.append('\u2122'); break; case CHARACTER_SMALL_CARONS: - currentCueBuilder.append('\u0161'); + currentCueInfoBuilder.append('\u0161'); break; case CHARACTER_SMALL_OE: - currentCueBuilder.append('\u0153'); + currentCueInfoBuilder.append('\u0153'); break; case CHARACTER_SM: - currentCueBuilder.append('\u2120'); + currentCueInfoBuilder.append('\u2120'); break; case CHARACTER_DIAERESIS_Y: - currentCueBuilder.append('\u0178'); + currentCueInfoBuilder.append('\u0178'); break; case CHARACTER_ONE_EIGHTH: - currentCueBuilder.append('\u215B'); + currentCueInfoBuilder.append('\u215B'); break; case CHARACTER_THREE_EIGHTHS: - currentCueBuilder.append('\u215C'); + currentCueInfoBuilder.append('\u215C'); break; case CHARACTER_FIVE_EIGHTHS: - currentCueBuilder.append('\u215D'); + currentCueInfoBuilder.append('\u215D'); break; case CHARACTER_SEVEN_EIGHTHS: - currentCueBuilder.append('\u215E'); + currentCueInfoBuilder.append('\u215E'); break; case CHARACTER_VERTICAL_BORDER: - currentCueBuilder.append('\u2502'); + currentCueInfoBuilder.append('\u2502'); break; case CHARACTER_UPPER_RIGHT_BORDER: - currentCueBuilder.append('\u2510'); + currentCueInfoBuilder.append('\u2510'); break; case CHARACTER_LOWER_LEFT_BORDER: - currentCueBuilder.append('\u2514'); + currentCueInfoBuilder.append('\u2514'); break; case CHARACTER_HORIZONTAL_BORDER: - currentCueBuilder.append('\u2500'); + currentCueInfoBuilder.append('\u2500'); break; case CHARACTER_LOWER_RIGHT_BORDER: - currentCueBuilder.append('\u2518'); + currentCueInfoBuilder.append('\u2518'); break; case CHARACTER_UPPER_LEFT_BORDER: - currentCueBuilder.append('\u250C'); + currentCueInfoBuilder.append('\u250C'); break; default: Log.w(TAG, "Invalid G2 character: " + characterCode); @@ -616,11 +627,11 @@ public final class Cea708Decoder extends CeaDecoder { private void handleG3Character(int characterCode) { if (characterCode == 0xA0) { - currentCueBuilder.append('\u33C4'); + currentCueInfoBuilder.append('\u33C4'); } else { Log.w(TAG, "Invalid G3 character: " + characterCode); // Substitute any unsupported G3 character with an underscore as per CEA-708 specification. - currentCueBuilder.append('_'); + currentCueInfoBuilder.append('_'); } } @@ -636,8 +647,8 @@ public final class Cea708Decoder extends CeaDecoder { int edgeType = serviceBlockPacket.readBits(3); int fontStyle = serviceBlockPacket.readBits(3); - currentCueBuilder.setPenAttributes(textTag, offset, penSize, italicsToggle, underlineToggle, - edgeType, fontStyle); + currentCueInfoBuilder.setPenAttributes( + textTag, offset, penSize, italicsToggle, underlineToggle, edgeType, fontStyle); } private void handleSetPenColor() { @@ -647,23 +658,23 @@ public final class Cea708Decoder extends CeaDecoder { int foregroundR = serviceBlockPacket.readBits(2); int foregroundG = serviceBlockPacket.readBits(2); int foregroundB = serviceBlockPacket.readBits(2); - int foregroundColor = CueBuilder.getArgbColorFromCeaColor(foregroundR, foregroundG, foregroundB, - foregroundO); + int foregroundColor = + CueInfoBuilder.getArgbColorFromCeaColor(foregroundR, foregroundG, foregroundB, foregroundO); // second byte int backgroundO = serviceBlockPacket.readBits(2); int backgroundR = serviceBlockPacket.readBits(2); int backgroundG = serviceBlockPacket.readBits(2); int backgroundB = serviceBlockPacket.readBits(2); - int backgroundColor = CueBuilder.getArgbColorFromCeaColor(backgroundR, backgroundG, backgroundB, - backgroundO); + int backgroundColor = + CueInfoBuilder.getArgbColorFromCeaColor(backgroundR, backgroundG, backgroundB, backgroundO); // third byte serviceBlockPacket.skipBits(2); // null padding int edgeR = serviceBlockPacket.readBits(2); int edgeG = serviceBlockPacket.readBits(2); int edgeB = serviceBlockPacket.readBits(2); - int edgeColor = CueBuilder.getArgbColorFromCeaColor(edgeR, edgeG, edgeB); + int edgeColor = CueInfoBuilder.getArgbColorFromCeaColor(edgeR, edgeG, edgeB); - currentCueBuilder.setPenColor(foregroundColor, backgroundColor, edgeColor); + currentCueInfoBuilder.setPenColor(foregroundColor, backgroundColor, edgeColor); } private void handleSetPenLocation() { @@ -675,7 +686,7 @@ public final class Cea708Decoder extends CeaDecoder { serviceBlockPacket.skipBits(2); int column = serviceBlockPacket.readBits(6); - currentCueBuilder.setPenLocation(row, column); + currentCueInfoBuilder.setPenLocation(row, column); } private void handleSetWindowAttributes() { @@ -685,13 +696,13 @@ public final class Cea708Decoder extends CeaDecoder { int fillR = serviceBlockPacket.readBits(2); int fillG = serviceBlockPacket.readBits(2); int fillB = serviceBlockPacket.readBits(2); - int fillColor = CueBuilder.getArgbColorFromCeaColor(fillR, fillG, fillB, fillO); + int fillColor = CueInfoBuilder.getArgbColorFromCeaColor(fillR, fillG, fillB, fillO); // second byte int borderType = serviceBlockPacket.readBits(2); // only the lower 2 bits of borderType int borderR = serviceBlockPacket.readBits(2); int borderG = serviceBlockPacket.readBits(2); int borderB = serviceBlockPacket.readBits(2); - int borderColor = CueBuilder.getArgbColorFromCeaColor(borderR, borderG, borderB); + int borderColor = CueInfoBuilder.getArgbColorFromCeaColor(borderR, borderG, borderB); // third byte if (serviceBlockPacket.readBit()) { borderType |= 0x04; // set the top bit of the 3-bit borderType @@ -704,12 +715,18 @@ public final class Cea708Decoder extends CeaDecoder { // Note that we don't intend to support display effects serviceBlockPacket.skipBits(8); // effectSpeed(4), effectDirection(2), displayEffect(2) - currentCueBuilder.setWindowAttributes(fillColor, borderColor, wordWrapToggle, borderType, - printDirection, scrollDirection, justification); + currentCueInfoBuilder.setWindowAttributes( + fillColor, + borderColor, + wordWrapToggle, + borderType, + printDirection, + scrollDirection, + justification); } private void handleDefineWindow(int window) { - CueBuilder cueBuilder = cueBuilders[window]; + CueInfoBuilder cueInfoBuilder = cueInfoBuilders[window]; // the DefineWindow command contains 6 bytes of data // first byte @@ -734,24 +751,44 @@ public final class Cea708Decoder extends CeaDecoder { int windowStyle = serviceBlockPacket.readBits(3); int penStyle = serviceBlockPacket.readBits(3); - cueBuilder.defineWindow(visible, rowLock, columnLock, priority, relativePositioning, - verticalAnchor, horizontalAnchor, rowCount, columnCount, anchorId, windowStyle, penStyle); + cueInfoBuilder.defineWindow( + visible, + rowLock, + columnLock, + priority, + relativePositioning, + verticalAnchor, + horizontalAnchor, + rowCount, + columnCount, + anchorId, + windowStyle, + penStyle); } private List getDisplayCues() { - List displayCues = new ArrayList<>(); + List displayCueInfos = new ArrayList<>(); for (int i = 0; i < NUM_WINDOWS; i++) { - if (!cueBuilders[i].isEmpty() && cueBuilders[i].isVisible()) { - displayCues.add(cueBuilders[i].build()); + if (!cueInfoBuilders[i].isEmpty() && cueInfoBuilders[i].isVisible()) { + @Nullable Cea708CueInfo cueInfo = cueInfoBuilders[i].build(); + if (cueInfo != null) { + displayCueInfos.add(cueInfo); + } } } - Collections.sort(displayCues); + Collections.sort( + displayCueInfos, + (thisInfo, thatInfo) -> Integer.compare(thisInfo.priority, thatInfo.priority)); + List displayCues = new ArrayList<>(displayCueInfos.size()); + for (int i = 0; i < displayCueInfos.size(); i++) { + displayCues.add(displayCueInfos.get(i).cue); + } return Collections.unmodifiableList(displayCues); } private void resetCueBuilders() { for (int i = 0; i < NUM_WINDOWS; i++) { - cueBuilders[i].reset(); + cueInfoBuilders[i].reset(); } } @@ -772,9 +809,9 @@ public final class Cea708Decoder extends CeaDecoder { } - // TODO: There is a lot of overlap between Cea708Decoder.CueBuilder and Cea608Decoder.CueBuilder - // which could be refactored into a separate class. - private static final class CueBuilder { + // TODO: There is a lot of overlap between Cea708Decoder.CueInfoBuilder and + // Cea608Decoder.CueBuilder which could be refactored into a separate class. + private static final class CueInfoBuilder { private static final int RELATIVE_CUE_SIZE = 99; private static final int VERTICAL_SIZE = 74; @@ -883,7 +920,7 @@ public final class Cea708Decoder extends CeaDecoder { private int backgroundColor; private int row; - public CueBuilder() { + public CueInfoBuilder() { rolledUpCaptions = new ArrayList<>(); captionStringBuilder = new SpannableStringBuilder(); reset(); @@ -1132,7 +1169,8 @@ public final class Cea708Decoder extends CeaDecoder { return new SpannableString(spannableStringBuilder); } - public Cea708Cue build() { + @Nullable + public Cea708CueInfo build() { if (isEmpty()) { // The cue is empty. return null; @@ -1207,8 +1245,17 @@ public final class Cea708Decoder extends CeaDecoder { boolean windowColorSet = (windowFillColor != COLOR_SOLID_BLACK); - return new Cea708Cue(cueString, alignment, line, Cue.LINE_TYPE_FRACTION, verticalAnchorType, - position, horizontalAnchorType, Cue.DIMEN_UNSET, windowColorSet, windowFillColor, + return new Cea708CueInfo( + cueString, + alignment, + line, + Cue.LINE_TYPE_FRACTION, + verticalAnchorType, + position, + horizontalAnchorType, + Cue.DIMEN_UNSET, + windowColorSet, + windowFillColor, priority); } @@ -1247,7 +1294,54 @@ public final class Cea708Decoder extends CeaDecoder { (green > 1 ? 255 : 0), (blue > 1 ? 255 : 0)); } - } + /** A {@link Cue} for CEA-708. */ + private static final class Cea708CueInfo { + + public final Cue cue; + + /** The priority of the cue box. */ + public final int priority; + + /** + * @param text See {@link Cue#text}. + * @param textAlignment See {@link Cue#textAlignment}. + * @param line See {@link Cue#line}. + * @param lineType See {@link Cue#lineType}. + * @param lineAnchor See {@link Cue#lineAnchor}. + * @param position See {@link Cue#position}. + * @param positionAnchor See {@link Cue#positionAnchor}. + * @param size See {@link Cue#size}. + * @param windowColorSet See {@link Cue#windowColorSet}. + * @param windowColor See {@link Cue#windowColor}. + * @param priority See (@link #priority}. + */ + public Cea708CueInfo( + CharSequence text, + Alignment textAlignment, + float line, + @Cue.LineType int lineType, + @AnchorType int lineAnchor, + float position, + @AnchorType int positionAnchor, + float size, + boolean windowColorSet, + int windowColor, + int priority) { + this.cue = + new Cue( + text, + textAlignment, + line, + lineType, + lineAnchor, + position, + positionAnchor, + size, + windowColorSet, + windowColor); + this.priority = priority; + } + } } 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 deleted file mode 100644 index 10bed14adc..0000000000 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/cea/Cea708InitializationData.java +++ /dev/null @@ -1,54 +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.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 ce9da9f5d5..f42b2a99cf 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 @@ -15,7 +15,7 @@ */ package com.google.android.exoplayer2.text.cea; -import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.text.Subtitle; @@ -24,6 +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 com.google.android.exoplayer2.util.Util; import java.util.ArrayDeque; import java.util.PriorityQueue; @@ -39,10 +40,11 @@ import java.util.PriorityQueue; private final ArrayDeque availableOutputBuffers; private final PriorityQueue queuedInputBuffers; - private CeaInputBuffer dequeuedInputBuffer; + @Nullable private CeaInputBuffer dequeuedInputBuffer; private long playbackPositionUs; private long queuedInputBufferCount; + @SuppressWarnings("nullness:methodref.receiver.bound.invalid") public CeaDecoder() { availableInputBuffers = new ArrayDeque<>(); for (int i = 0; i < NUM_INPUT_BUFFERS; i++) { @@ -50,7 +52,7 @@ import java.util.PriorityQueue; } availableOutputBuffers = new ArrayDeque<>(); for (int i = 0; i < NUM_OUTPUT_BUFFERS; i++) { - availableOutputBuffers.add(new CeaOutputBuffer()); + availableOutputBuffers.add(new CeaOutputBuffer(this::releaseOutputBuffer)); } queuedInputBuffers = new PriorityQueue<>(); } @@ -64,6 +66,7 @@ import java.util.PriorityQueue; } @Override + @Nullable public SubtitleInputBuffer dequeueInputBuffer() throws SubtitleDecoderException { Assertions.checkState(dequeuedInputBuffer == null); if (availableInputBuffers.isEmpty()) { @@ -76,18 +79,20 @@ import java.util.PriorityQueue; @Override public void queueInputBuffer(SubtitleInputBuffer inputBuffer) throws SubtitleDecoderException { Assertions.checkArgument(inputBuffer == dequeuedInputBuffer); - if (inputBuffer.isDecodeOnly()) { + CeaInputBuffer ceaInputBuffer = (CeaInputBuffer) inputBuffer; + if (ceaInputBuffer.isDecodeOnly()) { // We can drop this buffer early (i.e. before it would be decoded) as the CEA formats allow // for decoding to begin mid-stream. - releaseInputBuffer(dequeuedInputBuffer); + releaseInputBuffer(ceaInputBuffer); } else { - dequeuedInputBuffer.queuedInputBufferCount = queuedInputBufferCount++; - queuedInputBuffers.add(dequeuedInputBuffer); + ceaInputBuffer.queuedInputBufferCount = queuedInputBufferCount++; + queuedInputBuffers.add(ceaInputBuffer); } dequeuedInputBuffer = null; } @Override + @Nullable public SubtitleOutputBuffer dequeueOutputBuffer() throws SubtitleDecoderException { if (availableOutputBuffers.isEmpty()) { return null; @@ -96,13 +101,14 @@ import java.util.PriorityQueue; // to the current playback position; processing input buffers for future content should // be deferred until they would be applicable while (!queuedInputBuffers.isEmpty() - && queuedInputBuffers.peek().timeUs <= playbackPositionUs) { - CeaInputBuffer inputBuffer = queuedInputBuffers.poll(); + && Util.castNonNull(queuedInputBuffers.peek()).timeUs <= playbackPositionUs) { + CeaInputBuffer inputBuffer = Util.castNonNull(queuedInputBuffers.poll()); // If the input buffer indicates we've reached the end of the stream, we can // return immediately with an output buffer propagating that if (inputBuffer.isEndOfStream()) { - SubtitleOutputBuffer outputBuffer = availableOutputBuffers.pollFirst(); + // availableOutputBuffers.isEmpty() is checked at the top of the method, so this is safe. + SubtitleOutputBuffer outputBuffer = Util.castNonNull(availableOutputBuffers.pollFirst()); outputBuffer.addFlag(C.BUFFER_FLAG_END_OF_STREAM); releaseInputBuffer(inputBuffer); return outputBuffer; @@ -116,7 +122,8 @@ import java.util.PriorityQueue; // isn't accidentally prepended to the next subtitle Subtitle subtitle = createSubtitle(); if (!inputBuffer.isDecodeOnly()) { - SubtitleOutputBuffer outputBuffer = availableOutputBuffers.pollFirst(); + // availableOutputBuffers.isEmpty() is checked at the top of the method, so this is safe. + SubtitleOutputBuffer outputBuffer = Util.castNonNull(availableOutputBuffers.pollFirst()); outputBuffer.setContent(inputBuffer.timeUs, subtitle, Format.OFFSET_SAMPLE_RELATIVE); releaseInputBuffer(inputBuffer); return outputBuffer; @@ -125,7 +132,6 @@ import java.util.PriorityQueue; releaseInputBuffer(inputBuffer); } - return null; } @@ -144,7 +150,7 @@ import java.util.PriorityQueue; queuedInputBufferCount = 0; playbackPositionUs = 0; while (!queuedInputBuffers.isEmpty()) { - releaseInputBuffer(queuedInputBuffers.poll()); + releaseInputBuffer(Util.castNonNull(queuedInputBuffers.poll())); } if (dequeuedInputBuffer != null) { releaseInputBuffer(dequeuedInputBuffer); @@ -173,13 +179,22 @@ import java.util.PriorityQueue; */ protected abstract void decode(SubtitleInputBuffer inputBuffer); + @Nullable + protected final SubtitleOutputBuffer getAvailableOutputBuffer() { + return availableOutputBuffers.pollFirst(); + } + + protected final long getPositionUs() { + return playbackPositionUs; + } + private static final class CeaInputBuffer extends SubtitleInputBuffer implements Comparable { private long queuedInputBufferCount; @Override - public int compareTo(@NonNull CeaInputBuffer other) { + public int compareTo(CeaInputBuffer other) { if (isEndOfStream() != other.isEndOfStream()) { return isEndOfStream() ? 1 : -1; } @@ -194,11 +209,17 @@ import java.util.PriorityQueue; } } - private final class CeaOutputBuffer extends SubtitleOutputBuffer { + private static final class CeaOutputBuffer extends SubtitleOutputBuffer { + + private Owner owner; + + public CeaOutputBuffer(Owner owner) { + this.owner = owner; + } @Override public final void release() { - releaseOutputBuffer(this); + owner.releaseOutputBuffer(this); } } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/cea/package-info.java b/library/core/src/main/java/com/google/android/exoplayer2/text/cea/package-info.java new file mode 100644 index 0000000000..cbdf178b6a --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/cea/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2019 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. + */ +@NonNullApi +package com.google.android.exoplayer2.text.cea; + +import com.google.android.exoplayer2.util.NonNullApi; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/dvb/DvbParser.java b/library/core/src/main/java/com/google/android/exoplayer2/text/dvb/DvbParser.java index 3f2fef454f..55666718da 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/dvb/DvbParser.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/dvb/DvbParser.java @@ -22,6 +22,7 @@ import android.graphics.Paint; import android.graphics.PorterDuff; import android.graphics.PorterDuffXfermode; import android.util.SparseArray; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.text.Cue; import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.ParsableBitArray; @@ -29,6 +30,7 @@ import com.google.android.exoplayer2.util.Util; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /** * Parses {@link Cue}s from a DVB subtitle bitstream. @@ -85,7 +87,7 @@ import java.util.List; private final ClutDefinition defaultClutDefinition; private final SubtitleService subtitleService; - private Bitmap bitmap; + private @MonotonicNonNull Bitmap bitmap; /** * Construct an instance for the given subtitle and ancillary page ids. @@ -131,7 +133,8 @@ import java.util.List; parseSubtitlingSegment(dataBitArray, subtitleService); } - if (subtitleService.pageComposition == null) { + @Nullable PageComposition pageComposition = subtitleService.pageComposition; + if (pageComposition == null) { return Collections.emptyList(); } @@ -147,7 +150,7 @@ import java.util.List; // Build the cues. List cues = new ArrayList<>(); - SparseArray pageRegions = subtitleService.pageComposition.regions; + SparseArray pageRegions = pageComposition.regions; for (int i = 0; i < pageRegions.size(); i++) { // Save clean clipping state. canvas.save(); @@ -182,7 +185,7 @@ import java.util.List; objectData = subtitleService.ancillaryObjects.get(objectId); } if (objectData != null) { - Paint paint = objectData.nonModifyingColorFlag ? null : defaultPaint; + @Nullable Paint paint = objectData.nonModifyingColorFlag ? null : defaultPaint; paintPixelDataSubBlocks(objectData, clutDefinition, regionComposition.depth, baseHorizontalAddress + regionObject.horizontalPosition, baseVerticalAddress + regionObject.verticalPosition, paint, canvas); @@ -205,12 +208,23 @@ import java.util.List; fillRegionPaint); } - Bitmap cueBitmap = Bitmap.createBitmap(bitmap, baseHorizontalAddress, baseVerticalAddress, - regionComposition.width, regionComposition.height); - cues.add(new Cue(cueBitmap, (float) baseHorizontalAddress / displayDefinition.width, - Cue.ANCHOR_TYPE_START, (float) baseVerticalAddress / displayDefinition.height, - Cue.ANCHOR_TYPE_START, (float) regionComposition.width / displayDefinition.width, - (float) regionComposition.height / displayDefinition.height)); + cues.add( + new Cue.Builder() + .setBitmap( + Bitmap.createBitmap( + bitmap, + baseHorizontalAddress, + baseVerticalAddress, + regionComposition.width, + regionComposition.height)) + .setPosition((float) baseHorizontalAddress / displayDefinition.width) + .setPositionAnchor(Cue.ANCHOR_TYPE_START) + .setLine( + (float) baseVerticalAddress / displayDefinition.height, Cue.LINE_TYPE_FRACTION) + .setLineAnchor(Cue.ANCHOR_TYPE_START) + .setSize((float) regionComposition.width / displayDefinition.width) + .setBitmapHeight((float) regionComposition.height / displayDefinition.height) + .build()); canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR); // Restore clean clipping state. @@ -248,7 +262,7 @@ import java.util.List; break; case SEGMENT_TYPE_PAGE_COMPOSITION: if (pageId == service.subtitlePageId) { - PageComposition current = service.pageComposition; + @Nullable PageComposition current = service.pageComposition; PageComposition pageComposition = parsePageComposition(data, dataFieldLength); if (pageComposition.state != PAGE_STATE_NORMAL) { service.pageComposition = pageComposition; @@ -261,11 +275,15 @@ import java.util.List; } break; case SEGMENT_TYPE_REGION_COMPOSITION: - PageComposition pageComposition = service.pageComposition; + @Nullable PageComposition pageComposition = service.pageComposition; if (pageId == service.subtitlePageId && pageComposition != null) { RegionComposition regionComposition = parseRegionComposition(data, dataFieldLength); if (pageComposition.state == PAGE_STATE_NORMAL) { - regionComposition.mergeFrom(service.regions.get(regionComposition.id)); + @Nullable + RegionComposition existingRegionComposition = service.regions.get(regionComposition.id); + if (existingRegionComposition != null) { + regionComposition.mergeFrom(existingRegionComposition); + } } service.regions.put(regionComposition.id, regionComposition); } @@ -470,8 +488,8 @@ import java.util.List; boolean nonModifyingColorFlag = data.readBit(); data.skipBits(1); // Skip reserved. - byte[] topFieldData = null; - byte[] bottomFieldData = null; + byte[] topFieldData = Util.EMPTY_BYTE_ARRAY; + byte[] bottomFieldData = Util.EMPTY_BYTE_ARRAY; if (objectCodingMethod == OBJECT_CODING_STRING) { int numberOfCodes = data.readBits(8); @@ -577,11 +595,15 @@ import java.util.List; // Static drawing. - /** - * Draws a pixel data sub-block, as defined by ETSI EN 300 743 7.2.5.1, into a canvas. - */ - private static void paintPixelDataSubBlocks(ObjectData objectData, ClutDefinition clutDefinition, - int regionDepth, int horizontalAddress, int verticalAddress, Paint paint, Canvas canvas) { + /** Draws a pixel data sub-block, as defined by ETSI EN 300 743 7.2.5.1, into a canvas. */ + private static void paintPixelDataSubBlocks( + ObjectData objectData, + ClutDefinition clutDefinition, + int regionDepth, + int horizontalAddress, + int verticalAddress, + @Nullable Paint paint, + Canvas canvas) { int[] clutEntries; if (regionDepth == REGION_DEPTH_8_BIT) { clutEntries = clutDefinition.clutEntries8Bit; @@ -596,23 +618,27 @@ import java.util.List; verticalAddress + 1, paint, canvas); } - /** - * Draws a pixel data sub-block, as defined by ETSI EN 300 743 7.2.5.1, into a canvas. - */ - private static void paintPixelDataSubBlock(byte[] pixelData, int[] clutEntries, int regionDepth, - int horizontalAddress, int verticalAddress, Paint paint, Canvas canvas) { + /** Draws a pixel data sub-block, as defined by ETSI EN 300 743 7.2.5.1, into a canvas. */ + private static void paintPixelDataSubBlock( + byte[] pixelData, + int[] clutEntries, + int regionDepth, + int horizontalAddress, + int verticalAddress, + @Nullable Paint paint, + Canvas canvas) { ParsableBitArray data = new ParsableBitArray(pixelData); int column = horizontalAddress; int line = verticalAddress; - byte[] clutMapTable2To4 = null; - byte[] clutMapTable2To8 = null; - byte[] clutMapTable4To8 = null; + @Nullable byte[] clutMapTable2To4 = null; + @Nullable byte[] clutMapTable2To8 = null; + @Nullable byte[] clutMapTable4To8 = null; while (data.bitsLeft() != 0) { int dataType = data.readBits(8); switch (dataType) { case DATA_TYPE_2BP_CODE_STRING: - byte[] clutMapTable2ToX; + @Nullable byte[] clutMapTable2ToX; if (regionDepth == REGION_DEPTH_8_BIT) { clutMapTable2ToX = clutMapTable2To8 == null ? defaultMap2To8 : clutMapTable2To8; } else if (regionDepth == REGION_DEPTH_4_BIT) { @@ -625,7 +651,7 @@ import java.util.List; data.byteAlign(); break; case DATA_TYPE_4BP_CODE_STRING: - byte[] clutMapTable4ToX; + @Nullable byte[] clutMapTable4ToX; if (regionDepth == REGION_DEPTH_8_BIT) { clutMapTable4ToX = clutMapTable4To8 == null ? defaultMap4To8 : clutMapTable4To8; } else { @@ -636,7 +662,9 @@ import java.util.List; data.byteAlign(); break; case DATA_TYPE_8BP_CODE_STRING: - column = paint8BitPixelCodeString(data, clutEntries, null, column, line, paint, canvas); + column = + paint8BitPixelCodeString( + data, clutEntries, /* clutMapTable= */ null, column, line, paint, canvas); break; case DATA_TYPE_24_TABLE_DATA: clutMapTable2To4 = buildClutMapTable(4, 4, data); @@ -645,7 +673,7 @@ import java.util.List; clutMapTable2To8 = buildClutMapTable(4, 8, data); break; case DATA_TYPE_48_TABLE_DATA: - clutMapTable2To8 = buildClutMapTable(16, 8, data); + clutMapTable4To8 = buildClutMapTable(16, 8, data); break; case DATA_TYPE_END_LINE: column = horizontalAddress; @@ -658,11 +686,15 @@ import java.util.List; } } - /** - * Paint a 2-bit/pixel code string, as defined by ETSI EN 300 743 7.2.5.2, to a canvas. - */ - private static int paint2BitPixelCodeString(ParsableBitArray data, int[] clutEntries, - byte[] clutMapTable, int column, int line, Paint paint, Canvas canvas) { + /** Paint a 2-bit/pixel code string, as defined by ETSI EN 300 743 7.2.5.2, to a canvas. */ + private static int paint2BitPixelCodeString( + ParsableBitArray data, + int[] clutEntries, + @Nullable byte[] clutMapTable, + int column, + int line, + @Nullable Paint paint, + Canvas canvas) { boolean endOfPixelCodeString = false; do { int runLength = 0; @@ -706,11 +738,15 @@ import java.util.List; return column; } - /** - * Paint a 4-bit/pixel code string, as defined by ETSI EN 300 743 7.2.5.2, to a canvas. - */ - private static int paint4BitPixelCodeString(ParsableBitArray data, int[] clutEntries, - byte[] clutMapTable, int column, int line, Paint paint, Canvas canvas) { + /** Paint a 4-bit/pixel code string, as defined by ETSI EN 300 743 7.2.5.2, to a canvas. */ + private static int paint4BitPixelCodeString( + ParsableBitArray data, + int[] clutEntries, + @Nullable byte[] clutMapTable, + int column, + int line, + @Nullable Paint paint, + Canvas canvas) { boolean endOfPixelCodeString = false; do { int runLength = 0; @@ -760,11 +796,15 @@ import java.util.List; return column; } - /** - * Paint an 8-bit/pixel code string, as defined by ETSI EN 300 743 7.2.5.2, to a canvas. - */ - private static int paint8BitPixelCodeString(ParsableBitArray data, int[] clutEntries, - byte[] clutMapTable, int column, int line, Paint paint, Canvas canvas) { + /** Paint an 8-bit/pixel code string, as defined by ETSI EN 300 743 7.2.5.2, to a canvas. */ + private static int paint8BitPixelCodeString( + ParsableBitArray data, + int[] clutEntries, + @Nullable byte[] clutMapTable, + int column, + int line, + @Nullable Paint paint, + Canvas canvas) { boolean endOfPixelCodeString = false; do { int runLength = 0; @@ -816,18 +856,23 @@ import java.util.List; public final int subtitlePageId; public final int ancillaryPageId; - public final SparseArray regions = new SparseArray<>(); - public final SparseArray cluts = new SparseArray<>(); - public final SparseArray objects = new SparseArray<>(); - public final SparseArray ancillaryCluts = new SparseArray<>(); - public final SparseArray ancillaryObjects = new SparseArray<>(); + public final SparseArray regions; + public final SparseArray cluts; + public final SparseArray objects; + public final SparseArray ancillaryCluts; + public final SparseArray ancillaryObjects; - public DisplayDefinition displayDefinition; - public PageComposition pageComposition; + @Nullable public DisplayDefinition displayDefinition; + @Nullable public PageComposition pageComposition; public SubtitleService(int subtitlePageId, int ancillaryPageId) { this.subtitlePageId = subtitlePageId; this.ancillaryPageId = ancillaryPageId; + regions = new SparseArray<>(); + cluts = new SparseArray<>(); + objects = new SparseArray<>(); + ancillaryCluts = new SparseArray<>(); + ancillaryObjects = new SparseArray<>(); } public void reset() { @@ -944,9 +989,6 @@ import java.util.List; } public void mergeFrom(RegionComposition otherRegionComposition) { - if (otherRegionComposition == null) { - return; - } SparseArray otherRegionObjects = otherRegionComposition.regionObjects; for (int i = 0; i < otherRegionObjects.size(); i++) { regionObjects.put(otherRegionObjects.keyAt(i), otherRegionObjects.valueAt(i)); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/dvb/package-info.java b/library/core/src/main/java/com/google/android/exoplayer2/text/dvb/package-info.java new file mode 100644 index 0000000000..e5ec87a1a5 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/dvb/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2019 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. + */ +@NonNullApi +package com.google.android.exoplayer2.text.dvb; + +import com.google.android.exoplayer2.util.NonNullApi; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/package-info.java b/library/core/src/main/java/com/google/android/exoplayer2/text/package-info.java new file mode 100644 index 0000000000..5c5b3bbc31 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2019 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. + */ +@NonNullApi +package com.google.android.exoplayer2.text; + +import com.google.android.exoplayer2.util.NonNullApi; 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 9ef3556c8f..fe8bf12d47 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 @@ -235,14 +235,15 @@ public final class PgsDecoder extends SimpleSubtitleDecoder { Bitmap bitmap = Bitmap.createBitmap(argbBitmapData, bitmapWidth, bitmapHeight, Bitmap.Config.ARGB_8888); // Build the cue. - return new Cue( - bitmap, - (float) bitmapX / planeWidth, - Cue.ANCHOR_TYPE_START, - (float) bitmapY / planeHeight, - Cue.ANCHOR_TYPE_START, - (float) bitmapWidth / planeWidth, - (float) bitmapHeight / planeHeight); + return new Cue.Builder() + .setBitmap(bitmap) + .setPosition((float) bitmapX / planeWidth) + .setPositionAnchor(Cue.ANCHOR_TYPE_START) + .setLine((float) bitmapY / planeHeight, Cue.LINE_TYPE_FRACTION) + .setLineAnchor(Cue.ANCHOR_TYPE_START) + .setSize((float) bitmapWidth / planeWidth) + .setBitmapHeight((float) bitmapHeight / planeHeight) + .build(); } public void reset() { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/span/HorizontalTextInVerticalContextSpan.java b/library/core/src/main/java/com/google/android/exoplayer2/text/span/HorizontalTextInVerticalContextSpan.java new file mode 100644 index 0000000000..587e1647c6 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/span/HorizontalTextInVerticalContextSpan.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2020 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.span; + +/** + * A styling span for horizontal text in a vertical context. + * + *

      This is used in vertical text to write some characters in a horizontal orientation, known in + * Japanese as tate-chu-yoko. + * + *

      More information on tate-chu-yoko and span styling. + */ +// NOTE: There's no Android layout support for this, so this span currently doesn't extend any +// styling superclasses (e.g. MetricAffectingSpan). The only way to render this styling is to +// extract the spans and do the layout manually. +public final class HorizontalTextInVerticalContextSpan {} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/span/RubySpan.java b/library/core/src/main/java/com/google/android/exoplayer2/text/span/RubySpan.java new file mode 100644 index 0000000000..8ed84d6f6b --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/span/RubySpan.java @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2020 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.span; + +import static java.lang.annotation.RetentionPolicy.SOURCE; + +import androidx.annotation.IntDef; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; + +/** + * A styling span for ruby text. + * + *

      The text covered by this span is known as the "base text", and the ruby text is stored in + * {@link #rubyText}. + * + *

      More information on ruby characters + * and span styling. + */ +// NOTE: There's no Android layout support for rubies, so this span currently doesn't extend any +// styling superclasses (e.g. MetricAffectingSpan). The only way to render these rubies is to +// extract the spans and do the layout manually. +// TODO: Consider adding support for parenthetical text to be used when rendering doesn't support +// rubies (e.g. HTML tag). +public final class RubySpan { + + /** The ruby position is unknown. */ + public static final int POSITION_UNKNOWN = -1; + + /** + * The ruby text should be positioned above the base text. + * + *

      For vertical text it should be positioned to the right, same as CSS's ruby-position. + */ + public static final int POSITION_OVER = 1; + + /** + * The ruby text should be positioned below the base text. + * + *

      For vertical text it should be positioned to the left, same as CSS's ruby-position. + */ + public static final int POSITION_UNDER = 2; + + /** + * The possible positions of the ruby text relative to the base text. + * + *

      One of: + * + *

        + *
      • {@link #POSITION_UNKNOWN} + *
      • {@link #POSITION_OVER} + *
      • {@link #POSITION_UNDER} + *
      + */ + @Documented + @Retention(SOURCE) + @IntDef({POSITION_UNKNOWN, POSITION_OVER, POSITION_UNDER}) + public @interface Position {} + + /** The ruby text, i.e. the smaller explanatory characters. */ + public final String rubyText; + + /** The position of the ruby text relative to the base text. */ + @Position public final int position; + + public RubySpan(String rubyText, @Position int position) { + this.rubyText = rubyText; + this.position = position; + } +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/span/SpanUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/text/span/SpanUtil.java new file mode 100644 index 0000000000..d215f368a4 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/span/SpanUtil.java @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2020 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.span; + +import android.text.Spannable; +import android.text.style.ForegroundColorSpan; + +/** + * Utility methods for Android span + * styling. + */ +public final class SpanUtil { + + /** + * Adds {@code span} to {@code spannable} between {@code start} and {@code end}, removing any + * existing spans of the same type and with the same indices and flags. + * + *

      This is useful for types of spans that don't make sense to duplicate and where the + * evaluation order might have an unexpected impact on the final text, e.g. {@link + * ForegroundColorSpan}. + * + * @param spannable The {@link Spannable} to add {@code span} to. + * @param span The span object to be added. + * @param start The start index to add the new span at. + * @param end The end index to add the new span at. + * @param spanFlags The flags to pass to {@link Spannable#setSpan(Object, int, int, int)}. + */ + public static void addOrReplaceSpan( + Spannable spannable, Object span, int start, int end, int spanFlags) { + Object[] existingSpans = spannable.getSpans(start, end, span.getClass()); + for (Object existingSpan : existingSpans) { + if (spannable.getSpanStart(existingSpan) == start + && spannable.getSpanEnd(existingSpan) == end + && spannable.getSpanFlags(existingSpan) == spanFlags) { + spannable.removeSpan(existingSpan); + } + } + spannable.setSpan(span, start, end, spanFlags); + } + + private SpanUtil() {} +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/span/package-info.java b/library/core/src/main/java/com/google/android/exoplayer2/text/span/package-info.java new file mode 100644 index 0000000000..87876b1054 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/span/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2020 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. + */ +@NonNullApi +package com.google.android.exoplayer2.text.span; + +import com.google.android.exoplayer2.util.NonNullApi; 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 2e78b433bd..b963b60479 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 @@ -15,7 +15,9 @@ */ package com.google.android.exoplayer2.text.ssa; -import android.text.TextUtils; +import static com.google.android.exoplayer2.util.Util.castNonNull; + +import android.text.Layout; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.text.Cue; @@ -23,71 +25,90 @@ import com.google.android.exoplayer2.text.SimpleSubtitleDecoder; import com.google.android.exoplayer2.text.Subtitle; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Log; -import com.google.android.exoplayer2.util.LongArray; import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.Util; import java.util.ArrayList; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; -/** - * A {@link SimpleSubtitleDecoder} for SSA/ASS. - */ +/** A {@link SimpleSubtitleDecoder} for SSA/ASS. */ public final class SsaDecoder extends SimpleSubtitleDecoder { private static final String TAG = "SsaDecoder"; private static final Pattern SSA_TIMECODE_PATTERN = Pattern.compile("(?:(\\d+):)?(\\d+):(\\d+)[:.](\\d+)"); - private static final String FORMAT_LINE_PREFIX = "Format: "; - private static final String DIALOGUE_LINE_PREFIX = "Dialogue: "; + + /* package */ static final String FORMAT_LINE_PREFIX = "Format:"; + /* package */ static final String STYLE_LINE_PREFIX = "Style:"; + private static final String DIALOGUE_LINE_PREFIX = "Dialogue:"; + + private static final float DEFAULT_MARGIN = 0.05f; private final boolean haveInitializationData; + @Nullable private final SsaDialogueFormat dialogueFormatFromInitializationData; - private int formatKeyCount; - private int formatStartIndex; - private int formatEndIndex; - private int formatTextIndex; + private @MonotonicNonNull Map styles; + + /** + * The horizontal resolution used by the subtitle author - all cue positions are relative to this. + * + *

      Parsed from the {@code PlayResX} value in the {@code [Script Info]} section. + */ + private float screenWidth; + /** + * The vertical resolution used by the subtitle author - all cue positions are relative to this. + * + *

      Parsed from the {@code PlayResY} value in the {@code [Script Info]} section. + */ + private float screenHeight; public SsaDecoder() { this(/* initializationData= */ null); } /** + * Constructs an SsaDecoder with optional format and header info. + * * @param initializationData Optional initialization data for the decoder. If not null or empty, * the initialization data must consist of two byte arrays. The first must contain an SSA * format line. The second must contain an SSA header that will be assumed common to all - * samples. + * samples. The header is everything in an SSA file before the {@code [Events]} section (i.e. + * {@code [Script Info]} and optional {@code [V4+ Styles]} section. */ public SsaDecoder(@Nullable List initializationData) { super("SsaDecoder"); + screenWidth = Cue.DIMEN_UNSET; + screenHeight = Cue.DIMEN_UNSET; + if (initializationData != null && !initializationData.isEmpty()) { haveInitializationData = true; String formatLine = Util.fromUtf8Bytes(initializationData.get(0)); Assertions.checkArgument(formatLine.startsWith(FORMAT_LINE_PREFIX)); - parseFormatLine(formatLine); + dialogueFormatFromInitializationData = + Assertions.checkNotNull(SsaDialogueFormat.fromFormatLine(formatLine)); parseHeader(new ParsableByteArray(initializationData.get(1))); } else { haveInitializationData = false; + dialogueFormatFromInitializationData = null; } } @Override protected Subtitle decode(byte[] bytes, int length, boolean reset) { - ArrayList cues = new ArrayList<>(); - LongArray cueTimesUs = new LongArray(); + List> cues = new ArrayList<>(); + List cueTimesUs = new ArrayList<>(); ParsableByteArray data = new ParsableByteArray(bytes, length); if (!haveInitializationData) { parseHeader(data); } parseEventBody(data, cues, cueTimesUs); - - Cue[] cuesArray = new Cue[cues.size()]; - cues.toArray(cuesArray); - long[] cueTimesUsArray = cueTimesUs.toArray(); - return new SsaSubtitle(cuesArray, cueTimesUsArray); + return new SsaSubtitle(cues, cueTimesUs); } /** @@ -96,111 +117,161 @@ public final class SsaDecoder extends SimpleSubtitleDecoder { * @param data A {@link ParsableByteArray} from which the header should be read. */ private void parseHeader(ParsableByteArray data) { - String currentLine; + @Nullable String currentLine; while ((currentLine = data.readLine()) != null) { - // TODO: Parse useful data from the header. - if (currentLine.startsWith("[Events]")) { - // We've reached the event body. + if ("[Script Info]".equalsIgnoreCase(currentLine)) { + parseScriptInfo(data); + } else if ("[V4+ Styles]".equalsIgnoreCase(currentLine)) { + styles = parseStyles(data); + } else if ("[V4 Styles]".equalsIgnoreCase(currentLine)) { + Log.i(TAG, "[V4 Styles] are not supported"); + } else if ("[Events]".equalsIgnoreCase(currentLine)) { + // We've reached the [Events] section, so the header is over. return; } } } + /** + * Parse the {@code [Script Info]} section. + * + *

      When this returns, {@code data.position} will be set to the beginning of the first line that + * starts with {@code [} (i.e. the title of the next section). + * + * @param data A {@link ParsableByteArray} with {@link ParsableByteArray#getPosition() position} + * set to the beginning of the first line after {@code [Script Info]}. + */ + private void parseScriptInfo(ParsableByteArray data) { + @Nullable String currentLine; + while ((currentLine = data.readLine()) != null + && (data.bytesLeft() == 0 || data.peekUnsignedByte() != '[')) { + String[] infoNameAndValue = currentLine.split(":"); + if (infoNameAndValue.length != 2) { + continue; + } + switch (Util.toLowerInvariant(infoNameAndValue[0].trim())) { + case "playresx": + try { + screenWidth = Float.parseFloat(infoNameAndValue[1].trim()); + } catch (NumberFormatException e) { + // Ignore invalid PlayResX value. + } + break; + case "playresy": + try { + screenHeight = Float.parseFloat(infoNameAndValue[1].trim()); + } catch (NumberFormatException e) { + // Ignore invalid PlayResY value. + } + break; + } + } + } + + /** + * Parse the {@code [V4+ Styles]} section. + * + *

      When this returns, {@code data.position} will be set to the beginning of the first line that + * starts with {@code [} (i.e. the title of the next section). + * + * @param data A {@link ParsableByteArray} with {@link ParsableByteArray#getPosition()} pointing + * at the beginning of the first line after {@code [V4+ Styles]}. + */ + private static Map parseStyles(ParsableByteArray data) { + Map styles = new LinkedHashMap<>(); + @Nullable SsaStyle.Format formatInfo = null; + @Nullable String currentLine; + while ((currentLine = data.readLine()) != null + && (data.bytesLeft() == 0 || data.peekUnsignedByte() != '[')) { + if (currentLine.startsWith(FORMAT_LINE_PREFIX)) { + formatInfo = SsaStyle.Format.fromFormatLine(currentLine); + } else if (currentLine.startsWith(STYLE_LINE_PREFIX)) { + if (formatInfo == null) { + Log.w(TAG, "Skipping 'Style:' line before 'Format:' line: " + currentLine); + continue; + } + @Nullable SsaStyle style = SsaStyle.fromStyleLine(currentLine, formatInfo); + if (style != null) { + styles.put(style.name, style); + } + } + } + return styles; + } + /** * Parses the event body of the subtitle. * * @param data A {@link ParsableByteArray} from which the body should be read. * @param cues A list to which parsed cues will be added. - * @param cueTimesUs An array to which parsed cue timestamps will be added. + * @param cueTimesUs A sorted list to which parsed cue timestamps will be added. */ - private void parseEventBody(ParsableByteArray data, List cues, LongArray cueTimesUs) { - String currentLine; + private void parseEventBody(ParsableByteArray data, List> cues, List cueTimesUs) { + @Nullable + SsaDialogueFormat format = haveInitializationData ? dialogueFormatFromInitializationData : null; + @Nullable String currentLine; while ((currentLine = data.readLine()) != null) { - if (!haveInitializationData && currentLine.startsWith(FORMAT_LINE_PREFIX)) { - parseFormatLine(currentLine); + if (currentLine.startsWith(FORMAT_LINE_PREFIX)) { + format = SsaDialogueFormat.fromFormatLine(currentLine); } else if (currentLine.startsWith(DIALOGUE_LINE_PREFIX)) { - parseDialogueLine(currentLine, cues, cueTimesUs); + if (format == null) { + Log.w(TAG, "Skipping dialogue line before complete format: " + currentLine); + continue; + } + parseDialogueLine(currentLine, format, cues, cueTimesUs); } } } - /** - * Parses a format line. - * - * @param formatLine The line to parse. - */ - private void parseFormatLine(String formatLine) { - String[] values = TextUtils.split(formatLine.substring(FORMAT_LINE_PREFIX.length()), ","); - formatKeyCount = values.length; - formatStartIndex = C.INDEX_UNSET; - formatEndIndex = C.INDEX_UNSET; - formatTextIndex = C.INDEX_UNSET; - for (int i = 0; i < formatKeyCount; i++) { - String key = Util.toLowerInvariant(values[i].trim()); - switch (key) { - case "start": - formatStartIndex = i; - break; - case "end": - formatEndIndex = i; - break; - case "text": - formatTextIndex = i; - break; - default: - // Do nothing. - break; - } - } - if (formatStartIndex == C.INDEX_UNSET - || formatEndIndex == C.INDEX_UNSET - || formatTextIndex == C.INDEX_UNSET) { - // Set to 0 so that parseDialogueLine skips lines until a complete format line is found. - formatKeyCount = 0; - } - } - /** * Parses a dialogue line. * - * @param dialogueLine The line to parse. + * @param dialogueLine The dialogue values (i.e. everything after {@code Dialogue:}). + * @param format The dialogue format to use when parsing {@code dialogueLine}. * @param cues A list to which parsed cues will be added. - * @param cueTimesUs An array to which parsed cue timestamps will be added. + * @param cueTimesUs A sorted list to which parsed cue timestamps will be added. */ - private void parseDialogueLine(String dialogueLine, List cues, LongArray cueTimesUs) { - if (formatKeyCount == 0) { - Log.w(TAG, "Skipping dialogue line before complete format: " + dialogueLine); - return; - } - - String[] lineValues = dialogueLine.substring(DIALOGUE_LINE_PREFIX.length()) - .split(",", formatKeyCount); - if (lineValues.length != formatKeyCount) { + private void parseDialogueLine( + String dialogueLine, SsaDialogueFormat format, List> cues, List cueTimesUs) { + Assertions.checkArgument(dialogueLine.startsWith(DIALOGUE_LINE_PREFIX)); + String[] lineValues = + dialogueLine.substring(DIALOGUE_LINE_PREFIX.length()).split(",", format.length); + if (lineValues.length != format.length) { Log.w(TAG, "Skipping dialogue line with fewer columns than format: " + dialogueLine); return; } - long startTimeUs = parseTimecodeUs(lineValues[formatStartIndex]); + long startTimeUs = parseTimecodeUs(lineValues[format.startTimeIndex]); if (startTimeUs == C.TIME_UNSET) { Log.w(TAG, "Skipping invalid timing: " + dialogueLine); return; } - long endTimeUs = parseTimecodeUs(lineValues[formatEndIndex]); + long endTimeUs = parseTimecodeUs(lineValues[format.endTimeIndex]); if (endTimeUs == C.TIME_UNSET) { Log.w(TAG, "Skipping invalid timing: " + dialogueLine); return; } + @Nullable + SsaStyle style = + styles != null && format.styleIndex != C.INDEX_UNSET + ? styles.get(lineValues[format.styleIndex].trim()) + : null; + String rawText = lineValues[format.textIndex]; + SsaStyle.Overrides styleOverrides = SsaStyle.Overrides.parseFromDialogue(rawText); String text = - lineValues[formatTextIndex] - .replaceAll("\\{.*?\\}", "") // Warning that \\} can be replaced with } is bogus. + SsaStyle.Overrides.stripStyleOverrides(rawText) .replaceAll("\\\\N", "\n") .replaceAll("\\\\n", "\n"); - cues.add(new Cue(text)); - cueTimesUs.add(startTimeUs); - cues.add(Cue.EMPTY); - cueTimesUs.add(endTimeUs); + Cue cue = createCue(text, style, styleOverrides, screenWidth, screenHeight); + + int startTimeIndex = addCuePlacerholderByTime(startTimeUs, cueTimesUs, cues); + int endTimeIndex = addCuePlacerholderByTime(endTimeUs, cueTimesUs, cues); + // Iterate on cues from startTimeIndex until endTimeIndex, adding the current cue. + for (int i = startTimeIndex; i < endTimeIndex; i++) { + cues.get(i).add(cue); + } } /** @@ -209,16 +280,167 @@ public final class SsaDecoder extends SimpleSubtitleDecoder { * @param timeString The string to parse. * @return The parsed timestamp in microseconds. */ - public static long parseTimecodeUs(String timeString) { - Matcher matcher = SSA_TIMECODE_PATTERN.matcher(timeString); + private static long parseTimecodeUs(String timeString) { + Matcher matcher = SSA_TIMECODE_PATTERN.matcher(timeString.trim()); if (!matcher.matches()) { return C.TIME_UNSET; } - long timestampUs = Long.parseLong(matcher.group(1)) * 60 * 60 * C.MICROS_PER_SECOND; - timestampUs += Long.parseLong(matcher.group(2)) * 60 * C.MICROS_PER_SECOND; - timestampUs += Long.parseLong(matcher.group(3)) * C.MICROS_PER_SECOND; - timestampUs += Long.parseLong(matcher.group(4)) * 10000; // 100ths of a second. + long timestampUs = + Long.parseLong(castNonNull(matcher.group(1))) * 60 * 60 * C.MICROS_PER_SECOND; + timestampUs += Long.parseLong(castNonNull(matcher.group(2))) * 60 * C.MICROS_PER_SECOND; + timestampUs += Long.parseLong(castNonNull(matcher.group(3))) * C.MICROS_PER_SECOND; + timestampUs += Long.parseLong(castNonNull(matcher.group(4))) * 10000; // 100ths of a second. return timestampUs; } + private static Cue createCue( + String text, + @Nullable SsaStyle style, + SsaStyle.Overrides styleOverrides, + float screenWidth, + float screenHeight) { + @SsaStyle.SsaAlignment int alignment; + if (styleOverrides.alignment != SsaStyle.SSA_ALIGNMENT_UNKNOWN) { + alignment = styleOverrides.alignment; + } else if (style != null) { + alignment = style.alignment; + } else { + alignment = SsaStyle.SSA_ALIGNMENT_UNKNOWN; + } + @Cue.AnchorType int positionAnchor = toPositionAnchor(alignment); + @Cue.AnchorType int lineAnchor = toLineAnchor(alignment); + + float position; + float line; + if (styleOverrides.position != null + && screenHeight != Cue.DIMEN_UNSET + && screenWidth != Cue.DIMEN_UNSET) { + position = styleOverrides.position.x / screenWidth; + line = styleOverrides.position.y / screenHeight; + } else { + // TODO: Read the MarginL, MarginR and MarginV values from the Style & Dialogue lines. + position = computeDefaultLineOrPosition(positionAnchor); + line = computeDefaultLineOrPosition(lineAnchor); + } + + return new Cue( + text, + toTextAlignment(alignment), + line, + Cue.LINE_TYPE_FRACTION, + lineAnchor, + position, + positionAnchor, + /* size= */ Cue.DIMEN_UNSET); + } + + @Nullable + private static Layout.Alignment toTextAlignment(@SsaStyle.SsaAlignment int alignment) { + switch (alignment) { + case SsaStyle.SSA_ALIGNMENT_BOTTOM_LEFT: + case SsaStyle.SSA_ALIGNMENT_MIDDLE_LEFT: + case SsaStyle.SSA_ALIGNMENT_TOP_LEFT: + return Layout.Alignment.ALIGN_NORMAL; + case SsaStyle.SSA_ALIGNMENT_BOTTOM_CENTER: + case SsaStyle.SSA_ALIGNMENT_MIDDLE_CENTER: + case SsaStyle.SSA_ALIGNMENT_TOP_CENTER: + return Layout.Alignment.ALIGN_CENTER; + case SsaStyle.SSA_ALIGNMENT_BOTTOM_RIGHT: + case SsaStyle.SSA_ALIGNMENT_MIDDLE_RIGHT: + case SsaStyle.SSA_ALIGNMENT_TOP_RIGHT: + return Layout.Alignment.ALIGN_OPPOSITE; + case SsaStyle.SSA_ALIGNMENT_UNKNOWN: + return null; + default: + Log.w(TAG, "Unknown alignment: " + alignment); + return null; + } + } + + @Cue.AnchorType + private static int toLineAnchor(@SsaStyle.SsaAlignment int alignment) { + switch (alignment) { + case SsaStyle.SSA_ALIGNMENT_BOTTOM_LEFT: + case SsaStyle.SSA_ALIGNMENT_BOTTOM_CENTER: + case SsaStyle.SSA_ALIGNMENT_BOTTOM_RIGHT: + return Cue.ANCHOR_TYPE_END; + case SsaStyle.SSA_ALIGNMENT_MIDDLE_LEFT: + case SsaStyle.SSA_ALIGNMENT_MIDDLE_CENTER: + case SsaStyle.SSA_ALIGNMENT_MIDDLE_RIGHT: + return Cue.ANCHOR_TYPE_MIDDLE; + case SsaStyle.SSA_ALIGNMENT_TOP_LEFT: + case SsaStyle.SSA_ALIGNMENT_TOP_CENTER: + case SsaStyle.SSA_ALIGNMENT_TOP_RIGHT: + return Cue.ANCHOR_TYPE_START; + case SsaStyle.SSA_ALIGNMENT_UNKNOWN: + return Cue.TYPE_UNSET; + default: + Log.w(TAG, "Unknown alignment: " + alignment); + return Cue.TYPE_UNSET; + } + } + + @Cue.AnchorType + private static int toPositionAnchor(@SsaStyle.SsaAlignment int alignment) { + switch (alignment) { + case SsaStyle.SSA_ALIGNMENT_BOTTOM_LEFT: + case SsaStyle.SSA_ALIGNMENT_MIDDLE_LEFT: + case SsaStyle.SSA_ALIGNMENT_TOP_LEFT: + return Cue.ANCHOR_TYPE_START; + case SsaStyle.SSA_ALIGNMENT_BOTTOM_CENTER: + case SsaStyle.SSA_ALIGNMENT_MIDDLE_CENTER: + case SsaStyle.SSA_ALIGNMENT_TOP_CENTER: + return Cue.ANCHOR_TYPE_MIDDLE; + case SsaStyle.SSA_ALIGNMENT_BOTTOM_RIGHT: + case SsaStyle.SSA_ALIGNMENT_MIDDLE_RIGHT: + case SsaStyle.SSA_ALIGNMENT_TOP_RIGHT: + return Cue.ANCHOR_TYPE_END; + case SsaStyle.SSA_ALIGNMENT_UNKNOWN: + return Cue.TYPE_UNSET; + default: + Log.w(TAG, "Unknown alignment: " + alignment); + return Cue.TYPE_UNSET; + } + } + + private static float computeDefaultLineOrPosition(@Cue.AnchorType int anchor) { + switch (anchor) { + case Cue.ANCHOR_TYPE_START: + return DEFAULT_MARGIN; + case Cue.ANCHOR_TYPE_MIDDLE: + return 0.5f; + case Cue.ANCHOR_TYPE_END: + return 1.0f - DEFAULT_MARGIN; + case Cue.TYPE_UNSET: + default: + return Cue.DIMEN_UNSET; + } + } + + /** + * Searches for {@code timeUs} in {@code sortedCueTimesUs}, inserting it if it's not found, and + * returns the index. + * + *

      If it's inserted, we also insert a matching entry to {@code cues}. + */ + private static int addCuePlacerholderByTime( + long timeUs, List sortedCueTimesUs, List> cues) { + int insertionIndex = 0; + for (int i = sortedCueTimesUs.size() - 1; i >= 0; i--) { + if (sortedCueTimesUs.get(i) == timeUs) { + return i; + } + + if (sortedCueTimesUs.get(i) < timeUs) { + insertionIndex = i + 1; + break; + } + } + sortedCueTimesUs.add(insertionIndex, timeUs); + // Copy over cues from left, or use an empty list if we're inserting at the beginning. + cues.add( + insertionIndex, + insertionIndex == 0 ? new ArrayList<>() : new ArrayList<>(cues.get(insertionIndex - 1))); + return insertionIndex; + } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaDialogueFormat.java b/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaDialogueFormat.java new file mode 100644 index 0000000000..03c025cd94 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaDialogueFormat.java @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2019 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.ssa; + +import static com.google.android.exoplayer2.text.ssa.SsaDecoder.FORMAT_LINE_PREFIX; + +import android.text.TextUtils; +import androidx.annotation.Nullable; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.Util; + +/** + * Represents a {@code Format:} line from the {@code [Events]} section + * + *

      The indices are used to determine the location of particular properties in each {@code + * Dialogue:} line. + */ +/* package */ final class SsaDialogueFormat { + + public final int startTimeIndex; + public final int endTimeIndex; + public final int styleIndex; + public final int textIndex; + public final int length; + + private SsaDialogueFormat( + int startTimeIndex, int endTimeIndex, int styleIndex, int textIndex, int length) { + this.startTimeIndex = startTimeIndex; + this.endTimeIndex = endTimeIndex; + this.styleIndex = styleIndex; + this.textIndex = textIndex; + this.length = length; + } + + /** + * Parses the format info from a 'Format:' line in the [Events] section. + * + * @return the parsed info, or null if {@code formatLine} doesn't contain both 'start' and 'end'. + */ + @Nullable + public static SsaDialogueFormat fromFormatLine(String formatLine) { + int startTimeIndex = C.INDEX_UNSET; + int endTimeIndex = C.INDEX_UNSET; + int styleIndex = C.INDEX_UNSET; + int textIndex = C.INDEX_UNSET; + Assertions.checkArgument(formatLine.startsWith(FORMAT_LINE_PREFIX)); + String[] keys = TextUtils.split(formatLine.substring(FORMAT_LINE_PREFIX.length()), ","); + for (int i = 0; i < keys.length; i++) { + switch (Util.toLowerInvariant(keys[i].trim())) { + case "start": + startTimeIndex = i; + break; + case "end": + endTimeIndex = i; + break; + case "style": + styleIndex = i; + break; + case "text": + textIndex = i; + break; + } + } + return (startTimeIndex != C.INDEX_UNSET && endTimeIndex != C.INDEX_UNSET) + ? new SsaDialogueFormat(startTimeIndex, endTimeIndex, styleIndex, textIndex, keys.length) + : null; + } +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaStyle.java b/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaStyle.java new file mode 100644 index 0000000000..0cba339034 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaStyle.java @@ -0,0 +1,304 @@ +/* + * Copyright (C) 2019 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.ssa; + +import static com.google.android.exoplayer2.text.ssa.SsaDecoder.STYLE_LINE_PREFIX; +import static java.lang.annotation.RetentionPolicy.SOURCE; + +import android.graphics.PointF; +import android.text.TextUtils; +import androidx.annotation.IntDef; +import androidx.annotation.Nullable; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.Log; +import com.google.android.exoplayer2.util.Util; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** Represents a line from an SSA/ASS {@code [V4+ Styles]} section. */ +/* package */ final class SsaStyle { + + private static final String TAG = "SsaStyle"; + + /** + * The SSA/ASS alignments. + * + *

      Allowed values: + * + *

        + *
      • {@link #SSA_ALIGNMENT_UNKNOWN} + *
      • {@link #SSA_ALIGNMENT_BOTTOM_LEFT} + *
      • {@link #SSA_ALIGNMENT_BOTTOM_CENTER} + *
      • {@link #SSA_ALIGNMENT_BOTTOM_RIGHT} + *
      • {@link #SSA_ALIGNMENT_MIDDLE_LEFT} + *
      • {@link #SSA_ALIGNMENT_MIDDLE_CENTER} + *
      • {@link #SSA_ALIGNMENT_MIDDLE_RIGHT} + *
      • {@link #SSA_ALIGNMENT_TOP_LEFT} + *
      • {@link #SSA_ALIGNMENT_TOP_CENTER} + *
      • {@link #SSA_ALIGNMENT_TOP_RIGHT} + *
      + */ + @IntDef({ + SSA_ALIGNMENT_UNKNOWN, + SSA_ALIGNMENT_BOTTOM_LEFT, + SSA_ALIGNMENT_BOTTOM_CENTER, + SSA_ALIGNMENT_BOTTOM_RIGHT, + SSA_ALIGNMENT_MIDDLE_LEFT, + SSA_ALIGNMENT_MIDDLE_CENTER, + SSA_ALIGNMENT_MIDDLE_RIGHT, + SSA_ALIGNMENT_TOP_LEFT, + SSA_ALIGNMENT_TOP_CENTER, + SSA_ALIGNMENT_TOP_RIGHT, + }) + @Documented + @Retention(SOURCE) + public @interface SsaAlignment {} + + // The numbering follows the ASS (v4+) spec (i.e. the points on the number pad). + public static final int SSA_ALIGNMENT_UNKNOWN = -1; + public static final int SSA_ALIGNMENT_BOTTOM_LEFT = 1; + public static final int SSA_ALIGNMENT_BOTTOM_CENTER = 2; + public static final int SSA_ALIGNMENT_BOTTOM_RIGHT = 3; + public static final int SSA_ALIGNMENT_MIDDLE_LEFT = 4; + public static final int SSA_ALIGNMENT_MIDDLE_CENTER = 5; + public static final int SSA_ALIGNMENT_MIDDLE_RIGHT = 6; + public static final int SSA_ALIGNMENT_TOP_LEFT = 7; + public static final int SSA_ALIGNMENT_TOP_CENTER = 8; + public static final int SSA_ALIGNMENT_TOP_RIGHT = 9; + + public final String name; + @SsaAlignment public final int alignment; + + private SsaStyle(String name, @SsaAlignment int alignment) { + this.name = name; + this.alignment = alignment; + } + + @Nullable + public static SsaStyle fromStyleLine(String styleLine, Format format) { + Assertions.checkArgument(styleLine.startsWith(STYLE_LINE_PREFIX)); + String[] styleValues = TextUtils.split(styleLine.substring(STYLE_LINE_PREFIX.length()), ","); + if (styleValues.length != format.length) { + Log.w( + TAG, + Util.formatInvariant( + "Skipping malformed 'Style:' line (expected %s values, found %s): '%s'", + format.length, styleValues.length, styleLine)); + return null; + } + try { + return new SsaStyle( + styleValues[format.nameIndex].trim(), parseAlignment(styleValues[format.alignmentIndex])); + } catch (RuntimeException e) { + Log.w(TAG, "Skipping malformed 'Style:' line: '" + styleLine + "'", e); + return null; + } + } + + @SsaAlignment + private static int parseAlignment(String alignmentStr) { + try { + @SsaAlignment int alignment = Integer.parseInt(alignmentStr.trim()); + if (isValidAlignment(alignment)) { + return alignment; + } + } catch (NumberFormatException e) { + // Swallow the exception and return UNKNOWN below. + } + Log.w(TAG, "Ignoring unknown alignment: " + alignmentStr); + return SSA_ALIGNMENT_UNKNOWN; + } + + private static boolean isValidAlignment(@SsaAlignment int alignment) { + switch (alignment) { + case SSA_ALIGNMENT_BOTTOM_CENTER: + case SSA_ALIGNMENT_BOTTOM_LEFT: + case SSA_ALIGNMENT_BOTTOM_RIGHT: + case SSA_ALIGNMENT_MIDDLE_CENTER: + case SSA_ALIGNMENT_MIDDLE_LEFT: + case SSA_ALIGNMENT_MIDDLE_RIGHT: + case SSA_ALIGNMENT_TOP_CENTER: + case SSA_ALIGNMENT_TOP_LEFT: + case SSA_ALIGNMENT_TOP_RIGHT: + return true; + case SSA_ALIGNMENT_UNKNOWN: + default: + return false; + } + } + + /** + * Represents a {@code Format:} line from the {@code [V4+ Styles]} section + * + *

      The indices are used to determine the location of particular properties in each {@code + * Style:} line. + */ + /* package */ static final class Format { + + public final int nameIndex; + public final int alignmentIndex; + public final int length; + + private Format(int nameIndex, int alignmentIndex, int length) { + this.nameIndex = nameIndex; + this.alignmentIndex = alignmentIndex; + this.length = length; + } + + /** + * Parses the format info from a 'Format:' line in the [V4+ Styles] section. + * + * @return the parsed info, or null if {@code styleFormatLine} doesn't contain 'name'. + */ + @Nullable + public static Format fromFormatLine(String styleFormatLine) { + int nameIndex = C.INDEX_UNSET; + int alignmentIndex = C.INDEX_UNSET; + String[] keys = + TextUtils.split(styleFormatLine.substring(SsaDecoder.FORMAT_LINE_PREFIX.length()), ","); + for (int i = 0; i < keys.length; i++) { + switch (Util.toLowerInvariant(keys[i].trim())) { + case "name": + nameIndex = i; + break; + case "alignment": + alignmentIndex = i; + break; + } + } + return nameIndex != C.INDEX_UNSET ? new Format(nameIndex, alignmentIndex, keys.length) : null; + } + } + + /** + * Represents the style override information parsed from an SSA/ASS dialogue line. + * + *

      Overrides are contained in braces embedded in the dialogue text of the cue. + */ + /* package */ static final class Overrides { + + private static final String TAG = "SsaStyle.Overrides"; + + /** Matches "{foo}" and returns "foo" in group 1 */ + // Warning that \\} can be replaced with } is bogus [internal: b/144480183]. + private static final Pattern BRACES_PATTERN = Pattern.compile("\\{([^}]*)\\}"); + + private static final String PADDED_DECIMAL_PATTERN = "\\s*\\d+(?:\\.\\d+)?\\s*"; + + /** Matches "\pos(x,y)" and returns "x" in group 1 and "y" in group 2 */ + private static final Pattern POSITION_PATTERN = + Pattern.compile(Util.formatInvariant("\\\\pos\\((%1$s),(%1$s)\\)", PADDED_DECIMAL_PATTERN)); + /** Matches "\move(x1,y1,x2,y2[,t1,t2])" and returns "x2" in group 1 and "y2" in group 2 */ + private static final Pattern MOVE_PATTERN = + Pattern.compile( + Util.formatInvariant( + "\\\\move\\(%1$s,%1$s,(%1$s),(%1$s)(?:,%1$s,%1$s)?\\)", PADDED_DECIMAL_PATTERN)); + + /** Matches "\anx" and returns x in group 1 */ + private static final Pattern ALIGNMENT_OVERRIDE_PATTERN = Pattern.compile("\\\\an(\\d+)"); + + @SsaAlignment public final int alignment; + @Nullable public final PointF position; + + private Overrides(@SsaAlignment int alignment, @Nullable PointF position) { + this.alignment = alignment; + this.position = position; + } + + public static Overrides parseFromDialogue(String text) { + @SsaAlignment int alignment = SSA_ALIGNMENT_UNKNOWN; + PointF position = null; + Matcher matcher = BRACES_PATTERN.matcher(text); + while (matcher.find()) { + String braceContents = Assertions.checkNotNull(matcher.group(1)); + try { + PointF parsedPosition = parsePosition(braceContents); + if (parsedPosition != null) { + position = parsedPosition; + } + } catch (RuntimeException e) { + // Ignore invalid \pos() or \move() function. + } + try { + @SsaAlignment + int parsedAlignment = parseAlignmentOverride(braceContents); + if (parsedAlignment != SSA_ALIGNMENT_UNKNOWN) { + alignment = parsedAlignment; + } + } catch (RuntimeException e) { + // Ignore invalid \an alignment override. + } + } + return new Overrides(alignment, position); + } + + public static String stripStyleOverrides(String dialogueLine) { + return BRACES_PATTERN.matcher(dialogueLine).replaceAll(""); + } + + /** + * Parses the position from a style override, returns null if no position is found. + * + *

      The attribute is expected to be in the form {@code \pos(x,y)} or {@code + * \move(x1,y1,x2,y2,startTime,endTime)} (startTime and endTime are optional). In the case of + * {@code \move()}, this returns {@code (x2, y2)} (i.e. the end position of the move). + * + * @param styleOverride The string to parse. + * @return The parsed position, or null if no position is found. + */ + @Nullable + private static PointF parsePosition(String styleOverride) { + Matcher positionMatcher = POSITION_PATTERN.matcher(styleOverride); + Matcher moveMatcher = MOVE_PATTERN.matcher(styleOverride); + boolean hasPosition = positionMatcher.find(); + boolean hasMove = moveMatcher.find(); + + String x; + String y; + if (hasPosition) { + if (hasMove) { + Log.i( + TAG, + "Override has both \\pos(x,y) and \\move(x1,y1,x2,y2); using \\pos values. override='" + + styleOverride + + "'"); + } + x = positionMatcher.group(1); + y = positionMatcher.group(2); + } else if (hasMove) { + x = moveMatcher.group(1); + y = moveMatcher.group(2); + } else { + return null; + } + return new PointF( + Float.parseFloat(Assertions.checkNotNull(x).trim()), + Float.parseFloat(Assertions.checkNotNull(y).trim())); + } + + @SsaAlignment + private static int parseAlignmentOverride(String braceContents) { + Matcher matcher = ALIGNMENT_OVERRIDE_PATTERN.matcher(braceContents); + return matcher.find() + ? parseAlignment(Assertions.checkNotNull(matcher.group(1))) + : SSA_ALIGNMENT_UNKNOWN; + } + } +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaSubtitle.java b/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaSubtitle.java index 9a3756194f..4093f7974d 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaSubtitle.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaSubtitle.java @@ -28,14 +28,14 @@ import java.util.List; */ /* package */ final class SsaSubtitle implements Subtitle { - private final Cue[] cues; - private final long[] cueTimesUs; + private final List> cues; + private final List cueTimesUs; /** * @param cues The cues in the subtitle. * @param cueTimesUs The cue times, in microseconds. */ - public SsaSubtitle(Cue[] cues, long[] cueTimesUs) { + public SsaSubtitle(List> cues, List cueTimesUs) { this.cues = cues; this.cueTimesUs = cueTimesUs; } @@ -43,30 +43,29 @@ import java.util.List; @Override public int getNextEventTimeIndex(long timeUs) { int index = Util.binarySearchCeil(cueTimesUs, timeUs, false, false); - return index < cueTimesUs.length ? index : C.INDEX_UNSET; + return index < cueTimesUs.size() ? index : C.INDEX_UNSET; } @Override public int getEventTimeCount() { - return cueTimesUs.length; + return cueTimesUs.size(); } @Override public long getEventTime(int index) { Assertions.checkArgument(index >= 0); - Assertions.checkArgument(index < cueTimesUs.length); - return cueTimesUs[index]; + Assertions.checkArgument(index < cueTimesUs.size()); + return cueTimesUs.get(index); } @Override public List getCues(long timeUs) { int index = Util.binarySearchFloor(cueTimesUs, timeUs, true, false); - if (index == -1 || cues[index] == Cue.EMPTY) { - // timeUs is earlier than the start of the first cue, or we have an empty cue. + if (index == -1) { + // timeUs is earlier than the start of the first cue. return Collections.emptyList(); } else { - return Collections.singletonList(cues[index]); + return cues.get(index); } } - } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/package-info.java b/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/package-info.java new file mode 100644 index 0000000000..cdf891d016 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2019 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. + */ +@NonNullApi +package com.google.android.exoplayer2.text.ssa; + +import com.google.android.exoplayer2.util.NonNullApi; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/subrip/SubripDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/text/subrip/SubripDecoder.java index 20b7efe50a..6e25dfc52a 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/subrip/SubripDecoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/subrip/SubripDecoder.java @@ -22,6 +22,7 @@ import androidx.annotation.Nullable; import com.google.android.exoplayer2.text.Cue; import com.google.android.exoplayer2.text.SimpleSubtitleDecoder; import com.google.android.exoplayer2.text.Subtitle; +import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.LongArray; import com.google.android.exoplayer2.util.ParsableByteArray; @@ -41,10 +42,12 @@ public final class SubripDecoder extends SimpleSubtitleDecoder { private static final String TAG = "SubripDecoder"; - private static final String SUBRIP_TIMECODE = "(?:(\\d+):)?(\\d+):(\\d+),(\\d+)"; + // Some SRT files don't include hours or milliseconds in the timecode, so we use optional groups. + private static final String SUBRIP_TIMECODE = "(?:(\\d+):)?(\\d+):(\\d+)(?:,(\\d+))?"; private static final Pattern SUBRIP_TIMING_LINE = Pattern.compile("\\s*(" + SUBRIP_TIMECODE + ")\\s*-->\\s*(" + SUBRIP_TIMECODE + ")\\s*"); + // NOTE: Android Studio's suggestion to simplify '\\}' is incorrect [internal: b/144480183]. private static final Pattern SUBRIP_TAG_PATTERN = Pattern.compile("\\{\\\\.*?\\}"); private static final String SUBRIP_ALIGNMENT_TAG = "\\{\\\\an[1-9]\\}"; @@ -73,8 +76,8 @@ public final class SubripDecoder extends SimpleSubtitleDecoder { ArrayList cues = new ArrayList<>(); LongArray cueTimesUs = new LongArray(); ParsableByteArray subripData = new ParsableByteArray(bytes, length); - String currentLine; + @Nullable String currentLine; while ((currentLine = subripData.readLine()) != null) { if (currentLine.length() == 0) { // Skip blank lines. @@ -119,7 +122,7 @@ public final class SubripDecoder extends SimpleSubtitleDecoder { Spanned text = Html.fromHtml(textBuilder.toString()); - String alignmentTag = null; + @Nullable String alignmentTag = null; for (int i = 0; i < tags.size(); i++) { String tag = tags.get(i); if (tag.matches(SUBRIP_ALIGNMENT_TAG)) { @@ -132,8 +135,7 @@ public final class SubripDecoder extends SimpleSubtitleDecoder { cues.add(Cue.EMPTY); } - Cue[] cuesArray = new Cue[cues.size()]; - cues.toArray(cuesArray); + Cue[] cuesArray = cues.toArray(new Cue[0]); long[] cueTimesUsArray = cueTimesUs.toArray(); return new SubripSubtitle(cuesArray, cueTimesUsArray); } @@ -229,10 +231,15 @@ public final class SubripDecoder extends SimpleSubtitleDecoder { } private static long parseTimecode(Matcher matcher, int groupOffset) { - long timestampMs = Long.parseLong(matcher.group(groupOffset + 1)) * 60 * 60 * 1000; - timestampMs += Long.parseLong(matcher.group(groupOffset + 2)) * 60 * 1000; - timestampMs += Long.parseLong(matcher.group(groupOffset + 3)) * 1000; - timestampMs += Long.parseLong(matcher.group(groupOffset + 4)); + @Nullable String hours = matcher.group(groupOffset + 1); + long timestampMs = hours != null ? Long.parseLong(hours) * 60 * 60 * 1000 : 0; + timestampMs += + Long.parseLong(Assertions.checkNotNull(matcher.group(groupOffset + 2))) * 60 * 1000; + timestampMs += Long.parseLong(Assertions.checkNotNull(matcher.group(groupOffset + 3))) * 1000; + @Nullable String millis = matcher.group(groupOffset + 4); + if (millis != null) { + timestampMs += Long.parseLong(millis); + } return timestampMs * 1000; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/DeleteTextSpan.java b/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/DeleteTextSpan.java new file mode 100644 index 0000000000..be41c3957c --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/DeleteTextSpan.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2020 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.ttml; + +import android.text.Spanned; + +/** + * A span used to mark a section of text for later deletion. + * + *

      This is deliberately package-private because it's not generally supported by Android and + * results in surprising behaviour when simply calling {@link Spanned#toString} (i.e. the text isn't + * deleted). + * + *

      This span is explicitly handled in {@code TtmlNode#cleanUpText}. + */ +/* package */ final class DeleteTextSpan {} 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 6dabcdd904..e9d6d88c4a 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 @@ -16,11 +16,14 @@ package com.google.android.exoplayer2.text.ttml; import android.text.Layout; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.text.Cue; import com.google.android.exoplayer2.text.SimpleSubtitleDecoder; import com.google.android.exoplayer2.text.Subtitle; import com.google.android.exoplayer2.text.SubtitleDecoderException; +import com.google.android.exoplayer2.text.span.RubySpan; +import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.ColorParser; import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.Util; @@ -32,6 +35,7 @@ import java.util.HashMap; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; +import org.checkerframework.checker.nullness.qual.PolyNull; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; import org.xmlpull.v1.XmlPullParserFactory; @@ -110,18 +114,18 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder { Map globalStyles = new HashMap<>(); Map regionMap = new HashMap<>(); Map imageMap = new HashMap<>(); - regionMap.put(TtmlNode.ANONYMOUS_REGION_ID, new TtmlRegion(null)); + regionMap.put(TtmlNode.ANONYMOUS_REGION_ID, new TtmlRegion(TtmlNode.ANONYMOUS_REGION_ID)); ByteArrayInputStream inputStream = new ByteArrayInputStream(bytes, 0, length); xmlParser.setInput(inputStream, null); - TtmlSubtitle ttmlSubtitle = null; + @Nullable TtmlSubtitle ttmlSubtitle = null; ArrayDeque nodeStack = new ArrayDeque<>(); int unsupportedNodeDepth = 0; int eventType = xmlParser.getEventType(); FrameAndTickRate frameAndTickRate = DEFAULT_FRAME_AND_TICK_RATE; CellResolution cellResolution = DEFAULT_CELL_RESOLUTION; - TtsExtent ttsExtent = null; + @Nullable TtsExtent ttsExtent = null; while (eventType != XmlPullParser.END_DOCUMENT) { - TtmlNode parent = nodeStack.peek(); + @Nullable TtmlNode parent = nodeStack.peek(); if (unsupportedNodeDepth == 0) { String name = xmlParser.getName(); if (eventType == XmlPullParser.START_TAG) { @@ -149,10 +153,12 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder { } } } else if (eventType == XmlPullParser.TEXT) { - parent.addChild(TtmlNode.buildTextNode(xmlParser.getText())); + Assertions.checkNotNull(parent).addChild(TtmlNode.buildTextNode(xmlParser.getText())); } else if (eventType == XmlPullParser.END_TAG) { if (xmlParser.getName().equals(TtmlNode.TAG_TT)) { - ttmlSubtitle = new TtmlSubtitle(nodeStack.peek(), globalStyles, regionMap, imageMap); + ttmlSubtitle = + new TtmlSubtitle( + Assertions.checkNotNull(nodeStack.peek()), globalStyles, regionMap, imageMap); } nodeStack.pop(); } @@ -166,7 +172,11 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder { xmlParser.next(); eventType = xmlParser.getEventType(); } - return ttmlSubtitle; + if (ttmlSubtitle != null) { + return ttmlSubtitle; + } else { + throw new SubtitleDecoderException("No TTML subtitles found"); + } } catch (XmlPullParserException xppe) { throw new SubtitleDecoderException("Unable to decode source", xppe); } catch (IOException e) { @@ -174,7 +184,7 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder { } } - private FrameAndTickRate parseFrameAndTickRates(XmlPullParser xmlParser) + private static FrameAndTickRate parseFrameAndTickRates(XmlPullParser xmlParser) throws SubtitleDecoderException { int frameRate = DEFAULT_FRAME_RATE; String frameRateString = xmlParser.getAttributeValue(TTP, "frameRate"); @@ -208,8 +218,8 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder { return new FrameAndTickRate(frameRate * frameRateMultiplier, subFrameRate, tickRate); } - private CellResolution parseCellResolution(XmlPullParser xmlParser, CellResolution defaultValue) - throws SubtitleDecoderException { + private static CellResolution parseCellResolution( + XmlPullParser xmlParser, CellResolution defaultValue) throws SubtitleDecoderException { String cellResolution = xmlParser.getAttributeValue(TTP, "cellResolution"); if (cellResolution == null) { return defaultValue; @@ -221,8 +231,8 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder { return defaultValue; } try { - int columns = Integer.parseInt(cellResolutionMatcher.group(1)); - int rows = Integer.parseInt(cellResolutionMatcher.group(2)); + int columns = Integer.parseInt(Assertions.checkNotNull(cellResolutionMatcher.group(1))); + int rows = Integer.parseInt(Assertions.checkNotNull(cellResolutionMatcher.group(2))); if (columns == 0 || rows == 0) { throw new SubtitleDecoderException("Invalid cell resolution " + columns + " " + rows); } @@ -233,7 +243,9 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder { } } - private TtsExtent parseTtsExtent(XmlPullParser xmlParser) { + @Nullable + private static TtsExtent parseTtsExtent(XmlPullParser xmlParser) { + @Nullable String ttsExtent = XmlPullParserUtil.getAttributeValue(xmlParser, TtmlNode.ATTR_TTS_EXTENT); if (ttsExtent == null) { return null; @@ -245,8 +257,8 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder { return null; } try { - int width = Integer.parseInt(extentMatcher.group(1)); - int height = Integer.parseInt(extentMatcher.group(2)); + int width = Integer.parseInt(Assertions.checkNotNull(extentMatcher.group(1))); + int height = Integer.parseInt(Assertions.checkNotNull(extentMatcher.group(2))); return new TtsExtent(width, height); } catch (NumberFormatException e) { Log.w(TAG, "Ignoring malformed tts extent: " + ttsExtent); @@ -254,28 +266,30 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder { } } - private Map parseHeader( + private static Map parseHeader( XmlPullParser xmlParser, Map globalStyles, CellResolution cellResolution, - TtsExtent ttsExtent, + @Nullable TtsExtent ttsExtent, Map globalRegions, Map imageMap) throws IOException, XmlPullParserException { do { xmlParser.next(); if (XmlPullParserUtil.isStartTag(xmlParser, TtmlNode.TAG_STYLE)) { - String parentStyleId = XmlPullParserUtil.getAttributeValue(xmlParser, ATTR_STYLE); + @Nullable String parentStyleId = XmlPullParserUtil.getAttributeValue(xmlParser, ATTR_STYLE); TtmlStyle style = parseStyleAttributes(xmlParser, new TtmlStyle()); if (parentStyleId != null) { for (String id : parseStyleIds(parentStyleId)) { style.chain(globalStyles.get(id)); } } - if (style.getId() != null) { - globalStyles.put(style.getId(), style); + String styleId = style.getId(); + if (styleId != null) { + globalStyles.put(styleId, style); } } else if (XmlPullParserUtil.isStartTag(xmlParser, TtmlNode.TAG_REGION)) { + @Nullable TtmlRegion ttmlRegion = parseRegionAttributes(xmlParser, cellResolution, ttsExtent); if (ttmlRegion != null) { globalRegions.put(ttmlRegion.id, ttmlRegion); @@ -287,12 +301,12 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder { return globalStyles; } - private void parseMetadata(XmlPullParser xmlParser, Map imageMap) + private static void parseMetadata(XmlPullParser xmlParser, Map imageMap) throws IOException, XmlPullParserException { do { xmlParser.next(); if (XmlPullParserUtil.isStartTag(xmlParser, TtmlNode.TAG_IMAGE)) { - String id = XmlPullParserUtil.getAttributeValue(xmlParser, "id"); + @Nullable String id = XmlPullParserUtil.getAttributeValue(xmlParser, "id"); if (id != null) { String encodedBitmapData = xmlParser.nextText(); imageMap.put(id, encodedBitmapData); @@ -309,9 +323,10 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder { * fractions. In case of missing tts:extent the pixel defined regions can't be parsed, and null is * returned. */ - private TtmlRegion parseRegionAttributes( - XmlPullParser xmlParser, CellResolution cellResolution, TtsExtent ttsExtent) { - String regionId = XmlPullParserUtil.getAttributeValue(xmlParser, TtmlNode.ATTR_ID); + @Nullable + private static TtmlRegion parseRegionAttributes( + XmlPullParser xmlParser, CellResolution cellResolution, @Nullable TtsExtent ttsExtent) { + @Nullable String regionId = XmlPullParserUtil.getAttributeValue(xmlParser, TtmlNode.ATTR_ID); if (regionId == null) { return null; } @@ -319,14 +334,16 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder { float position; float line; + @Nullable String regionOrigin = XmlPullParserUtil.getAttributeValue(xmlParser, TtmlNode.ATTR_TTS_ORIGIN); if (regionOrigin != null) { Matcher originPercentageMatcher = PERCENTAGE_COORDINATES.matcher(regionOrigin); Matcher originPixelMatcher = PIXEL_COORDINATES.matcher(regionOrigin); if (originPercentageMatcher.matches()) { try { - position = Float.parseFloat(originPercentageMatcher.group(1)) / 100f; - line = Float.parseFloat(originPercentageMatcher.group(2)) / 100f; + position = + Float.parseFloat(Assertions.checkNotNull(originPercentageMatcher.group(1))) / 100f; + line = Float.parseFloat(Assertions.checkNotNull(originPercentageMatcher.group(2))) / 100f; } catch (NumberFormatException e) { Log.w(TAG, "Ignoring region with malformed origin: " + regionOrigin); return null; @@ -337,8 +354,8 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder { return null; } try { - int width = Integer.parseInt(originPixelMatcher.group(1)); - int height = Integer.parseInt(originPixelMatcher.group(2)); + int width = Integer.parseInt(Assertions.checkNotNull(originPixelMatcher.group(1))); + int height = Integer.parseInt(Assertions.checkNotNull(originPixelMatcher.group(2))); // Convert pixel values to fractions. position = width / (float) ttsExtent.width; line = height / (float) ttsExtent.height; @@ -362,14 +379,17 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder { float width; float height; + @Nullable String regionExtent = XmlPullParserUtil.getAttributeValue(xmlParser, TtmlNode.ATTR_TTS_EXTENT); if (regionExtent != null) { Matcher extentPercentageMatcher = PERCENTAGE_COORDINATES.matcher(regionExtent); Matcher extentPixelMatcher = PIXEL_COORDINATES.matcher(regionExtent); if (extentPercentageMatcher.matches()) { try { - width = Float.parseFloat(extentPercentageMatcher.group(1)) / 100f; - height = Float.parseFloat(extentPercentageMatcher.group(2)) / 100f; + width = + Float.parseFloat(Assertions.checkNotNull(extentPercentageMatcher.group(1))) / 100f; + height = + Float.parseFloat(Assertions.checkNotNull(extentPercentageMatcher.group(2))) / 100f; } catch (NumberFormatException e) { Log.w(TAG, "Ignoring region with malformed extent: " + regionOrigin); return null; @@ -380,8 +400,8 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder { return null; } try { - int extentWidth = Integer.parseInt(extentPixelMatcher.group(1)); - int extentHeight = Integer.parseInt(extentPixelMatcher.group(2)); + int extentWidth = Integer.parseInt(Assertions.checkNotNull(extentPixelMatcher.group(1))); + int extentHeight = Integer.parseInt(Assertions.checkNotNull(extentPixelMatcher.group(2))); // Convert pixel values to fractions. width = extentWidth / (float) ttsExtent.width; height = extentHeight / (float) ttsExtent.height; @@ -404,8 +424,9 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder { } @Cue.AnchorType int lineAnchor = Cue.ANCHOR_TYPE_START; - String displayAlign = XmlPullParserUtil.getAttributeValue(xmlParser, - TtmlNode.ATTR_TTS_DISPLAY_ALIGN); + @Nullable + String displayAlign = + XmlPullParserUtil.getAttributeValue(xmlParser, TtmlNode.ATTR_TTS_DISPLAY_ALIGN); if (displayAlign != null) { switch (Util.toLowerInvariant(displayAlign)) { case "center": @@ -435,12 +456,13 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder { /* textSize= */ regionTextHeight); } - private String[] parseStyleIds(String parentStyleIds) { + private static String[] parseStyleIds(String parentStyleIds) { parentStyleIds = parentStyleIds.trim(); return parentStyleIds.isEmpty() ? new String[0] : Util.split(parentStyleIds, "\\s+"); } - private TtmlStyle parseStyleAttributes(XmlPullParser parser, TtmlStyle style) { + private static @PolyNull TtmlStyle parseStyleAttributes( + XmlPullParser parser, @PolyNull TtmlStyle style) { int attributeCount = parser.getAttributeCount(); for (int i = 0; i < attributeCount; i++) { String attributeValue = parser.getAttributeValue(i); @@ -488,20 +510,66 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder { case TtmlNode.ATTR_TTS_TEXT_ALIGN: switch (Util.toLowerInvariant(attributeValue)) { case TtmlNode.LEFT: - style = createIfNull(style).setTextAlign(Layout.Alignment.ALIGN_NORMAL); - break; case TtmlNode.START: style = createIfNull(style).setTextAlign(Layout.Alignment.ALIGN_NORMAL); break; case TtmlNode.RIGHT: - style = createIfNull(style).setTextAlign(Layout.Alignment.ALIGN_OPPOSITE); - break; case TtmlNode.END: style = createIfNull(style).setTextAlign(Layout.Alignment.ALIGN_OPPOSITE); break; case TtmlNode.CENTER: style = createIfNull(style).setTextAlign(Layout.Alignment.ALIGN_CENTER); break; + default: + // ignore + break; + } + break; + case TtmlNode.ATTR_TTS_TEXT_COMBINE: + switch (Util.toLowerInvariant(attributeValue)) { + case TtmlNode.COMBINE_NONE: + style = createIfNull(style).setTextCombine(false); + break; + case TtmlNode.COMBINE_ALL: + style = createIfNull(style).setTextCombine(true); + break; + default: + // ignore + break; + } + break; + case TtmlNode.ATTR_TTS_RUBY: + switch (Util.toLowerInvariant(attributeValue)) { + case TtmlNode.RUBY_CONTAINER: + style = createIfNull(style).setRubyType(TtmlStyle.RUBY_TYPE_CONTAINER); + break; + case TtmlNode.RUBY_BASE: + case TtmlNode.RUBY_BASE_CONTAINER: + style = createIfNull(style).setRubyType(TtmlStyle.RUBY_TYPE_BASE); + break; + case TtmlNode.RUBY_TEXT: + case TtmlNode.RUBY_TEXT_CONTAINER: + style = createIfNull(style).setRubyType(TtmlStyle.RUBY_TYPE_TEXT); + break; + case TtmlNode.RUBY_DELIMITER: + style = createIfNull(style).setRubyType(TtmlStyle.RUBY_TYPE_DELIMITER); + break; + default: + // ignore + break; + } + break; + case TtmlNode.ATTR_TTS_RUBY_POSITION: + switch (Util.toLowerInvariant(attributeValue)) { + case TtmlNode.RUBY_BEFORE: + style = createIfNull(style).setRubyPosition(RubySpan.POSITION_OVER); + break; + case TtmlNode.RUBY_AFTER: + style = createIfNull(style).setRubyPosition(RubySpan.POSITION_UNDER); + break; + default: + // ignore + break; } break; case TtmlNode.ATTR_TTS_TEXT_DECORATION: @@ -520,6 +588,21 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder { break; } break; + case TtmlNode.ATTR_TTS_WRITING_MODE: + switch (Util.toLowerInvariant(attributeValue)) { + // TODO: Support horizontal RTL modes. + case TtmlNode.VERTICAL: + case TtmlNode.VERTICAL_LR: + style = createIfNull(style).setVerticalType(Cue.VERTICAL_TYPE_LR); + break; + case TtmlNode.VERTICAL_RL: + style = createIfNull(style).setVerticalType(Cue.VERTICAL_TYPE_RL); + break; + default: + // ignore + break; + } + break; default: // ignore break; @@ -528,21 +611,24 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder { return style; } - private TtmlStyle createIfNull(TtmlStyle style) { + private static TtmlStyle createIfNull(@Nullable TtmlStyle style) { return style == null ? new TtmlStyle() : style; } - private TtmlNode parseNode(XmlPullParser parser, TtmlNode parent, - Map regionMap, FrameAndTickRate frameAndTickRate) + private static TtmlNode parseNode( + XmlPullParser parser, + @Nullable TtmlNode parent, + Map regionMap, + FrameAndTickRate frameAndTickRate) throws SubtitleDecoderException { long duration = C.TIME_UNSET; long startTime = C.TIME_UNSET; long endTime = C.TIME_UNSET; String regionId = TtmlNode.ANONYMOUS_REGION_ID; - String imageId = null; - String[] styleIds = null; + @Nullable String imageId = null; + @Nullable String[] styleIds = null; int attributeCount = parser.getAttributeCount(); - TtmlStyle style = parseStyleAttributes(parser, null); + @Nullable TtmlStyle style = parseStyleAttributes(parser, null); for (int i = 0; i < attributeCount; i++) { String attr = parser.getAttributeName(i); String value = parser.getAttributeValue(i); @@ -599,8 +685,9 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder { endTime = parent.endTimeUs; } } + return TtmlNode.buildNode( - parser.getName(), startTime, endTime, style, styleIds, regionId, imageId); + parser.getName(), startTime, endTime, style, styleIds, regionId, imageId, parent); } private static boolean isSupportedTag(String tag) { @@ -637,7 +724,7 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder { } if (matcher.matches()) { - String unit = matcher.group(3); + String unit = Assertions.checkNotNull(matcher.group(3)); switch (unit) { case "px": out.setFontSizeUnit(TtmlStyle.FONT_SIZE_UNIT_PIXEL); @@ -651,7 +738,7 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder { default: throw new SubtitleDecoderException("Invalid unit for fontSize: '" + unit + "'."); } - out.setFontSize(Float.valueOf(matcher.group(1))); + out.setFontSize(Float.parseFloat(Assertions.checkNotNull(matcher.group(1)))); } else { throw new SubtitleDecoderException("Invalid expression for fontSize: '" + expression + "'."); } @@ -672,18 +759,18 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder { throws SubtitleDecoderException { Matcher matcher = CLOCK_TIME.matcher(time); if (matcher.matches()) { - String hours = matcher.group(1); + String hours = Assertions.checkNotNull(matcher.group(1)); double durationSeconds = Long.parseLong(hours) * 3600; - String minutes = matcher.group(2); + String minutes = Assertions.checkNotNull(matcher.group(2)); durationSeconds += Long.parseLong(minutes) * 60; - String seconds = matcher.group(3); + String seconds = Assertions.checkNotNull(matcher.group(3)); durationSeconds += Long.parseLong(seconds); - String fraction = matcher.group(4); + @Nullable String fraction = matcher.group(4); durationSeconds += (fraction != null) ? Double.parseDouble(fraction) : 0; - String frames = matcher.group(5); + @Nullable String frames = matcher.group(5); durationSeconds += (frames != null) ? Long.parseLong(frames) / frameAndTickRate.effectiveFrameRate : 0; - String subframes = matcher.group(6); + @Nullable String subframes = matcher.group(6); durationSeconds += (subframes != null) ? ((double) Long.parseLong(subframes)) / frameAndTickRate.subFrameRate / frameAndTickRate.effectiveFrameRate @@ -692,9 +779,9 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder { } matcher = OFFSET_TIME.matcher(time); if (matcher.matches()) { - String timeValue = matcher.group(1); + String timeValue = Assertions.checkNotNull(matcher.group(1)); double offsetSeconds = Double.parseDouble(timeValue); - String unit = matcher.group(2); + String unit = Assertions.checkNotNull(matcher.group(2)); switch (unit) { case "h": offsetSeconds *= 3600; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlNode.java b/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlNode.java index 3365749e1a..7b1dda10fd 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlNode.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlNode.java @@ -28,9 +28,9 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Map.Entry; import java.util.TreeMap; import java.util.TreeSet; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /** * A package internal representation of TTML node. @@ -64,9 +64,25 @@ import java.util.TreeSet; public static final String ATTR_TTS_FONT_FAMILY = "fontFamily"; public static final String ATTR_TTS_FONT_WEIGHT = "fontWeight"; public static final String ATTR_TTS_COLOR = "color"; + public static final String ATTR_TTS_RUBY = "ruby"; + public static final String ATTR_TTS_RUBY_POSITION = "rubyPosition"; public static final String ATTR_TTS_TEXT_DECORATION = "textDecoration"; public static final String ATTR_TTS_TEXT_ALIGN = "textAlign"; + public static final String ATTR_TTS_TEXT_COMBINE = "textCombine"; + public static final String ATTR_TTS_WRITING_MODE = "writingMode"; + // Values for ruby + public static final String RUBY_CONTAINER = "container"; + public static final String RUBY_BASE = "base"; + public static final String RUBY_BASE_CONTAINER = "baseContainer"; + public static final String RUBY_TEXT = "text"; + public static final String RUBY_TEXT_CONTAINER = "textContainer"; + public static final String RUBY_DELIMITER = "delimiter"; + + // Values for rubyPosition + public static final String RUBY_BEFORE = "before"; + public static final String RUBY_AFTER = "after"; + // Values for textDecoration public static final String LINETHROUGH = "linethrough"; public static final String NO_LINETHROUGH = "nolinethrough"; public static final String UNDERLINE = "underline"; @@ -74,12 +90,22 @@ import java.util.TreeSet; public static final String ITALIC = "italic"; public static final String BOLD = "bold"; + // Values for textAlign public static final String LEFT = "left"; public static final String CENTER = "center"; public static final String RIGHT = "right"; public static final String START = "start"; public static final String END = "end"; + // Values for textCombine + public static final String COMBINE_NONE = "none"; + public static final String COMBINE_ALL = "all"; + + // Values for writingMode + public static final String VERTICAL = "tb"; + public static final String VERTICAL_LR = "tblr"; + public static final String VERTICAL_RL = "tbrl"; + @Nullable public final String tag; @Nullable public final String text; public final boolean isTextNode; @@ -89,11 +115,12 @@ import java.util.TreeSet; @Nullable private final String[] styleIds; public final String regionId; @Nullable public final String imageId; + @Nullable public final TtmlNode parent; private final HashMap nodeStartsByRegion; private final HashMap nodeEndsByRegion; - private List children; + private @MonotonicNonNull List children; public static TtmlNode buildTextNode(String text) { return new TtmlNode( @@ -104,7 +131,8 @@ import java.util.TreeSet; /* style= */ null, /* styleIds= */ null, ANONYMOUS_REGION_ID, - /* imageId= */ null); + /* imageId= */ null, + /* parent= */ null); } public static TtmlNode buildNode( @@ -114,9 +142,10 @@ import java.util.TreeSet; @Nullable TtmlStyle style, @Nullable String[] styleIds, String regionId, - @Nullable String imageId) { + @Nullable String imageId, + @Nullable TtmlNode parent) { return new TtmlNode( - tag, /* text= */ null, startTimeUs, endTimeUs, style, styleIds, regionId, imageId); + tag, /* text= */ null, startTimeUs, endTimeUs, style, styleIds, regionId, imageId, parent); } private TtmlNode( @@ -127,7 +156,8 @@ import java.util.TreeSet; @Nullable TtmlStyle style, @Nullable String[] styleIds, String regionId, - @Nullable String imageId) { + @Nullable String imageId, + @Nullable TtmlNode parent) { this.tag = tag; this.text = text; this.imageId = imageId; @@ -137,6 +167,7 @@ import java.util.TreeSet; this.startTimeUs = startTimeUs; this.endTimeUs = endTimeUs; this.regionId = Assertions.checkNotNull(regionId); + this.parent = parent; nodeStartsByRegion = new HashMap<>(); nodeEndsByRegion = new HashMap<>(); } @@ -196,6 +227,7 @@ import java.util.TreeSet; } } + @Nullable public String[] getStyleIds() { return styleIds; } @@ -209,7 +241,7 @@ import java.util.TreeSet; List> regionImageOutputs = new ArrayList<>(); traverseForImage(timeUs, regionId, regionImageOutputs); - TreeMap regionTextOutputs = new TreeMap<>(); + TreeMap regionTextOutputs = new TreeMap<>(); traverseForText(timeUs, false, regionId, regionTextOutputs); traverseForStyle(timeUs, globalStyles, regionTextOutputs); @@ -217,7 +249,7 @@ import java.util.TreeSet; // Create image based cues. for (Pair regionImagePair : regionImageOutputs) { - String encodedBitmapData = imageMap.get(regionImagePair.second); + @Nullable String encodedBitmapData = imageMap.get(regionImagePair.second); if (encodedBitmapData == null) { // Image reference points to an invalid image. Do nothing. continue; @@ -225,34 +257,31 @@ import java.util.TreeSet; byte[] bitmapData = Base64.decode(encodedBitmapData, Base64.DEFAULT); Bitmap bitmap = BitmapFactory.decodeByteArray(bitmapData, /* offset= */ 0, bitmapData.length); - TtmlRegion region = regionMap.get(regionImagePair.first); + TtmlRegion region = Assertions.checkNotNull(regionMap.get(regionImagePair.first)); cues.add( - new Cue( - bitmap, - region.position, - Cue.ANCHOR_TYPE_START, - region.line, - region.lineAnchor, - region.width, - region.height)); + new Cue.Builder() + .setBitmap(bitmap) + .setPosition(region.position) + .setPositionAnchor(Cue.ANCHOR_TYPE_START) + .setLine(region.line, Cue.LINE_TYPE_FRACTION) + .setLineAnchor(region.lineAnchor) + .setSize(region.width) + .setBitmapHeight(region.height) + .build()); } // Create text based cues. - for (Entry entry : regionTextOutputs.entrySet()) { - TtmlRegion region = regionMap.get(entry.getKey()); - cues.add( - new Cue( - cleanUpText(entry.getValue()), - /* textAlignment= */ null, - region.line, - region.lineType, - region.lineAnchor, - region.position, - /* positionAnchor= */ Cue.TYPE_UNSET, - region.width, - region.textSizeType, - region.textSize)); + for (Map.Entry entry : regionTextOutputs.entrySet()) { + TtmlRegion region = Assertions.checkNotNull(regionMap.get(entry.getKey())); + Cue.Builder regionOutput = entry.getValue(); + cleanUpText((SpannableStringBuilder) Assertions.checkNotNull(regionOutput.getText())); + regionOutput.setLine(region.line, region.lineType); + regionOutput.setLineAnchor(region.lineAnchor); + regionOutput.setPosition(region.position); + regionOutput.setSize(region.width); + regionOutput.setTextSize(region.textSize, region.textSizeType); + cues.add(regionOutput.build()); } return cues; @@ -274,7 +303,7 @@ import java.util.TreeSet; long timeUs, boolean descendsPNode, String inheritedRegion, - Map regionOutputs) { + Map regionOutputs) { nodeStartsByRegion.clear(); nodeEndsByRegion.clear(); if (TAG_METADATA.equals(tag)) { @@ -285,13 +314,14 @@ import java.util.TreeSet; String resolvedRegionId = ANONYMOUS_REGION_ID.equals(regionId) ? inheritedRegion : regionId; if (isTextNode && descendsPNode) { - getRegionOutput(resolvedRegionId, regionOutputs).append(text); + getRegionOutputText(resolvedRegionId, regionOutputs).append(Assertions.checkNotNull(text)); } else if (TAG_BR.equals(tag) && descendsPNode) { - getRegionOutput(resolvedRegionId, regionOutputs).append('\n'); + getRegionOutputText(resolvedRegionId, regionOutputs).append('\n'); } else if (isActive(timeUs)) { // This is a container node, which can contain zero or more children. - for (Entry entry : regionOutputs.entrySet()) { - nodeStartsByRegion.put(entry.getKey(), entry.getValue().length()); + for (Map.Entry entry : regionOutputs.entrySet()) { + nodeStartsByRegion.put( + entry.getKey(), Assertions.checkNotNull(entry.getValue().getText()).length()); } boolean isPNode = TAG_P.equals(tag); @@ -300,36 +330,38 @@ import java.util.TreeSet; regionOutputs); } if (isPNode) { - TtmlRenderUtil.endParagraph(getRegionOutput(resolvedRegionId, regionOutputs)); + TtmlRenderUtil.endParagraph(getRegionOutputText(resolvedRegionId, regionOutputs)); } - for (Entry entry : regionOutputs.entrySet()) { - nodeEndsByRegion.put(entry.getKey(), entry.getValue().length()); + for (Map.Entry entry : regionOutputs.entrySet()) { + nodeEndsByRegion.put( + entry.getKey(), Assertions.checkNotNull(entry.getValue().getText()).length()); } } } - private static SpannableStringBuilder getRegionOutput( - String resolvedRegionId, Map regionOutputs) { + private static SpannableStringBuilder getRegionOutputText( + String resolvedRegionId, Map regionOutputs) { if (!regionOutputs.containsKey(resolvedRegionId)) { - regionOutputs.put(resolvedRegionId, new SpannableStringBuilder()); + Cue.Builder regionOutput = new Cue.Builder(); + regionOutput.setText(new SpannableStringBuilder()); + regionOutputs.put(resolvedRegionId, regionOutput); } - return regionOutputs.get(resolvedRegionId); + return (SpannableStringBuilder) + Assertions.checkNotNull(regionOutputs.get(resolvedRegionId).getText()); } private void traverseForStyle( - long timeUs, - Map globalStyles, - Map regionOutputs) { + long timeUs, Map globalStyles, Map regionOutputs) { if (!isActive(timeUs)) { return; } - for (Entry entry : nodeEndsByRegion.entrySet()) { + for (Map.Entry entry : nodeEndsByRegion.entrySet()) { String regionId = entry.getKey(); int start = nodeStartsByRegion.containsKey(regionId) ? nodeStartsByRegion.get(regionId) : 0; int end = entry.getValue(); if (start != end) { - SpannableStringBuilder regionOutput = regionOutputs.get(regionId); + Cue.Builder regionOutput = Assertions.checkNotNull(regionOutputs.get(regionId)); applyStyleToOutput(globalStyles, regionOutput, start, end); } } @@ -339,21 +371,28 @@ import java.util.TreeSet; } private void applyStyleToOutput( - Map globalStyles, - SpannableStringBuilder regionOutput, - int start, - int end) { - TtmlStyle resolvedStyle = TtmlRenderUtil.resolveStyle(style, styleIds, globalStyles); + Map globalStyles, Cue.Builder regionOutput, int start, int end) { + @Nullable TtmlStyle resolvedStyle = TtmlRenderUtil.resolveStyle(style, styleIds, globalStyles); + @Nullable SpannableStringBuilder text = (SpannableStringBuilder) regionOutput.getText(); + if (text == null) { + text = new SpannableStringBuilder(); + regionOutput.setText(text); + } if (resolvedStyle != null) { - TtmlRenderUtil.applyStylesToSpan(regionOutput, start, end, resolvedStyle); + TtmlRenderUtil.applyStylesToSpan(text, start, end, resolvedStyle, parent); + regionOutput.setVerticalType(resolvedStyle.getVerticalType()); } } - private SpannableStringBuilder cleanUpText(SpannableStringBuilder builder) { + private static void cleanUpText(SpannableStringBuilder builder) { // Having joined the text elements, we need to do some final cleanup on the result. - // 1. Collapse multiple consecutive spaces into a single space. - int builderLength = builder.length(); - for (int i = 0; i < builderLength; i++) { + // Remove any text covered by a DeleteTextSpan (e.g. ruby text). + DeleteTextSpan[] deleteTextSpans = builder.getSpans(0, builder.length(), DeleteTextSpan.class); + for (DeleteTextSpan deleteTextSpan : deleteTextSpans) { + builder.replace(builder.getSpanStart(deleteTextSpan), builder.getSpanEnd(deleteTextSpan), ""); + } + // Collapse multiple consecutive spaces into a single space. + for (int i = 0; i < builder.length(); i++) { if (builder.charAt(i) == ' ') { int j = i + 1; while (j < builder.length() && builder.charAt(j) == ' ') { @@ -362,38 +401,31 @@ import java.util.TreeSet; int spacesToDelete = j - (i + 1); if (spacesToDelete > 0) { builder.delete(i, i + spacesToDelete); - builderLength -= spacesToDelete; } } } - // 2. Remove any spaces from the start of each line. - if (builderLength > 0 && builder.charAt(0) == ' ') { + // Remove any spaces from the start of each line. + if (builder.length() > 0 && builder.charAt(0) == ' ') { builder.delete(0, 1); - builderLength--; } - for (int i = 0; i < builderLength - 1; i++) { + for (int i = 0; i < builder.length() - 1; i++) { if (builder.charAt(i) == '\n' && builder.charAt(i + 1) == ' ') { builder.delete(i + 1, i + 2); - builderLength--; } } - // 3. Remove any spaces from the end of each line. - if (builderLength > 0 && builder.charAt(builderLength - 1) == ' ') { - builder.delete(builderLength - 1, builderLength); - builderLength--; + // Remove any spaces from the end of each line. + if (builder.length() > 0 && builder.charAt(builder.length() - 1) == ' ') { + builder.delete(builder.length() - 1, builder.length()); } - for (int i = 0; i < builderLength - 1; i++) { + for (int i = 0; i < builder.length() - 1; i++) { if (builder.charAt(i) == ' ' && builder.charAt(i + 1) == '\n') { builder.delete(i, i + 1); - builderLength--; } } - // 4. Trim a trailing newline, if there is one. - if (builderLength > 0 && builder.charAt(builderLength - 1) == '\n') { - builder.delete(builderLength - 1, builderLength); - /*builderLength--;*/ + // Trim a trailing newline, if there is one. + if (builder.length() > 0 && builder.charAt(builder.length() - 1) == '\n') { + builder.delete(builder.length() - 1, builder.length()); } - return builder; } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlRenderUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlRenderUtil.java index 21333081c6..e5ba2c9c1c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlRenderUtil.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlRenderUtil.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.text.ttml; +import android.text.Layout.Alignment; import android.text.Spannable; import android.text.SpannableStringBuilder; import android.text.Spanned; @@ -27,6 +28,14 @@ import android.text.style.StrikethroughSpan; import android.text.style.StyleSpan; import android.text.style.TypefaceSpan; import android.text.style.UnderlineSpan; +import androidx.annotation.Nullable; +import com.google.android.exoplayer2.text.span.HorizontalTextInVerticalContextSpan; +import com.google.android.exoplayer2.text.span.RubySpan; +import com.google.android.exoplayer2.text.span.SpanUtil; +import com.google.android.exoplayer2.util.Log; +import com.google.android.exoplayer2.util.Util; +import java.util.ArrayDeque; +import java.util.Deque; import java.util.Map; /** @@ -34,37 +43,44 @@ import java.util.Map; */ /* package */ final class TtmlRenderUtil { - public static TtmlStyle resolveStyle(TtmlStyle style, String[] styleIds, - Map globalStyles) { - if (style == null && styleIds == null) { - // No styles at all. - return null; - } else if (style == null && styleIds.length == 1) { - // Only one single referential style present. - return globalStyles.get(styleIds[0]); - } else if (style == null && styleIds.length > 1) { - // Only multiple referential styles present. - TtmlStyle chainedStyle = new TtmlStyle(); - for (String id : styleIds) { - chainedStyle.chain(globalStyles.get(id)); + private static final String TAG = "TtmlRenderUtil"; + + @Nullable + public static TtmlStyle resolveStyle( + @Nullable TtmlStyle style, @Nullable String[] styleIds, Map globalStyles) { + if (style == null) { + if (styleIds == null) { + // No styles at all. + return null; + } else if (styleIds.length == 1) { + // Only one single referential style present. + return globalStyles.get(styleIds[0]); + } else if (styleIds.length > 1) { + // Only multiple referential styles present. + TtmlStyle chainedStyle = new TtmlStyle(); + for (String id : styleIds) { + chainedStyle.chain(globalStyles.get(id)); + } + return chainedStyle; } - return chainedStyle; - } else if (style != null && styleIds != null && styleIds.length == 1) { - // Merge a single referential style into inline style. - return style.chain(globalStyles.get(styleIds[0])); - } else if (style != null && styleIds != null && styleIds.length > 1) { - // Merge multiple referential styles into inline style. - for (String id : styleIds) { - style.chain(globalStyles.get(id)); + } else /* style != null */ { + if (styleIds != null && styleIds.length == 1) { + // Merge a single referential style into inline style. + return style.chain(globalStyles.get(styleIds[0])); + } else if (styleIds != null && styleIds.length > 1) { + // Merge multiple referential styles into inline style. + for (String id : styleIds) { + style.chain(globalStyles.get(id)); + } + return style; } - return style; } // Only inline styles available. return style; } - public static void applyStylesToSpan(SpannableStringBuilder builder, - int start, int end, TtmlStyle style) { + public static void applyStylesToSpan( + Spannable builder, int start, int end, TtmlStyle style, @Nullable TtmlNode parent) { if (style.getStyle() != TtmlStyle.UNSPECIFIED) { builder.setSpan(new StyleSpan(style.getStyle()), start, end, @@ -77,32 +93,116 @@ import java.util.Map; builder.setSpan(new UnderlineSpan(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); } if (style.hasFontColor()) { - builder.setSpan(new ForegroundColorSpan(style.getFontColor()), start, end, - Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); - } - if (style.hasBackgroundColor()) { - builder.setSpan(new BackgroundColorSpan(style.getBackgroundColor()), start, end, - Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); - } - if (style.getFontFamily() != null) { - builder.setSpan(new TypefaceSpan(style.getFontFamily()), start, end, + SpanUtil.addOrReplaceSpan( + builder, + new ForegroundColorSpan(style.getFontColor()), + start, + end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); } - if (style.getTextAlign() != null) { - builder.setSpan(new AlignmentSpan.Standard(style.getTextAlign()), start, end, + if (style.hasBackgroundColor()) { + SpanUtil.addOrReplaceSpan( + builder, + new BackgroundColorSpan(style.getBackgroundColor()), + start, + end, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } + if (style.getFontFamily() != null) { + SpanUtil.addOrReplaceSpan( + builder, + new TypefaceSpan(style.getFontFamily()), + start, + end, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } + switch (style.getRubyType()) { + case TtmlStyle.RUBY_TYPE_BASE: + // look for the sibling RUBY_TEXT and add it as span between start & end. + @Nullable TtmlNode containerNode = findRubyContainerNode(parent); + if (containerNode == null) { + // No matching container node + break; + } + @Nullable TtmlNode textNode = findRubyTextNode(containerNode); + if (textNode == null) { + // no matching text node + break; + } + String rubyText; + if (textNode.getChildCount() == 1 && textNode.getChild(0).text != null) { + rubyText = Util.castNonNull(textNode.getChild(0).text); + } else { + Log.i(TAG, "Skipping rubyText node without exactly one text child."); + break; + } + + // TODO: Get rubyPosition from `textNode` when TTML inheritance is implemented. + @RubySpan.Position + int rubyPosition = + containerNode.style != null + ? containerNode.style.getRubyPosition() + : RubySpan.POSITION_UNKNOWN; + builder.setSpan( + new RubySpan(rubyText, rubyPosition), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + break; + case TtmlStyle.RUBY_TYPE_DELIMITER: + // TODO: Add support for this when RubySpan supports parenthetical text. For now, just + // fall through and delete the text. + case TtmlStyle.RUBY_TYPE_TEXT: + // We can't just remove the text directly from `builder` here because TtmlNode has fixed + // ideas of where every node starts and ends (nodeStartsByRegion and nodeEndsByRegion) so + // all these indices become invalid if we mutate the underlying string at this point. + // Instead we add a special span that's then handled in TtmlNode#cleanUpText. + builder.setSpan(new DeleteTextSpan(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + break; + case TtmlStyle.RUBY_TYPE_CONTAINER: + case TtmlStyle.UNSPECIFIED: + default: + // Do nothing + break; + } + + @Nullable Alignment textAlign = style.getTextAlign(); + if (textAlign != null) { + SpanUtil.addOrReplaceSpan( + builder, + new AlignmentSpan.Standard(textAlign), + start, + end, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } + if (style.getTextCombine()) { + SpanUtil.addOrReplaceSpan( + builder, + new HorizontalTextInVerticalContextSpan(), + start, + end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); } switch (style.getFontSizeUnit()) { case TtmlStyle.FONT_SIZE_UNIT_PIXEL: - builder.setSpan(new AbsoluteSizeSpan((int) style.getFontSize(), true), start, end, + SpanUtil.addOrReplaceSpan( + builder, + new AbsoluteSizeSpan((int) style.getFontSize(), true), + start, + end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); break; case TtmlStyle.FONT_SIZE_UNIT_EM: - builder.setSpan(new RelativeSizeSpan(style.getFontSize()), start, end, + SpanUtil.addOrReplaceSpan( + builder, + new RelativeSizeSpan(style.getFontSize()), + start, + end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); break; case TtmlStyle.FONT_SIZE_UNIT_PERCENT: - builder.setSpan(new RelativeSizeSpan(style.getFontSize() / 100), start, end, + SpanUtil.addOrReplaceSpan( + builder, + new RelativeSizeSpan(style.getFontSize() / 100), + start, + end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); break; case TtmlStyle.UNSPECIFIED: @@ -111,6 +211,35 @@ import java.util.Map; } } + @Nullable + private static TtmlNode findRubyTextNode(TtmlNode rubyContainerNode) { + Deque childNodesStack = new ArrayDeque<>(); + childNodesStack.push(rubyContainerNode); + while (!childNodesStack.isEmpty()) { + TtmlNode childNode = childNodesStack.pop(); + if (childNode.style != null && childNode.style.getRubyType() == TtmlStyle.RUBY_TYPE_TEXT) { + return childNode; + } + for (int i = childNode.getChildCount() - 1; i >= 0; i--) { + childNodesStack.push(childNode.getChild(i)); + } + } + + return null; + } + + @Nullable + private static TtmlNode findRubyContainerNode(@Nullable TtmlNode node) { + while (node != null) { + @Nullable TtmlStyle style = node.style; + if (style != null && style.getRubyType() == TtmlStyle.RUBY_TYPE_CONTAINER) { + return node; + } + node = node.parent; + } + return null; + } + /** * Called when the end of a paragraph is encountered. Adds a newline if there are one or more * non-space characters since the previous newline. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlStyle.java b/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlStyle.java index e90b099173..928af3620c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlStyle.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlStyle.java @@ -18,7 +18,10 @@ package com.google.android.exoplayer2.text.ttml; import android.graphics.Typeface; import android.text.Layout; import androidx.annotation.IntDef; -import com.google.android.exoplayer2.util.Assertions; +import androidx.annotation.Nullable; +import com.google.android.exoplayer2.text.Cue; +import com.google.android.exoplayer2.text.Cue.VerticalType; +import com.google.android.exoplayer2.text.span.RubySpan; import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -59,7 +62,17 @@ import java.lang.annotation.RetentionPolicy; private static final int OFF = 0; private static final int ON = 1; - private String fontFamily; + @Documented + @Retention(RetentionPolicy.SOURCE) + @IntDef({UNSPECIFIED, RUBY_TYPE_CONTAINER, RUBY_TYPE_BASE, RUBY_TYPE_TEXT, RUBY_TYPE_DELIMITER}) + public @interface RubyType {} + + public static final int RUBY_TYPE_CONTAINER = 1; + public static final int RUBY_TYPE_BASE = 2; + public static final int RUBY_TYPE_TEXT = 3; + public static final int RUBY_TYPE_DELIMITER = 4; + + @Nullable private String fontFamily; private int fontColor; private boolean hasFontColor; private int backgroundColor; @@ -70,9 +83,12 @@ import java.lang.annotation.RetentionPolicy; @OptionalBoolean private int italic; @FontSizeUnit private int fontSizeUnit; private float fontSize; - private String id; - private TtmlStyle inheritableStyle; - private Layout.Alignment textAlign; + @Nullable private String id; + @RubyType private int rubyType; + @RubySpan.Position private int rubyPosition; + @Nullable private Layout.Alignment textAlign; + @OptionalBoolean private int textCombine; + @Cue.VerticalType private int verticalType; public TtmlStyle() { linethrough = UNSPECIFIED; @@ -80,6 +96,10 @@ import java.lang.annotation.RetentionPolicy; bold = UNSPECIFIED; italic = UNSPECIFIED; fontSizeUnit = UNSPECIFIED; + rubyType = UNSPECIFIED; + rubyPosition = RubySpan.POSITION_UNKNOWN; + textCombine = UNSPECIFIED; + verticalType = Cue.TYPE_UNSET; } /** @@ -101,7 +121,6 @@ import java.lang.annotation.RetentionPolicy; } public TtmlStyle setLinethrough(boolean linethrough) { - Assertions.checkState(inheritableStyle == null); this.linethrough = linethrough ? ON : OFF; return this; } @@ -111,29 +130,26 @@ import java.lang.annotation.RetentionPolicy; } public TtmlStyle setUnderline(boolean underline) { - Assertions.checkState(inheritableStyle == null); this.underline = underline ? ON : OFF; return this; } public TtmlStyle setBold(boolean bold) { - Assertions.checkState(inheritableStyle == null); this.bold = bold ? ON : OFF; return this; } public TtmlStyle setItalic(boolean italic) { - Assertions.checkState(inheritableStyle == null); this.italic = italic ? ON : OFF; return this; } + @Nullable public String getFontFamily() { return fontFamily; } - public TtmlStyle setFontFamily(String fontFamily) { - Assertions.checkState(inheritableStyle == null); + public TtmlStyle setFontFamily(@Nullable String fontFamily) { this.fontFamily = fontFamily; return this; } @@ -146,7 +162,6 @@ import java.lang.annotation.RetentionPolicy; } public TtmlStyle setFontColor(int fontColor) { - Assertions.checkState(inheritableStyle == null); this.fontColor = fontColor; hasFontColor = true; return this; @@ -174,27 +189,27 @@ import java.lang.annotation.RetentionPolicy; } /** - * Inherits from an ancestor style. Properties like tts:backgroundColor which - * are not inheritable are not inherited as well as properties which are already set locally - * are never overridden. - * - * @param ancestor the ancestor style to inherit from - */ - public TtmlStyle inherit(TtmlStyle ancestor) { - return inherit(ancestor, false); - } - - /** - * Chains this style to referential style. Local properties which are already set - * are never overridden. + * Chains this style to referential style. Local properties which are already set are never + * overridden. * * @param ancestor the referential style to inherit from */ - public TtmlStyle chain(TtmlStyle ancestor) { + public TtmlStyle chain(@Nullable TtmlStyle ancestor) { return inherit(ancestor, true); } - private TtmlStyle inherit(TtmlStyle ancestor, boolean chaining) { + /** + * Inherits from an ancestor style. Properties like tts:backgroundColor which are not + * inheritable are not inherited as well as properties which are already set locally are never + * overridden. + * + * @param ancestor the ancestor style to inherit from + */ + public TtmlStyle inherit(@Nullable TtmlStyle ancestor) { + return inherit(ancestor, false); + } + + private TtmlStyle inherit(@Nullable TtmlStyle ancestor, boolean chaining) { if (ancestor != null) { if (!hasFontColor && ancestor.hasFontColor) { setFontColor(ancestor.fontColor); @@ -205,7 +220,7 @@ import java.lang.annotation.RetentionPolicy; if (italic == UNSPECIFIED) { italic = ancestor.italic; } - if (fontFamily == null) { + if (fontFamily == null && ancestor.fontFamily != null) { fontFamily = ancestor.fontFamily; } if (linethrough == UNSPECIFIED) { @@ -214,9 +229,15 @@ import java.lang.annotation.RetentionPolicy; if (underline == UNSPECIFIED) { underline = ancestor.underline; } - if (textAlign == null) { + if (rubyPosition == RubySpan.POSITION_UNKNOWN) { + rubyPosition = ancestor.rubyPosition; + } + if (textAlign == null && ancestor.textAlign != null) { textAlign = ancestor.textAlign; } + if (textCombine == UNSPECIFIED) { + textCombine = ancestor.textCombine; + } if (fontSizeUnit == UNSPECIFIED) { fontSizeUnit = ancestor.fontSizeUnit; fontSize = ancestor.fontSize; @@ -225,28 +246,66 @@ import java.lang.annotation.RetentionPolicy; if (chaining && !hasBackgroundColor && ancestor.hasBackgroundColor) { setBackgroundColor(ancestor.backgroundColor); } + if (chaining && rubyType == UNSPECIFIED && ancestor.rubyType != UNSPECIFIED) { + rubyType = ancestor.rubyType; + } + if (chaining && verticalType == Cue.TYPE_UNSET && ancestor.verticalType != Cue.TYPE_UNSET) { + setVerticalType(ancestor.verticalType); + } } return this; } - public TtmlStyle setId(String id) { + public TtmlStyle setId(@Nullable String id) { this.id = id; return this; } + @Nullable public String getId() { return id; } + public TtmlStyle setRubyType(@RubyType int rubyType) { + this.rubyType = rubyType; + return this; + } + + @RubyType + public int getRubyType() { + return rubyType; + } + + public TtmlStyle setRubyPosition(@RubySpan.Position int position) { + this.rubyPosition = position; + return this; + } + + @RubySpan.Position + public int getRubyPosition() { + return rubyPosition; + } + + @Nullable public Layout.Alignment getTextAlign() { return textAlign; } - public TtmlStyle setTextAlign(Layout.Alignment textAlign) { + public TtmlStyle setTextAlign(@Nullable Layout.Alignment textAlign) { this.textAlign = textAlign; return this; } + /** Returns true if the source entity has {@code tts:textCombine=all}. */ + public boolean getTextCombine() { + return textCombine == ON; + } + + public TtmlStyle setTextCombine(boolean combine) { + this.textCombine = combine ? ON : OFF; + return this; + } + public TtmlStyle setFontSize(float fontSize) { this.fontSize = fontSize; return this; @@ -265,4 +324,13 @@ import java.lang.annotation.RetentionPolicy; return fontSize; } + public TtmlStyle setVerticalType(@VerticalType int verticalType) { + this.verticalType = verticalType; + return this; + } + + @VerticalType + public int getVerticalType() { + return verticalType; + } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/package-info.java b/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/package-info.java new file mode 100644 index 0000000000..5b0685e24c --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2019 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. + */ +@NonNullApi +package com.google.android.exoplayer2.text.ttml; + +import com.google.android.exoplayer2.util.NonNullApi; 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 9a5ac40a05..5efe378a9b 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 @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.text.webvtt; import android.text.TextUtils; import androidx.annotation.Nullable; +import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.ColorParser; import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.Util; @@ -31,14 +32,19 @@ import java.util.regex.Pattern; */ /* package */ final class CssParser { + private static final String TAG = "CssParser"; + + private static final String RULE_START = "{"; + private static final String RULE_END = "}"; private static final String PROPERTY_BGCOLOR = "background-color"; private static final String PROPERTY_FONT_FAMILY = "font-family"; private static final String PROPERTY_FONT_WEIGHT = "font-weight"; + private static final String PROPERTY_TEXT_COMBINE_UPRIGHT = "text-combine-upright"; + private static final String VALUE_ALL = "all"; + private static final String VALUE_DIGITS = "digits"; private static final String PROPERTY_TEXT_DECORATION = "text-decoration"; private static final String VALUE_BOLD = "bold"; private static final String VALUE_UNDERLINE = "underline"; - private static final String RULE_START = "{"; - private static final String RULE_END = "}"; private static final String PROPERTY_FONT_STYLE = "font-style"; private static final String VALUE_ITALIC = "italic"; @@ -182,6 +188,8 @@ import java.util.regex.Pattern; style.setFontColor(ColorParser.parseCssColor(value)); } else if (PROPERTY_BGCOLOR.equals(property)) { style.setBackgroundColor(ColorParser.parseCssColor(value)); + } else if (PROPERTY_TEXT_COMBINE_UPRIGHT.equals(property)) { + style.setCombineUpright(VALUE_ALL.equals(value) || value.startsWith(VALUE_DIGITS)); } else if (PROPERTY_TEXT_DECORATION.equals(property)) { if (VALUE_UNDERLINE.equals(value)) { style.setUnderline(true); @@ -315,8 +323,8 @@ import java.util.regex.Pattern; } /** - * Sets the target of a {@link WebvttCssStyle} by splitting a selector of the form - * {@code ::cue(tag#id.class1.class2[voice="someone"]}, where every element is optional. + * Sets the target of a {@link WebvttCssStyle} by splitting a selector of the form {@code + * ::cue(tag#id.class1.class2[voice="someone"]}, where every element is optional. */ private void applySelectorToStyle(WebvttCssStyle style, String selector) { if ("".equals(selector)) { @@ -326,7 +334,7 @@ import java.util.regex.Pattern; if (voiceStartIndex != -1) { Matcher matcher = VOICE_NAME_PATTERN.matcher(selector.substring(voiceStartIndex)); if (matcher.matches()) { - style.setTargetVoice(matcher.group(1)); + style.setTargetVoice(Assertions.checkNotNull(matcher.group(1))); } selector = selector.substring(0, voiceStartIndex); } 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 8b255ac2bd..82023e6c58 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 @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.text.webvtt; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.text.Cue; import com.google.android.exoplayer2.text.SimpleSubtitleDecoder; import com.google.android.exoplayer2.text.Subtitle; @@ -41,12 +42,10 @@ public final class Mp4WebvttDecoder extends SimpleSubtitleDecoder { private static final int TYPE_vttc = 0x76747463; private final ParsableByteArray sampleData; - private final WebvttCue.Builder builder; public Mp4WebvttDecoder() { super("Mp4WebvttDecoder"); sampleData = new ParsableByteArray(); - builder = new WebvttCue.Builder(); } @Override @@ -63,7 +62,7 @@ public final class Mp4WebvttDecoder extends SimpleSubtitleDecoder { int boxSize = sampleData.readInt(); int boxType = sampleData.readInt(); if (boxType == TYPE_vttc) { - resultingCueList.add(parseVttCueBox(sampleData, builder, boxSize - BOX_HEADER_SIZE)); + resultingCueList.add(parseVttCueBox(sampleData, boxSize - BOX_HEADER_SIZE)); } else { // Peers of the VTTCueBox are still not supported and are skipped. sampleData.skipBytes(boxSize - BOX_HEADER_SIZE); @@ -72,9 +71,10 @@ public final class Mp4WebvttDecoder extends SimpleSubtitleDecoder { return new Mp4WebvttSubtitle(resultingCueList); } - private static Cue parseVttCueBox(ParsableByteArray sampleData, WebvttCue.Builder builder, - int remainingCueBoxBytes) throws SubtitleDecoderException { - builder.reset(); + private static Cue parseVttCueBox(ParsableByteArray sampleData, int remainingCueBoxBytes) + throws SubtitleDecoderException { + @Nullable Cue.Builder cueBuilder = null; + @Nullable CharSequence cueText = null; while (remainingCueBoxBytes > 0) { if (remainingCueBoxBytes < BOX_HEADER_SIZE) { throw new SubtitleDecoderException("Incomplete vtt cue box header found."); @@ -88,14 +88,20 @@ public final class Mp4WebvttDecoder extends SimpleSubtitleDecoder { sampleData.skipBytes(payloadLength); remainingCueBoxBytes -= payloadLength; if (boxType == TYPE_sttg) { - WebvttCueParser.parseCueSettingsList(boxPayload, builder); + cueBuilder = WebvttCueParser.parseCueSettingsList(boxPayload); } else if (boxType == TYPE_payl) { - WebvttCueParser.parseCueText(null, boxPayload.trim(), builder, Collections.emptyList()); + cueText = + WebvttCueParser.parseCueText( + /* id= */ null, boxPayload.trim(), /* styles= */ Collections.emptyList()); } else { // Other VTTCueBox children are still not supported and are ignored. } } - return builder.build(); + if (cueText == null) { + cueText = ""; + } + return cueBuilder != null + ? cueBuilder.setText(cueText).build() + : WebvttCueParser.newCueForText(cueText); } - } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCssStyle.java b/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCssStyle.java index 9186455702..cd08ad18cf 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCssStyle.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCssStyle.java @@ -95,6 +95,7 @@ public final class WebvttCssStyle { @FontSizeUnit private int fontSizeUnit; private float fontSize; @Nullable private Layout.Alignment textAlign; + private boolean combineUpright; // Calling reset() is forbidden because `this` isn't initialized. This can be safely suppressed // because reset() only assigns fields, it doesn't read any. @@ -118,6 +119,7 @@ public final class WebvttCssStyle { italic = UNSPECIFIED; fontSizeUnit = UNSPECIFIED; textAlign = null; + combineUpright = false; } public void setTargetId(String targetId) { @@ -220,7 +222,7 @@ public final class WebvttCssStyle { return fontFamily; } - public WebvttCssStyle setFontFamily(String fontFamily) { + public WebvttCssStyle setFontFamily(@Nullable String fontFamily) { this.fontFamily = Util.toLowerInvariant(fontFamily); return this; } @@ -264,7 +266,7 @@ public final class WebvttCssStyle { return textAlign; } - public WebvttCssStyle setTextAlign(Layout.Alignment textAlign) { + public WebvttCssStyle setTextAlign(@Nullable Layout.Alignment textAlign) { this.textAlign = textAlign; return this; } @@ -287,35 +289,12 @@ public final class WebvttCssStyle { return fontSize; } - public void cascadeFrom(WebvttCssStyle style) { - if (style.hasFontColor) { - setFontColor(style.fontColor); - } - if (style.bold != UNSPECIFIED) { - bold = style.bold; - } - if (style.italic != UNSPECIFIED) { - italic = style.italic; - } - if (style.fontFamily != null) { - fontFamily = style.fontFamily; - } - if (linethrough == UNSPECIFIED) { - linethrough = style.linethrough; - } - if (underline == UNSPECIFIED) { - underline = style.underline; - } - if (textAlign == null) { - textAlign = style.textAlign; - } - if (fontSizeUnit == UNSPECIFIED) { - fontSizeUnit = style.fontSizeUnit; - fontSize = style.fontSize; - } - if (style.hasBackgroundColor) { - setBackgroundColor(style.backgroundColor); - } + public void setCombineUpright(boolean enabled) { + this.combineUpright = enabled; + } + + public boolean getCombineUpright() { + return combineUpright; } private static int updateScoreForMatch( diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCue.java b/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCue.java deleted file mode 100644 index eae879c21b..0000000000 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCue.java +++ /dev/null @@ -1,320 +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.text.webvtt; - -import static java.lang.annotation.RetentionPolicy.SOURCE; - -import android.text.Layout.Alignment; -import androidx.annotation.IntDef; -import androidx.annotation.Nullable; -import com.google.android.exoplayer2.text.Cue; -import com.google.android.exoplayer2.util.Assertions; -import com.google.android.exoplayer2.util.Log; -import java.lang.annotation.Documented; -import java.lang.annotation.Retention; - -/** - * A representation of a WebVTT cue. - */ -public final class WebvttCue extends Cue { - - private static final float DEFAULT_POSITION = 0.5f; - - public final long startTime; - public final long endTime; - - private WebvttCue( - long startTime, - long endTime, - CharSequence text, - @Nullable Alignment textAlignment, - float line, - @Cue.LineType int lineType, - @Cue.AnchorType int lineAnchor, - float position, - @Cue.AnchorType int positionAnchor, - float width) { - super(text, textAlignment, line, lineType, lineAnchor, position, positionAnchor, width); - this.startTime = startTime; - this.endTime = endTime; - } - - /** - * Returns whether or not this cue should be placed in the default position and rolled-up with - * the other "normal" cues. - * - * @return Whether this cue should be placed in the default position. - */ - public boolean isNormalCue() { - return (line == DIMEN_UNSET && position == DEFAULT_POSITION); - } - - /** Builder for WebVTT cues. */ - @SuppressWarnings("hiding") - public static class Builder { - - /** - * Valid values for {@link #setTextAlignment(int)}. - * - *

      We use a custom list (and not {@link Alignment} directly) in order to include both {@code - * START}/{@code LEFT} and {@code END}/{@code RIGHT}. The distinction is important for {@link - * #derivePosition(int)}. - * - *

      These correspond to the valid values for the 'align' cue setting in the WebVTT spec. - */ - @Documented - @Retention(SOURCE) - @IntDef({ - TextAlignment.START, - TextAlignment.CENTER, - TextAlignment.END, - TextAlignment.LEFT, - TextAlignment.RIGHT - }) - public @interface TextAlignment { - /** - * See WebVTT's align:start. - */ - int START = 1; - /** - * See WebVTT's align:center. - */ - int CENTER = 2; - /** - * See WebVTT's align:end. - */ - int END = 3; - /** - * See WebVTT's align:left. - */ - int LEFT = 4; - /** - * See WebVTT's align:right. - */ - int RIGHT = 5; - } - - private static final String TAG = "WebvttCueBuilder"; - - private long startTime; - private long endTime; - @Nullable private CharSequence text; - @TextAlignment private int textAlignment; - private float line; - // Equivalent to WebVTT's snap-to-lines flag: - // https://www.w3.org/TR/webvtt1/#webvtt-cue-snap-to-lines-flag - @LineType private int lineType; - @AnchorType private int lineAnchor; - private float position; - @AnchorType private int positionAnchor; - private float width; - - // Initialization methods - - // Calling reset() is forbidden because `this` isn't initialized. This can be safely - // suppressed because reset() only assigns fields, it doesn't read any. - @SuppressWarnings("nullness:method.invocation.invalid") - public Builder() { - reset(); - } - - public void reset() { - startTime = 0; - endTime = 0; - text = null; - // Default: https://www.w3.org/TR/webvtt1/#webvtt-cue-text-alignment - textAlignment = TextAlignment.CENTER; - line = Cue.DIMEN_UNSET; - // Defaults to NUMBER (true): https://www.w3.org/TR/webvtt1/#webvtt-cue-snap-to-lines-flag - lineType = Cue.LINE_TYPE_NUMBER; - // Default: https://www.w3.org/TR/webvtt1/#webvtt-cue-line-alignment - lineAnchor = Cue.ANCHOR_TYPE_START; - position = Cue.DIMEN_UNSET; - positionAnchor = Cue.TYPE_UNSET; - // Default: https://www.w3.org/TR/webvtt1/#webvtt-cue-size - width = 1.0f; - } - - // Construction methods. - - public WebvttCue build() { - line = computeLine(line, lineType); - - if (position == Cue.DIMEN_UNSET) { - position = derivePosition(textAlignment); - } - - if (positionAnchor == Cue.TYPE_UNSET) { - positionAnchor = derivePositionAnchor(textAlignment); - } - - width = Math.min(width, deriveMaxSize(positionAnchor, position)); - - return new WebvttCue( - startTime, - endTime, - Assertions.checkNotNull(text), - convertTextAlignment(textAlignment), - line, - lineType, - lineAnchor, - position, - positionAnchor, - width); - } - - public Builder setStartTime(long time) { - startTime = time; - return this; - } - - public Builder setEndTime(long time) { - endTime = time; - return this; - } - - public Builder setText(CharSequence text) { - this.text = text; - return this; - } - - public Builder setTextAlignment(@TextAlignment int textAlignment) { - this.textAlignment = textAlignment; - return this; - } - - public Builder setLine(float line) { - this.line = line; - return this; - } - - public Builder setLineType(@LineType int lineType) { - this.lineType = lineType; - return this; - } - - public Builder setLineAnchor(@AnchorType int lineAnchor) { - this.lineAnchor = lineAnchor; - return this; - } - - public Builder setPosition(float position) { - this.position = position; - return this; - } - - public Builder setPositionAnchor(@AnchorType int positionAnchor) { - this.positionAnchor = positionAnchor; - return this; - } - - public Builder setWidth(float width) { - this.width = width; - return this; - } - - // https://www.w3.org/TR/webvtt1/#webvtt-cue-line - private static float computeLine(float line, @LineType int lineType) { - if (line != Cue.DIMEN_UNSET - && lineType == Cue.LINE_TYPE_FRACTION - && (line < 0.0f || line > 1.0f)) { - return 1.0f; // Step 1 - } else if (line != Cue.DIMEN_UNSET) { - // Step 2: Do nothing, line is already correct. - return line; - } else if (lineType == Cue.LINE_TYPE_FRACTION) { - return 1.0f; // Step 3 - } else { - // Steps 4 - 10 (stacking multiple simultaneous cues) are handled by WebvttSubtitle#getCues - // and WebvttCue#isNormalCue. - return DIMEN_UNSET; - } - } - - // https://www.w3.org/TR/webvtt1/#webvtt-cue-position - private static float derivePosition(@TextAlignment int textAlignment) { - switch (textAlignment) { - case TextAlignment.LEFT: - return 0.0f; - case TextAlignment.RIGHT: - return 1.0f; - case TextAlignment.START: - case TextAlignment.CENTER: - case TextAlignment.END: - default: - return DEFAULT_POSITION; - } - } - - // https://www.w3.org/TR/webvtt1/#webvtt-cue-position-alignment - @AnchorType - private static int derivePositionAnchor(@TextAlignment int textAlignment) { - switch (textAlignment) { - case TextAlignment.LEFT: - case TextAlignment.START: - return Cue.ANCHOR_TYPE_START; - case TextAlignment.RIGHT: - case TextAlignment.END: - return Cue.ANCHOR_TYPE_END; - case TextAlignment.CENTER: - default: - return Cue.ANCHOR_TYPE_MIDDLE; - } - } - - @Nullable - private static Alignment convertTextAlignment(@TextAlignment int textAlignment) { - switch (textAlignment) { - case TextAlignment.START: - case TextAlignment.LEFT: - return Alignment.ALIGN_NORMAL; - case TextAlignment.CENTER: - return Alignment.ALIGN_CENTER; - case TextAlignment.END: - case TextAlignment.RIGHT: - return Alignment.ALIGN_OPPOSITE; - default: - Log.w(TAG, "Unknown textAlignment: " + textAlignment); - return null; - } - } - - // Step 2 here: https://www.w3.org/TR/webvtt1/#processing-cue-settings - private static float deriveMaxSize(@AnchorType int positionAnchor, float position) { - switch (positionAnchor) { - case Cue.ANCHOR_TYPE_START: - return 1.0f - position; - case Cue.ANCHOR_TYPE_END: - return position; - case Cue.ANCHOR_TYPE_MIDDLE: - if (position <= 0.5f) { - return position * 2; - } else { - return (1.0f - position) * 2; - } - case Cue.TYPE_UNSET: - default: - throw new IllegalStateException(String.valueOf(positionAnchor)); - } - } - } -} diff --git a/library/core/src/test/java/com/google/android/exoplayer2/extractor/ts/PsExtractorTest.java b/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCueInfo.java similarity index 57% rename from library/core/src/test/java/com/google/android/exoplayer2/extractor/ts/PsExtractorTest.java rename to library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCueInfo.java index 4cf8f817fe..2119bd1c04 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/extractor/ts/PsExtractorTest.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCueInfo.java @@ -13,19 +13,21 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.google.android.exoplayer2.extractor.ts; +package com.google.android.exoplayer2.text.webvtt; -import androidx.test.ext.junit.runners.AndroidJUnit4; -import com.google.android.exoplayer2.testutil.ExtractorAsserts; -import org.junit.Test; -import org.junit.runner.RunWith; -/** Unit test for {@link PsExtractor}. */ -@RunWith(AndroidJUnit4.class) -public final class PsExtractorTest { +import com.google.android.exoplayer2.text.Cue; - @Test - public void testSample() throws Exception { - ExtractorAsserts.assertBehavior(PsExtractor::new, "ts/sample.ps"); +/** A representation of a WebVTT cue. */ +public final class WebvttCueInfo { + + public final Cue cue; + public final long startTimeUs; + public final long endTimeUs; + + public WebvttCueInfo(Cue cue, long startTimeUs, long endTimeUs) { + this.cue = cue; + this.startTimeUs = startTimeUs; + this.endTimeUs = endTimeUs; } } 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 f587d70e90..3c974d8a41 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 @@ -15,11 +15,15 @@ */ package com.google.android.exoplayer2.text.webvtt; +import static com.google.android.exoplayer2.text.span.SpanUtil.addOrReplaceSpan; +import static java.lang.annotation.RetentionPolicy.SOURCE; + +import android.graphics.Color; import android.graphics.Typeface; import android.text.Layout; -import android.text.Spannable; import android.text.SpannableStringBuilder; import android.text.Spanned; +import android.text.SpannedString; import android.text.TextUtils; import android.text.style.AbsoluteSizeSpan; import android.text.style.AlignmentSpan; @@ -30,28 +34,82 @@ import android.text.style.StrikethroughSpan; import android.text.style.StyleSpan; import android.text.style.TypefaceSpan; import android.text.style.UnderlineSpan; -import androidx.annotation.NonNull; +import androidx.annotation.IntDef; import androidx.annotation.Nullable; import com.google.android.exoplayer2.text.Cue; +import com.google.android.exoplayer2.text.span.HorizontalTextInVerticalContextSpan; +import com.google.android.exoplayer2.text.span.RubySpan; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.Util; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; -/** - * Parser for WebVTT cues. (https://w3c.github.io/webvtt/#cues) - */ +/** Parser for WebVTT cues. (https://w3c.github.io/webvtt/#cues) */ public final class WebvttCueParser { + /** + * Valid values for {@link WebvttCueInfoBuilder#textAlignment}. + * + *

      We use a custom list (and not {@link Layout.Alignment} directly) in order to include both + * {@code START}/{@code LEFT} and {@code END}/{@code RIGHT}. The distinction is important for + * {@link WebvttCueInfoBuilder#derivePosition(int)}. + * + *

      These correspond to the valid values for the 'align' cue setting in the WebVTT spec. + */ + @Documented + @Retention(SOURCE) + @IntDef({ + TEXT_ALIGNMENT_START, + TEXT_ALIGNMENT_CENTER, + TEXT_ALIGNMENT_END, + TEXT_ALIGNMENT_LEFT, + TEXT_ALIGNMENT_RIGHT + }) + private @interface TextAlignment {} + + /** + * See WebVTT's align:start. + */ + private static final int TEXT_ALIGNMENT_START = 1; + + /** + * See WebVTT's align:center. + */ + private static final int TEXT_ALIGNMENT_CENTER = 2; + + /** + * See WebVTT's align:end. + */ + private static final int TEXT_ALIGNMENT_END = 3; + + /** + * See WebVTT's align:left. + */ + private static final int TEXT_ALIGNMENT_LEFT = 4; + + /** + * See WebVTT's align:right. + */ + private static final int TEXT_ALIGNMENT_RIGHT = 5; + public static final Pattern CUE_HEADER_PATTERN = Pattern .compile("^(\\S+)\\s+-->\\s+(\\S+)(.*)?$"); - private static final Pattern CUE_SETTING_PATTERN = Pattern.compile("(\\S+?):(\\S+)"); private static final char CHAR_LESS_THAN = '<'; @@ -67,101 +125,124 @@ public final class WebvttCueParser { private static final String ENTITY_NON_BREAK_SPACE = "nbsp"; private static final String TAG_BOLD = "b"; - private static final String TAG_ITALIC = "i"; - private static final String TAG_UNDERLINE = "u"; private static final String TAG_CLASS = "c"; - private static final String TAG_VOICE = "v"; + private static final String TAG_ITALIC = "i"; private static final String TAG_LANG = "lang"; + private static final String TAG_RUBY = "ruby"; + private static final String TAG_RUBY_TEXT = "rt"; + private static final String TAG_UNDERLINE = "u"; + private static final String TAG_VOICE = "v"; private static final int STYLE_BOLD = Typeface.BOLD; private static final int STYLE_ITALIC = Typeface.ITALIC; + /* package */ static final float DEFAULT_POSITION = 0.5f; + private static final String TAG = "WebvttCueParser"; - private final StringBuilder textBuilder; + /** + * See WebVTT's default text + * colors. + */ + private static final Map DEFAULT_TEXT_COLORS; - public WebvttCueParser() { - textBuilder = new StringBuilder(); + static { + Map defaultColors = new HashMap<>(); + defaultColors.put("white", Color.rgb(255, 255, 255)); + defaultColors.put("lime", Color.rgb(0, 255, 0)); + defaultColors.put("cyan", Color.rgb(0, 255, 255)); + defaultColors.put("red", Color.rgb(255, 0, 0)); + defaultColors.put("yellow", Color.rgb(255, 255, 0)); + defaultColors.put("magenta", Color.rgb(255, 0, 255)); + defaultColors.put("blue", Color.rgb(0, 0, 255)); + defaultColors.put("black", Color.rgb(0, 0, 0)); + DEFAULT_TEXT_COLORS = Collections.unmodifiableMap(defaultColors); + } + + /** + * See WebVTT's default text + * background colors. + */ + private static final Map DEFAULT_BACKGROUND_COLORS; + + static { + Map defaultBackgroundColors = new HashMap<>(); + defaultBackgroundColors.put("bg_white", Color.rgb(255, 255, 255)); + defaultBackgroundColors.put("bg_lime", Color.rgb(0, 255, 0)); + defaultBackgroundColors.put("bg_cyan", Color.rgb(0, 255, 255)); + defaultBackgroundColors.put("bg_red", Color.rgb(255, 0, 0)); + defaultBackgroundColors.put("bg_yellow", Color.rgb(255, 255, 0)); + defaultBackgroundColors.put("bg_magenta", Color.rgb(255, 0, 255)); + defaultBackgroundColors.put("bg_blue", Color.rgb(0, 0, 255)); + defaultBackgroundColors.put("bg_black", Color.rgb(0, 0, 0)); + DEFAULT_BACKGROUND_COLORS = Collections.unmodifiableMap(defaultBackgroundColors); } /** * Parses the next valid WebVTT cue in a parsable array, including timestamps, settings and text. * * @param webvttData Parsable WebVTT file data. - * @param builder Builder for WebVTT Cues (output parameter). * @param styles List of styles defined by the CSS style blocks preceding the cues. - * @return Whether a valid Cue was found. + * @return The parsed cue info, or null if no valid cue was found. */ - public boolean parseCue( - ParsableByteArray webvttData, WebvttCue.Builder builder, List styles) { - String firstLine = webvttData.readLine(); + @Nullable + public static WebvttCueInfo parseCue(ParsableByteArray webvttData, List styles) { + @Nullable String firstLine = webvttData.readLine(); if (firstLine == null) { - return false; + return null; } Matcher cueHeaderMatcher = WebvttCueParser.CUE_HEADER_PATTERN.matcher(firstLine); if (cueHeaderMatcher.matches()) { // We have found the timestamps in the first line. No id present. - return parseCue(null, cueHeaderMatcher, webvttData, builder, textBuilder, styles); + return parseCue(null, cueHeaderMatcher, webvttData, styles); } // The first line is not the timestamps, but could be the cue id. - String secondLine = webvttData.readLine(); + @Nullable String secondLine = webvttData.readLine(); if (secondLine == null) { - return false; + return null; } cueHeaderMatcher = WebvttCueParser.CUE_HEADER_PATTERN.matcher(secondLine); if (cueHeaderMatcher.matches()) { // We can do the rest of the parsing, including the id. - return parseCue(firstLine.trim(), cueHeaderMatcher, webvttData, builder, textBuilder, - styles); + return parseCue(firstLine.trim(), cueHeaderMatcher, webvttData, styles); } - return false; + return null; } /** * Parses a string containing a list of cue settings. * * @param cueSettingsList String containing the settings for a given cue. - * @param builder The {@link WebvttCue.Builder} where incremental construction takes place. + * @return The cue settings parsed into a {@link Cue.Builder}. */ - /* package */ static void parseCueSettingsList(String cueSettingsList, - WebvttCue.Builder builder) { - // Parse the cue settings list. - Matcher cueSettingMatcher = CUE_SETTING_PATTERN.matcher(cueSettingsList); - while (cueSettingMatcher.find()) { - String name = cueSettingMatcher.group(1); - String value = cueSettingMatcher.group(2); - try { - if ("line".equals(name)) { - parseLineAttribute(value, builder); - } else if ("align".equals(name)) { - builder.setTextAlignment(parseTextAlignment(value)); - } else if ("position".equals(name)) { - parsePositionAttribute(value, builder); - } else if ("size".equals(name)) { - builder.setWidth(WebvttParserUtil.parsePercentage(value)); - } else { - Log.w(TAG, "Unknown cue setting " + name + ":" + value); - } - } catch (NumberFormatException e) { - Log.w(TAG, "Skipping bad cue setting: " + cueSettingMatcher.group()); - } - } + /* package */ static Cue.Builder parseCueSettingsList(String cueSettingsList) { + WebvttCueInfoBuilder builder = new WebvttCueInfoBuilder(); + parseCueSettingsList(cueSettingsList, builder); + return builder.toCueBuilder(); + } + + /** Create a new {@link Cue} containing {@code text} and with WebVTT default values. */ + /* package */ static Cue newCueForText(CharSequence text) { + WebvttCueInfoBuilder infoBuilder = new WebvttCueInfoBuilder(); + infoBuilder.text = text; + return infoBuilder.toCueBuilder().build(); } /** - * Parses the text payload of a WebVTT Cue and applies modifications on {@link WebvttCue.Builder}. + * Parses the text payload of a WebVTT Cue and returns it as a styled {@link SpannedString}. * - * @param id Id of the cue, {@code null} if it is not present. + * @param id ID of the cue, {@code null} if it is not present. * @param markup The markup text to be parsed. * @param styles List of styles defined by the CSS style blocks preceding the cues. - * @param builder Output builder. + * @return The styled cue text. */ - /* package */ static void parseCueText( - @Nullable String id, String markup, WebvttCue.Builder builder, List styles) { + /* package */ static SpannedString parseCueText( + @Nullable String id, String markup, List styles) { SpannableStringBuilder spannedText = new SpannableStringBuilder(); ArrayDeque startTagStack = new ArrayDeque<>(); List scratchStyleMatches = new ArrayList<>(); int pos = 0; + List nestedElements = new ArrayList<>(); while (pos < markup.length()) { char curr = markup.charAt(pos); switch (curr) { @@ -190,8 +271,14 @@ public final class WebvttCueParser { break; } startTag = startTagStack.pop(); - applySpansForTag(id, startTag, spannedText, styles, scratchStyleMatches); - } while(!startTag.name.equals(tagName)); + applySpansForTag( + id, startTag, nestedElements, spannedText, styles, scratchStyleMatches); + if (!startTagStack.isEmpty()) { + nestedElements.add(new Element(startTag, spannedText.length())); + } else { + nestedElements.clear(); + } + } while (!startTag.name.equals(tagName)); } else if (!isVoidTag) { startTagStack.push(StartTag.buildStartTag(fullTagExpression, spannedText.length())); } @@ -221,33 +308,43 @@ public final class WebvttCueParser { } // apply unclosed tags while (!startTagStack.isEmpty()) { - applySpansForTag(id, startTagStack.pop(), spannedText, styles, scratchStyleMatches); + applySpansForTag( + id, startTagStack.pop(), nestedElements, spannedText, styles, scratchStyleMatches); } - applySpansForTag(id, StartTag.buildWholeCueVirtualTag(), spannedText, styles, + applySpansForTag( + id, + StartTag.buildWholeCueVirtualTag(), + /* nestedElements= */ Collections.emptyList(), + spannedText, + styles, scratchStyleMatches); - builder.setText(spannedText); + return SpannedString.valueOf(spannedText); } - private static boolean parseCue( + // Internal methods + + @Nullable + private static WebvttCueInfo parseCue( @Nullable String id, Matcher cueHeaderMatcher, ParsableByteArray webvttData, - WebvttCue.Builder builder, - StringBuilder textBuilder, List styles) { + WebvttCueInfoBuilder builder = new WebvttCueInfoBuilder(); try { // Parse the cue start and end times. - builder.setStartTime(WebvttParserUtil.parseTimestampUs(cueHeaderMatcher.group(1))) - .setEndTime(WebvttParserUtil.parseTimestampUs(cueHeaderMatcher.group(2))); + builder.startTimeUs = + WebvttParserUtil.parseTimestampUs(Assertions.checkNotNull(cueHeaderMatcher.group(1))); + builder.endTimeUs = + WebvttParserUtil.parseTimestampUs(Assertions.checkNotNull(cueHeaderMatcher.group(2))); } catch (NumberFormatException e) { Log.w(TAG, "Skipping cue with bad header: " + cueHeaderMatcher.group()); - return false; + return null; } - parseCueSettingsList(cueHeaderMatcher.group(3), builder); + parseCueSettingsList(Assertions.checkNotNull(cueHeaderMatcher.group(3)), builder); // Parse the cue text. - textBuilder.setLength(0); + StringBuilder textBuilder = new StringBuilder(); for (String line = webvttData.readLine(); !TextUtils.isEmpty(line); line = webvttData.readLine()) { @@ -256,20 +353,46 @@ public final class WebvttCueParser { } textBuilder.append(line.trim()); } - parseCueText(id, textBuilder.toString(), builder, styles); - return true; + builder.text = parseCueText(id, textBuilder.toString(), styles); + return builder.build(); } - // Internal methods + private static void parseCueSettingsList(String cueSettingsList, WebvttCueInfoBuilder builder) { + // Parse the cue settings list. + Matcher cueSettingMatcher = CUE_SETTING_PATTERN.matcher(cueSettingsList); - private static void parseLineAttribute(String s, WebvttCue.Builder builder) { + while (cueSettingMatcher.find()) { + String name = Assertions.checkNotNull(cueSettingMatcher.group(1)); + String value = Assertions.checkNotNull(cueSettingMatcher.group(2)); + try { + if ("line".equals(name)) { + parseLineAttribute(value, builder); + } else if ("align".equals(name)) { + builder.textAlignment = parseTextAlignment(value); + } else if ("position".equals(name)) { + parsePositionAttribute(value, builder); + } else if ("size".equals(name)) { + builder.size = WebvttParserUtil.parsePercentage(value); + } else if ("vertical".equals(name)) { + builder.verticalType = parseVerticalAttribute(value); + } else { + Log.w(TAG, "Unknown cue setting " + name + ":" + value); + } + } catch (NumberFormatException e) { + Log.w(TAG, "Skipping bad cue setting: " + cueSettingMatcher.group()); + } + } + } + + private static void parseLineAttribute(String s, WebvttCueInfoBuilder builder) { int commaIndex = s.indexOf(','); if (commaIndex != -1) { - builder.setLineAnchor(parsePositionAnchor(s.substring(commaIndex + 1))); + builder.lineAnchor = parseLineAnchor(s.substring(commaIndex + 1)); s = s.substring(0, commaIndex); } if (s.endsWith("%")) { - builder.setLine(WebvttParserUtil.parsePercentage(s)).setLineType(Cue.LINE_TYPE_FRACTION); + builder.line = WebvttParserUtil.parsePercentage(s); + builder.lineType = Cue.LINE_TYPE_FRACTION; } else { int lineNumber = Integer.parseInt(s); if (lineNumber < 0) { @@ -277,21 +400,13 @@ public final class WebvttCueParser { // Cue defines it to be the first row that's not visible. lineNumber--; } - builder.setLine(lineNumber).setLineType(Cue.LINE_TYPE_NUMBER); + builder.line = lineNumber; + builder.lineType = Cue.LINE_TYPE_NUMBER; } } - private static void parsePositionAttribute(String s, WebvttCue.Builder builder) { - int commaIndex = s.indexOf(','); - if (commaIndex != -1) { - builder.setPositionAnchor(parsePositionAnchor(s.substring(commaIndex + 1))); - s = s.substring(0, commaIndex); - } - builder.setPosition(WebvttParserUtil.parsePercentage(s)); - } - @Cue.AnchorType - private static int parsePositionAnchor(String s) { + private static int parseLineAnchor(String s) { switch (s) { case "start": return Cue.ANCHOR_TYPE_START; @@ -306,24 +421,64 @@ public final class WebvttCueParser { } } - @WebvttCue.Builder.TextAlignment + private static void parsePositionAttribute(String s, WebvttCueInfoBuilder builder) { + int commaIndex = s.indexOf(','); + if (commaIndex != -1) { + builder.positionAnchor = parsePositionAnchor(s.substring(commaIndex + 1)); + s = s.substring(0, commaIndex); + } + builder.position = WebvttParserUtil.parsePercentage(s); + } + + @Cue.AnchorType + private static int parsePositionAnchor(String s) { + switch (s) { + case "line-left": + case "start": + return Cue.ANCHOR_TYPE_START; + case "center": + case "middle": + return Cue.ANCHOR_TYPE_MIDDLE; + case "line-right": + case "end": + return Cue.ANCHOR_TYPE_END; + default: + Log.w(TAG, "Invalid anchor value: " + s); + return Cue.TYPE_UNSET; + } + } + + @Cue.VerticalType + private static int parseVerticalAttribute(String s) { + switch (s) { + case "rl": + return Cue.VERTICAL_TYPE_RL; + case "lr": + return Cue.VERTICAL_TYPE_LR; + default: + Log.w(TAG, "Invalid 'vertical' value: " + s); + return Cue.TYPE_UNSET; + } + } + + @TextAlignment private static int parseTextAlignment(String s) { switch (s) { case "start": - return WebvttCue.Builder.TextAlignment.START; + return TEXT_ALIGNMENT_START; case "left": - return WebvttCue.Builder.TextAlignment.LEFT; + return TEXT_ALIGNMENT_LEFT; case "center": case "middle": - return WebvttCue.Builder.TextAlignment.CENTER; + return TEXT_ALIGNMENT_CENTER; case "end": - return WebvttCue.Builder.TextAlignment.END; + return TEXT_ALIGNMENT_END; case "right": - return WebvttCue.Builder.TextAlignment.RIGHT; + return TEXT_ALIGNMENT_RIGHT; default: Log.w(TAG, "Invalid alignment value: " + s); // Default value: https://www.w3.org/TR/webvtt1/#webvtt-cue-text-alignment - return WebvttCue.Builder.TextAlignment.CENTER; + return TEXT_ALIGNMENT_CENTER; } } @@ -365,6 +520,8 @@ public final class WebvttCueParser { case TAG_CLASS: case TAG_ITALIC: case TAG_LANG: + case TAG_RUBY: + case TAG_RUBY_TEXT: case TAG_UNDERLINE: case TAG_VOICE: return true; @@ -376,6 +533,7 @@ public final class WebvttCueParser { private static void applySpansForTag( @Nullable String cueId, StartTag startTag, + List nestedElements, SpannableStringBuilder text, List styles, List scratchStyleMatches) { @@ -390,10 +548,15 @@ public final class WebvttCueParser { text.setSpan(new StyleSpan(STYLE_ITALIC), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); break; + case TAG_RUBY: + applyRubySpans(nestedElements, text, start); + break; case TAG_UNDERLINE: text.setSpan(new UnderlineSpan(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); break; case TAG_CLASS: + applyDefaultColors(text, startTag.classes, start, end); + break; case TAG_LANG: case TAG_VOICE: case "": // Case of the "whole cue" virtual tag. @@ -409,13 +572,65 @@ public final class WebvttCueParser { } } + private static void applyRubySpans( + List nestedElements, SpannableStringBuilder text, int startTagPosition) { + List sortedNestedElements = new ArrayList<>(nestedElements.size()); + sortedNestedElements.addAll(nestedElements); + Collections.sort(sortedNestedElements, Element.BY_START_POSITION_ASC); + int deletedCharCount = 0; + int lastRubyTextEnd = startTagPosition; + for (int i = 0; i < sortedNestedElements.size(); i++) { + if (!TAG_RUBY_TEXT.equals(sortedNestedElements.get(i).startTag.name)) { + continue; + } + Element rubyTextElement = sortedNestedElements.get(i); + // Move the rubyText from spannedText into the RubySpan. + int adjustedRubyTextStart = rubyTextElement.startTag.position - deletedCharCount; + int adjustedRubyTextEnd = rubyTextElement.endPosition - deletedCharCount; + CharSequence rubyText = text.subSequence(adjustedRubyTextStart, adjustedRubyTextEnd); + text.delete(adjustedRubyTextStart, adjustedRubyTextEnd); + text.setSpan( + new RubySpan(rubyText.toString(), RubySpan.POSITION_OVER), + lastRubyTextEnd, + adjustedRubyTextStart, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + deletedCharCount += rubyText.length(); + // The ruby text has been deleted, so new-start == old-end. + lastRubyTextEnd = adjustedRubyTextStart; + } + } + + /** + * Adds {@link ForegroundColorSpan}s and {@link BackgroundColorSpan}s to {@code text} for entries + * in {@code classes} that match WebVTT's default text colors or default text background + * colors. + */ + private static void applyDefaultColors( + SpannableStringBuilder text, String[] classes, int start, int end) { + for (String className : classes) { + if (DEFAULT_TEXT_COLORS.containsKey(className)) { + int color = DEFAULT_TEXT_COLORS.get(className); + text.setSpan(new ForegroundColorSpan(color), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } else if (DEFAULT_BACKGROUND_COLORS.containsKey(className)) { + int color = DEFAULT_BACKGROUND_COLORS.get(className); + text.setSpan(new BackgroundColorSpan(color), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } + } + } + private static void applyStyleToText(SpannableStringBuilder spannedText, WebvttCssStyle style, int start, int end) { if (style == null) { return; } if (style.getStyle() != WebvttCssStyle.UNSPECIFIED) { - spannedText.setSpan(new StyleSpan(style.getStyle()), start, end, + addOrReplaceSpan( + spannedText, + new StyleSpan(style.getStyle()), + start, + end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); } if (style.isLinethrough()) { @@ -425,39 +640,71 @@ public final class WebvttCueParser { spannedText.setSpan(new UnderlineSpan(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); } if (style.hasFontColor()) { - spannedText.setSpan(new ForegroundColorSpan(style.getFontColor()), start, end, - Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + addOrReplaceSpan( + spannedText, + new ForegroundColorSpan(style.getFontColor()), + start, + end, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); } if (style.hasBackgroundColor()) { - spannedText.setSpan(new BackgroundColorSpan(style.getBackgroundColor()), start, end, - Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + addOrReplaceSpan( + spannedText, + new BackgroundColorSpan(style.getBackgroundColor()), + start, + end, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); } if (style.getFontFamily() != null) { - spannedText.setSpan(new TypefaceSpan(style.getFontFamily()), start, end, + addOrReplaceSpan( + spannedText, + new TypefaceSpan(style.getFontFamily()), + start, + end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); } Layout.Alignment textAlign = style.getTextAlign(); if (textAlign != null) { - spannedText.setSpan( - new AlignmentSpan.Standard(textAlign), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + addOrReplaceSpan( + spannedText, + new AlignmentSpan.Standard(textAlign), + start, + end, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); } switch (style.getFontSizeUnit()) { case WebvttCssStyle.FONT_SIZE_UNIT_PIXEL: - spannedText.setSpan(new AbsoluteSizeSpan((int) style.getFontSize(), true), start, end, + addOrReplaceSpan( + spannedText, + new AbsoluteSizeSpan((int) style.getFontSize(), true), + start, + end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); break; case WebvttCssStyle.FONT_SIZE_UNIT_EM: - spannedText.setSpan(new RelativeSizeSpan(style.getFontSize()), start, end, + addOrReplaceSpan( + spannedText, + new RelativeSizeSpan(style.getFontSize()), + start, + end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); break; case WebvttCssStyle.FONT_SIZE_UNIT_PERCENT: - spannedText.setSpan(new RelativeSizeSpan(style.getFontSize() / 100), start, end, + addOrReplaceSpan( + spannedText, + new RelativeSizeSpan(style.getFontSize() / 100), + start, + end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); break; case WebvttCssStyle.UNSPECIFIED: // Do nothing. break; } + if (style.getCombineUpright()) { + spannedText.setSpan( + new HorizontalTextInVerticalContextSpan(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } } /** @@ -488,6 +735,154 @@ public final class WebvttCueParser { Collections.sort(output); } + private static final class WebvttCueInfoBuilder { + + public long startTimeUs; + public long endTimeUs; + public @MonotonicNonNull CharSequence text; + @TextAlignment public int textAlignment; + public float line; + // Equivalent to WebVTT's snap-to-lines flag: + // https://www.w3.org/TR/webvtt1/#webvtt-cue-snap-to-lines-flag + @Cue.LineType public int lineType; + @Cue.AnchorType public int lineAnchor; + public float position; + @Cue.AnchorType public int positionAnchor; + public float size; + @Cue.VerticalType public int verticalType; + + public WebvttCueInfoBuilder() { + startTimeUs = 0; + endTimeUs = 0; + // Default: https://www.w3.org/TR/webvtt1/#webvtt-cue-text-alignment + textAlignment = TEXT_ALIGNMENT_CENTER; + line = Cue.DIMEN_UNSET; + // Defaults to NUMBER (true): https://www.w3.org/TR/webvtt1/#webvtt-cue-snap-to-lines-flag + lineType = Cue.LINE_TYPE_NUMBER; + // Default: https://www.w3.org/TR/webvtt1/#webvtt-cue-line-alignment + lineAnchor = Cue.ANCHOR_TYPE_START; + position = Cue.DIMEN_UNSET; + positionAnchor = Cue.TYPE_UNSET; + // Default: https://www.w3.org/TR/webvtt1/#webvtt-cue-size + size = 1.0f; + verticalType = Cue.TYPE_UNSET; + } + + public WebvttCueInfo build() { + return new WebvttCueInfo(toCueBuilder().build(), startTimeUs, endTimeUs); + } + + public Cue.Builder toCueBuilder() { + float position = + this.position != Cue.DIMEN_UNSET ? this.position : derivePosition(textAlignment); + @Cue.AnchorType + int positionAnchor = + this.positionAnchor != Cue.TYPE_UNSET + ? this.positionAnchor + : derivePositionAnchor(textAlignment); + Cue.Builder cueBuilder = + new Cue.Builder() + .setTextAlignment(convertTextAlignment(textAlignment)) + .setLine(computeLine(line, lineType), lineType) + .setLineAnchor(lineAnchor) + .setPosition(position) + .setPositionAnchor(positionAnchor) + .setSize(Math.min(size, deriveMaxSize(positionAnchor, position))) + .setVerticalType(verticalType); + + if (text != null) { + cueBuilder.setText(text); + } + + return cueBuilder; + } + + // https://www.w3.org/TR/webvtt1/#webvtt-cue-line + private static float computeLine(float line, @Cue.LineType int lineType) { + if (line != Cue.DIMEN_UNSET + && lineType == Cue.LINE_TYPE_FRACTION + && (line < 0.0f || line > 1.0f)) { + return 1.0f; // Step 1 + } else if (line != Cue.DIMEN_UNSET) { + // Step 2: Do nothing, line is already correct. + return line; + } else if (lineType == Cue.LINE_TYPE_FRACTION) { + return 1.0f; // Step 3 + } else { + // Steps 4 - 10 (stacking multiple simultaneous cues) are handled by + // WebvttSubtitle.getCues(long) and WebvttSubtitle.isNormal(Cue). + return Cue.DIMEN_UNSET; + } + } + + // https://www.w3.org/TR/webvtt1/#webvtt-cue-position + private static float derivePosition(@TextAlignment int textAlignment) { + switch (textAlignment) { + case TEXT_ALIGNMENT_LEFT: + return 0.0f; + case TEXT_ALIGNMENT_RIGHT: + return 1.0f; + case TEXT_ALIGNMENT_START: + case TEXT_ALIGNMENT_CENTER: + case TEXT_ALIGNMENT_END: + default: + return DEFAULT_POSITION; + } + } + + // https://www.w3.org/TR/webvtt1/#webvtt-cue-position-alignment + @Cue.AnchorType + private static int derivePositionAnchor(@TextAlignment int textAlignment) { + switch (textAlignment) { + case TEXT_ALIGNMENT_LEFT: + case TEXT_ALIGNMENT_START: + return Cue.ANCHOR_TYPE_START; + case TEXT_ALIGNMENT_RIGHT: + case TEXT_ALIGNMENT_END: + return Cue.ANCHOR_TYPE_END; + case TEXT_ALIGNMENT_CENTER: + default: + return Cue.ANCHOR_TYPE_MIDDLE; + } + } + + @Nullable + private static Layout.Alignment convertTextAlignment(@TextAlignment int textAlignment) { + switch (textAlignment) { + case TEXT_ALIGNMENT_START: + case TEXT_ALIGNMENT_LEFT: + return Layout.Alignment.ALIGN_NORMAL; + case TEXT_ALIGNMENT_CENTER: + return Layout.Alignment.ALIGN_CENTER; + case TEXT_ALIGNMENT_END: + case TEXT_ALIGNMENT_RIGHT: + return Layout.Alignment.ALIGN_OPPOSITE; + default: + Log.w(TAG, "Unknown textAlignment: " + textAlignment); + return null; + } + } + + // Step 2 here: https://www.w3.org/TR/webvtt1/#processing-cue-settings + private static float deriveMaxSize(@Cue.AnchorType int positionAnchor, float position) { + switch (positionAnchor) { + case Cue.ANCHOR_TYPE_START: + return 1.0f - position; + case Cue.ANCHOR_TYPE_END: + return position; + case Cue.ANCHOR_TYPE_MIDDLE: + if (position <= 0.5f) { + return position * 2; + } else { + return (1.0f - position) * 2; + } + case Cue.TYPE_UNSET: + default: + throw new IllegalStateException(String.valueOf(positionAnchor)); + } + } + } + private static final class StyleMatch implements Comparable { public final int score; @@ -499,7 +894,7 @@ public final class WebvttCueParser { } @Override - public int compareTo(@NonNull StyleMatch another) { + public int compareTo(StyleMatch another) { return this.score - another.score; } @@ -549,4 +944,21 @@ public final class WebvttCueParser { } + /** Information about a complete element (i.e. start tag and end position). */ + private static class Element { + private static final Comparator BY_START_POSITION_ASC = + (e1, e2) -> Integer.compare(e1.startTag.position, e2.startTag.position); + + private final StartTag startTag; + /** + * The position of the end of this element's text in the un-marked-up cue text (i.e. the + * corollary to {@link StartTag#position}). + */ + private final int endPosition; + + private Element(StartTag startTag, int endPosition) { + this.startTag = startTag; + this.endPosition = endPosition; + } + } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttDecoder.java index 9b356f0988..fe36770aee 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttDecoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttDecoder.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.text.webvtt; import android.text.TextUtils; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.text.SimpleSubtitleDecoder; import com.google.android.exoplayer2.text.Subtitle; @@ -40,28 +41,20 @@ public final class WebvttDecoder extends SimpleSubtitleDecoder { private static final String COMMENT_START = "NOTE"; private static final String STYLE_START = "STYLE"; - private final WebvttCueParser cueParser; private final ParsableByteArray parsableWebvttData; - private final WebvttCue.Builder webvttCueBuilder; private final CssParser cssParser; - private final List definedStyles; public WebvttDecoder() { super("WebvttDecoder"); - cueParser = new WebvttCueParser(); parsableWebvttData = new ParsableByteArray(); - webvttCueBuilder = new WebvttCue.Builder(); cssParser = new CssParser(); - definedStyles = new ArrayList<>(); } @Override protected Subtitle decode(byte[] bytes, int length, boolean reset) throws SubtitleDecoderException { parsableWebvttData.reset(bytes, length); - // Initialization for consistent starting state. - webvttCueBuilder.reset(); - definedStyles.clear(); + List definedStyles = new ArrayList<>(); // Validate the first line of the header, and skip the remainder. try { @@ -72,24 +65,25 @@ public final class WebvttDecoder extends SimpleSubtitleDecoder { while (!TextUtils.isEmpty(parsableWebvttData.readLine())) {} int event; - ArrayList subtitles = new ArrayList<>(); + List cueInfos = new ArrayList<>(); while ((event = getNextEvent(parsableWebvttData)) != EVENT_END_OF_FILE) { if (event == EVENT_COMMENT) { skipComment(parsableWebvttData); } else if (event == EVENT_STYLE_BLOCK) { - if (!subtitles.isEmpty()) { + if (!cueInfos.isEmpty()) { throw new SubtitleDecoderException("A style block was found after the first cue."); } parsableWebvttData.readLine(); // Consume the "STYLE" header. definedStyles.addAll(cssParser.parseBlock(parsableWebvttData)); } else if (event == EVENT_CUE) { - if (cueParser.parseCue(parsableWebvttData, webvttCueBuilder, definedStyles)) { - subtitles.add(webvttCueBuilder.build()); - webvttCueBuilder.reset(); + @Nullable + WebvttCueInfo cueInfo = WebvttCueParser.parseCue(parsableWebvttData, definedStyles); + if (cueInfo != null) { + cueInfos.add(cueInfo); } } } - return new WebvttSubtitle(subtitles); + return new WebvttSubtitle(cueInfos); } /** 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 dce8f8157f..9075083111 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 @@ -52,7 +52,7 @@ public final class WebvttParserUtil { * @param input The input from which the line should be read. */ public static boolean isWebvttHeaderLine(ParsableByteArray input) { - String line = input.readLine(); + @Nullable String line = input.readLine(); return line != null && line.startsWith(WEBVTT_HEADER); } @@ -101,7 +101,7 @@ public final class WebvttParserUtil { */ @Nullable public static Matcher findNextCueHeader(ParsableByteArray input) { - String line; + @Nullable String line; while ((line = input.readLine()) != null) { if (COMMENT.matcher(line).matches()) { // Skip until the end of the comment block. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttSubtitle.java b/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttSubtitle.java index 2833ff2d0b..6832033165 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttSubtitle.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttSubtitle.java @@ -15,7 +15,6 @@ */ package com.google.android.exoplayer2.text.webvtt; -import android.text.SpannableStringBuilder; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.text.Cue; import com.google.android.exoplayer2.text.Subtitle; @@ -23,6 +22,7 @@ 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.Collections; import java.util.List; /** @@ -30,23 +30,19 @@ import java.util.List; */ /* package */ final class WebvttSubtitle implements Subtitle { - private final List cues; - private final int numCues; + private final List cueInfos; private final long[] cueTimesUs; private final long[] sortedCueTimesUs; - /** - * @param cues A list of the cues in this subtitle. - */ - public WebvttSubtitle(List cues) { - this.cues = cues; - numCues = cues.size(); - cueTimesUs = new long[2 * numCues]; - for (int cueIndex = 0; cueIndex < numCues; cueIndex++) { - WebvttCue cue = cues.get(cueIndex); + /** Constructs a new WebvttSubtitle from a list of {@link WebvttCueInfo}s. */ + public WebvttSubtitle(List cueInfos) { + this.cueInfos = Collections.unmodifiableList(new ArrayList<>(cueInfos)); + cueTimesUs = new long[2 * cueInfos.size()]; + for (int cueIndex = 0; cueIndex < cueInfos.size(); cueIndex++) { + WebvttCueInfo cueInfo = cueInfos.get(cueIndex); int arrayIndex = cueIndex * 2; - cueTimesUs[arrayIndex] = cue.startTime; - cueTimesUs[arrayIndex + 1] = cue.endTime; + cueTimesUs[arrayIndex] = cueInfo.startTimeUs; + cueTimesUs[arrayIndex + 1] = cueInfo.endTimeUs; } sortedCueTimesUs = Arrays.copyOf(cueTimesUs, cueTimesUs.length); Arrays.sort(sortedCueTimesUs); @@ -72,44 +68,34 @@ import java.util.List; @Override public List getCues(long timeUs) { - List list = new ArrayList<>(); - WebvttCue firstNormalCue = null; - SpannableStringBuilder normalCueTextBuilder = null; - - for (int i = 0; i < numCues; i++) { + List currentCues = new ArrayList<>(); + List cuesWithUnsetLine = new ArrayList<>(); + for (int i = 0; i < cueInfos.size(); i++) { if ((cueTimesUs[i * 2] <= timeUs) && (timeUs < cueTimesUs[i * 2 + 1])) { - WebvttCue cue = cues.get(i); - // TODO(ibaker): Replace this with a closer implementation of the WebVTT spec (keeping - // individual cues, but tweaking their `line` value): - // https://www.w3.org/TR/webvtt1/#cue-computed-line - if (cue.isNormalCue()) { - // we want to merge all of the normal cues into a single cue to ensure they are drawn - // correctly (i.e. don't overlap) and to emulate roll-up, but only if there are multiple - // normal cues, otherwise we can just append the single normal cue - if (firstNormalCue == null) { - firstNormalCue = cue; - } else if (normalCueTextBuilder == null) { - normalCueTextBuilder = new SpannableStringBuilder(); - normalCueTextBuilder - .append(Assertions.checkNotNull(firstNormalCue.text)) - .append("\n") - .append(Assertions.checkNotNull(cue.text)); - } else { - normalCueTextBuilder.append("\n").append(Assertions.checkNotNull(cue.text)); - } + WebvttCueInfo cueInfo = cueInfos.get(i); + if (cueInfo.cue.line == Cue.DIMEN_UNSET) { + cuesWithUnsetLine.add(cueInfo); } else { - list.add(cue); + currentCues.add(cueInfo.cue); } } } - if (normalCueTextBuilder != null) { - // there were multiple normal cues, so create a new cue with all of the text - list.add(new WebvttCue.Builder().setText(normalCueTextBuilder).build()); - } else if (firstNormalCue != null) { - // there was only a single normal cue, so just add it to the list - list.add(firstNormalCue); + // Steps 4 - 10 of https://www.w3.org/TR/webvtt1/#cue-computed-line + // (steps 1 - 3 are handled by WebvttCueParser#computeLine(float, int)) + Collections.sort(cuesWithUnsetLine, (c1, c2) -> Long.compare(c1.startTimeUs, c2.startTimeUs)); + for (int i = 0; i < cuesWithUnsetLine.size(); i++) { + Cue cue = cuesWithUnsetLine.get(i).cue; + currentCues.add( + cue.buildUpon() + .setLine((float) (-1 - i), Cue.LINE_TYPE_NUMBER) + // WebVTT doesn't use 'line alignment' (i.e. Cue#lineAnchor) when computing position + // with snap-to-lines=true (i.e. Cue#LINE_TYPE_NUMBER) but Cue does use lineAnchor + // when describing how numeric cues should be displayed. So we have to manually set + // lineAnchor=ANCHOR_TYPE_END to avoid the bottom line of cues being off the screen. + // https://www.w3.org/TR/webvtt1/#processing-cue-settings + .setLineAnchor(Cue.ANCHOR_TYPE_END) + .build()); } - return list; + return currentCues; } - } 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 3e8cdd1ca4..9a599279ec 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 @@ -610,18 +610,13 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { @Nullable private long[][] allocationCheckpoints; - /* package */ - // the constructor does not initialize fields: allocationCheckpoints - @SuppressWarnings("nullness:initialization.fields.uninitialized") - DefaultBandwidthProvider( + /* package */ DefaultBandwidthProvider( BandwidthMeter bandwidthMeter, float bandwidthFraction, long reservedBandwidth) { this.bandwidthMeter = bandwidthMeter; this.bandwidthFraction = bandwidthFraction; this.reservedBandwidth = reservedBandwidth; } - // unboxing a possibly-null reference allocationCheckpoints[nextIndex][0] - @SuppressWarnings("nullness:unboxing.of.nullable") @Override public long getAllocatedBandwidth() { long totalBandwidth = (long) (bandwidthMeter.getBitrateEstimate() * bandwidthFraction); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/BufferSizeAdaptationBuilder.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/BufferSizeAdaptationBuilder.java deleted file mode 100644 index b850a08aeb..0000000000 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/BufferSizeAdaptationBuilder.java +++ /dev/null @@ -1,494 +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.trackselection; - -import android.util.Pair; -import androidx.annotation.Nullable; -import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.DefaultLoadControl; -import com.google.android.exoplayer2.Format; -import com.google.android.exoplayer2.LoadControl; -import com.google.android.exoplayer2.source.TrackGroup; -import com.google.android.exoplayer2.source.chunk.MediaChunk; -import com.google.android.exoplayer2.source.chunk.MediaChunkIterator; -import com.google.android.exoplayer2.trackselection.TrackSelection.Definition; -import com.google.android.exoplayer2.upstream.BandwidthMeter; -import com.google.android.exoplayer2.upstream.DefaultAllocator; -import com.google.android.exoplayer2.util.Assertions; -import com.google.android.exoplayer2.util.Clock; -import java.util.List; -import org.checkerframework.checker.nullness.compatqual.NullableType; - -/** - * Builder for a {@link TrackSelection.Factory} and {@link LoadControl} that implement buffer size - * based track adaptation. - */ -public final class BufferSizeAdaptationBuilder { - - /** Dynamic filter for formats, which is applied when selecting a new track. */ - public interface DynamicFormatFilter { - - /** Filter which allows all formats. */ - DynamicFormatFilter NO_FILTER = (format, trackBitrate, isInitialSelection) -> true; - - /** - * Called when updating the selected track to determine whether a candidate track is allowed. If - * no format is allowed or eligible, the lowest quality format will be used. - * - * @param format The {@link Format} of the candidate track. - * @param trackBitrate The estimated bitrate of the track. May differ from {@link - * Format#bitrate} if a more accurate estimate of the current track bitrate is available. - * @param isInitialSelection Whether this is for the initial track selection. - */ - boolean isFormatAllowed(Format format, int trackBitrate, boolean isInitialSelection); - } - - /** - * The default minimum duration of media that the player will attempt to ensure is buffered at all - * times, in milliseconds. - */ - public static final int DEFAULT_MIN_BUFFER_MS = 15000; - - /** - * The default maximum duration of media that the player will attempt to buffer, in milliseconds. - */ - public static final int DEFAULT_MAX_BUFFER_MS = 50000; - - /** - * The default duration of media that must be buffered for playback to start or resume following a - * user action such as a seek, in milliseconds. - */ - public static final int DEFAULT_BUFFER_FOR_PLAYBACK_MS = - DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_MS; - - /** - * The default duration of media that must be buffered for playback to resume after a rebuffer, in - * milliseconds. A rebuffer is defined to be caused by buffer depletion rather than a user action. - */ - public static final int DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS = - DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS; - - /** - * The default offset the current duration of buffered media must deviate from the ideal duration - * of buffered media for the currently selected format, before the selected format is changed. - */ - public static final int DEFAULT_HYSTERESIS_BUFFER_MS = 5000; - - /** - * During start-up phase, the default fraction of the available bandwidth that the selection - * should consider available for use. Setting to a value less than 1 is recommended to account for - * inaccuracies in the bandwidth estimator. - */ - public static final float DEFAULT_START_UP_BANDWIDTH_FRACTION = - AdaptiveTrackSelection.DEFAULT_BANDWIDTH_FRACTION; - - /** - * During start-up phase, the default minimum duration of buffered media required for the selected - * track to switch to one of higher quality based on measured bandwidth. - */ - public static final int DEFAULT_START_UP_MIN_BUFFER_FOR_QUALITY_INCREASE_MS = - AdaptiveTrackSelection.DEFAULT_MIN_DURATION_FOR_QUALITY_INCREASE_MS; - - @Nullable private DefaultAllocator allocator; - private Clock clock; - private int minBufferMs; - private int maxBufferMs; - private int bufferForPlaybackMs; - private int bufferForPlaybackAfterRebufferMs; - private int hysteresisBufferMs; - private float startUpBandwidthFraction; - private int startUpMinBufferForQualityIncreaseMs; - private DynamicFormatFilter dynamicFormatFilter; - private boolean buildCalled; - - /** Creates builder with default values. */ - public BufferSizeAdaptationBuilder() { - clock = Clock.DEFAULT; - minBufferMs = DEFAULT_MIN_BUFFER_MS; - maxBufferMs = DEFAULT_MAX_BUFFER_MS; - bufferForPlaybackMs = DEFAULT_BUFFER_FOR_PLAYBACK_MS; - bufferForPlaybackAfterRebufferMs = DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS; - hysteresisBufferMs = DEFAULT_HYSTERESIS_BUFFER_MS; - startUpBandwidthFraction = DEFAULT_START_UP_BANDWIDTH_FRACTION; - startUpMinBufferForQualityIncreaseMs = DEFAULT_START_UP_MIN_BUFFER_FOR_QUALITY_INCREASE_MS; - dynamicFormatFilter = DynamicFormatFilter.NO_FILTER; - } - - /** - * Set the clock to use. Should only be set for testing purposes. - * - * @param clock The {@link Clock}. - * @return This builder, for convenience. - * @throws IllegalStateException If {@link #buildPlayerComponents()} has already been called. - */ - public BufferSizeAdaptationBuilder setClock(Clock clock) { - Assertions.checkState(!buildCalled); - this.clock = clock; - return this; - } - - /** - * Sets the {@link DefaultAllocator} used by the loader. - * - * @param allocator The {@link DefaultAllocator}. - * @return This builder, for convenience. - * @throws IllegalStateException If {@link #buildPlayerComponents()} has already been called. - */ - public BufferSizeAdaptationBuilder setAllocator(DefaultAllocator allocator) { - Assertions.checkState(!buildCalled); - this.allocator = allocator; - return this; - } - - /** - * Sets the buffer duration parameters. - * - * @param minBufferMs The minimum duration of media that the player will attempt to ensure is - * buffered at all times, in milliseconds. - * @param maxBufferMs The maximum duration of media that the player will attempt to buffer, in - * milliseconds. - * @param bufferForPlaybackMs The duration of media that must be buffered for playback to start or - * resume following a user action such as a seek, in milliseconds. - * @param bufferForPlaybackAfterRebufferMs The default duration of media that must be buffered for - * playback to resume after a rebuffer, in milliseconds. A rebuffer is defined to be caused by - * buffer depletion rather than a user action. - * @return This builder, for convenience. - * @throws IllegalStateException If {@link #buildPlayerComponents()} has already been called. - */ - public BufferSizeAdaptationBuilder setBufferDurationsMs( - int minBufferMs, - int maxBufferMs, - int bufferForPlaybackMs, - int bufferForPlaybackAfterRebufferMs) { - Assertions.checkState(!buildCalled); - this.minBufferMs = minBufferMs; - this.maxBufferMs = maxBufferMs; - this.bufferForPlaybackMs = bufferForPlaybackMs; - this.bufferForPlaybackAfterRebufferMs = bufferForPlaybackAfterRebufferMs; - return this; - } - - /** - * Sets the hysteresis buffer used to prevent repeated format switching. - * - * @param hysteresisBufferMs The offset the current duration of buffered media must deviate from - * the ideal duration of buffered media for the currently selected format, before the selected - * format is changed. This value must be smaller than {@code maxBufferMs - minBufferMs}. - * @return This builder, for convenience. - * @throws IllegalStateException If {@link #buildPlayerComponents()} has already been called. - */ - public BufferSizeAdaptationBuilder setHysteresisBufferMs(int hysteresisBufferMs) { - Assertions.checkState(!buildCalled); - this.hysteresisBufferMs = hysteresisBufferMs; - return this; - } - - /** - * Sets track selection parameters used during the start-up phase before the selection can be made - * purely on based on buffer size. During the start-up phase the selection is based on the current - * bandwidth estimate. - * - * @param bandwidthFraction The fraction of the available bandwidth that the selection should - * consider available for use. Setting to a value less than 1 is recommended to account for - * inaccuracies in the bandwidth estimator. - * @param minBufferForQualityIncreaseMs The minimum duration of buffered media required for the - * selected track to switch to one of higher quality. - * @return This builder, for convenience. - * @throws IllegalStateException If {@link #buildPlayerComponents()} has already been called. - */ - public BufferSizeAdaptationBuilder setStartUpTrackSelectionParameters( - float bandwidthFraction, int minBufferForQualityIncreaseMs) { - Assertions.checkState(!buildCalled); - this.startUpBandwidthFraction = bandwidthFraction; - this.startUpMinBufferForQualityIncreaseMs = minBufferForQualityIncreaseMs; - return this; - } - - /** - * Sets the {@link DynamicFormatFilter} to use when updating the selected track. - * - * @param dynamicFormatFilter The {@link DynamicFormatFilter}. - * @return This builder, for convenience. - * @throws IllegalStateException If {@link #buildPlayerComponents()} has already been called. - */ - public BufferSizeAdaptationBuilder setDynamicFormatFilter( - DynamicFormatFilter dynamicFormatFilter) { - Assertions.checkState(!buildCalled); - this.dynamicFormatFilter = dynamicFormatFilter; - return this; - } - - /** - * Builds player components for buffer size based track adaptation. - * - * @return A pair of a {@link TrackSelection.Factory} and a {@link LoadControl}, which should be - * used to construct the player. - */ - public Pair buildPlayerComponents() { - Assertions.checkArgument(hysteresisBufferMs < maxBufferMs - minBufferMs); - Assertions.checkState(!buildCalled); - buildCalled = true; - - DefaultLoadControl.Builder loadControlBuilder = - new DefaultLoadControl.Builder() - .setTargetBufferBytes(/* targetBufferBytes = */ Integer.MAX_VALUE) - .setBufferDurationsMs( - /* minBufferMs= */ maxBufferMs, - maxBufferMs, - bufferForPlaybackMs, - bufferForPlaybackAfterRebufferMs); - if (allocator != null) { - loadControlBuilder.setAllocator(allocator); - } - - TrackSelection.Factory trackSelectionFactory = - new TrackSelection.Factory() { - @Override - public @NullableType TrackSelection[] createTrackSelections( - @NullableType Definition[] definitions, BandwidthMeter bandwidthMeter) { - return TrackSelectionUtil.createTrackSelectionsForDefinitions( - definitions, - definition -> - new BufferSizeAdaptiveTrackSelection( - definition.group, - definition.tracks, - bandwidthMeter, - minBufferMs, - maxBufferMs, - hysteresisBufferMs, - startUpBandwidthFraction, - startUpMinBufferForQualityIncreaseMs, - dynamicFormatFilter, - clock)); - } - }; - - return Pair.create(trackSelectionFactory, loadControlBuilder.createDefaultLoadControl()); - } - - private static final class BufferSizeAdaptiveTrackSelection extends BaseTrackSelection { - - private static final int BITRATE_BLACKLISTED = Format.NO_VALUE; - - private final BandwidthMeter bandwidthMeter; - private final Clock clock; - private final DynamicFormatFilter dynamicFormatFilter; - private final int[] formatBitrates; - private final long minBufferUs; - private final long maxBufferUs; - private final long hysteresisBufferUs; - private final float startUpBandwidthFraction; - private final long startUpMinBufferForQualityIncreaseUs; - private final int minBitrate; - private final int maxBitrate; - private final double bitrateToBufferFunctionSlope; - private final double bitrateToBufferFunctionIntercept; - - private boolean isInSteadyState; - private int selectedIndex; - private int selectionReason; - private float playbackSpeed; - - private BufferSizeAdaptiveTrackSelection( - TrackGroup trackGroup, - int[] tracks, - BandwidthMeter bandwidthMeter, - int minBufferMs, - int maxBufferMs, - int hysteresisBufferMs, - float startUpBandwidthFraction, - int startUpMinBufferForQualityIncreaseMs, - DynamicFormatFilter dynamicFormatFilter, - Clock clock) { - super(trackGroup, tracks); - this.bandwidthMeter = bandwidthMeter; - this.minBufferUs = C.msToUs(minBufferMs); - this.maxBufferUs = C.msToUs(maxBufferMs); - this.hysteresisBufferUs = C.msToUs(hysteresisBufferMs); - this.startUpBandwidthFraction = startUpBandwidthFraction; - this.startUpMinBufferForQualityIncreaseUs = C.msToUs(startUpMinBufferForQualityIncreaseMs); - this.dynamicFormatFilter = dynamicFormatFilter; - this.clock = clock; - - formatBitrates = new int[length]; - maxBitrate = getFormat(/* index= */ 0).bitrate; - minBitrate = getFormat(/* index= */ length - 1).bitrate; - selectionReason = C.SELECTION_REASON_UNKNOWN; - playbackSpeed = 1.0f; - - // We use a log-linear function to map from bitrate to buffer size: - // buffer = slope * ln(bitrate) + intercept, - // with buffer(minBitrate) = minBuffer and buffer(maxBitrate) = maxBuffer - hysteresisBuffer. - bitrateToBufferFunctionSlope = - (maxBufferUs - hysteresisBufferUs - minBufferUs) - / Math.log((double) maxBitrate / minBitrate); - bitrateToBufferFunctionIntercept = - minBufferUs - bitrateToBufferFunctionSlope * Math.log(minBitrate); - } - - @Override - public void onPlaybackSpeed(float playbackSpeed) { - this.playbackSpeed = playbackSpeed; - } - - @Override - public void onDiscontinuity() { - isInSteadyState = false; - } - - @Override - public int getSelectedIndex() { - return selectedIndex; - } - - @Override - public int getSelectionReason() { - return selectionReason; - } - - @Override - @Nullable - public Object getSelectionData() { - return null; - } - - @Override - public void updateSelectedTrack( - long playbackPositionUs, - long bufferedDurationUs, - long availableDurationUs, - List queue, - MediaChunkIterator[] mediaChunkIterators) { - updateFormatBitrates(/* nowMs= */ clock.elapsedRealtime()); - - // Make initial selection - if (selectionReason == C.SELECTION_REASON_UNKNOWN) { - selectionReason = C.SELECTION_REASON_INITIAL; - selectedIndex = selectIdealIndexUsingBandwidth(/* isInitialSelection= */ true); - return; - } - - long bufferUs = getCurrentPeriodBufferedDurationUs(playbackPositionUs, bufferedDurationUs); - int oldSelectedIndex = selectedIndex; - if (isInSteadyState) { - selectIndexSteadyState(bufferUs); - } else { - selectIndexStartUpPhase(bufferUs); - } - if (selectedIndex != oldSelectedIndex) { - selectionReason = C.SELECTION_REASON_ADAPTIVE; - } - } - - // Steady state. - - private void selectIndexSteadyState(long bufferUs) { - if (isOutsideHysteresis(bufferUs)) { - selectedIndex = selectIdealIndexUsingBufferSize(bufferUs); - } - } - - private boolean isOutsideHysteresis(long bufferUs) { - if (formatBitrates[selectedIndex] == BITRATE_BLACKLISTED) { - return true; - } - long targetBufferForCurrentBitrateUs = - getTargetBufferForBitrateUs(formatBitrates[selectedIndex]); - long bufferDiffUs = bufferUs - targetBufferForCurrentBitrateUs; - return Math.abs(bufferDiffUs) > hysteresisBufferUs; - } - - private int selectIdealIndexUsingBufferSize(long bufferUs) { - int lowestBitrateNonBlacklistedIndex = 0; - for (int i = 0; i < formatBitrates.length; i++) { - if (formatBitrates[i] != BITRATE_BLACKLISTED) { - if (getTargetBufferForBitrateUs(formatBitrates[i]) <= bufferUs - && dynamicFormatFilter.isFormatAllowed( - getFormat(i), formatBitrates[i], /* isInitialSelection= */ false)) { - return i; - } - lowestBitrateNonBlacklistedIndex = i; - } - } - return lowestBitrateNonBlacklistedIndex; - } - - // Startup. - - private void selectIndexStartUpPhase(long bufferUs) { - int startUpSelectedIndex = selectIdealIndexUsingBandwidth(/* isInitialSelection= */ false); - int steadyStateSelectedIndex = selectIdealIndexUsingBufferSize(bufferUs); - if (steadyStateSelectedIndex <= selectedIndex) { - // Switch to steady state if we have enough buffer to maintain current selection. - selectedIndex = steadyStateSelectedIndex; - isInSteadyState = true; - } else { - if (bufferUs < startUpMinBufferForQualityIncreaseUs - && startUpSelectedIndex < selectedIndex - && formatBitrates[selectedIndex] != BITRATE_BLACKLISTED) { - // Switching up from a non-blacklisted track is only allowed if we have enough buffer. - return; - } - selectedIndex = startUpSelectedIndex; - } - } - - private int selectIdealIndexUsingBandwidth(boolean isInitialSelection) { - long effectiveBitrate = - (long) (bandwidthMeter.getBitrateEstimate() * startUpBandwidthFraction); - int lowestBitrateNonBlacklistedIndex = 0; - for (int i = 0; i < formatBitrates.length; i++) { - if (formatBitrates[i] != BITRATE_BLACKLISTED) { - if (Math.round(formatBitrates[i] * playbackSpeed) <= effectiveBitrate - && dynamicFormatFilter.isFormatAllowed( - getFormat(i), formatBitrates[i], isInitialSelection)) { - return i; - } - lowestBitrateNonBlacklistedIndex = i; - } - } - return lowestBitrateNonBlacklistedIndex; - } - - // Utility methods. - - private void updateFormatBitrates(long nowMs) { - for (int i = 0; i < length; i++) { - if (nowMs == Long.MIN_VALUE || !isBlacklisted(i, nowMs)) { - formatBitrates[i] = getFormat(i).bitrate; - } else { - formatBitrates[i] = BITRATE_BLACKLISTED; - } - } - } - - private long getTargetBufferForBitrateUs(int bitrate) { - if (bitrate <= minBitrate) { - return minBufferUs; - } - if (bitrate >= maxBitrate) { - return maxBufferUs - hysteresisBufferUs; - } - return (int) - (bitrateToBufferFunctionSlope * Math.log(bitrate) + bitrateToBufferFunctionIntercept); - } - - private static long getCurrentPeriodBufferedDurationUs( - long playbackPositionUs, long bufferedDurationUs) { - return playbackPositionUs >= 0 ? bufferedDurationUs : playbackPositionUs + bufferedDurationUs; - } - } -} 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 437546559c..668202993a 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 @@ -30,6 +30,9 @@ import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Renderer; import com.google.android.exoplayer2.RendererCapabilities; +import com.google.android.exoplayer2.RendererCapabilities.AdaptiveSupport; +import com.google.android.exoplayer2.RendererCapabilities.Capabilities; +import com.google.android.exoplayer2.RendererCapabilities.FormatSupport; import com.google.android.exoplayer2.RendererConfiguration; import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroupArray; @@ -352,7 +355,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { /** * Equivalent to calling {@link #setViewportSize(int, int, boolean)} with the viewport size - * obtained from {@link Util#getPhysicalDisplaySize(Context)}. + * obtained from {@link Util#getCurrentDisplayModeSize(Context)}. * * @param context Any context. * @param viewportOrientationMayChange Whether the viewport orientation may change during @@ -362,7 +365,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { public ParametersBuilder setViewportSizeToPhysicalDisplaySize( Context context, boolean viewportOrientationMayChange) { // Assume the viewport is fullscreen. - Point viewportSize = Util.getPhysicalDisplaySize(context); + Point viewportSize = Util.getCurrentDisplayModeSize(context); return setViewportSize(viewportSize.x, viewportSize.y, viewportOrientationMayChange); } @@ -546,23 +549,6 @@ public class DefaultTrackSelector extends MappingTrackSelector { return this; } - /** - * @deprecated Use {@link #setAllowVideoMixedMimeTypeAdaptiveness(boolean)} and {@link - * #setAllowAudioMixedMimeTypeAdaptiveness(boolean)}. - */ - @Deprecated - public ParametersBuilder setAllowMixedMimeAdaptiveness(boolean allowMixedMimeAdaptiveness) { - setAllowAudioMixedMimeTypeAdaptiveness(allowMixedMimeAdaptiveness); - setAllowVideoMixedMimeTypeAdaptiveness(allowMixedMimeAdaptiveness); - return this; - } - - /** @deprecated Use {@link #setAllowVideoNonSeamlessAdaptiveness(boolean)} */ - @Deprecated - public ParametersBuilder setAllowNonSeamlessAdaptiveness(boolean allowNonSeamlessAdaptiveness) { - return setAllowVideoNonSeamlessAdaptiveness(allowNonSeamlessAdaptiveness); - } - /** * Sets whether to exceed renderer capabilities when no selection can be made otherwise. * @@ -816,19 +802,6 @@ public class DefaultTrackSelector extends MappingTrackSelector { @SuppressWarnings("deprecation") public static final Parameters DEFAULT_WITHOUT_CONTEXT = new ParametersBuilder().build(); - /** - * @deprecated This instance does not have {@link Context} constraints configured. Use {@link - * #getDefaults(Context)} instead. - */ - @Deprecated public static final Parameters DEFAULT_WITHOUT_VIEWPORT = DEFAULT_WITHOUT_CONTEXT; - - /** - * @deprecated This instance does not have {@link Context} constraints configured. Use {@link - * #getDefaults(Context)} instead. - */ - @Deprecated - public static final Parameters DEFAULT = DEFAULT_WITHOUT_CONTEXT; - /** Returns an instance configured with default values. */ public static Parameters getDefaults(Context context) { return new ParametersBuilder(context).build(); @@ -943,13 +916,6 @@ public class DefaultTrackSelector extends MappingTrackSelector { * other constraints. The default value is {@code false}. */ public final boolean forceHighestSupportedBitrate; - /** - * @deprecated Use {@link #allowVideoMixedMimeTypeAdaptiveness} and {@link - * #allowAudioMixedMimeTypeAdaptiveness}. - */ - @Deprecated public final boolean allowMixedMimeAdaptiveness; - /** @deprecated Use {@link #allowVideoNonSeamlessAdaptiveness}. */ - @Deprecated public final boolean allowNonSeamlessAdaptiveness; /** * Whether to exceed renderer capabilities when no selection can be made otherwise. * @@ -1034,9 +1000,6 @@ public class DefaultTrackSelector extends MappingTrackSelector { this.forceHighestSupportedBitrate = forceHighestSupportedBitrate; this.exceedRendererCapabilitiesIfNecessary = exceedRendererCapabilitiesIfNecessary; this.tunnelingAudioSessionId = tunnelingAudioSessionId; - // Deprecated fields. - this.allowMixedMimeAdaptiveness = allowVideoMixedMimeTypeAdaptiveness; - this.allowNonSeamlessAdaptiveness = allowVideoNonSeamlessAdaptiveness; // Overrides this.selectionOverrides = selectionOverrides; this.rendererDisabledFlags = rendererDisabledFlags; @@ -1071,9 +1034,6 @@ public class DefaultTrackSelector extends MappingTrackSelector { // Overrides this.selectionOverrides = readSelectionOverrides(in); this.rendererDisabledFlags = Util.castNonNull(in.readSparseBooleanArray()); - // Deprecated fields. - this.allowMixedMimeAdaptiveness = allowVideoMixedMimeTypeAdaptiveness; - this.allowNonSeamlessAdaptiveness = allowVideoNonSeamlessAdaptiveness; } /** @@ -1534,65 +1494,6 @@ public class DefaultTrackSelector extends MappingTrackSelector { return getParameters().buildUpon(); } - /** @deprecated Use {@link ParametersBuilder#setRendererDisabled(int, boolean)}. */ - @Deprecated - public final void setRendererDisabled(int rendererIndex, boolean disabled) { - setParameters(buildUponParameters().setRendererDisabled(rendererIndex, disabled)); - } - - /** @deprecated Use {@link Parameters#getRendererDisabled(int)}. */ - @Deprecated - public final boolean getRendererDisabled(int rendererIndex) { - return getParameters().getRendererDisabled(rendererIndex); - } - - /** - * @deprecated Use {@link ParametersBuilder#setSelectionOverride(int, TrackGroupArray, - * SelectionOverride)}. - */ - @Deprecated - public final void setSelectionOverride( - int rendererIndex, TrackGroupArray groups, @Nullable SelectionOverride override) { - setParameters(buildUponParameters().setSelectionOverride(rendererIndex, groups, override)); - } - - /** @deprecated Use {@link Parameters#hasSelectionOverride(int, TrackGroupArray)}. */ - @Deprecated - public final boolean hasSelectionOverride(int rendererIndex, TrackGroupArray groups) { - return getParameters().hasSelectionOverride(rendererIndex, groups); - } - - /** @deprecated Use {@link Parameters#getSelectionOverride(int, TrackGroupArray)}. */ - @Deprecated - @Nullable - public final SelectionOverride getSelectionOverride(int rendererIndex, TrackGroupArray groups) { - return getParameters().getSelectionOverride(rendererIndex, groups); - } - - /** @deprecated Use {@link ParametersBuilder#clearSelectionOverride(int, TrackGroupArray)}. */ - @Deprecated - public final void clearSelectionOverride(int rendererIndex, TrackGroupArray groups) { - setParameters(buildUponParameters().clearSelectionOverride(rendererIndex, groups)); - } - - /** @deprecated Use {@link ParametersBuilder#clearSelectionOverrides(int)}. */ - @Deprecated - public final void clearSelectionOverrides(int rendererIndex) { - setParameters(buildUponParameters().clearSelectionOverrides(rendererIndex)); - } - - /** @deprecated Use {@link ParametersBuilder#clearSelectionOverrides()}. */ - @Deprecated - public final void clearSelectionOverrides() { - setParameters(buildUponParameters().clearSelectionOverrides()); - } - - /** @deprecated Use {@link ParametersBuilder#setTunnelingAudioSessionId(int)}. */ - @Deprecated - public void setTunnelingAudioSessionId(int tunnelingAudioSessionId) { - setParameters(buildUponParameters().setTunnelingAudioSessionId(tunnelingAudioSessionId)); - } - /** * Allows the creation of multiple adaptive track selections. * @@ -1608,8 +1509,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { protected final Pair<@NullableType RendererConfiguration[], @NullableType TrackSelection[]> selectTracks( MappedTrackInfo mappedTrackInfo, - int[][][] rendererFormatSupports, - int[] rendererMixedMimeTypeAdaptationSupports) + @Capabilities int[][][] rendererFormatSupports, + @AdaptiveSupport int[] rendererMixedMimeTypeAdaptationSupports) throws ExoPlaybackException { Parameters params = parametersReference.get(); int rendererCount = mappedTrackInfo.getRendererCount(); @@ -1678,18 +1579,18 @@ public class DefaultTrackSelector extends MappingTrackSelector { * generated by this method will be overridden to account for these properties. * * @param mappedTrackInfo Mapped track information. - * @param rendererFormatSupports The result of {@link RendererCapabilities#supportsFormat} for - * each mapped track, indexed by renderer, track group and track (in that order). - * @param rendererMixedMimeTypeAdaptationSupports The result of {@link - * RendererCapabilities#supportsMixedMimeTypeAdaptation()} for each renderer. + * @param rendererFormatSupports The {@link Capabilities} for each mapped track, indexed by + * renderer, track group and track (in that order). + * @param rendererMixedMimeTypeAdaptationSupports The {@link AdaptiveSupport} for mixed MIME type + * adaptation for the renderer. * @return The {@link TrackSelection.Definition}s for the renderers. A null entry indicates no * selection was made. * @throws ExoPlaybackException If an error occurs while selecting the tracks. */ protected TrackSelection.@NullableType Definition[] selectAllTracks( MappedTrackInfo mappedTrackInfo, - int[][][] rendererFormatSupports, - int[] rendererMixedMimeTypeAdaptationSupports, + @Capabilities int[][][] rendererFormatSupports, + @AdaptiveSupport int[] rendererMixedMimeTypeAdaptationSupports, Parameters params) throws ExoPlaybackException { int rendererCount = mappedTrackInfo.getRendererCount(); @@ -1793,10 +1694,10 @@ public class DefaultTrackSelector extends MappingTrackSelector { * {@link TrackSelection} for a video renderer. * * @param groups The {@link TrackGroupArray} mapped to the renderer. - * @param formatSupports The result of {@link RendererCapabilities#supportsFormat} for each mapped - * track, indexed by track group index and track index (in that order). - * @param mixedMimeTypeAdaptationSupports The result of {@link - * RendererCapabilities#supportsMixedMimeTypeAdaptation()} for the renderer. + * @param formatSupports The {@link Capabilities} for each mapped track, indexed by renderer, + * track group and track (in that order). + * @param mixedMimeTypeAdaptationSupports The {@link AdaptiveSupport} for mixed MIME type + * adaptation for the renderer. * @param params The selector's current constraint parameters. * @param enableAdaptiveTrackSelection Whether adaptive track selection is allowed. * @return The {@link TrackSelection.Definition} for the renderer, or null if no selection was @@ -1806,8 +1707,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { @Nullable protected TrackSelection.Definition selectVideoTrack( TrackGroupArray groups, - int[][] formatSupports, - int mixedMimeTypeAdaptationSupports, + @Capabilities int[][] formatSupports, + @AdaptiveSupport int mixedMimeTypeAdaptationSupports, Parameters params, boolean enableAdaptiveTrackSelection) throws ExoPlaybackException { @@ -1827,8 +1728,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { @Nullable private static TrackSelection.Definition selectAdaptiveVideoTrack( TrackGroupArray groups, - int[][] formatSupport, - int mixedMimeTypeAdaptationSupports, + @Capabilities int[][] formatSupport, + @AdaptiveSupport int mixedMimeTypeAdaptationSupports, Parameters params) { int requiredAdaptiveSupport = params.allowVideoNonSeamlessAdaptiveness @@ -1861,7 +1762,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { private static int[] getAdaptiveVideoTracksForGroup( TrackGroup group, - int[] formatSupport, + @Capabilities int[] formatSupport, boolean allowMixedMimeTypes, int requiredAdaptiveSupport, int maxVideoWidth, @@ -1926,7 +1827,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { private static int getAdaptiveVideoTrackCountForMimeType( TrackGroup group, - int[] formatSupport, + @Capabilities int[] formatSupport, int requiredAdaptiveSupport, @Nullable String mimeType, int maxVideoWidth, @@ -1954,7 +1855,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { private static void filterAdaptiveVideoTrackCountForMimeType( TrackGroup group, - int[] formatSupport, + @Capabilities int[] formatSupport, int requiredAdaptiveSupport, @Nullable String mimeType, int maxVideoWidth, @@ -1981,12 +1882,16 @@ public class DefaultTrackSelector extends MappingTrackSelector { private static boolean isSupportedAdaptiveVideoTrack( Format format, @Nullable String mimeType, - int formatSupport, + @Capabilities int formatSupport, int requiredAdaptiveSupport, int maxVideoWidth, int maxVideoHeight, int maxVideoFrameRate, int maxVideoBitrate) { + if ((format.roleFlags & C.ROLE_FLAG_TRICK_PLAY) != 0) { + // Ignore trick-play tracks for now. + return false; + } return isSupported(formatSupport, false) && ((formatSupport & requiredAdaptiveSupport) != 0) && (mimeType == null || Util.areEqual(format.sampleMimeType, mimeType)) @@ -1998,7 +1903,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { @Nullable private static TrackSelection.Definition selectFixedVideoTrack( - TrackGroupArray groups, int[][] formatSupports, Parameters params) { + TrackGroupArray groups, @Capabilities int[][] formatSupports, Parameters params) { TrackGroup selectedGroup = null; int selectedTrackIndex = 0; int selectedTrackScore = 0; @@ -2008,11 +1913,15 @@ public class DefaultTrackSelector extends MappingTrackSelector { TrackGroup trackGroup = groups.get(groupIndex); List selectedTrackIndices = getViewportFilteredTrackIndices(trackGroup, params.viewportWidth, params.viewportHeight, params.viewportOrientationMayChange); - int[] trackFormatSupport = formatSupports[groupIndex]; + @Capabilities int[] trackFormatSupport = formatSupports[groupIndex]; for (int trackIndex = 0; trackIndex < trackGroup.length; trackIndex++) { + Format format = trackGroup.getFormat(trackIndex); + if ((format.roleFlags & C.ROLE_FLAG_TRICK_PLAY) != 0) { + // Ignore trick-play tracks for now. + continue; + } if (isSupported(trackFormatSupport[trackIndex], params.exceedRendererCapabilitiesIfNecessary)) { - Format format = trackGroup.getFormat(trackIndex); boolean isWithinConstraints = selectedTrackIndices.contains(trackIndex) && (format.width == Format.NO_VALUE || format.width <= params.maxVideoWidth) @@ -2071,10 +1980,10 @@ public class DefaultTrackSelector extends MappingTrackSelector { * {@link TrackSelection} for an audio renderer. * * @param groups The {@link TrackGroupArray} mapped to the renderer. - * @param formatSupports The result of {@link RendererCapabilities#supportsFormat} for each mapped - * track, indexed by track group index and track index (in that order). - * @param mixedMimeTypeAdaptationSupports The result of {@link - * RendererCapabilities#supportsMixedMimeTypeAdaptation()} for the renderer. + * @param formatSupports The {@link Capabilities} for each mapped track, indexed by renderer, + * track group and track (in that order). + * @param mixedMimeTypeAdaptationSupports The {@link AdaptiveSupport} for mixed MIME type + * adaptation for the renderer. * @param params The selector's current constraint parameters. * @param enableAdaptiveTrackSelection Whether adaptive track selection is allowed. * @return The {@link TrackSelection.Definition} and corresponding {@link AudioTrackScore}, or @@ -2085,8 +1994,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { @Nullable protected Pair selectAudioTrack( TrackGroupArray groups, - int[][] formatSupports, - int mixedMimeTypeAdaptationSupports, + @Capabilities int[][] formatSupports, + @AdaptiveSupport int mixedMimeTypeAdaptationSupports, Parameters params, boolean enableAdaptiveTrackSelection) throws ExoPlaybackException { @@ -2095,7 +2004,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { AudioTrackScore selectedTrackScore = null; for (int groupIndex = 0; groupIndex < groups.length; groupIndex++) { TrackGroup trackGroup = groups.get(groupIndex); - int[] trackFormatSupport = formatSupports[groupIndex]; + @Capabilities int[] trackFormatSupport = formatSupports[groupIndex]; for (int trackIndex = 0; trackIndex < trackGroup.length; trackIndex++) { if (isSupported(trackFormatSupport[trackIndex], params.exceedRendererCapabilitiesIfNecessary)) { @@ -2130,11 +2039,12 @@ public class DefaultTrackSelector extends MappingTrackSelector { getAdaptiveAudioTracks( selectedGroup, formatSupports[selectedGroupIndex], + selectedTrackIndex, params.maxAudioBitrate, params.allowAudioMixedMimeTypeAdaptiveness, params.allowAudioMixedSampleRateAdaptiveness, params.allowAudioMixedChannelCountAdaptiveness); - if (adaptiveTracks.length > 0) { + if (adaptiveTracks.length > 1) { definition = new TrackSelection.Definition(selectedGroup, adaptiveTracks); } } @@ -2148,101 +2058,50 @@ public class DefaultTrackSelector extends MappingTrackSelector { private static int[] getAdaptiveAudioTracks( TrackGroup group, - int[] formatSupport, - int maxAudioBitrate, - boolean allowMixedMimeTypeAdaptiveness, - boolean allowMixedSampleRateAdaptiveness, - boolean allowAudioMixedChannelCountAdaptiveness) { - int selectedConfigurationTrackCount = 0; - AudioConfigurationTuple selectedConfiguration = null; - HashSet seenConfigurationTuples = new HashSet<>(); - for (int i = 0; i < group.length; i++) { - Format format = group.getFormat(i); - AudioConfigurationTuple configuration = - new AudioConfigurationTuple( - format.channelCount, format.sampleRate, format.sampleMimeType); - if (seenConfigurationTuples.add(configuration)) { - int configurationCount = - getAdaptiveAudioTrackCount( - group, - formatSupport, - configuration, - maxAudioBitrate, - allowMixedMimeTypeAdaptiveness, - allowMixedSampleRateAdaptiveness, - allowAudioMixedChannelCountAdaptiveness); - if (configurationCount > selectedConfigurationTrackCount) { - selectedConfiguration = configuration; - selectedConfigurationTrackCount = configurationCount; - } - } - } - - if (selectedConfigurationTrackCount > 1) { - Assertions.checkNotNull(selectedConfiguration); - int[] adaptiveIndices = new int[selectedConfigurationTrackCount]; - int index = 0; - for (int i = 0; i < group.length; i++) { - Format format = group.getFormat(i); - if (isSupportedAdaptiveAudioTrack( - format, - formatSupport[i], - selectedConfiguration, - maxAudioBitrate, - allowMixedMimeTypeAdaptiveness, - allowMixedSampleRateAdaptiveness, - allowAudioMixedChannelCountAdaptiveness)) { - adaptiveIndices[index++] = i; - } - } - return adaptiveIndices; - } - return NO_TRACKS; - } - - private static int getAdaptiveAudioTrackCount( - TrackGroup group, - int[] formatSupport, - AudioConfigurationTuple configuration, + @Capabilities int[] formatSupport, + int primaryTrackIndex, int maxAudioBitrate, boolean allowMixedMimeTypeAdaptiveness, boolean allowMixedSampleRateAdaptiveness, boolean allowAudioMixedChannelCountAdaptiveness) { + Format primaryFormat = group.getFormat(primaryTrackIndex); + int[] adaptiveIndices = new int[group.length]; int count = 0; for (int i = 0; i < group.length; i++) { - if (isSupportedAdaptiveAudioTrack( - group.getFormat(i), - formatSupport[i], - configuration, - maxAudioBitrate, - allowMixedMimeTypeAdaptiveness, - allowMixedSampleRateAdaptiveness, - allowAudioMixedChannelCountAdaptiveness)) { - count++; + if (i == primaryTrackIndex + || isSupportedAdaptiveAudioTrack( + group.getFormat(i), + formatSupport[i], + primaryFormat, + maxAudioBitrate, + allowMixedMimeTypeAdaptiveness, + allowMixedSampleRateAdaptiveness, + allowAudioMixedChannelCountAdaptiveness)) { + adaptiveIndices[count++] = i; } } - return count; + return Arrays.copyOf(adaptiveIndices, count); } private static boolean isSupportedAdaptiveAudioTrack( Format format, - int formatSupport, - AudioConfigurationTuple configuration, + @Capabilities int formatSupport, + Format primaryFormat, int maxAudioBitrate, boolean allowMixedMimeTypeAdaptiveness, boolean allowMixedSampleRateAdaptiveness, boolean allowAudioMixedChannelCountAdaptiveness) { - return isSupported(formatSupport, false) + return isSupported(formatSupport, /* allowExceedsCapabilities= */ false) && (format.bitrate == Format.NO_VALUE || format.bitrate <= maxAudioBitrate) && (allowAudioMixedChannelCountAdaptiveness || (format.channelCount != Format.NO_VALUE - && format.channelCount == configuration.channelCount)) + && format.channelCount == primaryFormat.channelCount)) && (allowMixedMimeTypeAdaptiveness || (format.sampleMimeType != null - && TextUtils.equals(format.sampleMimeType, configuration.mimeType))) + && TextUtils.equals(format.sampleMimeType, primaryFormat.sampleMimeType))) && (allowMixedSampleRateAdaptiveness || (format.sampleRate != Format.NO_VALUE - && format.sampleRate == configuration.sampleRate)); + && format.sampleRate == primaryFormat.sampleRate)); } // Text track selection implementation. @@ -2252,8 +2111,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { * {@link TrackSelection} for a text renderer. * * @param groups The {@link TrackGroupArray} mapped to the renderer. - * @param formatSupport The result of {@link RendererCapabilities#supportsFormat} for each mapped - * track, indexed by track group index and track index (in that order). + * @param formatSupport The {@link Capabilities} for each mapped track, indexed by renderer, track + * group and track (in that order). * @param params The selector's current constraint parameters. * @param selectedAudioLanguage The language of the selected audio track. May be null if the * selected text track declares no language or no text track was selected. @@ -2264,7 +2123,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { @Nullable protected Pair selectTextTrack( TrackGroupArray groups, - int[][] formatSupport, + @Capabilities int[][] formatSupport, Parameters params, @Nullable String selectedAudioLanguage) throws ExoPlaybackException { @@ -2273,7 +2132,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { TextTrackScore selectedTrackScore = null; for (int groupIndex = 0; groupIndex < groups.length; groupIndex++) { TrackGroup trackGroup = groups.get(groupIndex); - int[] trackFormatSupport = formatSupport[groupIndex]; + @Capabilities int[] trackFormatSupport = formatSupport[groupIndex]; for (int trackIndex = 0; trackIndex < trackGroup.length; trackIndex++) { if (isSupported(trackFormatSupport[trackIndex], params.exceedRendererCapabilitiesIfNecessary)) { @@ -2305,22 +2164,22 @@ public class DefaultTrackSelector extends MappingTrackSelector { * * @param trackType The type of the renderer. * @param groups The {@link TrackGroupArray} mapped to the renderer. - * @param formatSupport The result of {@link RendererCapabilities#supportsFormat} for each mapped - * track, indexed by track group index and track index (in that order). + * @param formatSupport The {@link Capabilities} for each mapped track, indexed by renderer, track + * group and track (in that order). * @param params The selector's current constraint parameters. * @return The {@link TrackSelection} for the renderer, or null if no selection was made. * @throws ExoPlaybackException If an error occurs while selecting the tracks. */ @Nullable protected TrackSelection.Definition selectOtherTrack( - int trackType, TrackGroupArray groups, int[][] formatSupport, Parameters params) + int trackType, TrackGroupArray groups, @Capabilities int[][] formatSupport, Parameters params) throws ExoPlaybackException { TrackGroup selectedGroup = null; int selectedTrackIndex = 0; int selectedTrackScore = 0; for (int groupIndex = 0; groupIndex < groups.length; groupIndex++) { TrackGroup trackGroup = groups.get(groupIndex); - int[] trackFormatSupport = formatSupport[groupIndex]; + @Capabilities int[] trackFormatSupport = formatSupport[groupIndex]; for (int trackIndex = 0; trackIndex < trackGroup.length; trackIndex++) { if (isSupported(trackFormatSupport[trackIndex], params.exceedRendererCapabilitiesIfNecessary)) { @@ -2351,6 +2210,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { * renderers if so. * * @param mappedTrackInfo Mapped track information. + * @param renderererFormatSupports The {@link Capabilities} for each mapped track, indexed by + * renderer, track group and track (in that order). * @param rendererConfigurations The renderer configurations. Configurations may be replaced with * ones that enable tunneling as a result of this call. * @param trackSelections The renderer track selections. @@ -2359,7 +2220,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { */ private static void maybeConfigureRenderersForTunneling( MappedTrackInfo mappedTrackInfo, - int[][][] renderererFormatSupports, + @Capabilities int[][][] renderererFormatSupports, @NullableType RendererConfiguration[] rendererConfigurations, @NullableType TrackSelection[] trackSelections, int tunnelingAudioSessionId) { @@ -2408,21 +2269,22 @@ public class DefaultTrackSelector extends MappingTrackSelector { /** * Returns whether a renderer supports tunneling for a {@link TrackSelection}. * - * @param formatSupports The result of {@link RendererCapabilities#supportsFormat} for each track, - * indexed by group index and track index (in that order). + * @param formatSupports The {@link Capabilities} for each track, indexed by group index and track + * index (in that order). * @param trackGroups The {@link TrackGroupArray}s for the renderer. * @param selection The track selection. * @return Whether the renderer supports tunneling for the {@link TrackSelection}. */ private static boolean rendererSupportsTunneling( - int[][] formatSupports, TrackGroupArray trackGroups, TrackSelection selection) { + @Capabilities int[][] formatSupports, TrackGroupArray trackGroups, TrackSelection selection) { if (selection == null) { return false; } int trackGroupIndex = trackGroups.indexOf(selection.getTrackGroup()); for (int i = 0; i < selection.length(); i++) { + @Capabilities int trackFormatSupport = formatSupports[trackGroupIndex][selection.getIndexInTrackGroup(i)]; - if ((trackFormatSupport & RendererCapabilities.TUNNELING_SUPPORT_MASK) + if (RendererCapabilities.getTunnelingSupport(trackFormatSupport) != RendererCapabilities.TUNNELING_SUPPORTED) { return false; } @@ -2446,20 +2308,20 @@ public class DefaultTrackSelector extends MappingTrackSelector { } /** - * Applies the {@link RendererCapabilities#FORMAT_SUPPORT_MASK} to a value obtained from - * {@link RendererCapabilities#supportsFormat(Format)}, returning true if the result is - * {@link RendererCapabilities#FORMAT_HANDLED} or if {@code allowExceedsCapabilities} is set - * and the result is {@link RendererCapabilities#FORMAT_EXCEEDS_CAPABILITIES}. + * Returns true if the {@link FormatSupport} in the given {@link Capabilities} is {@link + * RendererCapabilities#FORMAT_HANDLED} or if {@code allowExceedsCapabilities} is set and the + * format support is {@link RendererCapabilities#FORMAT_EXCEEDS_CAPABILITIES}. * - * @param formatSupport A value obtained from {@link RendererCapabilities#supportsFormat(Format)}. - * @param allowExceedsCapabilities Whether to return true if the format support component of the - * value is {@link RendererCapabilities#FORMAT_EXCEEDS_CAPABILITIES}. - * @return True if the format support component is {@link RendererCapabilities#FORMAT_HANDLED}, or - * if {@code allowExceedsCapabilities} is set and the format support component is - * {@link RendererCapabilities#FORMAT_EXCEEDS_CAPABILITIES}. + * @param formatSupport {@link Capabilities}. + * @param allowExceedsCapabilities Whether to return true if {@link FormatSupport} is {@link + * RendererCapabilities#FORMAT_EXCEEDS_CAPABILITIES}. + * @return True if {@link FormatSupport} is {@link RendererCapabilities#FORMAT_HANDLED}, or if + * {@code allowExceedsCapabilities} is set and the format support is {@link + * RendererCapabilities#FORMAT_EXCEEDS_CAPABILITIES}. */ - protected static boolean isSupported(int formatSupport, boolean allowExceedsCapabilities) { - int maskedSupport = formatSupport & RendererCapabilities.FORMAT_SUPPORT_MASK; + protected static boolean isSupported( + @Capabilities int formatSupport, boolean allowExceedsCapabilities) { + @FormatSupport int maskedSupport = RendererCapabilities.getFormatSupport(formatSupport); return maskedSupport == RendererCapabilities.FORMAT_HANDLED || (allowExceedsCapabilities && maskedSupport == RendererCapabilities.FORMAT_EXCEEDS_CAPABILITIES); } @@ -2615,7 +2477,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { private final int sampleRate; private final int bitrate; - public AudioTrackScore(Format format, Parameters parameters, int formatSupport) { + public AudioTrackScore(Format format, Parameters parameters, @Capabilities int formatSupport) { this.parameters = parameters; this.language = normalizeUndeterminedLanguageToNull(format.language); isWithinRendererCapabilities = isSupported(formatSupport, false); @@ -2699,41 +2561,6 @@ public class DefaultTrackSelector extends MappingTrackSelector { } } - private static final class AudioConfigurationTuple { - - public final int channelCount; - public final int sampleRate; - @Nullable public final String mimeType; - - public AudioConfigurationTuple(int channelCount, int sampleRate, @Nullable String mimeType) { - this.channelCount = channelCount; - this.sampleRate = sampleRate; - this.mimeType = mimeType; - } - - @Override - public boolean equals(@Nullable Object obj) { - if (this == obj) { - return true; - } - if (obj == null || getClass() != obj.getClass()) { - return false; - } - AudioConfigurationTuple other = (AudioConfigurationTuple) obj; - return channelCount == other.channelCount && sampleRate == other.sampleRate - && TextUtils.equals(mimeType, other.mimeType); - } - - @Override - public int hashCode() { - int result = channelCount; - result = 31 * result + sampleRate; - result = 31 * result + (mimeType != null ? mimeType.hashCode() : 0); - return result; - } - - } - /** Represents how well a text track matches the selection {@link Parameters}. */ protected static final class TextTrackScore implements Comparable { @@ -2754,7 +2581,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { public TextTrackScore( Format format, Parameters parameters, - int trackFormatSupport, + @Capabilities int trackFormatSupport, @Nullable String selectedAudioLanguage) { isWithinRendererCapabilities = isSupported(trackFormatSupport, /* allowExceedsCapabilities= */ false); 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 425da6c1c4..59d50af405 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 @@ -22,11 +22,15 @@ import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.Renderer; import com.google.android.exoplayer2.RendererCapabilities; +import com.google.android.exoplayer2.RendererCapabilities.AdaptiveSupport; +import com.google.android.exoplayer2.RendererCapabilities.Capabilities; +import com.google.android.exoplayer2.RendererCapabilities.FormatSupport; import com.google.android.exoplayer2.RendererConfiguration; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroupArray; +import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.Util; import java.lang.annotation.Documented; import java.lang.annotation.Retention; @@ -88,28 +92,32 @@ public abstract class MappingTrackSelector extends TrackSelector { @Deprecated public final int length; private final int rendererCount; + private final String[] rendererNames; private final int[] rendererTrackTypes; private final TrackGroupArray[] rendererTrackGroups; - private final int[] rendererMixedMimeTypeAdaptiveSupports; - private final int[][][] rendererFormatSupports; + @AdaptiveSupport private final int[] rendererMixedMimeTypeAdaptiveSupports; + @Capabilities private final int[][][] rendererFormatSupports; private final TrackGroupArray unmappedTrackGroups; /** + * @param rendererNames The name of each renderer. * @param rendererTrackTypes The track type handled by each renderer. * @param rendererTrackGroups The {@link TrackGroup}s mapped to each renderer. - * @param rendererMixedMimeTypeAdaptiveSupports The result of {@link - * RendererCapabilities#supportsMixedMimeTypeAdaptation()} for each renderer. - * @param rendererFormatSupports The result of {@link RendererCapabilities#supportsFormat} for - * each mapped track, indexed by renderer, track group and track (in that order). + * @param rendererMixedMimeTypeAdaptiveSupports The {@link AdaptiveSupport} for mixed MIME type + * adaptation for the renderer. + * @param rendererFormatSupports The {@link Capabilities} for each mapped track, indexed by + * renderer, track group and track (in that order). * @param unmappedTrackGroups {@link TrackGroup}s not mapped to any renderer. */ @SuppressWarnings("deprecation") /* package */ MappedTrackInfo( + String[] rendererNames, int[] rendererTrackTypes, TrackGroupArray[] rendererTrackGroups, - int[] rendererMixedMimeTypeAdaptiveSupports, - int[][][] rendererFormatSupports, + @AdaptiveSupport int[] rendererMixedMimeTypeAdaptiveSupports, + @Capabilities int[][][] rendererFormatSupports, TrackGroupArray unmappedTrackGroups) { + this.rendererNames = rendererNames; this.rendererTrackTypes = rendererTrackTypes; this.rendererTrackGroups = rendererTrackGroups; this.rendererFormatSupports = rendererFormatSupports; @@ -124,6 +132,17 @@ public abstract class MappingTrackSelector extends TrackSelector { return rendererCount; } + /** + * Returns the name of the renderer at a given index. + * + * @see Renderer#getName() + * @param rendererIndex The renderer index. + * @return The name of the renderer. + */ + public String getRendererName(int rendererIndex) { + return rendererNames[rendererIndex]; + } + /** * Returns the track type that the renderer at a given index handles. * @@ -149,25 +168,28 @@ public abstract class MappingTrackSelector extends TrackSelector { * Returns the extent to which a renderer can play the tracks that are mapped to it. * * @param rendererIndex The renderer index. - * @return One of {@link #RENDERER_SUPPORT_PLAYABLE_TRACKS}, {@link - * #RENDERER_SUPPORT_EXCEEDS_CAPABILITIES_TRACKS}, {@link - * #RENDERER_SUPPORT_UNSUPPORTED_TRACKS} and {@link #RENDERER_SUPPORT_NO_TRACKS}. + * @return The {@link RendererSupport}. */ - public @RendererSupport int getRendererSupport(int rendererIndex) { - int bestRendererSupport = RENDERER_SUPPORT_NO_TRACKS; - int[][] rendererFormatSupport = rendererFormatSupports[rendererIndex]; - for (int[] trackGroupFormatSupport : rendererFormatSupport) { - for (int trackFormatSupport : trackGroupFormatSupport) { + @RendererSupport + public int getRendererSupport(int rendererIndex) { + @RendererSupport int bestRendererSupport = RENDERER_SUPPORT_NO_TRACKS; + @Capabilities int[][] rendererFormatSupport = rendererFormatSupports[rendererIndex]; + for (@Capabilities int[] trackGroupFormatSupport : rendererFormatSupport) { + for (@Capabilities int trackFormatSupport : trackGroupFormatSupport) { int trackRendererSupport; - switch (trackFormatSupport & RendererCapabilities.FORMAT_SUPPORT_MASK) { + switch (RendererCapabilities.getFormatSupport(trackFormatSupport)) { case RendererCapabilities.FORMAT_HANDLED: return RENDERER_SUPPORT_PLAYABLE_TRACKS; case RendererCapabilities.FORMAT_EXCEEDS_CAPABILITIES: trackRendererSupport = RENDERER_SUPPORT_EXCEEDS_CAPABILITIES_TRACKS; break; - default: + case RendererCapabilities.FORMAT_UNSUPPORTED_TYPE: + case RendererCapabilities.FORMAT_UNSUPPORTED_SUBTYPE: + case RendererCapabilities.FORMAT_UNSUPPORTED_DRM: trackRendererSupport = RENDERER_SUPPORT_UNSUPPORTED_TRACKS; break; + default: + throw new IllegalStateException(); } bestRendererSupport = Math.max(bestRendererSupport, trackRendererSupport); } @@ -177,7 +199,8 @@ public abstract class MappingTrackSelector extends TrackSelector { /** @deprecated Use {@link #getTypeSupport(int)}. */ @Deprecated - public @RendererSupport int getTrackTypeRendererSupport(int trackType) { + @RendererSupport + public int getTrackTypeRendererSupport(int trackType) { return getTypeSupport(trackType); } @@ -188,12 +211,11 @@ public abstract class MappingTrackSelector extends TrackSelector { * returned. * * @param trackType The track type. One of the {@link C} {@code TRACK_TYPE_*} constants. - * @return One of {@link #RENDERER_SUPPORT_PLAYABLE_TRACKS}, {@link - * #RENDERER_SUPPORT_EXCEEDS_CAPABILITIES_TRACKS}, {@link - * #RENDERER_SUPPORT_UNSUPPORTED_TRACKS} and {@link #RENDERER_SUPPORT_NO_TRACKS}. + * @return The {@link RendererSupport}. */ - public @RendererSupport int getTypeSupport(int trackType) { - int bestRendererSupport = RENDERER_SUPPORT_NO_TRACKS; + @RendererSupport + public int getTypeSupport(int trackType) { + @RendererSupport int bestRendererSupport = RENDERER_SUPPORT_NO_TRACKS; for (int i = 0; i < rendererCount; i++) { if (rendererTrackTypes[i] == trackType) { bestRendererSupport = Math.max(bestRendererSupport, getRendererSupport(i)); @@ -204,6 +226,7 @@ public abstract class MappingTrackSelector extends TrackSelector { /** @deprecated Use {@link #getTrackSupport(int, int, int)}. */ @Deprecated + @FormatSupport public int getTrackFormatSupport(int rendererIndex, int groupIndex, int trackIndex) { return getTrackSupport(rendererIndex, groupIndex, trackIndex); } @@ -214,15 +237,12 @@ public abstract class MappingTrackSelector extends TrackSelector { * @param rendererIndex The renderer index. * @param groupIndex The index of the track group to which the track belongs. * @param trackIndex The index of the track within the track group. - * @return One of {@link RendererCapabilities#FORMAT_HANDLED}, {@link - * RendererCapabilities#FORMAT_EXCEEDS_CAPABILITIES}, {@link - * RendererCapabilities#FORMAT_UNSUPPORTED_DRM}, {@link - * RendererCapabilities#FORMAT_UNSUPPORTED_SUBTYPE} and {@link - * RendererCapabilities#FORMAT_UNSUPPORTED_TYPE}. + * @return The {@link FormatSupport}. */ + @FormatSupport public int getTrackSupport(int rendererIndex, int groupIndex, int trackIndex) { - return rendererFormatSupports[rendererIndex][groupIndex][trackIndex] - & RendererCapabilities.FORMAT_SUPPORT_MASK; + return RendererCapabilities.getFormatSupport( + rendererFormatSupports[rendererIndex][groupIndex][trackIndex]); } /** @@ -242,10 +262,9 @@ public abstract class MappingTrackSelector extends TrackSelector { * @param groupIndex The index of the track group. * @param includeCapabilitiesExceededTracks Whether tracks that exceed the capabilities of the * renderer are included when determining support. - * @return One of {@link RendererCapabilities#ADAPTIVE_SEAMLESS}, {@link - * RendererCapabilities#ADAPTIVE_NOT_SEAMLESS} and {@link - * RendererCapabilities#ADAPTIVE_NOT_SUPPORTED}. + * @return The {@link AdaptiveSupport}. */ + @AdaptiveSupport public int getAdaptiveSupport( int rendererIndex, int groupIndex, boolean includeCapabilitiesExceededTracks) { int trackCount = rendererTrackGroups[rendererIndex].get(groupIndex).length; @@ -253,7 +272,7 @@ public abstract class MappingTrackSelector extends TrackSelector { int[] trackIndices = new int[trackCount]; int trackIndexCount = 0; for (int i = 0; i < trackCount; i++) { - int fixedSupport = getTrackSupport(rendererIndex, groupIndex, i); + @FormatSupport int fixedSupport = getTrackSupport(rendererIndex, groupIndex, i); if (fixedSupport == RendererCapabilities.FORMAT_HANDLED || (includeCapabilitiesExceededTracks && fixedSupport == RendererCapabilities.FORMAT_EXCEEDS_CAPABILITIES)) { @@ -270,13 +289,12 @@ public abstract class MappingTrackSelector extends TrackSelector { * * @param rendererIndex The renderer index. * @param groupIndex The index of the track group. - * @return One of {@link RendererCapabilities#ADAPTIVE_SEAMLESS}, {@link - * RendererCapabilities#ADAPTIVE_NOT_SEAMLESS} and {@link - * RendererCapabilities#ADAPTIVE_NOT_SUPPORTED}. + * @return The {@link AdaptiveSupport}. */ + @AdaptiveSupport public int getAdaptiveSupport(int rendererIndex, int groupIndex, int[] trackIndices) { int handledTrackCount = 0; - int adaptiveSupport = RendererCapabilities.ADAPTIVE_SEAMLESS; + @AdaptiveSupport int adaptiveSupport = RendererCapabilities.ADAPTIVE_SEAMLESS; boolean multipleMimeTypes = false; String firstSampleMimeType = null; for (int i = 0; i < trackIndices.length; i++) { @@ -291,8 +309,8 @@ public abstract class MappingTrackSelector extends TrackSelector { adaptiveSupport = Math.min( adaptiveSupport, - rendererFormatSupports[rendererIndex][groupIndex][i] - & RendererCapabilities.ADAPTIVE_SUPPORT_MASK); + RendererCapabilities.getAdaptiveSupport( + rendererFormatSupports[rendererIndex][groupIndex][i])); } return multipleMimeTypes ? Math.min(adaptiveSupport, rendererMixedMimeTypeAdaptiveSupports[rendererIndex]) @@ -341,13 +359,14 @@ public abstract class MappingTrackSelector extends TrackSelector { // any renderer. int[] rendererTrackGroupCounts = new int[rendererCapabilities.length + 1]; TrackGroup[][] rendererTrackGroups = new TrackGroup[rendererCapabilities.length + 1][]; - int[][][] rendererFormatSupports = new int[rendererCapabilities.length + 1][][]; + @Capabilities int[][][] rendererFormatSupports = new int[rendererCapabilities.length + 1][][]; for (int i = 0; i < rendererTrackGroups.length; i++) { rendererTrackGroups[i] = new TrackGroup[trackGroups.length]; rendererFormatSupports[i] = new int[trackGroups.length][]; } // Determine the extent to which each renderer supports mixed mimeType adaptation. + @AdaptiveSupport int[] rendererMixedMimeTypeAdaptationSupports = getMixedMimeTypeAdaptationSupports(rendererCapabilities); @@ -356,10 +375,17 @@ public abstract class MappingTrackSelector extends TrackSelector { for (int groupIndex = 0; groupIndex < trackGroups.length; groupIndex++) { TrackGroup group = trackGroups.get(groupIndex); // Associate the group to a preferred renderer. - int rendererIndex = findRenderer(rendererCapabilities, group); + boolean preferUnassociatedRenderer = + MimeTypes.getTrackType(group.getFormat(0).sampleMimeType) == C.TRACK_TYPE_METADATA; + int rendererIndex = + findRenderer( + rendererCapabilities, group, rendererTrackGroupCounts, preferUnassociatedRenderer); // Evaluate the support that the renderer provides for each track in the group. - int[] rendererFormatSupport = rendererIndex == rendererCapabilities.length - ? new int[group.length] : getFormatSupport(rendererCapabilities[rendererIndex], group); + @Capabilities + int[] rendererFormatSupport = + rendererIndex == rendererCapabilities.length + ? new int[group.length] + : getFormatSupport(rendererCapabilities[rendererIndex], group); // Stash the results. int rendererTrackGroupCount = rendererTrackGroupCounts[rendererIndex]; rendererTrackGroups[rendererIndex][rendererTrackGroupCount] = group; @@ -369,6 +395,7 @@ public abstract class MappingTrackSelector extends TrackSelector { // Create a track group array for each renderer, and trim each rendererFormatSupports entry. TrackGroupArray[] rendererTrackGroupArrays = new TrackGroupArray[rendererCapabilities.length]; + String[] rendererNames = new String[rendererCapabilities.length]; int[] rendererTrackTypes = new int[rendererCapabilities.length]; for (int i = 0; i < rendererCapabilities.length; i++) { int rendererTrackGroupCount = rendererTrackGroupCounts[i]; @@ -377,6 +404,7 @@ public abstract class MappingTrackSelector extends TrackSelector { Util.nullSafeArrayCopy(rendererTrackGroups[i], rendererTrackGroupCount)); rendererFormatSupports[i] = Util.nullSafeArrayCopy(rendererFormatSupports[i], rendererTrackGroupCount); + rendererNames[i] = rendererCapabilities[i].getName(); rendererTrackTypes[i] = rendererCapabilities[i].getTrackType(); } @@ -390,6 +418,7 @@ public abstract class MappingTrackSelector extends TrackSelector { // Package up the track information and selections. MappedTrackInfo mappedTrackInfo = new MappedTrackInfo( + rendererNames, rendererTrackTypes, rendererTrackGroupArrays, rendererMixedMimeTypeAdaptationSupports, @@ -406,10 +435,10 @@ public abstract class MappingTrackSelector extends TrackSelector { * Given mapped track information, returns a track selection and configuration for each renderer. * * @param mappedTrackInfo Mapped track information. - * @param rendererFormatSupports The result of {@link RendererCapabilities#supportsFormat} for - * each mapped track, indexed by renderer, track group and track (in that order). - * @param rendererMixedMimeTypeAdaptationSupport The result of {@link - * RendererCapabilities#supportsMixedMimeTypeAdaptation()} for each renderer. + * @param rendererFormatSupports The {@link Capabilities} for ach mapped track, indexed by + * renderer, track group and track (in that order). + * @param rendererMixedMimeTypeAdaptationSupport The {@link AdaptiveSupport} for mixed MIME type + * adaptation for the renderer. * @return A pair consisting of the track selections and configurations for each renderer. A null * configuration indicates the renderer should be disabled, in which case the track selection * will also be null. A track selection may also be null for a non-disabled renderer if {@link @@ -419,65 +448,89 @@ public abstract class MappingTrackSelector extends TrackSelector { protected abstract Pair<@NullableType RendererConfiguration[], @NullableType TrackSelection[]> selectTracks( MappedTrackInfo mappedTrackInfo, - int[][][] rendererFormatSupports, - int[] rendererMixedMimeTypeAdaptationSupport) + @Capabilities int[][][] rendererFormatSupports, + @AdaptiveSupport int[] rendererMixedMimeTypeAdaptationSupport) throws ExoPlaybackException; /** * Finds the renderer to which the provided {@link TrackGroup} should be mapped. - *

      - * A {@link TrackGroup} is mapped to the renderer that reports the highest of (listed in - * decreasing order of support) {@link RendererCapabilities#FORMAT_HANDLED}, - * {@link RendererCapabilities#FORMAT_EXCEEDS_CAPABILITIES}, - * {@link RendererCapabilities#FORMAT_UNSUPPORTED_DRM} and - * {@link RendererCapabilities#FORMAT_UNSUPPORTED_SUBTYPE}. In the case that two or more renderers - * report the same level of support, the renderer with the lowest index is associated. - *

      - * If all renderers report {@link RendererCapabilities#FORMAT_UNSUPPORTED_TYPE} for all of the + * + *

      A {@link TrackGroup} is mapped to the renderer that reports the highest of (listed in + * decreasing order of support) {@link RendererCapabilities#FORMAT_HANDLED}, {@link + * RendererCapabilities#FORMAT_EXCEEDS_CAPABILITIES}, {@link + * RendererCapabilities#FORMAT_UNSUPPORTED_DRM} and {@link + * RendererCapabilities#FORMAT_UNSUPPORTED_SUBTYPE}. + * + *

      In the case that two or more renderers report the same level of support, the assignment + * depends on {@code preferUnassociatedRenderer}. + * + *

        + *
      • If {@code preferUnassociatedRenderer} is false, the renderer with the lowest index is + * chosen regardless of how many other track groups are already mapped to this renderer. + *
      • If {@code preferUnassociatedRenderer} is true, the renderer with the lowest index and no + * other mapped track group is chosen, or the renderer with the lowest index if all + * available renderers have already mapped track groups. + *
      + * + *

      If all renderers report {@link RendererCapabilities#FORMAT_UNSUPPORTED_TYPE} for all of the * tracks in the group, then {@code renderers.length} is returned to indicate that the group was * not mapped to any renderer. * * @param rendererCapabilities The {@link RendererCapabilities} of the renderers. * @param group The track group to map to a renderer. - * @return The index of the renderer to which the track group was mapped, or - * {@code renderers.length} if it was not mapped to any renderer. + * @param rendererTrackGroupCounts The number of already mapped track groups for each renderer. + * @param preferUnassociatedRenderer Whether renderers unassociated to any track group should be + * preferred. + * @return The index of the renderer to which the track group was mapped, or {@code + * renderers.length} if it was not mapped to any renderer. * @throws ExoPlaybackException If an error occurs finding a renderer. */ - private static int findRenderer(RendererCapabilities[] rendererCapabilities, TrackGroup group) + private static int findRenderer( + RendererCapabilities[] rendererCapabilities, + TrackGroup group, + int[] rendererTrackGroupCounts, + boolean preferUnassociatedRenderer) throws ExoPlaybackException { int bestRendererIndex = rendererCapabilities.length; - int bestFormatSupportLevel = RendererCapabilities.FORMAT_UNSUPPORTED_TYPE; + @FormatSupport int bestFormatSupportLevel = RendererCapabilities.FORMAT_UNSUPPORTED_TYPE; + boolean bestRendererIsUnassociated = true; for (int rendererIndex = 0; rendererIndex < rendererCapabilities.length; rendererIndex++) { RendererCapabilities rendererCapability = rendererCapabilities[rendererIndex]; + @FormatSupport int formatSupportLevel = RendererCapabilities.FORMAT_UNSUPPORTED_TYPE; for (int trackIndex = 0; trackIndex < group.length; trackIndex++) { - int formatSupportLevel = rendererCapability.supportsFormat(group.getFormat(trackIndex)) - & RendererCapabilities.FORMAT_SUPPORT_MASK; - if (formatSupportLevel > bestFormatSupportLevel) { - bestRendererIndex = rendererIndex; - bestFormatSupportLevel = formatSupportLevel; - if (bestFormatSupportLevel == RendererCapabilities.FORMAT_HANDLED) { - // We can't do better. - return bestRendererIndex; - } - } + @FormatSupport + int trackFormatSupportLevel = + RendererCapabilities.getFormatSupport( + rendererCapability.supportsFormat(group.getFormat(trackIndex))); + formatSupportLevel = Math.max(formatSupportLevel, trackFormatSupportLevel); + } + boolean rendererIsUnassociated = rendererTrackGroupCounts[rendererIndex] == 0; + if (formatSupportLevel > bestFormatSupportLevel + || (formatSupportLevel == bestFormatSupportLevel + && preferUnassociatedRenderer + && !bestRendererIsUnassociated + && rendererIsUnassociated)) { + bestRendererIndex = rendererIndex; + bestFormatSupportLevel = formatSupportLevel; + bestRendererIsUnassociated = rendererIsUnassociated; } } return bestRendererIndex; } /** - * Calls {@link RendererCapabilities#supportsFormat} for each track in the specified - * {@link TrackGroup}, returning the results in an array. + * Calls {@link RendererCapabilities#supportsFormat} for each track in the specified {@link + * TrackGroup}, returning the results in an array. * * @param rendererCapabilities The {@link RendererCapabilities} of the renderer. * @param group The track group to evaluate. - * @return An array containing the result of calling - * {@link RendererCapabilities#supportsFormat} for each track in the group. + * @return An array containing {@link Capabilities} for each track in the group. * @throws ExoPlaybackException If an error occurs determining the format support. */ + @Capabilities private static int[] getFormatSupport(RendererCapabilities rendererCapabilities, TrackGroup group) throws ExoPlaybackException { - int[] formatSupport = new int[group.length]; + @Capabilities int[] formatSupport = new int[group.length]; for (int i = 0; i < group.length; i++) { formatSupport[i] = rendererCapabilities.supportsFormat(group.getFormat(i)); } @@ -489,13 +542,14 @@ public abstract class MappingTrackSelector extends TrackSelector { * returning the results in an array. * * @param rendererCapabilities The {@link RendererCapabilities} of the renderers. - * @return An array containing the result of calling {@link - * RendererCapabilities#supportsMixedMimeTypeAdaptation()} for each renderer. + * @return An array containing the {@link AdaptiveSupport} for mixed MIME type adaptation for the + * renderer. * @throws ExoPlaybackException If an error occurs determining the adaptation support. */ + @AdaptiveSupport private static int[] getMixedMimeTypeAdaptationSupports( RendererCapabilities[] rendererCapabilities) throws ExoPlaybackException { - int[] mixedMimeTypeAdaptationSupport = new int[rendererCapabilities.length]; + @AdaptiveSupport int[] mixedMimeTypeAdaptationSupport = new int[rendererCapabilities.length]; for (int i = 0; i < mixedMimeTypeAdaptationSupport.length; i++) { mixedMimeTypeAdaptationSupport[i] = rendererCapabilities[i].supportsMixedMimeTypeAdaptation(); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectionParameters.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectionParameters.java index 6e10171f08..3871a31a3b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectionParameters.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectionParameters.java @@ -15,7 +15,6 @@ */ package com.google.android.exoplayer2.trackselection; -import android.annotation.TargetApi; import android.content.Context; import android.os.Looper; import android.os.Parcel; @@ -23,6 +22,7 @@ import android.os.Parcelable; import android.text.TextUtils; import android.view.accessibility.CaptioningManager; import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.util.Util; import java.util.Locale; @@ -47,7 +47,7 @@ public class TrackSelectionParameters implements Parcelable { * * @param context Any context. */ - @SuppressWarnings({"deprecation", "initialization:method.invocation.invalid"}) + @SuppressWarnings({"deprecation", "nullness:method.invocation.invalid"}) public Builder(Context context) { this(); setPreferredTextLanguageAndRoleFlagsToCaptioningManagerSettings(context); @@ -169,7 +169,7 @@ public class TrackSelectionParameters implements Parcelable { disabledTextTrackSelectionFlags); } - @TargetApi(19) + @RequiresApi(19) private void setPreferredTextLanguageAndRoleFlagsToCaptioningManagerSettingsV19( Context context) { if (Util.SDK_INT < 23 && Looper.myLooper() == null) { 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 fb74bd9d54..d48c140ac8 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 @@ -36,8 +36,6 @@ import com.google.android.exoplayer2.util.Assertions; * * 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, BandwidthMeter)}. @@ -75,7 +73,7 @@ import com.google.android.exoplayer2.util.Assertions; * 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. + * determine the track selection and corresponding renderer configurations in a single step. * *

        Threading model

        * 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 e592c3bec3..55c580ead2 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 @@ -36,8 +36,6 @@ public final class DataSchemeDataSource extends BaseDataSource { private int endPosition; private int readPosition; - // the constructor does not initialize fields: data - @SuppressWarnings("nullness:initialization.fields.uninitialized") public DataSchemeDataSource() { super(/* isNetwork= */ false); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSourceInputStream.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSourceInputStream.java index 6c4e77a90a..c4296bd6f6 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSourceInputStream.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSourceInputStream.java @@ -15,7 +15,6 @@ */ package com.google.android.exoplayer2.upstream; -import androidx.annotation.NonNull; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.util.Assertions; import java.io.IOException; @@ -72,12 +71,12 @@ public final class DataSourceInputStream extends InputStream { } @Override - public int read(@NonNull byte[] buffer) throws IOException { + public int read(byte[] buffer) throws IOException { return read(buffer, 0, buffer.length); } @Override - public int read(@NonNull byte[] buffer, int offset, int length) throws IOException { + public int read(byte[] buffer, int offset, int length) throws IOException { Assertions.checkState(!closed); checkOpened(); int bytesRead = dataSource.read(buffer, offset, length); 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 deleted file mode 100644 index acf5550427..0000000000 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSpec.java +++ /dev/null @@ -1,478 +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.upstream; - -import android.net.Uri; -import androidx.annotation.IntDef; -import androidx.annotation.Nullable; -import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.util.Assertions; -import java.lang.annotation.Documented; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; - -/** - * Defines a region of data. - */ -public final class DataSpec { - - /** - * The flags that apply to any request for data. Possible flag values are {@link - * #FLAG_ALLOW_GZIP}, {@link #FLAG_DONT_CACHE_IF_LENGTH_UNKNOWN} and {@link - * #FLAG_ALLOW_CACHE_FRAGMENTATION}. - */ - @Documented - @Retention(RetentionPolicy.SOURCE) - @IntDef( - flag = true, - value = {FLAG_ALLOW_GZIP, FLAG_DONT_CACHE_IF_LENGTH_UNKNOWN, FLAG_ALLOW_CACHE_FRAGMENTATION}) - public @interface Flags {} - /** - * Allows an underlying network stack to request that the server use gzip compression. - * - *

        Should not typically be set if the data being requested is already compressed (e.g. most - * audio and video requests). May be set when requesting other data. - * - *

        When a {@link DataSource} is used to request data with this flag set, and if the {@link - * DataSource} does make a network request, then the value returned from {@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; - /** Prevents caching if the length cannot be resolved when the {@link DataSource} is opened. */ - public static final int FLAG_DONT_CACHE_IF_LENGTH_UNKNOWN = 1 << 1; // 2 - /** - * Allows fragmentation of this request into multiple cache files, meaning a cache eviction policy - * will be able to evict individual fragments of the data. Depending on the cache implementation, - * setting this flag may also enable more concurrent access to the data (e.g. reading one fragment - * whilst writing another). - */ - public static final int FLAG_ALLOW_CACHE_FRAGMENTATION = 1 << 2; // 4 - - /** - * The set of HTTP methods that are supported by ExoPlayer {@link HttpDataSource}s. One of {@link - * #HTTP_METHOD_GET}, {@link #HTTP_METHOD_POST} or {@link #HTTP_METHOD_HEAD}. - */ - @Documented - @Retention(RetentionPolicy.SOURCE) - @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; - - /** - * 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 @HttpMethod int httpMethod; - - /** - * The HTTP request body, null otherwise. If the body is non-null, then httpBody.length will be - * non-zero. - */ - @Nullable public final byte[] httpBody; - - /** Immutable map containing the headers to use in HTTP requests. */ - public final Map httpRequestHeaders; - - /** The absolute position of the data in the full stream. */ - public final long absoluteStreamPosition; - /** - * The position of the data when read from {@link #uri}. - *

        - * Always equal to {@link #absoluteStreamPosition} unless the {@link #uri} defines the location - * of a subset of the underlying data. - */ - public final long position; - /** - * The length of the data, or {@link C#LENGTH_UNSET}. - */ - public final long length; - /** - * A key that uniquely identifies the original stream. Used for cache indexing. May be null if the - * data spec is not intended to be used in conjunction with a cache. - */ - @Nullable public final String key; - /** Request {@link Flags flags}. */ - public final @Flags int flags; - - /** - * Construct a data spec for the given uri and with {@link #key} set to null. - * - * @param uri {@link #uri}. - */ - public DataSpec(Uri uri) { - this(uri, 0); - } - - /** - * Construct a data spec for the given uri and with {@link #key} set to null. - * - * @param uri {@link #uri}. - * @param flags {@link #flags}. - */ - public DataSpec(Uri uri, @Flags int flags) { - this(uri, 0, C.LENGTH_UNSET, null, flags); - } - - /** - * Construct a data spec where {@link #position} equals {@link #absoluteStreamPosition}. - * - * @param uri {@link #uri}. - * @param absoluteStreamPosition {@link #absoluteStreamPosition}, equal to {@link #position}. - * @param length {@link #length}. - * @param key {@link #key}. - */ - public DataSpec(Uri uri, long absoluteStreamPosition, long length, @Nullable String key) { - this(uri, absoluteStreamPosition, absoluteStreamPosition, length, key, 0); - } - - /** - * Construct a data spec where {@link #position} equals {@link #absoluteStreamPosition}. - * - * @param uri {@link #uri}. - * @param absoluteStreamPosition {@link #absoluteStreamPosition}, equal to {@link #position}. - * @param length {@link #length}. - * @param key {@link #key}. - * @param flags {@link #flags}. - */ - public DataSpec( - Uri uri, long absoluteStreamPosition, long length, @Nullable String key, @Flags int flags) { - this(uri, absoluteStreamPosition, absoluteStreamPosition, length, key, flags); - } - - /** - * Construct a data spec where {@link #position} equals {@link #absoluteStreamPosition} and has - * request headers. - * - * @param uri {@link #uri}. - * @param absoluteStreamPosition {@link #absoluteStreamPosition}, equal to {@link #position}. - * @param length {@link #length}. - * @param key {@link #key}. - * @param flags {@link #flags}. - * @param httpRequestHeaders {@link #httpRequestHeaders} - */ - public DataSpec( - Uri uri, - long absoluteStreamPosition, - long length, - @Nullable String key, - @Flags int flags, - Map httpRequestHeaders) { - this( - uri, - inferHttpMethod(null), - null, - absoluteStreamPosition, - absoluteStreamPosition, - length, - key, - flags, - httpRequestHeaders); - } - - /** - * Construct a data spec where {@link #position} may differ from {@link #absoluteStreamPosition}. - * - * @param uri {@link #uri}. - * @param absoluteStreamPosition {@link #absoluteStreamPosition}. - * @param position {@link #position}. - * @param length {@link #length}. - * @param key {@link #key}. - * @param flags {@link #flags}. - */ - public DataSpec( - Uri uri, - long absoluteStreamPosition, - long position, - long length, - @Nullable String key, - @Flags int flags) { - this(uri, null, absoluteStreamPosition, position, length, key, flags); - } - - /** - * Construct a data spec 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 #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}. - * @param key {@link #key}. - * @param flags {@link #flags}. - */ - public DataSpec( - Uri uri, - @Nullable byte[] postBody, - long absoluteStreamPosition, - long position, - long length, - @Nullable String key, - @Flags int flags) { - this( - uri, - /* httpMethod= */ inferHttpMethod(postBody), - /* httpBody= */ postBody, - absoluteStreamPosition, - position, - length, - key, - flags); - } - - /** - * Construct a data spec 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) { - this( - uri, - httpMethod, - httpBody, - absoluteStreamPosition, - position, - length, - key, - flags, - /* httpRequestHeaders= */ Collections.emptyMap()); - } - - /** - * Construct a data spec with request parameters to be used as HTTP headers inside HTTP requests. - * - * @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}. - * @param httpRequestHeaders {@link #httpRequestHeaders}. - */ - public DataSpec( - Uri uri, - @HttpMethod int httpMethod, - @Nullable byte[] httpBody, - long absoluteStreamPosition, - long position, - long length, - @Nullable String key, - @Flags int flags, - Map httpRequestHeaders) { - Assertions.checkArgument(absoluteStreamPosition >= 0); - Assertions.checkArgument(position >= 0); - Assertions.checkArgument(length > 0 || length == C.LENGTH_UNSET); - this.uri = uri; - this.httpMethod = httpMethod; - this.httpBody = (httpBody != null && httpBody.length != 0) ? httpBody : null; - this.absoluteStreamPosition = absoluteStreamPosition; - this.position = position; - this.length = length; - this.key = key; - this.flags = flags; - this.httpRequestHeaders = Collections.unmodifiableMap(new HashMap<>(httpRequestHeaders)); - } - - /** - * Returns whether the given flag is set. - * - * @param flag Flag to be checked if it is set. - */ - public boolean isFlagSet(@Flags int flag) { - return (this.flags & flag) == flag; - } - - @Override - public String toString() { - 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); - } - } - - /** - * Returns a data spec that represents a subrange of the data defined by this DataSpec. The - * subrange includes data from the offset up to the end of this DataSpec. - * - * @param offset The offset of the subrange. - * @return A data spec that represents a subrange of the data defined by this DataSpec. - */ - public DataSpec subrange(long offset) { - return subrange(offset, length == C.LENGTH_UNSET ? C.LENGTH_UNSET : length - offset); - } - - /** - * Returns a data spec that represents a subrange of the data defined by this DataSpec. - * - * @param offset The offset of the subrange. - * @param length The length of the subrange. - * @return A data spec that represents a subrange of the data defined by this DataSpec. - */ - public DataSpec subrange(long offset, long length) { - if (offset == 0 && this.length == length) { - return this; - } else { - return new DataSpec( - uri, - httpMethod, - httpBody, - absoluteStreamPosition + offset, - position + offset, - length, - key, - flags, - httpRequestHeaders); - } - } - - /** - * Returns a copy of this data spec with the specified Uri. - * - * @param uri The new source {@link Uri}. - * @return The copied data spec with the specified Uri. - */ - public DataSpec withUri(Uri uri) { - return new DataSpec( - uri, - httpMethod, - httpBody, - absoluteStreamPosition, - position, - length, - key, - flags, - httpRequestHeaders); - } - - /** - * Returns a copy of this data spec with the specified request headers. - * - * @param requestHeaders The HTTP request headers. - * @return The copied data spec with the specified request headers. - */ - public DataSpec withRequestHeaders(Map requestHeaders) { - return new DataSpec( - uri, - httpMethod, - httpBody, - absoluteStreamPosition, - position, - length, - key, - flags, - requestHeaders); - } - - /** - * Returns a copy this data spec with additional request headers. - * - *

        Note: Values in {@code requestHeaders} will overwrite values with the same header key that - * were previously set in this instance's {@code #httpRequestHeaders}. - * - * @param requestHeaders The additional HTTP request headers. - * @return The copied data with the additional HTTP request headers. - */ - public DataSpec withAdditionalHeaders(Map requestHeaders) { - Map totalHeaders = new HashMap<>(this.httpRequestHeaders); - totalHeaders.putAll(requestHeaders); - - return new DataSpec( - uri, - httpMethod, - httpBody, - absoluteStreamPosition, - position, - length, - key, - flags, - totalHeaders); - } - - @HttpMethod - private static int inferHttpMethod(@Nullable byte[] postBody) { - return postBody != null ? HTTP_METHOD_POST : HTTP_METHOD_GET; - } -} 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 71e2d8d19f..ca9cca255d 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 @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.upstream; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Util; import java.util.Arrays; @@ -28,7 +29,7 @@ public final class DefaultAllocator implements Allocator { private final boolean trimOnReset; private final int individualAllocationSize; - private final byte[] initialAllocationBlock; + @Nullable private final byte[] initialAllocationBlock; private final Allocation[] singleAllocationReleaseHolder; private int targetBufferSize; 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 f688bb9447..ceaefad0b9 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 @@ -56,19 +56,19 @@ public final class DefaultBandwidthMeter implements BandwidthMeter, TransferList /** Default initial Wifi bitrate estimate in bits per second. */ public static final long[] DEFAULT_INITIAL_BITRATE_ESTIMATES_WIFI = - new long[] {5_700_000, 3_500_000, 2_000_000, 1_100_000, 470_000}; + new long[] {5_800_000, 3_500_000, 1_900_000, 1_000_000, 520_000}; /** Default initial 2G bitrate estimates in bits per second. */ public static final long[] DEFAULT_INITIAL_BITRATE_ESTIMATES_2G = - new long[] {200_000, 148_000, 132_000, 115_000, 95_000}; + new long[] {204_000, 154_000, 139_000, 122_000, 102_000}; /** Default initial 3G bitrate estimates in bits per second. */ public static final long[] DEFAULT_INITIAL_BITRATE_ESTIMATES_3G = - new long[] {2_200_000, 1_300_000, 970_000, 810_000, 490_000}; + new long[] {2_200_000, 1_150_000, 810_000, 640_000, 450_000}; /** Default initial 4G bitrate estimates in bits per second. */ public static final long[] DEFAULT_INITIAL_BITRATE_ESTIMATES_4G = - new long[] {5_300_000, 3_200_000, 2_000_000, 1_400_000, 690_000}; + new long[] {4_900_000, 2_300_000, 1_500_000, 970_000, 540_000}; /** * Default initial bitrate estimate used when the device is offline or the network type cannot be @@ -203,14 +203,15 @@ public final class DefaultBandwidthMeter implements BandwidthMeter, TransferList result.append(C.NETWORK_TYPE_2G, DEFAULT_INITIAL_BITRATE_ESTIMATES_2G[groupIndices[1]]); result.append(C.NETWORK_TYPE_3G, DEFAULT_INITIAL_BITRATE_ESTIMATES_3G[groupIndices[2]]); result.append(C.NETWORK_TYPE_4G, DEFAULT_INITIAL_BITRATE_ESTIMATES_4G[groupIndices[3]]); - // Assume default Wifi bitrate for Ethernet to prevent using the slower fallback bitrate. + // Assume default Wifi bitrate for Ethernet and 5G to prevent using the slower fallback. result.append( C.NETWORK_TYPE_ETHERNET, DEFAULT_INITIAL_BITRATE_ESTIMATES_WIFI[groupIndices[0]]); + result.append(C.NETWORK_TYPE_5G, DEFAULT_INITIAL_BITRATE_ESTIMATES_WIFI[groupIndices[0]]); return result; } private static int[] getCountryGroupIndices(String countryCode) { - int[] groupIndices = DEFAULT_INITIAL_BITRATE_COUNTRY_GROUPS.get(countryCode); + @Nullable int[] groupIndices = DEFAULT_INITIAL_BITRATE_COUNTRY_GROUPS.get(countryCode); // Assume median group if not found. return groupIndices == null ? new int[] {2, 2, 2, 2} : groupIndices; } @@ -304,7 +305,6 @@ public final class DefaultBandwidthMeter implements BandwidthMeter, TransferList } @Override - @Nullable public TransferListener getTransferListener() { return this; } @@ -327,7 +327,7 @@ public final class DefaultBandwidthMeter implements BandwidthMeter, TransferList @Override public synchronized void onTransferStart( DataSource source, DataSpec dataSpec, boolean isNetwork) { - if (!isNetwork) { + if (!isTransferAtFullNetworkSpeed(dataSpec, isNetwork)) { return; } if (streamCount == 0) { @@ -339,7 +339,7 @@ public final class DefaultBandwidthMeter implements BandwidthMeter, TransferList @Override public synchronized void onBytesTransferred( DataSource source, DataSpec dataSpec, boolean isNetwork, int bytes) { - if (!isNetwork) { + if (!isTransferAtFullNetworkSpeed(dataSpec, isNetwork)) { return; } sampleBytesTransferred += bytes; @@ -347,7 +347,7 @@ public final class DefaultBandwidthMeter implements BandwidthMeter, TransferList @Override public synchronized void onTransferEnd(DataSource source, DataSpec dataSpec, boolean isNetwork) { - if (!isNetwork) { + if (!isTransferAtFullNetworkSpeed(dataSpec, isNetwork)) { return; } Assertions.checkState(streamCount > 0); @@ -421,6 +421,10 @@ public final class DefaultBandwidthMeter implements BandwidthMeter, TransferList return initialBitrateEstimate; } + private static boolean isTransferAtFullNetworkSpeed(DataSpec dataSpec, boolean isNetwork) { + return isNetwork && !dataSpec.isFlagSet(DataSpec.FLAG_MIGHT_NOT_USE_FULL_NETWORK_SPEED); + } + /* * Note: This class only holds a weak reference to DefaultBandwidthMeter instances. It should not * be made non-static, since doing so adds a strong reference (i.e. DefaultBandwidthMeter.this). @@ -487,244 +491,245 @@ public final class DefaultBandwidthMeter implements BandwidthMeter, TransferList private static Map createInitialBitrateCountryGroupAssignment() { HashMap countryGroupAssignment = new HashMap<>(); - countryGroupAssignment.put("AD", new int[] {1, 1, 0, 0}); - countryGroupAssignment.put("AE", new int[] {1, 4, 4, 4}); + countryGroupAssignment.put("AD", new int[] {0, 2, 0, 0}); + countryGroupAssignment.put("AE", new int[] {2, 4, 4, 4}); countryGroupAssignment.put("AF", new int[] {4, 4, 3, 3}); - countryGroupAssignment.put("AG", new int[] {3, 1, 0, 1}); - countryGroupAssignment.put("AI", new int[] {1, 0, 0, 3}); + countryGroupAssignment.put("AG", new int[] {4, 2, 2, 3}); + countryGroupAssignment.put("AI", new int[] {0, 3, 2, 4}); countryGroupAssignment.put("AL", new int[] {1, 2, 0, 1}); - countryGroupAssignment.put("AM", new int[] {2, 2, 2, 2}); - countryGroupAssignment.put("AO", new int[] {3, 4, 2, 0}); - countryGroupAssignment.put("AR", new int[] {2, 3, 2, 2}); - countryGroupAssignment.put("AS", new int[] {3, 0, 4, 2}); + countryGroupAssignment.put("AM", new int[] {2, 2, 1, 2}); + countryGroupAssignment.put("AO", new int[] {3, 4, 3, 1}); + countryGroupAssignment.put("AQ", new int[] {4, 2, 2, 2}); + countryGroupAssignment.put("AR", new int[] {2, 3, 1, 2}); + countryGroupAssignment.put("AS", new int[] {2, 2, 4, 2}); countryGroupAssignment.put("AT", new int[] {0, 3, 0, 0}); - countryGroupAssignment.put("AU", new int[] {0, 3, 0, 1}); - countryGroupAssignment.put("AW", new int[] {1, 1, 0, 3}); - countryGroupAssignment.put("AX", new int[] {0, 3, 0, 2}); + countryGroupAssignment.put("AU", new int[] {0, 2, 0, 1}); + countryGroupAssignment.put("AW", new int[] {1, 1, 2, 4}); + countryGroupAssignment.put("AX", new int[] {0, 1, 0, 0}); countryGroupAssignment.put("AZ", new int[] {3, 3, 3, 3}); countryGroupAssignment.put("BA", new int[] {1, 1, 0, 1}); - countryGroupAssignment.put("BB", new int[] {0, 2, 0, 0}); - countryGroupAssignment.put("BD", new int[] {2, 1, 3, 3}); - countryGroupAssignment.put("BE", new int[] {0, 0, 0, 1}); + countryGroupAssignment.put("BB", new int[] {0, 3, 0, 0}); + countryGroupAssignment.put("BD", new int[] {2, 0, 4, 3}); + countryGroupAssignment.put("BE", new int[] {0, 1, 2, 3}); countryGroupAssignment.put("BF", new int[] {4, 4, 4, 1}); countryGroupAssignment.put("BG", new int[] {0, 1, 0, 0}); - countryGroupAssignment.put("BH", new int[] {2, 1, 3, 4}); + countryGroupAssignment.put("BH", new int[] {1, 0, 3, 4}); countryGroupAssignment.put("BI", new int[] {4, 4, 4, 4}); - countryGroupAssignment.put("BJ", new int[] {4, 4, 4, 4}); - countryGroupAssignment.put("BL", new int[] {1, 0, 2, 2}); - countryGroupAssignment.put("BM", new int[] {1, 2, 0, 0}); - countryGroupAssignment.put("BN", new int[] {4, 1, 3, 2}); - countryGroupAssignment.put("BO", new int[] {1, 2, 3, 2}); - countryGroupAssignment.put("BQ", new int[] {1, 1, 2, 4}); - countryGroupAssignment.put("BR", new int[] {2, 3, 3, 2}); - countryGroupAssignment.put("BS", new int[] {2, 1, 1, 4}); + countryGroupAssignment.put("BJ", new int[] {4, 4, 3, 4}); + countryGroupAssignment.put("BL", new int[] {1, 0, 4, 3}); + countryGroupAssignment.put("BM", new int[] {0, 1, 0, 0}); + countryGroupAssignment.put("BN", new int[] {4, 0, 2, 4}); + countryGroupAssignment.put("BO", new int[] {1, 3, 3, 3}); + countryGroupAssignment.put("BQ", new int[] {1, 0, 1, 0}); + countryGroupAssignment.put("BR", new int[] {2, 4, 3, 1}); + countryGroupAssignment.put("BS", new int[] {3, 1, 1, 3}); countryGroupAssignment.put("BT", new int[] {3, 0, 3, 1}); - countryGroupAssignment.put("BW", new int[] {4, 4, 1, 2}); - countryGroupAssignment.put("BY", new int[] {0, 1, 1, 2}); - countryGroupAssignment.put("BZ", new int[] {2, 2, 2, 1}); - countryGroupAssignment.put("CA", new int[] {0, 3, 1, 3}); - countryGroupAssignment.put("CD", new int[] {4, 4, 2, 2}); - countryGroupAssignment.put("CF", new int[] {4, 4, 3, 0}); - countryGroupAssignment.put("CG", new int[] {3, 4, 2, 4}); - countryGroupAssignment.put("CH", new int[] {0, 0, 1, 0}); + countryGroupAssignment.put("BW", new int[] {3, 4, 3, 3}); + countryGroupAssignment.put("BY", new int[] {0, 1, 1, 1}); + countryGroupAssignment.put("BZ", new int[] {1, 3, 2, 1}); + countryGroupAssignment.put("CA", new int[] {0, 3, 2, 2}); + countryGroupAssignment.put("CD", new int[] {3, 4, 2, 2}); + countryGroupAssignment.put("CF", new int[] {4, 3, 2, 2}); + countryGroupAssignment.put("CG", new int[] {3, 4, 1, 1}); + countryGroupAssignment.put("CH", new int[] {0, 0, 0, 0}); countryGroupAssignment.put("CI", new int[] {3, 4, 3, 3}); - countryGroupAssignment.put("CK", new int[] {2, 4, 1, 0}); + countryGroupAssignment.put("CK", new int[] {2, 0, 1, 0}); countryGroupAssignment.put("CL", new int[] {1, 2, 2, 3}); - countryGroupAssignment.put("CM", new int[] {3, 4, 3, 1}); - countryGroupAssignment.put("CN", new int[] {2, 0, 2, 3}); - countryGroupAssignment.put("CO", new int[] {2, 3, 2, 2}); - countryGroupAssignment.put("CR", new int[] {2, 3, 4, 4}); - countryGroupAssignment.put("CU", new int[] {4, 4, 3, 1}); - countryGroupAssignment.put("CV", new int[] {2, 3, 1, 2}); + countryGroupAssignment.put("CM", new int[] {3, 4, 3, 2}); + countryGroupAssignment.put("CN", new int[] {1, 0, 1, 1}); + countryGroupAssignment.put("CO", new int[] {2, 3, 3, 2}); + countryGroupAssignment.put("CR", new int[] {2, 2, 4, 4}); + countryGroupAssignment.put("CU", new int[] {4, 4, 2, 1}); + countryGroupAssignment.put("CV", new int[] {2, 3, 3, 2}); countryGroupAssignment.put("CW", new int[] {1, 1, 0, 0}); countryGroupAssignment.put("CY", new int[] {1, 1, 0, 0}); countryGroupAssignment.put("CZ", new int[] {0, 1, 0, 0}); - countryGroupAssignment.put("DE", new int[] {0, 1, 1, 3}); - countryGroupAssignment.put("DJ", new int[] {4, 3, 4, 1}); - countryGroupAssignment.put("DK", new int[] {0, 0, 1, 1}); - countryGroupAssignment.put("DM", new int[] {1, 0, 1, 3}); + countryGroupAssignment.put("DE", new int[] {0, 1, 2, 3}); + countryGroupAssignment.put("DJ", new int[] {4, 2, 4, 4}); + countryGroupAssignment.put("DK", new int[] {0, 0, 1, 0}); + countryGroupAssignment.put("DM", new int[] {1, 1, 0, 2}); countryGroupAssignment.put("DO", new int[] {3, 3, 4, 4}); countryGroupAssignment.put("DZ", new int[] {3, 3, 4, 4}); - countryGroupAssignment.put("EC", new int[] {2, 3, 4, 3}); - countryGroupAssignment.put("EE", new int[] {0, 1, 0, 0}); - countryGroupAssignment.put("EG", new int[] {3, 4, 2, 2}); - countryGroupAssignment.put("EH", new int[] {2, 0, 3, 3}); - countryGroupAssignment.put("ER", new int[] {4, 2, 2, 0}); + countryGroupAssignment.put("EC", new int[] {2, 3, 4, 2}); + countryGroupAssignment.put("EE", new int[] {0, 0, 0, 0}); + countryGroupAssignment.put("EG", new int[] {3, 4, 2, 1}); + countryGroupAssignment.put("EH", new int[] {2, 0, 3, 1}); + countryGroupAssignment.put("ER", new int[] {4, 2, 4, 4}); countryGroupAssignment.put("ES", new int[] {0, 1, 1, 1}); - countryGroupAssignment.put("ET", new int[] {4, 4, 4, 0}); + countryGroupAssignment.put("ET", new int[] {4, 4, 4, 1}); countryGroupAssignment.put("FI", new int[] {0, 0, 1, 0}); - countryGroupAssignment.put("FJ", new int[] {3, 0, 3, 3}); - countryGroupAssignment.put("FK", new int[] {3, 4, 2, 2}); - countryGroupAssignment.put("FM", new int[] {4, 0, 4, 0}); - countryGroupAssignment.put("FO", new int[] {0, 0, 0, 0}); - countryGroupAssignment.put("FR", new int[] {1, 0, 3, 1}); - countryGroupAssignment.put("GA", new int[] {3, 3, 2, 2}); - countryGroupAssignment.put("GB", new int[] {0, 1, 3, 3}); - countryGroupAssignment.put("GD", new int[] {2, 0, 4, 4}); - countryGroupAssignment.put("GE", new int[] {1, 1, 1, 4}); - countryGroupAssignment.put("GF", new int[] {2, 3, 4, 4}); - countryGroupAssignment.put("GG", new int[] {0, 1, 0, 0}); - countryGroupAssignment.put("GH", new int[] {3, 3, 2, 2}); - countryGroupAssignment.put("GI", new int[] {0, 0, 0, 1}); - countryGroupAssignment.put("GL", new int[] {2, 2, 0, 2}); - countryGroupAssignment.put("GM", new int[] {4, 4, 3, 4}); + countryGroupAssignment.put("FJ", new int[] {3, 0, 4, 4}); + countryGroupAssignment.put("FK", new int[] {2, 2, 2, 1}); + countryGroupAssignment.put("FM", new int[] {3, 2, 4, 1}); + countryGroupAssignment.put("FO", new int[] {1, 1, 0, 0}); + countryGroupAssignment.put("FR", new int[] {1, 1, 1, 1}); + countryGroupAssignment.put("GA", new int[] {3, 2, 2, 2}); + countryGroupAssignment.put("GB", new int[] {0, 1, 1, 1}); + countryGroupAssignment.put("GD", new int[] {1, 1, 3, 1}); + countryGroupAssignment.put("GE", new int[] {1, 0, 1, 4}); + countryGroupAssignment.put("GF", new int[] {2, 0, 1, 3}); + countryGroupAssignment.put("GG", new int[] {1, 0, 0, 0}); + countryGroupAssignment.put("GH", new int[] {3, 3, 3, 3}); + countryGroupAssignment.put("GI", new int[] {4, 4, 0, 0}); + countryGroupAssignment.put("GL", new int[] {2, 1, 1, 2}); + countryGroupAssignment.put("GM", new int[] {4, 3, 2, 4}); countryGroupAssignment.put("GN", new int[] {3, 4, 4, 2}); - countryGroupAssignment.put("GP", new int[] {2, 1, 1, 4}); - countryGroupAssignment.put("GQ", new int[] {4, 4, 3, 0}); - countryGroupAssignment.put("GR", new int[] {1, 1, 0, 2}); - countryGroupAssignment.put("GT", new int[] {3, 3, 3, 3}); - countryGroupAssignment.put("GU", new int[] {1, 2, 4, 4}); - countryGroupAssignment.put("GW", new int[] {4, 4, 4, 1}); + countryGroupAssignment.put("GP", new int[] {2, 1, 3, 4}); + countryGroupAssignment.put("GQ", new int[] {4, 4, 4, 0}); + countryGroupAssignment.put("GR", new int[] {1, 1, 0, 1}); + countryGroupAssignment.put("GT", new int[] {3, 2, 2, 2}); + countryGroupAssignment.put("GU", new int[] {1, 0, 2, 2}); + countryGroupAssignment.put("GW", new int[] {3, 4, 4, 3}); countryGroupAssignment.put("GY", new int[] {3, 2, 1, 1}); countryGroupAssignment.put("HK", new int[] {0, 2, 3, 4}); - countryGroupAssignment.put("HN", new int[] {3, 2, 3, 2}); + countryGroupAssignment.put("HN", new int[] {3, 1, 3, 3}); countryGroupAssignment.put("HR", new int[] {1, 1, 0, 1}); countryGroupAssignment.put("HT", new int[] {4, 4, 4, 4}); countryGroupAssignment.put("HU", new int[] {0, 1, 0, 0}); - countryGroupAssignment.put("ID", new int[] {3, 2, 3, 4}); + countryGroupAssignment.put("ID", new int[] {2, 2, 2, 3}); countryGroupAssignment.put("IE", new int[] {1, 0, 1, 1}); - countryGroupAssignment.put("IL", new int[] {0, 0, 2, 3}); + countryGroupAssignment.put("IL", new int[] {1, 0, 2, 3}); countryGroupAssignment.put("IM", new int[] {0, 0, 0, 1}); - countryGroupAssignment.put("IN", new int[] {2, 2, 4, 4}); - countryGroupAssignment.put("IO", new int[] {4, 2, 2, 2}); + countryGroupAssignment.put("IN", new int[] {2, 2, 4, 3}); + countryGroupAssignment.put("IO", new int[] {4, 4, 2, 3}); countryGroupAssignment.put("IQ", new int[] {3, 3, 4, 2}); - countryGroupAssignment.put("IR", new int[] {3, 0, 2, 2}); + countryGroupAssignment.put("IR", new int[] {3, 0, 2, 1}); countryGroupAssignment.put("IS", new int[] {0, 1, 0, 0}); - countryGroupAssignment.put("IT", new int[] {1, 0, 1, 2}); + countryGroupAssignment.put("IT", new int[] {1, 1, 1, 2}); countryGroupAssignment.put("JE", new int[] {1, 0, 0, 1}); - countryGroupAssignment.put("JM", new int[] {2, 3, 3, 1}); - countryGroupAssignment.put("JO", new int[] {1, 2, 1, 2}); - countryGroupAssignment.put("JP", new int[] {0, 2, 1, 1}); - countryGroupAssignment.put("KE", new int[] {3, 4, 4, 3}); - countryGroupAssignment.put("KG", new int[] {1, 1, 2, 2}); - countryGroupAssignment.put("KH", new int[] {1, 0, 4, 4}); - countryGroupAssignment.put("KI", new int[] {4, 4, 4, 4}); - countryGroupAssignment.put("KM", new int[] {4, 3, 2, 3}); - countryGroupAssignment.put("KN", new int[] {1, 0, 1, 3}); - countryGroupAssignment.put("KP", new int[] {4, 2, 4, 2}); - countryGroupAssignment.put("KR", new int[] {0, 1, 1, 1}); - countryGroupAssignment.put("KW", new int[] {2, 3, 1, 1}); - countryGroupAssignment.put("KY", new int[] {1, 1, 0, 1}); - countryGroupAssignment.put("KZ", new int[] {1, 2, 2, 3}); + countryGroupAssignment.put("JM", new int[] {3, 3, 3, 4}); + countryGroupAssignment.put("JO", new int[] {1, 2, 1, 1}); + countryGroupAssignment.put("JP", new int[] {0, 2, 0, 0}); + countryGroupAssignment.put("KE", new int[] {3, 4, 3, 3}); + countryGroupAssignment.put("KG", new int[] {2, 0, 2, 2}); + countryGroupAssignment.put("KH", new int[] {1, 0, 4, 3}); + countryGroupAssignment.put("KI", new int[] {4, 4, 4, 0}); + countryGroupAssignment.put("KM", new int[] {4, 3, 2, 4}); + countryGroupAssignment.put("KN", new int[] {1, 0, 2, 4}); + countryGroupAssignment.put("KP", new int[] {4, 2, 0, 2}); + countryGroupAssignment.put("KR", new int[] {0, 1, 0, 1}); + countryGroupAssignment.put("KW", new int[] {2, 3, 1, 2}); + countryGroupAssignment.put("KY", new int[] {3, 1, 2, 3}); + countryGroupAssignment.put("KZ", new int[] {1, 2, 2, 2}); countryGroupAssignment.put("LA", new int[] {2, 2, 1, 1}); countryGroupAssignment.put("LB", new int[] {3, 2, 0, 0}); countryGroupAssignment.put("LC", new int[] {1, 1, 0, 0}); - countryGroupAssignment.put("LI", new int[] {0, 0, 2, 4}); - countryGroupAssignment.put("LK", new int[] {2, 1, 2, 3}); - countryGroupAssignment.put("LR", new int[] {3, 4, 3, 1}); - countryGroupAssignment.put("LS", new int[] {3, 3, 2, 0}); + countryGroupAssignment.put("LI", new int[] {0, 0, 1, 1}); + countryGroupAssignment.put("LK", new int[] {2, 0, 2, 3}); + countryGroupAssignment.put("LR", new int[] {3, 4, 4, 2}); + countryGroupAssignment.put("LS", new int[] {3, 3, 2, 2}); countryGroupAssignment.put("LT", new int[] {0, 0, 0, 0}); countryGroupAssignment.put("LU", new int[] {0, 0, 0, 0}); countryGroupAssignment.put("LV", new int[] {0, 0, 0, 0}); - countryGroupAssignment.put("LY", new int[] {4, 4, 4, 4}); - countryGroupAssignment.put("MA", new int[] {2, 1, 2, 1}); - countryGroupAssignment.put("MC", new int[] {0, 0, 0, 1}); + countryGroupAssignment.put("LY", new int[] {3, 3, 4, 3}); + countryGroupAssignment.put("MA", new int[] {3, 2, 3, 2}); + countryGroupAssignment.put("MC", new int[] {0, 4, 0, 0}); countryGroupAssignment.put("MD", new int[] {1, 1, 0, 0}); - countryGroupAssignment.put("ME", new int[] {1, 2, 1, 2}); - countryGroupAssignment.put("MF", new int[] {1, 1, 1, 1}); - countryGroupAssignment.put("MG", new int[] {3, 4, 2, 2}); + countryGroupAssignment.put("ME", new int[] {1, 3, 1, 2}); + countryGroupAssignment.put("MF", new int[] {2, 3, 1, 1}); + countryGroupAssignment.put("MG", new int[] {3, 4, 2, 3}); countryGroupAssignment.put("MH", new int[] {4, 0, 2, 4}); countryGroupAssignment.put("MK", new int[] {1, 0, 0, 0}); countryGroupAssignment.put("ML", new int[] {4, 4, 2, 0}); - countryGroupAssignment.put("MM", new int[] {3, 3, 1, 2}); - countryGroupAssignment.put("MN", new int[] {2, 3, 2, 3}); + countryGroupAssignment.put("MM", new int[] {3, 3, 2, 2}); + countryGroupAssignment.put("MN", new int[] {2, 3, 1, 1}); countryGroupAssignment.put("MO", new int[] {0, 0, 4, 4}); - countryGroupAssignment.put("MP", new int[] {0, 2, 4, 4}); - countryGroupAssignment.put("MQ", new int[] {2, 1, 1, 4}); - countryGroupAssignment.put("MR", new int[] {4, 2, 4, 2}); - countryGroupAssignment.put("MS", new int[] {1, 2, 3, 3}); - countryGroupAssignment.put("MT", new int[] {0, 1, 0, 0}); - countryGroupAssignment.put("MU", new int[] {2, 2, 3, 4}); - countryGroupAssignment.put("MV", new int[] {4, 3, 0, 2}); - countryGroupAssignment.put("MW", new int[] {3, 2, 1, 0}); - countryGroupAssignment.put("MX", new int[] {2, 4, 4, 3}); - countryGroupAssignment.put("MY", new int[] {2, 2, 3, 3}); - countryGroupAssignment.put("MZ", new int[] {3, 3, 2, 1}); - countryGroupAssignment.put("NA", new int[] {3, 3, 2, 1}); - countryGroupAssignment.put("NC", new int[] {2, 0, 3, 3}); - countryGroupAssignment.put("NE", new int[] {4, 4, 4, 3}); - countryGroupAssignment.put("NF", new int[] {1, 2, 2, 2}); - countryGroupAssignment.put("NG", new int[] {3, 4, 3, 1}); - countryGroupAssignment.put("NI", new int[] {3, 3, 4, 4}); - countryGroupAssignment.put("NL", new int[] {0, 2, 3, 3}); - countryGroupAssignment.put("NO", new int[] {0, 1, 1, 0}); + countryGroupAssignment.put("MP", new int[] {0, 2, 1, 2}); + countryGroupAssignment.put("MQ", new int[] {2, 1, 1, 3}); + countryGroupAssignment.put("MR", new int[] {4, 2, 4, 4}); + countryGroupAssignment.put("MS", new int[] {1, 4, 3, 4}); + countryGroupAssignment.put("MT", new int[] {0, 0, 0, 0}); + countryGroupAssignment.put("MU", new int[] {2, 2, 4, 4}); + countryGroupAssignment.put("MV", new int[] {4, 3, 2, 4}); + countryGroupAssignment.put("MW", new int[] {3, 1, 1, 1}); + countryGroupAssignment.put("MX", new int[] {2, 4, 3, 3}); + countryGroupAssignment.put("MY", new int[] {2, 1, 3, 3}); + countryGroupAssignment.put("MZ", new int[] {3, 3, 3, 3}); + countryGroupAssignment.put("NA", new int[] {4, 3, 3, 3}); + countryGroupAssignment.put("NC", new int[] {2, 0, 4, 4}); + countryGroupAssignment.put("NE", new int[] {4, 4, 4, 4}); + countryGroupAssignment.put("NF", new int[] {1, 2, 2, 0}); + countryGroupAssignment.put("NG", new int[] {3, 3, 2, 2}); + countryGroupAssignment.put("NI", new int[] {3, 2, 4, 3}); + countryGroupAssignment.put("NL", new int[] {0, 2, 3, 2}); + countryGroupAssignment.put("NO", new int[] {0, 2, 1, 0}); countryGroupAssignment.put("NP", new int[] {2, 2, 2, 2}); - countryGroupAssignment.put("NR", new int[] {4, 0, 3, 1}); + countryGroupAssignment.put("NR", new int[] {4, 0, 3, 2}); countryGroupAssignment.put("NZ", new int[] {0, 0, 1, 2}); - countryGroupAssignment.put("OM", new int[] {3, 2, 1, 3}); - countryGroupAssignment.put("PA", new int[] {1, 3, 3, 4}); - countryGroupAssignment.put("PE", new int[] {2, 3, 4, 4}); - countryGroupAssignment.put("PF", new int[] {2, 2, 0, 1}); - countryGroupAssignment.put("PG", new int[] {4, 3, 3, 1}); + countryGroupAssignment.put("OM", new int[] {2, 3, 0, 2}); + countryGroupAssignment.put("PA", new int[] {1, 3, 3, 3}); + countryGroupAssignment.put("PE", new int[] {2, 4, 4, 4}); + countryGroupAssignment.put("PF", new int[] {2, 1, 1, 1}); + countryGroupAssignment.put("PG", new int[] {4, 3, 3, 2}); countryGroupAssignment.put("PH", new int[] {3, 0, 3, 4}); - countryGroupAssignment.put("PK", new int[] {3, 3, 3, 3}); - countryGroupAssignment.put("PL", new int[] {1, 0, 1, 3}); + countryGroupAssignment.put("PK", new int[] {3, 2, 3, 2}); + countryGroupAssignment.put("PL", new int[] {1, 0, 1, 2}); countryGroupAssignment.put("PM", new int[] {0, 2, 2, 0}); - countryGroupAssignment.put("PR", new int[] {1, 2, 3, 3}); - countryGroupAssignment.put("PS", new int[] {3, 3, 2, 4}); + countryGroupAssignment.put("PR", new int[] {2, 2, 2, 2}); + countryGroupAssignment.put("PS", new int[] {3, 3, 1, 4}); countryGroupAssignment.put("PT", new int[] {1, 1, 0, 0}); - countryGroupAssignment.put("PW", new int[] {2, 1, 2, 0}); - countryGroupAssignment.put("PY", new int[] {2, 0, 2, 3}); - countryGroupAssignment.put("QA", new int[] {2, 2, 1, 2}); + countryGroupAssignment.put("PW", new int[] {1, 1, 3, 0}); + countryGroupAssignment.put("PY", new int[] {2, 0, 3, 3}); + countryGroupAssignment.put("QA", new int[] {2, 3, 1, 1}); countryGroupAssignment.put("RE", new int[] {1, 0, 2, 2}); countryGroupAssignment.put("RO", new int[] {0, 1, 1, 2}); countryGroupAssignment.put("RS", new int[] {1, 2, 0, 0}); - countryGroupAssignment.put("RU", new int[] {0, 1, 1, 1}); - countryGroupAssignment.put("RW", new int[] {4, 4, 2, 4}); + countryGroupAssignment.put("RU", new int[] {0, 1, 0, 1}); + countryGroupAssignment.put("RW", new int[] {4, 4, 4, 4}); countryGroupAssignment.put("SA", new int[] {2, 2, 2, 1}); - countryGroupAssignment.put("SB", new int[] {4, 4, 3, 0}); + countryGroupAssignment.put("SB", new int[] {4, 4, 4, 1}); countryGroupAssignment.put("SC", new int[] {4, 2, 0, 1}); - countryGroupAssignment.put("SD", new int[] {4, 4, 4, 3}); + countryGroupAssignment.put("SD", new int[] {4, 4, 4, 4}); countryGroupAssignment.put("SE", new int[] {0, 1, 0, 0}); - countryGroupAssignment.put("SG", new int[] {0, 2, 3, 3}); - countryGroupAssignment.put("SH", new int[] {4, 4, 2, 3}); - countryGroupAssignment.put("SI", new int[] {0, 0, 0, 0}); - countryGroupAssignment.put("SJ", new int[] {2, 0, 2, 4}); + countryGroupAssignment.put("SG", new int[] {1, 0, 3, 3}); + countryGroupAssignment.put("SH", new int[] {4, 2, 2, 2}); + countryGroupAssignment.put("SI", new int[] {0, 1, 0, 0}); + countryGroupAssignment.put("SJ", new int[] {2, 2, 2, 4}); countryGroupAssignment.put("SK", new int[] {0, 1, 0, 0}); - countryGroupAssignment.put("SL", new int[] {4, 3, 3, 3}); - countryGroupAssignment.put("SM", new int[] {0, 0, 2, 4}); - countryGroupAssignment.put("SN", new int[] {3, 4, 4, 2}); - countryGroupAssignment.put("SO", new int[] {3, 4, 4, 3}); - countryGroupAssignment.put("SR", new int[] {2, 2, 1, 0}); - countryGroupAssignment.put("SS", new int[] {4, 3, 4, 3}); - countryGroupAssignment.put("ST", new int[] {3, 4, 2, 2}); - countryGroupAssignment.put("SV", new int[] {2, 3, 3, 4}); + countryGroupAssignment.put("SL", new int[] {4, 3, 3, 1}); + countryGroupAssignment.put("SM", new int[] {0, 0, 1, 2}); + countryGroupAssignment.put("SN", new int[] {4, 4, 4, 3}); + countryGroupAssignment.put("SO", new int[] {3, 4, 3, 4}); + countryGroupAssignment.put("SR", new int[] {2, 2, 2, 1}); + countryGroupAssignment.put("SS", new int[] {4, 4, 4, 4}); + countryGroupAssignment.put("ST", new int[] {2, 3, 1, 2}); + countryGroupAssignment.put("SV", new int[] {2, 2, 4, 4}); countryGroupAssignment.put("SX", new int[] {2, 4, 1, 0}); - countryGroupAssignment.put("SY", new int[] {4, 3, 2, 1}); + countryGroupAssignment.put("SY", new int[] {4, 3, 1, 1}); countryGroupAssignment.put("SZ", new int[] {4, 4, 3, 4}); - countryGroupAssignment.put("TC", new int[] {1, 2, 1, 1}); - countryGroupAssignment.put("TD", new int[] {4, 4, 4, 2}); - countryGroupAssignment.put("TG", new int[] {3, 3, 1, 0}); - countryGroupAssignment.put("TH", new int[] {1, 3, 4, 4}); + countryGroupAssignment.put("TC", new int[] {1, 2, 1, 0}); + countryGroupAssignment.put("TD", new int[] {4, 4, 4, 3}); + countryGroupAssignment.put("TG", new int[] {3, 2, 1, 0}); + countryGroupAssignment.put("TH", new int[] {1, 3, 3, 3}); countryGroupAssignment.put("TJ", new int[] {4, 4, 4, 4}); countryGroupAssignment.put("TL", new int[] {4, 2, 4, 4}); - countryGroupAssignment.put("TM", new int[] {4, 1, 2, 2}); - countryGroupAssignment.put("TN", new int[] {2, 2, 1, 2}); - countryGroupAssignment.put("TO", new int[] {3, 3, 3, 1}); - countryGroupAssignment.put("TR", new int[] {2, 2, 1, 2}); - countryGroupAssignment.put("TT", new int[] {1, 3, 1, 2}); - countryGroupAssignment.put("TV", new int[] {4, 2, 2, 4}); + countryGroupAssignment.put("TM", new int[] {4, 2, 2, 2}); + countryGroupAssignment.put("TN", new int[] {2, 1, 1, 1}); + countryGroupAssignment.put("TO", new int[] {4, 3, 4, 4}); + countryGroupAssignment.put("TR", new int[] {1, 2, 1, 1}); + countryGroupAssignment.put("TT", new int[] {1, 3, 2, 4}); + countryGroupAssignment.put("TV", new int[] {4, 2, 3, 4}); countryGroupAssignment.put("TW", new int[] {0, 0, 0, 0}); - countryGroupAssignment.put("TZ", new int[] {3, 3, 4, 3}); - countryGroupAssignment.put("UA", new int[] {0, 2, 1, 2}); - countryGroupAssignment.put("UG", new int[] {4, 3, 3, 2}); - countryGroupAssignment.put("US", new int[] {1, 1, 3, 3}); - countryGroupAssignment.put("UY", new int[] {2, 2, 1, 1}); - countryGroupAssignment.put("UZ", new int[] {2, 2, 2, 2}); - countryGroupAssignment.put("VA", new int[] {1, 2, 4, 2}); - countryGroupAssignment.put("VC", new int[] {2, 0, 2, 4}); - countryGroupAssignment.put("VE", new int[] {4, 4, 4, 3}); - countryGroupAssignment.put("VG", new int[] {3, 0, 1, 3}); - countryGroupAssignment.put("VI", new int[] {1, 1, 4, 4}); - countryGroupAssignment.put("VN", new int[] {0, 2, 4, 4}); - countryGroupAssignment.put("VU", new int[] {4, 1, 3, 1}); - countryGroupAssignment.put("WS", new int[] {3, 3, 3, 2}); + countryGroupAssignment.put("TZ", new int[] {3, 4, 3, 3}); + countryGroupAssignment.put("UA", new int[] {0, 3, 1, 1}); + countryGroupAssignment.put("UG", new int[] {3, 2, 2, 3}); + countryGroupAssignment.put("US", new int[] {0, 1, 2, 2}); + countryGroupAssignment.put("UY", new int[] {2, 1, 2, 2}); + countryGroupAssignment.put("UZ", new int[] {2, 2, 3, 2}); + countryGroupAssignment.put("VA", new int[] {0, 2, 2, 2}); + countryGroupAssignment.put("VC", new int[] {2, 3, 0, 2}); + countryGroupAssignment.put("VE", new int[] {4, 4, 4, 4}); + countryGroupAssignment.put("VG", new int[] {3, 1, 2, 4}); + countryGroupAssignment.put("VI", new int[] {1, 4, 4, 3}); + countryGroupAssignment.put("VN", new int[] {0, 1, 3, 4}); + countryGroupAssignment.put("VU", new int[] {4, 0, 3, 3}); + countryGroupAssignment.put("WS", new int[] {3, 2, 4, 3}); countryGroupAssignment.put("XK", new int[] {1, 2, 1, 0}); countryGroupAssignment.put("YE", new int[] {4, 4, 4, 3}); countryGroupAssignment.put("YT", new int[] {2, 2, 2, 3}); - countryGroupAssignment.put("ZA", new int[] {2, 4, 2, 2}); - countryGroupAssignment.put("ZM", new int[] {3, 2, 2, 1}); - countryGroupAssignment.put("ZW", new int[] {3, 3, 2, 1}); + countryGroupAssignment.put("ZA", new int[] {2, 3, 2, 2}); + countryGroupAssignment.put("ZM", new int[] {3, 2, 3, 3}); + countryGroupAssignment.put("ZW", new int[] {3, 3, 2, 3}); return Collections.unmodifiableMap(countryGroupAssignment); } } 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 ae115ab58c..17f8427dd1 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 @@ -279,8 +279,8 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou try { connection = makeConnection(dataSpec); } catch (IOException e) { - throw new HttpDataSourceException("Unable to connect to " + dataSpec.uri.toString(), e, - dataSpec, HttpDataSourceException.TYPE_OPEN); + throw new HttpDataSourceException( + "Unable to connect", e, dataSpec, HttpDataSourceException.TYPE_OPEN); } String responseMessage; @@ -289,8 +289,8 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou responseMessage = connection.getResponseMessage(); } catch (IOException e) { closeConnectionQuietly(); - throw new HttpDataSourceException("Unable to connect to " + dataSpec.uri.toString(), e, - dataSpec, HttpDataSourceException.TYPE_OPEN); + throw new HttpDataSourceException( + "Unable to connect", e, dataSpec, HttpDataSourceException.TYPE_OPEN); } // Check for a valid response code. @@ -386,7 +386,8 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou * * @return The current open connection, or null. */ - protected final @Nullable HttpURLConnection getConnection() { + @Nullable + protected final HttpURLConnection getConnection() { return connection; } @@ -428,7 +429,7 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou private HttpURLConnection makeConnection(DataSpec dataSpec) throws IOException { URL url = new URL(dataSpec.uri.toString()); @HttpMethod int httpMethod = dataSpec.httpMethod; - byte[] httpBody = dataSpec.httpBody; + @Nullable byte[] httpBody = dataSpec.httpBody; long position = dataSpec.position; long length = dataSpec.length; boolean allowGzip = dataSpec.isFlagSet(DataSpec.FLAG_ALLOW_GZIP); @@ -495,7 +496,7 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou * * @param url The url to connect to. * @param httpMethod The http method. - * @param httpBody The body data. + * @param httpBody The body data, or {@code null} if not required. * @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. @@ -505,7 +506,7 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou private HttpURLConnection makeConnection( URL url, @HttpMethod int httpMethod, - byte[] httpBody, + @Nullable byte[] httpBody, long position, long length, boolean allowGzip, @@ -562,11 +563,11 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou * Handles a redirect. * * @param originalUrl The original URL. - * @param location The Location header in the response. + * @param location The Location header in the response. May be {@code null}. * @return The next URL. * @throws IOException If redirection isn't possible. */ - private static URL handleRedirect(URL originalUrl, String location) throws IOException { + private static URL handleRedirect(URL originalUrl, @Nullable String location) throws IOException { if (location == null) { throw new ProtocolException("Null location redirect"); } 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 index 307652f456..435f4bf578 100644 --- 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 @@ -71,6 +71,7 @@ public class DefaultLoadErrorHandlingPolicy implements LoadErrorHandlingPolicy { int responseCode = ((InvalidResponseCodeException) exception).responseCode; return responseCode == 404 // HTTP 404 Not Found. || responseCode == 410 // HTTP 410 Gone. + || responseCode == 416 // HTTP 416 Range Not Satisfiable. ? DEFAULT_TRACK_BLACKLIST_MS : C.TIME_UNSET; } 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 2661469efd..93c1ce9adf 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 @@ -86,7 +86,6 @@ public final class FileDataSource extends BaseDataSource { transferInitializing(dataSpec); this.file = openLocalFile(uri); - file.seek(dataSpec.position); bytesRemaining = dataSpec.length == C.LENGTH_UNSET ? file.length() - dataSpec.position : dataSpec.length; @@ -103,23 +102,6 @@ public final class FileDataSource extends BaseDataSource { return bytesRemaining; } - private static RandomAccessFile openLocalFile(Uri uri) throws FileDataSourceException { - try { - return new RandomAccessFile(Assertions.checkNotNull(uri.getPath()), "r"); - } catch (FileNotFoundException e) { - if (!TextUtils.isEmpty(uri.getQuery()) || !TextUtils.isEmpty(uri.getFragment())) { - throw new FileDataSourceException( - String.format( - "uri has query and/or fragment, which are not supported. Did you call Uri.parse()" - + " on a string containing '?' or '#'? Use Uri.fromFile(new File(path)) to" - + " avoid this. path=%s,query=%s,fragment=%s", - uri.getPath(), uri.getQuery(), uri.getFragment()), - e); - } - throw new FileDataSourceException(e); - } - } - @Override public int read(byte[] buffer, int offset, int readLength) throws FileDataSourceException { if (readLength == 0) { @@ -168,4 +150,20 @@ public final class FileDataSource extends BaseDataSource { } } + private static RandomAccessFile openLocalFile(Uri uri) throws FileDataSourceException { + try { + return new RandomAccessFile(Assertions.checkNotNull(uri.getPath()), "r"); + } catch (FileNotFoundException e) { + if (!TextUtils.isEmpty(uri.getQuery()) || !TextUtils.isEmpty(uri.getFragment())) { + throw new FileDataSourceException( + String.format( + "uri has query and/or fragment, which are not supported. Did you call Uri.parse()" + + " on a string containing '?' or '#'? Use Uri.fromFile(new File(path)) to" + + " avoid this. path=%s,query=%s,fragment=%s", + uri.getPath(), uri.getQuery(), uri.getFragment()), + e); + } + throw new FileDataSourceException(e); + } + } } 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 index 68ab2a7a47..61e3b8309a 100644 --- 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 @@ -16,6 +16,8 @@ package com.google.android.exoplayer2.upstream; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.source.LoadEventInfo; +import com.google.android.exoplayer2.source.MediaLoadData; import com.google.android.exoplayer2.upstream.Loader.Callback; import com.google.android.exoplayer2.upstream.Loader.Loadable; import java.io.IOException; @@ -41,22 +43,23 @@ public interface LoadErrorHandlingPolicy { /** Holds information about a load task error. */ final class LoadErrorInfo { - /** One of the {@link C C.DATA_TYPE_*} constants indicating the type of data to load. */ - public final int dataType; - /** - * The duration in milliseconds of the load from the start of the first load attempt up to the - * point at which the error occurred. - */ - public final long loadDurationMs; + /** The {@link LoadEventInfo} associated with the load that encountered an error. */ + public final LoadEventInfo loadEventInfo; + /** {@link MediaLoadData} associated with the load that encountered an error. */ + public final MediaLoadData mediaLoadData; /** The exception associated to the load error. */ public final IOException exception; /** The number of errors this load task has encountered, including this one. */ public final int errorCount; /** Creates an instance with the given values. */ - public LoadErrorInfo(int dataType, long loadDurationMs, IOException exception, int errorCount) { - this.dataType = dataType; - this.loadDurationMs = loadDurationMs; + public LoadErrorInfo( + LoadEventInfo loadEventInfo, + MediaLoadData mediaLoadData, + IOException exception, + int errorCount) { + this.loadEventInfo = loadEventInfo; + this.mediaLoadData = mediaLoadData; this.exception = exception; this.errorCount = errorCount; } @@ -88,8 +91,8 @@ public interface LoadErrorHandlingPolicy { */ default long getBlacklistDurationMsFor(LoadErrorInfo loadErrorInfo) { return getBlacklistDurationMsFor( - loadErrorInfo.dataType, - loadErrorInfo.loadDurationMs, + loadErrorInfo.mediaLoadData.dataType, + loadErrorInfo.loadEventInfo.loadDurationMs, loadErrorInfo.exception, loadErrorInfo.errorCount); } @@ -127,12 +130,20 @@ public interface LoadErrorHandlingPolicy { */ default long getRetryDelayMsFor(LoadErrorInfo loadErrorInfo) { return getRetryDelayMsFor( - loadErrorInfo.dataType, - loadErrorInfo.loadDurationMs, + loadErrorInfo.mediaLoadData.dataType, + loadErrorInfo.loadEventInfo.loadDurationMs, loadErrorInfo.exception, loadErrorInfo.errorCount); } + /** + * Called once {@code loadTaskId} will not be associated with any more load errors. + * + *

        Implementations should clean up any resources associated with {@code loadTaskId} when this + * method is called. + */ + default void onLoadTaskConcluded(long loadTaskId) {} + /** * Returns the minimum number of times to retry a load in the case of a load error, before * propagating the error. 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 a498f510dd..4ff58b108c 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 @@ -63,10 +63,8 @@ public final class Loader implements LoaderErrorThrower { * Performs the load, returning on completion or cancellation. * * @throws IOException If the input could not be loaded. - * @throws InterruptedException If the thread was interrupted. */ - void load() throws IOException, InterruptedException; - + void load() throws IOException; } /** @@ -363,7 +361,7 @@ public final class Loader implements LoaderErrorThrower { } else { canceled = true; loadable.cancelLoad(); - Thread executorThread = this.executorThread; + @Nullable Thread executorThread = this.executorThread; if (executorThread != null) { executorThread.interrupt(); } @@ -400,12 +398,6 @@ public final class Loader implements LoaderErrorThrower { if (!released) { obtainMessage(MSG_IO_EXCEPTION, e).sendToTarget(); } - } catch (InterruptedException e) { - // The load was canceled. - Assertions.checkState(canceled); - if (!released) { - sendEmptyMessage(MSG_END_OF_SOURCE); - } } catch (Exception e) { // This should never happen, but handle it anyway. Log.e(TAG, "Unexpected exception loading stream", e); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/LoaderErrorThrower.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/LoaderErrorThrower.java index 4f9e9fa5e6..54c3d4cbe5 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/LoaderErrorThrower.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/LoaderErrorThrower.java @@ -49,15 +49,14 @@ public interface LoaderErrorThrower { final class Dummy implements LoaderErrorThrower { @Override - public void maybeThrowError() throws IOException { + public void maybeThrowError() { // Do nothing. } @Override - public void maybeThrowError(int minRetryCount) throws IOException { + public void maybeThrowError(int minRetryCount) { // Do nothing. } - } } 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 edec849b88..c9701ed9c9 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 @@ -19,6 +19,7 @@ import android.net.Uri; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ParserException; +import com.google.android.exoplayer2.source.LoadEventInfo; import com.google.android.exoplayer2.upstream.Loader.Loadable; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Util; @@ -87,9 +88,9 @@ public final class ParsingLoadable implements Loadable { return Assertions.checkNotNull(loadable.getResult()); } - /** - * The {@link DataSpec} that defines the data to be loaded. - */ + /** Identifies the load task for this loadable. */ + public final long loadTaskId; + /** The {@link DataSpec} that defines the data to be loaded. */ public final DataSpec dataSpec; /** * The type of the data. One of the {@code DATA_TYPE_*} constants defined in {@link C}. For @@ -109,7 +110,11 @@ public final class ParsingLoadable implements Loadable { * @param parser Parses the object from the response. */ public ParsingLoadable(DataSource dataSource, Uri uri, int type, Parser parser) { - this(dataSource, new DataSpec(uri, DataSpec.FLAG_ALLOW_GZIP), type, parser); + this( + dataSource, + new DataSpec.Builder().setUri(uri).setFlags(DataSpec.FLAG_ALLOW_GZIP).build(), + type, + parser); } /** @@ -124,10 +129,12 @@ public final class ParsingLoadable implements Loadable { this.dataSpec = dataSpec; this.type = type; this.parser = parser; + loadTaskId = LoadEventInfo.getNewId(); } /** Returns the loaded object, or null if an object has not been loaded. */ - public final @Nullable T getResult() { + @Nullable + public final T getResult() { return result; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/ResolvingDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/ResolvingDataSource.java index 412f866e99..f5fb67e40e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/ResolvingDataSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/ResolvingDataSource.java @@ -110,10 +110,10 @@ public final class ResolvingDataSource implements DataSource { return upstreamDataSource.read(buffer, offset, readLength); } - @Nullable @Override + @Nullable public Uri getUri() { - Uri reportedUri = upstreamDataSource.getUri(); + @Nullable Uri reportedUri = upstreamDataSource.getUri(); return reportedUri == null ? null : resolver.resolveReportedUri(reportedUri); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSink.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSink.java index 22ed3892ec..454674f665 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSink.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSink.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.upstream.cache; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.upstream.DataSink; import com.google.android.exoplayer2.upstream.DataSpec; @@ -27,6 +28,7 @@ import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /** * Writes data into a cache. @@ -37,6 +39,78 @@ import java.io.OutputStream; */ public final class CacheDataSink implements DataSink { + /** {@link DataSink.Factory} for {@link CacheDataSink} instances. */ + public static final class Factory implements DataSink.Factory { + + private @MonotonicNonNull Cache cache; + private long fragmentSize; + private int bufferSize; + + /** Creates an instance. */ + public Factory() { + fragmentSize = CacheDataSink.DEFAULT_FRAGMENT_SIZE; + bufferSize = CacheDataSink.DEFAULT_BUFFER_SIZE; + } + + /** + * Sets the cache to which data will be written. + * + *

        Must be called before the factory is used. + * + * @param cache The cache to which data will be written. + * @return This factory. + */ + public Factory setCache(Cache cache) { + this.cache = cache; + return this; + } + + /** + * Sets the cache file fragment size. For requests that should be fragmented into multiple cache + * files, this is the maximum size of a cache file in bytes. If set to {@link C#LENGTH_UNSET} + * then no fragmentation will occur. Using a small value allows for finer-grained cache eviction + * policies, at the cost of increased overhead both on the cache implementation and the file + * system. Values under {@code (2 * 1024 * 1024)} are not recommended. + * + *

        The default value is {@link CacheDataSink#DEFAULT_FRAGMENT_SIZE}. + * + * @param fragmentSize The fragment size in bytes, or {@link C#LENGTH_UNSET} to disable + * fragmentation. + * @return This factory. + */ + public Factory setFragmentSize(long fragmentSize) { + this.fragmentSize = fragmentSize; + return this; + } + + /** + * Sets the size of an in-memory buffer used when writing to a cache file. A zero or negative + * value disables buffering. + * + *

        The default value is {@link CacheDataSink#DEFAULT_BUFFER_SIZE}. + * + * @param bufferSize The buffer size in bytes. + * @return This factory. + */ + public Factory setBufferSize(int bufferSize) { + this.bufferSize = bufferSize; + return this; + } + + @Override + public DataSink createDataSink() { + return new CacheDataSink(Assertions.checkNotNull(cache), fragmentSize, bufferSize); + } + } + + /** Thrown when an {@link IOException} is encountered when writing data to the sink. */ + public static final class CacheDataSinkException extends CacheException { + + public CacheDataSinkException(IOException cause) { + super(cause); + } + } + /** Default {@code fragmentSize} recommended for caching use cases. */ public static final long DEFAULT_FRAGMENT_SIZE = 5 * 1024 * 1024; /** Default buffer size in bytes. */ @@ -49,24 +123,13 @@ public final class CacheDataSink implements DataSink { private final long fragmentSize; private final int bufferSize; - private DataSpec dataSpec; + @Nullable private DataSpec dataSpec; private long dataSpecFragmentSize; - private File file; - private OutputStream outputStream; + @Nullable private File file; + @Nullable private OutputStream outputStream; private long outputStreamBytesWritten; private long dataSpecBytesWritten; - private ReusableBufferedOutputStream bufferedOutputStream; - - /** - * Thrown when IOException is encountered when writing data into sink. - */ - public static class CacheDataSinkException extends CacheException { - - public CacheDataSinkException(IOException cause) { - super(cause); - } - - } + private @MonotonicNonNull ReusableBufferedOutputStream bufferedOutputStream; /** * Constructs an instance using {@link #DEFAULT_BUFFER_SIZE}. @@ -167,9 +230,7 @@ public final class CacheDataSink implements DataSink { dataSpec.length == C.LENGTH_UNSET ? C.LENGTH_UNSET : Math.min(dataSpec.length - dataSpecBytesWritten, dataSpecFragmentSize); - file = - cache.startFile( - dataSpec.key, dataSpec.absoluteStreamPosition + dataSpecBytesWritten, length); + file = cache.startFile(dataSpec.key, dataSpec.position + dataSpecBytesWritten, length); FileOutputStream underlyingFileOutputStream = new FileOutputStream(file); if (bufferSize > 0) { if (bufferedOutputStream == null) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSinkFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSinkFactory.java index ce9735badd..effb5f213e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSinkFactory.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSinkFactory.java @@ -17,9 +17,8 @@ package com.google.android.exoplayer2.upstream.cache; import com.google.android.exoplayer2.upstream.DataSink; -/** - * A {@link DataSink.Factory} that produces {@link CacheDataSink}. - */ +/** @deprecated Use {@link CacheDataSink.Factory}. */ +@Deprecated public final class CacheDataSinkFactory implements DataSink.Factory { private final Cache cache; 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 541c3b2d9d..3f9010a609 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 @@ -23,12 +23,14 @@ 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.DummyDataSource; import com.google.android.exoplayer2.upstream.FileDataSource; +import com.google.android.exoplayer2.upstream.PriorityDataSource; 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 com.google.android.exoplayer2.util.PriorityTaskManager; import java.io.IOException; import java.io.InterruptedIOException; import java.lang.annotation.Documented; @@ -37,6 +39,7 @@ import java.lang.annotation.RetentionPolicy; import java.util.Collections; import java.util.List; import java.util.Map; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /** * A {@link DataSource} that reads and writes a {@link Cache}. Requests are fulfilled from the cache @@ -45,6 +48,274 @@ import java.util.Map; */ public final class CacheDataSource implements DataSource { + /** {@link DataSource.Factory} for {@link CacheDataSource} instances. */ + public static final class Factory implements DataSource.Factory { + + private @MonotonicNonNull Cache cache; + private DataSource.Factory cacheReadDataSourceFactory; + @Nullable private DataSink.Factory cacheWriteDataSinkFactory; + private CacheKeyFactory cacheKeyFactory; + private boolean cacheIsReadOnly; + @Nullable private DataSource.Factory upstreamDataSourceFactory; + @Nullable private PriorityTaskManager upstreamPriorityTaskManager; + private int upstreamPriority; + @CacheDataSource.Flags private int flags; + @Nullable private CacheDataSource.EventListener eventListener; + + public Factory() { + cacheReadDataSourceFactory = new FileDataSource.Factory(); + cacheKeyFactory = CacheUtil.DEFAULT_CACHE_KEY_FACTORY; + } + + /** + * Sets the cache that will be used. + * + *

        Must be called before the factory is used. + * + * @param cache The cache that will be used. + * @return This factory. + */ + public Factory setCache(Cache cache) { + this.cache = cache; + return this; + } + + /** + * Returns the cache that will be used, or {@code null} if {@link #setCache} has yet to be + * called. + */ + @Nullable + public Cache getCache() { + return cache; + } + + /** + * Sets the {@link DataSource.Factory} for {@link DataSource DataSources} for reading from the + * cache. + * + *

        The default is a {@link FileDataSource.Factory} in its default configuration. + * + * @param cacheReadDataSourceFactory The {@link DataSource.Factory} for reading from the cache. + * @return This factory. + */ + public Factory setCacheReadDataSourceFactory(DataSource.Factory cacheReadDataSourceFactory) { + this.cacheReadDataSourceFactory = cacheReadDataSourceFactory; + return this; + } + + /** + * Sets the {@link DataSink.Factory} for generating {@link DataSink DataSinks} for writing data + * to the cache. Passing {@code null} causes the cache to be read-only. + * + *

        The default is a {@link CacheDataSink.Factory} in its default configuration. + * + * @param cacheWriteDataSinkFactory The {@link DataSink.Factory} for generating {@link DataSink + * DataSinks} for writing data to the cache, or {@code null} to disable writing. + * @return This factory. + */ + public Factory setCacheWriteDataSinkFactory( + @Nullable DataSink.Factory cacheWriteDataSinkFactory) { + this.cacheWriteDataSinkFactory = cacheWriteDataSinkFactory; + this.cacheIsReadOnly = cacheWriteDataSinkFactory == null; + return this; + } + + /** + * Sets the {@link CacheKeyFactory}. + * + *

        The default is {@link CacheUtil#DEFAULT_CACHE_KEY_FACTORY}. + * + * @param cacheKeyFactory The {@link CacheKeyFactory}. + * @return This factory. + */ + public Factory setCacheKeyFactory(CacheKeyFactory cacheKeyFactory) { + this.cacheKeyFactory = cacheKeyFactory; + return this; + } + + /** Returns the {@link CacheKeyFactory} that will be used. */ + public CacheKeyFactory getCacheKeyFactory() { + return cacheKeyFactory; + } + + /** + * Sets the {@link DataSource.Factory} for upstream {@link DataSource DataSources}, which are + * used to read data in the case of a cache miss. + * + *

        The default is {@code null}, and so this method must be called before the factory is used + * in order for data to be read from upstream in the case of a cache miss. + * + * @param upstreamDataSourceFactory The upstream {@link DataSource} for reading data not in the + * cache, or {@code null} to cause failure in the case of a cache miss. + * @return This factory. + */ + public Factory setUpstreamDataSourceFactory( + @Nullable DataSource.Factory upstreamDataSourceFactory) { + this.upstreamDataSourceFactory = upstreamDataSourceFactory; + return this; + } + + /** + * Sets an optional {@link PriorityTaskManager} to use when requesting data from upstream. + * + *

        If set, reads from the upstream {@link DataSource} will only be allowed to proceed if + * there are no higher priority tasks registered to the {@link PriorityTaskManager}. If there + * exists a higher priority task then {@link PriorityTaskManager.PriorityTooLowException} will + * be thrown instead. + * + *

        Note that requests to {@link CacheDataSource} instances are intended to be used as parts + * of (possibly larger) tasks that are registered with the {@link PriorityTaskManager}, and + * hence {@link CacheDataSource} does not register a task by itself. This must be done + * by the surrounding code that uses the {@link CacheDataSource} instances. + * + *

        The default is {@code null}. + * + * @param upstreamPriorityTaskManager The upstream {@link PriorityTaskManager}. + * @return This factory. + */ + public Factory setUpstreamPriorityTaskManager( + @Nullable PriorityTaskManager upstreamPriorityTaskManager) { + this.upstreamPriorityTaskManager = upstreamPriorityTaskManager; + return this; + } + + /** + * Returns the {@link PriorityTaskManager} that will bs used when requesting data from upstream, + * or {@code null} if there is none. + */ + @Nullable + public PriorityTaskManager getUpstreamPriorityTaskManager() { + return upstreamPriorityTaskManager; + } + + /** + * Sets the priority to use when requesting data from upstream. The priority is only used if a + * {@link PriorityTaskManager} is set by calling {@link #setUpstreamPriorityTaskManager}. + * + *

        The default is {@link C#PRIORITY_PLAYBACK}. + * + * @param upstreamPriority The priority to use when requesting data from upstream. + * @return This factory. + */ + public Factory setUpstreamPriority(int upstreamPriority) { + this.upstreamPriority = upstreamPriority; + return this; + } + + /** + * Sets the {@link CacheDataSource.Flags}. + * + *

        The default is {@code 0}. + * + * @param flags The {@link CacheDataSource.Flags}. + * @return This factory. + */ + public Factory setFlags(@CacheDataSource.Flags int flags) { + this.flags = flags; + return this; + } + + /** + * Sets the {link EventListener} to which events are delivered. + * + *

        The default is {@code null}. + * + * @param eventListener The {@link EventListener}. + * @return This factory. + */ + public Factory setEventListener(@Nullable EventListener eventListener) { + this.eventListener = eventListener; + return this; + } + + @Override + public CacheDataSource createDataSource() { + return createDataSourceInternal( + upstreamDataSourceFactory != null ? upstreamDataSourceFactory.createDataSource() : null, + flags, + upstreamPriority); + } + + /** + * Returns an instance suitable for downloading content. The created instance is equivalent to + * one that would be created by {@link #createDataSource()}, except: + * + *

          + *
        • The {@link #FLAG_BLOCK_ON_CACHE} is always set. + *
        • The task priority is overridden to be {@link C#PRIORITY_DOWNLOAD}. + *
        + * + * @return An instance suitable for downloading content. + */ + public CacheDataSource createDataSourceForDownloading() { + return createDataSourceInternal( + upstreamDataSourceFactory != null ? upstreamDataSourceFactory.createDataSource() : null, + flags | FLAG_BLOCK_ON_CACHE, + C.PRIORITY_DOWNLOAD); + } + + /** + * Returns an instance suitable for reading cached content as part of removing a download. The + * created instance is equivalent to one that would be created by {@link #createDataSource()}, + * except: + * + *
          + *
        • The upstream is overridden to be {@code null}, since when removing content we don't + * want to request anything that's not already cached. + *
        • The {@link #FLAG_BLOCK_ON_CACHE} is always set. + *
        • The task priority is overridden to be {@link C#PRIORITY_DOWNLOAD}. + *
        + * + * @return An instance suitable for reading cached content as part of removing a download. + */ + public CacheDataSource createDataSourceForRemovingDownload() { + return createDataSourceInternal( + /* upstreamDataSource= */ null, flags | FLAG_BLOCK_ON_CACHE, C.PRIORITY_DOWNLOAD); + } + + private CacheDataSource createDataSourceInternal( + @Nullable DataSource upstreamDataSource, @Flags int flags, int upstreamPriority) { + Cache cache = Assertions.checkNotNull(this.cache); + @Nullable DataSink cacheWriteDataSink; + if (cacheIsReadOnly || upstreamDataSource == null) { + cacheWriteDataSink = null; + } else if (cacheWriteDataSinkFactory != null) { + cacheWriteDataSink = cacheWriteDataSinkFactory.createDataSink(); + } else { + cacheWriteDataSink = new CacheDataSink.Factory().setCache(cache).createDataSink(); + } + return new CacheDataSource( + cache, + upstreamDataSource, + cacheReadDataSourceFactory.createDataSource(), + cacheWriteDataSink, + cacheKeyFactory, + flags, + upstreamPriorityTaskManager, + upstreamPriority, + eventListener); + } + } + + /** Listener of {@link CacheDataSource} events. */ + public interface EventListener { + + /** + * Called when bytes have been read from the cache. + * + * @param cacheSizeBytes Current cache size in bytes. + * @param cachedBytesRead Total bytes read from the cache since this method was last called. + */ + void onCachedBytesRead(long cacheSizeBytes, long cachedBytesRead); + + /** + * Called when the current request ignores cache. + * + * @param reason Reason cache is bypassed. + */ + void onCacheIgnored(@CacheIgnoredReason int reason); + } + /** * Flags controlling the CacheDataSource's behavior. Possible flag values are {@link * #FLAG_BLOCK_ON_CACHE}, {@link #FLAG_IGNORE_CACHE_ON_ERROR} and {@link @@ -97,27 +368,6 @@ public final class CacheDataSource implements DataSource { /** Cache ignored due to a request with an unset length. */ public static final int CACHE_IGNORED_REASON_UNSET_LENGTH = 1; - /** - * Listener of {@link CacheDataSource} events. - */ - public interface EventListener { - - /** - * Called when bytes have been read from the cache. - * - * @param cacheSizeBytes Current cache size in bytes. - * @param cachedBytesRead Total bytes read from the cache since this method was last called. - */ - void onCachedBytesRead(long cacheSizeBytes, long cachedBytesRead); - - /** - * Called when the current request ignores cache. - * - * @param reason Reason cache is bypassed. - */ - void onCacheIgnored(@CacheIgnoredReason int reason); - } - /** Minimum number of bytes to read before checking cache for availability. */ private static final long MIN_READ_BEFORE_CHECKING_CACHE = 100 * 1024; @@ -126,20 +376,18 @@ public final class CacheDataSource implements DataSource { @Nullable private final DataSource cacheWriteDataSource; private final DataSource upstreamDataSource; private final CacheKeyFactory cacheKeyFactory; + @Nullable private final PriorityTaskManager upstreamPriorityTaskManager; + private final int upstreamPriority; @Nullable private final EventListener eventListener; private final boolean blockOnCache; private final boolean ignoreCacheOnError; private final boolean ignoreCacheForUnsetLengthRequests; + @Nullable private Uri actualUri; + @Nullable private DataSpec requestDataSpec; @Nullable private DataSource currentDataSource; private boolean currentDataSpecLengthUnset; - @Nullable private Uri uri; - @Nullable private Uri actualUri; - @HttpMethod private int httpMethod; - @Nullable private byte[] httpBody; - private int flags; - @Nullable private String key; private long readPosition; private long bytesRemaining; @Nullable private CacheSpan currentHoleSpan; @@ -153,10 +401,11 @@ public final class CacheDataSource implements DataSource { * reading and writing the cache. * * @param cache The cache. - * @param upstream A {@link DataSource} for reading data not in the cache. + * @param upstreamDataSource A {@link DataSource} for reading data not in the cache. If null, + * reading will fail if a cache miss occurs. */ - public CacheDataSource(Cache cache, DataSource upstream) { - this(cache, upstream, /* flags= */ 0); + public CacheDataSource(Cache cache, @Nullable DataSource upstreamDataSource) { + this(cache, upstreamDataSource, /* flags= */ 0); } /** @@ -164,14 +413,15 @@ public final class CacheDataSource implements DataSource { * reading and writing the cache. * * @param cache The cache. - * @param upstream A {@link DataSource} for reading data not in the cache. + * @param upstreamDataSource A {@link DataSource} for reading data not in the cache. If null, + * reading will fail if a cache miss occurs. * @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. */ - public CacheDataSource(Cache cache, DataSource upstream, @Flags int flags) { + public CacheDataSource(Cache cache, @Nullable DataSource upstreamDataSource, @Flags int flags) { this( cache, - upstream, + upstreamDataSource, new FileDataSource(), new CacheDataSink(cache, CacheDataSink.DEFAULT_FRAGMENT_SIZE), flags, @@ -184,7 +434,8 @@ public final class CacheDataSource implements DataSource { * before it is written to disk. * * @param cache The cache. - * @param upstream A {@link DataSource} for reading data not in the cache. + * @param upstreamDataSource A {@link DataSource} for reading data not in the cache. If null, + * reading will fail if a cache miss occurs. * @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. @@ -194,14 +445,14 @@ public final class CacheDataSource implements DataSource { */ public CacheDataSource( Cache cache, - DataSource upstream, + @Nullable DataSource upstreamDataSource, DataSource cacheReadDataSource, @Nullable DataSink cacheWriteDataSink, @Flags int flags, @Nullable EventListener eventListener) { this( cache, - upstream, + upstreamDataSource, cacheReadDataSource, cacheWriteDataSink, flags, @@ -215,7 +466,8 @@ public final class CacheDataSource implements DataSource { * before it is written to disk. * * @param cache The cache. - * @param upstream A {@link DataSource} for reading data not in the cache. + * @param upstreamDataSource A {@link DataSource} for reading data not in the cache. If null, + * reading will fail if a cache miss occurs. * @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. @@ -226,12 +478,34 @@ public final class CacheDataSource implements DataSource { */ public CacheDataSource( Cache cache, - DataSource upstream, + @Nullable DataSource upstreamDataSource, DataSource cacheReadDataSource, @Nullable DataSink cacheWriteDataSink, @Flags int flags, @Nullable EventListener eventListener, @Nullable CacheKeyFactory cacheKeyFactory) { + this( + cache, + upstreamDataSource, + cacheReadDataSource, + cacheWriteDataSink, + cacheKeyFactory, + flags, + /* upstreamPriorityTaskManager= */ null, + /* upstreamPriority= */ C.PRIORITY_PLAYBACK, + eventListener); + } + + private CacheDataSource( + Cache cache, + @Nullable DataSource upstreamDataSource, + DataSource cacheReadDataSource, + @Nullable DataSink cacheWriteDataSink, + @Nullable CacheKeyFactory cacheKeyFactory, + @Flags int flags, + @Nullable PriorityTaskManager upstreamPriorityTaskManager, + int upstreamPriority, + @Nullable EventListener eventListener) { this.cache = cache; this.cacheReadDataSource = cacheReadDataSource; this.cacheKeyFactory = @@ -240,15 +514,54 @@ public final class CacheDataSource implements DataSource { this.ignoreCacheOnError = (flags & FLAG_IGNORE_CACHE_ON_ERROR) != 0; this.ignoreCacheForUnsetLengthRequests = (flags & FLAG_IGNORE_CACHE_FOR_UNSET_LENGTH_REQUESTS) != 0; - this.upstreamDataSource = upstream; - if (cacheWriteDataSink != null) { - this.cacheWriteDataSource = new TeeDataSource(upstream, cacheWriteDataSink); + this.upstreamPriority = upstreamPriority; + this.upstreamPriorityTaskManager = upstreamPriorityTaskManager; + if (upstreamDataSource != null) { + if (upstreamPriorityTaskManager != null) { + upstreamDataSource = + new PriorityDataSource( + upstreamDataSource, upstreamPriorityTaskManager, upstreamPriority); + } + this.upstreamDataSource = upstreamDataSource; + this.cacheWriteDataSource = + cacheWriteDataSink != null + ? new TeeDataSource(upstreamDataSource, cacheWriteDataSink) + : null; } else { + this.upstreamDataSource = DummyDataSource.INSTANCE; this.cacheWriteDataSource = null; } this.eventListener = eventListener; } + /** Returns the {@link Cache} used by this instance. */ + public Cache getCache() { + return cache; + } + + /** Returns the {@link CacheKeyFactory} used by this instance. */ + public CacheKeyFactory getCacheKeyFactory() { + return cacheKeyFactory; + } + + /** + * Returns the {@link PriorityTaskManager} used when there's a cache miss and requests need to be + * made to the upstream {@link DataSource}, or {@code null} if there is none. + */ + @Nullable + public PriorityTaskManager getUpstreamPriorityTaskManager() { + return upstreamPriorityTaskManager; + } + + /** + * Returns the priority used when there's a cache miss and requests need to be made to the + * upstream {@link DataSource}. The priority is only used if the source has a {@link + * PriorityTaskManager}. + */ + public int getUpstreamPriority() { + return upstreamPriority; + } + @Override public void addTransferListener(TransferListener transferListener) { cacheReadDataSource.addTransferListener(transferListener); @@ -258,12 +571,9 @@ public final class CacheDataSource implements DataSource { @Override public long open(DataSpec dataSpec) throws IOException { try { - key = cacheKeyFactory.buildCacheKey(dataSpec); - uri = dataSpec.uri; - actualUri = getRedirectedUriOrDefault(cache, key, /* defaultUri= */ uri); - httpMethod = dataSpec.httpMethod; - httpBody = dataSpec.httpBody; - flags = dataSpec.flags; + String key = cacheKeyFactory.buildCacheKey(dataSpec); + requestDataSpec = dataSpec.buildUpon().setKey(key).build(); + actualUri = getRedirectedUriOrDefault(cache, key, /* defaultUri= */ requestDataSpec.uri); readPosition = dataSpec.position; int reason = shouldIgnoreCacheForRequest(dataSpec); @@ -349,10 +659,9 @@ public final class CacheDataSource implements DataSource { @Override public void close() throws IOException { - uri = null; + requestDataSpec = null; actualUri = null; - httpMethod = DataSpec.HTTP_METHOD_GET; - httpBody = null; + readPosition = 0; notifyBytesRead(); try { closeCurrentSource(); @@ -377,7 +686,8 @@ public final class CacheDataSource implements DataSource { * reading from {@link #upstreamDataSource}, which is the currently open source. */ private void openNextSource(boolean checkCache) throws IOException { - CacheSpan nextSpan; + @Nullable CacheSpan nextSpan; + String key = requestDataSpec.key; if (currentRequestIgnoresCache) { nextSpan = null; } else if (blockOnCache) { @@ -398,17 +708,24 @@ public final class CacheDataSource implements DataSource { // from upstream. nextDataSource = upstreamDataSource; nextDataSpec = - new DataSpec( - uri, httpMethod, httpBody, readPosition, readPosition, bytesRemaining, key, flags); + requestDataSpec.buildUpon().setPosition(readPosition).setLength(bytesRemaining).build(); } else if (nextSpan.isCached) { - // Data is cached, read from cache. + // Data is cached in a span file starting at nextSpan.position. Uri fileUri = Uri.fromFile(nextSpan.file); - long filePosition = readPosition - nextSpan.position; - long length = nextSpan.length - filePosition; + long filePositionOffset = nextSpan.position; + long positionInFile = readPosition - filePositionOffset; + long length = nextSpan.length - positionInFile; if (bytesRemaining != C.LENGTH_UNSET) { length = Math.min(length, bytesRemaining); } - nextDataSpec = new DataSpec(fileUri, readPosition, filePosition, length, key, flags); + nextDataSpec = + requestDataSpec + .buildUpon() + .setUri(fileUri) + .setUriPositionOffset(filePositionOffset) + .setPosition(positionInFile) + .setLength(length) + .build(); nextDataSource = cacheReadDataSource; } else { // Data is not cached, and data is not locked, read from upstream with cache backing. @@ -422,7 +739,7 @@ public final class CacheDataSource implements DataSource { } } nextDataSpec = - new DataSpec(uri, httpMethod, httpBody, readPosition, readPosition, length, key, flags); + requestDataSpec.buildUpon().setPosition(readPosition).setLength(length).build(); if (cacheWriteDataSource != null) { nextDataSource = cacheWriteDataSource; } else { @@ -469,7 +786,7 @@ public final class CacheDataSource implements DataSource { } if (isReadingFromUpstream()) { actualUri = currentDataSource.getUri(); - boolean isRedirected = !uri.equals(actualUri); + boolean isRedirected = !requestDataSpec.uri.equals(actualUri); ContentMetadataMutations.setRedirectedUri(mutations, isRedirected ? actualUri : null); } if (isWritingToCache()) { @@ -482,12 +799,12 @@ public final class CacheDataSource implements DataSource { if (isWritingToCache()) { ContentMetadataMutations mutations = new ContentMetadataMutations(); ContentMetadataMutations.setContentLength(mutations, readPosition); - cache.applyContentMetadataMutations(key, mutations); + cache.applyContentMetadataMutations(requestDataSpec.key, mutations); } } private static Uri getRedirectedUriOrDefault(Cache cache, String key, Uri defaultUri) { - Uri redirectedUri = ContentMetadata.getRedirectedUri(cache.getContentMetadata(key)); + @Nullable Uri redirectedUri = ContentMetadata.getRedirectedUri(cache.getContentMetadata(key)); return redirectedUri != null ? redirectedUri : defaultUri; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceFactory.java index 21758bdceb..a9348b7d3a 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceFactory.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceFactory.java @@ -20,7 +20,8 @@ import com.google.android.exoplayer2.upstream.DataSink; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.FileDataSource; -/** A {@link DataSource.Factory} that produces {@link CacheDataSource}. */ +/** @deprecated Use {@link CacheDataSource.Factory}. */ +@Deprecated public final class CacheDataSourceFactory implements DataSource.Factory { private final Cache cache; @@ -44,13 +45,14 @@ public final class CacheDataSourceFactory implements DataSource.Factory { } /** @see CacheDataSource#CacheDataSource(Cache, DataSource, int) */ + @SuppressWarnings("deprecation") public CacheDataSourceFactory( Cache cache, DataSource.Factory upstreamFactory, @CacheDataSource.Flags int flags) { this( cache, upstreamFactory, new FileDataSource.Factory(), - new CacheDataSinkFactory(cache, CacheDataSink.DEFAULT_FRAGMENT_SIZE), + new CacheDataSink.Factory().setCache(cache), flags, /* eventListener= */ null); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheFileMetadataIndex.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheFileMetadataIndex.java index dc27dec363..e288a5258e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheFileMetadataIndex.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheFileMetadataIndex.java @@ -43,7 +43,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; private static final int COLUMN_INDEX_LENGTH = 1; private static final int COLUMN_INDEX_LAST_TOUCH_TIMESTAMP = 2; - private static final String WHERE_NAME_EQUALS = COLUMN_INDEX_NAME + " = ?"; + private static final String WHERE_NAME_EQUALS = COLUMN_NAME + " = ?"; private static final String[] COLUMNS = new String[] { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheKeyFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheKeyFactory.java index bfa404c074..3401d6f575 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheKeyFactory.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheKeyFactory.java @@ -24,6 +24,7 @@ public interface CacheKeyFactory { * Returns a cache key for the given {@link DataSpec}. * * @param dataSpec The data being cached. + * @return The cache key. */ String buildCacheKey(DataSpec dataSpec); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheSpan.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheSpan.java index 609e933c9d..bf51a69240 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheSpan.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheSpan.java @@ -15,7 +15,6 @@ */ package com.google.android.exoplayer2.upstream.cache; -import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import java.io.File; @@ -95,7 +94,7 @@ public class CacheSpan implements Comparable { } @Override - public int compareTo(@NonNull CacheSpan another) { + public int compareTo(CacheSpan another) { if (!key.equals(another.key)) { return key.compareTo(another.key); } 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 ce16ea2439..f19b818e1a 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 @@ -28,6 +28,7 @@ import com.google.android.exoplayer2.util.PriorityTaskManager; import com.google.android.exoplayer2.util.Util; import java.io.EOFException; import java.io.IOException; +import java.io.InterruptedIOException; import java.util.NavigableSet; import java.util.concurrent.atomic.AtomicBoolean; @@ -79,7 +80,7 @@ public final class CacheUtil { public static Pair getCached( DataSpec dataSpec, Cache cache, @Nullable CacheKeyFactory cacheKeyFactory) { String key = buildCacheKey(dataSpec, cacheKeyFactory); - long position = dataSpec.absoluteStreamPosition; + long position = dataSpec.position; long requestLength = getRequestLength(dataSpec, cache, key); long bytesAlreadyCached = 0; long bytesLeft = requestLength; @@ -105,85 +106,74 @@ public final class CacheUtil { * Caches the data defined by {@code dataSpec}, skipping already cached data. Caching stops early * if the end of the input is reached. * + *

        To cancel the operation, the caller should both set {@code isCanceled} to true and interrupt + * the calling thread. + * *

        This method may be slow and shouldn't normally be called on the main thread. * - * @param dataSpec Defines the data to be cached. * @param cache A {@link Cache} to store the data. - * @param cacheKeyFactory An optional factory for cache keys. - * @param upstream A {@link DataSource} for reading data not in the cache. + * @param dataSpec Defines the data to be cached. + * @param upstreamDataSource A {@link DataSource} for reading data not in the cache. * @param progressListener A listener to receive progress updates, or {@code null}. - * @param isCanceled An optional flag that will interrupt caching if set to true. - * @throws IOException If an error occurs reading from the source. - * @throws InterruptedException If the thread was interrupted directly or via {@code isCanceled}. + * @param isCanceled An optional flag that will cancel the operation if set to true. + * @throws IOException If an error occurs caching the data, or if the operation was canceled. */ @WorkerThread public static void cache( - DataSpec dataSpec, Cache cache, - @Nullable CacheKeyFactory cacheKeyFactory, - DataSource upstream, + DataSpec dataSpec, + DataSource upstreamDataSource, @Nullable ProgressListener progressListener, @Nullable AtomicBoolean isCanceled) - throws IOException, InterruptedException { + throws IOException { cache( + new CacheDataSource(cache, upstreamDataSource), dataSpec, - cache, - cacheKeyFactory, - new CacheDataSource(cache, upstream), - new byte[DEFAULT_BUFFER_SIZE_BYTES], - /* priorityTaskManager= */ null, - /* priority= */ 0, progressListener, isCanceled, - /* enableEOFException= */ false); + /* enableEOFException= */ false, + new byte[DEFAULT_BUFFER_SIZE_BYTES]); } /** - * Caches the data defined by {@code dataSpec} while skipping already cached data. Caching stops - * early if end of input is reached and {@code enableEOFException} is false. + * Caches the data defined by {@code dataSpec}, skipping already cached data. Caching stops early + * if end of input is reached and {@code enableEOFException} is false. * - *

        If a {@link PriorityTaskManager} is given, it's used to pause and resume caching depending - * on {@code priority} and the priority of other tasks registered to the PriorityTaskManager. - * Please note that it's the responsibility of the calling code to call {@link - * PriorityTaskManager#add} to register with the manager before calling this method, and to call - * {@link PriorityTaskManager#remove} afterwards to unregister. + *

        If {@code dataSource} has a {@link PriorityTaskManager}, then it's the responsibility of the + * calling code to call {@link PriorityTaskManager#add} to register with the manager before + * calling this method, and to call {@link PriorityTaskManager#remove} afterwards to unregister. + * + *

        To cancel the operation, the caller should both set {@code isCanceled} to true and interrupt + * the calling thread. * *

        This method may be slow and shouldn't normally be called on the main thread. * + * @param dataSource A {@link CacheDataSource} to be used for caching the data. * @param dataSpec Defines the data to be cached. - * @param cache A {@link Cache} to store the data. - * @param cacheKeyFactory An optional factory for cache keys. - * @param dataSource A {@link CacheDataSource} that works on the {@code cache}. - * @param buffer The buffer to be used while caching. - * @param priorityTaskManager If not null it's used to check whether it is allowed to proceed with - * caching. - * @param priority The priority of this task. Used with {@code priorityTaskManager}. * @param progressListener A listener to receive progress updates, or {@code null}. - * @param isCanceled An optional flag that will interrupt caching if set to true. + * @param isCanceled An optional flag that will cancel the operation if set to true. * @param enableEOFException Whether to throw an {@link EOFException} if end of input has been * reached unexpectedly. - * @throws IOException If an error occurs reading from the source. - * @throws InterruptedException If the thread was interrupted directly or via {@code isCanceled}. + * @param temporaryBuffer A temporary buffer to be used during caching. + * @throws IOException If an error occurs caching the data, or if the operation was canceled. */ @WorkerThread public static void cache( - DataSpec dataSpec, - Cache cache, - @Nullable CacheKeyFactory cacheKeyFactory, CacheDataSource dataSource, - byte[] buffer, - @Nullable PriorityTaskManager priorityTaskManager, - int priority, + DataSpec dataSpec, @Nullable ProgressListener progressListener, @Nullable AtomicBoolean isCanceled, - boolean enableEOFException) - throws IOException, InterruptedException { + boolean enableEOFException, + byte[] temporaryBuffer) + throws IOException { Assertions.checkNotNull(dataSource); - Assertions.checkNotNull(buffer); + Assertions.checkNotNull(temporaryBuffer); + Cache cache = dataSource.getCache(); + CacheKeyFactory cacheKeyFactory = dataSource.getCacheKeyFactory(); String key = buildCacheKey(dataSpec, cacheKeyFactory); long bytesLeft; - ProgressNotifier progressNotifier = null; + @Nullable ProgressNotifier progressNotifier = null; if (progressListener != null) { progressNotifier = new ProgressNotifier(progressListener); Pair lengthAndBytesAlreadyCached = getCached(dataSpec, cache, cacheKeyFactory); @@ -193,10 +183,10 @@ public final class CacheUtil { bytesLeft = getRequestLength(dataSpec, cache, key); } - long position = dataSpec.absoluteStreamPosition; + long position = dataSpec.position; boolean lengthUnset = bytesLeft == C.LENGTH_UNSET; while (bytesLeft != 0) { - throwExceptionIfInterruptedOrCancelled(isCanceled); + throwExceptionIfCanceled(isCanceled); long blockLength = cache.getCachedLength(key, position, lengthUnset ? Long.MAX_VALUE : bytesLeft); if (blockLength > 0) { @@ -212,12 +202,10 @@ public final class CacheUtil { position, length, dataSource, - buffer, - priorityTaskManager, - priority, + isCanceled, progressNotifier, isLastBlock, - isCanceled); + temporaryBuffer); if (read < blockLength) { // Reached to the end of the data. if (enableEOFException && !lengthUnset) { @@ -238,51 +226,50 @@ public final class CacheUtil { return dataSpec.length; } else { long contentLength = ContentMetadata.getContentLength(cache.getContentMetadata(key)); - return contentLength == C.LENGTH_UNSET - ? C.LENGTH_UNSET - : contentLength - dataSpec.absoluteStreamPosition; + return contentLength == C.LENGTH_UNSET ? C.LENGTH_UNSET : contentLength - dataSpec.position; } } /** * Reads and discards all data specified by the {@code dataSpec}. * - * @param dataSpec Defines the data to be read. {@code absoluteStreamPosition} and {@code length} - * fields are overwritten by the following parameters. - * @param absoluteStreamPosition The absolute position of the data to be read. + * @param dataSpec Defines the data to be read. The {@code position} and {@code length} fields are + * overwritten by the following parameters. + * @param position The position of the data to be read. * @param length Length of the data to be read, or {@link C#LENGTH_UNSET} if it is unknown. - * @param dataSource The {@link DataSource} to read the data from. - * @param buffer The buffer to be used while downloading. - * @param priorityTaskManager If not null it's used to check whether it is allowed to proceed with - * caching. - * @param priority The priority of this task. + * @param dataSource The {@link CacheDataSource} to read the data from. + * @param isCanceled An optional flag that will cancel the operation if set to true. * @param progressNotifier A notifier through which to report progress updates, or {@code null}. * @param isLastBlock Whether this read block is the last block of the content. - * @param isCanceled An optional flag that will interrupt caching if set to true. + * @param temporaryBuffer A temporary buffer to be used during caching. * @return Number of read bytes, or 0 if no data is available because the end of the opened range * has been reached. + * @param isCanceled An optional flag that will cancel the operation if set to true. */ private static long readAndDiscard( DataSpec dataSpec, - long absoluteStreamPosition, + long position, long length, - DataSource dataSource, - byte[] buffer, - @Nullable PriorityTaskManager priorityTaskManager, - int priority, + CacheDataSource dataSource, + @Nullable AtomicBoolean isCanceled, @Nullable ProgressNotifier progressNotifier, boolean isLastBlock, - @Nullable AtomicBoolean isCanceled) - throws IOException, InterruptedException { - long positionOffset = absoluteStreamPosition - dataSpec.absoluteStreamPosition; + byte[] temporaryBuffer) + throws IOException { + long positionOffset = position - dataSpec.position; long initialPositionOffset = positionOffset; long endOffset = length != C.LENGTH_UNSET ? positionOffset + length : C.POSITION_UNSET; + @Nullable PriorityTaskManager priorityTaskManager = dataSource.getUpstreamPriorityTaskManager(); while (true) { if (priorityTaskManager != null) { // Wait for any other thread with higher priority to finish its job. - priorityTaskManager.proceed(priority); + try { + priorityTaskManager.proceed(dataSource.getUpstreamPriority()); + } catch (InterruptedException e) { + throw new InterruptedIOException(); + } } - throwExceptionIfInterruptedOrCancelled(isCanceled); + throwExceptionIfCanceled(isCanceled); try { long resolvedLength = C.LENGTH_UNSET; boolean isDataSourceOpen = false; @@ -309,14 +296,14 @@ public final class CacheUtil { progressNotifier.onRequestLengthResolved(positionOffset + resolvedLength); } while (positionOffset != endOffset) { - throwExceptionIfInterruptedOrCancelled(isCanceled); + throwExceptionIfCanceled(isCanceled); int bytesRead = dataSource.read( - buffer, + temporaryBuffer, 0, endOffset != C.POSITION_UNSET - ? (int) Math.min(buffer.length, endOffset - positionOffset) - : buffer.length); + ? (int) Math.min(temporaryBuffer.length, endOffset - positionOffset) + : temporaryBuffer.length); if (bytesRead == C.RESULT_END_OF_INPUT) { if (progressNotifier != null) { progressNotifier.onRequestLengthResolved(positionOffset); @@ -373,7 +360,7 @@ public final class CacheUtil { } /* package */ static boolean isCausedByPositionOutOfRange(IOException e) { - Throwable cause = e; + @Nullable Throwable cause = e; while (cause != null) { if (cause instanceof DataSourceException) { int reason = ((DataSourceException) cause).reason; @@ -392,10 +379,10 @@ public final class CacheUtil { .buildCacheKey(dataSpec); } - private static void throwExceptionIfInterruptedOrCancelled(@Nullable AtomicBoolean isCanceled) - throws InterruptedException { - if (Thread.interrupted() || (isCanceled != null && isCanceled.get())) { - throw new InterruptedException(); + private static void throwExceptionIfCanceled(@Nullable AtomicBoolean isCanceled) + throws InterruptedIOException { + if (isCanceled != null && isCanceled.get()) { + throw new InterruptedIOException(); } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndex.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndex.java index 7e09025ddd..43bf691701 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndex.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndex.java @@ -45,11 +45,11 @@ import java.io.OutputStream; import java.security.InvalidAlgorithmParameterException; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.Map; -import java.util.Random; import java.util.Set; import javax.crypto.Cipher; import javax.crypto.CipherInputStream; @@ -229,11 +229,12 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; * @return A new or existing CachedContent instance with the given key. */ public CachedContent getOrAdd(String key) { - CachedContent cachedContent = keyToContent.get(key); + @Nullable CachedContent cachedContent = keyToContent.get(key); return cachedContent == null ? addNew(key) : cachedContent; } /** Returns a CachedContent instance with the given key or null if there isn't one. */ + @Nullable public CachedContent get(String key) { return keyToContent.get(key); } @@ -254,14 +255,15 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; return getOrAdd(key).id; } - /** Returns the key which has the given id assigned. */ + /** Returns the key which has the given id assigned, or {@code null} if no such key exists. */ + @Nullable public String getKeyForId(int id) { return idToKey.get(id); } /** Removes {@link CachedContent} with the given key from index if it's empty and not locked. */ public void maybeRemove(String key) { - CachedContent cachedContent = keyToContent.get(key); + @Nullable CachedContent cachedContent = keyToContent.get(key); if (cachedContent != null && cachedContent.isEmpty() && !cachedContent.isLocked()) { keyToContent.remove(key); int id = cachedContent.id; @@ -492,7 +494,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; private final boolean encrypt; @Nullable private final Cipher cipher; @Nullable private final SecretKeySpec secretKeySpec; - @Nullable private final Random random; + @Nullable private final SecureRandom random; private final AtomicFile atomicFile; private boolean changed; @@ -515,7 +517,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; this.encrypt = encrypt; this.cipher = cipher; this.secretKeySpec = secretKeySpec; - random = encrypt ? new Random() : null; + random = encrypt ? new SecureRandom() : null; atomicFile = new AtomicFile(file); } @@ -626,7 +628,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; } private void writeFile(HashMap content) throws IOException { - DataOutputStream output = null; + @Nullable DataOutputStream output = null; try { OutputStream outputStream = atomicFile.startWrite(); if (bufferedOutputStream == null) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedRegionTracker.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedRegionTracker.java index fb2d4f694f..c3fa9e3b08 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedRegionTracker.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedRegionTracker.java @@ -15,7 +15,7 @@ */ package com.google.android.exoplayer2.upstream.cache; -import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.extractor.ChunkIndex; import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.Util; @@ -77,7 +77,7 @@ public final class CachedRegionTracker implements Cache.Listener { */ public synchronized int getRegionEndTimeMs(long byteOffset) { lookupRegion.startOffset = byteOffset; - Region floorRegion = regions.floor(lookupRegion); + @Nullable Region floorRegion = regions.floor(lookupRegion); if (floorRegion == null || byteOffset > floorRegion.endOffset || floorRegion.endOffsetIndex == -1) { return NOT_CACHED; @@ -102,7 +102,7 @@ public final class CachedRegionTracker implements Cache.Listener { Region removedRegion = new Region(span.position, span.position + span.length); // Look up a region this span falls into. - Region floorRegion = regions.floor(removedRegion); + @Nullable Region floorRegion = regions.floor(removedRegion); if (floorRegion == null) { Log.e(TAG, "Removed a span we were not aware of"); return; @@ -134,8 +134,8 @@ public final class CachedRegionTracker implements Cache.Listener { private void mergeSpan(CacheSpan span) { Region newRegion = new Region(span.position, span.position + span.length); - Region floorRegion = regions.floor(newRegion); - Region ceilingRegion = regions.ceiling(newRegion); + @Nullable Region floorRegion = regions.floor(newRegion); + @Nullable Region ceilingRegion = regions.ceiling(newRegion); boolean floorConnects = regionsConnect(floorRegion, newRegion); boolean ceilingConnects = regionsConnect(newRegion, ceilingRegion); @@ -168,7 +168,7 @@ public final class CachedRegionTracker implements Cache.Listener { } } - private boolean regionsConnect(Region lower, Region upper) { + private boolean regionsConnect(@Nullable Region lower, @Nullable Region upper) { return lower != null && upper != null && lower.endOffset == upper.startOffset; } @@ -195,7 +195,7 @@ public final class CachedRegionTracker implements Cache.Listener { } @Override - public int compareTo(@NonNull Region another) { + public int compareTo(Region another) { return Util.compareLong(startOffset, another.startOffset); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/ContentMetadata.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/ContentMetadata.java index 4cc6e6b860..26b6d83a43 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/ContentMetadata.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/ContentMetadata.java @@ -81,7 +81,7 @@ public interface ContentMetadata { */ @Nullable static Uri getRedirectedUri(ContentMetadata contentMetadata) { - String redirectedUri = contentMetadata.get(KEY_REDIRECTED_URI, (String) null); + @Nullable String redirectedUri = contentMetadata.get(KEY_REDIRECTED_URI, (String) null); return redirectedUri == null ? null : Uri.parse(redirectedUri); } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/ContentMetadataMutations.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/ContentMetadataMutations.java index 5715b8fbd4..f6cac58997 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/ContentMetadataMutations.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/ContentMetadataMutations.java @@ -73,8 +73,7 @@ public class ContentMetadataMutations { } /** - * Adds a mutation to set a metadata value. Passing {@code null} as {@code name} or {@code value} - * isn't allowed. + * Adds a mutation to set a metadata value. * * @param name The name of the metadata value. * @param value The value to be set. @@ -85,7 +84,7 @@ public class ContentMetadataMutations { } /** - * Adds a mutation to set a metadata value. Passing {@code null} as {@code name} isn't allowed. + * Adds a mutation to set a metadata value. * * @param name The name of the metadata value. * @param value The value to be set. @@ -96,8 +95,7 @@ public class ContentMetadataMutations { } /** - * Adds a mutation to set a metadata value. Passing {@code null} as {@code name} or {@code value} - * isn't allowed. + * Adds a mutation to set a metadata value. * * @param name The name of the metadata value. * @param value The value to be set. 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 1f07af938a..c3f06252e4 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 @@ -67,8 +67,8 @@ public final class DefaultContentMetadata implements ContentMetadata { @Override @Nullable public final byte[] get(String name, @Nullable byte[] defaultValue) { - if (metadata.containsKey(name)) { - byte[] bytes = metadata.get(name); + @Nullable byte[] bytes = metadata.get(name); + if (bytes != null) { return Arrays.copyOf(bytes, bytes.length); } else { return defaultValue; @@ -78,8 +78,8 @@ public final class DefaultContentMetadata implements ContentMetadata { @Override @Nullable public final String get(String name, @Nullable String defaultValue) { - if (metadata.containsKey(name)) { - byte[] bytes = metadata.get(name); + @Nullable byte[] bytes = metadata.get(name); + if (bytes != null) { return new String(bytes, Charset.forName(C.UTF8_NAME)); } else { return defaultValue; @@ -88,8 +88,8 @@ public final class DefaultContentMetadata implements ContentMetadata { @Override public final long get(String name, long defaultValue) { - if (metadata.containsKey(name)) { - byte[] bytes = metadata.get(name); + @Nullable byte[] bytes = metadata.get(name); + if (bytes != null) { return ByteBuffer.wrap(bytes).getLong(); } else { return defaultValue; @@ -130,7 +130,7 @@ public final class DefaultContentMetadata implements ContentMetadata { } for (Entry entry : first.entrySet()) { byte[] value = entry.getValue(); - byte[] otherValue = second.get(entry.getKey()); + @Nullable byte[] otherValue = second.get(entry.getKey()); if (!Arrays.equals(value, otherValue)) { return false; } @@ -153,8 +153,8 @@ public final class DefaultContentMetadata implements ContentMetadata { } private static void addValues(HashMap metadata, Map values) { - for (String name : values.keySet()) { - metadata.put(name, getBytes(values.get(name))); + for (Entry entry : values.entrySet()) { + metadata.put(entry.getKey(), getBytes(entry.getValue())); } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java index a4fade25e0..721dac4d4e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java @@ -16,7 +16,6 @@ package com.google.android.exoplayer2.upstream.cache; import android.os.ConditionVariable; -import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.WorkerThread; import com.google.android.exoplayer2.C; @@ -260,7 +259,7 @@ public final class SimpleCache implements Cache { // Start cache initialization. final ConditionVariable conditionVariable = new ConditionVariable(); - new Thread("SimpleCache.initialize()") { + new Thread("ExoPlayer:SimpleCacheInit") { @Override public void run() { synchronized (SimpleCache.this) { @@ -332,7 +331,6 @@ public final class SimpleCache implements Cache { } } - @NonNull @Override public synchronized NavigableSet getCachedSpans(String key) { Assertions.checkState(!released); @@ -465,8 +463,7 @@ public final class SimpleCache implements Cache { @Override public synchronized void releaseHoleSpan(CacheSpan holeSpan) { Assertions.checkState(!released); - CachedContent cachedContent = contentIndex.get(holeSpan.key); - Assertions.checkNotNull(cachedContent); + CachedContent cachedContent = Assertions.checkNotNull(contentIndex.get(holeSpan.key)); Assertions.checkState(cachedContent.isLocked()); cachedContent.setLocked(false); contentIndex.maybeRemove(cachedContent.key); @@ -482,14 +479,14 @@ public final class SimpleCache implements Cache { @Override public synchronized boolean isCached(String key, long position, long length) { Assertions.checkState(!released); - CachedContent cachedContent = contentIndex.get(key); + @Nullable CachedContent cachedContent = contentIndex.get(key); return cachedContent != null && cachedContent.getCachedBytesLength(position, length) >= length; } @Override public synchronized long getCachedLength(String key, long position, long length) { Assertions.checkState(!released); - CachedContent cachedContent = contentIndex.get(key); + @Nullable CachedContent cachedContent = contentIndex.get(key); return cachedContent != null ? cachedContent.getCachedBytesLength(position, length) : -length; } @@ -524,7 +521,7 @@ public final class SimpleCache implements Cache { } } - File[] files = cacheDir.listFiles(); + @Nullable File[] files = cacheDir.listFiles(); if (files == null) { String message = "Failed to list cache directory files: " + cacheDir; Log.e(TAG, message); @@ -605,11 +602,13 @@ public final class SimpleCache implements Cache { } long length = C.LENGTH_UNSET; long lastTouchTimestamp = C.TIME_UNSET; + @Nullable CacheFileMetadata metadata = fileMetadata != null ? fileMetadata.remove(fileName) : null; if (metadata != null) { length = metadata.length; lastTouchTimestamp = metadata.lastTouchTimestamp; } + @Nullable SimpleCacheSpan span = SimpleCacheSpan.createCacheEntry(file, length, lastTouchTimestamp, contentIndex); if (span != null) { @@ -666,7 +665,7 @@ public final class SimpleCache implements Cache { * @return The corresponding cache {@link SimpleCacheSpan}. */ private SimpleCacheSpan getSpan(String key, long position) { - CachedContent cachedContent = contentIndex.get(key); + @Nullable CachedContent cachedContent = contentIndex.get(key); if (cachedContent == null) { return SimpleCacheSpan.createOpenHole(key, position); } @@ -694,7 +693,7 @@ public final class SimpleCache implements Cache { } private void removeSpanInternal(CacheSpan span) { - CachedContent cachedContent = contentIndex.get(span.key); + @Nullable CachedContent cachedContent = contentIndex.get(span.key); if (cachedContent == null || !cachedContent.removeSpan(span)) { return; } @@ -732,7 +731,7 @@ public final class SimpleCache implements Cache { } private void notifySpanRemoved(CacheSpan span) { - ArrayList keyListeners = listeners.get(span.key); + @Nullable ArrayList keyListeners = listeners.get(span.key); if (keyListeners != null) { for (int i = keyListeners.size() - 1; i >= 0; i--) { keyListeners.get(i).onSpanRemoved(this, span); @@ -742,7 +741,7 @@ public final class SimpleCache implements Cache { } private void notifySpanAdded(SimpleCacheSpan span) { - ArrayList keyListeners = listeners.get(span.key); + @Nullable ArrayList keyListeners = listeners.get(span.key); if (keyListeners != null) { for (int i = keyListeners.size() - 1; i >= 0; i--) { keyListeners.get(i).onSpanAdded(this, span); @@ -752,7 +751,7 @@ public final class SimpleCache implements Cache { } private void notifySpanTouched(SimpleCacheSpan oldSpan, CacheSpan newSpan) { - ArrayList keyListeners = listeners.get(oldSpan.key); + @Nullable ArrayList keyListeners = listeners.get(oldSpan.key); if (keyListeners != null) { for (int i = keyListeners.size() - 1; i >= 0; i--) { keyListeners.get(i).onSpanTouched(this, oldSpan, newSpan); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheSpan.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheSpan.java index 5f6ea338e6..d8a0671469 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheSpan.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheSpan.java @@ -91,6 +91,7 @@ import java.util.regex.Pattern; * @param length The length of the cache file in bytes, or {@link C#LENGTH_UNSET} to query the * underlying file system. Querying the underlying file system can be expensive, so callers * that already know the length of the file should pass it explicitly. + * @param index The cached content index. * @return The span, or null if the file name is not correctly formatted, or if the id is not * present in the content index, or if the length is 0. */ @@ -108,6 +109,7 @@ import java.util.regex.Pattern; * that already know the length of the file should pass it explicitly. * @param lastTouchTimestamp The last touch timestamp, or {@link C#TIME_UNSET} to use the file * timestamp. + * @param index The cached content index. * @return The span, or null if the file name is not correctly formatted, or if the id is not * present in the content index, or if the length is 0. */ @@ -129,8 +131,8 @@ import java.util.regex.Pattern; return null; } - int id = Integer.parseInt(matcher.group(1)); - String key = index.getKeyForId(id); + int id = Integer.parseInt(Assertions.checkNotNull(matcher.group(1))); + @Nullable String key = index.getKeyForId(id); if (key == null) { return null; } @@ -142,9 +144,9 @@ import java.util.regex.Pattern; return null; } - long position = Long.parseLong(matcher.group(2)); + long position = Long.parseLong(Assertions.checkNotNull(matcher.group(2))); if (lastTouchTimestamp == C.TIME_UNSET) { - lastTouchTimestamp = Long.parseLong(matcher.group(3)); + lastTouchTimestamp = Long.parseLong(Assertions.checkNotNull(matcher.group(3))); } return new SimpleCacheSpan(key, position, length, lastTouchTimestamp, file); } @@ -153,34 +155,34 @@ import java.util.regex.Pattern; * Upgrades the cache file if it is created by an earlier version of {@link SimpleCache}. * * @param file The cache file. - * @param index Cached content index. + * @param index The cached content index. * @return Upgraded cache file or {@code null} if the file name is not correctly formatted or the * file can not be renamed. */ @Nullable private static File upgradeFile(File file, CachedContentIndex index) { - String key; + @Nullable String key = null; String filename = file.getName(); Matcher matcher = CACHE_FILE_PATTERN_V2.matcher(filename); if (matcher.matches()) { - key = Util.unescapeFileName(matcher.group(1)); - if (key == null) { - return null; - } + key = Util.unescapeFileName(Assertions.checkNotNull(matcher.group(1))); } else { matcher = CACHE_FILE_PATTERN_V1.matcher(filename); - if (!matcher.matches()) { - return null; + if (matcher.matches()) { + key = Assertions.checkNotNull(matcher.group(1)); // Keys were not escaped in version 1. } - key = matcher.group(1); // Keys were not escaped in version 1. + } + + if (key == null) { + return null; } File newCacheFile = getCacheFile( Assertions.checkStateNotNull(file.getParentFile()), index.assignIdForKey(key), - Long.parseLong(matcher.group(2)), - Long.parseLong(matcher.group(3))); + Long.parseLong(Assertions.checkNotNull(matcher.group(2))), + Long.parseLong(Assertions.checkNotNull(matcher.group(3)))); if (!file.renameTo(newCacheFile)) { return null; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/package-info.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/package-info.java new file mode 100644 index 0000000000..bb6cf77458 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2019 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. + */ +@NonNullApi +package com.google.android.exoplayer2.upstream.cache; + +import com.google.android.exoplayer2.util.NonNullApi; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/crypto/AesCipherDataSink.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/crypto/AesCipherDataSink.java index d9b3ff0069..95295035e7 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/crypto/AesCipherDataSink.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/crypto/AesCipherDataSink.java @@ -68,8 +68,9 @@ public final class AesCipherDataSink implements DataSink { public void open(DataSpec dataSpec) throws IOException { wrappedDataSink.open(dataSpec); long nonce = CryptoUtil.getFNV64Hash(dataSpec.key); - cipher = new AesFlushingCipher(Cipher.ENCRYPT_MODE, secretKey, nonce, - dataSpec.absoluteStreamPosition); + cipher = + new AesFlushingCipher( + Cipher.ENCRYPT_MODE, secretKey, nonce, dataSpec.uriPositionOffset + dataSpec.position); } @Override 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 0910c63c19..665a47191e 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 @@ -52,8 +52,9 @@ public final class AesCipherDataSource implements DataSource { public long open(DataSpec dataSpec) throws IOException { long dataLength = upstream.open(dataSpec); long nonce = CryptoUtil.getFNV64Hash(dataSpec.key); - cipher = new AesFlushingCipher(Cipher.DECRYPT_MODE, secretKey, nonce, - dataSpec.absoluteStreamPosition); + cipher = + new AesFlushingCipher( + Cipher.DECRYPT_MODE, secretKey, nonce, dataSpec.uriPositionOffset + dataSpec.position); return dataLength; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/crypto/package-info.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/crypto/package-info.java new file mode 100644 index 0000000000..9c4005e815 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/crypto/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2019 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. + */ +@NonNullApi +package com.google.android.exoplayer2.upstream.crypto; + +import com.google.android.exoplayer2.util.NonNullApi; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/Clock.java b/library/core/src/main/java/com/google/android/exoplayer2/util/Clock.java index 7a87d7d9a3..ffb8236bd1 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/Clock.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/Clock.java @@ -30,6 +30,13 @@ public interface Clock { */ Clock DEFAULT = new SystemClock(); + /** + * Returns the current time in milliseconds since the Unix Epoch. + * + * @see System#currentTimeMillis() + */ + long currentTimeMillis(); + /** @see android.os.SystemClock#elapsedRealtime() */ long elapsedRealtime(); 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 54f52e0a14..85ef43f669 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 @@ -15,7 +15,9 @@ */ package com.google.android.exoplayer2.util; +import android.graphics.Color; import android.text.TextUtils; +import androidx.annotation.ColorInt; import java.util.HashMap; import java.util.Map; import java.util.regex.Matcher; @@ -49,6 +51,7 @@ public final class ColorParser { * @param colorExpression The color expression. * @return The parsed ARGB color. */ + @ColorInt public static int parseTtmlColor(String colorExpression) { return parseColorInternal(colorExpression, false); } @@ -59,10 +62,12 @@ public final class ColorParser { * @param colorExpression The color expression. * @return The parsed ARGB color. */ + @ColorInt public static int parseCssColor(String colorExpression) { return parseColorInternal(colorExpression, true); } + @ColorInt private static int parseColorInternal(String colorExpression, boolean alphaHasFloatFormat) { Assertions.checkArgument(!TextUtils.isEmpty(colorExpression)); colorExpression = colorExpression.replace(" ", ""); @@ -83,22 +88,21 @@ public final class ColorParser { Matcher matcher = (alphaHasFloatFormat ? RGBA_PATTERN_FLOAT_ALPHA : RGBA_PATTERN_INT_ALPHA) .matcher(colorExpression); if (matcher.matches()) { - return argb( - alphaHasFloatFormat ? (int) (255 * Float.parseFloat(matcher.group(4))) - : Integer.parseInt(matcher.group(4), 10), - Integer.parseInt(matcher.group(1), 10), - Integer.parseInt(matcher.group(2), 10), - Integer.parseInt(matcher.group(3), 10) - ); + return Color.argb( + alphaHasFloatFormat + ? (int) (255 * Float.parseFloat(Assertions.checkNotNull(matcher.group(4)))) + : Integer.parseInt(Assertions.checkNotNull(matcher.group(4)), 10), + Integer.parseInt(Assertions.checkNotNull(matcher.group(1)), 10), + Integer.parseInt(Assertions.checkNotNull(matcher.group(2)), 10), + Integer.parseInt(Assertions.checkNotNull(matcher.group(3)), 10)); } } else if (colorExpression.startsWith(RGB)) { Matcher matcher = RGB_PATTERN.matcher(colorExpression); if (matcher.matches()) { - return rgb( - Integer.parseInt(matcher.group(1), 10), - Integer.parseInt(matcher.group(2), 10), - Integer.parseInt(matcher.group(3), 10) - ); + return Color.rgb( + Integer.parseInt(Assertions.checkNotNull(matcher.group(1)), 10), + Integer.parseInt(Assertions.checkNotNull(matcher.group(2)), 10), + Integer.parseInt(Assertions.checkNotNull(matcher.group(3)), 10)); } } else { // we use our own color map @@ -110,14 +114,6 @@ public final class ColorParser { throw new IllegalArgumentException(); } - private static int argb(int alpha, int red, int green, int blue) { - return (alpha << 24) | (red << 16) | (green << 8) | blue; - } - - private static int rgb(int red, int green, int blue) { - return argb(0xFF, red, green, blue); - } - static { COLOR_MAP = new HashMap<>(); COLOR_MAP.put("aliceblue", 0xFFF0F8FF); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/ConditionVariable.java b/library/core/src/main/java/com/google/android/exoplayer2/util/ConditionVariable.java index c035c62a7e..b7f0d04e23 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/ConditionVariable.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/ConditionVariable.java @@ -16,13 +16,39 @@ package com.google.android.exoplayer2.util; /** - * An interruptible condition variable whose {@link #open()} and {@link #close()} methods return - * whether they resulted in a change of state. + * An interruptible condition variable. This class provides a number of benefits over {@link + * android.os.ConditionVariable}: + * + *

          + *
        • Consistent use of ({@link Clock#elapsedRealtime()} for timing {@link #block(long)} timeout + * intervals. {@link android.os.ConditionVariable} used {@link System#currentTimeMillis()} + * prior to Android 10, which is not a correct clock to use for interval timing because it's + * not guaranteed to be monotonic. + *
        • Support for injecting a custom {@link Clock}. + *
        • The ability to query the variable's current state, by calling {@link #isOpen()}. + *
        • {@link #open()} and {@link #close()} return whether they changed the variable's state. + *
        */ -public final class ConditionVariable { +public class ConditionVariable { + private final Clock clock; private boolean isOpen; + /** Creates an instance using {@link Clock#DEFAULT}. */ + public ConditionVariable() { + this(Clock.DEFAULT); + } + + /** + * Creates an instance. + * + * @param clock The {@link Clock} whose {@link Clock#elapsedRealtime()} method is used to + * determine when {@link #block(long)} should time out. + */ + public ConditionVariable(Clock clock) { + this.clock = clock; + } + /** * Opens the condition and releases all threads that are blocked. * @@ -60,18 +86,27 @@ public final class ConditionVariable { } /** - * Blocks until the condition is opened or until {@code timeout} milliseconds have passed. + * Blocks until the condition is opened or until {@code timeoutMs} have passed. * - * @param timeout The maximum time to wait in milliseconds. + * @param timeoutMs The maximum time to wait in milliseconds. If {@code timeoutMs <= 0} then the + * call will return immediately without blocking. * @return True if the condition was opened, false if the call returns because of the timeout. * @throws InterruptedException If the thread is interrupted. */ - public synchronized boolean block(long timeout) throws InterruptedException { - long now = android.os.SystemClock.elapsedRealtime(); - long end = now + timeout; - while (!isOpen && now < end) { - wait(end - now); - now = android.os.SystemClock.elapsedRealtime(); + public synchronized boolean block(long timeoutMs) throws InterruptedException { + if (timeoutMs <= 0) { + return isOpen; + } + long nowMs = clock.elapsedRealtime(); + long endMs = nowMs + timeoutMs; + if (endMs < nowMs) { + // timeoutMs is large enough for (nowMs + timeoutMs) to rollover. Block indefinitely. + block(); + } else { + while (!isOpen && nowMs < endMs) { + wait(endMs - nowMs); + nowMs = clock.elapsedRealtime(); + } } return isOpen; } 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 e72e72c3c4..60bd4f0c77 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 @@ -15,7 +15,6 @@ */ package com.google.android.exoplayer2.util; -import android.annotation.TargetApi; import android.graphics.SurfaceTexture; import android.opengl.EGL14; import android.opengl.EGLConfig; @@ -26,12 +25,13 @@ import android.opengl.GLES20; import android.os.Handler; import androidx.annotation.IntDef; import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; /** Generates a {@link SurfaceTexture} using EGL/GLES functions. */ -@TargetApi(17) +@RequiresApi(17) public final class EGLSurfaceTexture implements SurfaceTexture.OnFrameAvailableListener, Runnable { /** Listener to be called when the texture image on {@link SurfaceTexture} has been updated. */ 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 66531e8f28..3136556f2c 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 @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.util; import android.os.SystemClock; +import android.text.TextUtils; import android.view.Surface; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; @@ -25,6 +26,7 @@ import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Player.PlaybackSuppressionReason; import com.google.android.exoplayer2.RendererCapabilities; +import com.google.android.exoplayer2.RendererCapabilities.AdaptiveSupport; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.analytics.AnalyticsListener; import com.google.android.exoplayer2.audio.AudioAttributes; @@ -90,14 +92,22 @@ public class EventLogger implements AnalyticsListener { // AnalyticsListener @Override - public void onLoadingChanged(EventTime eventTime, boolean isLoading) { + public void onIsLoadingChanged(EventTime eventTime, boolean isLoading) { logd(eventTime, "loading", Boolean.toString(isLoading)); } @Override - public void onPlayerStateChanged( - EventTime eventTime, boolean playWhenReady, @Player.State int state) { - logd(eventTime, "state", playWhenReady + ", " + getStateString(state)); + public void onPlaybackStateChanged(EventTime eventTime, @Player.State int state) { + logd(eventTime, "state", getStateString(state)); + } + + @Override + public void onPlayWhenReadyChanged( + EventTime eventTime, boolean playWhenReady, @Player.PlayWhenReadyChangeReason int reason) { + logd( + eventTime, + "playWhenReady", + playWhenReady + ", " + getPlayWhenReadyChangeReasonString(reason)); } @Override @@ -140,9 +150,12 @@ public class EventLogger implements AnalyticsListener { logd( eventTime, "playbackParameters", - Util.formatInvariant( - "speed=%.2f, pitch=%.2f, skipSilence=%s", - playbackParameters.speed, playbackParameters.pitch, playbackParameters.skipSilence)); + Util.formatInvariant("speed=%.2f", playbackParameters.speed)); + } + + @Override + public void onPlaybackSpeedChanged(EventTime eventTime, float playbackSpeed) { + logd(eventTime, "playbackSpeed", Float.toString(playbackSpeed)); } @Override @@ -197,25 +210,28 @@ public class EventLogger implements AnalyticsListener { logd(eventTime, "tracks", "[]"); return; } - logd("tracks [" + getEventTimeString(eventTime) + ", "); + logd("tracks [" + getEventTimeString(eventTime)); // Log tracks associated to renderers. int rendererCount = mappedTrackInfo.getRendererCount(); for (int rendererIndex = 0; rendererIndex < rendererCount; rendererIndex++) { TrackGroupArray rendererTrackGroups = mappedTrackInfo.getTrackGroups(rendererIndex); TrackSelection trackSelection = trackSelections.get(rendererIndex); - if (rendererTrackGroups.length > 0) { - logd(" Renderer:" + rendererIndex + " ["); + if (rendererTrackGroups.length == 0) { + logd(" " + mappedTrackInfo.getRendererName(rendererIndex) + " []"); + } else { + logd(" " + mappedTrackInfo.getRendererName(rendererIndex) + " ["); for (int groupIndex = 0; groupIndex < rendererTrackGroups.length; groupIndex++) { TrackGroup trackGroup = rendererTrackGroups.get(groupIndex); String adaptiveSupport = getAdaptiveSupportString( trackGroup.length, - mappedTrackInfo.getAdaptiveSupport(rendererIndex, groupIndex, false)); + mappedTrackInfo.getAdaptiveSupport( + rendererIndex, groupIndex, /* includeCapabilitiesExceededTracks= */ false)); logd(" Group:" + groupIndex + ", adaptive_supported=" + adaptiveSupport + " ["); for (int trackIndex = 0; trackIndex < trackGroup.length; trackIndex++) { String status = getTrackStatusString(trackSelection, trackGroup, trackIndex); String formatSupport = - getFormatSupportString( + RendererCapabilities.getFormatSupportString( mappedTrackInfo.getTrackSupport(rendererIndex, groupIndex, trackIndex)); logd( " " @@ -247,14 +263,15 @@ public class EventLogger implements AnalyticsListener { // Log tracks not associated with a renderer. TrackGroupArray unassociatedTrackGroups = mappedTrackInfo.getUnmappedTrackGroups(); if (unassociatedTrackGroups.length > 0) { - logd(" Renderer:None ["); + logd(" Unmapped ["); for (int groupIndex = 0; groupIndex < unassociatedTrackGroups.length; groupIndex++) { logd(" Group:" + groupIndex + " ["); TrackGroup trackGroup = unassociatedTrackGroups.get(groupIndex); for (int trackIndex = 0; trackIndex < trackGroup.length; trackIndex++) { String status = getTrackStatusString(false); String formatSupport = - getFormatSupportString(RendererCapabilities.FORMAT_UNSUPPORTED_TYPE); + RendererCapabilities.getFormatSupportString( + RendererCapabilities.FORMAT_UNSUPPORTED_TYPE); logd( " " + status @@ -272,21 +289,16 @@ public class EventLogger implements AnalyticsListener { logd("]"); } - @Override - public void onSeekProcessed(EventTime eventTime) { - logd(eventTime, "seekProcessed"); - } - @Override public void onMetadata(EventTime eventTime, Metadata metadata) { - logd("metadata [" + getEventTimeString(eventTime) + ", "); + logd("metadata [" + getEventTimeString(eventTime)); printMetadata(metadata, " "); logd("]"); } @Override public void onDecoderEnabled(EventTime eventTime, int trackType, DecoderCounters counters) { - logd(eventTime, "decoderEnabled", getTrackTypeString(trackType)); + logd(eventTime, "decoderEnabled", Util.getTrackTypeString(trackType)); } @Override @@ -308,6 +320,11 @@ public class EventLogger implements AnalyticsListener { + audioAttributes.allowedCapturePolicy); } + @Override + public void onSkipSilenceEnabledChanged(EventTime eventTime, boolean skipSilenceEnabled) { + logd(eventTime, "skipSilenceEnabled", Boolean.toString(skipSilenceEnabled)); + } + @Override public void onVolumeChanged(EventTime eventTime, float volume) { logd(eventTime, "volume", Float.toString(volume)); @@ -316,7 +333,7 @@ public class EventLogger implements AnalyticsListener { @Override public void onDecoderInitialized( EventTime eventTime, int trackType, String decoderName, long initializationDurationMs) { - logd(eventTime, "decoderInitialized", getTrackTypeString(trackType) + ", " + decoderName); + logd(eventTime, "decoderInitialized", Util.getTrackTypeString(trackType) + ", " + decoderName); } @Override @@ -324,12 +341,12 @@ public class EventLogger implements AnalyticsListener { logd( eventTime, "decoderInputFormat", - getTrackTypeString(trackType) + ", " + Format.toLogString(format)); + Util.getTrackTypeString(trackType) + ", " + Format.toLogString(format)); } @Override public void onDecoderDisabled(EventTime eventTime, int trackType, DecoderCounters counters) { - logd(eventTime, "decoderDisabled", getTrackTypeString(trackType)); + logd(eventTime, "decoderDisabled", Util.getTrackTypeString(trackType)); } @Override @@ -466,27 +483,26 @@ public class EventLogger implements AnalyticsListener { } /** - * Logs an error message and exception. + * Logs an error message. * * @param msg The message to log. - * @param tr The exception to log. */ - protected void loge(String msg, @Nullable Throwable tr) { - Log.e(tag, msg, tr); + protected void loge(String msg) { + Log.e(tag, msg); } // Internal methods private void logd(EventTime eventTime, String eventName) { - logd(getEventString(eventTime, eventName)); + logd(getEventString(eventTime, eventName, /* eventDescription= */ null, /* throwable= */ null)); } private void logd(EventTime eventTime, String eventName, String eventDescription) { - logd(getEventString(eventTime, eventName, eventDescription)); + logd(getEventString(eventTime, eventName, eventDescription, /* throwable= */ null)); } private void loge(EventTime eventTime, String eventName, @Nullable Throwable throwable) { - loge(getEventString(eventTime, eventName), throwable); + loge(getEventString(eventTime, eventName, /* eventDescription= */ null, throwable)); } private void loge( @@ -494,7 +510,7 @@ public class EventLogger implements AnalyticsListener { String eventName, String eventDescription, @Nullable Throwable throwable) { - loge(getEventString(eventTime, eventName, eventDescription), throwable); + loge(getEventString(eventTime, eventName, eventDescription, throwable)); } private void printInternalError(EventTime eventTime, String type, Exception e) { @@ -507,12 +523,21 @@ public class EventLogger implements AnalyticsListener { } } - private String getEventString(EventTime eventTime, String eventName) { - return eventName + " [" + getEventTimeString(eventTime) + "]"; - } - - private String getEventString(EventTime eventTime, String eventName, String eventDescription) { - return eventName + " [" + getEventTimeString(eventTime) + ", " + eventDescription + "]"; + private String getEventString( + EventTime eventTime, + String eventName, + @Nullable String eventDescription, + @Nullable Throwable throwable) { + String eventString = eventName + " [" + getEventTimeString(eventTime); + if (eventDescription != null) { + eventString += ", " + eventDescription; + } + @Nullable String throwableString = Log.getThrowableString(throwable); + if (!TextUtils.isEmpty(throwableString)) { + eventString += "\n " + throwableString.replace("\n", "\n ") + '\n'; + } + eventString += "]"; + return eventString; } private String getEventTimeString(EventTime eventTime) { @@ -552,24 +577,8 @@ public class EventLogger implements AnalyticsListener { } } - private static String getFormatSupportString(int formatSupport) { - switch (formatSupport) { - case RendererCapabilities.FORMAT_HANDLED: - return "YES"; - case RendererCapabilities.FORMAT_EXCEEDS_CAPABILITIES: - return "NO_EXCEEDS_CAPABILITIES"; - case RendererCapabilities.FORMAT_UNSUPPORTED_DRM: - return "NO_UNSUPPORTED_DRM"; - case RendererCapabilities.FORMAT_UNSUPPORTED_SUBTYPE: - return "NO_UNSUPPORTED_TYPE"; - case RendererCapabilities.FORMAT_UNSUPPORTED_TYPE: - return "NO"; - default: - return "?"; - } - } - - private static String getAdaptiveSupportString(int trackCount, int adaptiveSupport) { + private static String getAdaptiveSupportString( + int trackCount, @AdaptiveSupport int adaptiveSupport) { if (trackCount < 2) { return "N/A"; } @@ -581,7 +590,7 @@ public class EventLogger implements AnalyticsListener { case RendererCapabilities.ADAPTIVE_NOT_SUPPORTED: return "NO"; default: - return "?"; + throw new IllegalStateException(); } } @@ -630,38 +639,15 @@ public class EventLogger implements AnalyticsListener { private static String getTimelineChangeReasonString(@Player.TimelineChangeReason int reason) { switch (reason) { - case Player.TIMELINE_CHANGE_REASON_PREPARED: - return "PREPARED"; - case Player.TIMELINE_CHANGE_REASON_RESET: - return "RESET"; - case Player.TIMELINE_CHANGE_REASON_DYNAMIC: - return "DYNAMIC"; + case Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE: + return "SOURCE_UPDATE"; + case Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED: + return "PLAYLIST_CHANGED"; default: return "?"; } } - private static String getTrackTypeString(int trackType) { - switch (trackType) { - case C.TRACK_TYPE_AUDIO: - return "audio"; - case C.TRACK_TYPE_DEFAULT: - return "default"; - case C.TRACK_TYPE_METADATA: - return "metadata"; - case C.TRACK_TYPE_CAMERA_MOTION: - return "camera motion"; - case C.TRACK_TYPE_NONE: - return "none"; - case C.TRACK_TYPE_TEXT: - return "text"; - case C.TRACK_TYPE_VIDEO: - return "video"; - default: - return trackType >= C.TRACK_TYPE_CUSTOM_BASE ? "custom (" + trackType + ")" : "?"; - } - } - private static String getPlaybackSuppressionReasonString( @PlaybackSuppressionReason int playbackSuppressionReason) { switch (playbackSuppressionReason) { @@ -673,4 +659,22 @@ public class EventLogger implements AnalyticsListener { return "?"; } } + + private static String getPlayWhenReadyChangeReasonString( + @Player.PlayWhenReadyChangeReason int reason) { + switch (reason) { + case Player.PLAY_WHEN_READY_CHANGE_REASON_AUDIO_BECOMING_NOISY: + return "AUDIO_BECOMING_NOISY"; + case Player.PLAY_WHEN_READY_CHANGE_REASON_AUDIO_FOCUS_LOSS: + return "AUDIO_FOCUS_LOSS"; + case Player.PLAY_WHEN_READY_CHANGE_REASON_REMOTE: + return "REMOTE"; + case Player.PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST: + return "USER_REQUEST"; + case Player.PLAY_WHEN_READY_CHANGE_REASON_END_OF_MEDIA_ITEM: + return "END_OF_MEDIA_ITEM"; + default: + return "?"; + } + } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/GlUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/util/GlUtil.java index c7feff516a..e90d133334 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/GlUtil.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/GlUtil.java @@ -17,23 +17,232 @@ package com.google.android.exoplayer2.util; import static android.opengl.GLU.gluErrorString; +import android.content.Context; +import android.content.pm.PackageManager; +import android.opengl.EGL14; +import android.opengl.EGLDisplay; import android.opengl.GLES11Ext; import android.opengl.GLES20; import android.text.TextUtils; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlayerLibraryInfo; +import java.nio.Buffer; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.nio.FloatBuffer; import java.nio.IntBuffer; +import javax.microedition.khronos.egl.EGL10; -/** GL utility methods. */ +/** GL utilities. */ public final class GlUtil { + + /** + * GL attribute, which can be attached to a buffer with {@link Attribute#setBuffer(float[], int)}. + */ + public static final class Attribute { + + /** The name of the attribute in the GLSL sources. */ + public final String name; + + private final int index; + private final int location; + + @Nullable private Buffer buffer; + private int size; + + /** + * Creates a new GL attribute. + * + * @param program The identifier of a compiled and linked GLSL shader program. + * @param index The index of the attribute. After this instance has been constructed, the name + * of the attribute is available via the {@link #name} field. + */ + public Attribute(int program, int index) { + int[] len = new int[1]; + GLES20.glGetProgramiv(program, GLES20.GL_ACTIVE_ATTRIBUTE_MAX_LENGTH, len, 0); + + int[] type = new int[1]; + int[] size = new int[1]; + byte[] nameBytes = new byte[len[0]]; + int[] ignore = new int[1]; + + GLES20.glGetActiveAttrib(program, index, len[0], ignore, 0, size, 0, type, 0, nameBytes, 0); + name = new String(nameBytes, 0, strlen(nameBytes)); + location = GLES20.glGetAttribLocation(program, name); + this.index = index; + } + + /** + * Configures {@link #bind()} to attach vertices in {@code buffer} (each of size {@code size} + * elements) to this {@link Attribute}. + * + * @param buffer Buffer to bind to this attribute. + * @param size Number of elements per vertex. + */ + public void setBuffer(float[] buffer, int size) { + this.buffer = createBuffer(buffer); + this.size = size; + } + + /** + * Sets the vertex attribute to whatever was attached via {@link #setBuffer(float[], int)}. + * + *

        Should be called before each drawing call. + */ + public void bind() { + Buffer buffer = Assertions.checkNotNull(this.buffer, "call setBuffer before bind"); + GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, 0); + GLES20.glVertexAttribPointer( + location, + size, // count + GLES20.GL_FLOAT, // type + false, // normalize + 0, // stride + buffer); + GLES20.glEnableVertexAttribArray(index); + checkGlError(); + } + } + + /** + * GL uniform, which can be attached to a sampler using {@link Uniform#setSamplerTexId(int, int)}. + */ + public static final class Uniform { + + /** The name of the uniform in the GLSL sources. */ + public final String name; + + private final int location; + private final int type; + private final float[] value; + + private int texId; + private int unit; + + /** + * Creates a new GL uniform. + * + * @param program The identifier of a compiled and linked GLSL shader program. + * @param index The index of the uniform. After this instance has been constructed, the name of + * the uniform is available via the {@link #name} field. + */ + public Uniform(int program, int index) { + int[] len = new int[1]; + GLES20.glGetProgramiv(program, GLES20.GL_ACTIVE_UNIFORM_MAX_LENGTH, len, 0); + + int[] type = new int[1]; + int[] size = new int[1]; + byte[] name = new byte[len[0]]; + int[] ignore = new int[1]; + + GLES20.glGetActiveUniform(program, index, len[0], ignore, 0, size, 0, type, 0, name, 0); + this.name = new String(name, 0, strlen(name)); + location = GLES20.glGetUniformLocation(program, this.name); + this.type = type[0]; + + value = new float[1]; + } + + /** + * Configures {@link #bind()} to use the specified {@code texId} for this sampler uniform. + * + * @param texId The GL texture identifier from which to sample. + * @param unit The GL texture unit index. + */ + public void setSamplerTexId(int texId, int unit) { + this.texId = texId; + this.unit = unit; + } + + /** Configures {@link #bind()} to use the specified float {@code value} for this uniform. */ + public void setFloat(float value) { + this.value[0] = value; + } + + /** + * Sets the uniform to whatever value was passed via {@link #setSamplerTexId(int, int)} or + * {@link #setFloat(float)}. + * + *

        Should be called before each drawing call. + */ + public void bind() { + if (type == GLES20.GL_FLOAT) { + GLES20.glUniform1fv(location, 1, value, 0); + checkGlError(); + return; + } + + if (texId == 0) { + throw new IllegalStateException("call setSamplerTexId before bind"); + } + GLES20.glActiveTexture(GLES20.GL_TEXTURE0 + unit); + if (type == GLES11Ext.GL_SAMPLER_EXTERNAL_OES) { + GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, texId); + } else if (type == GLES20.GL_SAMPLER_2D) { + GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, texId); + } else { + throw new IllegalStateException("unexpected uniform type: " + type); + } + GLES20.glUniform1i(location, unit); + GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR); + GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR); + GLES20.glTexParameteri( + GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE); + GLES20.glTexParameteri( + GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE); + checkGlError(); + } + } + private static final String TAG = "GlUtil"; + private static final String EXTENSION_PROTECTED_CONTENT = "EGL_EXT_protected_content"; + private static final String EXTENSION_SURFACELESS_CONTEXT = "EGL_KHR_surfaceless_context"; + /** Class only contains static methods. */ private GlUtil() {} + /** + * Returns whether creating a GL context with {@value EXTENSION_PROTECTED_CONTENT} is possible. If + * {@code true}, the device supports a protected output path for DRM content when using GL. + */ + public static boolean isProtectedContentExtensionSupported(Context context) { + if (Util.SDK_INT < 24) { + return false; + } + if (Util.SDK_INT < 26 && ("samsung".equals(Util.MANUFACTURER) || "XT1650".equals(Util.MODEL))) { + // Samsung devices running Nougat are known to be broken. See + // https://github.com/google/ExoPlayer/issues/3373 and [Internal: b/37197802]. + // Moto Z XT1650 is also affected. See + // https://github.com/google/ExoPlayer/issues/3215. + return false; + } + if (Util.SDK_INT < 26 + && !context + .getPackageManager() + .hasSystemFeature(PackageManager.FEATURE_VR_MODE_HIGH_PERFORMANCE)) { + // Pre API level 26 devices were not well tested unless they supported VR mode. + return false; + } + + EGLDisplay display = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY); + @Nullable String eglExtensions = EGL14.eglQueryString(display, EGL10.EGL_EXTENSIONS); + return eglExtensions != null && eglExtensions.contains(EXTENSION_PROTECTED_CONTENT); + } + + /** + * Returns whether creating a GL context with {@value EXTENSION_SURFACELESS_CONTEXT} is possible. + */ + public static boolean isSurfacelessContextExtensionSupported() { + if (Util.SDK_INT < 17) { + return false; + } + EGLDisplay display = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY); + @Nullable String eglExtensions = EGL14.eglQueryString(display, EGL10.EGL_EXTENSIONS); + return eglExtensions != null && eglExtensions.contains(EXTENSION_SURFACELESS_CONTEXT); + } + /** * If there is an OpenGl error, logs the error and if {@link * ExoPlayerLibraryInfo#GL_ASSERTIONS_ENABLED} is true throws a {@link RuntimeException}. @@ -90,6 +299,34 @@ public final class GlUtil { return program; } + /** Returns the {@link Attribute}s in the specified {@code program}. */ + public static Attribute[] getAttributes(int program) { + int[] attributeCount = new int[1]; + GLES20.glGetProgramiv(program, GLES20.GL_ACTIVE_ATTRIBUTES, attributeCount, 0); + if (attributeCount[0] != 2) { + throw new IllegalStateException("expected two attributes"); + } + + Attribute[] attributes = new Attribute[attributeCount[0]]; + for (int i = 0; i < attributeCount[0]; i++) { + attributes[i] = new Attribute(program, i); + } + return attributes; + } + + /** Returns the {@link Uniform}s in the specified {@code program}. */ + public static Uniform[] getUniforms(int program) { + int[] uniformCount = new int[1]; + GLES20.glGetProgramiv(program, GLES20.GL_ACTIVE_UNIFORMS, uniformCount, 0); + + Uniform[] uniforms = new Uniform[uniformCount[0]]; + for (int i = 0; i < uniformCount[0]; i++) { + uniforms[i] = new Uniform(program, i); + } + + return uniforms; + } + /** * Allocates a FloatBuffer with the given data. * @@ -151,4 +388,14 @@ public final class GlUtil { throw new RuntimeException(errorMsg); } } + + /** Returns the length of the null-terminated string in {@code strVal}. */ + private static int strlen(byte[] strVal) { + for (int i = 0; i < strVal.length; ++i) { + if (strVal[i] == '\0') { + return i; + } + } + return strVal.length; + } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/IntArrayQueue.java b/library/core/src/main/java/com/google/android/exoplayer2/util/IntArrayQueue.java index d6eb1ca35a..3277d042ed 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/IntArrayQueue.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/IntArrayQueue.java @@ -56,7 +56,7 @@ public final class IntArrayQueue { /** * Remove an item from the queue. * - * @throws {@link NoSuchElementException} if the queue is empty. + * @throws NoSuchElementException if the queue is empty. */ public int remove() { if (size == 0) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/MediaClock.java b/library/core/src/main/java/com/google/android/exoplayer2/util/MediaClock.java index e9f08a35c9..44c3c5e7fa 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/MediaClock.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/MediaClock.java @@ -15,8 +15,6 @@ */ package com.google.android.exoplayer2.util; -import com.google.android.exoplayer2.PlaybackParameters; - /** * Tracks the progression of media time. */ @@ -28,16 +26,13 @@ public interface MediaClock { long getPositionUs(); /** - * Attempts to set the playback parameters. The media clock may override these parameters if they - * are not supported. + * Attempts to set the playback speed. The media clock may override the speed if changing the + * speed is not supported. * - * @param playbackParameters The playback parameters to attempt to set. + * @param playbackSpeed The playback speed to attempt to set. */ - void setPlaybackParameters(PlaybackParameters playbackParameters); - - /** - * Returns the active playback parameters. - */ - PlaybackParameters getPlaybackParameters(); + void setPlaybackSpeed(float playbackSpeed); + /** Returns the active playback speed. */ + float getPlaybackSpeed(); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/MediaSourceEventDispatcher.java b/library/core/src/main/java/com/google/android/exoplayer2/util/MediaSourceEventDispatcher.java new file mode 100644 index 0000000000..c58221a12c --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/MediaSourceEventDispatcher.java @@ -0,0 +1,203 @@ +/* + * Copyright (C) 2020 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 android.os.Looper; +import androidx.annotation.CheckResult; +import androidx.annotation.Nullable; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; + +/** + * Event dispatcher which forwards events to a list of registered listeners. + * + *

        Adds the correct {@code windowIndex} and {@code mediaPeriodId} values (and {@code + * mediaTimeOffsetMs} if needed). + * + *

        Allows listeners of any type to be registered, calls to {@link #dispatch} then provide the + * type of listener to forward to, which is used to filter the registered listeners. + */ +// TODO: Make this final when MediaSourceEventListener.EventDispatcher is deleted. +public class MediaSourceEventDispatcher { + + /** + * Functional interface to send an event with {@code windowIndex} and {@code mediaPeriodId} + * attached. + */ + public interface EventWithPeriodId { + + /** Sends the event to a listener. */ + void sendTo(T listener, int windowIndex, @Nullable MediaPeriodId mediaPeriodId); + } + + /** The timeline window index reported with the events. */ + public final int windowIndex; + /** The {@link MediaPeriodId} reported with the events. */ + @Nullable public final MediaPeriodId mediaPeriodId; + + // TODO: Make these private when MediaSourceEventListener.EventDispatcher is deleted. + protected final CopyOnWriteMultiset listenerInfos; + // TODO: Define exactly what this means, and check it's always set correctly. + protected final long mediaTimeOffsetMs; + + /** Creates an event dispatcher. */ + public MediaSourceEventDispatcher() { + this( + /* listenerInfos= */ new CopyOnWriteMultiset<>(), + /* windowIndex= */ 0, + /* mediaPeriodId= */ null, + /* mediaTimeOffsetMs= */ 0); + } + + protected MediaSourceEventDispatcher( + CopyOnWriteMultiset listenerInfos, + int windowIndex, + @Nullable MediaPeriodId mediaPeriodId, + long mediaTimeOffsetMs) { + this.listenerInfos = listenerInfos; + this.windowIndex = windowIndex; + this.mediaPeriodId = mediaPeriodId; + this.mediaTimeOffsetMs = mediaTimeOffsetMs; + } + + /** + * Creates a view of the event dispatcher with pre-configured window index, media period id, and + * media time offset. + * + * @param windowIndex The timeline window index to be reported with the events. + * @param mediaPeriodId The {@link MediaPeriodId} to be reported with the events. + * @param mediaTimeOffsetMs The offset to be added to all media times, in milliseconds. + * @return A view of the event dispatcher with the pre-configured parameters. + */ + @CheckResult + public MediaSourceEventDispatcher withParameters( + int windowIndex, @Nullable MediaPeriodId mediaPeriodId, long mediaTimeOffsetMs) { + return new MediaSourceEventDispatcher( + listenerInfos, windowIndex, mediaPeriodId, mediaTimeOffsetMs); + } + + /** + * Adds a listener to the event dispatcher. + * + *

        Calls to {@link #dispatch(EventWithPeriodId, Class)} will propagate to {@code eventListener} + * if the {@code listenerClass} types are equal. + * + *

        The same listener instance can be added multiple times with different {@code listenerClass} + * values (i.e. if the instance implements multiple listener interfaces). + * + *

        Duplicate {@code {eventListener, listenerClass}} pairs are also permitted. In this case an + * event dispatched to {@code listenerClass} will only be passed to the {@code eventListener} + * once. + * + *

        NOTE: This doesn't interact well with hierarchies of listener interfaces. If a + * listener is registered with a super-class type then it will only receive events dispatched + * directly to that super-class type. Similarly, if a listener is registered with a sub-class type + * then it will only receive events dispatched directly to that sub-class. + * + * @param handler A handler on the which listener events will be posted. + * @param eventListener The listener to be added. + * @param listenerClass The type used to register the listener. Can be a superclass of {@code + * eventListener}. + */ + public void addEventListener(Handler handler, T eventListener, Class listenerClass) { + Assertions.checkNotNull(handler); + Assertions.checkNotNull(eventListener); + listenerInfos.add(new ListenerInfo(handler, eventListener, listenerClass)); + } + + /** + * Removes a listener from the event dispatcher. + * + *

        If there are duplicate registrations of {@code {eventListener, listenerClass}} this will + * only remove one (so events dispatched to {@code listenerClass} will still be passed to {@code + * eventListener}). + * + * @param eventListener The listener to be removed. + * @param listenerClass The listener type passed to {@link #addEventListener(Handler, Object, + * Class)}. + */ + public void removeEventListener(T eventListener, Class listenerClass) { + for (ListenerInfo listenerInfo : listenerInfos) { + if (listenerInfo.listener == eventListener + && listenerInfo.listenerClass.equals(listenerClass)) { + listenerInfos.remove(listenerInfo); + return; + } + } + } + + /** Dispatches {@code event} to all registered listeners of type {@code listenerClass}. */ + @SuppressWarnings("unchecked") // The cast is gated with listenerClass.isInstance() + public void dispatch(EventWithPeriodId event, Class listenerClass) { + for (ListenerInfo listenerInfo : listenerInfos.elementSet()) { + if (listenerInfo.listenerClass.equals(listenerClass)) { + postOrRun( + listenerInfo.handler, + () -> event.sendTo((T) listenerInfo.listener, windowIndex, mediaPeriodId)); + } + } + } + + private static void postOrRun(Handler handler, Runnable runnable) { + if (handler.getLooper() == Looper.myLooper()) { + runnable.run(); + } else { + handler.post(runnable); + } + } + + public static long adjustMediaTime(long mediaTimeUs, long mediaTimeOffsetMs) { + long mediaTimeMs = C.usToMs(mediaTimeUs); + return mediaTimeMs == C.TIME_UNSET ? C.TIME_UNSET : mediaTimeOffsetMs + mediaTimeMs; + } + + /** Container class for a {@link Handler}, {@code listener} and {@code listenerClass}. */ + protected static final class ListenerInfo { + + public final Handler handler; + public final Object listener; + public final Class listenerClass; + + public ListenerInfo(Handler handler, Object listener, Class listenerClass) { + this.handler = handler; + this.listener = listener; + this.listenerClass = listenerClass; + } + + @Override + public boolean equals(@Nullable Object o) { + if (this == o) { + return true; + } + if (!(o instanceof ListenerInfo)) { + return false; + } + + ListenerInfo that = (ListenerInfo) o; + + // We deliberately only consider listener and listenerClass (and not handler) in equals() and + // hashcode() because the handler used to process the callbacks is an implementation detail. + return listener.equals(that.listener) && listenerClass.equals(that.listenerClass); + } + + @Override + public int hashCode() { + int result = 31 * listener.hashCode(); + return result + 31 * listenerClass.hashCode(); + } + } +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/SntpClient.java b/library/core/src/main/java/com/google/android/exoplayer2/util/SntpClient.java new file mode 100644 index 0000000000..fa2edf253d --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/SntpClient.java @@ -0,0 +1,310 @@ +/* + * Copyright (C) 2019 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.SystemClock; +import androidx.annotation.GuardedBy; +import androidx.annotation.Nullable; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.upstream.Loader; +import com.google.android.exoplayer2.upstream.Loader.LoadErrorAction; +import com.google.android.exoplayer2.upstream.Loader.Loadable; +import java.io.IOException; +import java.net.DatagramPacket; +import java.net.DatagramSocket; +import java.net.InetAddress; +import java.util.Arrays; + +/** + * Static utility to retrieve the device time offset using SNTP. + * + *

        Based on the Android + * framework SntpClient. + */ +public final class SntpClient { + + /** Callback for calls to {@link #initialize(Loader, InitializationCallback)}. */ + public interface InitializationCallback { + + /** Called when the device time offset has been initialized. */ + void onInitialized(); + + /** + * Called when the device time offset failed to initialize. + * + * @param error The error that caused the initialization failure. + */ + void onInitializationFailed(IOException error); + } + + private static final String NTP_HOST = "pool.ntp.org"; + private static final int TIMEOUT_MS = 10_000; + + private static final int ORIGINATE_TIME_OFFSET = 24; + private static final int RECEIVE_TIME_OFFSET = 32; + private static final int TRANSMIT_TIME_OFFSET = 40; + private static final int NTP_PACKET_SIZE = 48; + + private static final int NTP_PORT = 123; + private static final int NTP_MODE_CLIENT = 3; + private static final int NTP_MODE_SERVER = 4; + private static final int NTP_MODE_BROADCAST = 5; + private static final int NTP_VERSION = 3; + + private static final int NTP_LEAP_NOSYNC = 3; + private static final int NTP_STRATUM_DEATH = 0; + private static final int NTP_STRATUM_MAX = 15; + + private static final long OFFSET_1900_TO_1970 = ((365L * 70L) + 17L) * 24L * 60L * 60L; + + private static final Object loaderLock = new Object(); + private static final Object valueLock = new Object(); + + @GuardedBy("valueLock") + private static boolean isInitialized; + + @GuardedBy("valueLock") + private static long elapsedRealtimeOffsetMs; + + private SntpClient() {} + + /** + * Returns whether the device time offset has already been loaded. + * + *

        If {@code false}, use {@link #initialize(Loader, InitializationCallback)} to start the + * initialization. + */ + public static boolean isInitialized() { + synchronized (valueLock) { + return isInitialized; + } + } + + /** + * Returns the offset between {@link SystemClock#elapsedRealtime()} and the NTP server time in + * milliseconds, or {@link C#TIME_UNSET} if {@link #isInitialized()} returns false. + * + *

        The offset is calculated as {@code ntpServerTime - deviceElapsedRealTime}. + */ + public static long getElapsedRealtimeOffsetMs() { + synchronized (valueLock) { + return isInitialized ? elapsedRealtimeOffsetMs : C.TIME_UNSET; + } + } + + /** + * Starts loading the device time offset. + * + * @param loader A {@link Loader} to use for loading the time offset, or null to create a new one. + * @param callback An optional {@link InitializationCallback} to be notified when the time offset + * has been initialized or initialization failed. + */ + public static void initialize( + @Nullable Loader loader, @Nullable InitializationCallback callback) { + if (isInitialized()) { + if (callback != null) { + callback.onInitialized(); + } + return; + } + if (loader == null) { + loader = new Loader("SntpClient"); + } + loader.startLoading( + new NtpTimeLoadable(), new NtpTimeCallback(callback), /* defaultMinRetryCount= */ 1); + } + + private static long loadNtpTimeOffsetMs() throws IOException { + InetAddress address = InetAddress.getByName(NTP_HOST); + try (DatagramSocket socket = new DatagramSocket()) { + socket.setSoTimeout(TIMEOUT_MS); + byte[] buffer = new byte[NTP_PACKET_SIZE]; + DatagramPacket request = new DatagramPacket(buffer, buffer.length, address, NTP_PORT); + + // Set mode = 3 (client) and version = 3. Mode is in low 3 bits of the first byte and Version + // is in bits 3-5 of the first byte. + buffer[0] = NTP_MODE_CLIENT | (NTP_VERSION << 3); + + // Get current time and write it to the request packet. + long requestTime = System.currentTimeMillis(); + long requestTicks = SystemClock.elapsedRealtime(); + writeTimestamp(buffer, TRANSMIT_TIME_OFFSET, requestTime); + + socket.send(request); + + // Read the response. + DatagramPacket response = new DatagramPacket(buffer, buffer.length); + socket.receive(response); + final long responseTicks = SystemClock.elapsedRealtime(); + final long responseTime = requestTime + (responseTicks - requestTicks); + + // Extract the results. + final byte leap = (byte) ((buffer[0] >> 6) & 0x3); + final byte mode = (byte) (buffer[0] & 0x7); + final int stratum = (int) (buffer[1] & 0xff); + final long originateTime = readTimestamp(buffer, ORIGINATE_TIME_OFFSET); + final long receiveTime = readTimestamp(buffer, RECEIVE_TIME_OFFSET); + final long transmitTime = readTimestamp(buffer, TRANSMIT_TIME_OFFSET); + + // Do sanity check according to RFC. + checkValidServerReply(leap, mode, stratum, transmitTime); + + // receiveTime = originateTime + transit + skew + // responseTime = transmitTime + transit - skew + // clockOffset = ((receiveTime - originateTime) + (transmitTime - responseTime))/2 + // = ((originateTime + transit + skew - originateTime) + + // (transmitTime - (transmitTime + transit - skew)))/2 + // = ((transit + skew) + (transmitTime - transmitTime - transit + skew))/2 + // = (transit + skew - transit + skew)/2 + // = (2 * skew)/2 = skew + long clockOffset = ((receiveTime - originateTime) + (transmitTime - responseTime)) / 2; + + // Save our results using the times on this side of the network latency (i.e. response rather + // than request time) + long ntpTime = responseTime + clockOffset; + long ntpTimeReference = responseTicks; + + return ntpTime - ntpTimeReference; + } + } + + private static long readTimestamp(byte[] buffer, int offset) { + long seconds = read32(buffer, offset); + long fraction = read32(buffer, offset + 4); + // Special case: zero means zero. + if (seconds == 0 && fraction == 0) { + return 0; + } + return ((seconds - OFFSET_1900_TO_1970) * 1000) + ((fraction * 1000L) / 0x100000000L); + } + + private static void writeTimestamp(byte[] buffer, int offset, long time) { + // Special case: zero means zero. + if (time == 0) { + Arrays.fill(buffer, offset, offset + 8, (byte) 0x00); + return; + } + + long seconds = time / 1000L; + long milliseconds = time - seconds * 1000L; + seconds += OFFSET_1900_TO_1970; + + // Write seconds in big endian format. + buffer[offset++] = (byte) (seconds >> 24); + buffer[offset++] = (byte) (seconds >> 16); + buffer[offset++] = (byte) (seconds >> 8); + buffer[offset++] = (byte) (seconds >> 0); + + long fraction = milliseconds * 0x100000000L / 1000L; + // Write fraction in big endian format. + buffer[offset++] = (byte) (fraction >> 24); + buffer[offset++] = (byte) (fraction >> 16); + buffer[offset++] = (byte) (fraction >> 8); + // Low order bits should be random data. + buffer[offset++] = (byte) (Math.random() * 255.0); + } + + private static long read32(byte[] buffer, int offset) { + byte b0 = buffer[offset]; + byte b1 = buffer[offset + 1]; + byte b2 = buffer[offset + 2]; + byte b3 = buffer[offset + 3]; + + // Convert signed bytes to unsigned values. + int i0 = ((b0 & 0x80) == 0x80 ? (b0 & 0x7F) + 0x80 : b0); + int i1 = ((b1 & 0x80) == 0x80 ? (b1 & 0x7F) + 0x80 : b1); + int i2 = ((b2 & 0x80) == 0x80 ? (b2 & 0x7F) + 0x80 : b2); + int i3 = ((b3 & 0x80) == 0x80 ? (b3 & 0x7F) + 0x80 : b3); + + return ((long) i0 << 24) + ((long) i1 << 16) + ((long) i2 << 8) + (long) i3; + } + + private static void checkValidServerReply(byte leap, byte mode, int stratum, long transmitTime) + throws IOException { + if (leap == NTP_LEAP_NOSYNC) { + throw new IOException("SNTP: Unsynchronized server"); + } + if ((mode != NTP_MODE_SERVER) && (mode != NTP_MODE_BROADCAST)) { + throw new IOException("SNTP: Untrusted mode: " + mode); + } + if ((stratum == NTP_STRATUM_DEATH) || (stratum > NTP_STRATUM_MAX)) { + throw new IOException("SNTP: Untrusted stratum: " + stratum); + } + if (transmitTime == 0) { + throw new IOException("SNTP: Zero transmitTime"); + } + } + + private static final class NtpTimeLoadable implements Loadable { + + @Override + public void cancelLoad() {} + + @Override + public void load() throws IOException { + // Synchronized to prevent redundant parallel requests. + synchronized (loaderLock) { + synchronized (valueLock) { + if (isInitialized) { + return; + } + } + long offsetMs = loadNtpTimeOffsetMs(); + synchronized (valueLock) { + elapsedRealtimeOffsetMs = offsetMs; + isInitialized = true; + } + } + } + } + + private static final class NtpTimeCallback implements Loader.Callback { + + @Nullable private final InitializationCallback callback; + + public NtpTimeCallback(@Nullable InitializationCallback callback) { + this.callback = callback; + } + + @Override + public void onLoadCompleted(Loadable loadable, long elapsedRealtimeMs, long loadDurationMs) { + Assertions.checkState(SntpClient.isInitialized()); + if (callback != null) { + callback.onInitialized(); + } + } + + @Override + public void onLoadCanceled( + Loadable loadable, long elapsedRealtimeMs, long loadDurationMs, boolean released) { + // Ignore. + } + + @Override + public LoadErrorAction onLoadError( + Loadable loadable, + long elapsedRealtimeMs, + long loadDurationMs, + IOException error, + int errorCount) { + if (callback != null) { + callback.onInitializationFailed(error); + } + return Loader.DONT_RETRY; + } + } +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/StandaloneMediaClock.java b/library/core/src/main/java/com/google/android/exoplayer2/util/StandaloneMediaClock.java index e5f9aa645f..e1df77a200 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/StandaloneMediaClock.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/StandaloneMediaClock.java @@ -16,7 +16,7 @@ package com.google.android.exoplayer2.util; import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.PlaybackParameters; +import com.google.android.exoplayer2.Player; /** * A {@link MediaClock} whose position advances with real time based on the playback parameters when @@ -29,7 +29,8 @@ public final class StandaloneMediaClock implements MediaClock { private boolean started; private long baseUs; private long baseElapsedMs; - private PlaybackParameters playbackParameters; + private float playbackSpeed; + private int scaledUsPerMs; /** * Creates a new standalone media clock using the given {@link Clock} implementation. @@ -38,7 +39,8 @@ public final class StandaloneMediaClock implements MediaClock { */ public StandaloneMediaClock(Clock clock) { this.clock = clock; - this.playbackParameters = PlaybackParameters.DEFAULT; + playbackSpeed = Player.DEFAULT_PLAYBACK_SPEED; + scaledUsPerMs = getScaledUsPerMs(playbackSpeed); } /** @@ -78,27 +80,33 @@ public final class StandaloneMediaClock implements MediaClock { long positionUs = baseUs; if (started) { long elapsedSinceBaseMs = clock.elapsedRealtime() - baseElapsedMs; - if (playbackParameters.speed == 1f) { + if (playbackSpeed == 1f) { positionUs += C.msToUs(elapsedSinceBaseMs); } else { - positionUs += playbackParameters.getMediaTimeUsForPlayoutTimeMs(elapsedSinceBaseMs); + // Add the media time in microseconds that will elapse in elapsedSinceBaseMs milliseconds of + // wallclock time + positionUs += elapsedSinceBaseMs * scaledUsPerMs; } } return positionUs; } @Override - public void setPlaybackParameters(PlaybackParameters playbackParameters) { + public void setPlaybackSpeed(float playbackSpeed) { // Store the current position as the new base, in case the playback speed has changed. if (started) { resetPosition(getPositionUs()); } - this.playbackParameters = playbackParameters; + this.playbackSpeed = playbackSpeed; + scaledUsPerMs = getScaledUsPerMs(playbackSpeed); } @Override - public PlaybackParameters getPlaybackParameters() { - return playbackParameters; + public float getPlaybackSpeed() { + return playbackSpeed; } + private static int getScaledUsPerMs(float playbackSpeed) { + return Math.round(playbackSpeed * 1000f); + } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/SystemClock.java b/library/core/src/main/java/com/google/android/exoplayer2/util/SystemClock.java index be526595c6..89e1c60d7a 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/SystemClock.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/SystemClock.java @@ -21,9 +21,17 @@ import android.os.Looper; import androidx.annotation.Nullable; /** - * The standard implementation of {@link Clock}. + * The standard implementation of {@link Clock}, an instance of which is available via {@link + * SystemClock#DEFAULT}. */ -/* package */ final class SystemClock implements Clock { +public class SystemClock implements Clock { + + protected SystemClock() {} + + @Override + public long currentTimeMillis() { + return System.currentTimeMillis(); + } @Override public long elapsedRealtime() { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/SimpleDecoderVideoRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/video/DecoderVideoRenderer.java similarity index 84% rename from library/core/src/main/java/com/google/android/exoplayer2/video/SimpleDecoderVideoRenderer.java rename to library/core/src/main/java/com/google/android/exoplayer2/video/DecoderVideoRenderer.java index 73c964d1fe..23cae05a03 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/SimpleDecoderVideoRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/DecoderVideoRenderer.java @@ -24,16 +24,18 @@ import androidx.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.ExoPlayer; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.FormatHolder; -import com.google.android.exoplayer2.RendererCapabilities; +import com.google.android.exoplayer2.PlayerMessage.Target; +import com.google.android.exoplayer2.decoder.Decoder; import com.google.android.exoplayer2.decoder.DecoderCounters; +import com.google.android.exoplayer2.decoder.DecoderException; import com.google.android.exoplayer2.decoder.DecoderInputBuffer; -import com.google.android.exoplayer2.decoder.SimpleDecoder; import com.google.android.exoplayer2.drm.DrmSession; import com.google.android.exoplayer2.drm.DrmSession.DrmSessionException; -import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.drm.ExoMediaCrypto; +import com.google.android.exoplayer2.source.SampleStream; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.TimedValueQueue; import com.google.android.exoplayer2.util.TraceUtil; @@ -42,8 +44,24 @@ import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; -/** Decodes and renders video using a {@link SimpleDecoder}. */ -public abstract class SimpleDecoderVideoRenderer extends BaseRenderer { +/** + * Decodes and renders video using a {@link Decoder}. + * + *

        This renderer accepts the following messages sent via {@link ExoPlayer#createMessage(Target)} + * on the playback thread: + * + *

          + *
        • Message with type {@link #MSG_SET_SURFACE} to set the output surface. The message payload + * should be the target {@link Surface}, or null. + *
        • Message with type {@link #MSG_SET_VIDEO_DECODER_OUTPUT_BUFFER_RENDERER} to set the output + * buffer renderer. The message payload should be the target {@link + * VideoDecoderOutputBufferRenderer}, or null. + *
        • Message with type {@link #MSG_SET_VIDEO_FRAME_METADATA_LISTENER} to set a listener for + * metadata associated with frames being rendered. The message payload should be the {@link + * VideoFrameMetadataListener}, or null. + *
        + */ +public abstract class DecoderVideoRenderer extends BaseRenderer { /** Decoder reinitialization states. */ @Documented @@ -71,32 +89,31 @@ public abstract class SimpleDecoderVideoRenderer extends BaseRenderer { private final long allowedJoiningTimeMs; private final int maxDroppedFramesToNotify; - private final boolean playClearSamplesWithoutKeys; private final EventDispatcher eventDispatcher; private final TimedValueQueue formatQueue; private final DecoderInputBuffer flagsOnlyBuffer; - private final DrmSessionManager drmSessionManager; private Format inputFormat; private Format outputFormat; - private SimpleDecoder< - VideoDecoderInputBuffer, - ? extends VideoDecoderOutputBuffer, - ? extends VideoDecoderException> + private Decoder< + VideoDecoderInputBuffer, ? extends VideoDecoderOutputBuffer, ? extends DecoderException> decoder; private VideoDecoderInputBuffer inputBuffer; private VideoDecoderOutputBuffer outputBuffer; @Nullable private Surface surface; @Nullable private VideoDecoderOutputBufferRenderer outputBufferRenderer; + @Nullable private VideoFrameMetadataListener frameMetadataListener; @C.VideoOutputMode private int outputMode; - @Nullable private DrmSession decoderDrmSession; - @Nullable private DrmSession sourceDrmSession; + @Nullable private DrmSession decoderDrmSession; + @Nullable private DrmSession sourceDrmSession; @ReinitializationState private int decoderReinitializationState; private boolean decoderReceivedBuffers; - private boolean renderedFirstFrame; + private boolean renderedFirstFrameAfterReset; + private boolean mayRenderFirstFrameAfterEnableIfNotStarted; + private boolean renderedFirstFrameAfterEnable; private long initialPositionUs; private long joiningDeadlineMs; private boolean waitingForKeys; @@ -125,26 +142,15 @@ public abstract class SimpleDecoderVideoRenderer extends BaseRenderer { * @param eventListener A listener of events. May be null if delivery of events is not required. * @param maxDroppedFramesToNotify The maximum number of frames that can be dropped between * invocations of {@link VideoRendererEventListener#onDroppedFrames(int, long)}. - * @param drmSessionManager For use with encrypted media. May be null if support for encrypted - * media is not required. - * @param playClearSamplesWithoutKeys Encrypted media may contain clear (un-encrypted) regions. - * For example a media file may start with a short clear region so as to allow playback to - * 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. */ - protected SimpleDecoderVideoRenderer( + protected DecoderVideoRenderer( long allowedJoiningTimeMs, @Nullable Handler eventHandler, @Nullable VideoRendererEventListener eventListener, - int maxDroppedFramesToNotify, - @Nullable DrmSessionManager drmSessionManager, - boolean playClearSamplesWithoutKeys) { + int maxDroppedFramesToNotify) { super(C.TRACK_TYPE_VIDEO); this.allowedJoiningTimeMs = allowedJoiningTimeMs; this.maxDroppedFramesToNotify = maxDroppedFramesToNotify; - this.drmSessionManager = drmSessionManager; - this.playClearSamplesWithoutKeys = playClearSamplesWithoutKeys; joiningDeadlineMs = C.TIME_UNSET; clearReportedVideoSize(); formatQueue = new TimedValueQueue<>(); @@ -156,11 +162,6 @@ public abstract class SimpleDecoderVideoRenderer extends BaseRenderer { // BaseRenderer implementation. - @Override - public final int supportsFormat(Format format) { - return supportsFormatInternal(drmSessionManager, format); - } - @Override public void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException { if (outputStreamEnded) { @@ -171,7 +172,7 @@ public abstract class SimpleDecoderVideoRenderer extends BaseRenderer { // We don't have a format yet, so try and read one. FormatHolder formatHolder = getFormatHolder(); flagsOnlyBuffer.clear(); - int result = readSource(formatHolder, flagsOnlyBuffer, true); + @SampleStream.ReadDataResult int result = readSource(formatHolder, flagsOnlyBuffer, true); if (result == C.RESULT_FORMAT_READ) { onInputFormatChanged(formatHolder); } else if (result == C.RESULT_BUFFER_READ) { @@ -196,8 +197,8 @@ public abstract class SimpleDecoderVideoRenderer extends BaseRenderer { while (drainOutputBuffer(positionUs, elapsedRealtimeUs)) {} while (feedInputBuffer()) {} TraceUtil.endSection(); - } catch (VideoDecoderException e) { - throw ExoPlaybackException.createForRenderer(e, getIndex()); + } catch (DecoderException e) { + throw createRendererException(e, inputFormat); } decoderCounters.ensureUpdated(); } @@ -215,7 +216,7 @@ public abstract class SimpleDecoderVideoRenderer extends BaseRenderer { } if (inputFormat != null && (isSourceReady() || outputBuffer != null) - && (renderedFirstFrame || !hasOutput())) { + && (renderedFirstFrameAfterReset || !hasOutput())) { // Ready. If we were joining then we've now joined, so clear the joining deadline. joiningDeadlineMs = C.TIME_UNSET; return true; @@ -232,12 +233,30 @@ public abstract class SimpleDecoderVideoRenderer extends BaseRenderer { } } + // PlayerMessage.Target implementation. + + @Override + public void handleMessage(int messageType, @Nullable Object message) throws ExoPlaybackException { + if (messageType == MSG_SET_SURFACE) { + setOutputSurface((Surface) message); + } else if (messageType == MSG_SET_VIDEO_DECODER_OUTPUT_BUFFER_RENDERER) { + setOutputBufferRenderer((VideoDecoderOutputBufferRenderer) message); + } else if (messageType == MSG_SET_VIDEO_FRAME_METADATA_LISTENER) { + frameMetadataListener = (VideoFrameMetadataListener) message; + } else { + super.handleMessage(messageType, message); + } + } + // Protected methods. @Override - protected void onEnabled(boolean joining) throws ExoPlaybackException { + protected void onEnabled(boolean joining, boolean mayRenderStartOfStream) + throws ExoPlaybackException { decoderCounters = new DecoderCounters(); eventDispatcher.enabled(decoderCounters); + mayRenderFirstFrameAfterEnableIfNotStarted = mayRenderStartOfStream; + renderedFirstFrameAfterEnable = false; } @Override @@ -287,6 +306,9 @@ public abstract class SimpleDecoderVideoRenderer extends BaseRenderer { @Override protected void onStreamChanged(Format[] formats, long offsetUs) throws ExoPlaybackException { + // TODO: This shouldn't just update the output stream offset as long as there are still buffers + // of the previous stream in the decoder. It should also make sure to render the first frame of + // the next stream if the playback position reached the new stream. outputStreamOffsetUs = offsetUs; super.onStreamChanged(formats, offsetUs); } @@ -353,19 +375,16 @@ public abstract class SimpleDecoderVideoRenderer extends BaseRenderer { * @throws ExoPlaybackException If an error occurs (re-)initializing the decoder. */ @CallSuper - @SuppressWarnings("unchecked") protected void onInputFormatChanged(FormatHolder formatHolder) throws ExoPlaybackException { waitingForFirstSampleInFormat = true; Format newFormat = Assertions.checkNotNull(formatHolder.format); - if (formatHolder.includesDrmSession) { - setSourceDrmSession((DrmSession) formatHolder.drmSession); - } else { - sourceDrmSession = - getUpdatedSourceDrmSession(inputFormat, newFormat, drmSessionManager, sourceDrmSession); - } + setSourceDrmSession(formatHolder.drmSession); + Format oldFormat = inputFormat; inputFormat = newFormat; - if (sourceDrmSession != decoderDrmSession) { + if (decoder == null) { + maybeInitDecoder(); + } else if (sourceDrmSession != decoderDrmSession || !canKeepCodec(oldFormat, inputFormat)) { if (decoderReceivedBuffers) { // Signal end of stream and wait for any final output buffers before re-initialization. decoderReinitializationState = REINITIALIZATION_STATE_SIGNAL_END_OF_STREAM; @@ -497,17 +516,6 @@ public abstract class SimpleDecoderVideoRenderer extends BaseRenderer { } } - /** - * Returns the extent to which the subclass supports a given format. - * - * @param drmSessionManager The renderer's {@link DrmSessionManager}. - * @param format The format, which has a video {@link Format#sampleMimeType}. - * @return The extent to which the subclass supports the format itself. - * @see RendererCapabilities#supportsFormat(Format) - */ - protected abstract int supportsFormatInternal( - @Nullable DrmSessionManager drmSessionManager, Format format); - /** * Creates a decoder for the given format. * @@ -515,14 +523,11 @@ public abstract class SimpleDecoderVideoRenderer extends BaseRenderer { * @param mediaCrypto The {@link ExoMediaCrypto} object required for decoding encrypted content. * May be null and can be ignored if decoder does not handle encrypted content. * @return The decoder. - * @throws VideoDecoderException If an error occurred creating a suitable decoder. + * @throws DecoderException If an error occurred creating a suitable decoder. */ - protected abstract SimpleDecoder< - VideoDecoderInputBuffer, - ? extends VideoDecoderOutputBuffer, - ? extends VideoDecoderException> - createDecoder(Format format, @Nullable ExoMediaCrypto mediaCrypto) - throws VideoDecoderException; + protected abstract Decoder< + VideoDecoderInputBuffer, ? extends VideoDecoderOutputBuffer, ? extends DecoderException> + createDecoder(Format format, @Nullable ExoMediaCrypto mediaCrypto) throws DecoderException; /** * Renders the specified output buffer. @@ -533,11 +538,15 @@ public abstract class SimpleDecoderVideoRenderer extends BaseRenderer { * @param outputBuffer {@link VideoDecoderOutputBuffer} to render. * @param presentationTimeUs Presentation time in microseconds. * @param outputFormat Output {@link Format}. - * @throws VideoDecoderException If an error occurs when rendering the output buffer. + * @throws DecoderException If an error occurs when rendering the output buffer. */ protected void renderOutputBuffer( VideoDecoderOutputBuffer outputBuffer, long presentationTimeUs, Format outputFormat) - throws VideoDecoderException { + throws DecoderException { + if (frameMetadataListener != null) { + frameMetadataListener.onVideoFrameAboutToBeRendered( + presentationTimeUs, System.nanoTime(), outputFormat, /* mediaFormat= */ null); + } lastRenderTimeUs = C.msToUs(SystemClock.elapsedRealtime() * 1000); int bufferMode = outputBuffer.mode; boolean renderSurface = bufferMode == C.VIDEO_OUTPUT_MODE_SURFACE_YUV && surface != null; @@ -565,10 +574,10 @@ public abstract class SimpleDecoderVideoRenderer extends BaseRenderer { * * @param outputBuffer {@link VideoDecoderOutputBuffer} to render. * @param surface Output {@link Surface}. - * @throws VideoDecoderException If an error occurs when rendering the output buffer. + * @throws DecoderException If an error occurs when rendering the output buffer. */ protected abstract void renderOutputBufferToSurface( - VideoDecoderOutputBuffer outputBuffer, Surface surface) throws VideoDecoderException; + VideoDecoderOutputBuffer outputBuffer, Surface surface) throws DecoderException; /** * Sets output surface. @@ -634,14 +643,25 @@ public abstract class SimpleDecoderVideoRenderer extends BaseRenderer { */ protected abstract void setDecoderOutputMode(@C.VideoOutputMode int outputMode); + /** + * Returns whether the existing decoder can be kept for a new format. + * + * @param oldFormat The previous format. + * @param newFormat The new format. + * @return Whether the existing decoder can be kept. + */ + protected boolean canKeepCodec(Format oldFormat, Format newFormat) { + return false; + } + // Internal methods. - private void setSourceDrmSession(@Nullable DrmSession session) { + private void setSourceDrmSession(@Nullable DrmSession session) { DrmSession.replaceSession(sourceDrmSession, session); sourceDrmSession = session; } - private void setDecoderDrmSession(@Nullable DrmSession session) { + private void setDecoderDrmSession(@Nullable DrmSession session) { DrmSession.replaceSession(decoderDrmSession, session); decoderDrmSession = session; } @@ -678,12 +698,12 @@ public abstract class SimpleDecoderVideoRenderer extends BaseRenderer { decoderInitializedTimestamp, decoderInitializedTimestamp - decoderInitializingTimestamp); decoderCounters.decoderInitCount++; - } catch (VideoDecoderException e) { - throw ExoPlaybackException.createForRenderer(e, getIndex()); + } catch (DecoderException e) { + throw createRendererException(e, inputFormat); } } - private boolean feedInputBuffer() throws VideoDecoderException, ExoPlaybackException { + private boolean feedInputBuffer() throws DecoderException, ExoPlaybackException { if (decoder == null || decoderReinitializationState == REINITIALIZATION_STATE_WAIT_END_OF_STREAM || inputStreamEnded) { @@ -706,7 +726,7 @@ public abstract class SimpleDecoderVideoRenderer extends BaseRenderer { return false; } - int result; + @SampleStream.ReadDataResult int result; FormatHolder formatHolder = getFormatHolder(); if (waitingForKeys) { // We've already read an encrypted sample into buffer, and are waiting for keys. @@ -759,7 +779,7 @@ public abstract class SimpleDecoderVideoRenderer extends BaseRenderer { * @throws ExoPlaybackException If an error occurs draining the output buffer. */ private boolean drainOutputBuffer(long positionUs, long elapsedRealtimeUs) - throws ExoPlaybackException, VideoDecoderException { + throws ExoPlaybackException, DecoderException { if (outputBuffer == null) { outputBuffer = decoder.dequeueOutputBuffer(); if (outputBuffer == null) { @@ -801,7 +821,7 @@ public abstract class SimpleDecoderVideoRenderer extends BaseRenderer { * @throws ExoPlaybackException If an error occurs processing the output buffer. */ private boolean processOutputBuffer(long positionUs, long elapsedRealtimeUs) - throws ExoPlaybackException, VideoDecoderException { + throws ExoPlaybackException, DecoderException { if (initialPositionUs == C.TIME_UNSET) { initialPositionUs = positionUs; } @@ -823,10 +843,15 @@ public abstract class SimpleDecoderVideoRenderer extends BaseRenderer { } long elapsedRealtimeNowUs = SystemClock.elapsedRealtime() * 1000; + long elapsedSinceLastRenderUs = elapsedRealtimeNowUs - lastRenderTimeUs; boolean isStarted = getState() == STATE_STARTED; - if (!renderedFirstFrame - || (isStarted - && shouldForceRenderOutputBuffer(earlyUs, elapsedRealtimeNowUs - lastRenderTimeUs))) { + boolean shouldRenderFirstFrame = + !renderedFirstFrameAfterEnable + ? (isStarted || mayRenderFirstFrameAfterEnableIfNotStarted) + : !renderedFirstFrameAfterReset; + // TODO: We shouldn't force render while we are joining an ongoing playback. + if (shouldRenderFirstFrame + || (isStarted && shouldForceRenderOutputBuffer(earlyUs, elapsedSinceLastRenderUs))) { renderOutputBuffer(outputBuffer, presentationTimeUs, outputFormat); return true; } @@ -835,6 +860,7 @@ public abstract class SimpleDecoderVideoRenderer extends BaseRenderer { return false; } + // TODO: Treat dropped buffers as skipped while we are joining an ongoing playback. if (shouldDropBuffersToKeyframe(earlyUs, elapsedRealtimeUs) && maybeDropBuffersToKeyframe(positionUs)) { return false; @@ -878,14 +904,14 @@ public abstract class SimpleDecoderVideoRenderer extends BaseRenderer { } private boolean shouldWaitForKeys(boolean bufferEncrypted) throws ExoPlaybackException { + DrmSession decoderDrmSession = this.decoderDrmSession; if (decoderDrmSession == null - || (!bufferEncrypted - && (playClearSamplesWithoutKeys || decoderDrmSession.playClearSamplesWithoutKeys()))) { + || (!bufferEncrypted && decoderDrmSession.playClearSamplesWithoutKeys())) { return false; } @DrmSession.State int drmSessionState = decoderDrmSession.getState(); if (drmSessionState == DrmSession.STATE_ERROR) { - throw ExoPlaybackException.createForRenderer(decoderDrmSession.getError(), getIndex()); + throw createRendererException(decoderDrmSession.getError(), inputFormat); } return drmSessionState != DrmSession.STATE_OPENED_WITH_KEYS; } @@ -898,18 +924,19 @@ public abstract class SimpleDecoderVideoRenderer extends BaseRenderer { } private void clearRenderedFirstFrame() { - renderedFirstFrame = false; + renderedFirstFrameAfterReset = false; } private void maybeNotifyRenderedFirstFrame() { - if (!renderedFirstFrame) { - renderedFirstFrame = true; + renderedFirstFrameAfterEnable = true; + if (!renderedFirstFrameAfterReset) { + renderedFirstFrameAfterReset = true; eventDispatcher.renderedFirstFrame(surface); } } private void maybeRenotifyRenderedFirstFrame() { - if (renderedFirstFrame) { + if (renderedFirstFrameAfterReset) { eventDispatcher.renderedFirstFrame(surface); } } 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 d1f874b428..a5dd9cfefb 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 @@ -19,37 +19,29 @@ import static com.google.android.exoplayer2.util.EGLSurfaceTexture.SECURE_MODE_N import static com.google.android.exoplayer2.util.EGLSurfaceTexture.SECURE_MODE_PROTECTED_PBUFFER; import static com.google.android.exoplayer2.util.EGLSurfaceTexture.SECURE_MODE_SURFACELESS_CONTEXT; -import android.annotation.TargetApi; import android.content.Context; -import android.content.pm.PackageManager; import android.graphics.SurfaceTexture; -import android.opengl.EGL14; -import android.opengl.EGLDisplay; import android.os.Handler; import android.os.Handler.Callback; import android.os.HandlerThread; import android.os.Message; import android.view.Surface; import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.EGLSurfaceTexture; import com.google.android.exoplayer2.util.EGLSurfaceTexture.SecureMode; +import com.google.android.exoplayer2.util.GlUtil; import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.Util; -import javax.microedition.khronos.egl.EGL10; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; -/** - * A dummy {@link Surface}. - */ -@TargetApi(17) +/** A dummy {@link Surface}. */ +@RequiresApi(17) public final class DummySurface extends Surface { private static final String TAG = "DummySurface"; - private static final String EXTENSION_PROTECTED_CONTENT = "EGL_EXT_protected_content"; - private static final String EXTENSION_SURFACELESS_CONTEXT = "EGL_KHR_surfaceless_context"; - /** * Whether the surface is secure. */ @@ -69,7 +61,7 @@ public final class DummySurface extends Surface { */ public static synchronized boolean isSecureSupported(Context context) { if (!secureModeInitialized) { - secureMode = Util.SDK_INT < 24 ? SECURE_MODE_NONE : getSecureModeV24(context); + secureMode = getSecureMode(context); secureModeInitialized = true; } return secureMode != SECURE_MODE_NONE; @@ -88,7 +80,6 @@ public final class DummySurface extends Surface { * {@link #isSecureSupported(Context)} returns {@code false}. */ public static DummySurface newInstanceV17(Context context, boolean secure) { - assertApiLevel17OrHigher(); Assertions.checkState(!secure || isSecureSupported(context)); DummySurfaceThread thread = new DummySurfaceThread(); return thread.init(secure ? secureMode : SECURE_MODE_NONE); @@ -115,40 +106,21 @@ public final class DummySurface extends Surface { } } - private static void assertApiLevel17OrHigher() { - if (Util.SDK_INT < 17) { - throw new UnsupportedOperationException("Unsupported prior to API level 17"); - } - } - - @TargetApi(24) - private static @SecureMode int getSecureModeV24(Context context) { - if (Util.SDK_INT < 26 && ("samsung".equals(Util.MANUFACTURER) || "XT1650".equals(Util.MODEL))) { - // Samsung devices running Nougat are known to be broken. See - // https://github.com/google/ExoPlayer/issues/3373 and [Internal: b/37197802]. - // Moto Z XT1650 is also affected. See - // https://github.com/google/ExoPlayer/issues/3215. + @SecureMode + private static int getSecureMode(Context context) { + if (GlUtil.isProtectedContentExtensionSupported(context)) { + if (GlUtil.isSurfacelessContextExtensionSupported()) { + return SECURE_MODE_SURFACELESS_CONTEXT; + } else { + // If we can't use surfaceless contexts, we use a protected 1 * 1 pixel buffer surface. + // This may require support for EXT_protected_surface, but in practice it works on some + // devices that don't have that extension. See also + // https://github.com/google/ExoPlayer/issues/3558. + return SECURE_MODE_PROTECTED_PBUFFER; + } + } else { return SECURE_MODE_NONE; } - if (Util.SDK_INT < 26 && !context.getPackageManager().hasSystemFeature( - PackageManager.FEATURE_VR_MODE_HIGH_PERFORMANCE)) { - // Pre API level 26 devices were not well tested unless they supported VR mode. - return SECURE_MODE_NONE; - } - EGLDisplay display = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY); - String eglExtensions = EGL14.eglQueryString(display, EGL10.EGL_EXTENSIONS); - if (eglExtensions == null) { - return SECURE_MODE_NONE; - } - if (!eglExtensions.contains(EXTENSION_PROTECTED_CONTENT)) { - return SECURE_MODE_NONE; - } - // If we can't use surfaceless contexts, we use a protected 1 * 1 pixel buffer surface. This may - // require support for EXT_protected_surface, but in practice it works on some devices that - // don't have that extension. See also https://github.com/google/ExoPlayer/issues/3558. - return eglExtensions.contains(EXTENSION_SURFACELESS_CONTEXT) - ? SECURE_MODE_SURFACELESS_CONTEXT - : SECURE_MODE_PROTECTED_PBUFFER; } private static class DummySurfaceThread extends HandlerThread implements Callback { @@ -163,7 +135,7 @@ public final class DummySurface extends Surface { @Nullable private DummySurface surface; public DummySurfaceThread() { - super("dummySurface"); + super("ExoPlayer:DummySurface"); } public DummySurface init(@SecureMode int secureMode) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoDecoderException.java b/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoDecoderException.java new file mode 100644 index 0000000000..9846ecdca6 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoDecoderException.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2020 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.video; + +import android.media.MediaCodec; +import android.view.Surface; +import androidx.annotation.Nullable; +import com.google.android.exoplayer2.mediacodec.MediaCodecDecoderException; +import com.google.android.exoplayer2.mediacodec.MediaCodecInfo; + +/** Thrown when a failure occurs in a {@link MediaCodec} video decoder. */ +public class MediaCodecVideoDecoderException extends MediaCodecDecoderException { + + /** The {@link System#identityHashCode(Object)} of the surface when the exception occurred. */ + public final int surfaceIdentityHashCode; + + /** Whether the surface was valid when the exception occurred. */ + public final boolean isSurfaceValid; + + public MediaCodecVideoDecoderException( + Throwable cause, @Nullable MediaCodecInfo codecInfo, @Nullable Surface surface) { + super(cause, codecInfo); + surfaceIdentityHashCode = System.identityHashCode(surface); + isSurfaceValid = surface == null || surface.isValid(); + } +} 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 e27c738f08..9dc0c7230d 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 @@ -26,34 +26,36 @@ import android.media.MediaCrypto; import android.media.MediaFormat; import android.os.Bundle; import android.os.Handler; +import android.os.Message; import android.os.SystemClock; import android.util.Pair; import android.view.Surface; import androidx.annotation.CallSuper; import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.FormatHolder; import com.google.android.exoplayer2.PlayerMessage.Target; +import com.google.android.exoplayer2.RendererCapabilities; import com.google.android.exoplayer2.decoder.DecoderInputBuffer; import com.google.android.exoplayer2.drm.DrmInitData; -import com.google.android.exoplayer2.drm.DrmSessionManager; -import com.google.android.exoplayer2.drm.FrameworkMediaCrypto; +import com.google.android.exoplayer2.mediacodec.MediaCodecDecoderException; import com.google.android.exoplayer2.mediacodec.MediaCodecInfo; import com.google.android.exoplayer2.mediacodec.MediaCodecRenderer; import com.google.android.exoplayer2.mediacodec.MediaCodecSelector; import com.google.android.exoplayer2.mediacodec.MediaCodecUtil; import com.google.android.exoplayer2.mediacodec.MediaCodecUtil.DecoderQueryException; import com.google.android.exoplayer2.mediacodec.MediaFormatUtil; -import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.TraceUtil; import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.video.VideoRendererEventListener.EventDispatcher; +import java.lang.reflect.Method; import java.nio.ByteBuffer; import java.util.Collections; import java.util.List; @@ -65,12 +67,15 @@ import java.util.List; * on the playback thread: * *
          - *
        • Message with type {@link C#MSG_SET_SURFACE} to set the output surface. The message payload + *
        • Message with type {@link #MSG_SET_SURFACE} to set the output surface. The message payload * should be the target {@link Surface}, or null. - *
        • Message with type {@link C#MSG_SET_SCALING_MODE} to set the video scaling mode. The message - * payload should be one of the integer scaling modes in {@link C.VideoScalingMode}. Note that + *
        • Message with type {@link #MSG_SET_SCALING_MODE} to set the video scaling mode. The message + * payload should be one of the integer scaling modes in {@link VideoScalingMode}. Note that * the scaling mode only applies if the {@link Surface} targeted by this renderer is owned by * a {@link android.view.SurfaceView}. + *
        • Message with type {@link #MSG_SET_VIDEO_FRAME_METADATA_LISTENER} to set a listener for + * metadata associated with frames being rendered. The message payload should be the {@link + * VideoFrameMetadataListener}, or null. *
        */ public class MediaCodecVideoRenderer extends MediaCodecRenderer { @@ -85,9 +90,6 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { private static final int[] STANDARD_LONG_EDGE_VIDEO_PX = new int[] { 1920, 1600, 1440, 1280, 960, 854, 640, 540, 480}; - // 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[])}. @@ -97,22 +99,23 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { /** Magic frame render timestamp that indicates the EOS in tunneling mode. */ private static final long TUNNELING_EOS_PRESENTATION_TIME_US = Long.MAX_VALUE; - /** A {@link DecoderException} with additional surface information. */ - public static final class VideoDecoderException extends DecoderException { + // TODO: Remove reflection once we target API level 30. + @Nullable private static final Method surfaceSetFrameRateMethod; - /** The {@link System#identityHashCode(Object)} of the surface when the exception occurred. */ - public final int surfaceIdentityHashCode; - - /** Whether the surface was valid when the exception occurred. */ - public final boolean isSurfaceValid; - - public VideoDecoderException( - Throwable cause, @Nullable MediaCodecInfo codecInfo, @Nullable Surface surface) { - super(cause, codecInfo); - surfaceIdentityHashCode = System.identityHashCode(surface); - isSurfaceValid = surface == null || surface.isValid(); + static { + @Nullable Method setFrameRateMethod = null; + if (Util.SDK_INT >= 30) { + try { + setFrameRateMethod = Surface.class.getMethod("setFrameRate", float.class, int.class); + } catch (NoSuchMethodException e) { + // Do nothing. + } } + surfaceSetFrameRateMethod = setFrameRateMethod; } + // TODO: Remove these constants and use those defined by Surface once we target API level 30. + private static final int SURFACE_FRAME_RATE_COMPATIBILITY_DEFAULT = 0; + private static final int SURFACE_FRAME_RATE_COMPATIBILITY_FIXED_SOURCE = 1; private static boolean evaluatedDeviceNeedsSetOutputSurfaceWorkaround; private static boolean deviceNeedsSetOutputSurfaceWorkaround; @@ -123,18 +126,18 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { private final long allowedJoiningTimeMs; private final int maxDroppedFramesToNotify; private final boolean deviceNeedsNoPostProcessWorkaround; - private final long[] pendingOutputStreamOffsetsUs; - private final long[] pendingOutputStreamSwitchTimesUs; private CodecMaxValues codecMaxValues; private boolean codecNeedsSetOutputSurfaceWorkaround; private boolean codecHandlesHdr10PlusOutOfBandMetadata; private Surface surface; + private float surfaceFrameRate; private Surface dummySurface; - @C.VideoScalingMode - private int scalingMode; - private boolean renderedFirstFrame; + @VideoScalingMode private int scalingMode; + private boolean renderedFirstFrameAfterReset; + private boolean mayRenderFirstFrameAfterEnableIfNotStarted; + private boolean renderedFirstFrameAfterEnable; private long initialPositionUs; private long joiningDeadlineMs; private long droppedFrameAccumulationStartTimeMs; @@ -142,14 +145,17 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { private int consecutiveDroppedFrameCount; private int buffersInCodecCount; private long lastRenderTimeUs; + private long totalVideoFrameProcessingOffsetUs; + private int videoFrameProcessingOffsetCount; - private int pendingRotationDegrees; - private float pendingPixelWidthHeightRatio; @Nullable private MediaFormat currentMediaFormat; + private int mediaFormatWidth; + private int mediaFormatHeight; private int currentWidth; private int currentHeight; private int currentUnappliedRotationDegrees; private float currentPixelWidthHeightRatio; + private float currentFrameRate; private int reportedWidth; private int reportedHeight; private int reportedUnappliedRotationDegrees; @@ -157,11 +163,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { private boolean tunneling; private int tunnelingAudioSessionId; - /* package */ OnFrameRenderedListenerV23 tunnelingOnFrameRenderedListener; - - private long lastInputTimeUs; - private long outputStreamOffsetUs; - private int pendingOutputStreamOffsetCount; + /* package */ @Nullable OnFrameRenderedListenerV23 tunnelingOnFrameRenderedListener; @Nullable private VideoFrameMetadataListener frameMetadataListener; /** @@ -200,7 +202,6 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { * @param maxDroppedFramesToNotify The maximum number of frames that can be dropped between * invocations of {@link VideoRendererEventListener#onDroppedFrames(int, long)}. */ - @SuppressWarnings("deprecation") public MediaCodecVideoRenderer( Context context, MediaCodecSelector mediaCodecSelector, @@ -212,51 +213,6 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { context, mediaCodecSelector, allowedJoiningTimeMs, - /* drmSessionManager= */ null, - /* playClearSamplesWithoutKeys= */ false, - eventHandler, - eventListener, - maxDroppedFramesToNotify); - } - - /** - * @param context A context. - * @param mediaCodecSelector A decoder selector. - * @param allowedJoiningTimeMs The maximum duration in milliseconds for which this video renderer - * can attempt to seamlessly join an ongoing playback. - * @param drmSessionManager For use with encrypted content. May be null if support for encrypted - * content is not required. - * @param playClearSamplesWithoutKeys Encrypted media may contain clear (un-encrypted) regions. - * For example a media file may start with a short clear region so as to allow playback to - * 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 eventHandler A handler to use when delivering events to {@code eventListener}. 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 maxDroppedFramesToNotify The maximum number of frames that can be dropped between - * invocations of {@link VideoRendererEventListener#onDroppedFrames(int, long)}. - * @deprecated Use {@link #MediaCodecVideoRenderer(Context, MediaCodecSelector, long, boolean, - * Handler, VideoRendererEventListener, int)} instead, and pass DRM-related parameters to the - * {@link MediaSource} factories. - */ - @Deprecated - @SuppressWarnings("deprecation") - public MediaCodecVideoRenderer( - Context context, - MediaCodecSelector mediaCodecSelector, - long allowedJoiningTimeMs, - @Nullable DrmSessionManager drmSessionManager, - boolean playClearSamplesWithoutKeys, - @Nullable Handler eventHandler, - @Nullable VideoRendererEventListener eventListener, - int maxDroppedFramesToNotify) { - this( - context, - mediaCodecSelector, - allowedJoiningTimeMs, - drmSessionManager, - playClearSamplesWithoutKeys, /* enableDecoderFallback= */ false, eventHandler, eventListener, @@ -277,7 +233,6 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { * @param maxDroppedFramesToNotify The maximum number of frames that can be dropped between * invocations of {@link VideoRendererEventListener#onDroppedFrames(int, long)}. */ - @SuppressWarnings("deprecation") public MediaCodecVideoRenderer( Context context, MediaCodecSelector mediaCodecSelector, @@ -286,58 +241,9 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { @Nullable Handler eventHandler, @Nullable VideoRendererEventListener eventListener, int maxDroppedFramesToNotify) { - this( - context, - mediaCodecSelector, - allowedJoiningTimeMs, - /* drmSessionManager= */ null, - /* playClearSamplesWithoutKeys= */ false, - enableDecoderFallback, - eventHandler, - eventListener, - maxDroppedFramesToNotify); - } - - /** - * @param context A context. - * @param mediaCodecSelector A decoder selector. - * @param allowedJoiningTimeMs The maximum duration in milliseconds for which this video renderer - * can attempt to seamlessly join an ongoing playback. - * @param drmSessionManager For use with encrypted content. May be null if support for encrypted - * content is not required. - * @param playClearSamplesWithoutKeys Encrypted media may contain clear (un-encrypted) regions. - * For example a media file may start with a short clear region so as to allow playback to - * 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 enableDecoderFallback Whether to enable fallback to lower-priority decoders if decoder - * initialization fails. This may result in using a decoder that is slower/less efficient than - * the primary decoder. - * @param eventHandler A handler to use when delivering events to {@code eventListener}. 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 maxDroppedFramesToNotify The maximum number of frames that can be dropped between - * invocations of {@link VideoRendererEventListener#onDroppedFrames(int, long)}. - * @deprecated Use {@link #MediaCodecVideoRenderer(Context, MediaCodecSelector, long, boolean, - * Handler, VideoRendererEventListener, int)} instead, and pass DRM-related parameters to the - * {@link MediaSource} factories. - */ - @Deprecated - public MediaCodecVideoRenderer( - Context context, - MediaCodecSelector mediaCodecSelector, - long allowedJoiningTimeMs, - @Nullable DrmSessionManager drmSessionManager, - boolean playClearSamplesWithoutKeys, - boolean enableDecoderFallback, - @Nullable Handler eventHandler, - @Nullable VideoRendererEventListener eventListener, - int maxDroppedFramesToNotify) { super( C.TRACK_TYPE_VIDEO, mediaCodecSelector, - drmSessionManager, - playClearSamplesWithoutKeys, enableDecoderFallback, /* assumedMinimumCodecOperatingRate= */ 30); this.allowedJoiningTimeMs = allowedJoiningTimeMs; @@ -346,28 +252,28 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { frameReleaseTimeHelper = new VideoFrameReleaseTimeHelper(this.context); eventDispatcher = new EventDispatcher(eventHandler, eventListener); deviceNeedsNoPostProcessWorkaround = deviceNeedsNoPostProcessWorkaround(); - pendingOutputStreamOffsetsUs = new long[MAX_PENDING_OUTPUT_STREAM_OFFSET_COUNT]; - pendingOutputStreamSwitchTimesUs = new long[MAX_PENDING_OUTPUT_STREAM_OFFSET_COUNT]; - outputStreamOffsetUs = C.TIME_UNSET; - lastInputTimeUs = C.TIME_UNSET; joiningDeadlineMs = C.TIME_UNSET; currentWidth = Format.NO_VALUE; currentHeight = Format.NO_VALUE; currentPixelWidthHeightRatio = Format.NO_VALUE; - pendingPixelWidthHeightRatio = Format.NO_VALUE; - scalingMode = C.VIDEO_SCALING_MODE_DEFAULT; + scalingMode = VIDEO_SCALING_MODE_DEFAULT; + mediaFormatWidth = Format.NO_VALUE; + mediaFormatHeight = Format.NO_VALUE; clearReportedVideoSize(); } @Override - protected int supportsFormat( - MediaCodecSelector mediaCodecSelector, - @Nullable DrmSessionManager drmSessionManager, - Format format) + public String getName() { + return TAG; + } + + @Override + @Capabilities + protected int supportsFormat(MediaCodecSelector mediaCodecSelector, Format format) throws DecoderQueryException { String mimeType = format.sampleMimeType; if (!MimeTypes.isVideo(mimeType)) { - return FORMAT_UNSUPPORTED_TYPE; + return RendererCapabilities.create(FORMAT_UNSUPPORTED_TYPE); } @Nullable DrmInitData drmInitData = format.drmInitData; // Assume encrypted content requires secure decoders. @@ -388,24 +294,20 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { /* requiresTunnelingDecoder= */ false); } if (decoderInfos.isEmpty()) { - return FORMAT_UNSUPPORTED_SUBTYPE; + return RendererCapabilities.create(FORMAT_UNSUPPORTED_SUBTYPE); } - boolean supportsFormatDrm = - drmInitData == null - || FrameworkMediaCrypto.class.equals(format.exoMediaCryptoType) - || (format.exoMediaCryptoType == null - && supportsFormatDrm(drmSessionManager, drmInitData)); - if (!supportsFormatDrm) { - return FORMAT_UNSUPPORTED_DRM; + if (!supportsFormatDrm(format)) { + return RendererCapabilities.create(FORMAT_UNSUPPORTED_DRM); } // Check capabilities for the first decoder in the list, which takes priority. MediaCodecInfo decoderInfo = decoderInfos.get(0); boolean isFormatSupported = decoderInfo.isFormatSupported(format); + @AdaptiveSupport int adaptiveSupport = decoderInfo.isSeamlessAdaptationSupported(format) ? ADAPTIVE_SEAMLESS : ADAPTIVE_NOT_SEAMLESS; - int tunnelingSupport = TUNNELING_NOT_SUPPORTED; + @TunnelingSupport int tunnelingSupport = TUNNELING_NOT_SUPPORTED; if (isFormatSupported) { List tunnelingDecoderInfos = getDecoderInfos( @@ -421,8 +323,9 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { } } } + @FormatSupport int formatSupport = isFormatSupported ? FORMAT_HANDLED : FORMAT_EXCEEDS_CAPABILITIES; - return adaptiveSupport | tunnelingSupport | formatSupport; + return RendererCapabilities.create(formatSupport, adaptiveSupport, tunnelingSupport); } @Override @@ -468,8 +371,9 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { } @Override - protected void onEnabled(boolean joining) throws ExoPlaybackException { - super.onEnabled(joining); + protected void onEnabled(boolean joining, boolean mayRenderStartOfStream) + throws ExoPlaybackException { + super.onEnabled(joining, mayRenderStartOfStream); int oldTunnelingAudioSessionId = tunnelingAudioSessionId; tunnelingAudioSessionId = getConfiguration().tunnelingAudioSessionId; tunneling = tunnelingAudioSessionId != C.AUDIO_SESSION_ID_UNSET; @@ -478,23 +382,8 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { } eventDispatcher.enabled(decoderCounters); frameReleaseTimeHelper.enable(); - } - - @Override - protected void onStreamChanged(Format[] formats, long offsetUs) throws ExoPlaybackException { - if (outputStreamOffsetUs == C.TIME_UNSET) { - outputStreamOffsetUs = offsetUs; - } else { - if (pendingOutputStreamOffsetCount == pendingOutputStreamOffsetsUs.length) { - Log.w(TAG, "Too many stream changes, so dropping offset: " - + pendingOutputStreamOffsetsUs[pendingOutputStreamOffsetCount - 1]); - } else { - pendingOutputStreamOffsetCount++; - } - pendingOutputStreamOffsetsUs[pendingOutputStreamOffsetCount - 1] = offsetUs; - pendingOutputStreamSwitchTimesUs[pendingOutputStreamOffsetCount - 1] = lastInputTimeUs; - } - super.onStreamChanged(formats, offsetUs); + mayRenderFirstFrameAfterEnableIfNotStarted = mayRenderStartOfStream; + renderedFirstFrameAfterEnable = false; } @Override @@ -503,11 +392,6 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { clearRenderedFirstFrame(); initialPositionUs = C.TIME_UNSET; consecutiveDroppedFrameCount = 0; - lastInputTimeUs = C.TIME_UNSET; - if (pendingOutputStreamOffsetCount != 0) { - outputStreamOffsetUs = pendingOutputStreamOffsetsUs[pendingOutputStreamOffsetCount - 1]; - pendingOutputStreamOffsetCount = 0; - } if (joining) { setJoiningDeadlineMs(); } else { @@ -517,8 +401,11 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { @Override public boolean isReady() { - if (super.isReady() && (renderedFirstFrame || (dummySurface != null && surface == dummySurface) - || getCodec() == null || tunneling)) { + if (super.isReady() + && (renderedFirstFrameAfterReset + || (dummySurface != null && surface == dummySurface) + || getCodec() == null + || tunneling)) { // Ready. If we were joining then we've now joined, so clear the joining deadline. joiningDeadlineMs = C.TIME_UNSET; return true; @@ -541,20 +428,22 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { droppedFrames = 0; droppedFrameAccumulationStartTimeMs = SystemClock.elapsedRealtime(); lastRenderTimeUs = SystemClock.elapsedRealtime() * 1000; + totalVideoFrameProcessingOffsetUs = 0; + videoFrameProcessingOffsetCount = 0; + updateSurfaceFrameRate(/* isNewSurface= */ false); } @Override protected void onStopped() { joiningDeadlineMs = C.TIME_UNSET; maybeNotifyDroppedFrames(); + maybeNotifyVideoFrameProcessingOffset(); + clearSurfaceFrameRate(); super.onStopped(); } @Override protected void onDisabled() { - lastInputTimeUs = C.TIME_UNSET; - outputStreamOffsetUs = C.TIME_UNSET; - pendingOutputStreamOffsetCount = 0; currentMediaFormat = null; clearReportedVideoSize(); clearRenderedFirstFrame(); @@ -584,15 +473,15 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { @Override public void handleMessage(int messageType, @Nullable Object message) throws ExoPlaybackException { - if (messageType == C.MSG_SET_SURFACE) { + if (messageType == MSG_SET_SURFACE) { setSurface((Surface) message); - } else if (messageType == C.MSG_SET_SCALING_MODE) { + } else if (messageType == MSG_SET_SCALING_MODE) { scalingMode = (Integer) message; MediaCodec codec = getCodec(); if (codec != null) { codec.setVideoScalingMode(scalingMode); } - } else if (messageType == C.MSG_SET_VIDEO_FRAME_METADATA_LISTENER) { + } else if (messageType == MSG_SET_VIDEO_FRAME_METADATA_LISTENER) { frameMetadataListener = (VideoFrameMetadataListener) message; } else { super.handleMessage(messageType, message); @@ -614,7 +503,10 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { } // We only need to update the codec if the surface has changed. if (this.surface != surface) { + clearSurfaceFrameRate(); this.surface = surface; + updateSurfaceFrameRate(/* isNewSurface= */ true); + @State int state = getState(); MediaCodec codec = getCodec(); if (codec != null) { @@ -622,7 +514,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { setOutputSurfaceV23(codec, surface); } else { releaseCodec(); - maybeInitCodec(); + maybeInitCodecOrPassthrough(); } } if (surface != null && surface != dummySurface) { @@ -675,7 +567,9 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { deviceNeedsNoPostProcessWorkaround, tunnelingAudioSessionId); if (surface == null) { - Assertions.checkState(shouldUseDummySurface(codecInfo)); + if (!shouldUseDummySurface(codecInfo)) { + throw new IllegalStateException(); + } if (dummySurface == null) { dummySurface = DummySurface.newInstanceV17(context, codecInfo.secure); } @@ -704,22 +598,15 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { @CallSuper @Override - protected void releaseCodec() { - try { - super.releaseCodec(); - } finally { - buffersInCodecCount = 0; - } + protected void resetCodecStateForFlush() { + super.resetCodecStateForFlush(); + buffersInCodecCount = 0; } - @CallSuper @Override - protected boolean flushOrReleaseCodec() { - try { - return super.flushOrReleaseCodec(); - } finally { - buffersInCodecCount = 0; - } + public void setOperatingRate(float operatingRate) throws ExoPlaybackException { + super.setOperatingRate(operatingRate); + updateSurfaceFrameRate(/* isNewSurface= */ false); } @Override @@ -749,10 +636,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { @Override protected void onInputFormatChanged(FormatHolder formatHolder) throws ExoPlaybackException { super.onInputFormatChanged(formatHolder); - Format newFormat = formatHolder.format; - eventDispatcher.inputFormatChanged(newFormat); - pendingPixelWidthHeightRatio = newFormat.pixelWidthHeightRatio; - pendingRotationDegrees = newFormat.rotationDegrees; + eventDispatcher.inputFormatChanged(formatHolder.format); } /** @@ -768,7 +652,6 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { if (!tunneling) { buffersInCodecCount++; } - lastInputTimeUs = Math.max(buffer.timeUs, lastInputTimeUs); if (Util.SDK_INT < 23 && tunneling) { // In tunneled mode before API 23 we don't have a way to know when the buffer is output, so // treat it as if it were output immediately. @@ -784,22 +667,59 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { && outputMediaFormat.containsKey(KEY_CROP_LEFT) && outputMediaFormat.containsKey(KEY_CROP_BOTTOM) && outputMediaFormat.containsKey(KEY_CROP_TOP); - int mediaFormatWidth = + mediaFormatWidth = hasCrop ? outputMediaFormat.getInteger(KEY_CROP_RIGHT) - outputMediaFormat.getInteger(KEY_CROP_LEFT) + 1 : outputMediaFormat.getInteger(MediaFormat.KEY_WIDTH); - int mediaFormatHeight = + mediaFormatHeight = hasCrop ? outputMediaFormat.getInteger(KEY_CROP_BOTTOM) - outputMediaFormat.getInteger(KEY_CROP_TOP) + 1 : outputMediaFormat.getInteger(MediaFormat.KEY_HEIGHT); - processOutputFormat(codec, mediaFormatWidth, mediaFormatHeight); + + // Must be applied each time the output MediaFormat changes. + codec.setVideoScalingMode(scalingMode); + maybeNotifyVideoFrameProcessingOffset(); } @Override + protected void onOutputFormatChanged(Format outputFormat) { + configureOutput(outputFormat); + } + + @Override + protected void configureOutput(Format outputFormat) { + if (tunneling) { + currentWidth = outputFormat.width; + currentHeight = outputFormat.height; + } else { + currentWidth = mediaFormatWidth; + currentHeight = mediaFormatHeight; + } + currentPixelWidthHeightRatio = outputFormat.pixelWidthHeightRatio; + if (Util.SDK_INT >= 21) { + // On API level 21 and above the decoder applies the rotation when rendering to the surface. + // Hence currentUnappliedRotation should always be 0. For 90 and 270 degree rotations, we need + // to flip the width, height and pixel aspect ratio to reflect the rotation that was applied. + if (outputFormat.rotationDegrees == 90 || outputFormat.rotationDegrees == 270) { + int rotatedHeight = currentWidth; + currentWidth = currentHeight; + currentHeight = rotatedHeight; + currentPixelWidthHeightRatio = 1 / currentPixelWidthHeightRatio; + } + } else { + // On API level 20 and below the decoder does not apply the rotation. + currentUnappliedRotationDegrees = outputFormat.rotationDegrees; + } + currentFrameRate = outputFormat.frameRate; + updateSurfaceFrameRate(/* isNewSurface= */ false); + } + + @Override + @TargetApi(29) // codecHandlesHdr10PlusOutOfBandMetadata is false if Util.SDK_INT < 29 protected void handleInputBufferSupplementalData(DecoderInputBuffer buffer) throws ExoPlaybackException { if (!codecHandlesHdr10PlusOutOfBandMetadata) { @@ -824,7 +744,6 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { byte[] hdr10PlusInfo = new byte[data.remaining()]; data.get(hdr10PlusInfo); data.position(0); - // If codecHandlesHdr10PlusOutOfBandMetadata is true, this is an API 29 or later build. setHdr10PlusInfoV29(getCodec(), hdr10PlusInfo); } } @@ -834,19 +753,23 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { protected boolean processOutputBuffer( long positionUs, long elapsedRealtimeUs, - MediaCodec codec, + @Nullable MediaCodec codec, ByteBuffer buffer, int bufferIndex, int bufferFlags, + int sampleCount, long bufferPresentationTimeUs, boolean isDecodeOnlyBuffer, boolean isLastBuffer, Format format) throws ExoPlaybackException { + Assertions.checkNotNull(codec); // Can not render video without codec + if (initialPositionUs == C.TIME_UNSET) { initialPositionUs = positionUs; } + long outputStreamOffsetUs = getOutputStreamOffsetUs(); long presentationTimeUs = bufferPresentationTimeUs - outputStreamOffsetUs; if (isDecodeOnlyBuffer && !isLastBuffer) { @@ -859,6 +782,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { // Skip frames in sync with playback, so we'll be at the right frame if the mode changes. if (isBufferLate(earlyUs)) { skipOutputBuffer(codec, bufferIndex, presentationTimeUs); + decoderCounters.addVideoFrameProcessingOffsetSample(earlyUs); return true; } return false; @@ -867,11 +791,15 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { long elapsedRealtimeNowUs = SystemClock.elapsedRealtime() * 1000; long elapsedSinceLastRenderUs = elapsedRealtimeNowUs - lastRenderTimeUs; boolean isStarted = getState() == STATE_STARTED; + boolean shouldRenderFirstFrame = + !renderedFirstFrameAfterEnable + ? (isStarted || mayRenderFirstFrameAfterEnableIfNotStarted) + : !renderedFirstFrameAfterReset; // Don't force output until we joined and the position reached the current stream. boolean forceRenderOutputBuffer = joiningDeadlineMs == C.TIME_UNSET && positionUs >= outputStreamOffsetUs - && (!renderedFirstFrame + && (shouldRenderFirstFrame || (isStarted && shouldForceRenderOutputBuffer(earlyUs, elapsedSinceLastRenderUs))); if (forceRenderOutputBuffer) { long releaseTimeNs = System.nanoTime(); @@ -881,6 +809,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { } else { renderOutputBuffer(codec, bufferIndex, presentationTimeUs); } + decoderCounters.addVideoFrameProcessingOffsetSample(earlyUs); return true; } @@ -913,6 +842,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { } else { dropOutputBuffer(codec, bufferIndex, presentationTimeUs); } + decoderCounters.addVideoFrameProcessingOffsetSample(earlyUs); return true; } @@ -922,6 +852,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { notifyFrameMetadataListener( presentationTimeUs, adjustedReleaseTimeNs, format, currentMediaFormat); renderOutputBufferV21(codec, bufferIndex, presentationTimeUs, adjustedReleaseTimeNs); + decoderCounters.addVideoFrameProcessingOffsetSample(earlyUs); return true; } } else { @@ -941,6 +872,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { notifyFrameMetadataListener( presentationTimeUs, adjustedReleaseTimeNs, format, currentMediaFormat); renderOutputBuffer(codec, bufferIndex, presentationTimeUs); + decoderCounters.addVideoFrameProcessingOffsetSample(earlyUs); return true; } } @@ -949,28 +881,6 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { return false; } - private void processOutputFormat(MediaCodec codec, int width, int height) { - currentWidth = width; - currentHeight = height; - currentPixelWidthHeightRatio = pendingPixelWidthHeightRatio; - if (Util.SDK_INT >= 21) { - // On API level 21 and above the decoder applies the rotation when rendering to the surface. - // Hence currentUnappliedRotation should always be 0. For 90 and 270 degree rotations, we need - // to flip the width, height and pixel aspect ratio to reflect the rotation that was applied. - if (pendingRotationDegrees == 90 || pendingRotationDegrees == 270) { - int rotatedHeight = currentWidth; - currentWidth = currentHeight; - currentHeight = rotatedHeight; - currentPixelWidthHeightRatio = 1 / currentPixelWidthHeightRatio; - } - } else { - // On API level 20 and below the decoder does not apply the rotation. - currentUnappliedRotationDegrees = pendingRotationDegrees; - } - // Must be applied each time the output MediaFormat changes. - codec.setVideoScalingMode(scalingMode); - } - private void notifyFrameMetadataListener( long presentationTimeUs, long releaseTimeNs, Format format, MediaFormat mediaFormat) { if (frameMetadataListener != null) { @@ -979,22 +889,11 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { } } - /** - * Returns the offset that should be subtracted from {@code bufferPresentationTimeUs} in {@link - * #processOutputBuffer(long, long, MediaCodec, ByteBuffer, int, int, long, boolean, boolean, - * Format)} to get the playback position with respect to the media. - */ - protected long getOutputStreamOffsetUs() { - return outputStreamOffsetUs; - } - /** Called when a buffer was processed in tunneling mode. */ protected void onProcessedTunneledBuffer(long presentationTimeUs) { - @Nullable Format format = updateOutputFormatForTime(presentationTimeUs); - if (format != null) { - processOutputFormat(getCodec(), format.width, format.height); - } + updateOutputFormatForTime(presentationTimeUs); maybeNotifyVideoSizeChanged(); + decoderCounters.renderedOutputBufferCount++; maybeNotifyRenderedFirstFrame(); onProcessedOutputBuffer(presentationTimeUs); } @@ -1004,35 +903,19 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { setPendingOutputEndOfStream(); } - /** - * Called when an output buffer is successfully processed. - * - * @param presentationTimeUs The timestamp associated with the output buffer. - */ @CallSuper @Override protected void onProcessedOutputBuffer(long presentationTimeUs) { + super.onProcessedOutputBuffer(presentationTimeUs); if (!tunneling) { buffersInCodecCount--; } - while (pendingOutputStreamOffsetCount != 0 - && presentationTimeUs >= pendingOutputStreamSwitchTimesUs[0]) { - outputStreamOffsetUs = pendingOutputStreamOffsetsUs[0]; - pendingOutputStreamOffsetCount--; - System.arraycopy( - pendingOutputStreamOffsetsUs, - /* srcPos= */ 1, - pendingOutputStreamOffsetsUs, - /* destPos= */ 0, - pendingOutputStreamOffsetCount); - System.arraycopy( - pendingOutputStreamSwitchTimesUs, - /* srcPos= */ 1, - pendingOutputStreamSwitchTimesUs, - /* destPos= */ 0, - pendingOutputStreamOffsetCount); - clearRenderedFirstFrame(); - } + } + + @Override + protected void onProcessedStreamChange() { + super.onProcessedStreamChange(); + clearRenderedFirstFrame(); } /** @@ -1189,7 +1072,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { * @param presentationTimeUs The presentation time of the output buffer, in microseconds. * @param releaseTimeNs The wallclock time at which the frame should be displayed, in nanoseconds. */ - @TargetApi(21) + @RequiresApi(21) protected void renderOutputBufferV21( MediaCodec codec, int index, long presentationTimeUs, long releaseTimeNs) { maybeNotifyVideoSizeChanged(); @@ -1202,6 +1085,52 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { maybeNotifyRenderedFirstFrame(); } + /** + * Updates the frame-rate of the current {@link #surface} based on the renderer operating rate, + * frame-rate of the content, and whether the renderer is started. + * + * @param isNewSurface Whether the current {@link #surface} is new. + */ + private void updateSurfaceFrameRate(boolean isNewSurface) { + if (Util.SDK_INT < 30 || surface == null || surface == dummySurface) { + return; + } + boolean shouldSetFrameRate = getState() == STATE_STARTED && currentFrameRate != Format.NO_VALUE; + float surfaceFrameRate = shouldSetFrameRate ? currentFrameRate * getOperatingRate() : 0; + // We always set the frame-rate if we have a new surface, since we have no way of knowing what + // it might have been set to previously. + if (this.surfaceFrameRate == surfaceFrameRate && !isNewSurface) { + return; + } + this.surfaceFrameRate = surfaceFrameRate; + setSurfaceFrameRateV30(surface, surfaceFrameRate); + } + + /** Clears the frame-rate of the current {@link #surface}. */ + private void clearSurfaceFrameRate() { + if (Util.SDK_INT < 30 || surface == null || surface == dummySurface || surfaceFrameRate == 0) { + return; + } + surfaceFrameRate = 0; + setSurfaceFrameRateV30(surface, /* frameRate= */ 0); + } + + @RequiresApi(30) + private void setSurfaceFrameRateV30(Surface surface, float frameRate) { + if (surfaceSetFrameRateMethod == null) { + Log.e(TAG, "Failed to call Surface.setFrameRate (method does not exist)"); + } + int compatibility = + frameRate == 0 + ? SURFACE_FRAME_RATE_COMPATIBILITY_DEFAULT + : SURFACE_FRAME_RATE_COMPATIBILITY_FIXED_SOURCE; + try { + surfaceSetFrameRateMethod.invoke(surface, frameRate, compatibility); + } catch (Exception e) { + Log.e(TAG, "Failed to call Surface.setFrameRate", e); + } + } + private boolean shouldUseDummySurface(MediaCodecInfo codecInfo) { return Util.SDK_INT >= 23 && !tunneling @@ -1215,7 +1144,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { } private void clearRenderedFirstFrame() { - renderedFirstFrame = false; + renderedFirstFrameAfterReset = false; // The first frame notification is triggered by renderOutputBuffer or renderOutputBufferV21 for // non-tunneled playback, onQueueInputBuffer for tunneled playback prior to API level 23, and // OnFrameRenderedListenerV23.onFrameRenderedListener for tunneled playback on API level 23 and @@ -1230,14 +1159,15 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { } /* package */ void maybeNotifyRenderedFirstFrame() { - if (!renderedFirstFrame) { - renderedFirstFrame = true; + renderedFirstFrameAfterEnable = true; + if (!renderedFirstFrameAfterReset) { + renderedFirstFrameAfterReset = true; eventDispatcher.renderedFirstFrame(surface); } } private void maybeRenotifyRenderedFirstFrame() { - if (renderedFirstFrame) { + if (renderedFirstFrameAfterReset) { eventDispatcher.renderedFirstFrame(surface); } } @@ -1280,6 +1210,22 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { } } + private void maybeNotifyVideoFrameProcessingOffset() { + Format outputFormat = getCurrentOutputFormat(); + if (outputFormat != null) { + long totalOffsetDelta = + decoderCounters.totalVideoFrameProcessingOffsetUs - totalVideoFrameProcessingOffsetUs; + int countDelta = + decoderCounters.videoFrameProcessingOffsetCount - videoFrameProcessingOffsetCount; + if (countDelta != 0) { + eventDispatcher.reportVideoFrameProcessingOffset( + totalOffsetDelta, countDelta, outputFormat); + totalVideoFrameProcessingOffsetUs = decoderCounters.totalVideoFrameProcessingOffsetUs; + videoFrameProcessingOffsetCount = decoderCounters.videoFrameProcessingOffsetCount; + } + } + } + private static boolean isBufferLate(long earlyUs) { // Class a buffer as late if it should have been presented more than 30 ms ago. return earlyUs < -30000; @@ -1290,19 +1236,19 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { return earlyUs < -500000; } - @TargetApi(29) + @RequiresApi(29) private static void setHdr10PlusInfoV29(MediaCodec codec, byte[] hdr10PlusInfo) { Bundle codecParameters = new Bundle(); codecParameters.putByteArray(MediaCodec.PARAMETER_KEY_HDR10_PLUS_INFO, hdr10PlusInfo); codec.setParameters(codecParameters); } - @TargetApi(23) + @RequiresApi(23) private static void setOutputSurfaceV23(MediaCodec codec, Surface surface) { codec.setOutputSurface(surface); } - @TargetApi(21) + @RequiresApi(21) private static void configureTunnelingV21(MediaFormat mediaFormat, int tunnelingAudioSessionId) { mediaFormat.setFeatureEnabled(CodecCapabilities.FEATURE_TunneledPlayback, true); mediaFormat.setInteger(MediaFormat.KEY_AUDIO_SESSION_ID, tunnelingAudioSessionId); @@ -1323,6 +1269,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { * @return The framework {@link MediaFormat} that should be used to configure the decoder. */ @SuppressLint("InlinedApi") + @TargetApi(21) // tunnelingAudioSessionId is unset if Util.SDK_INT < 21 protected MediaFormat getMediaFormat( Format format, String codecMimeType, @@ -1431,9 +1378,9 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { } @Override - protected DecoderException createDecoderException( + protected MediaCodecDecoderException createDecoderException( Throwable cause, @Nullable MediaCodecInfo codecInfo) { - return new VideoDecoderException(cause, codecInfo, surface); + return new MediaCodecVideoDecoderException(cause, codecInfo, surface); } /** @@ -1610,9 +1557,14 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { } synchronized (MediaCodecVideoRenderer.class) { if (!evaluatedDeviceNeedsSetOutputSurfaceWorkaround) { - if (Util.SDK_INT <= 27 && ("dangal".equals(Util.DEVICE) || "HWEML".equals(Util.DEVICE))) { - // A small number of devices are affected on API level 27: - // https://github.com/google/ExoPlayer/issues/5169. + if ("dangal".equals(Util.DEVICE)) { + // Workaround for MiTV devices: + // https://github.com/google/ExoPlayer/issues/5169, + // https://github.com/google/ExoPlayer/issues/6899. + deviceNeedsSetOutputSurfaceWorkaround = true; + } else if (Util.SDK_INT <= 27 && "HWEML".equals(Util.DEVICE)) { + // Workaround for Huawei P20: + // https://github.com/google/ExoPlayer/issues/4468#issuecomment-459291645. deviceNeedsSetOutputSurfaceWorkaround = true; } else if (Util.SDK_INT >= 27) { // In general, devices running API level 27 or later should be unaffected. Do nothing. @@ -1801,15 +1753,53 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { } - @TargetApi(23) - private final class OnFrameRenderedListenerV23 implements MediaCodec.OnFrameRenderedListener { + @RequiresApi(23) + private final class OnFrameRenderedListenerV23 + implements MediaCodec.OnFrameRenderedListener, Handler.Callback { - private OnFrameRenderedListenerV23(MediaCodec codec) { - codec.setOnFrameRenderedListener(/* listener= */ this, Util.createHandler()); + private static final int HANDLE_FRAME_RENDERED = 0; + + private final Handler handler; + + public OnFrameRenderedListenerV23(MediaCodec codec) { + handler = Util.createHandler(/* callback= */ this); + codec.setOnFrameRenderedListener(/* listener= */ this, handler); } @Override public void onFrameRendered(MediaCodec codec, long presentationTimeUs, long nanoTime) { + // Workaround bug in MediaCodec that causes deadlock if you call directly back into the + // MediaCodec from this listener method. + // Deadlock occurs because MediaCodec calls this listener method holding a lock, + // which may also be required by calls made back into the MediaCodec. + // This was fixed in https://android-review.googlesource.com/1156807. + // + // The workaround queues the event for subsequent processing, where the lock will not be held. + if (Util.SDK_INT < 30) { + Message message = + Message.obtain( + handler, + /* what= */ HANDLE_FRAME_RENDERED, + /* arg1= */ (int) (presentationTimeUs >> 32), + /* arg2= */ (int) presentationTimeUs); + handler.sendMessageAtFrontOfQueue(message); + } else { + handleFrameRendered(presentationTimeUs); + } + } + + @Override + public boolean handleMessage(Message message) { + switch (message.what) { + case HANDLE_FRAME_RENDERED: + handleFrameRendered(Util.toLong(message.arg1, message.arg2)); + return true; + default: + return false; + } + } + + private void handleFrameRendered(long presentationTimeUs) { if (this != tunnelingOnFrameRenderedListener) { // Stale event. return; @@ -1820,6 +1810,5 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { onProcessedTunneledBuffer(presentationTimeUs); } } - } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/VideoDecoderException.java b/library/core/src/main/java/com/google/android/exoplayer2/video/VideoDecoderException.java deleted file mode 100644 index 68108af636..0000000000 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/VideoDecoderException.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright (C) 2019 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.video; - -/** Thrown when a video decoder error occurs. */ -public class VideoDecoderException extends Exception { - - /** - * Creates an instance with the given message. - * - * @param message The detail message for this exception. - */ - public VideoDecoderException(String message) { - super(message); - } - - /** - * Creates an instance with the given message and cause. - * - * @param message The detail message for this exception. - * @param cause the cause (which is saved for later retrieval by the {@link #getCause()} method). - * A null value is permitted, and indicates that the cause is nonexistent or unknown. - */ - public VideoDecoderException(String message, Throwable cause) { - super(message, cause); - } -} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/VideoDecoderRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/video/VideoDecoderGLFrameRenderer.java similarity index 81% rename from library/core/src/main/java/com/google/android/exoplayer2/video/VideoDecoderRenderer.java rename to library/core/src/main/java/com/google/android/exoplayer2/video/VideoDecoderGLFrameRenderer.java index cb9c4eb59b..18453bae9b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/VideoDecoderRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/VideoDecoderGLFrameRenderer.java @@ -20,16 +20,19 @@ import android.opengl.GLSurfaceView; import androidx.annotation.Nullable; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.GlUtil; +import java.nio.ByteBuffer; import java.nio.FloatBuffer; import java.util.concurrent.atomic.AtomicReference; import javax.microedition.khronos.egl.EGLConfig; import javax.microedition.khronos.opengles.GL10; +import org.checkerframework.checker.nullness.compatqual.NullableType; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /** * GLSurfaceView.Renderer implementation that can render YUV Frames returned by a video decoder * after decoding. It does the YUV to RGB color conversion in the Fragment Shader. */ -/* package */ class VideoDecoderRenderer +/* package */ class VideoDecoderGLFrameRenderer implements GLSurfaceView.Renderer, VideoDecoderOutputBufferRenderer { private static final float[] kColorConversion601 = { @@ -86,7 +89,8 @@ import javax.microedition.khronos.opengles.GL10; GlUtil.createBuffer(new float[] {-1.0f, 1.0f, -1.0f, -1.0f, 1.0f, 1.0f, 1.0f, -1.0f}); private final GLSurfaceView surfaceView; private final int[] yuvTextures = new int[3]; - private final AtomicReference pendingOutputBufferReference; + private final AtomicReference<@NullableType VideoDecoderOutputBuffer> + pendingOutputBufferReference; // Kept in field rather than a local variable in order not to get garbage collected before // glDrawArrays uses it. @@ -98,10 +102,10 @@ import javax.microedition.khronos.opengles.GL10; private int[] previousWidths; private int[] previousStrides; - @Nullable - private VideoDecoderOutputBuffer renderedOutputBuffer; // Accessed only from the GL thread. + // Accessed only from the GL thread. + private @MonotonicNonNull VideoDecoderOutputBuffer renderedOutputBuffer; - public VideoDecoderRenderer(GLSurfaceView surfaceView) { + public VideoDecoderGLFrameRenderer(GLSurfaceView surfaceView) { this.surfaceView = surfaceView; pendingOutputBufferReference = new AtomicReference<>(); textureCoords = new FloatBuffer[3]; @@ -119,7 +123,13 @@ import javax.microedition.khronos.opengles.GL10; GLES20.glUseProgram(program); int posLocation = GLES20.glGetAttribLocation(program, "in_pos"); GLES20.glEnableVertexAttribArray(posLocation); - GLES20.glVertexAttribPointer(posLocation, 2, GLES20.GL_FLOAT, false, 0, TEXTURE_VERTICES); + GLES20.glVertexAttribPointer( + posLocation, + 2, + GLES20.GL_FLOAT, + /* normalized= */ false, + /* stride= */ 0, + TEXTURE_VERTICES); texLocations[0] = GLES20.glGetAttribLocation(program, "in_tc_y"); GLES20.glEnableVertexAttribArray(texLocations[0]); texLocations[1] = GLES20.glGetAttribLocation(program, "in_tc_u"); @@ -140,7 +150,9 @@ import javax.microedition.khronos.opengles.GL10; @Override public void onDrawFrame(GL10 unused) { - VideoDecoderOutputBuffer pendingOutputBuffer = pendingOutputBufferReference.getAndSet(null); + @Nullable + VideoDecoderOutputBuffer pendingOutputBuffer = + pendingOutputBufferReference.getAndSet(/* newValue= */ null); if (pendingOutputBuffer == null && renderedOutputBuffer == null) { // There is no output buffer to render at the moment. return; @@ -151,7 +163,9 @@ import javax.microedition.khronos.opengles.GL10; } renderedOutputBuffer = pendingOutputBuffer; } - VideoDecoderOutputBuffer outputBuffer = renderedOutputBuffer; + + VideoDecoderOutputBuffer outputBuffer = Assertions.checkNotNull(renderedOutputBuffer); + // Set color matrix. Assume BT709 if the color space is unknown. float[] colorConversion = kColorConversion709; switch (outputBuffer.colorspace) { @@ -163,9 +177,18 @@ import javax.microedition.khronos.opengles.GL10; break; case VideoDecoderOutputBuffer.COLORSPACE_BT709: default: - break; // Do nothing + // Do nothing. + break; } - GLES20.glUniformMatrix3fv(colorMatrixLocation, 1, false, colorConversion, 0); + GLES20.glUniformMatrix3fv( + colorMatrixLocation, + /* color= */ 1, + /* transpose= */ false, + colorConversion, + /* offset= */ 0); + + int[] yuvStrides = Assertions.checkNotNull(outputBuffer.yuvStrides); + ByteBuffer[] yuvPlanes = Assertions.checkNotNull(outputBuffer.yuvPlanes); for (int i = 0; i < 3; i++) { int h = (i == 0) ? outputBuffer.height : (outputBuffer.height + 1) / 2; @@ -174,14 +197,14 @@ import javax.microedition.khronos.opengles.GL10; GLES20.glPixelStorei(GLES20.GL_UNPACK_ALIGNMENT, 1); GLES20.glTexImage2D( GLES20.GL_TEXTURE_2D, - 0, + /* level= */ 0, GLES20.GL_LUMINANCE, - outputBuffer.yuvStrides[i], + yuvStrides[i], h, - 0, + /* border= */ 0, GLES20.GL_LUMINANCE, GLES20.GL_UNSIGNED_BYTE, - outputBuffer.yuvPlanes[i]); + yuvPlanes[i]); } int[] widths = new int[3]; @@ -192,28 +215,34 @@ import javax.microedition.khronos.opengles.GL10; widths[1] = widths[2] = (widths[0] + 1) / 2; for (int i = 0; i < 3; i++) { // Set cropping of stride if either width or stride has changed. - if (previousWidths[i] != widths[i] || previousStrides[i] != outputBuffer.yuvStrides[i]) { - Assertions.checkState(outputBuffer.yuvStrides[i] != 0); - float widthRatio = (float) widths[i] / outputBuffer.yuvStrides[i]; + if (previousWidths[i] != widths[i] || previousStrides[i] != yuvStrides[i]) { + Assertions.checkState(yuvStrides[i] != 0); + float widthRatio = (float) widths[i] / yuvStrides[i]; // These buffers are consumed during each call to glDrawArrays. They need to be member // variables rather than local variables in order not to get garbage collected. textureCoords[i] = GlUtil.createBuffer( new float[] {0.0f, 0.0f, 0.0f, 1.0f, widthRatio, 0.0f, widthRatio, 1.0f}); GLES20.glVertexAttribPointer( - texLocations[i], 2, GLES20.GL_FLOAT, false, 0, textureCoords[i]); + texLocations[i], + /* size= */ 2, + GLES20.GL_FLOAT, + /* normalized= */ false, + /* stride= */ 0, + textureCoords[i]); previousWidths[i] = widths[i]; - previousStrides[i] = outputBuffer.yuvStrides[i]; + previousStrides[i] = yuvStrides[i]; } } GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT); - GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4); + GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, /* first= */ 0, /* count= */ 4); GlUtil.checkGlError(); } @Override public void setOutputBuffer(VideoDecoderOutputBuffer outputBuffer) { + @Nullable VideoDecoderOutputBuffer oldPendingOutputBuffer = pendingOutputBufferReference.getAndSet(outputBuffer); if (oldPendingOutputBuffer != null) { @@ -224,7 +253,7 @@ import javax.microedition.khronos.opengles.GL10; } private void setupTextures() { - GLES20.glGenTextures(3, yuvTextures, 0); + GLES20.glGenTextures(3, yuvTextures, /* offset= */ 0); for (int i = 0; i < 3; i++) { GLES20.glUniform1i(GLES20.glGetUniformLocation(program, TEXTURE_UNIFORMS[i]), i); GLES20.glActiveTexture(GLES20.GL_TEXTURE0 + i); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/VideoDecoderGLSurfaceView.java b/library/core/src/main/java/com/google/android/exoplayer2/video/VideoDecoderGLSurfaceView.java index 99f3d07b65..b9d016b886 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/VideoDecoderGLSurfaceView.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/VideoDecoderGLSurfaceView.java @@ -30,7 +30,7 @@ import androidx.annotation.Nullable; */ public class VideoDecoderGLSurfaceView extends GLSurfaceView { - private final VideoDecoderRenderer renderer; + private final VideoDecoderGLFrameRenderer renderer; /** @param context A {@link Context}. */ public VideoDecoderGLSurfaceView(Context context) { @@ -41,9 +41,14 @@ public class VideoDecoderGLSurfaceView extends GLSurfaceView { * @param context A {@link Context}. * @param attrs Custom attributes. */ + @SuppressWarnings({ + "nullness:assignment.type.incompatible", + "nullness:argument.type.incompatible", + "nullness:method.invocation.invalid" + }) public VideoDecoderGLSurfaceView(Context context, @Nullable AttributeSet attrs) { super(context, attrs); - renderer = new VideoDecoderRenderer(this); + renderer = new VideoDecoderGLFrameRenderer(/* surfaceView= */ this); setPreserveEGLContextOnPause(true); setEGLContextClientVersion(2); setRenderer(renderer); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/VideoDecoderOutputBuffer.java b/library/core/src/main/java/com/google/android/exoplayer2/video/VideoDecoderOutputBuffer.java index 457aa30ade..8f2e4122b3 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/VideoDecoderOutputBuffer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/VideoDecoderOutputBuffer.java @@ -23,17 +23,6 @@ import java.nio.ByteBuffer; /** Video decoder output buffer containing video frame data. */ public class VideoDecoderOutputBuffer extends OutputBuffer { - /** Buffer owner. */ - public interface Owner { - - /** - * Releases the buffer. - * - * @param outputBuffer Output buffer. - */ - void releaseOutputBuffer(VideoDecoderOutputBuffer outputBuffer); - } - // LINT.IfChange public static final int COLORSPACE_UNKNOWN = 0; public static final int COLORSPACE_BT601 = 1; @@ -44,7 +33,7 @@ public class VideoDecoderOutputBuffer extends OutputBuffer { // ../../../../../../../../../../extensions/vp9/src/main/jni/vpx_jni.cc // ) - /** Decoder private data. */ + /** Decoder private data. Used from native code. */ public int decoderPrivate; /** Output mode. */ @@ -68,14 +57,14 @@ public class VideoDecoderOutputBuffer extends OutputBuffer { */ @Nullable public ByteBuffer supplementalData; - private final Owner owner; + private final Owner owner; /** * Creates VideoDecoderOutputBuffer. * * @param owner Buffer owner. */ - public VideoDecoderOutputBuffer(Owner owner) { + public VideoDecoderOutputBuffer(Owner owner) { this.owner = owner; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/VideoFrameMetadataListener.java b/library/core/src/main/java/com/google/android/exoplayer2/video/VideoFrameMetadataListener.java index 746903a101..bc275f1fb0 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/VideoFrameMetadataListener.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/VideoFrameMetadataListener.java @@ -19,14 +19,14 @@ import android.media.MediaFormat; import androidx.annotation.Nullable; import com.google.android.exoplayer2.Format; -/** A listener for metadata corresponding to video frame being rendered. */ +/** A listener for metadata corresponding to video frames being rendered. */ public interface VideoFrameMetadataListener { /** - * Called when the video frame about to be rendered. This method is called on the playback thread. + * Called on the playback thread when a video frame is about to be rendered. * - * @param presentationTimeUs The presentation time of the output buffer, in microseconds. + * @param presentationTimeUs The presentation time of the frame, in microseconds. * @param releaseTimeNs The wallclock time at which the frame should be displayed, in nanoseconds. - * If the platform API version of the device is less than 21, then this is the best effort. + * If the platform API version of the device is less than 21, then this is a best effort. * @param format The format associated with the frame. * @param mediaFormat The framework media format associated with the frame, or {@code null} if not * known or not applicable (e.g., because the frame was not output by a {@link 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 bf31ce2abb..2134772d9c 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 @@ -26,6 +26,7 @@ import android.view.Choreographer.FrameCallback; import android.view.Display; import android.view.WindowManager; import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.util.Util; @@ -40,9 +41,9 @@ public final class VideoFrameReleaseTimeHelper { private static final long VSYNC_OFFSET_PERCENTAGE = 80; private static final int MIN_FRAMES_FOR_ADJUSTMENT = 6; - private final WindowManager windowManager; - private final VSyncSampler vsyncSampler; - private final DefaultDisplayListener displayListener; + @Nullable private final WindowManager windowManager; + @Nullable private final VSyncSampler vsyncSampler; + @Nullable private final DefaultDisplayListener displayListener; private long vsyncDurationNs; private long vsyncOffsetNs; @@ -88,9 +89,8 @@ public final class VideoFrameReleaseTimeHelper { vsyncOffsetNs = C.TIME_UNSET; } - /** - * Enables the helper. Must be called from the playback thread. - */ + /** Enables the helper. Must be called from the playback thread. */ + @TargetApi(17) // displayListener is null if Util.SDK_INT < 17. public void enable() { haveSync = false; if (windowManager != null) { @@ -102,9 +102,8 @@ public final class VideoFrameReleaseTimeHelper { } } - /** - * Disables the helper. Must be called from the playback thread. - */ + /** Disables the helper. Must be called from the playback thread. */ + @TargetApi(17) // displayListener is null if Util.SDK_INT < 17. public void disable() { if (windowManager != null) { if (displayListener != null) { @@ -187,7 +186,7 @@ public final class VideoFrameReleaseTimeHelper { return snappedTimeNs - vsyncOffsetNs; } - @TargetApi(17) + @RequiresApi(17) private DefaultDisplayListener maybeBuildDefaultDisplayListenerV17(Context context) { DisplayManager manager = (DisplayManager) context.getSystemService(Context.DISPLAY_SERVICE); return manager == null ? null : new DefaultDisplayListener(manager); @@ -226,7 +225,7 @@ public final class VideoFrameReleaseTimeHelper { return snappedAfterDiff < snappedBeforeDiff ? snappedAfterNs : snappedBeforeNs; } - @TargetApi(17) + @RequiresApi(17) private final class DefaultDisplayListener implements DisplayManager.DisplayListener { private final DisplayManager displayManager; @@ -288,7 +287,7 @@ public final class VideoFrameReleaseTimeHelper { private VSyncSampler() { sampledVsyncTimeNs = C.TIME_UNSET; - choreographerOwnerThread = new HandlerThread("ChoreographerOwner:Handler"); + choreographerOwnerThread = new HandlerThread("ExoPlayer:FrameReleaseChoreographer"); choreographerOwnerThread.start(); 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 6f492c3975..948c388c30 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 @@ -52,7 +52,7 @@ public interface VideoListener { /** * 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. + * is rendered for the first time since the renderer was reset. */ default void onRenderedFirstFrame() {} } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/VideoRendererEventListener.java b/library/core/src/main/java/com/google/android/exoplayer2/video/VideoRendererEventListener.java index e7dfd123b1..671d66c31c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/VideoRendererEventListener.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/VideoRendererEventListener.java @@ -71,6 +71,28 @@ public interface VideoRendererEventListener { */ default void onDroppedFrames(int count, long elapsedMs) {} + /** + * Called to report the video processing offset of video frames processed by the video renderer. + * + *

        Video processing offset represents how early a video frame is processed compared to the + * player's current position. For each video frame, the offset is calculated as Pvf + * - Ppl where Pvf is the presentation timestamp of the video + * frame and Ppl is the current position of the player. Positive values + * indicate the frame was processed early enough whereas negative values indicate that the + * player's position had progressed beyond the frame's timestamp when the frame was processed (and + * the frame was probably dropped). + * + *

        The renderer reports the sum of video processing offset samples (one sample per processed + * video frame: dropped, skipped or rendered) and the total number of samples. + * + * @param totalProcessingOffsetUs The sum of all video frame processing offset samples for the + * video frames processed by the renderer in microseconds. + * @param frameCount The number of samples included in the {@code totalProcessingOffsetUs}. + * @param format The {@link Format} that is currently output. + */ + default void onVideoFrameProcessingOffset( + long totalProcessingOffsetUs, int frameCount, Format format) {} + /** * Called before a frame is rendered for the first time since setting the surface, and each time * there's a change in the size, rotation or pixel aspect ratio of the video being rendered. @@ -159,6 +181,17 @@ public interface VideoRendererEventListener { } } + /** Invokes {@link VideoRendererEventListener#onVideoFrameProcessingOffset}. */ + public void reportVideoFrameProcessingOffset( + long totalProcessingOffsetUs, int frameCount, Format format) { + if (handler != null) { + handler.post( + () -> + castNonNull(listener) + .onVideoFrameProcessingOffset(totalProcessingOffsetUs, frameCount, format)); + } + } + /** Invokes {@link VideoRendererEventListener#onVideoSizeChanged(int, int, int, float)}. */ public void videoSizeChanged( int width, diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/spherical/CameraMotionRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/video/spherical/CameraMotionRenderer.java index d1cf0abc56..abf08f3b4e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/spherical/CameraMotionRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/spherical/CameraMotionRenderer.java @@ -22,7 +22,9 @@ import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.FormatHolder; import com.google.android.exoplayer2.Renderer; +import com.google.android.exoplayer2.RendererCapabilities; import com.google.android.exoplayer2.decoder.DecoderInputBuffer; +import com.google.android.exoplayer2.source.SampleStream; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.Util; @@ -31,6 +33,7 @@ import java.nio.ByteBuffer; /** A {@link Renderer} that parses the camera motion track. */ public class CameraMotionRenderer extends BaseRenderer { + private static final String TAG = "CameraMotionRenderer"; // The amount of time to read samples ahead of the current time. private static final int SAMPLE_WINDOW_DURATION_US = 100000; @@ -48,15 +51,21 @@ public class CameraMotionRenderer extends BaseRenderer { } @Override + public String getName() { + return TAG; + } + + @Override + @Capabilities public int supportsFormat(Format format) { return MimeTypes.APPLICATION_CAMERA_MOTION.equals(format.sampleMimeType) - ? FORMAT_HANDLED - : FORMAT_UNSUPPORTED_TYPE; + ? RendererCapabilities.create(FORMAT_HANDLED) + : RendererCapabilities.create(FORMAT_UNSUPPORTED_TYPE); } @Override public void handleMessage(int messageType, @Nullable Object message) throws ExoPlaybackException { - if (messageType == C.MSG_SET_CAMERA_MOTION_LISTENER) { + if (messageType == MSG_SET_CAMERA_MOTION_LISTENER) { listener = (CameraMotionListener) message; } else { super.handleMessage(messageType, message); @@ -84,6 +93,7 @@ public class CameraMotionRenderer extends BaseRenderer { while (!hasReadStreamToEnd() && lastTimestampUs < positionUs + SAMPLE_WINDOW_DURATION_US) { buffer.clear(); FormatHolder formatHolder = getFormatHolder(); + @SampleStream.ReadDataResult int result = readSource(formatHolder, buffer, /* formatRequired= */ false); if (result != C.RESULT_BUFFER_READ || buffer.isEndOfStream()) { return; @@ -110,7 +120,8 @@ public class CameraMotionRenderer extends BaseRenderer { return true; } - private @Nullable float[] parseMetadata(ByteBuffer data) { + @Nullable + private float[] parseMetadata(ByteBuffer data) { if (data.remaining() != 16) { return null; } 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 deleted file mode 100644 index da907e004f..0000000000 --- a/library/core/src/test/assets/amr/sample_nb_cbr.amr.3.dump +++ /dev/null @@ -1,34 +0,0 @@ -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_wb_cbr.amr.3.dump b/library/core/src/test/assets/amr/sample_wb_cbr.amr.3.dump deleted file mode 100644 index 1ec8c6fdb7..0000000000 --- a/library/core/src/test/assets/amr/sample_wb_cbr.amr.3.dump +++ /dev/null @@ -1,34 +0,0 @@ -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/binary/1024_incrementing_bytes.mp3 b/library/core/src/test/assets/binary/1024_incrementing_bytes.mp3 deleted file mode 100644 index c8b49c8cd5..0000000000 Binary files a/library/core/src/test/assets/binary/1024_incrementing_bytes.mp3 and /dev/null differ diff --git a/library/core/src/test/assets/flac/bear_with_vorbis_comments.flac.0.dump b/library/core/src/test/assets/flac/bear_with_vorbis_comments.flac.0.dump deleted file mode 100644 index e35dcc2081..0000000000 --- a/library/core/src/test/assets/flac/bear_with_vorbis_comments.flac.0.dump +++ /dev/null @@ -1,163 +0,0 @@ -seekMap: - isSeekable = false - duration = 2741000 - getPosition(0) = [[timeUs=0, position=0]] -numberOfTracks = 1 -track 0: - format: - bitrate = 1536000 - id = null - containerMimeType = null - sampleMimeType = audio/flac - maxInputSize = 5776 - width = -1 - height = -1 - frameRate = -1.0 - rotationDegrees = 0 - pixelWidthHeightRatio = 1.0 - channelCount = 2 - sampleRate = 48000 - pcmEncoding = -1 - encoderDelay = 0 - encoderPadding = 0 - subsampleOffsetUs = 9223372036854775807 - selectionFlags = 0 - language = null - drmInitData = - - initializationData: - data = length 42, hash 83F6895 - total output bytes = 164431 - sample count = 33 - sample 0: - time = 0 - flags = 1 - data = length 5030, hash D2B60530 - sample 1: - time = 85333 - flags = 1 - data = length 5066, hash 4C932A54 - sample 2: - time = 170666 - flags = 1 - data = length 5112, hash 7E5A7B61 - sample 3: - time = 256000 - flags = 1 - data = length 5044, hash 7EF93F13 - sample 4: - time = 341333 - flags = 1 - data = length 4943, hash DE7E27F8 - sample 5: - time = 426666 - flags = 1 - data = length 5121, hash 6D0D0B40 - sample 6: - time = 512000 - flags = 1 - data = length 5068, hash 9924644F - sample 7: - time = 597333 - flags = 1 - data = length 5143, hash 6C34F0CE - sample 8: - time = 682666 - flags = 1 - data = length 5109, hash E3B7BEFB - sample 9: - time = 768000 - flags = 1 - data = length 5129, hash 44111D9B - sample 10: - time = 853333 - flags = 1 - data = length 5031, hash 9D55EA53 - sample 11: - time = 938666 - flags = 1 - data = length 5119, hash E1CB9BA6 - sample 12: - time = 1024000 - flags = 1 - data = length 5360, hash 17265C5D - sample 13: - time = 1109333 - flags = 1 - data = length 5340, hash A90FDDF1 - sample 14: - time = 1194666 - flags = 1 - data = length 5162, hash 31F65AD5 - sample 15: - time = 1280000 - flags = 1 - data = length 5168, hash F2394F2D - sample 16: - time = 1365333 - flags = 1 - data = length 5776, hash 58437AB3 - sample 17: - time = 1450666 - flags = 1 - data = length 5394, hash EBAB20A8 - sample 18: - time = 1536000 - flags = 1 - data = length 5168, hash BF37C7A5 - sample 19: - time = 1621333 - flags = 1 - data = length 5324, hash 59546B7B - sample 20: - time = 1706666 - flags = 1 - data = length 5172, hash 6036EF0B - sample 21: - time = 1792000 - flags = 1 - data = length 5102, hash 5A131071 - sample 22: - time = 1877333 - flags = 1 - data = length 5111, hash 3D9EBB3B - sample 23: - time = 1962666 - flags = 1 - data = length 5113, hash 61101D4F - sample 24: - time = 2048000 - flags = 1 - data = length 5229, hash D2E55742 - sample 25: - time = 2133333 - flags = 1 - data = length 5162, hash 7F2E97FA - sample 26: - time = 2218666 - flags = 1 - data = length 5255, hash D92A782 - sample 27: - time = 2304000 - flags = 1 - data = length 5196, hash 98FE5138 - sample 28: - time = 2389333 - flags = 1 - data = length 5214, hash 3D35C38C - sample 29: - time = 2474666 - flags = 1 - data = length 5211, hash 7E25420F - sample 30: - time = 2560000 - flags = 1 - data = length 5230, hash 2AD96FBC - sample 31: - time = 2645333 - flags = 1 - data = length 3384, hash 938BCDD9 - sample 32: - time = 2730666 - flags = 1 - data = length 445, hash A388E3D6 -tracksEnded = true diff --git a/library/core/src/test/assets/mp3/bear.mp3.1.dump b/library/core/src/test/assets/mp3/bear.mp3.1.dump deleted file mode 100644 index c2f37973b7..0000000000 --- a/library/core/src/test/assets/mp3/bear.mp3.1.dump +++ /dev/null @@ -1,338 +0,0 @@ -seekMap: - isSeekable = true - duration = 2784000 - getPosition(0) = [[timeUs=0, position=201]] -numberOfTracks = 1 -track 0: - format: - bitrate = -1 - id = null - containerMimeType = null - sampleMimeType = audio/mpeg - maxInputSize = 4096 - width = -1 - height = -1 - frameRate = -1.0 - rotationDegrees = 0 - pixelWidthHeightRatio = 1.0 - channelCount = 2 - sampleRate = 48000 - pcmEncoding = -1 - encoderDelay = 956 - encoderPadding = 3352 - subsampleOffsetUs = 9223372036854775807 - selectionFlags = 0 - language = null - drmInitData = - - initializationData: - total output bytes = 29568 - sample count = 77 - sample 0: - time = 928568 - flags = 1 - data = length 384, hash F7E344F4 - sample 1: - time = 952568 - flags = 1 - data = length 384, hash 14EF6AFD - sample 2: - time = 976568 - flags = 1 - data = length 384, hash 61C9B92C - sample 3: - time = 1000568 - flags = 1 - data = length 384, hash ABE1368 - sample 4: - time = 1024568 - flags = 1 - data = length 384, hash 6A3B8547 - sample 5: - time = 1048568 - flags = 1 - data = length 384, hash 30E905FA - sample 6: - time = 1072568 - flags = 1 - data = length 384, hash 21A267CD - sample 7: - time = 1096568 - flags = 1 - data = length 384, hash D96A2651 - sample 8: - time = 1120568 - flags = 1 - data = length 384, hash 72340177 - sample 9: - time = 1144568 - flags = 1 - data = length 384, hash 9345E744 - sample 10: - time = 1168568 - flags = 1 - data = length 384, hash FDE39E3A - sample 11: - time = 1192568 - flags = 1 - data = length 384, hash F0B7465 - sample 12: - time = 1216568 - flags = 1 - data = length 384, hash 3693AB86 - sample 13: - time = 1240568 - flags = 1 - data = length 384, hash F39719B1 - sample 14: - time = 1264568 - flags = 1 - data = length 384, hash DA3958DC - sample 15: - time = 1288568 - flags = 1 - data = length 384, hash FDC7599F - sample 16: - time = 1312568 - flags = 1 - data = length 384, hash AEFF8471 - sample 17: - time = 1336568 - flags = 1 - data = length 384, hash 89C92C19 - sample 18: - time = 1360568 - flags = 1 - data = length 384, hash 5C786A4B - sample 19: - time = 1384568 - flags = 1 - data = length 384, hash 5ACA8B - sample 20: - time = 1408568 - flags = 1 - data = length 384, hash 7755974C - sample 21: - time = 1432568 - flags = 1 - data = length 384, hash 3934B73C - sample 22: - time = 1456568 - flags = 1 - data = length 384, hash DDD70A2F - sample 23: - time = 1480568 - flags = 1 - data = length 384, hash 8FACE2EF - sample 24: - time = 1504568 - flags = 1 - data = length 384, hash 4A602591 - sample 25: - time = 1528568 - flags = 1 - data = length 384, hash D019AA2D - sample 26: - time = 1552568 - flags = 1 - data = length 384, hash 8A680B9D - sample 27: - time = 1576568 - flags = 1 - data = length 384, hash B655C959 - sample 28: - time = 1600568 - flags = 1 - data = length 384, hash 2168336B - sample 29: - time = 1624568 - flags = 1 - data = length 384, hash D77F6D31 - sample 30: - time = 1648568 - flags = 1 - data = length 384, hash 524B4B2F - sample 31: - time = 1672568 - flags = 1 - data = length 384, hash 4752DDFC - sample 32: - time = 1696568 - flags = 1 - data = length 384, hash E786727F - sample 33: - time = 1720568 - flags = 1 - data = length 384, hash 5DA6FB8C - sample 34: - time = 1744568 - flags = 1 - data = length 384, hash 92F24269 - sample 35: - time = 1768568 - flags = 1 - data = length 384, hash CD0A3BA1 - sample 36: - time = 1792568 - flags = 1 - data = length 384, hash 7D00409F - sample 37: - time = 1816568 - flags = 1 - data = length 384, hash D7ADB5FA - sample 38: - time = 1840568 - flags = 1 - data = length 384, hash 4A140209 - sample 39: - time = 1864568 - flags = 1 - data = length 384, hash E801184A - sample 40: - time = 1888568 - flags = 1 - data = length 384, hash 53C6CF9C - sample 41: - time = 1912568 - flags = 1 - data = length 384, hash 19A8D99F - sample 42: - time = 1936568 - flags = 1 - data = length 384, hash E47EB43F - sample 43: - time = 1960568 - flags = 1 - data = length 384, hash 4EA329E7 - sample 44: - time = 1984568 - flags = 1 - data = length 384, hash 1CCAAE62 - sample 45: - time = 2008568 - flags = 1 - data = length 384, hash ED3F8C66 - sample 46: - time = 2032568 - flags = 1 - data = length 384, hash D3D646B6 - sample 47: - time = 2056568 - flags = 1 - data = length 384, hash 68CD1574 - sample 48: - time = 2080568 - flags = 1 - data = length 384, hash 8CEAB382 - sample 49: - time = 2104568 - flags = 1 - data = length 384, hash D54B1C48 - sample 50: - time = 2128568 - flags = 1 - data = length 384, hash FFE2EE90 - sample 51: - time = 2152568 - flags = 1 - data = length 384, hash BFE8A673 - sample 52: - time = 2176568 - flags = 1 - data = length 384, hash 978B1C92 - sample 53: - time = 2200568 - flags = 1 - data = length 384, hash 810CC71E - sample 54: - time = 2224568 - flags = 1 - data = length 384, hash 44FE42D9 - sample 55: - time = 2248568 - flags = 1 - data = length 384, hash 2F5BB02C - sample 56: - time = 2272568 - flags = 1 - data = length 384, hash 77DDB90 - sample 57: - time = 2296568 - flags = 1 - data = length 384, hash 24FB5EDA - sample 58: - time = 2320568 - flags = 1 - data = length 384, hash E73203C6 - sample 59: - time = 2344568 - flags = 1 - data = length 384, hash 14B525F1 - sample 60: - time = 2368568 - flags = 1 - data = length 384, hash 5E0F4E2E - sample 61: - time = 2392568 - flags = 1 - data = length 384, hash 67EE4E31 - sample 62: - time = 2416568 - flags = 1 - data = length 384, hash 2E04EC4C - sample 63: - time = 2440568 - flags = 1 - data = length 384, hash 852CABA7 - sample 64: - time = 2464568 - flags = 1 - data = length 384, hash 19928903 - sample 65: - time = 2488568 - flags = 1 - data = length 384, hash 5DA42021 - sample 66: - time = 2512568 - flags = 1 - data = length 384, hash 45B20B7C - sample 67: - time = 2536568 - flags = 1 - data = length 384, hash D108A215 - sample 68: - time = 2560568 - flags = 1 - data = length 384, hash BD25DB7C - sample 69: - time = 2584568 - flags = 1 - data = length 384, hash DA7F9861 - sample 70: - time = 2608568 - flags = 1 - data = length 384, hash CCD576F - sample 71: - time = 2632568 - flags = 1 - data = length 384, hash 405C1EB5 - sample 72: - time = 2656568 - flags = 1 - data = length 384, hash 6640B74E - sample 73: - time = 2680568 - flags = 1 - data = length 384, hash B4E5937A - sample 74: - time = 2704568 - flags = 1 - data = length 384, hash CEE17733 - sample 75: - time = 2728568 - flags = 1 - data = length 384, hash 2A0DA733 - sample 76: - time = 2752568 - flags = 1 - data = length 384, hash 97F4129B -tracksEnded = true diff --git a/library/core/src/test/assets/mp3/bear.mp3.2.dump b/library/core/src/test/assets/mp3/bear.mp3.2.dump deleted file mode 100644 index 543cf44cc0..0000000000 --- a/library/core/src/test/assets/mp3/bear.mp3.2.dump +++ /dev/null @@ -1,182 +0,0 @@ -seekMap: - isSeekable = true - duration = 2784000 - getPosition(0) = [[timeUs=0, position=201]] -numberOfTracks = 1 -track 0: - format: - bitrate = -1 - id = null - containerMimeType = null - sampleMimeType = audio/mpeg - maxInputSize = 4096 - width = -1 - height = -1 - frameRate = -1.0 - rotationDegrees = 0 - pixelWidthHeightRatio = 1.0 - channelCount = 2 - sampleRate = 48000 - pcmEncoding = -1 - encoderDelay = 956 - encoderPadding = 3352 - subsampleOffsetUs = 9223372036854775807 - selectionFlags = 0 - language = null - drmInitData = - - initializationData: - total output bytes = 14592 - sample count = 38 - sample 0: - time = 1871586 - flags = 1 - data = length 384, hash E801184A - sample 1: - time = 1895586 - flags = 1 - data = length 384, hash 53C6CF9C - sample 2: - time = 1919586 - flags = 1 - data = length 384, hash 19A8D99F - sample 3: - time = 1943586 - flags = 1 - data = length 384, hash E47EB43F - sample 4: - time = 1967586 - flags = 1 - data = length 384, hash 4EA329E7 - sample 5: - time = 1991586 - flags = 1 - data = length 384, hash 1CCAAE62 - sample 6: - time = 2015586 - flags = 1 - data = length 384, hash ED3F8C66 - sample 7: - time = 2039586 - flags = 1 - data = length 384, hash D3D646B6 - sample 8: - time = 2063586 - flags = 1 - data = length 384, hash 68CD1574 - sample 9: - time = 2087586 - flags = 1 - data = length 384, hash 8CEAB382 - sample 10: - time = 2111586 - flags = 1 - data = length 384, hash D54B1C48 - sample 11: - time = 2135586 - flags = 1 - data = length 384, hash FFE2EE90 - sample 12: - time = 2159586 - flags = 1 - data = length 384, hash BFE8A673 - sample 13: - time = 2183586 - flags = 1 - data = length 384, hash 978B1C92 - sample 14: - time = 2207586 - flags = 1 - data = length 384, hash 810CC71E - sample 15: - time = 2231586 - flags = 1 - data = length 384, hash 44FE42D9 - sample 16: - time = 2255586 - flags = 1 - data = length 384, hash 2F5BB02C - sample 17: - time = 2279586 - flags = 1 - data = length 384, hash 77DDB90 - sample 18: - time = 2303586 - flags = 1 - data = length 384, hash 24FB5EDA - sample 19: - time = 2327586 - flags = 1 - data = length 384, hash E73203C6 - sample 20: - time = 2351586 - flags = 1 - data = length 384, hash 14B525F1 - sample 21: - time = 2375586 - flags = 1 - data = length 384, hash 5E0F4E2E - sample 22: - time = 2399586 - flags = 1 - data = length 384, hash 67EE4E31 - sample 23: - time = 2423586 - flags = 1 - data = length 384, hash 2E04EC4C - sample 24: - time = 2447586 - flags = 1 - data = length 384, hash 852CABA7 - sample 25: - time = 2471586 - flags = 1 - data = length 384, hash 19928903 - sample 26: - time = 2495586 - flags = 1 - data = length 384, hash 5DA42021 - sample 27: - time = 2519586 - flags = 1 - data = length 384, hash 45B20B7C - sample 28: - time = 2543586 - flags = 1 - data = length 384, hash D108A215 - sample 29: - time = 2567586 - flags = 1 - data = length 384, hash BD25DB7C - sample 30: - time = 2591586 - flags = 1 - data = length 384, hash DA7F9861 - sample 31: - time = 2615586 - flags = 1 - data = length 384, hash CCD576F - sample 32: - time = 2639586 - flags = 1 - data = length 384, hash 405C1EB5 - sample 33: - time = 2663586 - flags = 1 - data = length 384, hash 6640B74E - sample 34: - time = 2687586 - flags = 1 - data = length 384, hash B4E5937A - sample 35: - time = 2711586 - flags = 1 - data = length 384, hash CEE17733 - sample 36: - time = 2735586 - flags = 1 - data = length 384, hash 2A0DA733 - sample 37: - time = 2759586 - flags = 1 - data = length 384, hash 97F4129B -tracksEnded = true diff --git a/library/core/src/test/assets/mp3/bear.mp3.3.dump b/library/core/src/test/assets/mp3/bear.mp3.3.dump deleted file mode 100644 index a87b7d6d37..0000000000 --- a/library/core/src/test/assets/mp3/bear.mp3.3.dump +++ /dev/null @@ -1,30 +0,0 @@ -seekMap: - isSeekable = true - duration = 2784000 - getPosition(0) = [[timeUs=0, position=201]] -numberOfTracks = 1 -track 0: - format: - bitrate = -1 - id = null - containerMimeType = null - sampleMimeType = audio/mpeg - maxInputSize = 4096 - width = -1 - height = -1 - frameRate = -1.0 - rotationDegrees = 0 - pixelWidthHeightRatio = 1.0 - channelCount = 2 - sampleRate = 48000 - pcmEncoding = -1 - encoderDelay = 956 - encoderPadding = 3352 - 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/mp3/play-trimmed.mp3.0.dump b/library/core/src/test/assets/mp3/play-trimmed.mp3.0.dump deleted file mode 100644 index 96b0cd259c..0000000000 --- a/library/core/src/test/assets/mp3/play-trimmed.mp3.0.dump +++ /dev/null @@ -1,34 +0,0 @@ -seekMap: - isSeekable = true - duration = 26125 - getPosition(0) = [[timeUs=0, position=0]] -numberOfTracks = 1 -track 0: - format: - bitrate = -1 - id = null - containerMimeType = null - sampleMimeType = audio/mpeg - maxInputSize = 4096 - width = -1 - height = -1 - frameRate = -1.0 - rotationDegrees = 0 - pixelWidthHeightRatio = 1.0 - channelCount = 2 - sampleRate = 44100 - pcmEncoding = -1 - encoderDelay = 0 - encoderPadding = 0 - subsampleOffsetUs = 9223372036854775807 - selectionFlags = 0 - language = null - drmInitData = - - initializationData: - total output bytes = 418 - sample count = 1 - sample 0: - time = 0 - flags = 1 - data = length 418, hash B819987 -tracksEnded = true diff --git a/library/core/src/test/assets/mp3/play-trimmed.mp3.1.dump b/library/core/src/test/assets/mp3/play-trimmed.mp3.1.dump deleted file mode 100644 index 96b0cd259c..0000000000 --- a/library/core/src/test/assets/mp3/play-trimmed.mp3.1.dump +++ /dev/null @@ -1,34 +0,0 @@ -seekMap: - isSeekable = true - duration = 26125 - getPosition(0) = [[timeUs=0, position=0]] -numberOfTracks = 1 -track 0: - format: - bitrate = -1 - id = null - containerMimeType = null - sampleMimeType = audio/mpeg - maxInputSize = 4096 - width = -1 - height = -1 - frameRate = -1.0 - rotationDegrees = 0 - pixelWidthHeightRatio = 1.0 - channelCount = 2 - sampleRate = 44100 - pcmEncoding = -1 - encoderDelay = 0 - encoderPadding = 0 - subsampleOffsetUs = 9223372036854775807 - selectionFlags = 0 - language = null - drmInitData = - - initializationData: - total output bytes = 418 - sample count = 1 - sample 0: - time = 0 - flags = 1 - data = length 418, hash B819987 -tracksEnded = true diff --git a/library/core/src/test/assets/mp3/play-trimmed.mp3.2.dump b/library/core/src/test/assets/mp3/play-trimmed.mp3.2.dump deleted file mode 100644 index 96b0cd259c..0000000000 --- a/library/core/src/test/assets/mp3/play-trimmed.mp3.2.dump +++ /dev/null @@ -1,34 +0,0 @@ -seekMap: - isSeekable = true - duration = 26125 - getPosition(0) = [[timeUs=0, position=0]] -numberOfTracks = 1 -track 0: - format: - bitrate = -1 - id = null - containerMimeType = null - sampleMimeType = audio/mpeg - maxInputSize = 4096 - width = -1 - height = -1 - frameRate = -1.0 - rotationDegrees = 0 - pixelWidthHeightRatio = 1.0 - channelCount = 2 - sampleRate = 44100 - pcmEncoding = -1 - encoderDelay = 0 - encoderPadding = 0 - subsampleOffsetUs = 9223372036854775807 - selectionFlags = 0 - language = null - drmInitData = - - initializationData: - total output bytes = 418 - sample count = 1 - sample 0: - time = 0 - flags = 1 - data = length 418, hash B819987 -tracksEnded = true diff --git a/library/core/src/test/assets/mp3/play-trimmed.mp3.3.dump b/library/core/src/test/assets/mp3/play-trimmed.mp3.3.dump deleted file mode 100644 index 96b0cd259c..0000000000 --- a/library/core/src/test/assets/mp3/play-trimmed.mp3.3.dump +++ /dev/null @@ -1,34 +0,0 @@ -seekMap: - isSeekable = true - duration = 26125 - getPosition(0) = [[timeUs=0, position=0]] -numberOfTracks = 1 -track 0: - format: - bitrate = -1 - id = null - containerMimeType = null - sampleMimeType = audio/mpeg - maxInputSize = 4096 - width = -1 - height = -1 - frameRate = -1.0 - rotationDegrees = 0 - pixelWidthHeightRatio = 1.0 - channelCount = 2 - sampleRate = 44100 - pcmEncoding = -1 - encoderDelay = 0 - encoderPadding = 0 - subsampleOffsetUs = 9223372036854775807 - selectionFlags = 0 - language = null - drmInitData = - - initializationData: - total output bytes = 418 - sample count = 1 - sample 0: - time = 0 - flags = 1 - data = length 418, hash B819987 -tracksEnded = true diff --git a/library/core/src/test/assets/mp3/play-trimmed.mp3.unklen.dump b/library/core/src/test/assets/mp3/play-trimmed.mp3.unklen.dump deleted file mode 100644 index d28cca025b..0000000000 --- a/library/core/src/test/assets/mp3/play-trimmed.mp3.unklen.dump +++ /dev/null @@ -1,34 +0,0 @@ -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/mpeg - maxInputSize = 4096 - width = -1 - height = -1 - frameRate = -1.0 - rotationDegrees = 0 - pixelWidthHeightRatio = 1.0 - channelCount = 2 - sampleRate = 44100 - pcmEncoding = -1 - encoderDelay = 0 - encoderPadding = 0 - subsampleOffsetUs = 9223372036854775807 - selectionFlags = 0 - language = null - drmInitData = - - initializationData: - total output bytes = 418 - sample count = 1 - sample 0: - time = 0 - flags = 1 - data = length 418, hash B819987 -tracksEnded = true diff --git a/library/core/src/test/assets/mp4/testvid_1022ms.mp4 b/library/core/src/test/assets/mp4/testvid_1022ms.mp4 deleted file mode 100644 index bbd2729c4d..0000000000 Binary files a/library/core/src/test/assets/mp4/testvid_1022ms.mp4 and /dev/null differ diff --git a/library/core/src/test/assets/ogg/bear_vorbis.ogg.3.dump b/library/core/src/test/assets/ogg/bear_vorbis.ogg.3.dump deleted file mode 100644 index 18d869030d..0000000000 --- a/library/core/src/test/assets/ogg/bear_vorbis.ogg.3.dump +++ /dev/null @@ -1,32 +0,0 @@ -seekMap: - isSeekable = true - duration = 2741000 - getPosition(0) = [[timeUs=0, position=3995]] -numberOfTracks = 1 -track 0: - format: - bitrate = 112000 - id = null - containerMimeType = null - sampleMimeType = audio/vorbis - maxInputSize = -1 - width = -1 - height = -1 - frameRate = -1.0 - rotationDegrees = 0 - pixelWidthHeightRatio = 1.0 - channelCount = 2 - sampleRate = 48000 - pcmEncoding = -1 - encoderDelay = 0 - encoderPadding = 0 - subsampleOffsetUs = 9223372036854775807 - selectionFlags = 0 - language = null - drmInitData = - - initializationData: - data = length 30, hash 9A8FF207 - data = length 3832, hash 8A406249 - total output bytes = 0 - sample count = 0 -tracksEnded = true diff --git a/library/core/src/test/assets/ts/sample.ps.1.dump b/library/core/src/test/assets/ts/sample.ps.1.dump deleted file mode 100644 index ce0f223bd4..0000000000 --- a/library/core/src/test/assets/ts/sample.ps.1.dump +++ /dev/null @@ -1,59 +0,0 @@ -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 deleted file mode 100644 index 7d0a77037d..0000000000 --- a/library/core/src/test/assets/ts/sample.ps.2.dump +++ /dev/null @@ -1,55 +0,0 @@ -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 deleted file mode 100644 index a7258cd7ef..0000000000 --- a/library/core/src/test/assets/ts/sample.ps.3.dump +++ /dev/null @@ -1,55 +0,0 @@ -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.ts.0.dump b/library/core/src/test/assets/ts/sample.ts.0.dump deleted file mode 100644 index b45a32fd3a..0000000000 --- a/library/core/src/test/assets/ts/sample.ts.0.dump +++ /dev/null @@ -1,103 +0,0 @@ -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 = 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.ts.1.dump b/library/core/src/test/assets/ts/sample.ts.1.dump deleted file mode 100644 index 5c361e1246..0000000000 --- a/library/core/src/test/assets/ts/sample.ts.1.dump +++ /dev/null @@ -1,103 +0,0 @@ -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 = 45026 - sample count = 2 - sample 0: - time = 55610 - flags = 1 - data = length 20711, hash 34341E8 - sample 1: - time = 88977 - 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 = 44699 - flags = 1 - data = length 1253, hash 727FD1C6 - sample 1: - time = 70821 - flags = 1 - data = length 1254, hash 73FB07B8 - sample 2: - time = 96944 - flags = 1 - data = length 1254, hash 73FB07B8 - sample 3: - time = 123066 - 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 deleted file mode 100644 index cec91ae2b9..0000000000 --- a/library/core/src/test/assets/ts/sample.ts.2.dump +++ /dev/null @@ -1,103 +0,0 @@ -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 = 45026 - sample count = 2 - sample 0: - time = 77854 - flags = 1 - data = length 20711, hash 34341E8 - sample 1: - time = 111221 - 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 = 66943 - flags = 1 - data = length 1253, hash 727FD1C6 - sample 1: - time = 93065 - flags = 1 - data = length 1254, hash 73FB07B8 - sample 2: - time = 119188 - flags = 1 - data = length 1254, hash 73FB07B8 - sample 3: - time = 145310 - 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 deleted file mode 100644 index d8238e1626..0000000000 --- a/library/core/src/test/assets/ts/sample.ts.3.dump +++ /dev/null @@ -1,87 +0,0 @@ -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_cbs.adts.3.dump b/library/core/src/test/assets/ts/sample_cbs.adts.3.dump deleted file mode 100644 index e134a711bf..0000000000 --- a/library/core/src/test/assets/ts/sample_cbs.adts.3.dump +++ /dev/null @@ -1,59 +0,0 @@ -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_truncated.adts.3.dump b/library/core/src/test/assets/ts/sample_cbs_truncated.adts.3.dump deleted file mode 100644 index 9186e04d6f..0000000000 --- a/library/core/src/test/assets/ts/sample_cbs_truncated.adts.3.dump +++ /dev/null @@ -1,55 +0,0 @@ -seekMap: - isSeekable = true - duration = 3355717 - 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 = 164 - sample count = 0 -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/wav/sample.wav.0.dump b/library/core/src/test/assets/wav/sample.wav.0.dump deleted file mode 100644 index a6c46f75fc..0000000000 --- a/library/core/src/test/assets/wav/sample.wav.0.dump +++ /dev/null @@ -1,42 +0,0 @@ -seekMap: - isSeekable = true - duration = 1000000 - getPosition(0) = [[timeUs=0, position=78]] -numberOfTracks = 1 -track 0: - format: - bitrate = 705600 - id = null - containerMimeType = null - sampleMimeType = audio/raw - maxInputSize = 32768 - width = -1 - height = -1 - frameRate = -1.0 - rotationDegrees = 0 - pixelWidthHeightRatio = 1.0 - channelCount = 1 - sampleRate = 44100 - pcmEncoding = 2 - encoderDelay = 0 - encoderPadding = 0 - subsampleOffsetUs = 9223372036854775807 - selectionFlags = 0 - language = null - drmInitData = - - initializationData: - total output bytes = 88200 - sample count = 3 - sample 0: - time = 0 - flags = 1 - data = length 32768, hash 9A8CEEBA - sample 1: - time = 371519 - flags = 1 - data = length 32768, hash C1717317 - sample 2: - time = 743038 - flags = 1 - data = length 22664, hash 819F5F62 -tracksEnded = true diff --git a/library/core/src/test/assets/wav/sample.wav.1.dump b/library/core/src/test/assets/wav/sample.wav.1.dump deleted file mode 100644 index 3cc70dc71f..0000000000 --- a/library/core/src/test/assets/wav/sample.wav.1.dump +++ /dev/null @@ -1,38 +0,0 @@ -seekMap: - isSeekable = true - duration = 1000000 - getPosition(0) = [[timeUs=0, position=78]] -numberOfTracks = 1 -track 0: - format: - bitrate = 705600 - id = null - containerMimeType = null - sampleMimeType = audio/raw - maxInputSize = 32768 - width = -1 - height = -1 - frameRate = -1.0 - rotationDegrees = 0 - pixelWidthHeightRatio = 1.0 - channelCount = 1 - sampleRate = 44100 - pcmEncoding = 2 - encoderDelay = 0 - encoderPadding = 0 - subsampleOffsetUs = 9223372036854775807 - selectionFlags = 0 - language = null - drmInitData = - - initializationData: - total output bytes = 58802 - sample count = 2 - sample 0: - time = 333310 - flags = 1 - data = length 32768, hash 42D6E860 - sample 1: - time = 704829 - flags = 1 - data = length 26034, hash 62692C38 -tracksEnded = true diff --git a/library/core/src/test/assets/wav/sample.wav.2.dump b/library/core/src/test/assets/wav/sample.wav.2.dump deleted file mode 100644 index 07ce135dfa..0000000000 --- a/library/core/src/test/assets/wav/sample.wav.2.dump +++ /dev/null @@ -1,34 +0,0 @@ -seekMap: - isSeekable = true - duration = 1000000 - getPosition(0) = [[timeUs=0, position=78]] -numberOfTracks = 1 -track 0: - format: - bitrate = 705600 - id = null - containerMimeType = null - sampleMimeType = audio/raw - maxInputSize = 32768 - width = -1 - height = -1 - frameRate = -1.0 - rotationDegrees = 0 - pixelWidthHeightRatio = 1.0 - channelCount = 1 - sampleRate = 44100 - pcmEncoding = 2 - encoderDelay = 0 - encoderPadding = 0 - subsampleOffsetUs = 9223372036854775807 - selectionFlags = 0 - language = null - drmInitData = - - initializationData: - total output bytes = 29402 - sample count = 1 - sample 0: - time = 666643 - flags = 1 - data = length 29402, hash 4241604E -tracksEnded = true diff --git a/library/core/src/test/assets/wav/sample.wav.3.dump b/library/core/src/test/assets/wav/sample.wav.3.dump deleted file mode 100644 index 82ed95ad60..0000000000 --- a/library/core/src/test/assets/wav/sample.wav.3.dump +++ /dev/null @@ -1,34 +0,0 @@ -seekMap: - isSeekable = true - duration = 1000000 - getPosition(0) = [[timeUs=0, position=78]] -numberOfTracks = 1 -track 0: - format: - bitrate = 705600 - id = null - containerMimeType = null - sampleMimeType = audio/raw - maxInputSize = 32768 - width = -1 - height = -1 - frameRate = -1.0 - rotationDegrees = 0 - pixelWidthHeightRatio = 1.0 - channelCount = 1 - sampleRate = 44100 - pcmEncoding = 2 - encoderDelay = 0 - encoderPadding = 0 - subsampleOffsetUs = 9223372036854775807 - selectionFlags = 0 - language = null - drmInitData = - - initializationData: - total output bytes = 2 - sample count = 1 - sample 0: - time = 999977 - flags = 1 - data = length 2, hash 116 -tracksEnded = true diff --git a/library/core/src/test/assets/webm/vorbis_codec_private b/library/core/src/test/assets/webm/vorbis_codec_private deleted file mode 100644 index 6a613449a7..0000000000 Binary files a/library/core/src/test/assets/webm/vorbis_codec_private and /dev/null differ diff --git a/library/core/src/test/java/com/google/android/exoplayer2/AudioFocusManagerTest.java b/library/core/src/test/java/com/google/android/exoplayer2/AudioFocusManagerTest.java index 9a44d6def6..2b9f476c61 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/AudioFocusManagerTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/AudioFocusManagerTest.java @@ -21,6 +21,7 @@ import static com.google.android.exoplayer2.AudioFocusManager.PLAYER_COMMAND_WAI import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.fail; import static org.robolectric.annotation.Config.TARGET_SDK; +import static org.robolectric.annotation.LooperMode.Mode.LEGACY; import android.content.Context; import android.media.AudioFocusRequest; @@ -36,9 +37,11 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.Shadows; import org.robolectric.annotation.Config; +import org.robolectric.annotation.LooperMode; import org.robolectric.shadows.ShadowAudioManager; /** Unit tests for {@link AudioFocusManager}. */ +@LooperMode(LEGACY) @RunWith(AndroidJUnit4.class) public class AudioFocusManagerTest { private static final int NO_COMMAND_RECEIVED = ~PLAYER_COMMAND_WAIT_FOR_CALLBACK; @@ -64,14 +67,11 @@ public class AudioFocusManagerTest { @Test public void setAudioAttributes_withNullUsage_doesNotManageAudioFocus() { - // Ensure that NULL audio attributes -> don't manage audio focus - assertThat( - audioFocusManager.setAudioAttributes( - /* audioAttributes= */ null, /* playWhenReady= */ false, Player.STATE_IDLE)) + audioFocusManager.setAudioAttributes(/* audioAttributes= */ null); + + assertThat(audioFocusManager.updateAudioFocus(/* playWhenReady= */ false, Player.STATE_IDLE)) .isEqualTo(PLAYER_COMMAND_DO_NOT_PLAY); - assertThat( - audioFocusManager.setAudioAttributes( - /* audioAttributes= */ null, /* playWhenReady= */ true, Player.STATE_READY)) + assertThat(audioFocusManager.updateAudioFocus(/* playWhenReady= */ true, Player.STATE_READY)) .isEqualTo(PLAYER_COMMAND_PLAY_WHEN_READY); ShadowAudioManager.AudioFocusRequest request = Shadows.shadowOf(audioManager).getLastAudioFocusRequest(); @@ -80,23 +80,21 @@ public class AudioFocusManagerTest { @Test @Config(maxSdk = 25) - public void setAudioAttributes_withNullUsage_releasesAudioFocus() { - // Create attributes and request audio focus. - AudioAttributes media = new AudioAttributes.Builder().setUsage(C.USAGE_MEDIA).build(); + public void setAudioAttributes_withNullUsage_abandonsAudioFocus() { Shadows.shadowOf(audioManager) .setNextFocusRequestResponse(AudioManager.AUDIOFOCUS_REQUEST_GRANTED); - assertThat( - audioFocusManager.setAudioAttributes( - media, /* playWhenReady= */ true, Player.STATE_READY)) + audioFocusManager.setAudioAttributes(AudioAttributes.DEFAULT); + + assertThat(audioFocusManager.updateAudioFocus(/* playWhenReady= */ true, Player.STATE_READY)) .isEqualTo(PLAYER_COMMAND_PLAY_WHEN_READY); ShadowAudioManager.AudioFocusRequest request = Shadows.shadowOf(audioManager).getLastAudioFocusRequest(); assertThat(request.durationHint).isEqualTo(AudioManager.AUDIOFOCUS_GAIN); - // Ensure that setting null audio attributes with audio focus releases audio focus. - assertThat( - audioFocusManager.setAudioAttributes( - /* audioAttributes= */ null, /* playWhenReady= */ true, Player.STATE_READY)) + // Ensure that setting null audio attributes with focus releases focus. + audioFocusManager.setAudioAttributes(/* audioAttributes= */ null); + + assertThat(audioFocusManager.updateAudioFocus(/* playWhenReady= */ true, Player.STATE_READY)) .isEqualTo(PLAYER_COMMAND_PLAY_WHEN_READY); AudioManager.OnAudioFocusChangeListener lastRequest = Shadows.shadowOf(audioManager).getLastAbandonedAudioFocusListener(); @@ -105,23 +103,20 @@ public class AudioFocusManagerTest { @Test @Config(minSdk = 26, maxSdk = TARGET_SDK) - public void setAudioAttributes_withNullUsage_releasesAudioFocus_v26() { - // Create attributes and request audio focus. - AudioAttributes media = new AudioAttributes.Builder().setUsage(C.USAGE_MEDIA).build(); + public void setAudioAttributes_withNullUsage_abandonsAudioFocus_v26() { Shadows.shadowOf(audioManager) .setNextFocusRequestResponse(AudioManager.AUDIOFOCUS_REQUEST_GRANTED); - assertThat( - audioFocusManager.setAudioAttributes( - media, /* playWhenReady= */ true, Player.STATE_READY)) + audioFocusManager.setAudioAttributes(AudioAttributes.DEFAULT); + + assertThat(audioFocusManager.updateAudioFocus(/* playWhenReady= */ true, Player.STATE_READY)) .isEqualTo(PLAYER_COMMAND_PLAY_WHEN_READY); ShadowAudioManager.AudioFocusRequest request = Shadows.shadowOf(audioManager).getLastAudioFocusRequest(); assertThat(getAudioFocusGainFromRequest(request)).isEqualTo(AudioManager.AUDIOFOCUS_GAIN); - // Ensure that setting null audio attributes with audio focus releases audio focus. - assertThat( - audioFocusManager.setAudioAttributes( - /* audioAttributes= */ null, /* playWhenReady= */ true, Player.STATE_READY)) + // Ensure that setting null audio attributes with focus releases focus. + audioFocusManager.setAudioAttributes(/* audioAttributes= */ null); + assertThat(audioFocusManager.updateAudioFocus(/* playWhenReady= */ true, Player.STATE_READY)) .isEqualTo(PLAYER_COMMAND_PLAY_WHEN_READY); AudioFocusRequest lastRequest = Shadows.shadowOf(audioManager).getLastAbandonedAudioFocusRequest(); @@ -130,10 +125,10 @@ public class AudioFocusManagerTest { @Test public void setAudioAttributes_withUsageAlarm_throwsIllegalArgumentException() { - // Ensure that audio attributes that map to AUDIOFOCUS_GAIN_TRANSIENT* throw + // USAGE_ALARM attributes map to AUDIOFOCUS_GAIN_TRANSIENT, which should result in failure. AudioAttributes alarm = new AudioAttributes.Builder().setUsage(C.USAGE_ALARM).build(); try { - audioFocusManager.setAudioAttributes(alarm, /* playWhenReady= */ false, Player.STATE_IDLE); + audioFocusManager.setAudioAttributes(alarm); fail(); } catch (IllegalArgumentException e) { // Expected @@ -142,14 +137,14 @@ public class AudioFocusManagerTest { @Test public void setAudioAttributes_withUsageMedia_usesAudioFocusGain() { - // Ensure setting media type audio attributes requests AUDIOFOCUS_GAIN. - AudioAttributes media = new AudioAttributes.Builder().setUsage(C.USAGE_MEDIA).build(); Shadows.shadowOf(audioManager) .setNextFocusRequestResponse(AudioManager.AUDIOFOCUS_REQUEST_GRANTED); - assertThat( - audioFocusManager.setAudioAttributes( - media, /* playWhenReady= */ true, Player.STATE_READY)) + AudioAttributes mediaAudioAttributes = + new AudioAttributes.Builder().setUsage(C.USAGE_MEDIA).build(); + audioFocusManager.setAudioAttributes(mediaAudioAttributes); + + assertThat(audioFocusManager.updateAudioFocus(/* playWhenReady= */ true, Player.STATE_READY)) .isEqualTo(PLAYER_COMMAND_PLAY_WHEN_READY); ShadowAudioManager.AudioFocusRequest request = Shadows.shadowOf(audioManager).getLastAudioFocusRequest(); @@ -157,15 +152,12 @@ public class AudioFocusManagerTest { } @Test - public void setAudioAttributes_inStateEnded_requestsAudioFocus() { - // Ensure setting audio attributes when player is in STATE_ENDED requests audio focus. - AudioAttributes media = new AudioAttributes.Builder().setUsage(C.USAGE_MEDIA).build(); + public void setAudioAttributes_inEndedState_requestsAudioFocus() { Shadows.shadowOf(audioManager) .setNextFocusRequestResponse(AudioManager.AUDIOFOCUS_REQUEST_GRANTED); - assertThat( - audioFocusManager.setAudioAttributes( - media, /* playWhenReady= */ true, Player.STATE_ENDED)) + audioFocusManager.setAudioAttributes(AudioAttributes.DEFAULT); + assertThat(audioFocusManager.updateAudioFocus(/* playWhenReady= */ true, Player.STATE_ENDED)) .isEqualTo(PLAYER_COMMAND_PLAY_WHEN_READY); ShadowAudioManager.AudioFocusRequest request = Shadows.shadowOf(audioManager).getLastAudioFocusRequest(); @@ -173,61 +165,216 @@ public class AudioFocusManagerTest { } @Test - public void handlePrepare_afterSetAudioAttributes_setsPlayerCommandPlayWhenReady() { - // Ensure that when playWhenReady is true while the player is IDLE, audio focus is only - // requested after calling handlePrepare. - AudioAttributes media = new AudioAttributes.Builder().setUsage(C.USAGE_MEDIA).build(); - + public void updateAudioFocus_idleToBuffering_setsPlayerCommandPlayWhenReady() { Shadows.shadowOf(audioManager) .setNextFocusRequestResponse(AudioManager.AUDIOFOCUS_REQUEST_GRANTED); - assertThat( - audioFocusManager.setAudioAttributes( - media, /* playWhenReady= */ true, Player.STATE_IDLE)) + audioFocusManager.setAudioAttributes(AudioAttributes.DEFAULT); + assertThat(audioFocusManager.updateAudioFocus(/* playWhenReady= */ true, Player.STATE_IDLE)) .isEqualTo(PLAYER_COMMAND_PLAY_WHEN_READY); assertThat(Shadows.shadowOf(audioManager).getLastAudioFocusRequest()).isNull(); - assertThat(audioFocusManager.handlePrepare(/* playWhenReady= */ true)) + assertThat( + audioFocusManager.updateAudioFocus(/* playWhenReady= */ true, Player.STATE_BUFFERING)) + .isEqualTo(PLAYER_COMMAND_PLAY_WHEN_READY); + ShadowAudioManager.AudioFocusRequest request = + Shadows.shadowOf(audioManager).getLastAudioFocusRequest(); + assertThat(getAudioFocusGainFromRequest(request)).isEqualTo(AudioManager.AUDIOFOCUS_GAIN); + } + + @Test + public void updateAudioFocus_pausedToPlaying_setsPlayerCommandPlayWhenReady() { + Shadows.shadowOf(audioManager) + .setNextFocusRequestResponse(AudioManager.AUDIOFOCUS_REQUEST_GRANTED); + audioFocusManager.setAudioAttributes(AudioAttributes.DEFAULT); + + // Audio focus should not be requested yet, because playWhenReady is false. + assertThat(audioFocusManager.updateAudioFocus(/* playWhenReady= */ false, Player.STATE_READY)) + .isEqualTo(PLAYER_COMMAND_DO_NOT_PLAY); + assertThat(Shadows.shadowOf(audioManager).getLastAudioFocusRequest()).isNull(); + + // Audio focus should be requested now that playWhenReady is true. + assertThat(audioFocusManager.updateAudioFocus(/* playWhenReady= */ true, Player.STATE_READY)) + .isEqualTo(PLAYER_COMMAND_PLAY_WHEN_READY); + ShadowAudioManager.AudioFocusRequest request = + Shadows.shadowOf(audioManager).getLastAudioFocusRequest(); + assertThat(getAudioFocusGainFromRequest(request)).isEqualTo(AudioManager.AUDIOFOCUS_GAIN); + } + + @Test + public void updateAudioFocus_pausedToPlaying_withTransientLoss_setsPlayerCommandPlayWhenReady() { + Shadows.shadowOf(audioManager) + .setNextFocusRequestResponse(AudioManager.AUDIOFOCUS_REQUEST_GRANTED); + audioFocusManager.setAudioAttributes(AudioAttributes.DEFAULT); + + assertThat(audioFocusManager.updateAudioFocus(/* playWhenReady= */ true, Player.STATE_READY)) + .isEqualTo(PLAYER_COMMAND_PLAY_WHEN_READY); + + // Simulate transient focus loss. + audioFocusManager.getFocusListener().onAudioFocusChange(AudioManager.AUDIOFOCUS_LOSS_TRANSIENT); + + // Focus should be re-requested rather than staying in a state of transient focus loss. See + // https://github.com/google/ExoPlayer/issues/7182 for context. + assertThat(audioFocusManager.updateAudioFocus(/* playWhenReady= */ true, Player.STATE_READY)) .isEqualTo(PLAYER_COMMAND_PLAY_WHEN_READY); } @Test - public void handleSetPlayWhenReady_afterSetAudioAttributes_setsPlayerCommandPlayWhenReady() { - // Ensure that audio focus is not requested until playWhenReady is true. - AudioAttributes media = new AudioAttributes.Builder().setUsage(C.USAGE_MEDIA).build(); - + public void updateAudioFocus_pausedToPlaying_withTransientDuck_setsPlayerCommandPlayWhenReady() { Shadows.shadowOf(audioManager) .setNextFocusRequestResponse(AudioManager.AUDIOFOCUS_REQUEST_GRANTED); + audioFocusManager.setAudioAttributes(AudioAttributes.DEFAULT); - assertThat(audioFocusManager.handlePrepare(/* playWhenReady= */ false)) - .isEqualTo(PLAYER_COMMAND_DO_NOT_PLAY); - assertThat(Shadows.shadowOf(audioManager).getLastAudioFocusRequest()).isNull(); - assertThat( - audioFocusManager.setAudioAttributes( - media, /* playWhenReady= */ false, Player.STATE_READY)) - .isEqualTo(PLAYER_COMMAND_DO_NOT_PLAY); - assertThat(Shadows.shadowOf(audioManager).getLastAudioFocusRequest()).isNull(); - assertThat( - audioFocusManager.handleSetPlayWhenReady(/* playWhenReady= */ true, Player.STATE_READY)) + assertThat(audioFocusManager.updateAudioFocus(/* playWhenReady= */ true, Player.STATE_READY)) .isEqualTo(PLAYER_COMMAND_PLAY_WHEN_READY); + + // Simulate transient ducking. + audioFocusManager + .getFocusListener() + .onAudioFocusChange(AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK); + assertThat(testPlayerControl.lastVolumeMultiplier).isLessThan(1.0f); + + // Focus should be re-requested, rather than staying in a state of transient ducking. This + // should restore the volume to 1.0. See https://github.com/google/ExoPlayer/issues/7182 for + // context. + assertThat(audioFocusManager.updateAudioFocus(/* playWhenReady= */ true, Player.STATE_READY)) + .isEqualTo(PLAYER_COMMAND_PLAY_WHEN_READY); + assertThat(testPlayerControl.lastVolumeMultiplier).isEqualTo(1.0f); } @Test - public void onAudioFocusChange_withDuckEnabled_volumeReducedAndRestored() { - // Ensure that the volume multiplier is adjusted when audio focus is lost to - // AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK, and returns to the default value after focus is - // regained. - AudioAttributes media = new AudioAttributes.Builder().setUsage(C.USAGE_MEDIA).build(); - + public void updateAudioFocus_abandonFocusWhenDucked_restoresFullVolume() { Shadows.shadowOf(audioManager) .setNextFocusRequestResponse(AudioManager.AUDIOFOCUS_REQUEST_GRANTED); - assertThat( - audioFocusManager.setAudioAttributes( - media, /* playWhenReady= */ true, Player.STATE_READY)) + audioFocusManager.setAudioAttributes(AudioAttributes.DEFAULT); + + assertThat(audioFocusManager.updateAudioFocus(/* playWhenReady= */ true, Player.STATE_READY)) + .isEqualTo(PLAYER_COMMAND_PLAY_WHEN_READY); + + // Simulate transient ducking. + audioFocusManager + .getFocusListener() + .onAudioFocusChange(AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK); + assertThat(testPlayerControl.lastVolumeMultiplier).isLessThan(1.0f); + + // Configure the manager to no longer handle focus. + audioFocusManager.setAudioAttributes(null); + + // Focus should be abandoned, which should restore the volume to 1.0. + assertThat(audioFocusManager.updateAudioFocus(/* playWhenReady= */ true, Player.STATE_READY)) + .isEqualTo(PLAYER_COMMAND_PLAY_WHEN_READY); + assertThat(testPlayerControl.lastVolumeMultiplier).isEqualTo(1.0f); + } + + @Test + @Config(maxSdk = 25) + public void updateAudioFocus_readyToIdle_abandonsAudioFocus() { + Shadows.shadowOf(audioManager) + .setNextFocusRequestResponse(AudioManager.AUDIOFOCUS_REQUEST_GRANTED); + audioFocusManager.setAudioAttributes(AudioAttributes.DEFAULT); + + assertThat(audioFocusManager.updateAudioFocus(/* playWhenReady= */ true, Player.STATE_READY)) + .isEqualTo(PLAYER_COMMAND_PLAY_WHEN_READY); + assertThat(Shadows.shadowOf(audioManager).getLastAbandonedAudioFocusListener()).isNull(); + + ShadowAudioManager.AudioFocusRequest request = + Shadows.shadowOf(audioManager).getLastAudioFocusRequest(); + assertThat(audioFocusManager.updateAudioFocus(/* playWhenReady= */ true, Player.STATE_IDLE)) + .isEqualTo(PLAYER_COMMAND_PLAY_WHEN_READY); + assertThat(Shadows.shadowOf(audioManager).getLastAbandonedAudioFocusListener()) + .isEqualTo(request.listener); + } + + @Test + @Config(minSdk = 26, maxSdk = TARGET_SDK) + public void updateAudioFocus_readyToIdle_abandonsAudioFocus_v26() { + Shadows.shadowOf(audioManager) + .setNextFocusRequestResponse(AudioManager.AUDIOFOCUS_REQUEST_GRANTED); + audioFocusManager.setAudioAttributes(AudioAttributes.DEFAULT); + + assertThat(audioFocusManager.updateAudioFocus(/* playWhenReady= */ true, Player.STATE_READY)) + .isEqualTo(PLAYER_COMMAND_PLAY_WHEN_READY); + assertThat(Shadows.shadowOf(audioManager).getLastAbandonedAudioFocusRequest()).isNull(); + + ShadowAudioManager.AudioFocusRequest request = + Shadows.shadowOf(audioManager).getLastAudioFocusRequest(); + assertThat(audioFocusManager.updateAudioFocus(/* playWhenReady= */ true, Player.STATE_IDLE)) + .isEqualTo(PLAYER_COMMAND_PLAY_WHEN_READY); + assertThat(Shadows.shadowOf(audioManager).getLastAbandonedAudioFocusRequest()) + .isEqualTo(request.audioFocusRequest); + } + + @Test + @Config(maxSdk = 25) + public void updateAudioFocus_readyToIdle_withoutFocus_isNoOp() { + Shadows.shadowOf(audioManager) + .setNextFocusRequestResponse(AudioManager.AUDIOFOCUS_REQUEST_GRANTED); + audioFocusManager.setAudioAttributes(null); + + assertThat(audioFocusManager.updateAudioFocus(/* playWhenReady= */ false, Player.STATE_READY)) + .isEqualTo(PLAYER_COMMAND_DO_NOT_PLAY); + assertThat(Shadows.shadowOf(audioManager).getLastAbandonedAudioFocusListener()).isNull(); + ShadowAudioManager.AudioFocusRequest request = + Shadows.shadowOf(audioManager).getLastAudioFocusRequest(); + assertThat(request).isNull(); + + assertThat(audioFocusManager.updateAudioFocus(/* playWhenReady= */ false, Player.STATE_IDLE)) + .isEqualTo(PLAYER_COMMAND_DO_NOT_PLAY); + assertThat(Shadows.shadowOf(audioManager).getLastAbandonedAudioFocusListener()).isNull(); + } + + @Test + @Config(minSdk = 26, maxSdk = TARGET_SDK) + public void updateAudioFocus_readyToIdle_withoutFocus_isNoOp_v26() { + Shadows.shadowOf(audioManager) + .setNextFocusRequestResponse(AudioManager.AUDIOFOCUS_REQUEST_GRANTED); + audioFocusManager.setAudioAttributes(null); + + assertThat(audioFocusManager.updateAudioFocus(/* playWhenReady= */ false, Player.STATE_READY)) + .isEqualTo(PLAYER_COMMAND_DO_NOT_PLAY); + assertThat(Shadows.shadowOf(audioManager).getLastAbandonedAudioFocusRequest()).isNull(); + ShadowAudioManager.AudioFocusRequest request = + Shadows.shadowOf(audioManager).getLastAudioFocusRequest(); + assertThat(request).isNull(); + + assertThat(audioFocusManager.updateAudioFocus(/* playWhenReady= */ false, Player.STATE_IDLE)) + .isEqualTo(PLAYER_COMMAND_DO_NOT_PLAY); + assertThat(Shadows.shadowOf(audioManager).getLastAbandonedAudioFocusRequest()).isNull(); + } + + @Test + public void release_doesNotCallPlayerControlToRestoreVolume() { + Shadows.shadowOf(audioManager) + .setNextFocusRequestResponse(AudioManager.AUDIOFOCUS_REQUEST_GRANTED); + audioFocusManager.setAudioAttributes(AudioAttributes.DEFAULT); + + assertThat(audioFocusManager.updateAudioFocus(/* playWhenReady= */ true, Player.STATE_READY)) + .isEqualTo(PLAYER_COMMAND_PLAY_WHEN_READY); + + // Simulate transient ducking. + audioFocusManager + .getFocusListener() + .onAudioFocusChange(AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK); + assertThat(testPlayerControl.lastVolumeMultiplier).isLessThan(1.0f); + + audioFocusManager.release(); + + // PlaybackController.setVolumeMultiplier should not have been called to restore the volume. + assertThat(testPlayerControl.lastVolumeMultiplier).isLessThan(1.0f); + } + + @Test + public void onAudioFocusChange_withDuckEnabled_reducesAndRestoresVolume() { + Shadows.shadowOf(audioManager) + .setNextFocusRequestResponse(AudioManager.AUDIOFOCUS_REQUEST_GRANTED); + audioFocusManager.setAudioAttributes(AudioAttributes.DEFAULT); + + assertThat(audioFocusManager.updateAudioFocus(/* playWhenReady= */ true, Player.STATE_READY)) .isEqualTo(PLAYER_COMMAND_PLAY_WHEN_READY); audioFocusManager .getFocusListener() .onAudioFocusChange(AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK); + assertThat(testPlayerControl.lastVolumeMultiplier).isLessThan(1.0f); assertThat(testPlayerControl.lastPlayerCommand).isEqualTo(NO_COMMAND_RECEIVED); audioFocusManager.getFocusListener().onAudioFocusChange(AudioManager.AUDIOFOCUS_GAIN); @@ -236,19 +383,17 @@ public class AudioFocusManagerTest { @Test public void onAudioFocusChange_withPausedWhenDucked_sendsCommandWaitForCallback() { - // Ensure that the player is commanded to pause when audio focus is lost with - // AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK and the content type is CONTENT_TYPE_SPEECH. - AudioAttributes media = + Shadows.shadowOf(audioManager) + .setNextFocusRequestResponse(AudioManager.AUDIOFOCUS_REQUEST_GRANTED); + + AudioAttributes speechAudioAttributes = new AudioAttributes.Builder() .setUsage(C.USAGE_MEDIA) .setContentType(C.CONTENT_TYPE_SPEECH) .build(); + audioFocusManager.setAudioAttributes(speechAudioAttributes); - Shadows.shadowOf(audioManager) - .setNextFocusRequestResponse(AudioManager.AUDIOFOCUS_REQUEST_GRANTED); - assertThat( - audioFocusManager.setAudioAttributes( - media, /* playWhenReady= */ true, Player.STATE_READY)) + assertThat(audioFocusManager.updateAudioFocus(/* playWhenReady= */ true, Player.STATE_READY)) .isEqualTo(PLAYER_COMMAND_PLAY_WHEN_READY); audioFocusManager @@ -261,16 +406,12 @@ public class AudioFocusManagerTest { } @Test - public void onAudioFocusChange_withTransientLost_sendsCommandWaitForCallback() { - // Ensure that the player is commanded to pause when audio focus is lost with - // AUDIOFOCUS_LOSS_TRANSIENT. - AudioAttributes media = new AudioAttributes.Builder().setUsage(C.USAGE_MEDIA).build(); - + public void onAudioFocusChange_withTransientLoss_sendsCommandWaitForCallback() { Shadows.shadowOf(audioManager) .setNextFocusRequestResponse(AudioManager.AUDIOFOCUS_REQUEST_GRANTED); - assertThat( - audioFocusManager.setAudioAttributes( - media, /* playWhenReady= */ true, Player.STATE_READY)) + audioFocusManager.setAudioAttributes(AudioAttributes.DEFAULT); + + assertThat(audioFocusManager.updateAudioFocus(/* playWhenReady= */ true, Player.STATE_READY)) .isEqualTo(PLAYER_COMMAND_PLAY_WHEN_READY); audioFocusManager.getFocusListener().onAudioFocusChange(AudioManager.AUDIOFOCUS_LOSS_TRANSIENT); @@ -280,20 +421,12 @@ public class AudioFocusManagerTest { @Test @Config(maxSdk = 25) - public void onAudioFocusChange_withAudioFocusLost_sendsDoNotPlayAndAbandondsFocus() { - // Ensure that AUDIOFOCUS_LOSS causes AudioFocusManager to pause playback and abandon audio - // focus. - AudioAttributes media = - new AudioAttributes.Builder() - .setUsage(C.USAGE_MEDIA) - .setContentType(C.CONTENT_TYPE_SPEECH) - .build(); - + public void onAudioFocusChange_withFocusLoss_sendsDoNotPlayAndAbandonsFocus() { Shadows.shadowOf(audioManager) .setNextFocusRequestResponse(AudioManager.AUDIOFOCUS_REQUEST_GRANTED); - assertThat( - audioFocusManager.setAudioAttributes( - media, /* playWhenReady= */ true, Player.STATE_READY)) + audioFocusManager.setAudioAttributes(AudioAttributes.DEFAULT); + + assertThat(audioFocusManager.updateAudioFocus(/* playWhenReady= */ true, Player.STATE_READY)) .isEqualTo(PLAYER_COMMAND_PLAY_WHEN_READY); assertThat(Shadows.shadowOf(audioManager).getLastAbandonedAudioFocusListener()).isNull(); @@ -307,20 +440,12 @@ public class AudioFocusManagerTest { @Test @Config(minSdk = 26, maxSdk = TARGET_SDK) - public void onAudioFocusChange_withAudioFocusLost_sendsDoNotPlayAndAbandondsFocus_v26() { - // Ensure that AUDIOFOCUS_LOSS causes AudioFocusManager to pause playback and abandon audio - // focus. - AudioAttributes media = - new AudioAttributes.Builder() - .setUsage(C.USAGE_MEDIA) - .setContentType(C.CONTENT_TYPE_SPEECH) - .build(); - + public void onAudioFocusChange_withFocusLoss_sendsDoNotPlayAndAbandonsFocus_v26() { Shadows.shadowOf(audioManager) .setNextFocusRequestResponse(AudioManager.AUDIOFOCUS_REQUEST_GRANTED); - assertThat( - audioFocusManager.setAudioAttributes( - media, /* playWhenReady= */ true, Player.STATE_READY)) + audioFocusManager.setAudioAttributes(AudioAttributes.DEFAULT); + + assertThat(audioFocusManager.updateAudioFocus(/* playWhenReady= */ true, Player.STATE_READY)) .isEqualTo(PLAYER_COMMAND_PLAY_WHEN_READY); assertThat(Shadows.shadowOf(audioManager).getLastAbandonedAudioFocusRequest()).isNull(); @@ -330,120 +455,6 @@ public class AudioFocusManagerTest { .isEqualTo(Shadows.shadowOf(audioManager).getLastAudioFocusRequest().audioFocusRequest); } - @Test - @Config(maxSdk = 25) - public void handleStop_withAudioFocus_abandonsAudioFocus() { - // Ensure that handleStop causes AudioFocusManager to abandon audio focus. - AudioAttributes media = - new AudioAttributes.Builder() - .setUsage(C.USAGE_MEDIA) - .setContentType(C.CONTENT_TYPE_SPEECH) - .build(); - - Shadows.shadowOf(audioManager) - .setNextFocusRequestResponse(AudioManager.AUDIOFOCUS_REQUEST_GRANTED); - assertThat( - audioFocusManager.setAudioAttributes( - media, /* playWhenReady= */ true, Player.STATE_READY)) - .isEqualTo(PLAYER_COMMAND_PLAY_WHEN_READY); - assertThat(Shadows.shadowOf(audioManager).getLastAbandonedAudioFocusListener()).isNull(); - - ShadowAudioManager.AudioFocusRequest request = - Shadows.shadowOf(audioManager).getLastAudioFocusRequest(); - audioFocusManager.handleStop(); - assertThat(Shadows.shadowOf(audioManager).getLastAbandonedAudioFocusListener()) - .isEqualTo(request.listener); - } - - @Test - @Config(minSdk = 26, maxSdk = TARGET_SDK) - public void handleStop_withAudioFocus_abandonsAudioFocus_v26() { - // Ensure that handleStop causes AudioFocusManager to abandon audio focus. - AudioAttributes media = - new AudioAttributes.Builder() - .setUsage(C.USAGE_MEDIA) - .setContentType(C.CONTENT_TYPE_SPEECH) - .build(); - - Shadows.shadowOf(audioManager) - .setNextFocusRequestResponse(AudioManager.AUDIOFOCUS_REQUEST_GRANTED); - assertThat( - audioFocusManager.setAudioAttributes( - media, /* playWhenReady= */ true, Player.STATE_READY)) - .isEqualTo(PLAYER_COMMAND_PLAY_WHEN_READY); - assertThat(Shadows.shadowOf(audioManager).getLastAbandonedAudioFocusRequest()).isNull(); - - ShadowAudioManager.AudioFocusRequest request = - Shadows.shadowOf(audioManager).getLastAudioFocusRequest(); - audioFocusManager.handleStop(); - assertThat(Shadows.shadowOf(audioManager).getLastAbandonedAudioFocusRequest()) - .isEqualTo(request.audioFocusRequest); - } - - @Test - @Config(maxSdk = 25) - public void handleStop_withoutAudioFocus_stillAbandonsFocus() { - // Ensure that handleStop causes AudioFocusManager to call through to abandon audio focus - // even if focus wasn't requested. - AudioAttributes media = - new AudioAttributes.Builder() - .setUsage(C.USAGE_MEDIA) - .setContentType(C.CONTENT_TYPE_SPEECH) - .build(); - - Shadows.shadowOf(audioManager) - .setNextFocusRequestResponse(AudioManager.AUDIOFOCUS_REQUEST_GRANTED); - assertThat( - audioFocusManager.setAudioAttributes( - media, /* playWhenReady= */ false, Player.STATE_READY)) - .isEqualTo(PLAYER_COMMAND_DO_NOT_PLAY); - assertThat(Shadows.shadowOf(audioManager).getLastAbandonedAudioFocusListener()).isNull(); - ShadowAudioManager.AudioFocusRequest request = - Shadows.shadowOf(audioManager).getLastAudioFocusRequest(); - assertThat(request).isNull(); - - audioFocusManager.handleStop(); - assertThat(Shadows.shadowOf(audioManager).getLastAbandonedAudioFocusListener()).isNotNull(); - } - - @Test - @Config(maxSdk = 25) - public void handleStop_withoutHandlingAudioFocus_isNoOp() { - // Ensure that handleStop is a no-op if audio focus isn't handled. - Shadows.shadowOf(audioManager) - .setNextFocusRequestResponse(AudioManager.AUDIOFOCUS_REQUEST_GRANTED); - assertThat( - audioFocusManager.setAudioAttributes( - /* audioAttributes= */ null, /* playWhenReady= */ false, Player.STATE_READY)) - .isEqualTo(PLAYER_COMMAND_DO_NOT_PLAY); - assertThat(Shadows.shadowOf(audioManager).getLastAbandonedAudioFocusListener()).isNull(); - ShadowAudioManager.AudioFocusRequest request = - Shadows.shadowOf(audioManager).getLastAudioFocusRequest(); - assertThat(request).isNull(); - - audioFocusManager.handleStop(); - assertThat(Shadows.shadowOf(audioManager).getLastAbandonedAudioFocusListener()).isNull(); - } - - @Test - @Config(minSdk = 26, maxSdk = TARGET_SDK) - public void handleStop_withoutHandlingAudioFocus_isNoOp_v26() { - // Ensure that handleStop is a no-op if audio focus isn't handled. - Shadows.shadowOf(audioManager) - .setNextFocusRequestResponse(AudioManager.AUDIOFOCUS_REQUEST_GRANTED); - assertThat( - audioFocusManager.setAudioAttributes( - /* audioAttributes= */ null, /* playWhenReady= */ false, Player.STATE_READY)) - .isEqualTo(PLAYER_COMMAND_DO_NOT_PLAY); - assertThat(Shadows.shadowOf(audioManager).getLastAbandonedAudioFocusRequest()).isNull(); - ShadowAudioManager.AudioFocusRequest request = - Shadows.shadowOf(audioManager).getLastAudioFocusRequest(); - assertThat(request).isNull(); - - audioFocusManager.handleStop(); - assertThat(Shadows.shadowOf(audioManager).getLastAbandonedAudioFocusRequest()).isNull(); - } - private int getAudioFocusGainFromRequest(ShadowAudioManager.AudioFocusRequest audioFocusRequest) { return Util.SDK_INT >= 26 ? audioFocusRequest.audioFocusRequest.getFocusGain() diff --git a/library/core/src/test/java/com/google/android/exoplayer2/DefaultLoadControlTest.java b/library/core/src/test/java/com/google/android/exoplayer2/DefaultLoadControlTest.java index 31f432db15..f7065fbbc5 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/DefaultLoadControlTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/DefaultLoadControlTest.java @@ -19,6 +19,8 @@ import static com.google.common.truth.Truth.assertThat; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.DefaultLoadControl.Builder; +import com.google.android.exoplayer2.source.TrackGroupArray; +import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.upstream.DefaultAllocator; import org.junit.Before; import org.junit.Test; @@ -29,8 +31,8 @@ import org.junit.runner.RunWith; public class DefaultLoadControlTest { private static final float SPEED = 1f; - private static final long MIN_BUFFER_US = C.msToUs(DefaultLoadControl.DEFAULT_MIN_BUFFER_MS); private static final long MAX_BUFFER_US = C.msToUs(DefaultLoadControl.DEFAULT_MAX_BUFFER_MS); + private static final long MIN_BUFFER_US = MAX_BUFFER_US / 2; private static final int TARGET_BUFFER_BYTES = C.DEFAULT_BUFFER_SEGMENT_SIZE * 2; private Builder builder; @@ -44,76 +46,165 @@ public class DefaultLoadControlTest { } @Test - public void testShouldContinueLoading_untilMaxBufferExceeded() { - createDefaultLoadControl(); - assertThat(loadControl.shouldContinueLoading(/* bufferedDurationUs= */ 0, SPEED)).isTrue(); - assertThat(loadControl.shouldContinueLoading(MIN_BUFFER_US, SPEED)).isTrue(); - assertThat(loadControl.shouldContinueLoading(MAX_BUFFER_US - 1, SPEED)).isTrue(); - assertThat(loadControl.shouldContinueLoading(MAX_BUFFER_US, SPEED)).isFalse(); - } - - @Test - public void testShouldNotContinueLoadingOnceBufferingStopped_untilBelowMinBuffer() { - createDefaultLoadControl(); - assertThat(loadControl.shouldContinueLoading(MAX_BUFFER_US, SPEED)).isFalse(); - assertThat(loadControl.shouldContinueLoading(MAX_BUFFER_US - 1, SPEED)).isFalse(); - assertThat(loadControl.shouldContinueLoading(MIN_BUFFER_US, SPEED)).isFalse(); - assertThat(loadControl.shouldContinueLoading(MIN_BUFFER_US - 1, SPEED)).isTrue(); - } - - @Test - public void testShouldContinueLoadingWithTargetBufferBytesReached_untilMinBufferReached() { - createDefaultLoadControl(); - makeSureTargetBufferBytesReached(); - - assertThat(loadControl.shouldContinueLoading(/* bufferedDurationUs= */ 0, SPEED)).isTrue(); - assertThat(loadControl.shouldContinueLoading(MIN_BUFFER_US - 1, SPEED)).isTrue(); - assertThat(loadControl.shouldContinueLoading(MIN_BUFFER_US, SPEED)).isFalse(); - assertThat(loadControl.shouldContinueLoading(MAX_BUFFER_US, SPEED)).isFalse(); - } - - @Test - public void testShouldNeverContinueLoading_ifMaxBufferReachedAndNotPrioritizeTimeOverSize() { - builder.setPrioritizeTimeOverSizeThresholds(false); - createDefaultLoadControl(); - // Put loadControl in buffering state. - assertThat(loadControl.shouldContinueLoading(/* bufferedDurationUs= */ 0, SPEED)).isTrue(); - makeSureTargetBufferBytesReached(); - - assertThat(loadControl.shouldContinueLoading(/* bufferedDurationUs= */ 0, SPEED)).isFalse(); - assertThat(loadControl.shouldContinueLoading(MIN_BUFFER_US, SPEED)).isFalse(); - assertThat(loadControl.shouldContinueLoading(MAX_BUFFER_US, SPEED)).isFalse(); - } - - @Test - public void testShouldContinueLoadingWithMinBufferReached_inFastPlayback() { + public void shouldContinueLoading_untilMaxBufferExceeded() { createDefaultLoadControl(); - // At normal playback speed, we stop buffering when the buffer reaches the minimum. - assertThat(loadControl.shouldContinueLoading(MIN_BUFFER_US, SPEED)).isFalse(); - - // At double playback speed, we continue loading. - assertThat(loadControl.shouldContinueLoading(MIN_BUFFER_US, /* playbackSpeed= */ 2f)).isTrue(); - } - - @Test - public void testShouldNotContinueLoadingWithMaxBufferReached_inFastPlayback() { - createDefaultLoadControl(); - - assertThat(loadControl.shouldContinueLoading(MAX_BUFFER_US, /* playbackSpeed= */ 100f)) + assertThat( + loadControl.shouldContinueLoading( + /* playbackPositionUs= */ 0, /* bufferedDurationUs= */ 0, SPEED)) + .isTrue(); + assertThat( + loadControl.shouldContinueLoading( + /* playbackPositionUs= */ 0, MAX_BUFFER_US - 1, SPEED)) + .isTrue(); + assertThat(loadControl.shouldContinueLoading(/* playbackPositionUs= */ 0, MAX_BUFFER_US, SPEED)) .isFalse(); } @Test - public void testStartsPlayback_whenMinBufferSizeReached() { + public void shouldNotContinueLoadingOnceBufferingStopped_untilBelowMinBuffer() { + builder.setBufferDurationsMs( + /* minBufferMs= */ (int) C.usToMs(MIN_BUFFER_US), + /* maxBufferMs= */ (int) C.usToMs(MAX_BUFFER_US), + /* bufferForPlaybackMs= */ 0, + /* bufferForPlaybackAfterRebufferMs= */ 0); createDefaultLoadControl(); + + assertThat(loadControl.shouldContinueLoading(/* playbackPositionUs= */ 0, MAX_BUFFER_US, SPEED)) + .isFalse(); + assertThat( + loadControl.shouldContinueLoading( + /* playbackPositionUs= */ 0, MAX_BUFFER_US - 1, SPEED)) + .isFalse(); + assertThat(loadControl.shouldContinueLoading(/* playbackPositionUs= */ 0, MIN_BUFFER_US, SPEED)) + .isFalse(); + assertThat( + loadControl.shouldContinueLoading( + /* playbackPositionUs= */ 0, MIN_BUFFER_US - 1, SPEED)) + .isTrue(); + } + + @Test + public void continueLoadingOnceBufferingStopped_andBufferAlmostEmpty_evenIfMinBufferNotReached() { + builder.setBufferDurationsMs( + /* minBufferMs= */ 0, + /* maxBufferMs= */ (int) C.usToMs(MAX_BUFFER_US), + /* bufferForPlaybackMs= */ 0, + /* bufferForPlaybackAfterRebufferMs= */ 0); + createDefaultLoadControl(); + + assertThat(loadControl.shouldContinueLoading(/* playbackPositionUs= */ 0, MAX_BUFFER_US, SPEED)) + .isFalse(); + assertThat( + loadControl.shouldContinueLoading( + /* playbackPositionUs= */ 0, 5 * C.MICROS_PER_SECOND, SPEED)) + .isFalse(); + assertThat(loadControl.shouldContinueLoading(/* playbackPositionUs= */ 0, 500L, SPEED)) + .isTrue(); + } + + @Test + public void shouldContinueLoadingWithTargetBufferBytesReached_untilMinBufferReached() { + builder.setPrioritizeTimeOverSizeThresholds(true); + builder.setBufferDurationsMs( + /* minBufferMs= */ (int) C.usToMs(MIN_BUFFER_US), + /* maxBufferMs= */ (int) C.usToMs(MAX_BUFFER_US), + /* bufferForPlaybackMs= */ 0, + /* bufferForPlaybackAfterRebufferMs= */ 0); + createDefaultLoadControl(); + makeSureTargetBufferBytesReached(); + + assertThat( + loadControl.shouldContinueLoading( + /* playbackPositionUs= */ 0, /* bufferedDurationUs= */ 0, SPEED)) + .isTrue(); + assertThat( + loadControl.shouldContinueLoading( + /* playbackPositionUs= */ 0, MIN_BUFFER_US - 1, SPEED)) + .isTrue(); + assertThat(loadControl.shouldContinueLoading(/* playbackPositionUs= */ 0, MIN_BUFFER_US, SPEED)) + .isFalse(); + assertThat(loadControl.shouldContinueLoading(/* playbackPositionUs= */ 0, MAX_BUFFER_US, SPEED)) + .isFalse(); + } + + @Test + public void + shouldContinueLoading_withTargetBufferBytesReachedAndNotPrioritizeTimeOverSize_returnsTrueAsSoonAsTargetBufferReached() { + builder.setPrioritizeTimeOverSizeThresholds(false); + createDefaultLoadControl(); + + // Put loadControl in buffering state. + assertThat( + loadControl.shouldContinueLoading( + /* playbackPositionUs= */ 0, /* bufferedDurationUs= */ 0, SPEED)) + .isTrue(); + makeSureTargetBufferBytesReached(); + + assertThat( + loadControl.shouldContinueLoading( + /* playbackPositionUs= */ 0, /* bufferedDurationUs= */ 0, SPEED)) + .isFalse(); + assertThat( + loadControl.shouldContinueLoading( + /* playbackPositionUs= */ 0, MIN_BUFFER_US - 1, SPEED)) + .isFalse(); + assertThat(loadControl.shouldContinueLoading(/* playbackPositionUs= */ 0, MIN_BUFFER_US, SPEED)) + .isFalse(); + assertThat(loadControl.shouldContinueLoading(/* playbackPositionUs= */ 0, MAX_BUFFER_US, SPEED)) + .isFalse(); + } + + @Test + public void shouldContinueLoadingWithMinBufferReached_inFastPlayback() { + builder.setBufferDurationsMs( + /* minBufferMs= */ (int) C.usToMs(MIN_BUFFER_US), + /* maxBufferMs= */ (int) C.usToMs(MAX_BUFFER_US), + /* bufferForPlaybackMs= */ 0, + /* bufferForPlaybackAfterRebufferMs= */ 0); + createDefaultLoadControl(); + + // At normal playback speed, we stop buffering when the buffer reaches the minimum. + assertThat(loadControl.shouldContinueLoading(/* playbackPositionUs= */ 0, MIN_BUFFER_US, SPEED)) + .isFalse(); + // At double playback speed, we continue loading. + assertThat( + loadControl.shouldContinueLoading( + /* playbackPositionUs= */ 0, MIN_BUFFER_US, /* playbackSpeed= */ 2f)) + .isTrue(); + } + + @Test + public void shouldNotContinueLoadingWithMaxBufferReached_inFastPlayback() { + createDefaultLoadControl(); + + assertThat( + loadControl.shouldContinueLoading( + /* playbackPositionUs= */ 0, MAX_BUFFER_US, /* playbackSpeed= */ 100f)) + .isFalse(); + } + + @Test + public void startsPlayback_whenMinBufferSizeReached() { + createDefaultLoadControl(); + assertThat(loadControl.shouldStartPlayback(MIN_BUFFER_US, SPEED, /* rebuffering= */ false)) .isTrue(); } + @Test + public void shouldContinueLoading_withNoSelectedTracks_returnsTrue() { + loadControl = builder.createDefaultLoadControl(); + loadControl.onTracksSelected(new Renderer[0], TrackGroupArray.EMPTY, new TrackSelectionArray()); + + assertThat( + loadControl.shouldContinueLoading( + /* playbackPositionUs= */ 0, /* bufferedDurationUs= */ 0, /* playbackSpeed= */ 1f)) + .isTrue(); + } + private void createDefaultLoadControl() { - builder.setAllocator(allocator); - builder.setTargetBufferBytes(TARGET_BUFFER_BYTES); + builder.setAllocator(allocator).setTargetBufferBytes(TARGET_BUFFER_BYTES); loadControl = builder.createDefaultLoadControl(); loadControl.onTracksSelected(new Renderer[0], null, null); } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/DefaultMediaClockTest.java b/library/core/src/test/java/com/google/android/exoplayer2/DefaultMediaClockTest.java index b6e3d7a648..217df762f6 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/DefaultMediaClockTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/DefaultMediaClockTest.java @@ -22,7 +22,7 @@ import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.MockitoAnnotations.initMocks; import androidx.test.ext.junit.runners.AndroidJUnit4; -import com.google.android.exoplayer2.DefaultMediaClock.PlaybackParameterListener; +import com.google.android.exoplayer2.DefaultMediaClock.PlaybackSpeedListener; import com.google.android.exoplayer2.testutil.FakeClock; import com.google.android.exoplayer2.testutil.FakeMediaClockRenderer; import org.junit.Before; @@ -36,10 +36,9 @@ public class DefaultMediaClockTest { private static final long TEST_POSITION_US = 123456789012345678L; private static final long SLEEP_TIME_MS = 1_000; - private static final PlaybackParameters TEST_PLAYBACK_PARAMETERS = - new PlaybackParameters(/* speed= */ 2f); + private static final float TEST_PLAYBACK_SPEED = 2f; - @Mock private PlaybackParameterListener listener; + @Mock private PlaybackSpeedListener listener; private FakeClock fakeClock; private DefaultMediaClock mediaClock; @@ -110,119 +109,117 @@ public class DefaultMediaClockTest { } @Test - public void standaloneGetPlaybackParameters_initializedWithDefaultPlaybackParameters() { - assertThat(mediaClock.getPlaybackParameters()).isEqualTo(PlaybackParameters.DEFAULT); + public void standaloneGetPlaybackSpeed_initializedWithDefaultPlaybackSpeed() { + assertThat(mediaClock.getPlaybackSpeed()).isEqualTo(Player.DEFAULT_PLAYBACK_SPEED); } @Test - public void standaloneSetPlaybackParameters_getPlaybackParametersShouldReturnSameValue() { - mediaClock.setPlaybackParameters(TEST_PLAYBACK_PARAMETERS); - assertThat(mediaClock.getPlaybackParameters()).isEqualTo(TEST_PLAYBACK_PARAMETERS); + public void standaloneSetPlaybackSpeed_getPlaybackSpeedShouldReturnSameValue() { + mediaClock.setPlaybackSpeed(TEST_PLAYBACK_SPEED); + assertThat(mediaClock.getPlaybackSpeed()).isEqualTo(TEST_PLAYBACK_SPEED); } @Test - public void standaloneSetPlaybackParameters_shouldNotTriggerCallback() { - mediaClock.setPlaybackParameters(TEST_PLAYBACK_PARAMETERS); + public void standaloneSetPlaybackSpeed_shouldNotTriggerCallback() { + mediaClock.setPlaybackSpeed(TEST_PLAYBACK_SPEED); verifyNoMoreInteractions(listener); } @Test - public void standaloneSetPlaybackParameters_shouldApplyNewPlaybackSpeed() { - mediaClock.setPlaybackParameters(TEST_PLAYBACK_PARAMETERS); + public void standaloneSetPlaybackSpeed_shouldApplyNewPlaybackSpeed() { + mediaClock.setPlaybackSpeed(TEST_PLAYBACK_SPEED); mediaClock.start(); - // Asserts that clock is running with speed declared in getPlaybackParameters(). + // Asserts that clock is running with speed declared in getPlaybackSpeed(). assertClockIsRunning(/* isReadingAhead= */ false); } @Test - public void standaloneSetOtherPlaybackParameters_getPlaybackParametersShouldReturnSameValue() { - mediaClock.setPlaybackParameters(TEST_PLAYBACK_PARAMETERS); - mediaClock.setPlaybackParameters(PlaybackParameters.DEFAULT); - assertThat(mediaClock.getPlaybackParameters()).isEqualTo(PlaybackParameters.DEFAULT); + public void standaloneSetOtherPlaybackSpeed_getPlaybackSpeedShouldReturnSameValue() { + mediaClock.setPlaybackSpeed(TEST_PLAYBACK_SPEED); + mediaClock.setPlaybackSpeed(Player.DEFAULT_PLAYBACK_SPEED); + assertThat(mediaClock.getPlaybackSpeed()).isEqualTo(Player.DEFAULT_PLAYBACK_SPEED); } @Test - public void enableRendererMediaClock_shouldOverwriteRendererPlaybackParametersIfPossible() + public void enableRendererMediaClock_shouldOverwriteRendererPlaybackSpeedIfPossible() throws ExoPlaybackException { FakeMediaClockRenderer mediaClockRenderer = - new MediaClockRenderer(TEST_PLAYBACK_PARAMETERS, /* playbackParametersAreMutable= */ true); + new MediaClockRenderer(TEST_PLAYBACK_SPEED, /* playbackSpeedIsMutable= */ true); mediaClock.onRendererEnabled(mediaClockRenderer); - assertThat(mediaClock.getPlaybackParameters()).isEqualTo(PlaybackParameters.DEFAULT); + assertThat(mediaClock.getPlaybackSpeed()).isEqualTo(Player.DEFAULT_PLAYBACK_SPEED); verifyNoMoreInteractions(listener); } @Test - public void enableRendererMediaClockWithFixedParameters_usesRendererPlaybackParameters() + public void enableRendererMediaClockWithFixedPlaybackSpeed_usesRendererPlaybackSpeed() throws ExoPlaybackException { FakeMediaClockRenderer mediaClockRenderer = - new MediaClockRenderer(TEST_PLAYBACK_PARAMETERS, /* playbackParametersAreMutable= */ false); + new MediaClockRenderer(TEST_PLAYBACK_SPEED, /* playbackSpeedIsMutable= */ false); mediaClock.onRendererEnabled(mediaClockRenderer); - assertThat(mediaClock.getPlaybackParameters()).isEqualTo(TEST_PLAYBACK_PARAMETERS); + assertThat(mediaClock.getPlaybackSpeed()).isEqualTo(TEST_PLAYBACK_SPEED); } @Test - public void enableRendererMediaClockWithFixedParameters_shouldTriggerCallback() + public void enableRendererMediaClockWithFixedPlaybackSpeed_shouldTriggerCallback() throws ExoPlaybackException { FakeMediaClockRenderer mediaClockRenderer = - new MediaClockRenderer(TEST_PLAYBACK_PARAMETERS, /* playbackParametersAreMutable= */ false); + new MediaClockRenderer(TEST_PLAYBACK_SPEED, /* playbackSpeedIsMutable= */ false); mediaClock.onRendererEnabled(mediaClockRenderer); mediaClock.syncAndGetPositionUs(/* isReadingAhead= */ false); - verify(listener).onPlaybackParametersChanged(TEST_PLAYBACK_PARAMETERS); + verify(listener).onPlaybackSpeedChanged(TEST_PLAYBACK_SPEED); } @Test - public void enableRendererMediaClockWithFixedButSamePlaybackParameters_shouldNotTriggerCallback() + public void enableRendererMediaClockWithFixedButSamePlaybackSpeed_shouldNotTriggerCallback() throws ExoPlaybackException { - FakeMediaClockRenderer mediaClockRenderer = new MediaClockRenderer(PlaybackParameters.DEFAULT, - /* playbackParametersAreMutable= */ false); + FakeMediaClockRenderer mediaClockRenderer = + new MediaClockRenderer(Player.DEFAULT_PLAYBACK_SPEED, /* playbackSpeedIsMutable= */ false); mediaClock.onRendererEnabled(mediaClockRenderer); mediaClock.syncAndGetPositionUs(/* isReadingAhead= */ false); verifyNoMoreInteractions(listener); } @Test - public void disableRendererMediaClock_shouldKeepPlaybackParameters() - throws ExoPlaybackException { + public void disableRendererMediaClock_shouldKeepPlaybackSpeed() throws ExoPlaybackException { FakeMediaClockRenderer mediaClockRenderer = - new MediaClockRenderer(TEST_PLAYBACK_PARAMETERS, /* playbackParametersAreMutable= */ false); + new MediaClockRenderer(TEST_PLAYBACK_SPEED, /* playbackSpeedIsMutable= */ false); mediaClock.onRendererEnabled(mediaClockRenderer); mediaClock.syncAndGetPositionUs(/* isReadingAhead= */ false); mediaClock.onRendererDisabled(mediaClockRenderer); mediaClock.syncAndGetPositionUs(/* isReadingAhead= */ false); - assertThat(mediaClock.getPlaybackParameters()).isEqualTo(TEST_PLAYBACK_PARAMETERS); + assertThat(mediaClock.getPlaybackSpeed()).isEqualTo(TEST_PLAYBACK_SPEED); } @Test - public void rendererClockSetPlaybackParameters_getPlaybackParametersShouldReturnSameValue() + public void rendererClockSetPlaybackSpeed_getPlaybackSpeedShouldReturnSameValue() throws ExoPlaybackException { - FakeMediaClockRenderer mediaClockRenderer = new MediaClockRenderer(PlaybackParameters.DEFAULT, - /* playbackParametersAreMutable= */ true); + FakeMediaClockRenderer mediaClockRenderer = + new MediaClockRenderer(Player.DEFAULT_PLAYBACK_SPEED, /* playbackSpeedIsMutable= */ true); mediaClock.onRendererEnabled(mediaClockRenderer); mediaClock.syncAndGetPositionUs(/* isReadingAhead= */ false); - mediaClock.setPlaybackParameters(TEST_PLAYBACK_PARAMETERS); - assertThat(mediaClock.getPlaybackParameters()).isEqualTo(TEST_PLAYBACK_PARAMETERS); + mediaClock.setPlaybackSpeed(TEST_PLAYBACK_SPEED); + assertThat(mediaClock.getPlaybackSpeed()).isEqualTo(TEST_PLAYBACK_SPEED); } @Test - public void rendererClockSetPlaybackParameters_shouldNotTriggerCallback() - throws ExoPlaybackException { - FakeMediaClockRenderer mediaClockRenderer = new MediaClockRenderer(PlaybackParameters.DEFAULT, - /* playbackParametersAreMutable= */ true); + public void rendererClockSetPlaybackSpeed_shouldNotTriggerCallback() throws ExoPlaybackException { + FakeMediaClockRenderer mediaClockRenderer = + new MediaClockRenderer(Player.DEFAULT_PLAYBACK_SPEED, /* playbackSpeedIsMutable= */ true); mediaClock.onRendererEnabled(mediaClockRenderer); mediaClock.syncAndGetPositionUs(/* isReadingAhead= */ false); - mediaClock.setPlaybackParameters(TEST_PLAYBACK_PARAMETERS); + mediaClock.setPlaybackSpeed(TEST_PLAYBACK_SPEED); verifyNoMoreInteractions(listener); } @Test - public void rendererClockSetPlaybackParametersOverwrite_getParametersShouldReturnSameValue() + public void rendererClockSetPlaybackSpeedOverwrite_getPlaybackSpeedShouldReturnSameValue() throws ExoPlaybackException { - FakeMediaClockRenderer mediaClockRenderer = new MediaClockRenderer(PlaybackParameters.DEFAULT, - /* playbackParametersAreMutable= */ false); + FakeMediaClockRenderer mediaClockRenderer = + new MediaClockRenderer(Player.DEFAULT_PLAYBACK_SPEED, /* playbackSpeedIsMutable= */ false); mediaClock.onRendererEnabled(mediaClockRenderer); mediaClock.syncAndGetPositionUs(/* isReadingAhead= */ false); - mediaClock.setPlaybackParameters(TEST_PLAYBACK_PARAMETERS); - assertThat(mediaClock.getPlaybackParameters()).isEqualTo(PlaybackParameters.DEFAULT); + mediaClock.setPlaybackSpeed(TEST_PLAYBACK_SPEED); + assertThat(mediaClock.getPlaybackSpeed()).isEqualTo(Player.DEFAULT_PLAYBACK_SPEED); } @Test @@ -266,16 +263,15 @@ public class DefaultMediaClockTest { } @Test - public void getPositionWithPlaybackParameterChange_shouldTriggerCallback() + public void getPositionWithPlaybackSpeedChange_shouldTriggerCallback() throws ExoPlaybackException { MediaClockRenderer mediaClockRenderer = - new MediaClockRenderer( - PlaybackParameters.DEFAULT, /* playbackParametersAreMutable= */ true); + new MediaClockRenderer(Player.DEFAULT_PLAYBACK_SPEED, /* playbackSpeedIsMutable= */ true); mediaClock.onRendererEnabled(mediaClockRenderer); - // Silently change playback parameters of renderer clock. - mediaClockRenderer.playbackParameters = TEST_PLAYBACK_PARAMETERS; + // Silently change playback speed of renderer clock. + mediaClockRenderer.playbackSpeed = TEST_PLAYBACK_SPEED; mediaClock.syncAndGetPositionUs(/* isReadingAhead= */ false); - verify(listener).onPlaybackParametersChanged(TEST_PLAYBACK_PARAMETERS); + verify(listener).onPlaybackSpeedChanged(TEST_PLAYBACK_SPEED); } @Test @@ -360,10 +356,9 @@ public class DefaultMediaClockTest { private void assertClockIsRunning(boolean isReadingAhead) { long clockStartUs = mediaClock.syncAndGetPositionUs(isReadingAhead); fakeClock.advanceTime(SLEEP_TIME_MS); + int scaledUsPerMs = Math.round(mediaClock.getPlaybackSpeed() * 1000f); assertThat(mediaClock.syncAndGetPositionUs(isReadingAhead)) - .isEqualTo( - clockStartUs - + mediaClock.getPlaybackParameters().getMediaTimeUsForPlayoutTimeMs(SLEEP_TIME_MS)); + .isEqualTo(clockStartUs + (SLEEP_TIME_MS * scaledUsPerMs)); } private void assertClockIsStopped() { @@ -376,34 +371,37 @@ public class DefaultMediaClockTest { @SuppressWarnings("HidingField") private static class MediaClockRenderer extends FakeMediaClockRenderer { - private final boolean playbackParametersAreMutable; + private final boolean playbackSpeedIsMutable; private final boolean isReady; private final boolean isEnded; - public PlaybackParameters playbackParameters; + public float playbackSpeed; public long positionUs; public MediaClockRenderer() throws ExoPlaybackException { - this(PlaybackParameters.DEFAULT, false, true, false, false); + this(Player.DEFAULT_PLAYBACK_SPEED, false, true, false, false); } - public MediaClockRenderer(PlaybackParameters playbackParameters, - boolean playbackParametersAreMutable) + public MediaClockRenderer(float playbackSpeed, boolean playbackSpeedIsMutable) throws ExoPlaybackException { - this(playbackParameters, playbackParametersAreMutable, true, false, false); + this(playbackSpeed, playbackSpeedIsMutable, true, false, false); } public MediaClockRenderer(boolean isReady, boolean isEnded, boolean hasReadStreamToEnd) throws ExoPlaybackException { - this(PlaybackParameters.DEFAULT, false, isReady, isEnded, hasReadStreamToEnd); + this(Player.DEFAULT_PLAYBACK_SPEED, false, isReady, isEnded, hasReadStreamToEnd); } - private MediaClockRenderer(PlaybackParameters playbackParameters, - boolean playbackParametersAreMutable, boolean isReady, boolean isEnded, + private MediaClockRenderer( + float playbackSpeed, + boolean playbackSpeedIsMutable, + boolean isReady, + boolean isEnded, boolean hasReadStreamToEnd) throws ExoPlaybackException { - this.playbackParameters = playbackParameters; - this.playbackParametersAreMutable = playbackParametersAreMutable; + super(C.TRACK_TYPE_UNKNOWN); + this.playbackSpeed = playbackSpeed; + this.playbackSpeedIsMutable = playbackSpeedIsMutable; this.isReady = isReady; this.isEnded = isEnded; this.positionUs = TEST_POSITION_US; @@ -418,15 +416,15 @@ public class DefaultMediaClockTest { } @Override - public void setPlaybackParameters(PlaybackParameters playbackParameters) { - if (playbackParametersAreMutable) { - this.playbackParameters = playbackParameters; + public void setPlaybackSpeed(float playbackSpeed) { + if (playbackSpeedIsMutable) { + this.playbackSpeed = playbackSpeed; } } @Override - public PlaybackParameters getPlaybackParameters() { - return playbackParameters; + public float getPlaybackSpeed() { + return playbackSpeed; } @Override 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 f17cdae56b..770416bb4c 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 @@ -17,16 +17,23 @@ package com.google.android.exoplayer2; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertThrows; import static org.junit.Assert.fail; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; import static org.robolectric.Shadows.shadowOf; import android.content.Context; import android.content.Intent; import android.graphics.SurfaceTexture; import android.media.AudioManager; -import android.net.Uri; import android.os.Looper; import android.view.Surface; +import android.view.View; +import android.view.ViewGroup; import androidx.annotation.Nullable; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; @@ -39,19 +46,22 @@ import com.google.android.exoplayer2.source.ClippingMediaSource; import com.google.android.exoplayer2.source.CompositeMediaSource; import com.google.android.exoplayer2.source.ConcatenatingMediaSource; import com.google.android.exoplayer2.source.LoopingMediaSource; +import com.google.android.exoplayer2.source.MaskingMediaSource; import com.google.android.exoplayer2.source.MediaPeriod; 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.SampleStream; 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.source.ads.AdsLoader; +import com.google.android.exoplayer2.source.ads.AdsMediaSource; 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.FakeAdaptiveDataSet; import com.google.android.exoplayer2.testutil.FakeAdaptiveMediaSource; import com.google.android.exoplayer2.testutil.FakeChunkSource; @@ -60,15 +70,23 @@ import com.google.android.exoplayer2.testutil.FakeMediaClockRenderer; import com.google.android.exoplayer2.testutil.FakeMediaPeriod; import com.google.android.exoplayer2.testutil.FakeMediaSource; import com.google.android.exoplayer2.testutil.FakeRenderer; +import com.google.android.exoplayer2.testutil.FakeSampleStream; import com.google.android.exoplayer2.testutil.FakeShuffleOrder; import com.google.android.exoplayer2.testutil.FakeTimeline; import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition; import com.google.android.exoplayer2.testutil.FakeTrackSelection; import com.google.android.exoplayer2.testutil.FakeTrackSelector; +import com.google.android.exoplayer2.testutil.TestExoPlayer; +import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.trackselection.TrackSelectionArray; +import com.google.android.exoplayer2.upstream.Allocation; import com.google.android.exoplayer2.upstream.Allocator; +import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; +import com.google.android.exoplayer2.upstream.Loader; 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.Util; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; @@ -81,6 +99,7 @@ import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicReference; import org.junit.Before; +import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.annotation.LooperMode; @@ -91,6 +110,8 @@ import org.robolectric.shadows.ShadowAudioManager; @LooperMode(LooperMode.Mode.PAUSED) public final class ExoPlayerTest { + private static final String TAG = "ExoPlayerTest"; + /** * For tests that rely on the player transitioning to the ended state, the duration in * milliseconds after starting the player before the test will time out. This is to catch cases @@ -99,10 +120,12 @@ public final class ExoPlayerTest { private static final int TIMEOUT_MS = 10000; private Context context; + private Timeline dummyTimeline; @Before public void setUp() { context = ApplicationProvider.getApplicationContext(); + dummyTimeline = new MaskingMediaSource.DummyTimeline(/* tag= */ 0); } /** @@ -110,88 +133,103 @@ public final class ExoPlayerTest { * error. */ @Test - public void testPlayEmptyTimeline() throws Exception { + public void playEmptyTimeline() throws Exception { Timeline timeline = Timeline.EMPTY; - FakeRenderer renderer = new FakeRenderer(); + Timeline expectedMaskingTimeline = new MaskingMediaSource.DummyTimeline(/* tag= */ null); + FakeRenderer renderer = new FakeRenderer(C.TRACK_TYPE_UNKNOWN); ExoPlayerTestRunner testRunner = - new Builder() + new ExoPlayerTestRunner.Builder(context) .setTimeline(timeline) .setRenderers(renderer) - .build(context) + .build() .start() .blockUntilEnded(TIMEOUT_MS); testRunner.assertNoPositionDiscontinuities(); - testRunner.assertTimelinesEqual(timeline); - assertThat(renderer.formatReadCount).isEqualTo(0); + testRunner.assertTimelinesSame(expectedMaskingTimeline, Timeline.EMPTY); + testRunner.assertTimelineChangeReasonsEqual( + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, + Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE); + assertThat(renderer.getFormatsRead()).isEmpty(); assertThat(renderer.sampleBufferReadCount).isEqualTo(0); assertThat(renderer.isEnded).isFalse(); } /** Tests playback of a source that exposes a single period. */ @Test - public void testPlaySinglePeriodTimeline() throws Exception { + public void playSinglePeriodTimeline() throws Exception { Object manifest = new Object(); Timeline timeline = new FakeTimeline(/* windowCount= */ 1, manifest); - FakeRenderer renderer = new FakeRenderer(Builder.VIDEO_FORMAT); + FakeRenderer renderer = new FakeRenderer(C.TRACK_TYPE_VIDEO); ExoPlayerTestRunner testRunner = - new Builder() + new ExoPlayerTestRunner.Builder(context) .setTimeline(timeline) .setManifest(manifest) .setRenderers(renderer) - .build(context) + .build() .start() .blockUntilEnded(TIMEOUT_MS); testRunner.assertNoPositionDiscontinuities(); - testRunner.assertTimelinesEqual(timeline); - testRunner.assertTimelineChangeReasonsEqual(Player.TIMELINE_CHANGE_REASON_PREPARED); - testRunner.assertTrackGroupsEqual(new TrackGroupArray(new TrackGroup(Builder.VIDEO_FORMAT))); - assertThat(renderer.formatReadCount).isEqualTo(1); + testRunner.assertTimelinesSame(dummyTimeline, timeline); + testRunner.assertTimelineChangeReasonsEqual( + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, + Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE); + testRunner.assertTrackGroupsEqual( + new TrackGroupArray(new TrackGroup(ExoPlayerTestRunner.VIDEO_FORMAT))); + assertThat(renderer.getFormatsRead()).containsExactly(ExoPlayerTestRunner.VIDEO_FORMAT); assertThat(renderer.sampleBufferReadCount).isEqualTo(1); assertThat(renderer.isEnded).isTrue(); } /** Tests playback of a source that exposes three periods. */ @Test - public void testPlayMultiPeriodTimeline() throws Exception { + public void playMultiPeriodTimeline() throws Exception { Timeline timeline = new FakeTimeline(/* windowCount= */ 3); - FakeRenderer renderer = new FakeRenderer(Builder.VIDEO_FORMAT); + FakeRenderer renderer = new FakeRenderer(C.TRACK_TYPE_VIDEO); ExoPlayerTestRunner testRunner = - new Builder() + new ExoPlayerTestRunner.Builder(context) .setTimeline(timeline) .setRenderers(renderer) - .build(context) + .build() .start() .blockUntilEnded(TIMEOUT_MS); testRunner.assertPositionDiscontinuityReasonsEqual( Player.DISCONTINUITY_REASON_PERIOD_TRANSITION, Player.DISCONTINUITY_REASON_PERIOD_TRANSITION); - testRunner.assertTimelinesEqual(timeline); - testRunner.assertTimelineChangeReasonsEqual(Player.TIMELINE_CHANGE_REASON_PREPARED); - assertThat(renderer.formatReadCount).isEqualTo(3); + testRunner.assertTimelinesSame(new FakeMediaSource.InitialTimeline(timeline), timeline); + testRunner.assertTimelineChangeReasonsEqual( + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, + Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE); + assertThat(renderer.getFormatsRead()) + .containsExactly( + ExoPlayerTestRunner.VIDEO_FORMAT, + ExoPlayerTestRunner.VIDEO_FORMAT, + ExoPlayerTestRunner.VIDEO_FORMAT); assertThat(renderer.sampleBufferReadCount).isEqualTo(3); assertThat(renderer.isEnded).isTrue(); } /** Tests playback of periods with very short duration. */ @Test - public void testPlayShortDurationPeriods() throws Exception { + public void playShortDurationPeriods() throws Exception { // TimelineWindowDefinition.DEFAULT_WINDOW_DURATION_US / 100 = 1000 us per period. Timeline timeline = new FakeTimeline(new TimelineWindowDefinition(/* periodCount= */ 100, /* id= */ 0)); - FakeRenderer renderer = new FakeRenderer(Builder.VIDEO_FORMAT); + FakeRenderer renderer = new FakeRenderer(C.TRACK_TYPE_VIDEO); ExoPlayerTestRunner testRunner = - new Builder() + new ExoPlayerTestRunner.Builder(context) .setTimeline(timeline) .setRenderers(renderer) - .build(context) + .build() .start() .blockUntilEnded(TIMEOUT_MS); Integer[] expectedReasons = new Integer[99]; Arrays.fill(expectedReasons, Player.DISCONTINUITY_REASON_PERIOD_TRANSITION); testRunner.assertPositionDiscontinuityReasonsEqual(expectedReasons); - testRunner.assertTimelinesEqual(timeline); - testRunner.assertTimelineChangeReasonsEqual(Player.TIMELINE_CHANGE_REASON_PREPARED); - assertThat(renderer.formatReadCount).isEqualTo(100); + testRunner.assertTimelinesSame(dummyTimeline, timeline); + testRunner.assertTimelineChangeReasonsEqual( + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, + Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE); + assertThat(renderer.getFormatsRead()).hasSize(100); assertThat(renderer.sampleBufferReadCount).isEqualTo(100); assertThat(renderer.isEnded).isTrue(); } @@ -201,7 +239,7 @@ public final class ExoPlayerTest { * source. */ @Test - public void testReadAheadToEndDoesNotResetRenderer() throws Exception { + public void readAheadToEndDoesNotResetRenderer() throws Exception { // Use sufficiently short periods to ensure the player attempts to read all at once. TimelineWindowDefinition windowDefinition0 = new TimelineWindowDefinition( @@ -225,9 +263,9 @@ public final class ExoPlayerTest { /* isDynamic= */ false, /* durationUs= */ 100_000); Timeline timeline = new FakeTimeline(windowDefinition0, windowDefinition1, windowDefinition2); - final FakeRenderer videoRenderer = new FakeRenderer(Builder.VIDEO_FORMAT); + final FakeRenderer videoRenderer = new FakeRenderer(C.TRACK_TYPE_VIDEO); FakeMediaClockRenderer audioRenderer = - new FakeMediaClockRenderer(Builder.AUDIO_FORMAT) { + new FakeMediaClockRenderer(C.TRACK_TYPE_AUDIO) { @Override public long getPositionUs() { @@ -239,11 +277,11 @@ public final class ExoPlayerTest { } @Override - public void setPlaybackParameters(PlaybackParameters playbackParameters) {} + public void setPlaybackSpeed(float playbackSpeed) {} @Override - public PlaybackParameters getPlaybackParameters() { - return PlaybackParameters.DEFAULT; + public float getPlaybackSpeed() { + return Player.DEFAULT_PLAYBACK_SPEED; } @Override @@ -252,41 +290,46 @@ public final class ExoPlayerTest { } }; ExoPlayerTestRunner testRunner = - new Builder() + new ExoPlayerTestRunner.Builder(context) .setTimeline(timeline) .setRenderers(videoRenderer, audioRenderer) - .setSupportedFormats(Builder.VIDEO_FORMAT, Builder.AUDIO_FORMAT) - .build(context) + .setSupportedFormats(ExoPlayerTestRunner.VIDEO_FORMAT, ExoPlayerTestRunner.AUDIO_FORMAT) + .build() .start() .blockUntilEnded(TIMEOUT_MS); testRunner.assertPositionDiscontinuityReasonsEqual( Player.DISCONTINUITY_REASON_PERIOD_TRANSITION, Player.DISCONTINUITY_REASON_PERIOD_TRANSITION); - testRunner.assertTimelinesEqual(timeline); + testRunner.assertTimelinesSame(new FakeMediaSource.InitialTimeline(timeline), timeline); + testRunner.assertTimelineChangeReasonsEqual( + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, + Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE); assertThat(audioRenderer.positionResetCount).isEqualTo(1); assertThat(videoRenderer.isEnded).isTrue(); assertThat(audioRenderer.isEnded).isTrue(); } @Test - public void testRepreparationGivesFreshSourceInfo() throws Exception { - FakeRenderer renderer = new FakeRenderer(Builder.VIDEO_FORMAT); - Object firstSourceManifest = new Object(); - Timeline firstTimeline = new FakeTimeline(/* windowCount= */ 1, firstSourceManifest); - MediaSource firstSource = new FakeMediaSource(firstTimeline, Builder.VIDEO_FORMAT); + public void resettingMediaSourcesGivesFreshSourceInfo() throws Exception { + FakeRenderer renderer = new FakeRenderer(C.TRACK_TYPE_VIDEO); + Timeline firstTimeline = + new FakeTimeline( + new TimelineWindowDefinition( + /* isSeekable= */ true, /* isDynamic= */ false, 1000_000_000)); + MediaSource firstSource = new FakeMediaSource(firstTimeline, ExoPlayerTestRunner.VIDEO_FORMAT); final CountDownLatch queuedSourceInfoCountDownLatch = new CountDownLatch(1); final CountDownLatch completePreparationCountDownLatch = new CountDownLatch(1); Timeline secondTimeline = new FakeTimeline(/* windowCount= */ 1); MediaSource secondSource = - new FakeMediaSource(secondTimeline, Builder.VIDEO_FORMAT) { + new FakeMediaSource(secondTimeline, ExoPlayerTestRunner.VIDEO_FORMAT) { @Override public synchronized void prepareSourceInternal( @Nullable TransferListener mediaTransferListener) { super.prepareSourceInternal(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. + // test thread to set the third source to the playlist, and block this thread (the + // playback thread) until the test thread's call to setMediaSources() has returned. queuedSourceInfoCountDownLatch.countDown(); try { completePreparationCountDownLatch.await(); @@ -297,16 +340,17 @@ public final class ExoPlayerTest { }; Object thirdSourceManifest = new Object(); Timeline thirdTimeline = new FakeTimeline(/* windowCount= */ 1, thirdSourceManifest); - MediaSource thirdSource = new FakeMediaSource(thirdTimeline, Builder.VIDEO_FORMAT); + MediaSource thirdSource = new FakeMediaSource(thirdTimeline, ExoPlayerTestRunner.VIDEO_FORMAT); // Prepare the player with a source with the first manifest and a non-empty timeline. Prepare // the player again with a source and a new manifest, which will never be exposed. Allow the - // test thread to prepare the player with a third source, and block the playback thread until - // the test thread's call to prepare() has returned. + // test thread to set a third source, and block the playback thread until the test thread's call + // to setMediaSources() has returned. ActionSchedule actionSchedule = - new ActionSchedule.Builder("testRepreparation") - .waitForTimelineChanged(firstTimeline) - .prepareSource(secondSource) + new ActionSchedule.Builder(TAG) + .waitForTimelineChanged( + firstTimeline, /* expectedReason */ Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE) + .setMediaSources(secondSource) .executeRunnable( () -> { try { @@ -315,38 +359,45 @@ public final class ExoPlayerTest { // Ignore. } }) - .prepareSource(thirdSource) + .setMediaSources(thirdSource) .executeRunnable(completePreparationCountDownLatch::countDown) .build(); ExoPlayerTestRunner testRunner = - new Builder() - .setMediaSource(firstSource) + new ExoPlayerTestRunner.Builder(context) + .setMediaSources(firstSource) .setRenderers(renderer) .setActionSchedule(actionSchedule) - .build(context) + .build() .start() + .blockUntilActionScheduleFinished(TIMEOUT_MS) .blockUntilEnded(TIMEOUT_MS); testRunner.assertNoPositionDiscontinuities(); - // The first source's preparation completed with a non-empty timeline. When the player was - // re-prepared with the second source, it immediately exposed an empty timeline, but the source - // info refresh from the second source was suppressed as we re-prepared with the third source. - testRunner.assertTimelinesEqual(firstTimeline, Timeline.EMPTY, thirdTimeline); + // The first source's preparation completed with a real timeline. When the second source was + // prepared, it immediately exposed a dummy timeline, but the source info refresh from the + // second source was suppressed as we replace it with the third source before the update + // arrives. + testRunner.assertTimelinesSame( + dummyTimeline, firstTimeline, dummyTimeline, dummyTimeline, thirdTimeline); testRunner.assertTimelineChangeReasonsEqual( - Player.TIMELINE_CHANGE_REASON_PREPARED, - Player.TIMELINE_CHANGE_REASON_RESET, - Player.TIMELINE_CHANGE_REASON_PREPARED); - testRunner.assertTrackGroupsEqual(new TrackGroupArray(new TrackGroup(Builder.VIDEO_FORMAT))); + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, + Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE, + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, + Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE); + testRunner.assertTrackGroupsEqual( + new TrackGroupArray(new TrackGroup(ExoPlayerTestRunner.VIDEO_FORMAT))); assertThat(renderer.isEnded).isTrue(); } @Test - public void testRepeatModeChanges() throws Exception { + public void repeatModeChanges() throws Exception { Timeline timeline = new FakeTimeline(/* windowCount= */ 3); - FakeRenderer renderer = new FakeRenderer(Builder.VIDEO_FORMAT); + FakeRenderer renderer = new FakeRenderer(C.TRACK_TYPE_VIDEO); ActionSchedule actionSchedule = - new ActionSchedule.Builder("testRepeatMode") + new ActionSchedule.Builder(TAG) .pause() - .waitForTimelineChanged(timeline) + .waitForTimelineChanged( + timeline, /* expectedReason */ Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE) .playUntilStartOfWindow(/* windowIndex= */ 1) .setRepeatMode(Player.REPEAT_MODE_ONE) .playUntilStartOfWindow(/* windowIndex= */ 1) @@ -363,11 +414,11 @@ public final class ExoPlayerTest { .play() .build(); ExoPlayerTestRunner testRunner = - new ExoPlayerTestRunner.Builder() + new ExoPlayerTestRunner.Builder(context) .setTimeline(timeline) .setRenderers(renderer) .setActionSchedule(actionSchedule) - .build(context) + .build() .start() .blockUntilEnded(TIMEOUT_MS); testRunner.assertPlayedPeriodIndices(0, 1, 1, 2, 2, 0, 0, 0, 1, 2); @@ -381,24 +432,26 @@ public final class ExoPlayerTest { Player.DISCONTINUITY_REASON_PERIOD_TRANSITION, Player.DISCONTINUITY_REASON_PERIOD_TRANSITION, Player.DISCONTINUITY_REASON_PERIOD_TRANSITION); - testRunner.assertTimelinesEqual(timeline); - testRunner.assertTimelineChangeReasonsEqual(Player.TIMELINE_CHANGE_REASON_PREPARED); + testRunner.assertTimelinesSame(new FakeMediaSource.InitialTimeline(timeline), timeline); + testRunner.assertTimelineChangeReasonsEqual( + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, + Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE); assertThat(renderer.isEnded).isTrue(); } @Test - public void testShuffleModeEnabledChanges() throws Exception { + public void shuffleModeEnabledChanges() throws Exception { Timeline fakeTimeline = new FakeTimeline(/* windowCount= */ 1); MediaSource[] fakeMediaSources = { - new FakeMediaSource(fakeTimeline, Builder.VIDEO_FORMAT), - new FakeMediaSource(fakeTimeline, Builder.VIDEO_FORMAT), - new FakeMediaSource(fakeTimeline, Builder.VIDEO_FORMAT) + new FakeMediaSource(fakeTimeline, ExoPlayerTestRunner.VIDEO_FORMAT), + new FakeMediaSource(fakeTimeline, ExoPlayerTestRunner.VIDEO_FORMAT), + new FakeMediaSource(fakeTimeline, ExoPlayerTestRunner.VIDEO_FORMAT) }; ConcatenatingMediaSource mediaSource = new ConcatenatingMediaSource(false, new FakeShuffleOrder(3), fakeMediaSources); - FakeRenderer renderer = new FakeRenderer(Builder.VIDEO_FORMAT); + FakeRenderer renderer = new FakeRenderer(C.TRACK_TYPE_VIDEO); ActionSchedule actionSchedule = - new ActionSchedule.Builder("testShuffleModeEnabled") + new ActionSchedule.Builder(TAG) .pause() .waitForPlaybackState(Player.STATE_READY) .setRepeatMode(Player.REPEAT_MODE_ALL) @@ -410,11 +463,11 @@ public final class ExoPlayerTest { .play() .build(); ExoPlayerTestRunner testRunner = - new ExoPlayerTestRunner.Builder() - .setMediaSource(mediaSource) + new ExoPlayerTestRunner.Builder(context) + .setMediaSources(mediaSource) .setRenderers(renderer) .setActionSchedule(actionSchedule) - .build(context) + .build() .start() .blockUntilEnded(TIMEOUT_MS); testRunner.assertPlayedPeriodIndices(0, 1, 0, 2, 1, 2); @@ -428,10 +481,12 @@ public final class ExoPlayerTest { } @Test - public void testAdGroupWithLoadErrorIsSkipped() throws Exception { + public void adGroupWithLoadErrorIsSkipped() throws Exception { AdPlaybackState initialAdPlaybackState = FakeTimeline.createAdPlaybackState( - /* adsPerAdGroup= */ 1, /* adGroupTimesUs= */ 5 * C.MICROS_PER_SECOND); + /* adsPerAdGroup= */ 1, /* adGroupTimesUs=... */ + TimelineWindowDefinition.DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US + + 5 * C.MICROS_PER_SECOND); Timeline fakeTimeline = new FakeTimeline( new TimelineWindowDefinition( @@ -451,20 +506,22 @@ public final class ExoPlayerTest { /* isDynamic= */ false, /* durationUs= */ C.MICROS_PER_SECOND, errorAdPlaybackState)); - final FakeMediaSource fakeMediaSource = new FakeMediaSource(fakeTimeline, Builder.VIDEO_FORMAT); + final FakeMediaSource fakeMediaSource = + new FakeMediaSource(fakeTimeline, ExoPlayerTestRunner.VIDEO_FORMAT); ActionSchedule actionSchedule = - new ActionSchedule.Builder("testAdGroupWithLoadErrorIsSkipped") + new ActionSchedule.Builder(TAG) .pause() .waitForPlaybackState(Player.STATE_READY) - .executeRunnable(() -> fakeMediaSource.setNewSourceInfo(adErrorTimeline, null)) - .waitForTimelineChanged(adErrorTimeline) + .executeRunnable(() -> fakeMediaSource.setNewSourceInfo(adErrorTimeline)) + .waitForTimelineChanged( + adErrorTimeline, /* expectedReason */ Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE) .play() .build(); ExoPlayerTestRunner testRunner = - new ExoPlayerTestRunner.Builder() - .setMediaSource(fakeMediaSource) + new ExoPlayerTestRunner.Builder(context) + .setMediaSources(fakeMediaSource) .setActionSchedule(actionSchedule) - .build(context) + .build() .start() .blockUntilEnded(TIMEOUT_MS); // There is still one discontinuity from content to content for the failed ad insertion. @@ -472,133 +529,72 @@ public final class ExoPlayerTest { } @Test - public void testPeriodHoldersReleasedAfterSeekWithRepeatModeAll() throws Exception { - FakeRenderer renderer = new FakeRenderer(Builder.VIDEO_FORMAT); + public void periodHoldersReleasedAfterSeekWithRepeatModeAll() throws Exception { + FakeRenderer renderer = new FakeRenderer(C.TRACK_TYPE_VIDEO); ActionSchedule actionSchedule = - new ActionSchedule.Builder("testPeriodHoldersReleased") + new ActionSchedule.Builder(TAG) .setRepeatMode(Player.REPEAT_MODE_ALL) .waitForPositionDiscontinuity() .seek(0) // Seek with repeat mode set to Player.REPEAT_MODE_ALL. .waitForPositionDiscontinuity() .setRepeatMode(Player.REPEAT_MODE_OFF) // Turn off repeat so that playback can finish. .build(); - new Builder() + new ExoPlayerTestRunner.Builder(context) .setRenderers(renderer) .setActionSchedule(actionSchedule) - .build(context) + .build() .start() .blockUntilEnded(TIMEOUT_MS); assertThat(renderer.isEnded).isTrue(); } @Test - public void testSeekProcessedCallback() throws Exception { - Timeline timeline = new FakeTimeline(/* windowCount= */ 2); + public void illegalSeekPositionDoesThrow() throws Exception { + final IllegalSeekPositionException[] exception = new IllegalSeekPositionException[1]; ActionSchedule actionSchedule = - new ActionSchedule.Builder("testSeekProcessedCallback") - // Initial seek. Expect immediate seek processed. - .pause() - .seek(5) - .waitForSeekProcessed() - // Multiple overlapping seeks while the player is still preparing. Expect only one seek - // processed. - .seek(2) - .seek(10) - // Wait until media source prepared and re-seek to same position. Expect a seek - // processed while still being in STATE_READY. - .waitForPlaybackState(Player.STATE_READY) - .seek(10) - // Start playback and wait until playback reaches second window. - .playUntilStartOfWindow(/* windowIndex= */ 1) - // Seek twice in concession, expecting the first seek to be replaced (and thus except - // only on seek processed callback). - .seek(5) - .seek(60) - .play() - .build(); - final List playbackStatesWhenSeekProcessed = new ArrayList<>(); - EventListener eventListener = - new EventListener() { - private int currentPlaybackState = Player.STATE_IDLE; - - @Override - public void onPlayerStateChanged(boolean playWhenReady, @Player.State int playbackState) { - currentPlaybackState = playbackState; - } - - @Override - public void onSeekProcessed() { - playbackStatesWhenSeekProcessed.add(currentPlaybackState); - } - }; - ExoPlayerTestRunner testRunner = - new ExoPlayerTestRunner.Builder() - .setTimeline(timeline) - .setEventListener(eventListener) - .setActionSchedule(actionSchedule) - .build(context) - .start() - .blockUntilEnded(TIMEOUT_MS); - testRunner.assertPositionDiscontinuityReasonsEqual( - Player.DISCONTINUITY_REASON_SEEK, - Player.DISCONTINUITY_REASON_SEEK, - Player.DISCONTINUITY_REASON_SEEK, - Player.DISCONTINUITY_REASON_SEEK, - Player.DISCONTINUITY_REASON_PERIOD_TRANSITION, - Player.DISCONTINUITY_REASON_SEEK, - Player.DISCONTINUITY_REASON_SEEK); - assertThat(playbackStatesWhenSeekProcessed) - .containsExactly( - Player.STATE_BUFFERING, - Player.STATE_BUFFERING, - Player.STATE_READY, - Player.STATE_BUFFERING) - .inOrder(); - } - - @Test - public void testSeekProcessedCalledWithIllegalSeekPosition() throws Exception { - ActionSchedule actionSchedule = - new ActionSchedule.Builder("testSeekProcessedCalledWithIllegalSeekPosition") + new ActionSchedule.Builder(TAG) .waitForPlaybackState(Player.STATE_BUFFERING) - // The illegal seek position will end playback. - .seek(/* windowIndex= */ 100, /* positionMs= */ 0) + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + try { + player.seekTo(/* windowIndex= */ 100, /* positionMs= */ 0); + } catch (IllegalSeekPositionException e) { + exception[0] = e; + } + } + }) .waitForPlaybackState(Player.STATE_ENDED) .build(); - final boolean[] onSeekProcessedCalled = new boolean[1]; - EventListener listener = - new EventListener() { - @Override - public void onSeekProcessed() { - onSeekProcessedCalled[0] = true; - } - }; - ExoPlayerTestRunner testRunner = - new Builder().setActionSchedule(actionSchedule).setEventListener(listener).build(context); - testRunner.start().blockUntilActionScheduleFinished(TIMEOUT_MS).blockUntilEnded(TIMEOUT_MS); - assertThat(onSeekProcessedCalled[0]).isTrue(); + new ExoPlayerTestRunner.Builder(context) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilActionScheduleFinished(TIMEOUT_MS) + .blockUntilEnded(TIMEOUT_MS); + assertThat(exception[0]).isNotNull(); } @Test - public void testSeekDiscontinuity() throws Exception { + public void seekDiscontinuity() throws Exception { FakeTimeline timeline = new FakeTimeline(1); - ActionSchedule actionSchedule = - new ActionSchedule.Builder("testSeekDiscontinuity").seek(10).build(); + ActionSchedule actionSchedule = new ActionSchedule.Builder(TAG).seek(10).build(); ExoPlayerTestRunner testRunner = - new ExoPlayerTestRunner.Builder() + new ExoPlayerTestRunner.Builder(context) .setTimeline(timeline) .setActionSchedule(actionSchedule) - .build(context) + .build() .start() .blockUntilEnded(TIMEOUT_MS); testRunner.assertPositionDiscontinuityReasonsEqual(Player.DISCONTINUITY_REASON_SEEK); } @Test - public void testSeekDiscontinuityWithAdjustment() throws Exception { + public void seekDiscontinuityWithAdjustment() throws Exception { FakeTimeline timeline = new FakeTimeline(1); FakeMediaSource mediaSource = - new FakeMediaSource(timeline, Builder.VIDEO_FORMAT) { + new FakeMediaSource(timeline, ExoPlayerTestRunner.VIDEO_FORMAT) { @Override protected FakeMediaPeriod createFakeMediaPeriod( MediaPeriodId id, @@ -612,17 +608,17 @@ public final class ExoPlayerTest { } }; ActionSchedule actionSchedule = - new ActionSchedule.Builder("testSeekDiscontinuityAdjust") + new ActionSchedule.Builder(TAG) .pause() .waitForPlaybackState(Player.STATE_READY) .seek(10) .play() .build(); ExoPlayerTestRunner testRunner = - new ExoPlayerTestRunner.Builder() - .setMediaSource(mediaSource) + new ExoPlayerTestRunner.Builder(context) + .setMediaSources(mediaSource) .setActionSchedule(actionSchedule) - .build(context) + .build() .start() .blockUntilEnded(TIMEOUT_MS); testRunner.assertPositionDiscontinuityReasonsEqual( @@ -630,10 +626,10 @@ public final class ExoPlayerTest { } @Test - public void testInternalDiscontinuityAtNewPosition() throws Exception { + public void internalDiscontinuityAtNewPosition() throws Exception { FakeTimeline timeline = new FakeTimeline(1); FakeMediaSource mediaSource = - new FakeMediaSource(timeline, Builder.VIDEO_FORMAT) { + new FakeMediaSource(timeline, ExoPlayerTestRunner.VIDEO_FORMAT) { @Override protected FakeMediaPeriod createFakeMediaPeriod( MediaPeriodId id, @@ -647,19 +643,19 @@ public final class ExoPlayerTest { } }; ExoPlayerTestRunner testRunner = - new ExoPlayerTestRunner.Builder() - .setMediaSource(mediaSource) - .build(context) + new ExoPlayerTestRunner.Builder(context) + .setMediaSources(mediaSource) + .build() .start() .blockUntilEnded(TIMEOUT_MS); testRunner.assertPositionDiscontinuityReasonsEqual(Player.DISCONTINUITY_REASON_INTERNAL); } @Test - public void testInternalDiscontinuityAtInitialPosition() throws Exception { + public void internalDiscontinuityAtInitialPosition() throws Exception { FakeTimeline timeline = new FakeTimeline(1); FakeMediaSource mediaSource = - new FakeMediaSource(timeline, Builder.VIDEO_FORMAT) { + new FakeMediaSource(timeline, ExoPlayerTestRunner.VIDEO_FORMAT) { @Override protected FakeMediaPeriod createFakeMediaPeriod( MediaPeriodId id, @@ -668,14 +664,15 @@ public final class ExoPlayerTest { EventDispatcher eventDispatcher, @Nullable TransferListener transferListener) { FakeMediaPeriod mediaPeriod = new FakeMediaPeriod(trackGroupArray, eventDispatcher); - mediaPeriod.setDiscontinuityPositionUs(0); + mediaPeriod.setDiscontinuityPositionUs( + TimelineWindowDefinition.DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US); return mediaPeriod; } }; ExoPlayerTestRunner testRunner = - new ExoPlayerTestRunner.Builder() - .setMediaSource(mediaSource) - .build(context) + new ExoPlayerTestRunner.Builder(context) + .setMediaSources(mediaSource) + .build() .start() .blockUntilEnded(TIMEOUT_MS); // If the position is unchanged we do not expect the discontinuity to be reported externally. @@ -683,19 +680,20 @@ public final class ExoPlayerTest { } @Test - public void testAllActivatedTrackSelectionAreReleasedForSinglePeriod() throws Exception { + public void allActivatedTrackSelectionAreReleasedForSinglePeriod() throws Exception { Timeline timeline = new FakeTimeline(/* windowCount= */ 1); MediaSource mediaSource = - new FakeMediaSource(timeline, Builder.VIDEO_FORMAT, Builder.AUDIO_FORMAT); - FakeRenderer videoRenderer = new FakeRenderer(Builder.VIDEO_FORMAT); - FakeRenderer audioRenderer = new FakeRenderer(Builder.AUDIO_FORMAT); + new FakeMediaSource( + timeline, ExoPlayerTestRunner.VIDEO_FORMAT, ExoPlayerTestRunner.AUDIO_FORMAT); + FakeRenderer videoRenderer = new FakeRenderer(C.TRACK_TYPE_VIDEO); + FakeRenderer audioRenderer = new FakeRenderer(C.TRACK_TYPE_AUDIO); FakeTrackSelector trackSelector = new FakeTrackSelector(); - new Builder() - .setMediaSource(mediaSource) + new ExoPlayerTestRunner.Builder(context) + .setMediaSources(mediaSource) .setRenderers(videoRenderer, audioRenderer) .setTrackSelector(trackSelector) - .build(context) + .build() .start() .blockUntilEnded(TIMEOUT_MS); @@ -712,19 +710,20 @@ public final class ExoPlayerTest { } @Test - public void testAllActivatedTrackSelectionAreReleasedForMultiPeriods() throws Exception { + public void allActivatedTrackSelectionAreReleasedForMultiPeriods() throws Exception { Timeline timeline = new FakeTimeline(/* windowCount= */ 2); MediaSource mediaSource = - new FakeMediaSource(timeline, Builder.VIDEO_FORMAT, Builder.AUDIO_FORMAT); - FakeRenderer videoRenderer = new FakeRenderer(Builder.VIDEO_FORMAT); - FakeRenderer audioRenderer = new FakeRenderer(Builder.AUDIO_FORMAT); + new FakeMediaSource( + timeline, ExoPlayerTestRunner.VIDEO_FORMAT, ExoPlayerTestRunner.AUDIO_FORMAT); + FakeRenderer videoRenderer = new FakeRenderer(C.TRACK_TYPE_VIDEO); + FakeRenderer audioRenderer = new FakeRenderer(C.TRACK_TYPE_AUDIO); FakeTrackSelector trackSelector = new FakeTrackSelector(); - new Builder() - .setMediaSource(mediaSource) + new ExoPlayerTestRunner.Builder(context) + .setMediaSources(mediaSource) .setRenderers(videoRenderer, audioRenderer) .setTrackSelector(trackSelector) - .build(context) + .build() .start() .blockUntilEnded(TIMEOUT_MS); @@ -741,28 +740,28 @@ public final class ExoPlayerTest { } @Test - public void testAllActivatedTrackSelectionAreReleasedWhenTrackSelectionsAreRemade() - throws Exception { + public void allActivatedTrackSelectionAreReleasedWhenTrackSelectionsAreRemade() throws Exception { Timeline timeline = new FakeTimeline(/* windowCount= */ 1); MediaSource mediaSource = - new FakeMediaSource(timeline, Builder.VIDEO_FORMAT, Builder.AUDIO_FORMAT); - FakeRenderer videoRenderer = new FakeRenderer(Builder.VIDEO_FORMAT); - FakeRenderer audioRenderer = new FakeRenderer(Builder.AUDIO_FORMAT); + new FakeMediaSource( + timeline, ExoPlayerTestRunner.VIDEO_FORMAT, ExoPlayerTestRunner.AUDIO_FORMAT); + FakeRenderer videoRenderer = new FakeRenderer(C.TRACK_TYPE_VIDEO); + FakeRenderer audioRenderer = new FakeRenderer(C.TRACK_TYPE_AUDIO); final FakeTrackSelector trackSelector = new FakeTrackSelector(); ActionSchedule disableTrackAction = - new ActionSchedule.Builder("testChangeTrackSelection") + new ActionSchedule.Builder(TAG) .pause() .waitForPlaybackState(Player.STATE_READY) .disableRenderer(0) .play() .build(); - new Builder() - .setMediaSource(mediaSource) + new ExoPlayerTestRunner.Builder(context) + .setMediaSources(mediaSource) .setRenderers(videoRenderer, audioRenderer) .setTrackSelector(trackSelector) .setActionSchedule(disableTrackAction) - .build(context) + .build() .start() .blockUntilEnded(TIMEOUT_MS); @@ -780,28 +779,29 @@ public final class ExoPlayerTest { } @Test - public void testAllActivatedTrackSelectionAreReleasedWhenTrackSelectionsAreReused() - throws Exception { + public void allActivatedTrackSelectionAreReleasedWhenTrackSelectionsAreReused() throws Exception { Timeline timeline = new FakeTimeline(/* windowCount= */ 1); MediaSource mediaSource = - new FakeMediaSource(timeline, Builder.VIDEO_FORMAT, Builder.AUDIO_FORMAT); - FakeRenderer videoRenderer = new FakeRenderer(Builder.VIDEO_FORMAT); - FakeRenderer audioRenderer = new FakeRenderer(Builder.AUDIO_FORMAT); - final FakeTrackSelector trackSelector = new FakeTrackSelector(/* reuse track selection */ true); + new FakeMediaSource( + timeline, ExoPlayerTestRunner.VIDEO_FORMAT, ExoPlayerTestRunner.AUDIO_FORMAT); + FakeRenderer videoRenderer = new FakeRenderer(C.TRACK_TYPE_VIDEO); + FakeRenderer audioRenderer = new FakeRenderer(C.TRACK_TYPE_AUDIO); + final FakeTrackSelector trackSelector = + new FakeTrackSelector(/* mayReuseTrackSelection= */ true); ActionSchedule disableTrackAction = - new ActionSchedule.Builder("testReuseTrackSelection") + new ActionSchedule.Builder(TAG) .pause() .waitForPlaybackState(Player.STATE_READY) .disableRenderer(0) .play() .build(); - new Builder() - .setMediaSource(mediaSource) + new ExoPlayerTestRunner.Builder(context) + .setMediaSources(mediaSource) .setRenderers(videoRenderer, audioRenderer) .setTrackSelector(trackSelector) .setActionSchedule(disableTrackAction) - .build(context) + .build() .start() .blockUntilEnded(TIMEOUT_MS); @@ -820,32 +820,37 @@ public final class ExoPlayerTest { } @Test - public void testDynamicTimelineChangeReason() throws Exception { - Timeline timeline1 = new FakeTimeline(new TimelineWindowDefinition(false, false, 100000)); + public void dynamicTimelineChangeReason() throws Exception { + Timeline timeline = new FakeTimeline(new TimelineWindowDefinition(false, false, 100000)); final Timeline timeline2 = new FakeTimeline(new TimelineWindowDefinition(false, false, 20000)); - final FakeMediaSource mediaSource = new FakeMediaSource(timeline1, Builder.VIDEO_FORMAT); + final FakeMediaSource mediaSource = + new FakeMediaSource(timeline, ExoPlayerTestRunner.VIDEO_FORMAT); ActionSchedule actionSchedule = - new ActionSchedule.Builder("testDynamicTimelineChangeReason") + new ActionSchedule.Builder(TAG) .pause() - .waitForTimelineChanged(timeline1) - .executeRunnable(() -> mediaSource.setNewSourceInfo(timeline2, null)) - .waitForTimelineChanged(timeline2) + .waitForTimelineChanged( + timeline, /* expectedReason */ Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE) + .executeRunnable(() -> mediaSource.setNewSourceInfo(timeline2)) + .waitForTimelineChanged( + timeline2, /* expectedReason */ Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE) .play() .build(); ExoPlayerTestRunner testRunner = - new ExoPlayerTestRunner.Builder() - .setMediaSource(mediaSource) + new ExoPlayerTestRunner.Builder(context) + .setMediaSources(mediaSource) .setActionSchedule(actionSchedule) - .build(context) + .build() .start() .blockUntilEnded(TIMEOUT_MS); - testRunner.assertTimelinesEqual(timeline1, timeline2); + testRunner.assertTimelinesSame(dummyTimeline, timeline, timeline2); testRunner.assertTimelineChangeReasonsEqual( - Player.TIMELINE_CHANGE_REASON_PREPARED, Player.TIMELINE_CHANGE_REASON_DYNAMIC); + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, + Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE, + Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE); } @Test - public void testRepreparationWithPositionResetAndShufflingUsesFirstPeriod() throws Exception { + public void resetMediaSourcesWithPositionResetAndShufflingUsesFirstPeriod() throws Exception { Timeline fakeTimeline = new FakeTimeline( new TimelineWindowDefinition( @@ -854,43 +859,46 @@ public final class ExoPlayerTest { new ConcatenatingMediaSource( /* isAtomic= */ false, new FakeShuffleOrder(/* length= */ 2), - new FakeMediaSource(fakeTimeline, Builder.VIDEO_FORMAT), - new FakeMediaSource(fakeTimeline, Builder.VIDEO_FORMAT)); + new FakeMediaSource(fakeTimeline, ExoPlayerTestRunner.VIDEO_FORMAT), + new FakeMediaSource(fakeTimeline, ExoPlayerTestRunner.VIDEO_FORMAT)); ConcatenatingMediaSource secondMediaSource = new ConcatenatingMediaSource( /* isAtomic= */ false, new FakeShuffleOrder(/* length= */ 2), - new FakeMediaSource(fakeTimeline, Builder.VIDEO_FORMAT), - new FakeMediaSource(fakeTimeline, Builder.VIDEO_FORMAT)); + new FakeMediaSource(fakeTimeline, ExoPlayerTestRunner.VIDEO_FORMAT), + new FakeMediaSource(fakeTimeline, ExoPlayerTestRunner.VIDEO_FORMAT)); ActionSchedule actionSchedule = - new ActionSchedule.Builder("testRepreparationWithShuffle") + new ActionSchedule.Builder(TAG) // Wait for first preparation and enable shuffling. Plays period 0. .pause() .waitForPlaybackState(Player.STATE_READY) .setShuffleModeEnabled(true) - // Reprepare with second media source (keeping state, but with position reset). + // Set the second media source (with position reset). // Plays period 1 and 0 because of the reversed fake shuffle order. - .prepareSource(secondMediaSource, /* resetPosition= */ true, /* resetState= */ false) + .setMediaSources(/* resetPosition= */ true, secondMediaSource) .play() + .waitForPositionDiscontinuity() .build(); ExoPlayerTestRunner testRunner = - new ExoPlayerTestRunner.Builder() - .setMediaSource(firstMediaSource) + new ExoPlayerTestRunner.Builder(context) + .setMediaSources(firstMediaSource) .setActionSchedule(actionSchedule) - .build(context) + .build() .start() + .blockUntilActionScheduleFinished(TIMEOUT_MS) .blockUntilEnded(TIMEOUT_MS); testRunner.assertPlayedPeriodIndices(0, 1, 0); } @Test - public void testSetPlaybackParametersBeforePreparationCompletesSucceeds() throws Exception { + public void setPlaybackParametersBeforePreparationCompletesSucceeds() throws Exception { // Test that no exception is thrown when playback parameters are updated between creating a // period and preparation of the period completing. final CountDownLatch createPeriodCalledCountDownLatch = new CountDownLatch(1); final FakeMediaPeriod[] fakeMediaPeriodHolder = new FakeMediaPeriod[1]; MediaSource mediaSource = - new FakeMediaSource(new FakeTimeline(/* windowCount= */ 1), Builder.VIDEO_FORMAT) { + new FakeMediaSource( + new FakeTimeline(/* windowCount= */ 1), ExoPlayerTestRunner.VIDEO_FORMAT) { @Override protected FakeMediaPeriod createFakeMediaPeriod( MediaPeriodId id, @@ -906,7 +914,7 @@ public final class ExoPlayerTest { } }; ActionSchedule actionSchedule = - new ActionSchedule.Builder("testSetPlaybackParametersBeforePreparationCompletesSucceeds") + new ActionSchedule.Builder(TAG) .waitForPlaybackState(Player.STATE_BUFFERING) // Block until createPeriod has been called on the fake media source. .executeRunnable( @@ -917,25 +925,89 @@ public final class ExoPlayerTest { throw new IllegalStateException(e); } }) - // Set playback parameters (while the fake media period is not yet prepared). - .setPlaybackParameters(new PlaybackParameters(/* speed= */ 2f, /* pitch= */ 2f)) + // Set playback speed (while the fake media period is not yet prepared). + .setPlaybackSpeed(2f) // Complete preparation of the fake media period. .executeRunnable(() -> fakeMediaPeriodHolder[0].setPreparationComplete()) .build(); - new ExoPlayerTestRunner.Builder() - .setMediaSource(mediaSource) + new ExoPlayerTestRunner.Builder(context) + .setMediaSources(mediaSource) .setActionSchedule(actionSchedule) - .build(context) + .build() .start() .blockUntilEnded(TIMEOUT_MS); } @Test - public void testStopDoesNotResetPosition() throws Exception { + public void seekBeforePreparationCompletes_seeksToCorrectPosition() throws Exception { + CountDownLatch createPeriodCalledCountDownLatch = new CountDownLatch(1); + FakeMediaPeriod[] fakeMediaPeriodHolder = new FakeMediaPeriod[1]; + Timeline timeline = new FakeTimeline(/* windowCount= */ 1); + FakeMediaSource mediaSource = + new FakeMediaSource(/* timeline= */ null, ExoPlayerTestRunner.VIDEO_FORMAT) { + @Override + protected FakeMediaPeriod createFakeMediaPeriod( + MediaPeriodId id, + TrackGroupArray trackGroupArray, + Allocator allocator, + EventDispatcher eventDispatcher, + @Nullable TransferListener transferListener) { + // Defer completing preparation of the period until seek has been sent. + fakeMediaPeriodHolder[0] = + new FakeMediaPeriod(trackGroupArray, eventDispatcher, /* deferOnPrepared= */ true); + createPeriodCalledCountDownLatch.countDown(); + return fakeMediaPeriodHolder[0]; + } + }; + AtomicLong positionWhenReady = new AtomicLong(); + ActionSchedule actionSchedule = + new ActionSchedule.Builder(TAG) + .pause() + .waitForPlaybackState(Player.STATE_BUFFERING) + // Ensure we use the MaskingMediaPeriod by delaying the initial timeline update. + .delay(1) + .executeRunnable(() -> mediaSource.setNewSourceInfo(timeline)) + .waitForTimelineChanged() + // Block until createPeriod has been called on the fake media source. + .executeRunnable( + () -> { + try { + createPeriodCalledCountDownLatch.await(); + } catch (InterruptedException e) { + throw new IllegalStateException(e); + } + }) + // Seek before preparation completes. + .seek(5000) + // Complete preparation of the fake media period. + .executeRunnable(() -> fakeMediaPeriodHolder[0].setPreparationComplete()) + .waitForPlaybackState(Player.STATE_READY) + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + positionWhenReady.set(player.getCurrentPosition()); + } + }) + .play() + .build(); + new ExoPlayerTestRunner.Builder(context) + .initialSeek(/* windowIndex= */ 0, /* positionMs= */ 2000) + .setMediaSources(mediaSource) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilEnded(TIMEOUT_MS); + + assertThat(positionWhenReady.get()).isAtLeast(5000); + } + + @Test + public void stopDoesNotResetPosition() throws Exception { Timeline timeline = new FakeTimeline(/* windowCount= */ 1); final long[] positionHolder = new long[1]; ActionSchedule actionSchedule = - new ActionSchedule.Builder("testStopDoesNotResetPosition") + new ActionSchedule.Builder(TAG) .pause() .waitForPlaybackState(Player.STATE_READY) .playUntilPosition(/* windowIndex= */ 0, /* positionMs= */ 50) @@ -949,25 +1021,27 @@ public final class ExoPlayerTest { }) .build(); ExoPlayerTestRunner testRunner = - new ExoPlayerTestRunner.Builder() + new ExoPlayerTestRunner.Builder(context) .setTimeline(timeline) .setActionSchedule(actionSchedule) - .build(context) + .build() .start() .blockUntilActionScheduleFinished(TIMEOUT_MS) .blockUntilEnded(TIMEOUT_MS); - testRunner.assertTimelinesEqual(timeline); - testRunner.assertTimelineChangeReasonsEqual(Player.TIMELINE_CHANGE_REASON_PREPARED); + testRunner.assertTimelinesSame(dummyTimeline, timeline); + testRunner.assertTimelineChangeReasonsEqual( + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, + Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE); testRunner.assertNoPositionDiscontinuities(); assertThat(positionHolder[0]).isAtLeast(50L); } @Test - public void testStopWithoutResetDoesNotResetPosition() throws Exception { + public void stopWithoutResetDoesNotResetPosition() throws Exception { Timeline timeline = new FakeTimeline(/* windowCount= */ 1); final long[] positionHolder = new long[1]; ActionSchedule actionSchedule = - new ActionSchedule.Builder("testStopWithoutResetDoesNotReset") + new ActionSchedule.Builder(TAG) .pause() .waitForPlaybackState(Player.STATE_READY) .playUntilPosition(/* windowIndex= */ 0, /* positionMs= */ 50) @@ -981,25 +1055,27 @@ public final class ExoPlayerTest { }) .build(); ExoPlayerTestRunner testRunner = - new ExoPlayerTestRunner.Builder() + new ExoPlayerTestRunner.Builder(context) .setTimeline(timeline) .setActionSchedule(actionSchedule) - .build(context) + .build() .start() .blockUntilActionScheduleFinished(TIMEOUT_MS) .blockUntilEnded(TIMEOUT_MS); - testRunner.assertTimelinesEqual(timeline); - testRunner.assertTimelineChangeReasonsEqual(Player.TIMELINE_CHANGE_REASON_PREPARED); + testRunner.assertTimelinesSame(dummyTimeline, timeline); + testRunner.assertTimelineChangeReasonsEqual( + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, + Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE); testRunner.assertNoPositionDiscontinuities(); assertThat(positionHolder[0]).isAtLeast(50L); } @Test - public void testStopWithResetDoesResetPosition() throws Exception { + public void stopWithResetDoesResetPosition() throws Exception { Timeline timeline = new FakeTimeline(/* windowCount= */ 1); final long[] positionHolder = new long[1]; ActionSchedule actionSchedule = - new ActionSchedule.Builder("testStopWithResetDoesReset") + new ActionSchedule.Builder(TAG) .pause() .waitForPlaybackState(Player.STATE_READY) .playUntilPosition(/* windowIndex= */ 0, /* positionMs= */ 50) @@ -1013,34 +1089,37 @@ public final class ExoPlayerTest { }) .build(); ExoPlayerTestRunner testRunner = - new ExoPlayerTestRunner.Builder() + new ExoPlayerTestRunner.Builder(context) .setTimeline(timeline) .setActionSchedule(actionSchedule) - .build(context) + .build() .start() .blockUntilActionScheduleFinished(TIMEOUT_MS) .blockUntilEnded(TIMEOUT_MS); - testRunner.assertTimelinesEqual(timeline, Timeline.EMPTY); + testRunner.assertTimelinesSame(dummyTimeline, timeline, Timeline.EMPTY); testRunner.assertTimelineChangeReasonsEqual( - Player.TIMELINE_CHANGE_REASON_PREPARED, Player.TIMELINE_CHANGE_REASON_RESET); + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, + Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE, + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED); testRunner.assertNoPositionDiscontinuities(); assertThat(positionHolder[0]).isEqualTo(0); } @Test - public void testStopWithoutResetReleasesMediaSource() throws Exception { + public void stopWithoutResetReleasesMediaSource() throws Exception { Timeline timeline = new FakeTimeline(/* windowCount= */ 1); - final FakeMediaSource mediaSource = new FakeMediaSource(timeline, Builder.VIDEO_FORMAT); + final FakeMediaSource mediaSource = + new FakeMediaSource(timeline, ExoPlayerTestRunner.VIDEO_FORMAT); ActionSchedule actionSchedule = - new ActionSchedule.Builder("testStopReleasesMediaSource") + new ActionSchedule.Builder(TAG) .waitForPlaybackState(Player.STATE_READY) .stop(/* reset= */ false) .build(); ExoPlayerTestRunner testRunner = - new ExoPlayerTestRunner.Builder() + new ExoPlayerTestRunner.Builder(context) .setTimeline(timeline) .setActionSchedule(actionSchedule) - .build(context) + .build() .start() .blockUntilActionScheduleFinished(TIMEOUT_MS); mediaSource.assertReleased(); @@ -1048,19 +1127,20 @@ public final class ExoPlayerTest { } @Test - public void testStopWithResetReleasesMediaSource() throws Exception { + public void stopWithResetReleasesMediaSource() throws Exception { Timeline timeline = new FakeTimeline(/* windowCount= */ 1); - final FakeMediaSource mediaSource = new FakeMediaSource(timeline, Builder.VIDEO_FORMAT); + final FakeMediaSource mediaSource = + new FakeMediaSource(timeline, ExoPlayerTestRunner.VIDEO_FORMAT); ActionSchedule actionSchedule = - new ActionSchedule.Builder("testStopReleasesMediaSource") + new ActionSchedule.Builder(TAG) .waitForPlaybackState(Player.STATE_READY) .stop(/* reset= */ true) .build(); ExoPlayerTestRunner testRunner = - new ExoPlayerTestRunner.Builder() + new ExoPlayerTestRunner.Builder(context) .setTimeline(timeline) .setActionSchedule(actionSchedule) - .build(context) + .build() .start() .blockUntilActionScheduleFinished(TIMEOUT_MS); mediaSource.assertReleased(); @@ -1068,80 +1148,87 @@ public final class ExoPlayerTest { } @Test - public void testRepreparationDoesNotResetAfterStopWithReset() throws Exception { - Timeline timeline = new FakeTimeline(/* windowCount= */ 1); - MediaSource secondSource = new FakeMediaSource(timeline, Builder.VIDEO_FORMAT); - ActionSchedule actionSchedule = - new ActionSchedule.Builder("testRepreparationAfterStop") - .waitForPlaybackState(Player.STATE_READY) - .stop(/* reset= */ true) - .waitForPlaybackState(Player.STATE_IDLE) - .prepareSource(secondSource) - .build(); - ExoPlayerTestRunner testRunner = - new ExoPlayerTestRunner.Builder() - .setTimeline(timeline) - .setActionSchedule(actionSchedule) - .setExpectedPlayerEndedCount(2) - .build(context) - .start() - .blockUntilEnded(TIMEOUT_MS); - testRunner.assertTimelinesEqual(timeline, Timeline.EMPTY, timeline); - testRunner.assertTimelineChangeReasonsEqual( - Player.TIMELINE_CHANGE_REASON_PREPARED, - Player.TIMELINE_CHANGE_REASON_RESET, - Player.TIMELINE_CHANGE_REASON_PREPARED); - testRunner.assertNoPositionDiscontinuities(); - } - - @Test - public void testSeekBeforeRepreparationPossibleAfterStopWithReset() throws Exception { + public void settingNewStartPositionPossibleAfterStopWithReset() throws Exception { Timeline timeline = new FakeTimeline(/* windowCount= */ 1); Timeline secondTimeline = new FakeTimeline(/* windowCount= */ 2); - MediaSource secondSource = new FakeMediaSource(secondTimeline, Builder.VIDEO_FORMAT); + MediaSource secondSource = + new FakeMediaSource(secondTimeline, ExoPlayerTestRunner.VIDEO_FORMAT); + AtomicInteger windowIndexAfterStop = new AtomicInteger(); + AtomicLong positionAfterStop = new AtomicLong(); ActionSchedule actionSchedule = - new ActionSchedule.Builder("testSeekAfterStopWithReset") + new ActionSchedule.Builder(TAG) .waitForPlaybackState(Player.STATE_READY) .stop(/* reset= */ true) .waitForPlaybackState(Player.STATE_IDLE) - // If we were still using the first timeline, this would throw. - .seek(/* windowIndex= */ 1, /* positionMs= */ 0) - .prepareSource(secondSource, /* resetPosition= */ false, /* resetState= */ true) + .seek(/* windowIndex= */ 1, /* positionMs= */ 1000) + .setMediaSources(secondSource) + .prepare() + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + windowIndexAfterStop.set(player.getCurrentWindowIndex()); + positionAfterStop.set(player.getCurrentPosition()); + } + }) + .waitForPlaybackState(Player.STATE_READY) .build(); ExoPlayerTestRunner testRunner = - new ExoPlayerTestRunner.Builder() + new ExoPlayerTestRunner.Builder(context) .setTimeline(timeline) .setActionSchedule(actionSchedule) .setExpectedPlayerEndedCount(2) - .build(context) + .build() .start() .blockUntilEnded(TIMEOUT_MS); - testRunner.assertTimelinesEqual(timeline, Timeline.EMPTY, secondTimeline); + testRunner.assertPlaybackStatesEqual( + Player.STATE_BUFFERING, + Player.STATE_READY, + Player.STATE_IDLE, + Player.STATE_BUFFERING, + Player.STATE_READY, + Player.STATE_ENDED); + testRunner.assertTimelinesSame( + dummyTimeline, + timeline, + Timeline.EMPTY, + new FakeMediaSource.InitialTimeline(secondTimeline), + secondTimeline); testRunner.assertTimelineChangeReasonsEqual( - Player.TIMELINE_CHANGE_REASON_PREPARED, - Player.TIMELINE_CHANGE_REASON_RESET, - Player.TIMELINE_CHANGE_REASON_PREPARED); - testRunner.assertPositionDiscontinuityReasonsEqual(Player.DISCONTINUITY_REASON_SEEK); + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, + Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE, + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, // stop(true) + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, + Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE); + assertThat(windowIndexAfterStop.get()).isEqualTo(1); + assertThat(positionAfterStop.get()).isAtLeast(1000L); testRunner.assertPlayedPeriodIndices(0, 1); } @Test - public void testReprepareAndKeepPositionWithNewMediaSource() throws Exception { + public void resetPlaylistWithPreviousPosition() throws Exception { + Object firstWindowId = new Object(); Timeline timeline = new FakeTimeline( - new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ new Object())); + new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ firstWindowId)); + Timeline firstExpectedMaskingTimeline = + new MaskingMediaSource.DummyTimeline(/* tag= */ firstWindowId); + Object secondWindowId = new Object(); Timeline secondTimeline = new FakeTimeline( - new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ new Object())); + new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ secondWindowId)); + Timeline secondExpectedMaskingTimeline = + new MaskingMediaSource.DummyTimeline(/* tag= */ secondWindowId); MediaSource secondSource = new FakeMediaSource(secondTimeline); AtomicLong positionAfterReprepare = new AtomicLong(); ActionSchedule actionSchedule = - new ActionSchedule.Builder("testReprepareAndKeepPositionWithNewMediaSource") + new ActionSchedule.Builder(TAG) .pause() .waitForPlaybackState(Player.STATE_READY) .playUntilPosition(/* windowIndex= */ 0, /* positionMs= */ 2000) - .prepareSource(secondSource, /* resetPosition= */ false, /* resetState= */ true) - .waitForTimelineChanged(secondTimeline) + .setMediaSources(/* windowIndex= */ 0, /* positionMs= */ 2000, secondSource) + .waitForTimelineChanged( + secondTimeline, /* expectedReason */ Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE) .executeRunnable( new PlayerRunnable() { @Override @@ -1152,107 +1239,217 @@ public final class ExoPlayerTest { .play() .build(); ExoPlayerTestRunner testRunner = - new ExoPlayerTestRunner.Builder() + new ExoPlayerTestRunner.Builder(context) .setTimeline(timeline) .setActionSchedule(actionSchedule) - .build(context) + .build() .start() .blockUntilActionScheduleFinished(TIMEOUT_MS) .blockUntilEnded(TIMEOUT_MS); - testRunner.assertTimelinesEqual(timeline, Timeline.EMPTY, secondTimeline); + testRunner.assertTimelinesSame( + firstExpectedMaskingTimeline, timeline, secondExpectedMaskingTimeline, secondTimeline); + testRunner.assertTimelineChangeReasonsEqual( + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, + Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE, + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, + Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE); assertThat(positionAfterReprepare.get()).isAtLeast(2000L); } @Test - public void testStopDuringPreparationOverwritesPreparation() throws Exception { - Timeline timeline = new FakeTimeline(/* windowCount= */ 1); + public void resetPlaylistStartsFromDefaultPosition() throws Exception { + Object firstWindowId = new Object(); + Timeline timeline = + new FakeTimeline( + new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ firstWindowId)); + Timeline firstExpectedDummyTimeline = + new MaskingMediaSource.DummyTimeline(/* tag= */ firstWindowId); + Object secondWindowId = new Object(); + Timeline secondTimeline = + new FakeTimeline( + new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ secondWindowId)); + Timeline secondExpectedDummyTimeline = + new MaskingMediaSource.DummyTimeline(/* tag= */ secondWindowId); + MediaSource secondSource = new FakeMediaSource(secondTimeline); + AtomicLong positionAfterReprepare = new AtomicLong(); ActionSchedule actionSchedule = - new ActionSchedule.Builder("testStopOverwritesPrepare") - .waitForPlaybackState(Player.STATE_BUFFERING) - .seek(0) - .stop(true) - .waitForSeekProcessed() + new ActionSchedule.Builder(TAG) + .pause() + .waitForPlaybackState(Player.STATE_READY) + .playUntilPosition(/* windowIndex= */ 0, /* positionMs= */ 2000) + .setMediaSources(/* resetPosition= */ true, secondSource) + .waitForTimelineChanged( + secondTimeline, /* expectedReason */ Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE) + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + positionAfterReprepare.set(player.getCurrentPosition()); + } + }) + .play() .build(); ExoPlayerTestRunner testRunner = - new ExoPlayerTestRunner.Builder() + new ExoPlayerTestRunner.Builder(context) .setTimeline(timeline) .setActionSchedule(actionSchedule) - .build(context) + .build() .start() .blockUntilActionScheduleFinished(TIMEOUT_MS) .blockUntilEnded(TIMEOUT_MS); - testRunner.assertTimelinesEqual(Timeline.EMPTY); - testRunner.assertTimelineChangeReasonsEqual(Player.TIMELINE_CHANGE_REASON_PREPARED); - testRunner.assertPositionDiscontinuityReasonsEqual(Player.DISCONTINUITY_REASON_SEEK); + + testRunner.assertTimelinesSame( + firstExpectedDummyTimeline, timeline, secondExpectedDummyTimeline, secondTimeline); + testRunner.assertTimelineChangeReasonsEqual( + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, + Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE, + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, + Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE); + assertThat(positionAfterReprepare.get()).isEqualTo(0L); } @Test - public void testStopAndSeekAfterStopDoesNotResetTimeline() throws Exception { - // Combining additional stop and seek after initial stop in one test to get the seek processed - // callback which ensures that all operations have been processed by the player. + public void resetPlaylistWithoutResettingPositionStartsFromOldPosition() throws Exception { + Object firstWindowId = new Object(); + Timeline timeline = + new FakeTimeline( + new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ firstWindowId)); + Timeline firstExpectedDummyTimeline = + new MaskingMediaSource.DummyTimeline(/* tag= */ firstWindowId); + Object secondWindowId = new Object(); + Timeline secondTimeline = + new FakeTimeline( + new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ secondWindowId)); + Timeline secondExpectedDummyTimeline = + new MaskingMediaSource.DummyTimeline(/* tag= */ secondWindowId); + MediaSource secondSource = new FakeMediaSource(secondTimeline); + AtomicLong positionAfterReprepare = new AtomicLong(); + ActionSchedule actionSchedule = + new ActionSchedule.Builder(TAG) + .pause() + .waitForPlaybackState(Player.STATE_READY) + .playUntilPosition(/* windowIndex= */ 0, /* positionMs= */ 2000) + .setMediaSources(secondSource) + .waitForTimelineChanged( + secondTimeline, /* expectedReason */ Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE) + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + positionAfterReprepare.set(player.getCurrentPosition()); + } + }) + .play() + .build(); + ExoPlayerTestRunner testRunner = + new ExoPlayerTestRunner.Builder(context) + .setTimeline(timeline) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilActionScheduleFinished(TIMEOUT_MS) + .blockUntilEnded(TIMEOUT_MS); + + testRunner.assertTimelinesSame( + firstExpectedDummyTimeline, timeline, secondExpectedDummyTimeline, secondTimeline); + testRunner.assertTimelineChangeReasonsEqual( + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, + Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE, + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, + Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE); + assertThat(positionAfterReprepare.get()).isAtLeast(2000L); + } + + @Test + public void stopDuringPreparationOverwritesPreparation() throws Exception { Timeline timeline = new FakeTimeline(/* windowCount= */ 1); ActionSchedule actionSchedule = - new ActionSchedule.Builder("testStopTwice") + new ActionSchedule.Builder(TAG) + .waitForPlaybackState(Player.STATE_BUFFERING) + .stop(true) + .waitForPendingPlayerCommands() + .build(); + ExoPlayerTestRunner testRunner = + new ExoPlayerTestRunner.Builder(context) + .setTimeline(timeline) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilActionScheduleFinished(TIMEOUT_MS) + .blockUntilEnded(TIMEOUT_MS); + testRunner.assertTimelinesSame(dummyTimeline, Timeline.EMPTY); + testRunner.assertTimelineChangeReasonsEqual( + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED); + } + + @Test + public void stopAndSeekAfterStopDoesNotResetTimeline() throws Exception { + Timeline timeline = new FakeTimeline(/* windowCount= */ 1); + ActionSchedule actionSchedule = + new ActionSchedule.Builder(TAG) .waitForPlaybackState(Player.STATE_READY) .stop(false) .stop(false) - .seek(0) - .waitForSeekProcessed() + // Wait until the player fully processed the second stop to see that no further + // callbacks are triggered. + .waitForPendingPlayerCommands() .build(); ExoPlayerTestRunner testRunner = - new ExoPlayerTestRunner.Builder() + new ExoPlayerTestRunner.Builder(context) .setTimeline(timeline) .setActionSchedule(actionSchedule) - .build(context) + .build() .start() .blockUntilActionScheduleFinished(TIMEOUT_MS) .blockUntilEnded(TIMEOUT_MS); - testRunner.assertTimelinesEqual(timeline); - testRunner.assertTimelineChangeReasonsEqual(Player.TIMELINE_CHANGE_REASON_PREPARED); - testRunner.assertPositionDiscontinuityReasonsEqual(Player.DISCONTINUITY_REASON_SEEK); + testRunner.assertTimelinesSame(dummyTimeline, timeline); + testRunner.assertTimelineChangeReasonsEqual( + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, + Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE); } @Test - public void testReprepareAfterPlaybackError() throws Exception { + public void reprepareAfterPlaybackError() throws Exception { Timeline timeline = new FakeTimeline(/* windowCount= */ 1); ActionSchedule actionSchedule = - new ActionSchedule.Builder("testReprepareAfterPlaybackError") + new ActionSchedule.Builder(TAG) .waitForPlaybackState(Player.STATE_READY) .throwPlaybackException(ExoPlaybackException.createForSource(new IOException())) .waitForPlaybackState(Player.STATE_IDLE) - .prepareSource( - new FakeMediaSource(timeline), /* resetPosition= */ true, /* resetState= */ false) - .waitForPlaybackState(Player.STATE_READY) + .prepare() + .waitForPlaybackState(Player.STATE_BUFFERING) .build(); ExoPlayerTestRunner testRunner = - new ExoPlayerTestRunner.Builder() + new ExoPlayerTestRunner.Builder(context) .setTimeline(timeline) .setActionSchedule(actionSchedule) - .build(context); + .build(); try { testRunner.start().blockUntilActionScheduleFinished(TIMEOUT_MS).blockUntilEnded(TIMEOUT_MS); fail(); } catch (ExoPlaybackException e) { // Expected exception. } - testRunner.assertTimelinesEqual(timeline, timeline); + testRunner.assertTimelinesSame(dummyTimeline, timeline); testRunner.assertTimelineChangeReasonsEqual( - Player.TIMELINE_CHANGE_REASON_PREPARED, Player.TIMELINE_CHANGE_REASON_PREPARED); + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, + Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE); } @Test - public void testSeekAndReprepareAfterPlaybackError() throws Exception { + public void seekAndReprepareAfterPlaybackError() throws Exception { Timeline timeline = new FakeTimeline(/* windowCount= */ 1); final long[] positionHolder = new long[2]; ActionSchedule actionSchedule = - new ActionSchedule.Builder("testReprepareAfterPlaybackError") + new ActionSchedule.Builder(TAG) .pause() .waitForPlaybackState(Player.STATE_READY) .throwPlaybackException(ExoPlaybackException.createForSource(new IOException())) .waitForPlaybackState(Player.STATE_IDLE) .seek(/* positionMs= */ 50) - .waitForSeekProcessed() + .waitForPendingPlayerCommands() .executeRunnable( new PlayerRunnable() { @Override @@ -1260,8 +1457,7 @@ public final class ExoPlayerTest { positionHolder[0] = player.getCurrentPosition(); } }) - .prepareSource( - new FakeMediaSource(timeline), /* resetPosition= */ false, /* resetState= */ false) + .prepare() .waitForPlaybackState(Player.STATE_READY) .executeRunnable( new PlayerRunnable() { @@ -1273,62 +1469,41 @@ public final class ExoPlayerTest { .play() .build(); ExoPlayerTestRunner testRunner = - new ExoPlayerTestRunner.Builder() + new ExoPlayerTestRunner.Builder(context) .setTimeline(timeline) .setActionSchedule(actionSchedule) - .build(context); - try { - testRunner.start().blockUntilActionScheduleFinished(TIMEOUT_MS).blockUntilEnded(TIMEOUT_MS); - fail(); - } catch (ExoPlaybackException e) { - // Expected exception. - } - testRunner.assertTimelinesEqual(timeline, timeline); + .build(); + + assertThrows( + ExoPlaybackException.class, + () -> + testRunner + .start() + .blockUntilActionScheduleFinished(TIMEOUT_MS) + .blockUntilEnded(TIMEOUT_MS)); + testRunner.assertTimelinesSame(dummyTimeline, timeline); testRunner.assertTimelineChangeReasonsEqual( - Player.TIMELINE_CHANGE_REASON_PREPARED, Player.TIMELINE_CHANGE_REASON_PREPARED); + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, + Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE); testRunner.assertPositionDiscontinuityReasonsEqual(Player.DISCONTINUITY_REASON_SEEK); assertThat(positionHolder[0]).isEqualTo(50); assertThat(positionHolder[1]).isEqualTo(50); } - @Test - public void testInvalidSeekPositionAfterSourceInfoRefreshStillUpdatesTimeline() throws Exception { - final Timeline timeline = new FakeTimeline(/* windowCount= */ 1); - final FakeMediaSource mediaSource = new FakeMediaSource(/* timeline= */ null); - ActionSchedule actionSchedule = - new ActionSchedule.Builder("testInvalidSeekPositionSourceInfoRefreshStillUpdatesTimeline") - .waitForPlaybackState(Player.STATE_BUFFERING) - // Seeking to an invalid position will end playback. - .seek(/* windowIndex= */ 100, /* positionMs= */ 0) - .executeRunnable(() -> mediaSource.setNewSourceInfo(timeline, /* newManifest= */ null)) - .waitForPlaybackState(Player.STATE_ENDED) - .build(); - ExoPlayerTestRunner testRunner = - new ExoPlayerTestRunner.Builder() - .setMediaSource(mediaSource) - .setActionSchedule(actionSchedule) - .build(context); - testRunner.start().blockUntilActionScheduleFinished(TIMEOUT_MS).blockUntilEnded(TIMEOUT_MS); - - testRunner.assertTimelinesEqual(timeline); - testRunner.assertTimelineChangeReasonsEqual(Player.TIMELINE_CHANGE_REASON_PREPARED); - } - @Test public void testInvalidSeekPositionAfterSourceInfoRefreshWithShuffleModeEnabledUsesCorrectFirstPeriod() throws Exception { - FakeMediaSource mediaSource = new FakeMediaSource(new FakeTimeline(/* windowCount= */ 1)); - ConcatenatingMediaSource concatenatingMediaSource = - new ConcatenatingMediaSource( - /* isAtomic= */ false, new FakeShuffleOrder(0), mediaSource, mediaSource); + FakeMediaSource mediaSource = new FakeMediaSource(new FakeTimeline(/* windowCount= */ 2)); AtomicInteger windowIndexAfterUpdate = new AtomicInteger(); ActionSchedule actionSchedule = - new ActionSchedule.Builder("testInvalidSeekPositionSourceInfoRefreshUsesCorrectFirstPeriod") + new ActionSchedule.Builder(TAG) + .setShuffleOrder(new FakeShuffleOrder(/* length= */ 0)) .setShuffleModeEnabled(true) .waitForPlaybackState(Player.STATE_BUFFERING) // Seeking to an invalid position will end playback. - .seek(/* windowIndex= */ 100, /* positionMs= */ 0) + .seek( + /* windowIndex= */ 100, /* positionMs= */ 0, /* catchIllegalSeekException= */ true) .waitForPlaybackState(Player.STATE_ENDED) .executeRunnable( new PlayerRunnable() { @@ -1338,18 +1513,19 @@ public final class ExoPlayerTest { } }) .build(); - ExoPlayerTestRunner testRunner = - new ExoPlayerTestRunner.Builder() - .setMediaSource(concatenatingMediaSource) - .setActionSchedule(actionSchedule) - .build(context); - testRunner.start().blockUntilActionScheduleFinished(TIMEOUT_MS).blockUntilEnded(TIMEOUT_MS); + new ExoPlayerTestRunner.Builder(context) + .setMediaSources(mediaSource) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilActionScheduleFinished(TIMEOUT_MS) + .blockUntilEnded(TIMEOUT_MS); assertThat(windowIndexAfterUpdate.get()).isEqualTo(1); } @Test - public void testRestartAfterEmptyTimelineWithShuffleModeEnabledUsesCorrectFirstPeriod() + public void restartAfterEmptyTimelineWithShuffleModeEnabledUsesCorrectFirstPeriod() throws Exception { Timeline timeline = new FakeTimeline(/* windowCount= */ 1); FakeMediaSource mediaSource = new FakeMediaSource(timeline); @@ -1357,7 +1533,7 @@ public final class ExoPlayerTest { new ConcatenatingMediaSource(/* isAtomic= */ false, new FakeShuffleOrder(0)); AtomicInteger windowIndexAfterAddingSources = new AtomicInteger(); ActionSchedule actionSchedule = - new ActionSchedule.Builder("testRestartAfterEmptyTimelineUsesCorrectFirstPeriod") + new ActionSchedule.Builder(TAG) .setShuffleModeEnabled(true) // Preparing with an empty media source will transition to ended state. .waitForPlaybackState(Player.STATE_ENDED) @@ -1376,10 +1552,10 @@ public final class ExoPlayerTest { } }) .build(); - new ExoPlayerTestRunner.Builder() - .setMediaSource(concatenatingMediaSource) + new ExoPlayerTestRunner.Builder(context) + .setMediaSources(concatenatingMediaSource) .setActionSchedule(actionSchedule) - .build(context) + .build() .start() .blockUntilActionScheduleFinished(TIMEOUT_MS) .blockUntilEnded(TIMEOUT_MS); @@ -1387,13 +1563,13 @@ public final class ExoPlayerTest { } @Test - public void testPlaybackErrorAndReprepareDoesNotResetPosition() throws Exception { + public void playbackErrorAndReprepareDoesNotResetPosition() throws Exception { final Timeline timeline = new FakeTimeline(/* windowCount= */ 2); final long[] positionHolder = new long[3]; final int[] windowIndexHolder = new int[3]; - final FakeMediaSource secondMediaSource = new FakeMediaSource(/* timeline= */ null); + final FakeMediaSource firstMediaSource = new FakeMediaSource(timeline); ActionSchedule actionSchedule = - new ActionSchedule.Builder("testPlaybackErrorDoesNotResetPosition") + new ActionSchedule.Builder(TAG) .pause() .waitForPlaybackState(Player.STATE_READY) .playUntilPosition(/* windowIndex= */ 1, /* positionMs= */ 500) @@ -1408,8 +1584,7 @@ public final class ExoPlayerTest { windowIndexHolder[0] = player.getCurrentWindowIndex(); } }) - .prepareSource(secondMediaSource, /* resetPosition= */ false, /* resetState= */ false) - .waitForPlaybackState(Player.STATE_BUFFERING) + .prepare() .executeRunnable( new PlayerRunnable() { @Override @@ -1417,7 +1592,6 @@ public final class ExoPlayerTest { // Position while repreparing. positionHolder[1] = player.getCurrentPosition(); windowIndexHolder[1] = player.getCurrentWindowIndex(); - secondMediaSource.setNewSourceInfo(timeline, /* newManifest= */ null); } }) .waitForPlaybackState(Player.STATE_READY) @@ -1433,10 +1607,10 @@ public final class ExoPlayerTest { .play() .build(); ExoPlayerTestRunner testRunner = - new ExoPlayerTestRunner.Builder() - .setTimeline(timeline) + new ExoPlayerTestRunner.Builder(context) + .setMediaSources(firstMediaSource) .setActionSchedule(actionSchedule) - .build(context); + .build(); try { testRunner.start().blockUntilActionScheduleFinished(TIMEOUT_MS).blockUntilEnded(TIMEOUT_MS); fail(); @@ -1451,17 +1625,84 @@ public final class ExoPlayerTest { assertThat(windowIndexHolder[2]).isEqualTo(1); } + @Test + public void seekAfterPlaybackError() throws Exception { + final Timeline timeline = new FakeTimeline(/* windowCount= */ 2); + final long[] positionHolder = {C.TIME_UNSET, C.TIME_UNSET, C.TIME_UNSET}; + final int[] windowIndexHolder = {C.INDEX_UNSET, C.INDEX_UNSET, C.INDEX_UNSET}; + final FakeMediaSource firstMediaSource = new FakeMediaSource(timeline); + ActionSchedule actionSchedule = + new ActionSchedule.Builder(TAG) + .pause() + .waitForPlaybackState(Player.STATE_READY) + .playUntilPosition(/* windowIndex= */ 1, /* positionMs= */ 500) + .throwPlaybackException(ExoPlaybackException.createForSource(new IOException())) + .waitForPlaybackState(Player.STATE_IDLE) + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + // Position while in error state + positionHolder[0] = player.getCurrentPosition(); + windowIndexHolder[0] = player.getCurrentWindowIndex(); + } + }) + .seek(/* windowIndex= */ 0, /* positionMs= */ C.TIME_UNSET) + .waitForPendingPlayerCommands() + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + // Position while in error state + positionHolder[1] = player.getCurrentPosition(); + windowIndexHolder[1] = player.getCurrentWindowIndex(); + } + }) + .prepare() + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + // Position after prepare. + positionHolder[2] = player.getCurrentPosition(); + windowIndexHolder[2] = player.getCurrentWindowIndex(); + } + }) + .play() + .build(); + ExoPlayerTestRunner testRunner = + new ExoPlayerTestRunner.Builder(context) + .setMediaSources(firstMediaSource) + .setActionSchedule(actionSchedule) + .build(); + + assertThrows( + ExoPlaybackException.class, + () -> + testRunner + .start() + .blockUntilActionScheduleFinished(TIMEOUT_MS) + .blockUntilEnded(TIMEOUT_MS)); + assertThat(positionHolder[0]).isAtLeast(500L); + assertThat(positionHolder[1]).isEqualTo(0L); + assertThat(positionHolder[2]).isEqualTo(0L); + assertThat(windowIndexHolder[0]).isEqualTo(1); + assertThat(windowIndexHolder[1]).isEqualTo(0); + assertThat(windowIndexHolder[2]).isEqualTo(0); + } + @Test public void playbackErrorAndReprepareWithPositionResetKeepsWindowSequenceNumber() throws Exception { FakeMediaSource mediaSource = new FakeMediaSource(new FakeTimeline(/* windowCount= */ 1)); ActionSchedule actionSchedule = - new ActionSchedule.Builder("playbackErrorWithResetKeepsWindowSequenceNumber") + new ActionSchedule.Builder(TAG) .pause() .waitForPlaybackState(Player.STATE_READY) .throwPlaybackException(ExoPlaybackException.createForSource(new IOException())) .waitForPlaybackState(Player.STATE_IDLE) - .prepareSource(mediaSource, /* resetPosition= */ true, /* resetState= */ false) + .seek(0, C.TIME_UNSET) + .prepare() .waitForPlaybackState(Player.STATE_READY) .play() .build(); @@ -1477,11 +1718,11 @@ public final class ExoPlayerTest { } }; ExoPlayerTestRunner testRunner = - new ExoPlayerTestRunner.Builder() - .setMediaSource(mediaSource) + new ExoPlayerTestRunner.Builder(context) + .setMediaSources(mediaSource) .setActionSchedule(actionSchedule) .setAnalyticsListener(listener) - .build(context); + .build(); try { testRunner.start().blockUntilActionScheduleFinished(TIMEOUT_MS).blockUntilEnded(TIMEOUT_MS); fail(); @@ -1492,93 +1733,99 @@ public final class ExoPlayerTest { } @Test - public void testPlaybackErrorTwiceStillKeepsTimeline() throws Exception { + public void playbackErrorTwiceStillKeepsTimeline() throws Exception { final Timeline timeline = new FakeTimeline(/* windowCount= */ 1); - final FakeMediaSource mediaSource2 = new FakeMediaSource(/* timeline= */ null); + final FakeMediaSource mediaSource2 = new FakeMediaSource(timeline); ActionSchedule actionSchedule = - new ActionSchedule.Builder("testPlaybackErrorDoesNotResetPosition") + new ActionSchedule.Builder(TAG) .pause() .waitForPlaybackState(Player.STATE_READY) .throwPlaybackException(ExoPlaybackException.createForSource(new IOException())) .waitForPlaybackState(Player.STATE_IDLE) - .prepareSource(mediaSource2, /* resetPosition= */ false, /* resetState= */ false) + .setMediaSources(/* resetPosition= */ false, mediaSource2) + .prepare() .waitForPlaybackState(Player.STATE_BUFFERING) .throwPlaybackException(ExoPlaybackException.createForSource(new IOException())) + .waitForTimelineChanged(timeline, Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE) .waitForPlaybackState(Player.STATE_IDLE) .build(); ExoPlayerTestRunner testRunner = - new ExoPlayerTestRunner.Builder() + new ExoPlayerTestRunner.Builder(context) .setTimeline(timeline) .setActionSchedule(actionSchedule) - .build(context); + .build(); try { testRunner.start().blockUntilActionScheduleFinished(TIMEOUT_MS).blockUntilEnded(TIMEOUT_MS); fail(); } catch (ExoPlaybackException e) { // Expected exception. } - testRunner.assertTimelinesEqual(timeline, timeline); + testRunner.assertTimelinesSame(dummyTimeline, timeline, dummyTimeline, timeline); testRunner.assertTimelineChangeReasonsEqual( - Player.TIMELINE_CHANGE_REASON_PREPARED, Player.TIMELINE_CHANGE_REASON_PREPARED); + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, + Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE, + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, + Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE); } @Test - public void testSendMessagesDuringPreparation() throws Exception { + public void sendMessagesDuringPreparation() throws Exception { Timeline timeline = new FakeTimeline(/* windowCount= */ 1); PositionGrabbingMessageTarget target = new PositionGrabbingMessageTarget(); ActionSchedule actionSchedule = - new ActionSchedule.Builder("testSendMessages") + new ActionSchedule.Builder(TAG) .pause() .waitForPlaybackState(Player.STATE_BUFFERING) .sendMessage(target, /* positionMs= */ 50) .play() .build(); - new Builder() + new ExoPlayerTestRunner.Builder(context) .setTimeline(timeline) .setActionSchedule(actionSchedule) - .build(context) + .build() .start() .blockUntilEnded(TIMEOUT_MS); assertThat(target.positionMs).isAtLeast(50L); } @Test - public void testSendMessagesAfterPreparation() throws Exception { + public void sendMessagesAfterPreparation() throws Exception { Timeline timeline = new FakeTimeline(/* windowCount= */ 1); PositionGrabbingMessageTarget target = new PositionGrabbingMessageTarget(); ActionSchedule actionSchedule = - new ActionSchedule.Builder("testSendMessages") + new ActionSchedule.Builder(TAG) .pause() - .waitForTimelineChanged(timeline) + .waitForTimelineChanged( + timeline, /* expectedReason */ Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE) .sendMessage(target, /* positionMs= */ 50) .play() .build(); - new Builder() + new ExoPlayerTestRunner.Builder(context) .setTimeline(timeline) .setActionSchedule(actionSchedule) - .build(context) + .build() .start() .blockUntilEnded(TIMEOUT_MS); assertThat(target.positionMs).isAtLeast(50L); } @Test - public void testMultipleSendMessages() throws Exception { + public void multipleSendMessages() throws Exception { Timeline timeline = new FakeTimeline(/* windowCount= */ 1); PositionGrabbingMessageTarget target50 = new PositionGrabbingMessageTarget(); PositionGrabbingMessageTarget target80 = new PositionGrabbingMessageTarget(); ActionSchedule actionSchedule = - new ActionSchedule.Builder("testSendMessages") + new ActionSchedule.Builder(TAG) .pause() .waitForPlaybackState(Player.STATE_BUFFERING) .sendMessage(target80, /* positionMs= */ 80) .sendMessage(target50, /* positionMs= */ 50) .play() .build(); - new Builder() + new ExoPlayerTestRunner.Builder(context) .setTimeline(timeline) .setActionSchedule(actionSchedule) - .build(context) + .build() .start() .blockUntilEnded(TIMEOUT_MS); assertThat(target50.positionMs).isAtLeast(50L); @@ -1587,11 +1834,11 @@ public final class ExoPlayerTest { } @Test - public void testSendMessagesFromStartPositionOnlyOnce() throws Exception { + public void sendMessagesFromStartPositionOnlyOnce() throws Exception { Timeline timeline = new FakeTimeline(/* windowCount= */ 1); AtomicInteger counter = new AtomicInteger(); ActionSchedule actionSchedule = - new ActionSchedule.Builder("testSendMessagesFromStartPositionOnlyOnce") + new ActionSchedule.Builder(TAG) .waitForTimelineChanged() .pause() .sendMessage( @@ -1605,10 +1852,10 @@ public final class ExoPlayerTest { .delay(/* delayMs= */ 2000) .play() .build(); - new Builder() + new ExoPlayerTestRunner.Builder(context) .setTimeline(timeline) .setActionSchedule(actionSchedule) - .build(context) + .build() .start() .blockUntilActionScheduleFinished(TIMEOUT_MS) .blockUntilEnded(TIMEOUT_MS); @@ -1616,22 +1863,22 @@ public final class ExoPlayerTest { } @Test - public void testMultipleSendMessagesAtSameTime() throws Exception { + public void multipleSendMessagesAtSameTime() throws Exception { Timeline timeline = new FakeTimeline(/* windowCount= */ 1); PositionGrabbingMessageTarget target1 = new PositionGrabbingMessageTarget(); PositionGrabbingMessageTarget target2 = new PositionGrabbingMessageTarget(); ActionSchedule actionSchedule = - new ActionSchedule.Builder("testSendMessages") + new ActionSchedule.Builder(TAG) .pause() .waitForPlaybackState(Player.STATE_BUFFERING) .sendMessage(target1, /* positionMs= */ 50) .sendMessage(target2, /* positionMs= */ 50) .play() .build(); - new Builder() + new ExoPlayerTestRunner.Builder(context) .setTimeline(timeline) .setActionSchedule(actionSchedule) - .build(context) + .build() .start() .blockUntilEnded(TIMEOUT_MS); assertThat(target1.positionMs).isAtLeast(50L); @@ -1639,156 +1886,176 @@ public final class ExoPlayerTest { } @Test - public void testSendMessagesMultiPeriodResolution() throws Exception { + public void sendMessagesMultiPeriodResolution() throws Exception { Timeline timeline = new FakeTimeline(new TimelineWindowDefinition(/* periodCount= */ 10, /* id= */ 0)); PositionGrabbingMessageTarget target = new PositionGrabbingMessageTarget(); ActionSchedule actionSchedule = - new ActionSchedule.Builder("testSendMessages") + new ActionSchedule.Builder(TAG) .pause() .waitForPlaybackState(Player.STATE_BUFFERING) .sendMessage(target, /* positionMs= */ 50) .play() .build(); - new Builder() + new ExoPlayerTestRunner.Builder(context) .setTimeline(timeline) .setActionSchedule(actionSchedule) - .build(context) + .build() .start() .blockUntilEnded(TIMEOUT_MS); assertThat(target.positionMs).isAtLeast(50L); } @Test - public void testSendMessagesAtStartAndEndOfPeriod() throws Exception { + public void sendMessagesAtStartAndEndOfPeriod() throws Exception { Timeline timeline = new FakeTimeline(/* windowCount= */ 2); PositionGrabbingMessageTarget targetStartFirstPeriod = new PositionGrabbingMessageTarget(); - PositionGrabbingMessageTarget targetEndMiddlePeriod = new PositionGrabbingMessageTarget(); + PositionGrabbingMessageTarget targetEndMiddlePeriodResolved = + new PositionGrabbingMessageTarget(); + PositionGrabbingMessageTarget targetEndMiddlePeriodUnresolved = + new PositionGrabbingMessageTarget(); PositionGrabbingMessageTarget targetStartMiddlePeriod = new PositionGrabbingMessageTarget(); - PositionGrabbingMessageTarget targetEndLastPeriod = new PositionGrabbingMessageTarget(); + PositionGrabbingMessageTarget targetEndLastPeriodResolved = new PositionGrabbingMessageTarget(); + PositionGrabbingMessageTarget targetEndLastPeriodUnresolved = + new PositionGrabbingMessageTarget(); long duration1Ms = timeline.getWindow(0, new Window()).getDurationMs(); long duration2Ms = timeline.getWindow(1, new Window()).getDurationMs(); ActionSchedule actionSchedule = - new ActionSchedule.Builder("testSendMessages") - .pause() - .waitForPlaybackState(Player.STATE_BUFFERING) + new ActionSchedule.Builder(TAG) .sendMessage(targetStartFirstPeriod, /* windowIndex= */ 0, /* positionMs= */ 0) - .sendMessage(targetEndMiddlePeriod, /* windowIndex= */ 0, /* positionMs= */ duration1Ms) + .sendMessage( + targetEndMiddlePeriodResolved, + /* windowIndex= */ 0, + /* positionMs= */ duration1Ms - 1) + .sendMessage( + targetEndMiddlePeriodUnresolved, + /* windowIndex= */ 0, + /* positionMs= */ C.TIME_END_OF_SOURCE) .sendMessage(targetStartMiddlePeriod, /* windowIndex= */ 1, /* positionMs= */ 0) - .sendMessage(targetEndLastPeriod, /* windowIndex= */ 1, /* positionMs= */ duration2Ms) - .play() - // Add additional prepare at end and wait until it's processed to ensure that - // messages sent at end of playback are received before test ends. - .waitForPlaybackState(Player.STATE_ENDED) - .prepareSource( - new FakeMediaSource(timeline), /* resetPosition= */ false, /* resetState= */ true) - .waitForPlaybackState(Player.STATE_BUFFERING) - .waitForPlaybackState(Player.STATE_ENDED) + .sendMessage( + targetEndLastPeriodResolved, + /* windowIndex= */ 1, + /* positionMs= */ duration2Ms - 1) + .sendMessage( + targetEndLastPeriodUnresolved, + /* windowIndex= */ 1, + /* positionMs= */ C.TIME_END_OF_SOURCE) + .waitForMessage(targetEndLastPeriodUnresolved) .build(); - new Builder() + new ExoPlayerTestRunner.Builder(context) .setTimeline(timeline) .setActionSchedule(actionSchedule) - .build(context) + .build() .start() .blockUntilActionScheduleFinished(TIMEOUT_MS) .blockUntilEnded(TIMEOUT_MS); assertThat(targetStartFirstPeriod.windowIndex).isEqualTo(0); assertThat(targetStartFirstPeriod.positionMs).isAtLeast(0L); - assertThat(targetEndMiddlePeriod.windowIndex).isEqualTo(0); - assertThat(targetEndMiddlePeriod.positionMs).isAtLeast(duration1Ms); + assertThat(targetEndMiddlePeriodResolved.windowIndex).isEqualTo(0); + assertThat(targetEndMiddlePeriodResolved.positionMs).isAtLeast(duration1Ms - 1); + assertThat(targetEndMiddlePeriodUnresolved.windowIndex).isEqualTo(0); + assertThat(targetEndMiddlePeriodUnresolved.positionMs).isAtLeast(duration1Ms - 1); + assertThat(targetEndMiddlePeriodResolved.positionMs) + .isEqualTo(targetEndMiddlePeriodUnresolved.positionMs); assertThat(targetStartMiddlePeriod.windowIndex).isEqualTo(1); assertThat(targetStartMiddlePeriod.positionMs).isAtLeast(0L); - assertThat(targetEndLastPeriod.windowIndex).isEqualTo(1); - assertThat(targetEndLastPeriod.positionMs).isAtLeast(duration2Ms); + assertThat(targetEndLastPeriodResolved.windowIndex).isEqualTo(1); + assertThat(targetEndLastPeriodResolved.positionMs).isAtLeast(duration2Ms - 1); + assertThat(targetEndLastPeriodUnresolved.windowIndex).isEqualTo(1); + assertThat(targetEndLastPeriodUnresolved.positionMs).isAtLeast(duration2Ms - 1); + assertThat(targetEndLastPeriodResolved.positionMs) + .isEqualTo(targetEndLastPeriodUnresolved.positionMs); } @Test - public void testSendMessagesSeekOnDeliveryTimeDuringPreparation() throws Exception { + public void sendMessagesSeekOnDeliveryTimeDuringPreparation() throws Exception { Timeline timeline = new FakeTimeline(/* windowCount= */ 1); PositionGrabbingMessageTarget target = new PositionGrabbingMessageTarget(); ActionSchedule actionSchedule = - new ActionSchedule.Builder("testSendMessages") + new ActionSchedule.Builder(TAG) .waitForPlaybackState(Player.STATE_BUFFERING) .sendMessage(target, /* positionMs= */ 50) .seek(/* positionMs= */ 50) .build(); - new Builder() + new ExoPlayerTestRunner.Builder(context) .setTimeline(timeline) .setActionSchedule(actionSchedule) - .build(context) + .build() .start() .blockUntilEnded(TIMEOUT_MS); assertThat(target.positionMs).isAtLeast(50L); } @Test - public void testSendMessagesSeekOnDeliveryTimeAfterPreparation() throws Exception { + public void sendMessagesSeekOnDeliveryTimeAfterPreparation() throws Exception { Timeline timeline = new FakeTimeline(/* windowCount= */ 1); PositionGrabbingMessageTarget target = new PositionGrabbingMessageTarget(); ActionSchedule actionSchedule = - new ActionSchedule.Builder("testSendMessages") + new ActionSchedule.Builder(TAG) .waitForPlaybackState(Player.STATE_BUFFERING) .sendMessage(target, /* positionMs= */ 50) - .waitForTimelineChanged(timeline) + .waitForTimelineChanged( + timeline, /* expectedReason */ Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE) .seek(/* positionMs= */ 50) .build(); - new Builder() + new ExoPlayerTestRunner.Builder(context) .setTimeline(timeline) .setActionSchedule(actionSchedule) - .build(context) + .build() .start() .blockUntilEnded(TIMEOUT_MS); assertThat(target.positionMs).isAtLeast(50L); } @Test - public void testSendMessagesSeekAfterDeliveryTimeDuringPreparation() throws Exception { + public void sendMessagesSeekAfterDeliveryTimeDuringPreparation() throws Exception { Timeline timeline = new FakeTimeline(/* windowCount= */ 1); PositionGrabbingMessageTarget target = new PositionGrabbingMessageTarget(); ActionSchedule actionSchedule = - new ActionSchedule.Builder("testSendMessages") + new ActionSchedule.Builder(TAG) .pause() .waitForPlaybackState(Player.STATE_BUFFERING) .sendMessage(target, /* positionMs= */ 50) .seek(/* positionMs= */ 51) .play() .build(); - new Builder() + new ExoPlayerTestRunner.Builder(context) .setTimeline(timeline) .setActionSchedule(actionSchedule) - .build(context) + .build() .start() .blockUntilEnded(TIMEOUT_MS); assertThat(target.positionMs).isEqualTo(C.POSITION_UNSET); } @Test - public void testSendMessagesSeekAfterDeliveryTimeAfterPreparation() throws Exception { + public void sendMessagesSeekAfterDeliveryTimeAfterPreparation() throws Exception { Timeline timeline = new FakeTimeline(/* windowCount= */ 1); PositionGrabbingMessageTarget target = new PositionGrabbingMessageTarget(); ActionSchedule actionSchedule = - new ActionSchedule.Builder("testSendMessages") + new ActionSchedule.Builder(TAG) .pause() .sendMessage(target, /* positionMs= */ 50) - .waitForTimelineChanged(timeline) + .waitForTimelineChanged( + timeline, /* expectedReason */ Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE) .seek(/* positionMs= */ 51) .play() .build(); - new Builder() + new ExoPlayerTestRunner.Builder(context) .setTimeline(timeline) .setActionSchedule(actionSchedule) - .build(context) + .build() .start() .blockUntilEnded(TIMEOUT_MS); assertThat(target.positionMs).isEqualTo(C.POSITION_UNSET); } @Test - public void testSendMessagesRepeatDoesNotRepost() throws Exception { + public void sendMessagesRepeatDoesNotRepost() throws Exception { Timeline timeline = new FakeTimeline(/* windowCount= */ 1); PositionGrabbingMessageTarget target = new PositionGrabbingMessageTarget(); ActionSchedule actionSchedule = - new ActionSchedule.Builder("testSendMessages") + new ActionSchedule.Builder(TAG) .pause() .waitForPlaybackState(Player.STATE_BUFFERING) .sendMessage(target, /* positionMs= */ 50) @@ -1797,10 +2064,10 @@ public final class ExoPlayerTest { .waitForPositionDiscontinuity() .setRepeatMode(Player.REPEAT_MODE_OFF) .build(); - new Builder() + new ExoPlayerTestRunner.Builder(context) .setTimeline(timeline) .setActionSchedule(actionSchedule) - .build(context) + .build() .start() .blockUntilEnded(TIMEOUT_MS); assertThat(target.messageCount).isEqualTo(1); @@ -1808,11 +2075,11 @@ public final class ExoPlayerTest { } @Test - public void testSendMessagesRepeatWithoutDeletingDoesRepost() throws Exception { + public void sendMessagesRepeatWithoutDeletingDoesRepost() throws Exception { Timeline timeline = new FakeTimeline(/* windowCount= */ 1); PositionGrabbingMessageTarget target = new PositionGrabbingMessageTarget(); ActionSchedule actionSchedule = - new ActionSchedule.Builder("testSendMessages") + new ActionSchedule.Builder(TAG) .pause() .waitForPlaybackState(Player.STATE_BUFFERING) .sendMessage( @@ -1826,10 +2093,10 @@ public final class ExoPlayerTest { .setRepeatMode(Player.REPEAT_MODE_OFF) .play() .build(); - new Builder() + new ExoPlayerTestRunner.Builder(context) .setTimeline(timeline) .setActionSchedule(actionSchedule) - .build(context) + .build() .start() .blockUntilEnded(TIMEOUT_MS); assertThat(target.messageCount).isEqualTo(2); @@ -1837,28 +2104,31 @@ public final class ExoPlayerTest { } @Test - public void testSendMessagesMoveCurrentWindowIndex() throws Exception { + public void sendMessagesMoveCurrentWindowIndex() throws Exception { Timeline timeline = new FakeTimeline(new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ 0)); final Timeline secondTimeline = new FakeTimeline( new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ 1), new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ 0)); - final FakeMediaSource mediaSource = new FakeMediaSource(timeline, Builder.VIDEO_FORMAT); + final FakeMediaSource mediaSource = + new FakeMediaSource(timeline, ExoPlayerTestRunner.VIDEO_FORMAT); PositionGrabbingMessageTarget target = new PositionGrabbingMessageTarget(); ActionSchedule actionSchedule = - new ActionSchedule.Builder("testSendMessages") + new ActionSchedule.Builder(TAG) .pause() - .waitForTimelineChanged(timeline) + .waitForTimelineChanged( + timeline, /* expectedReason */ Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE) .sendMessage(target, /* positionMs= */ 50) - .executeRunnable(() -> mediaSource.setNewSourceInfo(secondTimeline, null)) - .waitForTimelineChanged(secondTimeline) + .executeRunnable(() -> mediaSource.setNewSourceInfo(secondTimeline)) + .waitForTimelineChanged( + secondTimeline, /* expectedReason */ Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE) .play() .build(); - new Builder() - .setMediaSource(mediaSource) + new ExoPlayerTestRunner.Builder(context) + .setMediaSources(mediaSource) .setActionSchedule(actionSchedule) - .build(context) + .build() .start() .blockUntilEnded(TIMEOUT_MS); assertThat(target.positionMs).isAtLeast(50L); @@ -1866,20 +2136,20 @@ public final class ExoPlayerTest { } @Test - public void testSendMessagesMultiWindowDuringPreparation() throws Exception { + public void sendMessagesMultiWindowDuringPreparation() throws Exception { Timeline timeline = new FakeTimeline(/* windowCount= */ 3); PositionGrabbingMessageTarget target = new PositionGrabbingMessageTarget(); ActionSchedule actionSchedule = - new ActionSchedule.Builder("testSendMessages") + new ActionSchedule.Builder(TAG) .pause() - .waitForPlaybackState(Player.STATE_BUFFERING) + .waitForTimelineChanged(timeline, Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE) .sendMessage(target, /* windowIndex = */ 2, /* positionMs= */ 50) .play() .build(); - new Builder() + new ExoPlayerTestRunner.Builder(context) .setTimeline(timeline) .setActionSchedule(actionSchedule) - .build(context) + .build() .start() .blockUntilEnded(TIMEOUT_MS); assertThat(target.windowIndex).isEqualTo(2); @@ -1887,20 +2157,21 @@ public final class ExoPlayerTest { } @Test - public void testSendMessagesMultiWindowAfterPreparation() throws Exception { + public void sendMessagesMultiWindowAfterPreparation() throws Exception { Timeline timeline = new FakeTimeline(/* windowCount= */ 3); PositionGrabbingMessageTarget target = new PositionGrabbingMessageTarget(); ActionSchedule actionSchedule = - new ActionSchedule.Builder("testSendMessages") + new ActionSchedule.Builder(TAG) .pause() - .waitForTimelineChanged(timeline) + .waitForTimelineChanged( + timeline, /* expectedReason */ Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE) .sendMessage(target, /* windowIndex = */ 2, /* positionMs= */ 50) .play() .build(); - new Builder() + new ExoPlayerTestRunner.Builder(context) .setTimeline(timeline) .setActionSchedule(actionSchedule) - .build(context) + .build() .start() .blockUntilEnded(TIMEOUT_MS); assertThat(target.windowIndex).isEqualTo(2); @@ -1908,7 +2179,7 @@ public final class ExoPlayerTest { } @Test - public void testSendMessagesMoveWindowIndex() throws Exception { + public void sendMessagesMoveWindowIndex() throws Exception { Timeline timeline = new FakeTimeline( new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ 0), @@ -1917,22 +2188,25 @@ public final class ExoPlayerTest { new FakeTimeline( new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ 1), new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ 0)); - final FakeMediaSource mediaSource = new FakeMediaSource(timeline, Builder.VIDEO_FORMAT); + final FakeMediaSource mediaSource = + new FakeMediaSource(timeline, ExoPlayerTestRunner.VIDEO_FORMAT); PositionGrabbingMessageTarget target = new PositionGrabbingMessageTarget(); ActionSchedule actionSchedule = - new ActionSchedule.Builder("testSendMessages") + new ActionSchedule.Builder(TAG) .pause() - .waitForTimelineChanged(timeline) + .waitForTimelineChanged( + timeline, /* expectedReason */ Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE) .sendMessage(target, /* windowIndex = */ 1, /* positionMs= */ 50) - .executeRunnable(() -> mediaSource.setNewSourceInfo(secondTimeline, null)) - .waitForTimelineChanged(secondTimeline) + .executeRunnable(() -> mediaSource.setNewSourceInfo(secondTimeline)) + .waitForTimelineChanged( + secondTimeline, /* expectedReason */ Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE) .seek(/* windowIndex= */ 0, /* positionMs= */ 0) .play() .build(); - new Builder() - .setMediaSource(mediaSource) + new ExoPlayerTestRunner.Builder(context) + .setMediaSources(mediaSource) .setActionSchedule(actionSchedule) - .build(context) + .build() .start() .blockUntilEnded(TIMEOUT_MS); assertThat(target.positionMs).isAtLeast(50L); @@ -1940,12 +2214,12 @@ public final class ExoPlayerTest { } @Test - public void testSendMessagesNonLinearPeriodOrder() throws Exception { + public void sendMessagesNonLinearPeriodOrder() throws Exception { Timeline fakeTimeline = new FakeTimeline(/* windowCount= */ 1); MediaSource[] fakeMediaSources = { - new FakeMediaSource(fakeTimeline, Builder.VIDEO_FORMAT), - new FakeMediaSource(fakeTimeline, Builder.VIDEO_FORMAT), - new FakeMediaSource(fakeTimeline, Builder.VIDEO_FORMAT) + new FakeMediaSource(fakeTimeline, ExoPlayerTestRunner.VIDEO_FORMAT), + new FakeMediaSource(fakeTimeline, ExoPlayerTestRunner.VIDEO_FORMAT), + new FakeMediaSource(fakeTimeline, ExoPlayerTestRunner.VIDEO_FORMAT) }; ConcatenatingMediaSource mediaSource = new ConcatenatingMediaSource(false, new FakeShuffleOrder(3), fakeMediaSources); @@ -1953,7 +2227,7 @@ public final class ExoPlayerTest { PositionGrabbingMessageTarget target2 = new PositionGrabbingMessageTarget(); PositionGrabbingMessageTarget target3 = new PositionGrabbingMessageTarget(); ActionSchedule actionSchedule = - new ActionSchedule.Builder("testSendMessages") + new ActionSchedule.Builder(TAG) .pause() .waitForPlaybackState(Player.STATE_READY) .sendMessage(target1, /* windowIndex = */ 0, /* positionMs= */ 50) @@ -1963,10 +2237,10 @@ public final class ExoPlayerTest { .seek(/* windowIndex= */ 2, /* positionMs= */ 0) .play() .build(); - new ExoPlayerTestRunner.Builder() - .setMediaSource(mediaSource) + new ExoPlayerTestRunner.Builder(context) + .setMediaSources(mediaSource) .setActionSchedule(actionSchedule) - .build(context) + .build() .start() .blockUntilEnded(TIMEOUT_MS); assertThat(target1.windowIndex).isEqualTo(0); @@ -1975,12 +2249,12 @@ public final class ExoPlayerTest { } @Test - public void testCancelMessageBeforeDelivery() throws Exception { + public void cancelMessageBeforeDelivery() throws Exception { Timeline timeline = new FakeTimeline(/* windowCount= */ 1); final PositionGrabbingMessageTarget target = new PositionGrabbingMessageTarget(); final AtomicReference message = new AtomicReference<>(); ActionSchedule actionSchedule = - new ActionSchedule.Builder("testCancelMessage") + new ActionSchedule.Builder(TAG) .pause() .waitForPlaybackState(Player.STATE_BUFFERING) .executeRunnable( @@ -1996,10 +2270,10 @@ public final class ExoPlayerTest { .executeRunnable(() -> message.get().cancel()) .play() .build(); - new Builder() + new ExoPlayerTestRunner.Builder(context) .setTimeline(timeline) .setActionSchedule(actionSchedule) - .build(context) + .build() .start() .blockUntilEnded(TIMEOUT_MS); assertThat(message.get().isCanceled()).isTrue(); @@ -2007,12 +2281,12 @@ public final class ExoPlayerTest { } @Test - public void testCancelRepeatedMessageAfterDelivery() throws Exception { + public void cancelRepeatedMessageAfterDelivery() throws Exception { Timeline timeline = new FakeTimeline(/* windowCount= */ 1); - final PositionGrabbingMessageTarget target = new PositionGrabbingMessageTarget(); + final CountingMessageTarget target = new CountingMessageTarget(); final AtomicReference message = new AtomicReference<>(); ActionSchedule actionSchedule = - new ActionSchedule.Builder("testCancelMessage") + new ActionSchedule.Builder(TAG) .pause() .waitForPlaybackState(Player.STATE_BUFFERING) .executeRunnable( @@ -2034,10 +2308,10 @@ public final class ExoPlayerTest { .executeRunnable(() -> message.get().cancel()) .play() .build(); - new Builder() + new ExoPlayerTestRunner.Builder(context) .setTimeline(timeline) .setActionSchedule(actionSchedule) - .build(context) + .build() .start() .blockUntilEnded(TIMEOUT_MS); assertThat(message.get().isCanceled()).isTrue(); @@ -2045,22 +2319,58 @@ public final class ExoPlayerTest { } @Test - public void testSetAndSwitchSurface() throws Exception { + public void sendMessages_withMediaRemoval_triggersCorrectMessagesAndDoesNotThrow() + throws Exception { + ExoPlayer player = new TestExoPlayer.Builder(context).build(); + MediaSource mediaSource = new FakeMediaSource(new FakeTimeline(/* windowCount= */ 1)); + player.addMediaSources(Arrays.asList(mediaSource, mediaSource)); + player + .createMessage((messageType, payload) -> {}) + .setPosition(/* windowIndex= */ 0, /* positionMs= */ 0) + .setDeleteAfterDelivery(false) + .send(); + PlayerMessage.Target secondMediaItemTarget = mock(PlayerMessage.Target.class); + player + .createMessage(secondMediaItemTarget) + .setPosition(/* windowIndex= */ 1, /* positionMs= */ 0) + .setDeleteAfterDelivery(false) + .send(); + + // Play through media once to trigger all messages. This ensures any internally saved message + // indices are non-zero. + player.prepare(); + player.play(); + TestExoPlayer.runUntilPlaybackState(player, Player.STATE_ENDED); + verify(secondMediaItemTarget).handleMessage(anyInt(), any()); + + // Remove first item and play second item again to check if message is triggered again. + // After removal, any internally saved message indices are invalid and will throw + // IndexOutOfBoundsException if used without updating. + // See https://github.com/google/ExoPlayer/issues/7278. + player.removeMediaItem(/* index= */ 0); + player.seekTo(/* positionMs= */ 0); + TestExoPlayer.runUntilPlaybackState(player, Player.STATE_ENDED); + + assertThat(player.getPlayerError()).isNull(); + verify(secondMediaItemTarget, times(2)).handleMessage(anyInt(), any()); + } + + @Test + public void setAndSwitchSurface() throws Exception { final List rendererMessages = new ArrayList<>(); Renderer videoRenderer = - new FakeRenderer(Builder.VIDEO_FORMAT) { + new FakeRenderer(C.TRACK_TYPE_VIDEO) { @Override public void handleMessage(int what, @Nullable Object object) throws ExoPlaybackException { super.handleMessage(what, object); rendererMessages.add(what); } }; - ActionSchedule actionSchedule = - addSurfaceSwitch(new ActionSchedule.Builder("testSetAndSwitchSurface")).build(); - new ExoPlayerTestRunner.Builder() + ActionSchedule actionSchedule = addSurfaceSwitch(new ActionSchedule.Builder(TAG)).build(); + new ExoPlayerTestRunner.Builder(context) .setRenderers(videoRenderer) .setActionSchedule(actionSchedule) - .build(context) + .build() .start() .blockUntilActionScheduleFinished(TIMEOUT_MS) .blockUntilEnded(TIMEOUT_MS); @@ -2068,22 +2378,21 @@ public final class ExoPlayerTest { } @Test - public void testSwitchSurfaceOnEndedState() throws Exception { + public void switchSurfaceOnEndedState() throws Exception { ActionSchedule.Builder scheduleBuilder = - new ActionSchedule.Builder("testSwitchSurfaceOnEndedState") - .waitForPlaybackState(Player.STATE_ENDED); + new ActionSchedule.Builder(TAG).waitForPlaybackState(Player.STATE_ENDED); ActionSchedule waitForEndedAndSwitchSchedule = addSurfaceSwitch(scheduleBuilder).build(); - new ExoPlayerTestRunner.Builder() + new ExoPlayerTestRunner.Builder(context) .setTimeline(Timeline.EMPTY) .setActionSchedule(waitForEndedAndSwitchSchedule) - .build(context) + .build() .start() .blockUntilActionScheduleFinished(TIMEOUT_MS) .blockUntilEnded(TIMEOUT_MS); } @Test - public void testTimelineUpdateDropsPrebufferedPeriods() throws Exception { + public void timelineUpdateDropsPrebufferedPeriods() throws Exception { Timeline timeline1 = new FakeTimeline( new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ 1), @@ -2092,26 +2401,32 @@ public final class ExoPlayerTest { new FakeTimeline( new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ 1), new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ 3)); - final FakeMediaSource mediaSource = new FakeMediaSource(timeline1, Builder.VIDEO_FORMAT); + final FakeMediaSource mediaSource = + new FakeMediaSource(timeline1, ExoPlayerTestRunner.VIDEO_FORMAT); ActionSchedule actionSchedule = - new ActionSchedule.Builder("testTimelineUpdateDropsPeriods") + new ActionSchedule.Builder(TAG) .pause() .waitForPlaybackState(Player.STATE_READY) // Ensure next period is pre-buffered by playing until end of first period. .playUntilPosition( /* windowIndex= */ 0, /* positionMs= */ C.usToMs(TimelineWindowDefinition.DEFAULT_WINDOW_DURATION_US)) - .executeRunnable(() -> mediaSource.setNewSourceInfo(timeline2, /* newManifest= */ null)) - .waitForTimelineChanged(timeline2) + .executeRunnable(() -> mediaSource.setNewSourceInfo(timeline2)) + .waitForTimelineChanged( + timeline2, /* expectedReason */ Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE) .play() .build(); ExoPlayerTestRunner testRunner = - new ExoPlayerTestRunner.Builder() - .setMediaSource(mediaSource) + new ExoPlayerTestRunner.Builder(context) + .setMediaSources(mediaSource) .setActionSchedule(actionSchedule) - .build(context) + .build() .start() .blockUntilEnded(TIMEOUT_MS); + testRunner.assertTimelineChangeReasonsEqual( + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, + Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE, + Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE); testRunner.assertPlayedPeriodIndices(0, 1); // Assert that the second period was re-created from the new timeline. assertThat(mediaSource.getCreatedMediaPeriods()).hasSize(3); @@ -2128,7 +2443,57 @@ public final class ExoPlayerTest { } @Test - public void testRepeatedSeeksToUnpreparedPeriodInSameWindowKeepsWindowSequenceNumber() + public void timelineUpdateWithNewMidrollAdCuePoint_dropsPrebufferedPeriod() throws Exception { + Timeline timeline1 = + new FakeTimeline(new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ 0)); + AdPlaybackState adPlaybackStateWithMidroll = + FakeTimeline.createAdPlaybackState( + /* adsPerAdGroup= */ 1, + /* adGroupTimesUs...= */ TimelineWindowDefinition + .DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US + + 5 * C.MICROS_PER_SECOND); + Timeline timeline2 = + new FakeTimeline( + new TimelineWindowDefinition( + /* periodCount= */ 1, + /* id= */ 0, + /* isSeekable= */ true, + /* isDynamic= */ false, + /* durationUs= */ 10_000_000, + adPlaybackStateWithMidroll)); + FakeMediaSource mediaSource = new FakeMediaSource(timeline1, ExoPlayerTestRunner.VIDEO_FORMAT); + ActionSchedule actionSchedule = + new ActionSchedule.Builder(TAG) + .pause() + .waitForPlaybackState(Player.STATE_READY) + .executeRunnable(() -> mediaSource.setNewSourceInfo(timeline2)) + .waitForTimelineChanged( + timeline2, /* expectedReason= */ Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE) + .play() + .build(); + ExoPlayerTestRunner testRunner = + new ExoPlayerTestRunner.Builder(context) + .setMediaSources(mediaSource) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilEnded(TIMEOUT_MS); + + testRunner.assertTimelineChangeReasonsEqual( + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, + Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE, + Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE); + testRunner.assertPlayedPeriodIndices(0); + assertThat(mediaSource.getCreatedMediaPeriods()).hasSize(4); + assertThat(mediaSource.getCreatedMediaPeriods().get(0).nextAdGroupIndex) + .isEqualTo(C.INDEX_UNSET); + assertThat(mediaSource.getCreatedMediaPeriods().get(1).nextAdGroupIndex).isEqualTo(0); + assertThat(mediaSource.getCreatedMediaPeriods().get(2).adGroupIndex).isEqualTo(0); + assertThat(mediaSource.getCreatedMediaPeriods().get(3).adGroupIndex).isEqualTo(C.INDEX_UNSET); + } + + @Test + public void repeatedSeeksToUnpreparedPeriodInSameWindowKeepsWindowSequenceNumber() throws Exception { Timeline timeline = new FakeTimeline( @@ -2140,22 +2505,23 @@ public final class ExoPlayerTest { /* durationUs= */ 10 * C.MICROS_PER_SECOND)); FakeMediaSource mediaSource = new FakeMediaSource(timeline); ActionSchedule actionSchedule = - new ActionSchedule.Builder("testSeekToUnpreparedPeriod") + new ActionSchedule.Builder(TAG) .pause() .waitForPlaybackState(Player.STATE_READY) .seek(/* windowIndex= */ 0, /* positionMs= */ 9999) - .waitForSeekProcessed() + // Wait after each seek until the internal player has updated its state. + .waitForPendingPlayerCommands() .seek(/* windowIndex= */ 0, /* positionMs= */ 1) - .waitForSeekProcessed() + .waitForPendingPlayerCommands() .seek(/* windowIndex= */ 0, /* positionMs= */ 9999) - .waitForSeekProcessed() + .waitForPendingPlayerCommands() .play() .build(); ExoPlayerTestRunner testRunner = - new ExoPlayerTestRunner.Builder() - .setMediaSource(mediaSource) + new ExoPlayerTestRunner.Builder(context) + .setMediaSources(mediaSource) .setActionSchedule(actionSchedule) - .build(context) + .build() .start() .blockUntilEnded(TIMEOUT_MS); @@ -2173,7 +2539,7 @@ public final class ExoPlayerTest { } @Test - public void testInvalidSeekFallsBackToSubsequentPeriodOfTheRemovedPeriod() throws Exception { + public void invalidSeekFallsBackToSubsequentPeriodOfTheRemovedPeriod() throws Exception { Timeline timeline = new FakeTimeline(/* windowCount= */ 1); CountDownLatch sourceReleasedCountDownLatch = new CountDownLatch(/* count= */ 1); MediaSource mediaSourceToRemove = @@ -2189,9 +2555,9 @@ public final class ExoPlayerTest { final int[] windowCount = {C.INDEX_UNSET}; final long[] position = {C.TIME_UNSET}; ActionSchedule actionSchedule = - new ActionSchedule.Builder("testInvalidSeekFallsBackToSubsequentPeriodOfTheRemovedPeriod") + new ActionSchedule.Builder(TAG) .pause() - .waitForTimelineChanged() + .waitForPlaybackState(Player.STATE_READY) .executeRunnable( new PlayerRunnable() { @Override @@ -2211,7 +2577,7 @@ public final class ExoPlayerTest { player.seekTo(/* windowIndex= */ 0, /* positionMs= */ 1000L); } }) - .waitForSeekProcessed() + .waitForPendingPlayerCommands() .executeRunnable( new PlayerRunnable() { @Override @@ -2222,10 +2588,10 @@ public final class ExoPlayerTest { }) .play() .build(); - new ExoPlayerTestRunner.Builder() - .setMediaSource(mediaSource) + new ExoPlayerTestRunner.Builder(context) + .setMediaSources(mediaSource) .setActionSchedule(actionSchedule) - .build(context) + .build() .start() .blockUntilActionScheduleFinished(TIMEOUT_MS) .blockUntilEnded(TIMEOUT_MS); @@ -2237,7 +2603,7 @@ public final class ExoPlayerTest { } @Test - public void testRecursivePlayerChangesReportConsistentValuesForAllListeners() throws Exception { + public void recursivePlayerChangesReportConsistentValuesForAllListeners() 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<>(); @@ -2246,7 +2612,7 @@ public final class ExoPlayerTest { final EventListener eventListener1 = new EventListener() { @Override - public void onPlayerStateChanged(boolean playWhenReady, @Player.State int playbackState) { + public void onPlaybackStateChanged(@Player.State int playbackState) { eventListener1States.add(playbackState); if (playbackState == Player.STATE_READY) { playerReference.get().stop(/* reset= */ true); @@ -2256,12 +2622,12 @@ public final class ExoPlayerTest { final EventListener eventListener2 = new EventListener() { @Override - public void onPlayerStateChanged(boolean playWhenReady, @Player.State int playbackState) { + public void onPlaybackStateChanged(@Player.State int playbackState) { eventListener2States.add(playbackState); } }; ActionSchedule actionSchedule = - new ActionSchedule.Builder("testRecursivePlayerChanges") + new ActionSchedule.Builder(TAG) .executeRunnable( new PlayerRunnable() { @Override @@ -2272,9 +2638,9 @@ public final class ExoPlayerTest { } }) .build(); - new ExoPlayerTestRunner.Builder() + new ExoPlayerTestRunner.Builder(context) .setActionSchedule(actionSchedule) - .build(context) + .build() .start() .blockUntilEnded(TIMEOUT_MS); @@ -2287,32 +2653,42 @@ public final class ExoPlayerTest { } @Test - public void testRecursivePlayerChangesAreReportedInCorrectOrder() throws Exception { + public void recursivePlayerChangesAreReportedInCorrectOrder() 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<>(); + List sequence = new ArrayList<>(); final EventListener eventListener = new EventListener() { + @Override - public void onTimelineChanged(Timeline timeline, int reason) { - if (timeline.isEmpty()) { - playerReference.get().setPlayWhenReady(/* playWhenReady= */ false); + public void onPlaybackStateChanged(@Player.State int playbackState) { + eventListenerStates.add(playbackState); + if (playbackState == Player.STATE_READY) { + playerReference.get().stop(/* reset= */ true); + sequence.add(0); } } @Override - public void onPlayerStateChanged(boolean playWhenReady, @Player.State int playbackState) { - eventListenerPlayWhenReady.add(playWhenReady); - eventListenerStates.add(playbackState); - if (playbackState == Player.STATE_READY) { - playerReference.get().stop(/* reset= */ true); + public void onTimelineChanged(Timeline timeline, int reason) { + if (timeline.isEmpty()) { + playerReference.get().pause(); + sequence.add(1); } } + + @Override + public void onPlayWhenReadyChanged( + boolean playWhenReady, @Player.PlayWhenReadyChangeReason int reason) { + eventListenerPlayWhenReady.add(playWhenReady); + sequence.add(2); + } }; ActionSchedule actionSchedule = - new ActionSchedule.Builder("testRecursivePlayerChanges") + new ActionSchedule.Builder(TAG) .executeRunnable( new PlayerRunnable() { @Override @@ -2322,21 +2698,75 @@ public final class ExoPlayerTest { } }) .build(); - new ExoPlayerTestRunner.Builder() + new ExoPlayerTestRunner.Builder(context) .setActionSchedule(actionSchedule) - .build(context) + .build() .start() .blockUntilEnded(TIMEOUT_MS); assertThat(eventListenerStates) - .containsExactly( - Player.STATE_BUFFERING, Player.STATE_READY, Player.STATE_IDLE, Player.STATE_IDLE) + .containsExactly(Player.STATE_BUFFERING, Player.STATE_READY, Player.STATE_IDLE) .inOrder(); - assertThat(eventListenerPlayWhenReady).containsExactly(true, true, true, false).inOrder(); + assertThat(eventListenerPlayWhenReady).containsExactly(false).inOrder(); + assertThat(sequence).containsExactly(0, 1, 2).inOrder(); } @Test - public void testClippedLoopedPeriodsArePlayedFully() throws Exception { + public void recursiveTimelineChangeInStopAreReportedInCorrectOrder() throws Exception { + Timeline firstTimeline = new FakeTimeline(/* windowCount= */ 2); + Timeline secondTimeline = new FakeTimeline(/* windowCount= */ 3); + final AtomicReference playerReference = new AtomicReference<>(); + FakeMediaSource secondMediaSource = new FakeMediaSource(secondTimeline); + final EventListener eventListener = + new EventListener() { + @Override + public void onPlaybackStateChanged(int state) { + if (state == Player.STATE_IDLE) { + playerReference.get().setMediaSource(secondMediaSource); + } + } + }; + ActionSchedule actionSchedule = + new ActionSchedule.Builder(TAG) + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + playerReference.set(player); + player.addListener(eventListener); + } + }) + // Ensure there are no further pending callbacks. + .delay(1) + .stop(/* reset= */ true) + .waitForPlaybackState(Player.STATE_IDLE) + .prepare() + .waitForPlaybackState(Player.STATE_ENDED) + .build(); + ExoPlayerTestRunner exoPlayerTestRunner = + new ExoPlayerTestRunner.Builder(context) + .setActionSchedule(actionSchedule) + .setTimeline(firstTimeline) + .build() + .start() + .blockUntilActionScheduleFinished(TIMEOUT_MS) + .blockUntilEnded(TIMEOUT_MS); + exoPlayerTestRunner.assertTimelinesSame( + new FakeMediaSource.InitialTimeline(firstTimeline), + firstTimeline, + Timeline.EMPTY, + new FakeMediaSource.InitialTimeline(secondTimeline), + secondTimeline); + exoPlayerTestRunner.assertTimelineChangeReasonsEqual( + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, + Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE, + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, + Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE); + } + + @Test + public void clippedLoopedPeriodsArePlayedFully() throws Exception { long startPositionUs = 300_000; long expectedDurationUs = 700_000; MediaSource mediaSource = @@ -2352,7 +2782,7 @@ public final class ExoPlayerTest { EventListener eventListener = new EventListener() { @Override - public void onPlayerStateChanged(boolean playWhenReady, @Player.State int playbackState) { + public void onPlaybackStateChanged(@Player.State int playbackState) { if (playbackState == Player.STATE_READY && clockAtStartMs.get() == C.TIME_UNSET) { clockAtStartMs.set(clock.elapsedRealtime()); } @@ -2367,7 +2797,7 @@ public final class ExoPlayerTest { } }; ActionSchedule actionSchedule = - new ActionSchedule.Builder("testClippedLoopedPeriodsArePlayedFully") + new ActionSchedule.Builder(TAG) .executeRunnable( new PlayerRunnable() { @Override @@ -2385,11 +2815,11 @@ public final class ExoPlayerTest { .setRepeatMode(Player.REPEAT_MODE_OFF) .play() .build(); - new ExoPlayerTestRunner.Builder() + new ExoPlayerTestRunner.Builder(context) .setClock(clock) - .setMediaSource(mediaSource) + .setMediaSources(mediaSource) .setActionSchedule(actionSchedule) - .build(context) + .build() .start() .blockUntilEnded(TIMEOUT_MS); @@ -2399,7 +2829,7 @@ public final class ExoPlayerTest { } @Test - public void testUpdateTrackSelectorThenSeekToUnpreparedPeriod_returnsEmptyTrackGroups() + public void updateTrackSelectorThenSeekToUnpreparedPeriod_returnsEmptyTrackGroups() throws Exception { // Use unset duration to prevent pre-loading of the second window. Timeline timelineUnsetDuration = @@ -2409,20 +2839,21 @@ public final class ExoPlayerTest { Timeline timelineSetDuration = new FakeTimeline(/* windowCount= */ 1); MediaSource mediaSource = new ConcatenatingMediaSource( - new FakeMediaSource(timelineUnsetDuration, Builder.VIDEO_FORMAT), - new FakeMediaSource(timelineSetDuration, Builder.AUDIO_FORMAT)); + new FakeMediaSource(timelineUnsetDuration, ExoPlayerTestRunner.VIDEO_FORMAT), + new FakeMediaSource(timelineSetDuration, ExoPlayerTestRunner.AUDIO_FORMAT)); ActionSchedule actionSchedule = - new ActionSchedule.Builder("testUpdateTrackSelectorThenSeekToUnpreparedPeriod") + new ActionSchedule.Builder(TAG) .pause() .waitForPlaybackState(Player.STATE_READY) .seek(/* windowIndex= */ 1, /* positionMs= */ 0) + .waitForPendingPlayerCommands() .play() .build(); List trackGroupsList = new ArrayList<>(); List trackSelectionsList = new ArrayList<>(); - new Builder() - .setMediaSource(mediaSource) - .setSupportedFormats(Builder.VIDEO_FORMAT, Builder.AUDIO_FORMAT) + new ExoPlayerTestRunner.Builder(context) + .setMediaSources(mediaSource) + .setSupportedFormats(ExoPlayerTestRunner.VIDEO_FORMAT, ExoPlayerTestRunner.AUDIO_FORMAT) .setActionSchedule(actionSchedule) .setEventListener( new EventListener() { @@ -2433,7 +2864,7 @@ public final class ExoPlayerTest { trackSelectionsList.add(trackSelections); } }) - .build(context) + .build() .start() .blockUntilEnded(TIMEOUT_MS); assertThat(trackGroupsList).hasSize(3); @@ -2441,14 +2872,16 @@ public final class ExoPlayerTest { // 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(0).get(0).getFormat(0)) + .isEqualTo(ExoPlayerTestRunner.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); + assertThat(trackGroupsList.get(2).get(0).getFormat(0)) + .isEqualTo(ExoPlayerTestRunner.AUDIO_FORMAT); } @Test - public void testSecondMediaSourceInPlaylistOnlyThrowsWhenPreviousPeriodIsFullyRead() + public void secondMediaSourceInPlaylistOnlyThrowsWhenPreviousPeriodIsFullyRead() throws Exception { Timeline fakeTimeline = new FakeTimeline( @@ -2456,9 +2889,10 @@ public final class ExoPlayerTest { /* isSeekable= */ true, /* isDynamic= */ false, /* durationUs= */ 10 * C.MICROS_PER_SECOND)); - MediaSource workingMediaSource = new FakeMediaSource(fakeTimeline, Builder.VIDEO_FORMAT); + MediaSource workingMediaSource = + new FakeMediaSource(fakeTimeline, ExoPlayerTestRunner.VIDEO_FORMAT); MediaSource failingMediaSource = - new FakeMediaSource(/* timeline= */ null, Builder.VIDEO_FORMAT) { + new FakeMediaSource(/* timeline= */ null, ExoPlayerTestRunner.VIDEO_FORMAT) { @Override public void maybeThrowSourceInfoRefreshError() throws IOException { throw new IOException(); @@ -2466,12 +2900,12 @@ public final class ExoPlayerTest { }; ConcatenatingMediaSource concatenatingMediaSource = new ConcatenatingMediaSource(workingMediaSource, failingMediaSource); - FakeRenderer renderer = new FakeRenderer(Builder.VIDEO_FORMAT); + FakeRenderer renderer = new FakeRenderer(C.TRACK_TYPE_VIDEO); ExoPlayerTestRunner testRunner = - new Builder() - .setMediaSource(concatenatingMediaSource) + new ExoPlayerTestRunner.Builder(context) + .setMediaSources(concatenatingMediaSource) .setRenderers(renderer) - .build(context); + .build(); try { testRunner.start().blockUntilEnded(TIMEOUT_MS); fail(); @@ -2492,9 +2926,10 @@ public final class ExoPlayerTest { /* isSeekable= */ true, /* isDynamic= */ false, /* durationUs= */ 10 * C.MICROS_PER_SECOND)); - MediaSource workingMediaSource = new FakeMediaSource(fakeTimeline, Builder.VIDEO_FORMAT); + MediaSource workingMediaSource = + new FakeMediaSource(fakeTimeline, ExoPlayerTestRunner.VIDEO_FORMAT); MediaSource failingMediaSource = - new FakeMediaSource(/* timeline= */ null, Builder.VIDEO_FORMAT) { + new FakeMediaSource(/* timeline= */ null, ExoPlayerTestRunner.VIDEO_FORMAT) { @Override public void maybeThrowSourceInfoRefreshError() throws IOException { throw new IOException(); @@ -2503,61 +2938,19 @@ public final class ExoPlayerTest { ConcatenatingMediaSource concatenatingMediaSource = new ConcatenatingMediaSource(workingMediaSource); ActionSchedule actionSchedule = - new ActionSchedule.Builder("testFailingSecondMediaSourceInPlaylistOnlyThrowsLater") + new ActionSchedule.Builder(TAG) .pause() .waitForPlaybackState(Player.STATE_READY) .executeRunnable(() -> concatenatingMediaSource.addMediaSource(failingMediaSource)) .play() .build(); - FakeRenderer renderer = new FakeRenderer(Builder.VIDEO_FORMAT); + FakeRenderer renderer = new FakeRenderer(C.TRACK_TYPE_VIDEO); ExoPlayerTestRunner testRunner = - new Builder() - .setMediaSource(concatenatingMediaSource) + new ExoPlayerTestRunner.Builder(context) + .setMediaSources(concatenatingMediaSource) .setActionSchedule(actionSchedule) .setRenderers(renderer) - .build(context); - try { - testRunner.start().blockUntilEnded(TIMEOUT_MS); - fail(); - } catch (ExoPlaybackException e) { - // Expected exception. - } - assertThat(renderer.sampleBufferReadCount).isAtLeast(1); - assertThat(renderer.hasReadStreamToEnd()).isTrue(); - } - - @Test - public void failingDynamicUpdateOnlyThrowsWhenAvailablePeriodHasBeenFullyRead() throws Exception { - Timeline fakeTimeline = - new FakeTimeline( - new TimelineWindowDefinition( - /* isSeekable= */ true, - /* isDynamic= */ true, - /* durationUs= */ 10 * C.MICROS_PER_SECOND)); - AtomicReference wasReadyOnce = new AtomicReference<>(false); - MediaSource mediaSource = - new FakeMediaSource(fakeTimeline, Builder.VIDEO_FORMAT) { - @Override - public void maybeThrowSourceInfoRefreshError() throws IOException { - if (wasReadyOnce.get()) { - throw new IOException(); - } - } - }; - ActionSchedule actionSchedule = - new ActionSchedule.Builder("testFailingDynamicMediaSourceInTimelineOnlyThrowsLater") - .pause() - .waitForPlaybackState(Player.STATE_READY) - .executeRunnable(() -> wasReadyOnce.set(true)) - .play() .build(); - FakeRenderer renderer = new FakeRenderer(Builder.VIDEO_FORMAT); - ExoPlayerTestRunner testRunner = - new Builder() - .setMediaSource(mediaSource) - .setActionSchedule(actionSchedule) - .setRenderers(renderer) - .build(context); try { testRunner.start().blockUntilEnded(TIMEOUT_MS); fail(); @@ -2577,7 +2970,7 @@ public final class ExoPlayerTest { MediaSource mediaSource = new FakeMediaSource(timeline); ConcatenatingMediaSource concatenatingMediaSource = new ConcatenatingMediaSource(mediaSource); ActionSchedule actionSchedule = - new ActionSchedule.Builder("removingLoopingLastPeriodFromPlaylistDoesNotThrow") + new ActionSchedule.Builder(TAG) .pause() .waitForPlaybackState(Player.STATE_READY) // Play almost to end to ensure the current period is fully buffered. @@ -2587,10 +2980,10 @@ public final class ExoPlayerTest { // Remove the media source. .executeRunnable(concatenatingMediaSource::clear) .build(); - new Builder() - .setMediaSource(concatenatingMediaSource) + new ExoPlayerTestRunner.Builder(context) + .setMediaSources(concatenatingMediaSource) .setActionSchedule(actionSchedule) - .build(context) + .build() .start() .blockUntilEnded(TIMEOUT_MS); } @@ -2608,12 +3001,14 @@ public final class ExoPlayerTest { MediaSource concatenatedMediaSource = new ConcatenatingMediaSource(clippedMediaSource); AtomicLong positionWhenReady = new AtomicLong(); ActionSchedule actionSchedule = - new ActionSchedule.Builder("seekToUnpreparedWindowWithNonZeroOffsetInConcatenation") + new ActionSchedule.Builder(TAG) .pause() .waitForPlaybackState(Player.STATE_BUFFERING) + // Seek while unprepared and wait until the player handled all updates. .seek(/* positionMs= */ 10) - .waitForTimelineChanged() - .executeRunnable(() -> mediaSource.setNewSourceInfo(timeline, /* newManifest= */ null)) + .waitForPendingPlayerCommands() + // Finish preparation. + .executeRunnable(() -> mediaSource.setNewSourceInfo(timeline)) .waitForTimelineChanged() .waitForPlaybackState(Player.STATE_READY) .executeRunnable( @@ -2625,10 +3020,10 @@ public final class ExoPlayerTest { }) .play() .build(); - new Builder() - .setMediaSource(concatenatedMediaSource) + new ExoPlayerTestRunner.Builder(context) + .setMediaSources(concatenatedMediaSource) .setActionSchedule(actionSchedule) - .build(context) + .build() .start() .blockUntilEnded(TIMEOUT_MS); @@ -2652,13 +3047,12 @@ public final class ExoPlayerTest { AtomicInteger periodIndexWhenReady = new AtomicInteger(); AtomicLong positionWhenReady = new AtomicLong(); ActionSchedule actionSchedule = - new ActionSchedule.Builder("seekToUnpreparedWindowWithMultiplePeriodsInConcatenation") + new ActionSchedule.Builder(TAG) .pause() .waitForPlaybackState(Player.STATE_BUFFERING) // Seek 10ms into the second period. .seek(/* positionMs= */ periodDurationMs + 10) - .waitForTimelineChanged() - .executeRunnable(() -> mediaSource.setNewSourceInfo(timeline, /* newManifest= */ null)) + .executeRunnable(() -> mediaSource.setNewSourceInfo(timeline)) .waitForTimelineChanged() .waitForPlaybackState(Player.STATE_READY) .executeRunnable( @@ -2671,10 +3065,10 @@ public final class ExoPlayerTest { }) .play() .build(); - new Builder() - .setMediaSource(concatenatedMediaSource) + new ExoPlayerTestRunner.Builder(context) + .setMediaSources(concatenatedMediaSource) .setActionSchedule(actionSchedule) - .build(context) + .build() .start() .blockUntilEnded(TIMEOUT_MS); @@ -2710,7 +3104,7 @@ public final class ExoPlayerTest { } }; ActionSchedule actionSchedule = - new ActionSchedule.Builder("periodTransitionReportsCorrectBufferedPosition") + new ActionSchedule.Builder(TAG) .executeRunnable( new PlayerRunnable() { @Override @@ -2725,10 +3119,10 @@ public final class ExoPlayerTest { .waitForIsLoading(/* targetIsLoading= */ false) .play() .build(); - new Builder() + new ExoPlayerTestRunner.Builder(context) .setTimeline(timeline) .setActionSchedule(actionSchedule) - .build(context) + .build() .start() .blockUntilEnded(TIMEOUT_MS); @@ -2738,10 +3132,7 @@ public final class ExoPlayerTest { @Test public void contentWithInitialSeekPositionAfterPrerollAdStartsAtSeekPosition() throws Exception { AdPlaybackState adPlaybackState = - FakeTimeline.createAdPlaybackState(/* adsPerAdGroup= */ 3, /* adGroupTimesUs= */ 0) - .withAdUri(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0, Uri.parse("https://ad1")) - .withAdUri(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 1, Uri.parse("https://ad2")) - .withAdUri(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 2, Uri.parse("https://ad3")); + FakeTimeline.createAdPlaybackState(/* adsPerAdGroup= */ 3, /* adGroupTimesUs...= */ 0); Timeline fakeTimeline = new FakeTimeline( new TimelineWindowDefinition( @@ -2751,7 +3142,7 @@ public final class ExoPlayerTest { /* isDynamic= */ false, /* durationUs= */ 10_000_000, adPlaybackState)); - final FakeMediaSource fakeMediaSource = new FakeMediaSource(fakeTimeline); + FakeMediaSource fakeMediaSource = new FakeMediaSource(/* timeline= */ null); AtomicReference playerReference = new AtomicReference<>(); AtomicLong contentStartPositionMs = new AtomicLong(C.TIME_UNSET); EventListener eventListener = @@ -2764,7 +3155,7 @@ public final class ExoPlayerTest { } }; ActionSchedule actionSchedule = - new ActionSchedule.Builder("contentWithInitialSeekAfterPrerollAd") + new ActionSchedule.Builder(TAG) .executeRunnable( new PlayerRunnable() { @Override @@ -2773,12 +3164,66 @@ public final class ExoPlayerTest { player.addListener(eventListener); } }) - .seek(5_000) + .seek(/* positionMs= */ 5_000) + .waitForPlaybackState(Player.STATE_BUFFERING) + .executeRunnable(() -> fakeMediaSource.setNewSourceInfo(fakeTimeline)) .build(); - new ExoPlayerTestRunner.Builder() - .setMediaSource(fakeMediaSource) + new ExoPlayerTestRunner.Builder(context) + .setMediaSources(fakeMediaSource) .setActionSchedule(actionSchedule) - .build(context) + .build() + .start() + .blockUntilEnded(TIMEOUT_MS); + + assertThat(contentStartPositionMs.get()).isAtLeast(5_000L); + } + + @Test + public void contentWithoutInitialSeekStartsAtDefaultPositionAfterPrerollAd() throws Exception { + AdPlaybackState adPlaybackState = + FakeTimeline.createAdPlaybackState(/* adsPerAdGroup= */ 3, /* adGroupTimesUs...= */ 0); + Timeline fakeTimeline = + new FakeTimeline( + new TimelineWindowDefinition( + /* periodCount= */ 1, + /* id= */ 0, + /* isSeekable= */ true, + /* isDynamic= */ false, + /* isLive= */ false, + /* isPlaceholder= */ false, + /* durationUs= */ 10_000_000, + /* defaultPositionUs= */ 5_000_000, + TimelineWindowDefinition.DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US, + adPlaybackState)); + FakeMediaSource fakeMediaSource = new FakeMediaSource(/* timeline= */ null); + AtomicReference playerReference = new AtomicReference<>(); + AtomicLong contentStartPositionMs = new AtomicLong(C.TIME_UNSET); + EventListener eventListener = + new EventListener() { + @Override + public void onPositionDiscontinuity(@DiscontinuityReason int reason) { + if (reason == Player.DISCONTINUITY_REASON_AD_INSERTION) { + contentStartPositionMs.set(playerReference.get().getContentPosition()); + } + } + }; + ActionSchedule actionSchedule = + new ActionSchedule.Builder(TAG) + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + playerReference.set(player); + player.addListener(eventListener); + } + }) + .waitForPlaybackState(Player.STATE_BUFFERING) + .executeRunnable(() -> fakeMediaSource.setNewSourceInfo(fakeTimeline)) + .build(); + new ExoPlayerTestRunner.Builder(context) + .setMediaSources(fakeMediaSource) + .setActionSchedule(actionSchedule) + .build() .start() .blockUntilEnded(TIMEOUT_MS); @@ -2789,35 +3234,30 @@ public final class ExoPlayerTest { public void setPlaybackParametersConsecutivelyNotifiesListenerForEveryChangeOnce() throws Exception { ActionSchedule actionSchedule = - new ActionSchedule.Builder("setPlaybackParametersNotifiesListenerForEveryChangeOnce") + new ActionSchedule.Builder(TAG) .pause() .waitForPlaybackState(Player.STATE_READY) - .setPlaybackParameters(new PlaybackParameters(1.1f)) - .setPlaybackParameters(new PlaybackParameters(1.2f)) - .setPlaybackParameters(new PlaybackParameters(1.3f)) + .setPlaybackSpeed(1.1f) + .setPlaybackSpeed(1.2f) + .setPlaybackSpeed(1.3f) .play() .build(); - List reportedPlaybackParameters = new ArrayList<>(); + List reportedPlaybackSpeeds = new ArrayList<>(); EventListener listener = new EventListener() { @Override - public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) { - reportedPlaybackParameters.add(playbackParameters); + public void onPlaybackSpeedChanged(float playbackSpeed) { + reportedPlaybackSpeeds.add(playbackSpeed); } }; - new ExoPlayerTestRunner.Builder() + new ExoPlayerTestRunner.Builder(context) .setActionSchedule(actionSchedule) .setEventListener(listener) - .build(context) + .build() .start() .blockUntilEnded(TIMEOUT_MS); - assertThat(reportedPlaybackParameters) - .containsExactly( - new PlaybackParameters(1.1f), - new PlaybackParameters(1.2f), - new PlaybackParameters(1.3f)) - .inOrder(); + assertThat(reportedPlaybackSpeeds).containsExactly(1.1f, 1.2f, 1.3f).inOrder(); } @Test @@ -2825,59 +3265,55 @@ public final class ExoPlayerTest { setUnsupportedPlaybackParametersConsecutivelyNotifiesListenerForEveryChangeOnceAndResetsOnceHandled() throws Exception { Renderer renderer = - new FakeMediaClockRenderer(Builder.AUDIO_FORMAT) { + new FakeMediaClockRenderer(C.TRACK_TYPE_AUDIO) { @Override public long getPositionUs() { return 0; } @Override - public void setPlaybackParameters(PlaybackParameters playbackParameters) {} + public void setPlaybackSpeed(float playbackSpeed) {} @Override - public PlaybackParameters getPlaybackParameters() { - return PlaybackParameters.DEFAULT; + public float getPlaybackSpeed() { + return Player.DEFAULT_PLAYBACK_SPEED; } }; ActionSchedule actionSchedule = - new ActionSchedule.Builder("setUnsupportedPlaybackParametersNotifiesListenersCorrectly") + new ActionSchedule.Builder(TAG) .pause() .waitForPlaybackState(Player.STATE_READY) - .setPlaybackParameters(new PlaybackParameters(1.1f)) - .setPlaybackParameters(new PlaybackParameters(1.2f)) - .setPlaybackParameters(new PlaybackParameters(1.3f)) + .setPlaybackSpeed(1.1f) + .setPlaybackSpeed(1.2f) + .setPlaybackSpeed(1.3f) .play() .build(); - List reportedPlaybackParameters = new ArrayList<>(); + List reportedPlaybackParameters = new ArrayList<>(); EventListener listener = new EventListener() { @Override - public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) { - reportedPlaybackParameters.add(playbackParameters); + public void onPlaybackSpeedChanged(float playbackSpeed) { + reportedPlaybackParameters.add(playbackSpeed); } }; - new ExoPlayerTestRunner.Builder() - .setSupportedFormats(Builder.AUDIO_FORMAT) + new ExoPlayerTestRunner.Builder(context) + .setSupportedFormats(ExoPlayerTestRunner.AUDIO_FORMAT) .setRenderers(renderer) .setActionSchedule(actionSchedule) .setEventListener(listener) - .build(context) + .build() .start() .blockUntilEnded(TIMEOUT_MS); assertThat(reportedPlaybackParameters) - .containsExactly( - new PlaybackParameters(1.1f), - new PlaybackParameters(1.2f), - new PlaybackParameters(1.3f), - PlaybackParameters.DEFAULT) + .containsExactly(1.1f, 1.2f, 1.3f, Player.DEFAULT_PLAYBACK_SPEED) .inOrder(); } @Test public void simplePlaybackHasNoPlaybackSuppression() throws Exception { ActionSchedule actionSchedule = - new ActionSchedule.Builder("simplePlaybackHasNoPlaybackSuppression") + new ActionSchedule.Builder(TAG) .play() .waitForPlaybackState(Player.STATE_READY) .pause() @@ -2892,10 +3328,10 @@ public final class ExoPlayerTest { seenPlaybackSuppression.set(true); } }; - new ExoPlayerTestRunner.Builder() + new ExoPlayerTestRunner.Builder(context) .setActionSchedule(actionSchedule) .setEventListener(listener) - .build(context) + .build() .start() .blockUntilEnded(TIMEOUT_MS); @@ -2909,7 +3345,7 @@ public final class ExoPlayerTest { PlayerStateGrabber playerStateGrabber = new PlayerStateGrabber(); ActionSchedule actionSchedule = - new ActionSchedule.Builder("audioFocusDenied") + new ActionSchedule.Builder(TAG) .setAudioAttributes(AudioAttributes.DEFAULT, /* handleAudioFocus= */ true) .play() .waitForPlaybackState(Player.STATE_READY) @@ -2924,10 +3360,10 @@ public final class ExoPlayerTest { seenPlaybackSuppression.set(true); } }; - new ExoPlayerTestRunner.Builder() + new ExoPlayerTestRunner.Builder(context) .setActionSchedule(actionSchedule) .setEventListener(listener) - .build(context) + .build() .start() .blockUntilActionScheduleFinished(TIMEOUT_MS); @@ -2936,7 +3372,7 @@ public final class ExoPlayerTest { } @Test - public void testDelegatingMediaSourceApproach() throws Exception { + public void delegatingMediaSourceApproach() throws Exception { Timeline fakeTimeline = new FakeTimeline( new TimelineWindowDefinition( @@ -2948,9 +3384,9 @@ public final class ExoPlayerTest { public void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) { super.prepareSourceInternal(mediaTransferListener); underlyingSource.addMediaSource( - new FakeMediaSource(fakeTimeline, Builder.VIDEO_FORMAT)); + new FakeMediaSource(fakeTimeline, ExoPlayerTestRunner.VIDEO_FORMAT)); underlyingSource.addMediaSource( - new FakeMediaSource(fakeTimeline, Builder.VIDEO_FORMAT)); + new FakeMediaSource(fakeTimeline, ExoPlayerTestRunner.VIDEO_FORMAT)); prepareChildSource(null, underlyingSource); } @@ -2970,15 +3406,27 @@ public final class ExoPlayerTest { Void id, MediaSource mediaSource, Timeline timeline) { refreshSourceInfo(timeline); } + + @Override + public boolean isSingleWindow() { + return false; + } + + @Override + @Nullable + public Timeline getInitialTimeline() { + return Timeline.EMPTY; + } }; int[] currentWindowIndices = new int[1]; long[] currentPlaybackPositions = new long[1]; long[] windowCounts = new long[1]; int seekToWindowIndex = 1; ActionSchedule actionSchedule = - new ActionSchedule.Builder("testDelegatingMediaSourceApproach") + new ActionSchedule.Builder(TAG) .seek(/* windowIndex= */ 1, /* positionMs= */ 5000) - .waitForSeekProcessed() + .waitForTimelineChanged( + /* expectedTimeline= */ null, Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE) .executeRunnable( new PlayerRunnable() { @Override @@ -2990,30 +3438,31 @@ public final class ExoPlayerTest { }) .build(); ExoPlayerTestRunner exoPlayerTestRunner = - new Builder() - .setMediaSource(delegatingMediaSource) + new ExoPlayerTestRunner.Builder(context) + .setMediaSources(delegatingMediaSource) .setActionSchedule(actionSchedule) - .build(context) + .build() .start() .blockUntilActionScheduleFinished(TIMEOUT_MS) .blockUntilEnded(TIMEOUT_MS); - exoPlayerTestRunner.assertTimelineChangeReasonsEqual(Player.TIMELINE_CHANGE_REASON_PREPARED); + exoPlayerTestRunner.assertTimelineChangeReasonsEqual( + Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE); assertArrayEquals(new long[] {2}, windowCounts); assertArrayEquals(new int[] {seekToWindowIndex}, currentWindowIndices); assertArrayEquals(new long[] {5_000}, currentPlaybackPositions); } @Test - public void testSeekTo_windowIndexIsReset_deprecated() throws Exception { + public void seekTo_windowIndexIsReset_deprecated() throws Exception { FakeTimeline fakeTimeline = new FakeTimeline(/* windowCount= */ 1); FakeMediaSource mediaSource = new FakeMediaSource(fakeTimeline); LoopingMediaSource loopingMediaSource = new LoopingMediaSource(mediaSource, 2); final int[] windowIndex = {C.INDEX_UNSET}; final long[] positionMs = {C.TIME_UNSET}; ActionSchedule actionSchedule = - new ActionSchedule.Builder("testSeekTo_windowIndexIsReset_deprecated") + new ActionSchedule.Builder(TAG) + .pause() .seek(/* windowIndex= */ 1, /* positionMs= */ C.TIME_UNSET) - .waitForSeekProcessed() .playUntilPosition(/* windowIndex= */ 1, /* positionMs= */ 5000) .executeRunnable( new PlayerRunnable() { @@ -3033,10 +3482,10 @@ public final class ExoPlayerTest { } }) .build(); - new ExoPlayerTestRunner.Builder() - .setMediaSource(loopingMediaSource) + new ExoPlayerTestRunner.Builder(context) + .setMediaSources(loopingMediaSource) .setActionSchedule(actionSchedule) - .build(context) + .build() .start() .blockUntilActionScheduleFinished(TIMEOUT_MS); @@ -3045,22 +3494,22 @@ public final class ExoPlayerTest { } @Test - public void testSeekTo_windowIndexIsReset() throws Exception { + public void seekTo_windowIndexIsReset() throws Exception { FakeTimeline fakeTimeline = new FakeTimeline(/* windowCount= */ 1); FakeMediaSource mediaSource = new FakeMediaSource(fakeTimeline); LoopingMediaSource loopingMediaSource = new LoopingMediaSource(mediaSource, 2); final int[] windowIndex = {C.INDEX_UNSET}; final long[] positionMs = {C.TIME_UNSET}; ActionSchedule actionSchedule = - new ActionSchedule.Builder("testSeekTo_windowIndexIsReset") + new ActionSchedule.Builder(TAG) + .pause() .seek(/* windowIndex= */ 1, /* positionMs= */ C.TIME_UNSET) - .waitForSeekProcessed() .playUntilPosition(/* windowIndex= */ 1, /* positionMs= */ 5000) .executeRunnable( new PlayerRunnable() { @Override public void run(SimpleExoPlayer player) { - player.setMediaItem(mediaSource, /* startPositionMs= */ 5000); + player.setMediaSource(mediaSource, /* startPositionMs= */ 5000); player.prepare(); } }) @@ -3073,10 +3522,10 @@ public final class ExoPlayerTest { } }) .build(); - new ExoPlayerTestRunner.Builder() - .setMediaSource(loopingMediaSource) + new ExoPlayerTestRunner.Builder(context) + .setMediaSources(loopingMediaSource) .setActionSchedule(actionSchedule) - .build(context) + .build() .start() .blockUntilActionScheduleFinished(TIMEOUT_MS); @@ -3090,7 +3539,7 @@ public final class ExoPlayerTest { CountDownLatch becomingNoisyDelivered = new CountDownLatch(1); PlayerStateGrabber playerStateGrabber = new PlayerStateGrabber(); ActionSchedule actionSchedule = - new ActionSchedule.Builder("becomingNoisyIgnoredIfBecomingNoisyHandlingIsDisabled") + new ActionSchedule.Builder(TAG) .executeRunnable( new PlayerRunnable() { @Override @@ -3111,7 +3560,7 @@ public final class ExoPlayerTest { .build(); ExoPlayerTestRunner testRunner = - new ExoPlayerTestRunner.Builder().setActionSchedule(actionSchedule).build(context).start(); + new ExoPlayerTestRunner.Builder(context).setActionSchedule(actionSchedule).build().start(); becomingNoisyHandlingDisabled.await(); deliverBroadcast(new Intent(AudioManager.ACTION_AUDIO_BECOMING_NOISY)); becomingNoisyDelivered.countDown(); @@ -3124,7 +3573,7 @@ public final class ExoPlayerTest { public void pausesWhenBecomingNoisyIfBecomingNoisyHandlingIsEnabled() throws Exception { CountDownLatch becomingNoisyHandlingEnabled = new CountDownLatch(1); ActionSchedule actionSchedule = - new ActionSchedule.Builder("pausesWhenBecomingNoisyIfBecomingNoisyHandlingIsEnabled") + new ActionSchedule.Builder(TAG) .executeRunnable( new PlayerRunnable() { @Override @@ -3138,7 +3587,7 @@ public final class ExoPlayerTest { .build(); ExoPlayerTestRunner testRunner = - new ExoPlayerTestRunner.Builder().setActionSchedule(actionSchedule).build(context).start(); + new ExoPlayerTestRunner.Builder(context).setActionSchedule(actionSchedule).build().start(); becomingNoisyHandlingEnabled.await(); deliverBroadcast(new Intent(AudioManager.ACTION_AUDIO_BECOMING_NOISY)); @@ -3147,13 +3596,131 @@ public final class ExoPlayerTest { testRunner.blockUntilActionScheduleFinished(TIMEOUT_MS).blockUntilEnded(TIMEOUT_MS); } + // Disabled until the flag to throw exceptions for [internal: b/144538905] is enabled by default. + @Ignore @Test - public void loadControlNeverWantsToLoadOrPlay_playbackDoesNotGetStuck() throws Exception { + public void loadControlNeverWantsToLoad_throwsIllegalStateException() { + LoadControl neverLoadingLoadControl = + new DefaultLoadControl() { + @Override + public boolean shouldContinueLoading( + long playbackPositionUs, long bufferedDurationUs, float playbackSpeed) { + return false; + } + + @Override + public boolean shouldStartPlayback( + long bufferedDurationUs, float playbackSpeed, boolean rebuffering) { + return true; + } + }; + + // Use chunked data to ensure the player actually needs to continue loading and playing. + FakeAdaptiveDataSet.Factory dataSetFactory = + new FakeAdaptiveDataSet.Factory( + /* chunkDurationUs= */ 500_000, /* bitratePercentStdDev= */ 10.0); + MediaSource chunkedMediaSource = + new FakeAdaptiveMediaSource( + new FakeTimeline(/* windowCount= */ 1), + new TrackGroupArray(new TrackGroup(ExoPlayerTestRunner.VIDEO_FORMAT)), + new FakeChunkSource.Factory(dataSetFactory, new FakeDataSource.Factory())); + + ExoPlaybackException exception = + assertThrows( + ExoPlaybackException.class, + () -> + new ExoPlayerTestRunner.Builder(context) + .setLoadControl(neverLoadingLoadControl) + .setMediaSources(chunkedMediaSource) + .build() + .start() + .blockUntilEnded(TIMEOUT_MS)); + assertThat(exception.type).isEqualTo(ExoPlaybackException.TYPE_UNEXPECTED); + assertThat(exception.getUnexpectedException()).isInstanceOf(IllegalStateException.class); + } + + @Test + public void + nextLoadPositionExceedingLoadControlMaxBuffer_whileCurrentLoadInProgress_doesNotThrowException() + throws Exception { + long maxBufferUs = 2 * C.MICROS_PER_SECOND; + LoadControl loadControlWithMaxBufferUs = + new DefaultLoadControl() { + @Override + public boolean shouldContinueLoading( + long playbackPositionUs, long bufferedDurationUs, float playbackSpeed) { + return bufferedDurationUs < maxBufferUs; + } + + @Override + public boolean shouldStartPlayback( + long bufferedDurationUs, float playbackSpeed, boolean rebuffering) { + return true; + } + }; + MediaSource mediaSourceWithLoadInProgress = + new FakeMediaSource( + new FakeTimeline(/* windowCount= */ 1), ExoPlayerTestRunner.VIDEO_FORMAT) { + @Override + protected FakeMediaPeriod createFakeMediaPeriod( + MediaPeriodId id, + TrackGroupArray trackGroupArray, + Allocator allocator, + EventDispatcher eventDispatcher, + @Nullable TransferListener transferListener) { + return new FakeMediaPeriod(trackGroupArray, eventDispatcher) { + @Override + public long getBufferedPositionUs() { + // Pretend not to have buffered data yet. + return 0; + } + + @Override + public long getNextLoadPositionUs() { + // Set next load position beyond the maxBufferUs configured in the LoadControl. + return Long.MAX_VALUE; + } + + @Override + public boolean isLoading() { + return true; + } + }; + } + }; + FakeRenderer rendererWaitingForData = + new FakeRenderer(C.TRACK_TYPE_VIDEO) { + @Override + public boolean isReady() { + return false; + } + }; + + ExoPlayer player = + new TestExoPlayer.Builder(context) + .setRenderers(rendererWaitingForData) + .setLoadControl(loadControlWithMaxBufferUs) + .experimental_setThrowWhenStuckBuffering(true) + .build(); + player.setMediaSource(mediaSourceWithLoadInProgress); + player.prepare(); + + // Wait until the MediaSource is prepared, i.e. returned its timeline, and at least one + // iteration of doSomeWork after this was run. + TestExoPlayer.runUntilTimelineChanged(player); + TestExoPlayer.runUntilPendingCommandsAreFullyHandled(player); + + assertThat(player.getPlayerError()).isNull(); + } + + @Test + public void loadControlNeverWantsToPlay_playbackDoesNotGetStuck() throws Exception { LoadControl neverLoadingOrPlayingLoadControl = new DefaultLoadControl() { @Override - public boolean shouldContinueLoading(long bufferedDurationUs, float playbackSpeed) { - return false; + public boolean shouldContinueLoading( + long playbackPositionUs, long bufferedDurationUs, float playbackSpeed) { + return true; } @Override @@ -3170,18 +3737,2854 @@ public final class ExoPlayerTest { MediaSource chunkedMediaSource = new FakeAdaptiveMediaSource( new FakeTimeline(/* windowCount= */ 1), - new TrackGroupArray(new TrackGroup(Builder.VIDEO_FORMAT)), + new TrackGroupArray(new TrackGroup(ExoPlayerTestRunner.VIDEO_FORMAT)), new FakeChunkSource.Factory(dataSetFactory, new FakeDataSource.Factory())); - new ExoPlayerTestRunner.Builder() + new ExoPlayerTestRunner.Builder(context) .setLoadControl(neverLoadingOrPlayingLoadControl) - .setMediaSource(chunkedMediaSource) - .build(context) + .setMediaSources(chunkedMediaSource) + .build() .start() // This throws if playback doesn't finish within timeout. .blockUntilEnded(TIMEOUT_MS); } + @Test + public void moveMediaItem() throws Exception { + TimelineWindowDefinition firstWindowDefinition = + new TimelineWindowDefinition( + /* periodCount= */ 1, + /* id= */ 1, + /* isSeekable= */ true, + /* isDynamic= */ false, + /* durationUs= */ C.msToUs(10000)); + TimelineWindowDefinition secondWindowDefinition = + new TimelineWindowDefinition( + /* periodCount= */ 1, + /* id= */ 2, + /* isSeekable= */ true, + /* isDynamic= */ false, + /* durationUs= */ C.msToUs(10000)); + Timeline timeline1 = new FakeTimeline(firstWindowDefinition); + Timeline timeline2 = new FakeTimeline(secondWindowDefinition); + MediaSource mediaSource1 = new FakeMediaSource(timeline1); + MediaSource mediaSource2 = new FakeMediaSource(timeline2); + Timeline expectedDummyTimeline = + new FakeTimeline( + TimelineWindowDefinition.createDummy(/* tag= */ 1), + TimelineWindowDefinition.createDummy(/* tag= */ 2)); + ActionSchedule actionSchedule = + new ActionSchedule.Builder(TAG) + .waitForTimelineChanged( + /* expectedTimeline= */ null, Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE) + .moveMediaItem(/* currentIndex= */ 0, /* newIndex= */ 1) + .build(); + ExoPlayerTestRunner exoPlayerTestRunner = + new ExoPlayerTestRunner.Builder(context) + .setMediaSources(mediaSource1, mediaSource2) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilActionScheduleFinished(TIMEOUT_MS) + .blockUntilEnded(TIMEOUT_MS); + + Timeline expectedRealTimeline = new FakeTimeline(firstWindowDefinition, secondWindowDefinition); + Timeline expectedRealTimelineAfterMove = + new FakeTimeline(secondWindowDefinition, firstWindowDefinition); + exoPlayerTestRunner.assertTimelineChangeReasonsEqual( + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, + Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE, + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED); + exoPlayerTestRunner.assertTimelinesSame( + expectedDummyTimeline, expectedRealTimeline, expectedRealTimelineAfterMove); + } + + @Test + public void removeMediaItem() throws Exception { + TimelineWindowDefinition firstWindowDefinition = + new TimelineWindowDefinition( + /* periodCount= */ 1, + /* id= */ 1, + /* isSeekable= */ true, + /* isDynamic= */ false, + /* durationUs= */ C.msToUs(10000)); + TimelineWindowDefinition secondWindowDefinition = + new TimelineWindowDefinition( + /* periodCount= */ 1, + /* id= */ 2, + /* isSeekable= */ true, + /* isDynamic= */ false, + /* durationUs= */ C.msToUs(10000)); + TimelineWindowDefinition thirdWindowDefinition = + new TimelineWindowDefinition( + /* periodCount= */ 1, + /* id= */ 3, + /* isSeekable= */ true, + /* isDynamic= */ false, + /* durationUs= */ C.msToUs(10000)); + Timeline timeline1 = new FakeTimeline(firstWindowDefinition); + Timeline timeline2 = new FakeTimeline(secondWindowDefinition); + Timeline timeline3 = new FakeTimeline(thirdWindowDefinition); + MediaSource mediaSource1 = new FakeMediaSource(timeline1); + MediaSource mediaSource2 = new FakeMediaSource(timeline2); + MediaSource mediaSource3 = new FakeMediaSource(timeline3); + ActionSchedule actionSchedule = + new ActionSchedule.Builder(TAG) + .waitForPlaybackState(Player.STATE_READY) + .removeMediaItem(/* index= */ 0) + .build(); + ExoPlayerTestRunner exoPlayerTestRunner = + new ExoPlayerTestRunner.Builder(context) + .setMediaSources(mediaSource1, mediaSource2, mediaSource3) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilActionScheduleFinished(TIMEOUT_MS) + .blockUntilEnded(TIMEOUT_MS); + + Timeline expectedDummyTimeline = + new FakeTimeline( + TimelineWindowDefinition.createDummy(/* tag= */ 1), + TimelineWindowDefinition.createDummy(/* tag= */ 2), + TimelineWindowDefinition.createDummy(/* tag= */ 3)); + Timeline expectedRealTimeline = + new FakeTimeline(firstWindowDefinition, secondWindowDefinition, thirdWindowDefinition); + Timeline expectedRealTimelineAfterRemove = + new FakeTimeline(secondWindowDefinition, thirdWindowDefinition); + exoPlayerTestRunner.assertTimelineChangeReasonsEqual( + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, + Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE, + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED); + exoPlayerTestRunner.assertTimelinesSame( + expectedDummyTimeline, expectedRealTimeline, expectedRealTimelineAfterRemove); + } + + @Test + public void removeMediaItems() throws Exception { + TimelineWindowDefinition firstWindowDefinition = + new TimelineWindowDefinition( + /* periodCount= */ 1, + /* id= */ 1, + /* isSeekable= */ true, + /* isDynamic= */ false, + /* durationUs= */ C.msToUs(10000)); + TimelineWindowDefinition secondWindowDefinition = + new TimelineWindowDefinition( + /* periodCount= */ 1, + /* id= */ 2, + /* isSeekable= */ true, + /* isDynamic= */ false, + /* durationUs= */ C.msToUs(10000)); + TimelineWindowDefinition thirdWindowDefinition = + new TimelineWindowDefinition( + /* periodCount= */ 1, + /* id= */ 3, + /* isSeekable= */ true, + /* isDynamic= */ false, + /* durationUs= */ C.msToUs(10000)); + Timeline timeline1 = new FakeTimeline(firstWindowDefinition); + Timeline timeline2 = new FakeTimeline(secondWindowDefinition); + Timeline timeline3 = new FakeTimeline(thirdWindowDefinition); + MediaSource mediaSource1 = new FakeMediaSource(timeline1); + MediaSource mediaSource2 = new FakeMediaSource(timeline2); + MediaSource mediaSource3 = new FakeMediaSource(timeline3); + ActionSchedule actionSchedule = + new ActionSchedule.Builder(TAG) + .waitForPlaybackState(Player.STATE_READY) + .removeMediaItems(/* fromIndex= */ 1, /* toIndex= */ 3) + .build(); + ExoPlayerTestRunner exoPlayerTestRunner = + new ExoPlayerTestRunner.Builder(context) + .setMediaSources(mediaSource1, mediaSource2, mediaSource3) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilActionScheduleFinished(TIMEOUT_MS) + .blockUntilEnded(TIMEOUT_MS); + + Timeline expectedDummyTimeline = + new FakeTimeline( + TimelineWindowDefinition.createDummy(/* tag= */ 1), + TimelineWindowDefinition.createDummy(/* tag= */ 2), + TimelineWindowDefinition.createDummy(/* tag= */ 3)); + Timeline expectedRealTimeline = + new FakeTimeline(firstWindowDefinition, secondWindowDefinition, thirdWindowDefinition); + Timeline expectedRealTimelineAfterRemove = new FakeTimeline(firstWindowDefinition); + exoPlayerTestRunner.assertTimelineChangeReasonsEqual( + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, + Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE, + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED); + exoPlayerTestRunner.assertTimelinesSame( + expectedDummyTimeline, expectedRealTimeline, expectedRealTimelineAfterRemove); + } + + @Test + public void clearMediaItems() throws Exception { + Timeline timeline = new FakeTimeline(/* windowCount= */ 1); + ActionSchedule actionSchedule = + new ActionSchedule.Builder(TAG) + .waitForTimelineChanged(timeline, Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE) + .waitForPlaybackState(Player.STATE_READY) + .clearMediaItems() + .waitForPlaybackState(Player.STATE_ENDED) + .build(); + ExoPlayerTestRunner exoPlayerTestRunner = + new ExoPlayerTestRunner.Builder(context) + .setTimeline(timeline) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilActionScheduleFinished(TIMEOUT_MS) + .blockUntilEnded(TIMEOUT_MS); + + exoPlayerTestRunner.assertPlaybackStatesEqual( + Player.STATE_BUFFERING, Player.STATE_READY, Player.STATE_ENDED); + exoPlayerTestRunner.assertTimelinesSame(dummyTimeline, timeline, Timeline.EMPTY); + exoPlayerTestRunner.assertTimelineChangeReasonsEqual( + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED /* media item set (masked timeline) */, + Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE /* source prepared */, + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED /* playlist cleared */); + } + + @Test + public void multipleModificationWithRecursiveListenerInvocations() throws Exception { + Timeline timeline = new FakeTimeline(/* windowCount= */ 1); + MediaSource mediaSource = new FakeMediaSource(timeline); + Timeline secondTimeline = new FakeTimeline(/* windowCount= */ 2); + MediaSource secondMediaSource = new FakeMediaSource(secondTimeline); + ActionSchedule actionSchedule = + new ActionSchedule.Builder(TAG) + .waitForTimelineChanged(timeline, Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE) + .clearMediaItems() + .setMediaSources(secondMediaSource) + .waitForTimelineChanged() + .build(); + ExoPlayerTestRunner exoPlayerTestRunner = + new ExoPlayerTestRunner.Builder(context) + .setMediaSources(mediaSource) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilActionScheduleFinished(TIMEOUT_MS) + .blockUntilEnded(TIMEOUT_MS); + + exoPlayerTestRunner.assertTimelinesSame( + dummyTimeline, + timeline, + Timeline.EMPTY, + new FakeMediaSource.InitialTimeline(secondTimeline), + secondTimeline); + exoPlayerTestRunner.assertTimelineChangeReasonsEqual( + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, + Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE, + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, + Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE); + } + + @Test + public void modifyPlaylistUnprepared_remainsInIdle_needsPrepareForBuffering() throws Exception { + Timeline firstTimeline = new FakeTimeline(/* windowCount= */ 1); + MediaSource firstMediaSource = new FakeMediaSource(firstTimeline); + Timeline secondTimeline = new FakeTimeline(/* windowCount= */ 1); + MediaSource secondMediaSource = new FakeMediaSource(secondTimeline); + int[] playbackStates = new int[4]; + int[] timelineWindowCounts = new int[4]; + int[] maskingPlaybackState = {C.INDEX_UNSET}; + ActionSchedule actionSchedule = + new ActionSchedule.Builder(TAG) + .waitForTimelineChanged(dummyTimeline, Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED) + .executeRunnable( + new PlaybackStateCollector(/* index= */ 0, playbackStates, timelineWindowCounts)) + .clearMediaItems() + .executeRunnable( + new PlaybackStateCollector(/* index= */ 1, playbackStates, timelineWindowCounts)) + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + player.setMediaSource(firstMediaSource, /* startPositionMs= */ 1000); + maskingPlaybackState[0] = player.getPlaybackState(); + } + }) + .executeRunnable( + new PlaybackStateCollector(/* index= */ 2, playbackStates, timelineWindowCounts)) + .addMediaSources(secondMediaSource) + .executeRunnable( + new PlaybackStateCollector(/* index= */ 3, playbackStates, timelineWindowCounts)) + .seek(/* windowIndex= */ 1, /* positionMs= */ 2000) + .prepare() + // The first expected buffering state arrives after prepare but not before. + .waitForPlaybackState(Player.STATE_BUFFERING) + .waitForPlaybackState(Player.STATE_READY) + .waitForPlaybackState(Player.STATE_ENDED) + .build(); + ExoPlayerTestRunner exoPlayerTestRunner = + new ExoPlayerTestRunner.Builder(context) + .setMediaSources(firstMediaSource) + .setActionSchedule(actionSchedule) + .build() + .start(/* doPrepare= */ false) + .blockUntilActionScheduleFinished(TIMEOUT_MS) + .blockUntilEnded(TIMEOUT_MS); + + assertArrayEquals( + new int[] {Player.STATE_IDLE, Player.STATE_IDLE, Player.STATE_IDLE, Player.STATE_IDLE}, + playbackStates); + assertArrayEquals(new int[] {1, 0, 1, 2}, timelineWindowCounts); + exoPlayerTestRunner.assertPlaybackStatesEqual( + Player.STATE_BUFFERING /* first buffering state after prepare */, + Player.STATE_READY, + Player.STATE_ENDED); + exoPlayerTestRunner.assertTimelineChangeReasonsEqual( + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED /* initial setMediaSources */, + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED /* clear */, + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED /* set media items */, + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED /* add media items */, + Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE /* source update after prepare */); + Timeline expectedSecondDummyTimeline = + new FakeTimeline( + TimelineWindowDefinition.createDummy(/* tag= */ 0), + TimelineWindowDefinition.createDummy(/* tag= */ 0)); + Timeline expectedSecondRealTimeline = + new FakeTimeline( + new TimelineWindowDefinition( + /* periodCount= */ 1, + /* id= */ 0, + /* isSeekable= */ true, + /* isDynamic= */ false, + /* durationUs= */ 10_000_000), + new TimelineWindowDefinition( + /* periodCount= */ 1, + /* id= */ 0, + /* isSeekable= */ true, + /* isDynamic= */ false, + /* durationUs= */ 10_000_000)); + exoPlayerTestRunner.assertTimelinesSame( + dummyTimeline, + Timeline.EMPTY, + dummyTimeline, + expectedSecondDummyTimeline, + expectedSecondRealTimeline); + assertArrayEquals(new int[] {Player.STATE_IDLE}, maskingPlaybackState); + } + + @Test + public void modifyPlaylistPrepared_remainsInEnded_needsSeekForBuffering() throws Exception { + Timeline timeline = new FakeTimeline(/* windowCount= */ 1); + FakeMediaSource secondMediaSource = new FakeMediaSource(timeline); + ActionSchedule actionSchedule = + new ActionSchedule.Builder(TAG) + .waitForTimelineChanged(timeline, Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE) + .waitForPlaybackState(Player.STATE_BUFFERING) + .waitForPlaybackState(Player.STATE_READY) + .clearMediaItems() + .waitForPlaybackState(Player.STATE_ENDED) + .addMediaSources(secondMediaSource) // add must not transition to buffering + .waitForTimelineChanged() + .clearMediaItems() // clear must remain in ended + .addMediaSources(secondMediaSource) // add again to be able to test the seek + .waitForTimelineChanged() + .seek(/* positionMs= */ 2_000) // seek must transition to buffering + .waitForPlaybackState(Player.STATE_BUFFERING) + .waitForPlaybackState(Player.STATE_READY) + .waitForPlaybackState(Player.STATE_ENDED) + .build(); + ExoPlayerTestRunner exoPlayerTestRunner = + new ExoPlayerTestRunner.Builder(context) + .setExpectedPlayerEndedCount(/* expectedPlayerEndedCount= */ 2) + .setTimeline(timeline) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilActionScheduleFinished(TIMEOUT_MS) + .blockUntilEnded(TIMEOUT_MS); + + exoPlayerTestRunner.assertPlaybackStatesEqual( + Player.STATE_BUFFERING, // first buffering + Player.STATE_READY, + Player.STATE_ENDED, // clear playlist + Player.STATE_BUFFERING, // second buffering after seek + Player.STATE_READY, + Player.STATE_ENDED); + exoPlayerTestRunner.assertTimelinesSame( + dummyTimeline, + timeline, + Timeline.EMPTY, + dummyTimeline, + timeline, + Timeline.EMPTY, + dummyTimeline, + timeline); + exoPlayerTestRunner.assertTimelineChangeReasonsEqual( + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED /* media item set (masked timeline) */, + Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE /* source prepared */, + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED /* playlist cleared */, + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED /* media items added */, + Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE /* source prepared */, + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED /* playlist cleared */, + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED /* media items added */, + Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE /* source prepared */); + } + + @Test + public void stopWithNoReset_modifyingPlaylistRemainsInIdleState_needsPrepareForBuffering() + throws Exception { + Timeline timeline = new FakeTimeline(/* windowCount= */ 1); + FakeMediaSource secondMediaSource = new FakeMediaSource(timeline); + int[] playbackStateHolder = new int[3]; + int[] windowCountHolder = new int[3]; + ActionSchedule actionSchedule = + new ActionSchedule.Builder(TAG) + .waitForPlaybackState(Player.STATE_READY) + .stop(/* reset= */ false) + .executeRunnable( + new PlaybackStateCollector(/* index= */ 0, playbackStateHolder, windowCountHolder)) + .clearMediaItems() + .executeRunnable( + new PlaybackStateCollector(/* index= */ 1, playbackStateHolder, windowCountHolder)) + .addMediaSources(secondMediaSource) + .executeRunnable( + new PlaybackStateCollector(/* index= */ 2, playbackStateHolder, windowCountHolder)) + .prepare() + .waitForPlaybackState(Player.STATE_BUFFERING) + .waitForPlaybackState(Player.STATE_READY) + .waitForPlaybackState(Player.STATE_ENDED) + .build(); + ExoPlayerTestRunner exoPlayerTestRunner = + new ExoPlayerTestRunner.Builder(context) + .setTimeline(timeline) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilActionScheduleFinished(TIMEOUT_MS) + .blockUntilEnded(TIMEOUT_MS); + + assertArrayEquals( + new int[] {Player.STATE_IDLE, Player.STATE_IDLE, Player.STATE_IDLE}, playbackStateHolder); + assertArrayEquals(new int[] {1, 0, 1}, windowCountHolder); + exoPlayerTestRunner.assertPlaybackStatesEqual( + Player.STATE_BUFFERING, // first buffering + Player.STATE_READY, + Player.STATE_IDLE, // stop + Player.STATE_BUFFERING, + Player.STATE_READY, + Player.STATE_ENDED); + exoPlayerTestRunner.assertTimelinesSame( + dummyTimeline, timeline, Timeline.EMPTY, dummyTimeline, timeline); + exoPlayerTestRunner.assertTimelineChangeReasonsEqual( + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED /* media item set (masked timeline) */, + Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE, /* source prepared */ + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED /* clear media items */, + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED /* media item add (masked timeline) */, + Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE /* source prepared */); + } + + @Test + public void prepareWithInvalidInitialSeek_expectEndedImmediately() throws Exception { + final int[] currentWindowIndices = {C.INDEX_UNSET}; + ActionSchedule actionSchedule = + new ActionSchedule.Builder(TAG) + .waitForPlaybackState(Player.STATE_ENDED) + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + currentWindowIndices[0] = player.getCurrentWindowIndex(); + } + }) + .prepare() + .build(); + ExoPlayerTestRunner exoPlayerTestRunner = + new ExoPlayerTestRunner.Builder(context) + .skipSettingMediaSources() + .initialSeek(/* windowIndex= */ 1, C.TIME_UNSET) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilActionScheduleFinished(TIMEOUT_MS) + .blockUntilEnded(TIMEOUT_MS); + + exoPlayerTestRunner.assertPlaybackStatesEqual(Player.STATE_ENDED); + exoPlayerTestRunner.assertTimelinesSame(); + exoPlayerTestRunner.assertTimelineChangeReasonsEqual(); + assertArrayEquals(new int[] {1}, currentWindowIndices); + } + + @Test + public void prepareWhenAlreadyPreparedIsANoop() throws Exception { + Timeline timeline = new FakeTimeline(/* windowCount= */ 1); + ActionSchedule actionSchedule = + new ActionSchedule.Builder(TAG).waitForPlaybackState(Player.STATE_READY).prepare().build(); + ExoPlayerTestRunner exoPlayerTestRunner = + new ExoPlayerTestRunner.Builder(context) + .setTimeline(timeline) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilActionScheduleFinished(TIMEOUT_MS) + .blockUntilEnded(TIMEOUT_MS); + + exoPlayerTestRunner.assertPlaybackStatesEqual( + Player.STATE_BUFFERING, Player.STATE_READY, Player.STATE_ENDED); + exoPlayerTestRunner.assertTimelinesSame(dummyTimeline, timeline); + exoPlayerTestRunner.assertTimelineChangeReasonsEqual( + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED /* media item set (masked timeline) */, + Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE /* source prepared */); + } + + @Test + public void seekToIndexLargerThanNumberOfPlaylistItems() throws Exception { + Timeline fakeTimeline = + new FakeTimeline( + new TimelineWindowDefinition( + /* isSeekable= */ true, /* isDynamic= */ false, /* durationUs= */ 10_000)); + ConcatenatingMediaSource concatenatingMediaSource = + new ConcatenatingMediaSource( + /* isAtomic= */ false, + new FakeMediaSource(fakeTimeline, ExoPlayerTestRunner.VIDEO_FORMAT), + new FakeMediaSource(fakeTimeline, ExoPlayerTestRunner.VIDEO_FORMAT)); + int[] currentWindowIndices = new int[1]; + long[] currentPlaybackPositions = new long[1]; + int seekToWindowIndex = 1; + ActionSchedule actionSchedule = + new ActionSchedule.Builder(TAG) + .waitForPlaybackState(Player.STATE_BUFFERING) + .waitForTimelineChanged() + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + currentWindowIndices[0] = player.getCurrentWindowIndex(); + currentPlaybackPositions[0] = player.getCurrentPosition(); + } + }) + .build(); + new ExoPlayerTestRunner.Builder(context) + .setMediaSources(concatenatingMediaSource) + .initialSeek(seekToWindowIndex, 5000) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilActionScheduleFinished(TIMEOUT_MS) + .blockUntilEnded(TIMEOUT_MS); + assertArrayEquals(new long[] {5_000}, currentPlaybackPositions); + assertArrayEquals(new int[] {seekToWindowIndex}, currentWindowIndices); + } + + @Test + public void seekToIndexWithEmptyMultiWindowMediaSource() throws Exception { + Timeline fakeTimeline = + new FakeTimeline( + new TimelineWindowDefinition( + /* isSeekable= */ true, /* isDynamic= */ false, /* durationUs= */ 10_000)); + ConcatenatingMediaSource concatenatingMediaSource = + new ConcatenatingMediaSource(/* isAtomic= */ false); + int[] currentWindowIndices = new int[2]; + long[] currentPlaybackPositions = new long[2]; + long[] windowCounts = new long[2]; + int seekToWindowIndex = 1; + ActionSchedule actionSchedule = + new ActionSchedule.Builder(TAG) + .waitForPlaybackState(Player.STATE_ENDED) + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + currentWindowIndices[0] = player.getCurrentWindowIndex(); + currentPlaybackPositions[0] = player.getCurrentPosition(); + windowCounts[0] = player.getCurrentTimeline().getWindowCount(); + } + }) + .executeRunnable( + () -> + concatenatingMediaSource.addMediaSources( + Arrays.asList( + new FakeMediaSource(fakeTimeline, ExoPlayerTestRunner.VIDEO_FORMAT), + new FakeMediaSource(fakeTimeline, ExoPlayerTestRunner.VIDEO_FORMAT)))) + .waitForTimelineChanged() + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + currentWindowIndices[1] = player.getCurrentWindowIndex(); + currentPlaybackPositions[1] = player.getCurrentPosition(); + windowCounts[1] = player.getCurrentTimeline().getWindowCount(); + } + }) + .build(); + new ExoPlayerTestRunner.Builder(context) + .setMediaSources(concatenatingMediaSource) + .initialSeek(seekToWindowIndex, 5000) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilActionScheduleFinished(TIMEOUT_MS) + .blockUntilEnded(TIMEOUT_MS); + assertArrayEquals(new long[] {0, 2}, windowCounts); + assertArrayEquals(new int[] {seekToWindowIndex, seekToWindowIndex}, currentWindowIndices); + assertArrayEquals(new long[] {5_000, 5_000}, currentPlaybackPositions); + } + + @Test + public void emptyMultiWindowMediaSource_doesNotEnterBufferState() throws Exception { + ConcatenatingMediaSource concatenatingMediaSource = + new ConcatenatingMediaSource(/* isAtomic= */ false); + ActionSchedule actionSchedule = + new ActionSchedule.Builder(TAG).waitForPlaybackState(Player.STATE_ENDED).build(); + ExoPlayerTestRunner exoPlayerTestRunner = + new ExoPlayerTestRunner.Builder(context) + .setMediaSources(concatenatingMediaSource) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilActionScheduleFinished(TIMEOUT_MS) + .blockUntilEnded(TIMEOUT_MS); + exoPlayerTestRunner.assertPlaybackStatesEqual(Player.STATE_ENDED); + } + + @Test + public void seekToIndexWithEmptyMultiWindowMediaSource_usesLazyPreparation() throws Exception { + Timeline fakeTimeline = + new FakeTimeline( + new TimelineWindowDefinition( + /* isSeekable= */ true, /* isDynamic= */ false, /* durationUs= */ 10_000)); + ConcatenatingMediaSource concatenatingMediaSource = + new ConcatenatingMediaSource(/* isAtomic= */ false); + int[] currentWindowIndices = new int[2]; + long[] currentPlaybackPositions = new long[2]; + long[] windowCounts = new long[2]; + int seekToWindowIndex = 1; + ActionSchedule actionSchedule = + new ActionSchedule.Builder(TAG) + .waitForPlaybackState(Player.STATE_ENDED) + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + currentWindowIndices[0] = player.getCurrentWindowIndex(); + currentPlaybackPositions[0] = player.getCurrentPosition(); + windowCounts[0] = player.getCurrentTimeline().getWindowCount(); + } + }) + .executeRunnable( + () -> + concatenatingMediaSource.addMediaSources( + Arrays.asList( + new FakeMediaSource(fakeTimeline, ExoPlayerTestRunner.VIDEO_FORMAT), + new FakeMediaSource(fakeTimeline, ExoPlayerTestRunner.VIDEO_FORMAT)))) + .waitForTimelineChanged() + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + currentWindowIndices[1] = player.getCurrentWindowIndex(); + currentPlaybackPositions[1] = player.getCurrentPosition(); + windowCounts[1] = player.getCurrentTimeline().getWindowCount(); + } + }) + .build(); + new ExoPlayerTestRunner.Builder(context) + .setMediaSources(concatenatingMediaSource) + .setUseLazyPreparation(/* useLazyPreparation= */ true) + .initialSeek(seekToWindowIndex, 5000) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilActionScheduleFinished(TIMEOUT_MS) + .blockUntilEnded(TIMEOUT_MS); + assertArrayEquals(new long[] {0, 2}, windowCounts); + assertArrayEquals(new int[] {seekToWindowIndex, seekToWindowIndex}, currentWindowIndices); + assertArrayEquals(new long[] {5_000, 5_000}, currentPlaybackPositions); + } + + @Test + public void + timelineUpdateInMultiWindowMediaSource_removingPeriod_withUnpreparedMaskingMediaPeriod_doesNotThrow() + throws Exception { + TimelineWindowDefinition window1 = + new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ 1); + TimelineWindowDefinition window2 = + new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ 2); + FakeMediaSource mediaSource = new FakeMediaSource(/* timeline= */ null); + ActionSchedule actionSchedule = + new ActionSchedule.Builder(TAG) + .waitForPlaybackState(Player.STATE_BUFFERING) + // Wait so that the player can create its unprepared MaskingMediaPeriod. + .waitForPendingPlayerCommands() + // Let the player assign the unprepared period to window1. + .executeRunnable(() -> mediaSource.setNewSourceInfo(new FakeTimeline(window1, window2))) + .waitForTimelineChanged() + // Remove window1 and assume the update is handled without throwing. + .executeRunnable(() -> mediaSource.setNewSourceInfo(new FakeTimeline(window2))) + .waitForTimelineChanged() + .stop() + .build(); + new ExoPlayerTestRunner.Builder(context) + .setMediaSources(mediaSource) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilActionScheduleFinished(TIMEOUT_MS) + .blockUntilEnded(TIMEOUT_MS); + + // Assertion is to not throw while running the action schedule above. + } + + @Test + public void setPlayWhenReady_keepsCurrentPosition() throws Exception { + AtomicLong positionAfterSetPlayWhenReady = new AtomicLong(C.TIME_UNSET); + ActionSchedule actionSchedule = + new ActionSchedule.Builder(TAG) + .playUntilPosition(0, 5000) + .play() + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + positionAfterSetPlayWhenReady.set(player.getCurrentPosition()); + } + }) + .build(); + new ExoPlayerTestRunner.Builder(context) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilEnded(TIMEOUT_MS); + + assertThat(positionAfterSetPlayWhenReady.get()).isAtLeast(5000); + } + + @Test + public void setShuffleOrder_keepsCurrentPosition() throws Exception { + AtomicLong positionAfterSetShuffleOrder = new AtomicLong(C.TIME_UNSET); + ActionSchedule actionSchedule = + new ActionSchedule.Builder(TAG) + .playUntilPosition(0, 5000) + .setShuffleOrder(new FakeShuffleOrder(/* length= */ 1)) + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + positionAfterSetShuffleOrder.set(player.getCurrentPosition()); + } + }) + .play() + .build(); + new ExoPlayerTestRunner.Builder(context) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilEnded(TIMEOUT_MS); + + assertThat(positionAfterSetShuffleOrder.get()).isAtLeast(5000); + } + + @Test + public void setMediaSources_secondAdMediaSource_throws() throws Exception { + AdsMediaSource adsMediaSource = + new AdsMediaSource( + new FakeMediaSource(new FakeTimeline(/* windowCount= */ 1)), + new DefaultDataSourceFactory( + context, Util.getUserAgent(context, ExoPlayerLibraryInfo.VERSION_SLASHY)), + new DummyAdsLoader(), + new DummyAdViewProvider()); + Exception[] exception = {null}; + ActionSchedule actionSchedule = + new ActionSchedule.Builder(TAG) + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + try { + player.setMediaSource(adsMediaSource); + player.addMediaSource(adsMediaSource); + } catch (Exception e) { + exception[0] = e; + } + player.prepare(); + } + }) + .build(); + + new ExoPlayerTestRunner.Builder(context) + .setActionSchedule(actionSchedule) + .build() + .start(/* doPrepare= */ false) + .blockUntilActionScheduleFinished(TIMEOUT_MS) + .blockUntilEnded(TIMEOUT_MS); + + assertThat(exception[0]).isInstanceOf(IllegalStateException.class); + } + + @Test + public void setMediaSources_multipleMediaSourcesWithAd_throws() throws Exception { + MediaSource mediaSource = new FakeMediaSource(new FakeTimeline(/* windowCount= */ 1)); + AdsMediaSource adsMediaSource = + new AdsMediaSource( + mediaSource, + new DefaultDataSourceFactory( + context, Util.getUserAgent(context, ExoPlayerLibraryInfo.VERSION_SLASHY)), + new DummyAdsLoader(), + new DummyAdViewProvider()); + final Exception[] exception = {null}; + ActionSchedule actionSchedule = + new ActionSchedule.Builder(TAG) + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + try { + List sources = new ArrayList<>(); + sources.add(mediaSource); + sources.add(adsMediaSource); + player.setMediaSources(sources); + } catch (Exception e) { + exception[0] = e; + } + player.prepare(); + } + }) + .build(); + + new ExoPlayerTestRunner.Builder(context) + .setActionSchedule(actionSchedule) + .build() + .start(/* doPrepare= */ false) + .blockUntilActionScheduleFinished(TIMEOUT_MS) + .blockUntilEnded(TIMEOUT_MS); + + assertThat(exception[0]).isInstanceOf(IllegalArgumentException.class); + } + + @Test + public void setMediaSources_addingMediaSourcesWithAdToNonEmptyPlaylist_throws() throws Exception { + MediaSource mediaSource = new FakeMediaSource(new FakeTimeline(/* windowCount= */ 1)); + AdsMediaSource adsMediaSource = + new AdsMediaSource( + mediaSource, + new DefaultDataSourceFactory( + context, Util.getUserAgent(context, ExoPlayerLibraryInfo.VERSION_SLASHY)), + new DummyAdsLoader(), + new DummyAdViewProvider()); + final Exception[] exception = {null}; + ActionSchedule actionSchedule = + new ActionSchedule.Builder(TAG) + .waitForPlaybackState(Player.STATE_READY) + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + try { + player.addMediaSource(adsMediaSource); + } catch (Exception e) { + exception[0] = e; + } + } + }) + .build(); + + new ExoPlayerTestRunner.Builder(context) + .setMediaSources(mediaSource) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilActionScheduleFinished(TIMEOUT_MS) + .blockUntilEnded(TIMEOUT_MS); + + assertThat(exception[0]).isInstanceOf(IllegalArgumentException.class); + } + + @Test + public void setMediaSources_empty_whenEmpty_correctMaskingWindowIndex() throws Exception { + Timeline secondTimeline = new FakeTimeline(/* windowCount= */ 1); + MediaSource secondMediaSource = new FakeMediaSource(secondTimeline); + final int[] currentWindowIndices = {C.INDEX_UNSET, C.INDEX_UNSET, C.INDEX_UNSET}; + ActionSchedule actionSchedule = + new ActionSchedule.Builder(TAG) + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + currentWindowIndices[0] = player.getCurrentWindowIndex(); + List listOfTwo = new ArrayList<>(); + listOfTwo.add(secondMediaSource); + listOfTwo.add(secondMediaSource); + player.addMediaSources(/* index= */ 0, listOfTwo); + currentWindowIndices[1] = player.getCurrentWindowIndex(); + } + }) + .prepare() + .waitForTimelineChanged() + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + currentWindowIndices[2] = player.getCurrentWindowIndex(); + } + }) + .build(); + new ExoPlayerTestRunner.Builder(context) + .setMediaSources(new ConcatenatingMediaSource()) + .setActionSchedule(actionSchedule) + .build() + .start(/* doPrepare= */ false) + .blockUntilActionScheduleFinished(TIMEOUT_MS) + .blockUntilEnded(TIMEOUT_MS); + assertArrayEquals(new int[] {0, 0, 0}, currentWindowIndices); + } + + @Test + public void setMediaSources_empty_whenEmpty_validInitialSeek_correctMaskingWindowIndex() + throws Exception { + Timeline secondTimeline = new FakeTimeline(/* windowCount= */ 1); + MediaSource secondMediaSource = new FakeMediaSource(secondTimeline); + final int[] currentWindowIndices = {C.INDEX_UNSET, C.INDEX_UNSET, C.INDEX_UNSET}; + ActionSchedule actionSchedule = + new ActionSchedule.Builder(TAG) + // Wait for initial seek to be fully handled by internal player. + .waitForPositionDiscontinuity() + .waitForPendingPlayerCommands() + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + currentWindowIndices[0] = player.getCurrentWindowIndex(); + List listOfTwo = new ArrayList<>(); + listOfTwo.add(secondMediaSource); + listOfTwo.add(secondMediaSource); + player.addMediaSources(/* index= */ 0, listOfTwo); + currentWindowIndices[1] = player.getCurrentWindowIndex(); + } + }) + .prepare() + .waitForTimelineChanged() + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + currentWindowIndices[2] = player.getCurrentWindowIndex(); + } + }) + .build(); + new ExoPlayerTestRunner.Builder(context) + .initialSeek(/* windowIndex= */ 1, C.TIME_UNSET) + .setMediaSources(new ConcatenatingMediaSource()) + .setActionSchedule(actionSchedule) + .build() + .start(/* doPrepare= */ false) + .blockUntilActionScheduleFinished(TIMEOUT_MS) + .blockUntilEnded(TIMEOUT_MS); + assertArrayEquals(new int[] {1, 1, 1}, currentWindowIndices); + } + + @Test + public void setMediaSources_empty_whenEmpty_invalidInitialSeek_correctMaskingWindowIndex() + throws Exception { + Timeline secondTimeline = new FakeTimeline(/* windowCount= */ 1); + MediaSource secondMediaSource = new FakeMediaSource(secondTimeline); + final int[] currentWindowIndices = {C.INDEX_UNSET, C.INDEX_UNSET, C.INDEX_UNSET}; + ActionSchedule actionSchedule = + new ActionSchedule.Builder(TAG) + // Wait for initial seek to be fully handled by internal player. + .waitForPositionDiscontinuity() + .waitForPendingPlayerCommands() + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + currentWindowIndices[0] = player.getCurrentWindowIndex(); + List listOfTwo = new ArrayList<>(); + listOfTwo.add(secondMediaSource); + listOfTwo.add(secondMediaSource); + player.addMediaSources(/* index= */ 0, listOfTwo); + currentWindowIndices[1] = player.getCurrentWindowIndex(); + } + }) + .prepare() + .waitForTimelineChanged() + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + currentWindowIndices[2] = player.getCurrentWindowIndex(); + } + }) + .build(); + new ExoPlayerTestRunner.Builder(context) + .initialSeek(/* windowIndex= */ 4, C.TIME_UNSET) + .setMediaSources(new ConcatenatingMediaSource()) + .setActionSchedule(actionSchedule) + .build() + .start(/* doPrepare= */ false) + .blockUntilActionScheduleFinished(TIMEOUT_MS) + .blockUntilEnded(TIMEOUT_MS); + assertArrayEquals(new int[] {4, 0, 0}, currentWindowIndices); + } + + @Test + public void setMediaSources_whenEmpty_correctMaskingWindowIndex() throws Exception { + Timeline firstTimeline = new FakeTimeline(/* windowCount= */ 1); + MediaSource firstMediaSource = new FakeMediaSource(firstTimeline); + Timeline secondTimeline = new FakeTimeline(/* windowCount= */ 1); + MediaSource secondMediaSource = new FakeMediaSource(secondTimeline); + final int[] currentWindowIndices = {C.INDEX_UNSET, C.INDEX_UNSET, C.INDEX_UNSET, C.INDEX_UNSET}; + ActionSchedule actionSchedule = + new ActionSchedule.Builder(TAG) + .waitForPlaybackState(Player.STATE_READY) + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + // Increase current window index. + player.addMediaSource(/* index= */ 0, secondMediaSource); + currentWindowIndices[0] = player.getCurrentWindowIndex(); + } + }) + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + // Current window index is unchanged. + player.addMediaSource(/* index= */ 2, secondMediaSource); + currentWindowIndices[1] = player.getCurrentWindowIndex(); + } + }) + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + MediaSource mediaSource = + new FakeMediaSource(new FakeTimeline(/* windowCount= */ 1)); + ConcatenatingMediaSource concatenatingMediaSource = + new ConcatenatingMediaSource(mediaSource, mediaSource, mediaSource); + // Increase current window with multi window source. + player.addMediaSource(/* index= */ 0, concatenatingMediaSource); + currentWindowIndices[2] = player.getCurrentWindowIndex(); + } + }) + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + ConcatenatingMediaSource concatenatingMediaSource = + new ConcatenatingMediaSource(); + // Current window index is unchanged when adding empty source. + player.addMediaSource(/* index= */ 0, concatenatingMediaSource); + currentWindowIndices[3] = player.getCurrentWindowIndex(); + } + }) + .build(); + new ExoPlayerTestRunner.Builder(context) + .setMediaSources(firstMediaSource) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilActionScheduleFinished(TIMEOUT_MS) + .blockUntilEnded(TIMEOUT_MS); + assertArrayEquals(new int[] {1, 1, 4, 4}, currentWindowIndices); + } + + @Test + public void setMediaSources_whenEmpty_validInitialSeek_correctMaskingWindowIndex() + throws Exception { + Timeline firstTimeline = new FakeTimeline(/* windowCount= */ 2); + MediaSource firstMediaSource = new FakeMediaSource(firstTimeline); + Timeline secondTimeline = new FakeTimeline(/* windowCount= */ 1, new Object()); + MediaSource secondMediaSource = new FakeMediaSource(secondTimeline); + final int[] currentWindowIndices = {C.INDEX_UNSET, C.INDEX_UNSET, C.INDEX_UNSET}; + ActionSchedule actionSchedule = + new ActionSchedule.Builder(TAG) + // Wait for initial seek to be fully handled by internal player. + .waitForPositionDiscontinuity() + .waitForPendingPlayerCommands() + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + currentWindowIndices[0] = player.getCurrentWindowIndex(); + // Increase current window index. + player.addMediaSource(/* index= */ 0, secondMediaSource); + currentWindowIndices[1] = player.getCurrentWindowIndex(); + } + }) + .prepare() + .waitForTimelineChanged() + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + currentWindowIndices[2] = player.getCurrentWindowIndex(); + } + }) + .build(); + new ExoPlayerTestRunner.Builder(context) + .initialSeek(/* windowIndex= */ 1, C.TIME_UNSET) + .setMediaSources(firstMediaSource) + .setActionSchedule(actionSchedule) + .build() + .start(/* doPrepare= */ false) + .blockUntilActionScheduleFinished(TIMEOUT_MS) + .blockUntilEnded(TIMEOUT_MS); + assertArrayEquals(new int[] {1, 2, 2}, currentWindowIndices); + } + + @Test + public void setMediaSources_whenEmpty_invalidInitialSeek_correctMaskingWindowIndex() + throws Exception { + Timeline firstTimeline = new FakeTimeline(/* windowCount= */ 1); + MediaSource firstMediaSource = new FakeMediaSource(firstTimeline); + Timeline secondTimeline = new FakeTimeline(/* windowCount= */ 1, new Object()); + MediaSource secondMediaSource = new FakeMediaSource(secondTimeline); + final int[] currentWindowIndices = {C.INDEX_UNSET, C.INDEX_UNSET, C.INDEX_UNSET}; + ActionSchedule actionSchedule = + new ActionSchedule.Builder(TAG) + // Wait for initial seek to be fully handled by internal player. + .waitForPositionDiscontinuity() + .waitForPendingPlayerCommands() + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + currentWindowIndices[0] = player.getCurrentWindowIndex(); + // Increase current window index. + player.addMediaSource(/* index= */ 0, secondMediaSource); + currentWindowIndices[1] = player.getCurrentWindowIndex(); + } + }) + .prepare() + .waitForTimelineChanged() + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + currentWindowIndices[2] = player.getCurrentWindowIndex(); + } + }) + .waitForPlaybackState(Player.STATE_ENDED) + .build(); + new ExoPlayerTestRunner.Builder(context) + .initialSeek(/* windowIndex= */ 1, C.TIME_UNSET) + .setMediaSources(firstMediaSource) + .setActionSchedule(actionSchedule) + .build() + .start(/* doPrepare= */ false) + .blockUntilActionScheduleFinished(TIMEOUT_MS) + .blockUntilEnded(TIMEOUT_MS); + assertArrayEquals(new int[] {0, 1, 1}, currentWindowIndices); + } + + @Test + public void setMediaSources_correctMaskingWindowIndex() throws Exception { + Timeline firstTimeline = new FakeTimeline(/* windowCount= */ 1); + MediaSource firstMediaSource = new FakeMediaSource(firstTimeline); + Timeline secondTimeline = new FakeTimeline(/* windowCount= */ 1, new Object()); + MediaSource secondMediaSource = new FakeMediaSource(secondTimeline); + final int[] currentWindowIndices = {C.INDEX_UNSET, C.INDEX_UNSET, C.INDEX_UNSET}; + ActionSchedule actionSchedule = + new ActionSchedule.Builder(TAG) + .waitForTimelineChanged() + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + currentWindowIndices[0] = player.getCurrentWindowIndex(); + // Increase current window index. + player.addMediaSource(/* index= */ 0, secondMediaSource); + currentWindowIndices[1] = player.getCurrentWindowIndex(); + } + }) + .waitForTimelineChanged() + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + currentWindowIndices[2] = player.getCurrentWindowIndex(); + } + }) + .build(); + new ExoPlayerTestRunner.Builder(context) + .setMediaSources(firstMediaSource) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilActionScheduleFinished(TIMEOUT_MS) + .blockUntilEnded(TIMEOUT_MS); + assertArrayEquals(new int[] {0, 1, 1}, currentWindowIndices); + } + + @Test + public void setMediaSources_whenIdle_correctMaskingPlaybackState() throws Exception { + Timeline firstTimeline = new FakeTimeline(/* windowCount= */ 1, 1L); + MediaSource firstMediaSource = new FakeMediaSource(firstTimeline); + Timeline secondTimeline = new FakeTimeline(/* windowCount= */ 1, 2L); + MediaSource secondMediaSource = new FakeMediaSource(secondTimeline); + final int[] maskingPlaybackStates = new int[4]; + Arrays.fill(maskingPlaybackStates, C.INDEX_UNSET); + ActionSchedule actionSchedule = + new ActionSchedule.Builder(TAG) + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + // Set empty media item with no seek. + player.setMediaSource(new ConcatenatingMediaSource()); + maskingPlaybackStates[0] = player.getPlaybackState(); + } + }) + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + // Set media item with an implicit seek to the current position. + player.setMediaSource(firstMediaSource); + maskingPlaybackStates[1] = player.getPlaybackState(); + } + }) + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + // Set media item with an explicit seek. + player.setMediaSource(secondMediaSource, /* startPositionMs= */ C.TIME_UNSET); + maskingPlaybackStates[2] = player.getPlaybackState(); + } + }) + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + // Set empty media item with an explicit seek. + player.setMediaSource( + new ConcatenatingMediaSource(), /* startPositionMs= */ C.TIME_UNSET); + maskingPlaybackStates[3] = player.getPlaybackState(); + } + }) + .prepare() + .build(); + ExoPlayerTestRunner exoPlayerTestRunner = + new ExoPlayerTestRunner.Builder(context) + .skipSettingMediaSources() + .setActionSchedule(actionSchedule) + .build() + .start(/* doPrepare= */ false) + .blockUntilActionScheduleFinished(TIMEOUT_MS) + .blockUntilEnded(TIMEOUT_MS); + // Expect reset of masking to first window. + exoPlayerTestRunner.assertPlaybackStatesEqual(Player.STATE_ENDED); + assertArrayEquals( + new int[] {Player.STATE_IDLE, Player.STATE_IDLE, Player.STATE_IDLE, Player.STATE_IDLE}, + maskingPlaybackStates); + exoPlayerTestRunner.assertTimelineChangeReasonsEqual( + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED); + } + + @Test + public void setMediaSources_whenIdle_invalidSeek_correctMaskingPlaybackState() throws Exception { + final int[] maskingPlaybackStates = new int[1]; + Arrays.fill(maskingPlaybackStates, C.INDEX_UNSET); + ActionSchedule actionSchedule = + new ActionSchedule.Builder(TAG) + // Wait for initial seek to be fully handled by internal player. + .waitForPositionDiscontinuity() + .waitForPendingPlayerCommands() + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + // Set a media item with an implicit seek to the current position which is + // invalid in the new timeline. + player.setMediaSource( + new FakeMediaSource(new FakeTimeline(/* windowCount= */ 1, 1L))); + maskingPlaybackStates[0] = player.getPlaybackState(); + } + }) + .prepare() + .waitForPlaybackState(Player.STATE_READY) + .build(); + ExoPlayerTestRunner exoPlayerTestRunner = + new ExoPlayerTestRunner.Builder(context) + .initialSeek(/* windowIndex= */ 1, /* positionMs= */ C.TIME_UNSET) + .setMediaSources(new ConcatenatingMediaSource()) + .setActionSchedule(actionSchedule) + .build() + .start(/* doPrepare= */ false) + .blockUntilActionScheduleFinished(TIMEOUT_MS) + .blockUntilEnded(TIMEOUT_MS); + // Expect reset of masking to first window. + exoPlayerTestRunner.assertPlaybackStatesEqual( + Player.STATE_BUFFERING, Player.STATE_READY, Player.STATE_ENDED); + assertArrayEquals(new int[] {Player.STATE_IDLE}, maskingPlaybackStates); + exoPlayerTestRunner.assertTimelineChangeReasonsEqual( + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, + Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE); + } + + @Test + public void setMediaSources_whenIdle_noSeek_correctMaskingPlaybackState() throws Exception { + final int[] maskingPlaybackStates = new int[1]; + Arrays.fill(maskingPlaybackStates, C.INDEX_UNSET); + ActionSchedule actionSchedule = + new ActionSchedule.Builder(TAG) + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + // Set media item with no seek. + player.setMediaSource( + new FakeMediaSource(new FakeTimeline(/* windowCount= */ 1))); + maskingPlaybackStates[0] = player.getPlaybackState(); + } + }) + .prepare() + .waitForPlaybackState(Player.STATE_READY) + .build(); + ExoPlayerTestRunner exoPlayerTestRunner = + new ExoPlayerTestRunner.Builder(context) + .skipSettingMediaSources() + .setActionSchedule(actionSchedule) + .build() + .start(/* doPrepare= */ false) + .blockUntilActionScheduleFinished(TIMEOUT_MS) + .blockUntilEnded(TIMEOUT_MS); + // Expect reset of masking to first window. + exoPlayerTestRunner.assertPlaybackStatesEqual( + Player.STATE_BUFFERING, Player.STATE_READY, Player.STATE_ENDED); + assertArrayEquals(new int[] {Player.STATE_IDLE}, maskingPlaybackStates); + exoPlayerTestRunner.assertTimelineChangeReasonsEqual( + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, + Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE); + } + + @Test + public void setMediaSources_whenIdle_noSeekEmpty_correctMaskingPlaybackState() throws Exception { + final int[] maskingPlaybackStates = new int[1]; + Arrays.fill(maskingPlaybackStates, C.INDEX_UNSET); + ActionSchedule actionSchedule = + new ActionSchedule.Builder(TAG) + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + // Set an empty media item with no seek. + player.setMediaSource(new ConcatenatingMediaSource()); + maskingPlaybackStates[0] = player.getPlaybackState(); + } + }) + .setMediaSources(new FakeMediaSource(new FakeTimeline(/* windowCount= */ 1))) + .prepare() + .waitForPlaybackState(Player.STATE_READY) + .build(); + ExoPlayerTestRunner exoPlayerTestRunner = + new ExoPlayerTestRunner.Builder(context) + .skipSettingMediaSources() + .setActionSchedule(actionSchedule) + .build() + .start(/* doPrepare= */ false) + .blockUntilActionScheduleFinished(TIMEOUT_MS) + .blockUntilEnded(TIMEOUT_MS); + // Expect reset of masking to first window. + exoPlayerTestRunner.assertPlaybackStatesEqual( + Player.STATE_BUFFERING, Player.STATE_READY, Player.STATE_ENDED); + assertArrayEquals(new int[] {Player.STATE_IDLE}, maskingPlaybackStates); + exoPlayerTestRunner.assertTimelineChangeReasonsEqual( + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, + Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE); + } + + @Test + public void setMediaSources_whenEnded_correctMaskingPlaybackState() throws Exception { + Timeline firstTimeline = new FakeTimeline(/* windowCount= */ 1, 1L); + MediaSource firstMediaSource = new FakeMediaSource(firstTimeline); + Timeline secondTimeline = new FakeTimeline(/* windowCount= */ 1, 2L); + MediaSource secondMediaSource = new FakeMediaSource(secondTimeline); + final int[] maskingPlaybackStates = new int[4]; + Arrays.fill(maskingPlaybackStates, C.INDEX_UNSET); + ActionSchedule actionSchedule = + new ActionSchedule.Builder(TAG) + .waitForPlaybackState(Player.STATE_ENDED) + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + // Set empty media item with an implicit seek to the current position. + player.setMediaSource(new ConcatenatingMediaSource()); + maskingPlaybackStates[0] = player.getPlaybackState(); + } + }) + .waitForPlaybackState(Player.STATE_ENDED) + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + // Set empty media item with an explicit seek. + player.setMediaSource( + new ConcatenatingMediaSource(), /* startPositionMs= */ C.TIME_UNSET); + maskingPlaybackStates[1] = player.getPlaybackState(); + } + }) + .waitForPlaybackState(Player.STATE_ENDED) + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + // Set media item with an implicit seek to the current position. + player.setMediaSource(firstMediaSource); + maskingPlaybackStates[2] = player.getPlaybackState(); + } + }) + .waitForPlaybackState(Player.STATE_READY) + .clearMediaItems() + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + // Set media item with an explicit seek. + player.setMediaSource(secondMediaSource, /* startPositionMs= */ C.TIME_UNSET); + maskingPlaybackStates[3] = player.getPlaybackState(); + } + }) + .waitForPlaybackState(Player.STATE_ENDED) + .build(); + ExoPlayerTestRunner exoPlayerTestRunner = + new ExoPlayerTestRunner.Builder(context) + .setExpectedPlayerEndedCount(/* expectedPlayerEndedCount= */ 3) + .setMediaSources(new ConcatenatingMediaSource()) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilActionScheduleFinished(TIMEOUT_MS) + .blockUntilEnded(TIMEOUT_MS); + // Expect reset of masking to first window. + exoPlayerTestRunner.assertPlaybackStatesEqual( + Player.STATE_ENDED, + Player.STATE_BUFFERING, + Player.STATE_READY, + Player.STATE_ENDED, + Player.STATE_BUFFERING, + Player.STATE_READY, + Player.STATE_ENDED); + assertArrayEquals( + new int[] { + Player.STATE_ENDED, Player.STATE_ENDED, Player.STATE_BUFFERING, Player.STATE_BUFFERING + }, + maskingPlaybackStates); + exoPlayerTestRunner.assertTimelineChangeReasonsEqual( + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, + Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE, + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, + Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE); + } + + @Test + public void setMediaSources_whenEnded_invalidSeek_correctMaskingPlaybackState() throws Exception { + final int[] maskingPlaybackStates = new int[1]; + Arrays.fill(maskingPlaybackStates, C.INDEX_UNSET); + ActionSchedule actionSchedule = + new ActionSchedule.Builder(TAG) + .waitForPlaybackState(Player.STATE_ENDED) + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + // Set media item with an invalid implicit seek to the current position. + player.setMediaSource( + new FakeMediaSource(new FakeTimeline(/* windowCount= */ 1, 1L)), + /* resetPosition= */ false); + maskingPlaybackStates[0] = player.getPlaybackState(); + } + }) + .waitForTimelineChanged() + .waitForPlaybackState(Player.STATE_ENDED) + .build(); + ExoPlayerTestRunner exoPlayerTestRunner = + new ExoPlayerTestRunner.Builder(context) + .initialSeek(/* windowIndex= */ 1, /* positionMs= */ C.TIME_UNSET) + .setMediaSources(new ConcatenatingMediaSource()) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilActionScheduleFinished(TIMEOUT_MS) + .blockUntilEnded(TIMEOUT_MS); + // Expect reset of masking to first window. + exoPlayerTestRunner.assertPlaybackStatesEqual(Player.STATE_ENDED); + assertArrayEquals(new int[] {Player.STATE_ENDED}, maskingPlaybackStates); + exoPlayerTestRunner.assertTimelineChangeReasonsEqual( + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, + Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE); + } + + @Test + public void setMediaSources_whenEnded_noSeek_correctMaskingPlaybackState() throws Exception { + final int[] maskingPlaybackStates = new int[1]; + Arrays.fill(maskingPlaybackStates, C.INDEX_UNSET); + ActionSchedule actionSchedule = + new ActionSchedule.Builder(TAG) + .waitForPlaybackState(Player.STATE_READY) + .clearMediaItems() + .waitForPlaybackState(Player.STATE_ENDED) + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + // Set media item with no seek (keep current position). + player.setMediaSource( + new FakeMediaSource(new FakeTimeline(/* windowCount= */ 1)), + /* resetPosition= */ false); + maskingPlaybackStates[0] = player.getPlaybackState(); + } + }) + .waitForTimelineChanged() + .waitForPlaybackState(Player.STATE_ENDED) + .build(); + ExoPlayerTestRunner exoPlayerTestRunner = + new ExoPlayerTestRunner.Builder(context) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilActionScheduleFinished(TIMEOUT_MS) + .blockUntilEnded(TIMEOUT_MS); + // Expect reset of masking to first window. + exoPlayerTestRunner.assertPlaybackStatesEqual( + Player.STATE_BUFFERING, Player.STATE_READY, Player.STATE_ENDED); + assertArrayEquals(new int[] {Player.STATE_ENDED}, maskingPlaybackStates); + exoPlayerTestRunner.assertTimelineChangeReasonsEqual( + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, + Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE, + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, + Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE); + } + + @Test + public void setMediaSources_whenEnded_noSeekEmpty_correctMaskingPlaybackState() throws Exception { + final int[] maskingPlaybackStates = new int[1]; + Arrays.fill(maskingPlaybackStates, C.INDEX_UNSET); + ActionSchedule actionSchedule = + new ActionSchedule.Builder(TAG) + .waitForPlaybackState(Player.STATE_READY) + .clearMediaItems() + .waitForPlaybackState(Player.STATE_ENDED) + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + // Set an empty media item with no seek. + player.setMediaSource(new ConcatenatingMediaSource()); + maskingPlaybackStates[0] = player.getPlaybackState(); + } + }) + .waitForPlaybackState(Player.STATE_ENDED) + .build(); + ExoPlayerTestRunner exoPlayerTestRunner = + new ExoPlayerTestRunner.Builder(context) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilActionScheduleFinished(TIMEOUT_MS) + .blockUntilEnded(TIMEOUT_MS); + // Expect reset of masking to first window. + exoPlayerTestRunner.assertPlaybackStatesEqual( + Player.STATE_BUFFERING, Player.STATE_READY, Player.STATE_ENDED); + assertArrayEquals(new int[] {Player.STATE_ENDED}, maskingPlaybackStates); + exoPlayerTestRunner.assertTimelineChangeReasonsEqual( + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, + Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE, + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED); + } + + @Test + public void setMediaSources_whenPrepared_correctMaskingPlaybackState() throws Exception { + Timeline firstTimeline = new FakeTimeline(/* windowCount= */ 1, 1L); + MediaSource firstMediaSource = new FakeMediaSource(firstTimeline); + Timeline secondTimeline = new FakeTimeline(/* windowCount= */ 1, 2L); + MediaSource secondMediaSource = new FakeMediaSource(secondTimeline); + final int[] maskingPlaybackStates = new int[4]; + Arrays.fill(maskingPlaybackStates, C.INDEX_UNSET); + ActionSchedule actionSchedule = + new ActionSchedule.Builder(TAG) + .pause() + .waitForPlaybackState(Player.STATE_READY) + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + // Set empty media item with an implicit seek to current position. + player.setMediaSource( + new ConcatenatingMediaSource(), /* resetPosition= */ false); + // Expect masking state is ended, + maskingPlaybackStates[0] = player.getPlaybackState(); + } + }) + .waitForPlaybackState(Player.STATE_ENDED) + .setMediaSources(/* windowIndex= */ 0, /* positionMs= */ C.TIME_UNSET, firstMediaSource) + .waitForPlaybackState(Player.STATE_READY) + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + // Set empty media item with an explicit seek. + player.setMediaSource( + new ConcatenatingMediaSource(), /* startPositionMs= */ C.TIME_UNSET); + // Expect masking state is ended, + maskingPlaybackStates[1] = player.getPlaybackState(); + } + }) + .waitForPlaybackState(Player.STATE_ENDED) + .setMediaSources(/* windowIndex= */ 0, /* positionMs= */ C.TIME_UNSET, firstMediaSource) + .waitForPlaybackState(Player.STATE_READY) + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + // Set media item with an explicit seek. + player.setMediaSource(secondMediaSource, /* startPositionMs= */ C.TIME_UNSET); + // Expect masking state is buffering, + maskingPlaybackStates[2] = player.getPlaybackState(); + } + }) + .waitForPlaybackState(Player.STATE_READY) + .setMediaSources(/* windowIndex= */ 0, /* positionMs= */ C.TIME_UNSET, firstMediaSource) + .waitForPlaybackState(Player.STATE_READY) + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + // Set media item with an implicit seek to the current position. + player.setMediaSource(secondMediaSource, /* resetPosition= */ false); + // Expect masking state is buffering, + maskingPlaybackStates[3] = player.getPlaybackState(); + } + }) + .play() + .waitForPlaybackState(Player.STATE_READY) + .build(); + ExoPlayerTestRunner exoPlayerTestRunner = + new ExoPlayerTestRunner.Builder(context) + .setExpectedPlayerEndedCount(/* expectedPlayerEndedCount= */ 3) + .setMediaSources(firstMediaSource) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilActionScheduleFinished(TIMEOUT_MS) + .blockUntilEnded(TIMEOUT_MS); + // Expect reset of masking to first window. + exoPlayerTestRunner.assertPlaybackStatesEqual( + Player.STATE_BUFFERING, + Player.STATE_READY, // Ready after initial prepare. + Player.STATE_ENDED, // Ended after setting empty source without seek. + Player.STATE_BUFFERING, + Player.STATE_READY, // Ready again after re-setting source. + Player.STATE_ENDED, // Ended after setting empty source with seek. + Player.STATE_BUFFERING, + Player.STATE_READY, // Ready again after re-setting source. + Player.STATE_BUFFERING, + Player.STATE_READY, // Ready after setting media item with seek. + Player.STATE_BUFFERING, + Player.STATE_READY, // Ready again after re-setting source. + Player.STATE_BUFFERING, // Play. + Player.STATE_READY, // Ready after setting media item without seek. + Player.STATE_ENDED); + assertArrayEquals( + new int[] { + Player.STATE_ENDED, Player.STATE_ENDED, Player.STATE_BUFFERING, Player.STATE_BUFFERING + }, + maskingPlaybackStates); + exoPlayerTestRunner.assertTimelineChangeReasonsEqual( + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, + Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE, // Initial source. + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, // Empty source. + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, + Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE, // Reset source. + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, // Empty source. + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, + Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE, // Reset source. + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, + Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE, // Set source with seek. + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, + Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE, // Reset source. + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, + Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE); // Set source without seek. + } + + @Test + public void setMediaSources_whenPrepared_invalidSeek_correctMaskingPlaybackState() + throws Exception { + Timeline firstTimeline = new FakeTimeline(/* windowCount= */ 1, 1L); + MediaSource firstMediaSource = new FakeMediaSource(firstTimeline); + final int[] maskingPlaybackStates = new int[1]; + Arrays.fill(maskingPlaybackStates, C.INDEX_UNSET); + ActionSchedule actionSchedule = + new ActionSchedule.Builder(TAG) + .pause() + .waitForPlaybackState(Player.STATE_ENDED) + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + // An implicit, invalid seek picking up the position set by the initial seek. + player.setMediaSource(firstMediaSource, /* resetPosition= */ false); + // Expect masking state is ended, + maskingPlaybackStates[0] = player.getPlaybackState(); + } + }) + .waitForTimelineChanged() + .setMediaSources(/* windowIndex= */ 0, /* positionMs= */ C.TIME_UNSET, firstMediaSource) + .waitForPlaybackState(Player.STATE_READY) + .play() + .waitForPlaybackState(Player.STATE_ENDED) + .build(); + ExoPlayerTestRunner exoPlayerTestRunner = + new ExoPlayerTestRunner.Builder(context) + .setExpectedPlayerEndedCount(/* expectedPlayerEndedCount= */ 2) + .initialSeek(/* windowIndex= */ 1, /* positionMs= */ C.TIME_UNSET) + .setMediaSources(new ConcatenatingMediaSource()) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilActionScheduleFinished(TIMEOUT_MS) + .blockUntilEnded(TIMEOUT_MS); + // Expect reset of masking to first window. + exoPlayerTestRunner.assertPlaybackStatesEqual( + Player.STATE_ENDED, // Empty source has been prepared. + Player.STATE_BUFFERING, // After setting another source. + Player.STATE_READY, + Player.STATE_ENDED); + assertArrayEquals(new int[] {Player.STATE_ENDED}, maskingPlaybackStates); + exoPlayerTestRunner.assertTimelineChangeReasonsEqual( + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, + Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE, + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, + Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE); + } + + @Test + public void addMediaSources_skipSettingMediaItems_validInitialSeek_correctMaskingWindowIndex() + throws Exception { + final int[] currentWindowIndices = new int[5]; + Arrays.fill(currentWindowIndices, C.INDEX_UNSET); + ActionSchedule actionSchedule = + new ActionSchedule.Builder(TAG) + // Wait for initial seek to be fully handled by internal player. + .waitForPositionDiscontinuity() + .waitForPendingPlayerCommands() + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + currentWindowIndices[0] = player.getCurrentWindowIndex(); + player.addMediaSource(/* index= */ 0, new ConcatenatingMediaSource()); + currentWindowIndices[1] = player.getCurrentWindowIndex(); + player.addMediaSource( + /* index= */ 0, + new FakeMediaSource(new FakeTimeline(/* windowCount= */ 2))); + currentWindowIndices[2] = player.getCurrentWindowIndex(); + player.addMediaSource( + /* index= */ 0, + new FakeMediaSource(new FakeTimeline(/* windowCount= */ 1))); + currentWindowIndices[3] = player.getCurrentWindowIndex(); + } + }) + .prepare() + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + currentWindowIndices[4] = player.getCurrentWindowIndex(); + } + }) + .build(); + new ExoPlayerTestRunner.Builder(context) + .skipSettingMediaSources() + .initialSeek(/* windowIndex= */ 1, C.TIME_UNSET) + .setActionSchedule(actionSchedule) + .build() + .start(/* doPrepare= */ false) + .blockUntilActionScheduleFinished(TIMEOUT_MS) + .blockUntilEnded(TIMEOUT_MS); + assertArrayEquals(new int[] {1, 1, 1, 2, 2}, currentWindowIndices); + } + + @Test + public void + testAddMediaSources_skipSettingMediaItems_invalidInitialSeek_correctMaskingWindowIndex() + throws Exception { + Timeline secondTimeline = new FakeTimeline(/* windowCount= */ 1); + MediaSource secondMediaSource = new FakeMediaSource(secondTimeline); + final int[] currentWindowIndices = {C.INDEX_UNSET, C.INDEX_UNSET, C.INDEX_UNSET}; + ActionSchedule actionSchedule = + new ActionSchedule.Builder(TAG) + // Wait for initial seek to be fully handled by internal player. + .waitForPositionDiscontinuity() + .waitForPendingPlayerCommands() + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + currentWindowIndices[0] = player.getCurrentWindowIndex(); + player.addMediaSource(secondMediaSource); + currentWindowIndices[1] = player.getCurrentWindowIndex(); + } + }) + .prepare() + .waitForTimelineChanged() + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + currentWindowIndices[2] = player.getCurrentWindowIndex(); + } + }) + .build(); + new ExoPlayerTestRunner.Builder(context) + .skipSettingMediaSources() + .initialSeek(/* windowIndex= */ 1, C.TIME_UNSET) + .setActionSchedule(actionSchedule) + .build() + .start(/* doPrepare= */ false) + .blockUntilActionScheduleFinished(TIMEOUT_MS) + .blockUntilEnded(TIMEOUT_MS); + assertArrayEquals(new int[] {1, 0, 0}, currentWindowIndices); + } + + @Test + public void moveMediaItems_correctMaskingWindowIndex() throws Exception { + Timeline timeline = new FakeTimeline(/* windowCount= */ 1); + MediaSource firstMediaSource = new FakeMediaSource(timeline); + MediaSource secondMediaSource = new FakeMediaSource(timeline); + MediaSource thirdMediaSource = new FakeMediaSource(timeline); + final int[] currentWindowIndices = { + C.INDEX_UNSET, C.INDEX_UNSET, C.INDEX_UNSET, C.INDEX_UNSET, C.INDEX_UNSET, C.INDEX_UNSET + }; + ActionSchedule actionSchedule = + new ActionSchedule.Builder(TAG) + .waitForPlaybackState(Player.STATE_READY) + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + // Move the current item down in the playlist. + player.moveMediaItems(/* fromIndex= */ 0, /* toIndex= */ 2, /* newIndex= */ 1); + currentWindowIndices[0] = player.getCurrentWindowIndex(); + } + }) + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + // Move the current item up in the playlist. + player.moveMediaItems(/* fromIndex= */ 1, /* toIndex= */ 3, /* newIndex= */ 0); + currentWindowIndices[1] = player.getCurrentWindowIndex(); + } + }) + .seek(/* windowIndex= */ 2, C.TIME_UNSET) + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + // Move items from before to behind the current item. + player.moveMediaItems(/* fromIndex= */ 0, /* toIndex= */ 2, /* newIndex= */ 1); + currentWindowIndices[2] = player.getCurrentWindowIndex(); + } + }) + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + // Move items from behind to before the current item. + player.moveMediaItems(/* fromIndex= */ 1, /* toIndex= */ 3, /* newIndex= */ 0); + currentWindowIndices[3] = player.getCurrentWindowIndex(); + } + }) + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + // Move items from before to before the current item. + // No change in currentWindowIndex. + player.moveMediaItems(/* fromIndex= */ 0, /* toIndex= */ 1, /* newIndex= */ 1); + currentWindowIndices[4] = player.getCurrentWindowIndex(); + } + }) + .seek(/* windowIndex= */ 0, C.TIME_UNSET) + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + // Move items from behind to behind the current item. + // No change in currentWindowIndex. + player.moveMediaItems(/* fromIndex= */ 1, /* toIndex= */ 2, /* newIndex= */ 2); + currentWindowIndices[5] = player.getCurrentWindowIndex(); + } + }) + .build(); + new ExoPlayerTestRunner.Builder(context) + .setMediaSources(firstMediaSource, secondMediaSource, thirdMediaSource) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilActionScheduleFinished(TIMEOUT_MS) + .blockUntilEnded(TIMEOUT_MS); + assertArrayEquals(new int[] {1, 0, 0, 2, 2, 0}, currentWindowIndices); + } + + @Test + public void moveMediaItems_unprepared_correctMaskingWindowIndex() throws Exception { + Timeline firstTimeline = new FakeTimeline(/* windowCount= */ 1); + MediaSource firstMediaSource = new FakeMediaSource(firstTimeline); + Timeline secondTimeline = new FakeTimeline(/* windowCount= */ 1); + MediaSource secondMediaSource = new FakeMediaSource(secondTimeline); + final int[] currentWindowIndices = {C.INDEX_UNSET, C.INDEX_UNSET, C.INDEX_UNSET}; + ActionSchedule actionSchedule = + new ActionSchedule.Builder(TAG) + .waitForTimelineChanged() + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + // Increase current window index. + currentWindowIndices[0] = player.getCurrentWindowIndex(); + player.moveMediaItem(/* currentIndex= */ 0, /* newIndex= */ 1); + currentWindowIndices[1] = player.getCurrentWindowIndex(); + } + }) + .prepare() + .waitForTimelineChanged() + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + currentWindowIndices[2] = player.getCurrentWindowIndex(); + } + }) + .build(); + new ExoPlayerTestRunner.Builder(context) + .setMediaSources(firstMediaSource, secondMediaSource) + .setActionSchedule(actionSchedule) + .build() + .start(/* doPrepare= */ false) + .blockUntilActionScheduleFinished(TIMEOUT_MS) + .blockUntilEnded(TIMEOUT_MS); + assertArrayEquals(new int[] {0, 1, 1}, currentWindowIndices); + } + + @Test + public void removeMediaItems_correctMaskingWindowIndex() throws Exception { + Timeline firstTimeline = new FakeTimeline(/* windowCount= */ 1); + MediaSource firstMediaSource = new FakeMediaSource(firstTimeline); + Timeline secondTimeline = new FakeTimeline(/* windowCount= */ 1); + MediaSource secondMediaSource = new FakeMediaSource(secondTimeline); + final int[] currentWindowIndices = {C.INDEX_UNSET, C.INDEX_UNSET}; + ActionSchedule actionSchedule = + new ActionSchedule.Builder(TAG) + .waitForPlaybackState(Player.STATE_BUFFERING) + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + // Decrease current window index. + currentWindowIndices[0] = player.getCurrentWindowIndex(); + player.removeMediaItem(/* index= */ 0); + currentWindowIndices[1] = player.getCurrentWindowIndex(); + } + }) + .build(); + new ExoPlayerTestRunner.Builder(context) + .initialSeek(/* windowIndex= */ 1, /* positionMs= */ C.TIME_UNSET) + .setMediaSources(firstMediaSource, secondMediaSource) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilActionScheduleFinished(TIMEOUT_MS) + .blockUntilEnded(TIMEOUT_MS); + assertArrayEquals(new int[] {1, 0}, currentWindowIndices); + } + + @Test + public void removeMediaItems_currentItemRemoved_correctMaskingWindowIndex() throws Exception { + Timeline firstTimeline = new FakeTimeline(/* windowCount= */ 1); + MediaSource firstMediaSource = new FakeMediaSource(firstTimeline); + Timeline secondTimeline = new FakeTimeline(/* windowCount= */ 1); + MediaSource secondMediaSource = new FakeMediaSource(secondTimeline); + final int[] currentWindowIndices = {C.INDEX_UNSET, C.INDEX_UNSET}; + final long[] currentPositions = {C.TIME_UNSET, C.TIME_UNSET}; + ActionSchedule actionSchedule = + new ActionSchedule.Builder(TAG) + .waitForPlaybackState(Player.STATE_BUFFERING) + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + // Remove the current item. + currentWindowIndices[0] = player.getCurrentWindowIndex(); + currentPositions[0] = player.getCurrentPosition(); + player.removeMediaItem(/* index= */ 1); + currentWindowIndices[1] = player.getCurrentWindowIndex(); + currentPositions[1] = player.getCurrentPosition(); + } + }) + .build(); + new ExoPlayerTestRunner.Builder(context) + .initialSeek(/* windowIndex= */ 1, /* positionMs= */ 5000) + .setMediaSources(firstMediaSource, secondMediaSource, firstMediaSource) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilActionScheduleFinished(TIMEOUT_MS) + .blockUntilEnded(TIMEOUT_MS); + assertArrayEquals(new int[] {1, 1}, currentWindowIndices); + assertThat(currentPositions[0]).isAtLeast(5000L); + assertThat(currentPositions[1]).isEqualTo(0); + } + + @Test + public void removeMediaItems_currentItemRemovedThatIsTheLast_correctMasking() throws Exception { + Timeline firstTimeline = new FakeTimeline(/* windowCount= */ 1, 1L); + MediaSource firstMediaSource = new FakeMediaSource(firstTimeline); + Timeline secondTimeline = new FakeTimeline(/* windowCount= */ 1, 2L); + MediaSource secondMediaSource = new FakeMediaSource(secondTimeline); + Timeline thirdTimeline = new FakeTimeline(/* windowCount= */ 1, 3L); + MediaSource thirdMediaSource = new FakeMediaSource(thirdTimeline); + Timeline fourthTimeline = new FakeTimeline(/* windowCount= */ 1, 3L); + MediaSource fourthMediaSource = new FakeMediaSource(fourthTimeline); + final int[] currentWindowIndices = new int[9]; + Arrays.fill(currentWindowIndices, C.INDEX_UNSET); + final int[] maskingPlaybackStates = new int[4]; + Arrays.fill(maskingPlaybackStates, C.INDEX_UNSET); + ActionSchedule actionSchedule = + new ActionSchedule.Builder(TAG) + .waitForPlaybackState(Player.STATE_READY) + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + // Expect the current window index to be 2 after seek. + currentWindowIndices[0] = player.getCurrentWindowIndex(); + player.removeMediaItem(/* index= */ 2); + // Expect the current window index to be 0 + // (default position of timeline after not finding subsequent period). + currentWindowIndices[1] = player.getCurrentWindowIndex(); + // Transition to ENDED. + maskingPlaybackStates[0] = player.getPlaybackState(); + } + }) + .waitForPlaybackState(Player.STATE_ENDED) + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + // Expects the current window index still on 0. + currentWindowIndices[2] = player.getCurrentWindowIndex(); + // Insert an item at begin when the playlist is not empty. + player.addMediaSource(/* index= */ 0, thirdMediaSource); + // Expects the current window index to be (0 + 1) after insertion at begin. + currentWindowIndices[3] = player.getCurrentWindowIndex(); + // Remains in ENDED. + maskingPlaybackStates[1] = player.getPlaybackState(); + } + }) + .waitForTimelineChanged() + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + currentWindowIndices[4] = player.getCurrentWindowIndex(); + // Implicit seek to the current window index, which is out of bounds in new + // timeline. + player.setMediaSource(fourthMediaSource, /* resetPosition= */ false); + // 0 after reset. + currentWindowIndices[5] = player.getCurrentWindowIndex(); + // Invalid seek, so we remain in ENDED. + maskingPlaybackStates[2] = player.getPlaybackState(); + } + }) + .waitForTimelineChanged() + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + currentWindowIndices[6] = player.getCurrentWindowIndex(); + // Explicit seek to (0, C.TIME_UNSET). Player transitions to BUFFERING. + player.setMediaSource(fourthMediaSource, /* startPositionMs= */ 5000); + // 0 after explicit seek. + currentWindowIndices[7] = player.getCurrentWindowIndex(); + // Transitions from ENDED to BUFFERING after explicit seek. + maskingPlaybackStates[3] = player.getPlaybackState(); + } + }) + .waitForTimelineChanged() + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + // Check whether actual window index is equal masking index from above. + currentWindowIndices[8] = player.getCurrentWindowIndex(); + } + }) + .build(); + ExoPlayerTestRunner exoPlayerTestRunner = + new ExoPlayerTestRunner.Builder(context) + .initialSeek(/* windowIndex= */ 2, /* positionMs= */ C.TIME_UNSET) + .setExpectedPlayerEndedCount(2) + .setMediaSources(firstMediaSource, secondMediaSource, thirdMediaSource) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilActionScheduleFinished(TIMEOUT_MS) + .blockUntilEnded(TIMEOUT_MS); + // Expect reset of masking to first window. + exoPlayerTestRunner.assertPlaybackStatesEqual( + Player.STATE_BUFFERING, + Player.STATE_READY, // Ready after initial prepare. + Player.STATE_ENDED, // ended after removing current window index + Player.STATE_BUFFERING, // buffers after set items with seek + Player.STATE_READY, + Player.STATE_ENDED); + assertArrayEquals( + new int[] { + Player.STATE_ENDED, // ended after removing current window index + Player.STATE_ENDED, // adding items does not change state + Player.STATE_ENDED, // set items with seek to current position. + Player.STATE_BUFFERING + }, // buffers after set items with seek + maskingPlaybackStates); + assertArrayEquals(new int[] {2, 0, 0, 1, 1, 0, 0, 0, 0}, currentWindowIndices); + } + + @Test + public void removeMediaItems_removeTailWithCurrentWindow_whenIdle_finishesPlayback() + throws Exception { + Timeline firstTimeline = new FakeTimeline(/* windowCount= */ 1); + MediaSource firstMediaSource = new FakeMediaSource(firstTimeline); + Timeline secondTimeline = new FakeTimeline(/* windowCount= */ 1, new Object()); + MediaSource secondMediaSource = new FakeMediaSource(secondTimeline); + ActionSchedule actionSchedule = + new ActionSchedule.Builder(TAG) + .seek(/* windowIndex= */ 1, /* positionMs= */ C.TIME_UNSET) + .waitForPendingPlayerCommands() + .removeMediaItem(/* index= */ 1) + .prepare() + .waitForPlaybackState(Player.STATE_ENDED) + .build(); + ExoPlayerTestRunner exoPlayerTestRunner = + new ExoPlayerTestRunner.Builder(context) + .setMediaSources(firstMediaSource, secondMediaSource) + .setActionSchedule(actionSchedule) + .build() + .start(/* doPrepare= */ false) + .blockUntilActionScheduleFinished(TIMEOUT_MS) + .blockUntilEnded(TIMEOUT_MS); + exoPlayerTestRunner.assertPlaybackStatesEqual( + Player.STATE_BUFFERING, Player.STATE_READY, Player.STATE_ENDED); + } + + @Test + public void clearMediaItems_correctMasking() throws Exception { + Timeline firstTimeline = new FakeTimeline(/* windowCount= */ 1); + MediaSource firstMediaSource = new FakeMediaSource(firstTimeline); + Timeline secondTimeline = new FakeTimeline(/* windowCount= */ 1); + MediaSource secondMediaSource = new FakeMediaSource(secondTimeline); + final int[] currentWindowIndices = {C.INDEX_UNSET, C.INDEX_UNSET}; + final int[] maskingPlaybackState = {C.INDEX_UNSET}; + ActionSchedule actionSchedule = + new ActionSchedule.Builder(TAG) + .waitForPlaybackState(Player.STATE_BUFFERING) + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + currentWindowIndices[0] = player.getCurrentWindowIndex(); + player.clearMediaItems(); + currentWindowIndices[1] = player.getCurrentWindowIndex(); + maskingPlaybackState[0] = player.getPlaybackState(); + } + }) + .build(); + new ExoPlayerTestRunner.Builder(context) + .initialSeek(/* windowIndex= */ 1, /* positionMs= */ C.TIME_UNSET) + .setMediaSources(firstMediaSource, secondMediaSource) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilActionScheduleFinished(TIMEOUT_MS) + .blockUntilEnded(TIMEOUT_MS); + assertArrayEquals(new int[] {1, 0}, currentWindowIndices); + assertArrayEquals(new int[] {Player.STATE_ENDED}, maskingPlaybackState); + } + + @Test + public void clearMediaItems_unprepared_correctMaskingWindowIndex_notEnded() throws Exception { + Timeline firstTimeline = new FakeTimeline(/* windowCount= */ 1); + MediaSource firstMediaSource = new FakeMediaSource(firstTimeline); + Timeline secondTimeline = new FakeTimeline(/* windowCount= */ 1); + MediaSource secondMediaSource = new FakeMediaSource(secondTimeline); + final int[] currentWindowIndices = {C.INDEX_UNSET, C.INDEX_UNSET}; + final int[] currentStates = {C.INDEX_UNSET, C.INDEX_UNSET, C.INDEX_UNSET}; + ActionSchedule actionSchedule = + new ActionSchedule.Builder(TAG) + // Wait for initial seek to be fully handled by internal player. + .waitForPositionDiscontinuity() + .waitForPendingPlayerCommands() + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + currentWindowIndices[0] = player.getCurrentWindowIndex(); + currentStates[0] = player.getPlaybackState(); + player.clearMediaItems(); + currentWindowIndices[1] = player.getCurrentWindowIndex(); + currentStates[1] = player.getPlaybackState(); + } + }) + .prepare() + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + // Transitions to ended when prepared with zero media items. + currentStates[2] = player.getPlaybackState(); + } + }) + .build(); + new ExoPlayerTestRunner.Builder(context) + .initialSeek(/* windowIndex= */ 1, /* positionMs= */ C.TIME_UNSET) + .setMediaSources(firstMediaSource, secondMediaSource) + .setActionSchedule(actionSchedule) + .build() + .start(/* doPrepare= */ false) + .blockUntilActionScheduleFinished(TIMEOUT_MS) + .blockUntilEnded(TIMEOUT_MS); + assertArrayEquals( + new int[] {Player.STATE_IDLE, Player.STATE_IDLE, Player.STATE_ENDED}, currentStates); + assertArrayEquals(new int[] {1, 0}, currentWindowIndices); + } + + // TODO(b/150584930): Fix reporting of renderer errors. + @Ignore + @Test + public void errorThrownDuringRendererEnableAtPeriodTransition_isReportedForNewPeriod() { + FakeMediaSource source1 = + new FakeMediaSource( + new FakeTimeline(/* windowCount= */ 1), ExoPlayerTestRunner.VIDEO_FORMAT); + FakeMediaSource source2 = + new FakeMediaSource( + new FakeTimeline(/* windowCount= */ 1), ExoPlayerTestRunner.AUDIO_FORMAT); + FakeRenderer videoRenderer = new FakeRenderer(C.TRACK_TYPE_VIDEO); + FakeRenderer audioRenderer = + new FakeRenderer(C.TRACK_TYPE_AUDIO) { + @Override + protected void onEnabled(boolean joining, boolean mayRenderStartOfStream) + throws ExoPlaybackException { + // Fail when enabling the renderer. This will happen during the period transition. + throw createRendererException( + new IllegalStateException(), ExoPlayerTestRunner.AUDIO_FORMAT); + } + }; + AtomicReference trackGroupsAfterError = new AtomicReference<>(); + AtomicReference trackSelectionsAfterError = new AtomicReference<>(); + AtomicInteger windowIndexAfterError = new AtomicInteger(); + ActionSchedule actionSchedule = + new ActionSchedule.Builder(TAG) + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + player.addAnalyticsListener( + new AnalyticsListener() { + @Override + public void onPlayerError( + EventTime eventTime, ExoPlaybackException error) { + trackGroupsAfterError.set(player.getCurrentTrackGroups()); + trackSelectionsAfterError.set(player.getCurrentTrackSelections()); + windowIndexAfterError.set(player.getCurrentWindowIndex()); + } + }); + } + }) + .build(); + ExoPlayerTestRunner testRunner = + new ExoPlayerTestRunner.Builder(context) + .setMediaSources(source1, source2) + .setActionSchedule(actionSchedule) + .setRenderers(videoRenderer, audioRenderer) + .build(); + + assertThrows( + ExoPlaybackException.class, + () -> + testRunner + .start(/* doPrepare= */ true) + .blockUntilActionScheduleFinished(TIMEOUT_MS) + .blockUntilEnded(TIMEOUT_MS)); + + assertThat(windowIndexAfterError.get()).isEqualTo(1); + assertThat(trackGroupsAfterError.get().length).isEqualTo(1); + assertThat(trackGroupsAfterError.get().get(0).getFormat(0)) + .isEqualTo(ExoPlayerTestRunner.AUDIO_FORMAT); + assertThat(trackSelectionsAfterError.get().get(0)).isNull(); // Video renderer. + assertThat(trackSelectionsAfterError.get().get(1)).isNotNull(); // Audio renderer. + } + + @Test + public void errorThrownDuringRendererDisableAtPeriodTransition_isReportedForCurrentPeriod() { + FakeMediaSource source1 = + new FakeMediaSource( + new FakeTimeline(/* windowCount= */ 1), ExoPlayerTestRunner.VIDEO_FORMAT); + FakeMediaSource source2 = + new FakeMediaSource( + new FakeTimeline(/* windowCount= */ 1), ExoPlayerTestRunner.AUDIO_FORMAT); + FakeRenderer videoRenderer = + new FakeRenderer(C.TRACK_TYPE_VIDEO) { + @Override + protected void onStopped() throws ExoPlaybackException { + // Fail when stopping the renderer. This will happen during the period transition. + throw createRendererException( + new IllegalStateException(), ExoPlayerTestRunner.VIDEO_FORMAT); + } + }; + FakeRenderer audioRenderer = new FakeRenderer(C.TRACK_TYPE_AUDIO); + AtomicReference trackGroupsAfterError = new AtomicReference<>(); + AtomicReference trackSelectionsAfterError = new AtomicReference<>(); + AtomicInteger windowIndexAfterError = new AtomicInteger(); + ActionSchedule actionSchedule = + new ActionSchedule.Builder(TAG) + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + player.addAnalyticsListener( + new AnalyticsListener() { + @Override + public void onPlayerError( + EventTime eventTime, ExoPlaybackException error) { + trackGroupsAfterError.set(player.getCurrentTrackGroups()); + trackSelectionsAfterError.set(player.getCurrentTrackSelections()); + windowIndexAfterError.set(player.getCurrentWindowIndex()); + } + }); + } + }) + .build(); + ExoPlayerTestRunner testRunner = + new ExoPlayerTestRunner.Builder(context) + .setMediaSources(source1, source2) + .setActionSchedule(actionSchedule) + .setRenderers(videoRenderer, audioRenderer) + .build(); + + assertThrows( + ExoPlaybackException.class, + () -> + testRunner + .start(/* doPrepare= */ true) + .blockUntilActionScheduleFinished(TIMEOUT_MS) + .blockUntilEnded(TIMEOUT_MS)); + + assertThat(windowIndexAfterError.get()).isEqualTo(0); + assertThat(trackGroupsAfterError.get().length).isEqualTo(1); + assertThat(trackGroupsAfterError.get().get(0).getFormat(0)) + .isEqualTo(ExoPlayerTestRunner.VIDEO_FORMAT); + assertThat(trackSelectionsAfterError.get().get(0)).isNotNull(); // Video renderer. + assertThat(trackSelectionsAfterError.get().get(1)).isNull(); // Audio renderer. + } + + // TODO(b/150584930): Fix reporting of renderer errors. + @Ignore + @Test + public void errorThrownDuringRendererReplaceStreamAtPeriodTransition_isReportedForNewPeriod() { + FakeMediaSource source1 = + new FakeMediaSource( + new FakeTimeline(/* windowCount= */ 1), + ExoPlayerTestRunner.VIDEO_FORMAT, + ExoPlayerTestRunner.AUDIO_FORMAT); + FakeMediaSource source2 = + new FakeMediaSource( + new FakeTimeline(/* windowCount= */ 1), ExoPlayerTestRunner.AUDIO_FORMAT); + FakeRenderer videoRenderer = new FakeRenderer(C.TRACK_TYPE_VIDEO); + FakeRenderer audioRenderer = + new FakeRenderer(C.TRACK_TYPE_AUDIO) { + @Override + protected void onStreamChanged(Format[] formats, long offsetUs) + throws ExoPlaybackException { + // Fail when changing streams. This will happen during the period transition. + throw createRendererException( + new IllegalStateException(), ExoPlayerTestRunner.AUDIO_FORMAT); + } + }; + AtomicReference trackGroupsAfterError = new AtomicReference<>(); + AtomicReference trackSelectionsAfterError = new AtomicReference<>(); + AtomicInteger windowIndexAfterError = new AtomicInteger(); + ActionSchedule actionSchedule = + new ActionSchedule.Builder(TAG) + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + player.addAnalyticsListener( + new AnalyticsListener() { + @Override + public void onPlayerError( + EventTime eventTime, ExoPlaybackException error) { + trackGroupsAfterError.set(player.getCurrentTrackGroups()); + trackSelectionsAfterError.set(player.getCurrentTrackSelections()); + windowIndexAfterError.set(player.getCurrentWindowIndex()); + } + }); + } + }) + .build(); + ExoPlayerTestRunner testRunner = + new ExoPlayerTestRunner.Builder(context) + .setMediaSources(source1, source2) + .setActionSchedule(actionSchedule) + .setRenderers(videoRenderer, audioRenderer) + .build(); + + assertThrows( + ExoPlaybackException.class, + () -> + testRunner + .start(/* doPrepare= */ true) + .blockUntilActionScheduleFinished(TIMEOUT_MS) + .blockUntilEnded(TIMEOUT_MS)); + + assertThat(windowIndexAfterError.get()).isEqualTo(1); + assertThat(trackGroupsAfterError.get().length).isEqualTo(1); + assertThat(trackGroupsAfterError.get().get(0).getFormat(0)) + .isEqualTo(ExoPlayerTestRunner.AUDIO_FORMAT); + assertThat(trackSelectionsAfterError.get().get(0)).isNull(); // Video renderer. + assertThat(trackSelectionsAfterError.get().get(1)).isNotNull(); // Audio renderer. + } + + @Test + public void errorThrownDuringPlaylistUpdate_keepsConsistentPlayerState() { + FakeMediaSource source1 = + new FakeMediaSource( + new FakeTimeline(/* windowCount= */ 1), + ExoPlayerTestRunner.VIDEO_FORMAT, + ExoPlayerTestRunner.AUDIO_FORMAT); + FakeMediaSource source2 = + new FakeMediaSource( + new FakeTimeline(/* windowCount= */ 1), ExoPlayerTestRunner.AUDIO_FORMAT); + AtomicInteger audioRendererEnableCount = new AtomicInteger(0); + FakeRenderer videoRenderer = new FakeRenderer(C.TRACK_TYPE_VIDEO); + FakeRenderer audioRenderer = + new FakeRenderer(C.TRACK_TYPE_AUDIO) { + @Override + protected void onEnabled(boolean joining, boolean mayRenderStartOfStream) + throws ExoPlaybackException { + if (audioRendererEnableCount.incrementAndGet() == 2) { + // Fail when enabling the renderer for the second time during the playlist update. + throw createRendererException( + new IllegalStateException(), ExoPlayerTestRunner.AUDIO_FORMAT); + } + } + }; + AtomicReference timelineAfterError = new AtomicReference<>(); + AtomicReference trackGroupsAfterError = new AtomicReference<>(); + AtomicReference trackSelectionsAfterError = new AtomicReference<>(); + AtomicInteger windowIndexAfterError = new AtomicInteger(); + ActionSchedule actionSchedule = + new ActionSchedule.Builder(TAG) + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + player.addAnalyticsListener( + new AnalyticsListener() { + @Override + public void onPlayerError( + EventTime eventTime, ExoPlaybackException error) { + timelineAfterError.set(player.getCurrentTimeline()); + trackGroupsAfterError.set(player.getCurrentTrackGroups()); + trackSelectionsAfterError.set(player.getCurrentTrackSelections()); + windowIndexAfterError.set(player.getCurrentWindowIndex()); + } + }); + } + }) + .pause() + // Wait until fully buffered so that the new renderer can be enabled immediately. + .waitForIsLoading(true) + .waitForIsLoading(false) + .waitForIsLoading(true) + .waitForIsLoading(false) + .removeMediaItem(0) + .build(); + ExoPlayerTestRunner testRunner = + new ExoPlayerTestRunner.Builder(context) + .setMediaSources(source1, source2) + .setActionSchedule(actionSchedule) + .setRenderers(videoRenderer, audioRenderer) + .build(); + + assertThrows( + ExoPlaybackException.class, + () -> + testRunner + .start(/* doPrepare= */ true) + .blockUntilActionScheduleFinished(TIMEOUT_MS) + .blockUntilEnded(TIMEOUT_MS)); + + assertThat(timelineAfterError.get().getWindowCount()).isEqualTo(1); + assertThat(windowIndexAfterError.get()).isEqualTo(0); + assertThat(trackGroupsAfterError.get().length).isEqualTo(1); + assertThat(trackGroupsAfterError.get().get(0).getFormat(0)) + .isEqualTo(ExoPlayerTestRunner.AUDIO_FORMAT); + assertThat(trackSelectionsAfterError.get().get(0)).isNull(); // Video renderer. + assertThat(trackSelectionsAfterError.get().get(1)).isNotNull(); // Audio renderer. + } + + @Test + public void seekToCurrentPosition_inEndedState_switchesToBufferingStateAndContinuesPlayback() + throws Exception { + MediaSource mediaSource = new FakeMediaSource(new FakeTimeline(/* windowCount = */ 1)); + AtomicInteger windowIndexAfterFinalEndedState = new AtomicInteger(); + ActionSchedule actionSchedule = + new ActionSchedule.Builder(TAG) + .waitForPlaybackState(Player.STATE_ENDED) + .addMediaSources(mediaSource) + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + player.seekTo(player.getCurrentPosition()); + } + }) + .waitForPlaybackState(Player.STATE_READY) + .waitForPlaybackState(Player.STATE_ENDED) + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + windowIndexAfterFinalEndedState.set(player.getCurrentWindowIndex()); + } + }) + .build(); + new ExoPlayerTestRunner.Builder(context) + .setMediaSources(mediaSource) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilActionScheduleFinished(TIMEOUT_MS) + .blockUntilEnded(TIMEOUT_MS); + + assertThat(windowIndexAfterFinalEndedState.get()).isEqualTo(1); + } + + @Test + public void pauseAtEndOfMediaItems_pausesPlaybackBeforeTransitioningToTheNextItem() + throws Exception { + TimelineWindowDefinition timelineWindowDefinition = + new TimelineWindowDefinition( + /* isSeekable= */ true, + /* isDynamic= */ false, + /* durationUs= */ 10 * C.MICROS_PER_SECOND); + MediaSource mediaSource = new FakeMediaSource(new FakeTimeline(timelineWindowDefinition)); + AtomicInteger playbackStateAfterPause = new AtomicInteger(C.INDEX_UNSET); + AtomicLong positionAfterPause = new AtomicLong(C.TIME_UNSET); + AtomicInteger windowIndexAfterPause = new AtomicInteger(C.INDEX_UNSET); + ActionSchedule actionSchedule = + new ActionSchedule.Builder(TAG) + .waitForPlayWhenReady(true) + .waitForPlayWhenReady(false) + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + playbackStateAfterPause.set(player.getPlaybackState()); + windowIndexAfterPause.set(player.getCurrentWindowIndex()); + positionAfterPause.set(player.getContentPosition()); + } + }) + .play() + .build(); + new ExoPlayerTestRunner.Builder(context) + .setPauseAtEndOfMediaItems(true) + .setMediaSources(mediaSource, mediaSource) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilEnded(TIMEOUT_MS); + + assertThat(playbackStateAfterPause.get()).isEqualTo(Player.STATE_READY); + assertThat(windowIndexAfterPause.get()).isEqualTo(0); + assertThat(positionAfterPause.get()).isEqualTo(10_000); + } + + @Test + public void pauseAtEndOfMediaItems_pausesPlaybackWhenEnded() throws Exception { + TimelineWindowDefinition timelineWindowDefinition = + new TimelineWindowDefinition( + /* isSeekable= */ true, + /* isDynamic= */ false, + /* durationUs= */ 10 * C.MICROS_PER_SECOND); + MediaSource mediaSource = new FakeMediaSource(new FakeTimeline(timelineWindowDefinition)); + AtomicInteger playbackStateAfterPause = new AtomicInteger(C.INDEX_UNSET); + AtomicLong positionAfterPause = new AtomicLong(C.TIME_UNSET); + AtomicInteger windowIndexAfterPause = new AtomicInteger(C.INDEX_UNSET); + ActionSchedule actionSchedule = + new ActionSchedule.Builder(TAG) + .waitForPlayWhenReady(true) + .waitForPlayWhenReady(false) + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + playbackStateAfterPause.set(player.getPlaybackState()); + windowIndexAfterPause.set(player.getCurrentWindowIndex()); + positionAfterPause.set(player.getContentPosition()); + } + }) + .build(); + new ExoPlayerTestRunner.Builder(context) + .setPauseAtEndOfMediaItems(true) + .setMediaSources(mediaSource) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilActionScheduleFinished(TIMEOUT_MS) + .blockUntilEnded(TIMEOUT_MS); + + assertThat(playbackStateAfterPause.get()).isEqualTo(Player.STATE_ENDED); + assertThat(windowIndexAfterPause.get()).isEqualTo(0); + assertThat(positionAfterPause.get()).isEqualTo(10_000); + } + + // Disabled until the flag to throw exceptions for [internal: b/144538905] is enabled by default. + @Ignore + @Test + public void + infiniteLoading_withSmallAllocations_oomIsPreventedByLoadControl_andThrowsStuckBufferingIllegalStateException() { + MediaSource continuouslyAllocatingMediaSource = + new FakeMediaSource( + new FakeTimeline(/* windowCount= */ 1), ExoPlayerTestRunner.VIDEO_FORMAT) { + @Override + protected FakeMediaPeriod createFakeMediaPeriod( + MediaPeriodId id, + TrackGroupArray trackGroupArray, + Allocator allocator, + EventDispatcher eventDispatcher, + @Nullable TransferListener transferListener) { + return new FakeMediaPeriod(trackGroupArray, eventDispatcher) { + + private final List allocations = new ArrayList<>(); + + private Callback callback; + + @Override + public synchronized void prepare(Callback callback, long positionUs) { + this.callback = callback; + super.prepare(callback, positionUs); + } + + @Override + public long getBufferedPositionUs() { + // Pretend not to make loading progress, so that continueLoading keeps being called. + return 0; + } + + @Override + public long getNextLoadPositionUs() { + // Pretend not to make loading progress, so that continueLoading keeps being called. + return 0; + } + + @Override + public boolean continueLoading(long positionUs) { + allocations.add(allocator.allocate()); + callback.onContinueLoadingRequested(this); + return true; + } + }; + } + }; + ActionSchedule actionSchedule = + new ActionSchedule.Builder(TAG) + // Prevent player from ever assuming it finished playing. + .setRepeatMode(Player.REPEAT_MODE_ALL) + .build(); + ExoPlayerTestRunner testRunner = + new ExoPlayerTestRunner.Builder(context) + .setActionSchedule(actionSchedule) + .setMediaSources(continuouslyAllocatingMediaSource) + .build(); + + ExoPlaybackException exception = + assertThrows( + ExoPlaybackException.class, () -> testRunner.start().blockUntilEnded(TIMEOUT_MS)); + assertThat(exception.type).isEqualTo(ExoPlaybackException.TYPE_UNEXPECTED); + assertThat(exception.getUnexpectedException()).isInstanceOf(IllegalStateException.class); + } + + @Test + public void loading_withLargeAllocationCausingOom_playsRemainingMediaAndThenThrows() { + Loader.Loadable loadable = + new Loader.Loadable() { + @SuppressWarnings("UnusedVariable") + @Override + public void load() throws IOException { + @SuppressWarnings("unused") // This test needs the allocation to cause an OOM. + byte[] largeBuffer = new byte[Integer.MAX_VALUE]; + } + + @Override + public void cancelLoad() {} + }; + MediaSource largeBufferAllocatingMediaSource = + new FakeMediaSource( + new FakeTimeline(/* windowCount= */ 1), ExoPlayerTestRunner.VIDEO_FORMAT) { + @Override + protected FakeMediaPeriod createFakeMediaPeriod( + MediaPeriodId id, + TrackGroupArray trackGroupArray, + Allocator allocator, + EventDispatcher eventDispatcher, + @Nullable TransferListener transferListener) { + return new FakeMediaPeriod(trackGroupArray, eventDispatcher) { + private Loader loader = new Loader("oomLoader"); + + @Override + public boolean continueLoading(long positionUs) { + loader.startLoading( + loadable, new DummyLoaderCallback(), /* defaultMinRetryCount= */ 1); + return true; + } + + @Override + protected SampleStream createSampleStream( + long positionUs, TrackSelection selection, EventDispatcher eventDispatcher) { + // Create 3 samples without end of stream signal to test that all 3 samples are + // still played before the exception is thrown. + return new FakeSampleStream( + selection.getSelectedFormat(), + eventDispatcher, + positionUs, + /* timeUsIncrement= */ 0, + new FakeSampleStream.FakeSampleStreamItem(new byte[] {0}), + new FakeSampleStream.FakeSampleStreamItem(new byte[] {0}), + new FakeSampleStream.FakeSampleStreamItem(new byte[] {0})) { + + @Override + public void maybeThrowError() throws IOException { + loader.maybeThrowError(); + } + }; + } + }; + } + }; + FakeRenderer renderer = new FakeRenderer(C.TRACK_TYPE_VIDEO); + ExoPlayerTestRunner testRunner = + new ExoPlayerTestRunner.Builder(context) + .setMediaSources(largeBufferAllocatingMediaSource) + .setRenderers(renderer) + .build(); + + ExoPlaybackException exception = + assertThrows( + ExoPlaybackException.class, () -> testRunner.start().blockUntilEnded(TIMEOUT_MS)); + assertThat(exception.type).isEqualTo(ExoPlaybackException.TYPE_SOURCE); + assertThat(exception.getSourceException()).isInstanceOf(Loader.UnexpectedLoaderException.class); + assertThat(exception.getSourceException().getCause()).isInstanceOf(OutOfMemoryError.class); + + assertThat(renderer.sampleBufferReadCount).isEqualTo(3); + } + + @Test + public void seekTo_whileReady_callsOnIsPlayingChanged() throws Exception { + ActionSchedule actionSchedule = + new ActionSchedule.Builder(TAG) + .waitForPlaybackState(Player.STATE_READY) + .seek(/* positionMs= */ 0) + .waitForPlaybackState(Player.STATE_ENDED) + .build(); + List onIsPlayingChanges = new ArrayList<>(); + Player.EventListener eventListener = + new Player.EventListener() { + @Override + public void onIsPlayingChanged(boolean isPlaying) { + onIsPlayingChanges.add(isPlaying); + } + }; + new ExoPlayerTestRunner.Builder(context) + .setEventListener(eventListener) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilActionScheduleFinished(TIMEOUT_MS) + .blockUntilEnded(TIMEOUT_MS); + + assertThat(onIsPlayingChanges).containsExactly(true, false, true, false).inOrder(); + } + + @Test + public void multipleListenersAndMultipleCallbacks_callbacksAreOrderedByType() throws Exception { + String playWhenReadyChange1 = "playWhenReadyChange1"; + String playWhenReadyChange2 = "playWhenReadyChange2"; + String isPlayingChange1 = "isPlayingChange1"; + String isPlayingChange2 = "isPlayingChange2"; + ArrayList events = new ArrayList<>(); + Player.EventListener eventListener1 = + new Player.EventListener() { + @Override + public void onPlayWhenReadyChanged(boolean playWhenReady, int reason) { + events.add(playWhenReadyChange1); + } + + @Override + public void onIsPlayingChanged(boolean isPlaying) { + events.add(isPlayingChange1); + } + }; + Player.EventListener eventListener2 = + new Player.EventListener() { + @Override + public void onPlayWhenReadyChanged(boolean playWhenReady, int reason) { + events.add(playWhenReadyChange2); + } + + @Override + public void onIsPlayingChanged(boolean isPlaying) { + events.add(isPlayingChange2); + } + }; + ActionSchedule actionSchedule = + new ActionSchedule.Builder(TAG) + .pause() + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + player.addListener(eventListener1); + player.addListener(eventListener2); + } + }) + .waitForPlaybackState(Player.STATE_READY) + .play() + .waitForPlaybackState(Player.STATE_ENDED) + .build(); + new ExoPlayerTestRunner.Builder(context) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilActionScheduleFinished(TIMEOUT_MS) + .blockUntilEnded(TIMEOUT_MS); + + assertThat(events) + .containsExactly( + playWhenReadyChange1, + playWhenReadyChange2, + isPlayingChange1, + isPlayingChange2, + isPlayingChange1, + isPlayingChange2) + .inOrder(); + } + // Internal methods. private static ActionSchedule.Builder addSurfaceSwitch(ActionSchedule.Builder builder) { @@ -3211,6 +6614,16 @@ public final class ExoPlayerTest { // Internal classes. + private static final class CountingMessageTarget implements PlayerMessage.Target { + + public int messageCount; + + @Override + public void handleMessage(int x, @Nullable Object message) { + messageCount++; + } + } + private static final class PositionGrabbingMessageTarget extends PlayerTarget { public int windowIndex; @@ -3224,10 +6637,8 @@ public final class ExoPlayerTest { @Override public void handleMessage(SimpleExoPlayer player, int messageType, @Nullable Object message) { - if (player != null) { - windowIndex = player.getCurrentWindowIndex(); - positionMs = player.getCurrentPosition(); - } + windowIndex = player.getCurrentWindowIndex(); + positionMs = player.getCurrentPosition(); messageCount++; } } @@ -3245,4 +6656,92 @@ public final class ExoPlayerTest { timeline = player.getCurrentTimeline(); } } + /** + * Provides a wrapper for a {@link Runnable} which does collect playback states and window counts. + * Can be used with {@link ActionSchedule.Builder#executeRunnable(Runnable)} to verify that a + * playback state did not change and hence no observable callback is called. + * + *

        This is specifically useful in cases when the test may end before a given state arrives or + * when an action of the action schedule might execute before a callback is called. + */ + public static class PlaybackStateCollector extends PlayerRunnable { + + private final int[] playbackStates; + private final int[] timelineWindowCount; + private final int index; + + /** + * Creates the collector. + * + * @param index The index to populate. + * @param playbackStates An array of playback states to populate. + * @param timelineWindowCount An array of window counts to populate. + */ + public PlaybackStateCollector(int index, int[] playbackStates, int[] timelineWindowCount) { + Assertions.checkArgument(playbackStates.length > index && timelineWindowCount.length > index); + this.playbackStates = playbackStates; + this.timelineWindowCount = timelineWindowCount; + this.index = index; + } + + @Override + public void run(SimpleExoPlayer player) { + playbackStates[index] = player.getPlaybackState(); + timelineWindowCount[index] = player.getCurrentTimeline().getWindowCount(); + } + } + + private static final class DummyLoaderCallback implements Loader.Callback { + @Override + public void onLoadCompleted( + Loader.Loadable loadable, long elapsedRealtimeMs, long loadDurationMs) {} + + @Override + public void onLoadCanceled( + Loader.Loadable loadable, long elapsedRealtimeMs, long loadDurationMs, boolean released) {} + + @Override + public Loader.LoadErrorAction onLoadError( + Loader.Loadable loadable, + long elapsedRealtimeMs, + long loadDurationMs, + IOException error, + int errorCount) { + return Loader.RETRY; + } + } + + private static class DummyAdsLoader implements AdsLoader { + + @Override + public void setPlayer(@Nullable Player player) {} + + @Override + public void release() {} + + @Override + public void setSupportedContentTypes(int... contentTypes) {} + + @Override + public void start(AdsLoader.EventListener eventListener, AdViewProvider adViewProvider) {} + + @Override + public void stop() {} + + @Override + public void handlePrepareError(int adGroupIndex, int adIndexInAdGroup, IOException exception) {} + } + + private static class DummyAdViewProvider implements AdsLoader.AdViewProvider { + + @Override + public ViewGroup getAdViewGroup() { + return null; + } + + @Override + public View[] getAdOverlayViews() { + return new View[0]; + } + } } 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 deleted file mode 100644 index fe2a8c7d4b..0000000000 --- a/library/core/src/test/java/com/google/android/exoplayer2/FormatTest.java +++ /dev/null @@ -1,107 +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; - -import static com.google.android.exoplayer2.C.WIDEVINE_UUID; -import static com.google.android.exoplayer2.util.MimeTypes.VIDEO_MP4; -import static com.google.android.exoplayer2.util.MimeTypes.VIDEO_WEBM; -import static com.google.common.truth.Truth.assertThat; - -import android.os.Parcel; -import androidx.test.ext.junit.runners.AndroidJUnit4; -import com.google.android.exoplayer2.drm.DrmInitData; -import com.google.android.exoplayer2.metadata.Metadata; -import com.google.android.exoplayer2.metadata.id3.TextInformationFrame; -import com.google.android.exoplayer2.testutil.TestUtil; -import com.google.android.exoplayer2.util.MimeTypes; -import com.google.android.exoplayer2.video.ColorInfo; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import org.junit.Test; -import org.junit.runner.RunWith; - -/** Unit test for {@link Format}. */ -@RunWith(AndroidJUnit4.class) -public final class FormatTest { - - private static final List initData; - static { - byte[] initData1 = new byte[] {1, 2, 3}; - byte[] initData2 = new byte[] {4, 5, 6}; - List initDataList = new ArrayList<>(); - initDataList.add(initData1); - initDataList.add(initData2); - initData = Collections.unmodifiableList(initDataList); - } - - @Test - public void testParcelable() { - DrmInitData.SchemeData drmData1 = new DrmInitData.SchemeData(WIDEVINE_UUID, VIDEO_MP4, - TestUtil.buildTestData(128, 1 /* data seed */)); - DrmInitData.SchemeData drmData2 = new DrmInitData.SchemeData(C.UUID_NIL, VIDEO_WEBM, - TestUtil.buildTestData(128, 1 /* data seed */)); - DrmInitData drmInitData = new DrmInitData(drmData1, drmData2); - byte[] projectionData = new byte[] {1, 2, 3}; - Metadata metadata = new Metadata( - new TextInformationFrame("id1", "description1", "value1"), - new TextInformationFrame("id2", "description2", "value2")); - 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", - "label", - C.SELECTION_FLAG_DEFAULT, - C.ROLE_FLAG_MAIN, - /* bitrate= */ 1024, - "codec", - metadata, - /* containerMimeType= */ MimeTypes.VIDEO_MP4, - /* sampleMimeType= */ MimeTypes.VIDEO_H264, - /* maxInputSize= */ 2048, - initData, - drmInitData, - Format.OFFSET_SAMPLE_RELATIVE, - /* 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, - "language", - /* accessibilityChannel= */ Format.NO_VALUE, - /* exoMediaCryptoType= */ null); - - Parcel parcel = Parcel.obtain(); - formatToParcel.writeToParcel(parcel, 0); - parcel.setDataPosition(0); - - Format formatFromParcel = Format.CREATOR.createFromParcel(parcel); - assertThat(formatFromParcel).isEqualTo(formatToParcel); - - parcel.recycle(); - } - -} diff --git a/library/core/src/test/java/com/google/android/exoplayer2/MediaPeriodQueueTest.java b/library/core/src/test/java/com/google/android/exoplayer2/MediaPeriodQueueTest.java index 1a0e13b6c1..ccc5156015 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/MediaPeriodQueueTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/MediaPeriodQueueTest.java @@ -18,23 +18,30 @@ package com.google.android.exoplayer2; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertNull; import static org.mockito.Mockito.mock; +import static org.robolectric.annotation.LooperMode.Mode.LEGACY; import android.net.Uri; import androidx.test.ext.junit.runners.AndroidJUnit4; -import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; +import com.google.android.exoplayer2.source.ShuffleOrder; import com.google.android.exoplayer2.source.SinglePeriodTimeline; import com.google.android.exoplayer2.source.ads.AdPlaybackState; import com.google.android.exoplayer2.source.ads.SinglePeriodAdTimeline; +import com.google.android.exoplayer2.testutil.FakeMediaSource; +import com.google.android.exoplayer2.testutil.FakeTimeline; +import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition; 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.Allocator; +import java.util.Collections; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.robolectric.annotation.LooperMode; /** Unit tests for {@link MediaPeriodQueue}. */ +@LooperMode(LEGACY) @RunWith(AndroidJUnit4.class) public final class MediaPeriodQueueTest { @@ -50,19 +57,20 @@ public final class MediaPeriodQueueTest { private MediaPeriodQueue mediaPeriodQueue; private AdPlaybackState adPlaybackState; - private Timeline timeline; - private Object periodUid; + private Object firstPeriodUid; private PlaybackInfo playbackInfo; private RendererCapabilities[] rendererCapabilities; private TrackSelector trackSelector; private Allocator allocator; - private MediaSource mediaSource; + private MediaSourceList mediaSourceList; + private FakeMediaSource fakeMediaSource; + private MediaSourceList.MediaSourceHolder mediaSourceHolder; @Before public void setUp() { mediaPeriodQueue = new MediaPeriodQueue(); - mediaSource = mock(MediaSource.class); + mediaSourceList = mock(MediaSourceList.class); rendererCapabilities = new RendererCapabilities[0]; trackSelector = mock(TrackSelector.class); allocator = mock(Allocator.class); @@ -70,40 +78,46 @@ public final class MediaPeriodQueueTest { @Test public void getNextMediaPeriodInfo_withoutAds_returnsLastMediaPeriodInfo() { - setupTimeline(/* initialPositionUs= */ 0); + setupAdTimeline(/* no ad groups */ ); assertGetNextMediaPeriodInfoReturnsContentMediaPeriod( + /* periodUid= */ firstPeriodUid, /* startPositionUs= */ 0, + /* requestedContentPositionUs= */ C.TIME_UNSET, /* endPositionUs= */ C.TIME_UNSET, /* durationUs= */ CONTENT_DURATION_US, - /* isLast= */ true, + /* isLastInPeriod= */ true, + /* isLastInWindow= */ true, /* nextAdGroupIndex= */ C.INDEX_UNSET); } @Test public void getNextMediaPeriodInfo_withPrerollAd_returnsCorrectMediaPeriodInfos() { - setupTimeline(/* initialPositionUs= */ 0, /* adGroupTimesUs= */ 0); + setupAdTimeline(/* adGroupTimesUs...= */ 0); setAdGroupLoaded(/* adGroupIndex= */ 0); - assertNextMediaPeriodInfoIsAd(/* adGroupIndex= */ 0, /* contentPositionUs= */ 0); + assertNextMediaPeriodInfoIsAd(/* adGroupIndex= */ 0, /* contentPositionUs= */ C.TIME_UNSET); advance(); assertGetNextMediaPeriodInfoReturnsContentMediaPeriod( + /* periodUid= */ firstPeriodUid, /* startPositionUs= */ 0, + /* requestedContentPositionUs= */ C.TIME_UNSET, /* endPositionUs= */ C.TIME_UNSET, /* durationUs= */ CONTENT_DURATION_US, - /* isLast= */ true, + /* isLastInPeriod= */ true, + /* isLastInWindow= */ true, /* nextAdGroupIndex= */ C.INDEX_UNSET); } @Test public void getNextMediaPeriodInfo_withMidrollAds_returnsCorrectMediaPeriodInfos() { - setupTimeline( - /* initialPositionUs= */ 0, - /* adGroupTimesUs= */ FIRST_AD_START_TIME_US, - SECOND_AD_START_TIME_US); + setupAdTimeline(/* adGroupTimesUs...= */ FIRST_AD_START_TIME_US, SECOND_AD_START_TIME_US); assertGetNextMediaPeriodInfoReturnsContentMediaPeriod( + /* periodUid= */ firstPeriodUid, /* startPositionUs= */ 0, + /* requestedContentPositionUs= */ C.TIME_UNSET, /* endPositionUs= */ FIRST_AD_START_TIME_US, /* durationUs= */ FIRST_AD_START_TIME_US, - /* isLast= */ false, + /* isLastInPeriod= */ false, + /* isLastInWindow= */ false, /* nextAdGroupIndex= */ 0); // The next media period info should be null as we haven't loaded the ad yet. advance(); @@ -113,10 +127,13 @@ public final class MediaPeriodQueueTest { /* adGroupIndex= */ 0, /* contentPositionUs= */ FIRST_AD_START_TIME_US); advance(); assertGetNextMediaPeriodInfoReturnsContentMediaPeriod( + /* periodUid= */ firstPeriodUid, /* startPositionUs= */ FIRST_AD_START_TIME_US, + /* requestedContentPositionUs= */ FIRST_AD_START_TIME_US, /* endPositionUs= */ SECOND_AD_START_TIME_US, /* durationUs= */ SECOND_AD_START_TIME_US, - /* isLast= */ false, + /* isLastInPeriod= */ false, + /* isLastInWindow= */ false, /* nextAdGroupIndex= */ 1); advance(); setAdGroupLoaded(/* adGroupIndex= */ 1); @@ -124,24 +141,27 @@ public final class MediaPeriodQueueTest { /* adGroupIndex= */ 1, /* contentPositionUs= */ SECOND_AD_START_TIME_US); advance(); assertGetNextMediaPeriodInfoReturnsContentMediaPeriod( + /* periodUid= */ firstPeriodUid, /* startPositionUs= */ SECOND_AD_START_TIME_US, + /* requestedContentPositionUs= */ SECOND_AD_START_TIME_US, /* endPositionUs= */ C.TIME_UNSET, /* durationUs= */ CONTENT_DURATION_US, - /* isLast= */ true, + /* isLastInPeriod= */ true, + /* isLastInWindow= */ true, /* nextAdGroupIndex= */ C.INDEX_UNSET); } @Test public void getNextMediaPeriodInfo_withMidrollAndPostroll_returnsCorrectMediaPeriodInfos() { - setupTimeline( - /* initialPositionUs= */ 0, - /* adGroupTimesUs= */ FIRST_AD_START_TIME_US, - C.TIME_END_OF_SOURCE); + setupAdTimeline(/* adGroupTimesUs...= */ FIRST_AD_START_TIME_US, C.TIME_END_OF_SOURCE); assertGetNextMediaPeriodInfoReturnsContentMediaPeriod( + /* periodUid= */ firstPeriodUid, /* startPositionUs= */ 0, + /* requestedContentPositionUs= */ C.TIME_UNSET, /* endPositionUs= */ FIRST_AD_START_TIME_US, /* durationUs= */ FIRST_AD_START_TIME_US, - /* isLast= */ false, + /* isLastInPeriod= */ false, + /* isLastInWindow= */ false, /* nextAdGroupIndex= */ 0); advance(); setAdGroupLoaded(/* adGroupIndex= */ 0); @@ -149,10 +169,13 @@ public final class MediaPeriodQueueTest { /* adGroupIndex= */ 0, /* contentPositionUs= */ FIRST_AD_START_TIME_US); advance(); assertGetNextMediaPeriodInfoReturnsContentMediaPeriod( + /* periodUid= */ firstPeriodUid, /* startPositionUs= */ FIRST_AD_START_TIME_US, + /* requestedContentPositionUs= */ FIRST_AD_START_TIME_US, /* endPositionUs= */ C.TIME_END_OF_SOURCE, /* durationUs= */ CONTENT_DURATION_US, - /* isLast= */ false, + /* isLastInPeriod= */ false, + /* isLastInWindow= */ false, /* nextAdGroupIndex= */ 1); advance(); setAdGroupLoaded(/* adGroupIndex= */ 1); @@ -160,39 +183,78 @@ public final class MediaPeriodQueueTest { /* adGroupIndex= */ 1, /* contentPositionUs= */ CONTENT_DURATION_US); advance(); assertGetNextMediaPeriodInfoReturnsContentMediaPeriod( - /* startPositionUs= */ CONTENT_DURATION_US, + /* periodUid= */ firstPeriodUid, + /* startPositionUs= */ CONTENT_DURATION_US - 1, + /* requestedContentPositionUs= */ CONTENT_DURATION_US, /* endPositionUs= */ C.TIME_UNSET, /* durationUs= */ CONTENT_DURATION_US, - /* isLast= */ true, + /* isLastInPeriod= */ true, + /* isLastInWindow= */ true, /* nextAdGroupIndex= */ C.INDEX_UNSET); } @Test public void getNextMediaPeriodInfo_withPostrollLoadError_returnsEmptyFinalMediaPeriodInfo() { - setupTimeline(/* initialPositionUs= */ 0, /* adGroupTimesUs= */ C.TIME_END_OF_SOURCE); + setupAdTimeline(/* adGroupTimesUs...= */ C.TIME_END_OF_SOURCE); assertGetNextMediaPeriodInfoReturnsContentMediaPeriod( + /* periodUid= */ firstPeriodUid, /* startPositionUs= */ 0, + /* requestedContentPositionUs= */ C.TIME_UNSET, /* endPositionUs= */ C.TIME_END_OF_SOURCE, /* durationUs= */ CONTENT_DURATION_US, - /* isLast= */ false, + /* isLastInPeriod= */ false, + /* isLastInWindow= */ false, /* nextAdGroupIndex= */ 0); advance(); setAdGroupFailedToLoad(/* adGroupIndex= */ 0); assertGetNextMediaPeriodInfoReturnsContentMediaPeriod( - /* startPositionUs= */ CONTENT_DURATION_US, + /* periodUid= */ firstPeriodUid, + /* startPositionUs= */ CONTENT_DURATION_US - 1, + /* requestedContentPositionUs= */ CONTENT_DURATION_US, /* endPositionUs= */ C.TIME_UNSET, /* durationUs= */ CONTENT_DURATION_US, - /* isLast= */ true, + /* isLastInPeriod= */ true, + /* isLastInWindow= */ true, + /* nextAdGroupIndex= */ C.INDEX_UNSET); + } + + @Test + public void getNextMediaPeriodInfo_inMultiPeriodWindow_returnsCorrectMediaPeriodInfos() { + setupTimeline( + new FakeTimeline( + new TimelineWindowDefinition( + /* periodCount= */ 2, + /* id= */ new Object(), + /* isSeekable= */ false, + /* isDynamic= */ false, + /* durationUs= */ 2 * CONTENT_DURATION_US))); + + assertGetNextMediaPeriodInfoReturnsContentMediaPeriod( + /* periodUid= */ playbackInfo.timeline.getUidOfPeriod(/* periodIndex= */ 0), + /* startPositionUs= */ 0, + /* requestedContentPositionUs= */ C.TIME_UNSET, + /* endPositionUs= */ C.TIME_UNSET, + /* durationUs= */ CONTENT_DURATION_US + + TimelineWindowDefinition.DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US, + /* isLastInPeriod= */ true, + /* isLastInWindow= */ false, + /* nextAdGroupIndex= */ C.INDEX_UNSET); + advance(); + assertGetNextMediaPeriodInfoReturnsContentMediaPeriod( + /* periodUid= */ playbackInfo.timeline.getUidOfPeriod(/* periodIndex= */ 1), + /* startPositionUs= */ 0, + /* requestedContentPositionUs= */ 0, + /* endPositionUs= */ C.TIME_UNSET, + /* durationUs= */ CONTENT_DURATION_US, + /* isLastInPeriod= */ true, + /* isLastInWindow= */ true, /* nextAdGroupIndex= */ C.INDEX_UNSET); } @Test public void updateQueuedPeriods_withDurationChangeAfterReadingPeriod_handlesChangeAndRemovesPeriodsAfterChangedPeriod() { - setupTimeline( - /* initialPositionUs= */ 0, - /* adGroupTimesUs= */ FIRST_AD_START_TIME_US, - SECOND_AD_START_TIME_US); + setupAdTimeline(/* adGroupTimesUs...= */ FIRST_AD_START_TIME_US, SECOND_AD_START_TIME_US); setAdGroupLoaded(/* adGroupIndex= */ 0); setAdGroupLoaded(/* adGroupIndex= */ 1); enqueueNext(); // Content before first ad. @@ -202,15 +264,13 @@ public final class MediaPeriodQueueTest { enqueueNext(); // Second ad. // Change position of second ad (= change duration of content between ads). - setupTimeline( - /* initialPositionUs= */ 0, - /* adGroupTimesUs= */ FIRST_AD_START_TIME_US, - SECOND_AD_START_TIME_US + 1); + updateAdPlaybackStateAndTimeline( + /* adGroupTimesUs...= */ FIRST_AD_START_TIME_US, SECOND_AD_START_TIME_US + 1); setAdGroupLoaded(/* adGroupIndex= */ 0); setAdGroupLoaded(/* adGroupIndex= */ 1); boolean changeHandled = mediaPeriodQueue.updateQueuedPeriods( - /* rendererPositionUs= */ 0, /* maxRendererReadPositionUs= */ 0); + playbackInfo.timeline, /* rendererPositionUs= */ 0, /* maxRendererReadPositionUs= */ 0); assertThat(changeHandled).isTrue(); assertThat(getQueueLength()).isEqualTo(3); @@ -219,10 +279,7 @@ public final class MediaPeriodQueueTest { @Test public void updateQueuedPeriods_withDurationChangeBeforeReadingPeriod_doesntHandleChangeAndRemovesPeriodsAfterChangedPeriod() { - setupTimeline( - /* initialPositionUs= */ 0, - /* adGroupTimesUs= */ FIRST_AD_START_TIME_US, - SECOND_AD_START_TIME_US); + setupAdTimeline(/* adGroupTimesUs...= */ FIRST_AD_START_TIME_US, SECOND_AD_START_TIME_US); setAdGroupLoaded(/* adGroupIndex= */ 0); setAdGroupLoaded(/* adGroupIndex= */ 1); enqueueNext(); // Content before first ad. @@ -233,15 +290,15 @@ public final class MediaPeriodQueueTest { advanceReading(); // Reading first ad. // Change position of first ad (= change duration of content before first ad). - setupTimeline( - /* initialPositionUs= */ 0, - /* adGroupTimesUs= */ FIRST_AD_START_TIME_US + 1, - SECOND_AD_START_TIME_US); + updateAdPlaybackStateAndTimeline( + /* adGroupTimesUs...= */ FIRST_AD_START_TIME_US + 1, SECOND_AD_START_TIME_US); setAdGroupLoaded(/* adGroupIndex= */ 0); setAdGroupLoaded(/* adGroupIndex= */ 1); boolean changeHandled = mediaPeriodQueue.updateQueuedPeriods( - /* rendererPositionUs= */ 0, /* maxRendererReadPositionUs= */ FIRST_AD_START_TIME_US); + playbackInfo.timeline, + /* rendererPositionUs= */ 0, + /* maxRendererReadPositionUs= */ FIRST_AD_START_TIME_US); assertThat(changeHandled).isFalse(); assertThat(getQueueLength()).isEqualTo(1); @@ -250,10 +307,7 @@ public final class MediaPeriodQueueTest { @Test public void updateQueuedPeriods_withDurationChangeInReadingPeriodAfterReadingPosition_handlesChangeAndRemovesPeriodsAfterChangedPeriod() { - setupTimeline( - /* initialPositionUs= */ 0, - /* adGroupTimesUs= */ FIRST_AD_START_TIME_US, - SECOND_AD_START_TIME_US); + setupAdTimeline(/* adGroupTimesUs...= */ FIRST_AD_START_TIME_US, SECOND_AD_START_TIME_US); setAdGroupLoaded(/* adGroupIndex= */ 0); setAdGroupLoaded(/* adGroupIndex= */ 1); enqueueNext(); // Content before first ad. @@ -265,15 +319,14 @@ public final class MediaPeriodQueueTest { advanceReading(); // Reading content between ads. // Change position of second ad (= change duration of content between ads). - setupTimeline( - /* initialPositionUs= */ 0, - /* adGroupTimesUs= */ FIRST_AD_START_TIME_US, - SECOND_AD_START_TIME_US - 1000); + updateAdPlaybackStateAndTimeline( + /* adGroupTimesUs...= */ FIRST_AD_START_TIME_US, SECOND_AD_START_TIME_US - 1000); setAdGroupLoaded(/* adGroupIndex= */ 0); setAdGroupLoaded(/* adGroupIndex= */ 1); long readingPositionAtStartOfContentBetweenAds = FIRST_AD_START_TIME_US + AD_DURATION_US; boolean changeHandled = mediaPeriodQueue.updateQueuedPeriods( + playbackInfo.timeline, /* rendererPositionUs= */ 0, /* maxRendererReadPositionUs= */ readingPositionAtStartOfContentBetweenAds); @@ -284,10 +337,7 @@ public final class MediaPeriodQueueTest { @Test public void updateQueuedPeriods_withDurationChangeInReadingPeriodBeforeReadingPosition_doesntHandleChangeAndRemovesPeriodsAfterChangedPeriod() { - setupTimeline( - /* initialPositionUs= */ 0, - /* adGroupTimesUs= */ FIRST_AD_START_TIME_US, - SECOND_AD_START_TIME_US); + setupAdTimeline(/* adGroupTimesUs...= */ FIRST_AD_START_TIME_US, SECOND_AD_START_TIME_US); setAdGroupLoaded(/* adGroupIndex= */ 0); setAdGroupLoaded(/* adGroupIndex= */ 1); enqueueNext(); // Content before first ad. @@ -299,15 +349,14 @@ public final class MediaPeriodQueueTest { advanceReading(); // Reading content between ads. // Change position of second ad (= change duration of content between ads). - setupTimeline( - /* initialPositionUs= */ 0, - /* adGroupTimesUs= */ FIRST_AD_START_TIME_US, - SECOND_AD_START_TIME_US - 1000); + updateAdPlaybackStateAndTimeline( + /* adGroupTimesUs...= */ FIRST_AD_START_TIME_US, SECOND_AD_START_TIME_US - 1000); setAdGroupLoaded(/* adGroupIndex= */ 0); setAdGroupLoaded(/* adGroupIndex= */ 1); long readingPositionAtEndOfContentBetweenAds = SECOND_AD_START_TIME_US + AD_DURATION_US; boolean changeHandled = mediaPeriodQueue.updateQueuedPeriods( + playbackInfo.timeline, /* rendererPositionUs= */ 0, /* maxRendererReadPositionUs= */ readingPositionAtEndOfContentBetweenAds); @@ -318,10 +367,7 @@ public final class MediaPeriodQueueTest { @Test public void updateQueuedPeriods_withDurationChangeInReadingPeriodReadToEnd_doesntHandleChangeAndRemovesPeriodsAfterChangedPeriod() { - setupTimeline( - /* initialPositionUs= */ 0, - /* adGroupTimesUs= */ FIRST_AD_START_TIME_US, - SECOND_AD_START_TIME_US); + setupAdTimeline(/* adGroupTimesUs...= */ FIRST_AD_START_TIME_US, SECOND_AD_START_TIME_US); setAdGroupLoaded(/* adGroupIndex= */ 0); setAdGroupLoaded(/* adGroupIndex= */ 1); enqueueNext(); // Content before first ad. @@ -333,43 +379,74 @@ public final class MediaPeriodQueueTest { advanceReading(); // Reading content between ads. // Change position of second ad (= change duration of content between ads). - setupTimeline( - /* initialPositionUs= */ 0, - /* adGroupTimesUs= */ FIRST_AD_START_TIME_US, - SECOND_AD_START_TIME_US - 1000); + updateAdPlaybackStateAndTimeline( + /* adGroupTimesUs...= */ FIRST_AD_START_TIME_US, SECOND_AD_START_TIME_US - 1000); setAdGroupLoaded(/* adGroupIndex= */ 0); setAdGroupLoaded(/* adGroupIndex= */ 1); boolean changeHandled = mediaPeriodQueue.updateQueuedPeriods( - /* rendererPositionUs= */ 0, /* maxRendererReadPositionUs= */ C.TIME_END_OF_SOURCE); + playbackInfo.timeline, + /* rendererPositionUs= */ 0, + /* maxRendererReadPositionUs= */ C.TIME_END_OF_SOURCE); assertThat(changeHandled).isFalse(); assertThat(getQueueLength()).isEqualTo(3); } - private void setupTimeline(long initialPositionUs, long... adGroupTimesUs) { + private void setupAdTimeline(long... adGroupTimesUs) { adPlaybackState = new AdPlaybackState(adGroupTimesUs).withContentDurationUs(CONTENT_DURATION_US); - timeline = new SinglePeriodAdTimeline(CONTENT_TIMELINE, adPlaybackState); - periodUid = timeline.getUidOfPeriod(/* periodIndex= */ 0); - mediaPeriodQueue.setTimeline(timeline); + SinglePeriodAdTimeline adTimeline = + new SinglePeriodAdTimeline(CONTENT_TIMELINE, adPlaybackState); + setupTimeline(adTimeline); + } + + private void setupTimeline(Timeline timeline) { + fakeMediaSource = new FakeMediaSource(timeline); + mediaSourceHolder = new MediaSourceList.MediaSourceHolder(fakeMediaSource, false); + mediaSourceHolder.mediaSource.prepareSourceInternal(/* mediaTransferListener */ null); + + Timeline playlistTimeline = createPlaylistTimeline(); + firstPeriodUid = playlistTimeline.getUidOfPeriod(/* periodIndex= */ 0); + playbackInfo = new PlaybackInfo( - timeline, - mediaPeriodQueue.resolveMediaPeriodIdForAds(periodUid, initialPositionUs), - /* startPositionUs= */ 0, - /* contentPositionUs= */ 0, + playlistTimeline, + mediaPeriodQueue.resolveMediaPeriodIdForAds( + playlistTimeline, firstPeriodUid, /* positionUs= */ 0), + /* requestedContentPositionUs= */ C.TIME_UNSET, Player.STATE_READY, /* playbackError= */ null, /* isLoading= */ false, /* trackGroups= */ null, /* trackSelectorResult= */ null, /* loadingMediaPeriodId= */ null, + /* playWhenReady= */ false, + Player.PLAYBACK_SUPPRESSION_REASON_NONE, /* bufferedPositionUs= */ 0, /* totalBufferedDurationUs= */ 0, /* positionUs= */ 0); } + private void updateAdPlaybackStateAndTimeline(long... adGroupTimesUs) { + adPlaybackState = + new AdPlaybackState(adGroupTimesUs).withContentDurationUs(CONTENT_DURATION_US); + updateTimeline(); + } + + private void updateTimeline() { + SinglePeriodAdTimeline adTimeline = + new SinglePeriodAdTimeline(CONTENT_TIMELINE, adPlaybackState); + fakeMediaSource.setNewSourceInfo(adTimeline); + playbackInfo = playbackInfo.copyWithTimeline(createPlaylistTimeline()); + } + + private MediaSourceList.PlaylistTimeline createPlaylistTimeline() { + return new MediaSourceList.PlaylistTimeline( + Collections.singleton(mediaSourceHolder), + new ShuffleOrder.DefaultShuffleOrder(/* length= */ 1)); + } + private void advance() { enqueueNext(); if (mediaPeriodQueue.getLoadingPeriod() != mediaPeriodQueue.getPlayingPeriod()) { @@ -390,7 +467,7 @@ public final class MediaPeriodQueueTest { rendererCapabilities, trackSelector, allocator, - mediaSource, + mediaSourceList, getNextMediaPeriodInfo(), new TrackSelectorResult( new RendererConfiguration[0], new TrackSelection[0], /* info= */ null)); @@ -422,27 +499,26 @@ public final class MediaPeriodQueueTest { updateTimeline(); } - private void updateTimeline() { - timeline = new SinglePeriodAdTimeline(CONTENT_TIMELINE, adPlaybackState); - mediaPeriodQueue.setTimeline(timeline); - } - private void assertGetNextMediaPeriodInfoReturnsContentMediaPeriod( + Object periodUid, long startPositionUs, + long requestedContentPositionUs, long endPositionUs, long durationUs, - boolean isLast, + boolean isLastInPeriod, + boolean isLastInWindow, int nextAdGroupIndex) { assertThat(getNextMediaPeriodInfo()) .isEqualTo( new MediaPeriodInfo( new MediaPeriodId(periodUid, /* windowSequenceNumber= */ 0, nextAdGroupIndex), startPositionUs, - /* contentPositionUs= */ C.TIME_UNSET, + requestedContentPositionUs, endPositionUs, durationUs, - /* isLastInTimelinePeriod= */ isLast, - /* isFinal= */ isLast)); + isLastInPeriod, + isLastInWindow, + /* isFinal= */ isLastInWindow)); } private void assertNextMediaPeriodInfoIsAd(int adGroupIndex, long contentPositionUs) { @@ -450,7 +526,7 @@ public final class MediaPeriodQueueTest { .isEqualTo( new MediaPeriodInfo( new MediaPeriodId( - periodUid, + firstPeriodUid, adGroupIndex, /* adIndexInAdGroup= */ 0, /* windowSequenceNumber= */ 0), @@ -459,6 +535,7 @@ public final class MediaPeriodQueueTest { /* endPositionUs= */ C.TIME_UNSET, /* durationUs= */ AD_DURATION_US, /* isLastInTimelinePeriod= */ false, + /* isLastInTimelineWindow= */ false, /* isFinal= */ false)); } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/PlaylistTest.java b/library/core/src/test/java/com/google/android/exoplayer2/MediaSourceListTest.java similarity index 67% rename from library/core/src/test/java/com/google/android/exoplayer2/PlaylistTest.java rename to library/core/src/test/java/com/google/android/exoplayer2/MediaSourceListTest.java index cc551db8ac..7ece4f3259 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/PlaylistTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/MediaSourceListTest.java @@ -37,46 +37,47 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; -/** Unit test for {@link Playlist}. */ +/** Unit test for {@link MediaSourceList}. */ @RunWith(AndroidJUnit4.class) -public class PlaylistTest { +public class MediaSourceListTest { - private static final int PLAYLIST_SIZE = 4; + private static final int MEDIA_SOURCE_LIST_SIZE = 4; - private Playlist playlist; + private MediaSourceList mediaSourceList; @Before public void setUp() { - playlist = new Playlist(mock(Playlist.PlaylistInfoRefreshListener.class)); + mediaSourceList = + new MediaSourceList(mock(MediaSourceList.MediaSourceListInfoRefreshListener.class)); } @Test - public void testEmptyPlaylist_expectConstantTimelineInstanceEMPTY() { + public void emptyMediaSourceList_expectConstantTimelineInstanceEMPTY() { ShuffleOrder.DefaultShuffleOrder shuffleOrder = new ShuffleOrder.DefaultShuffleOrder(/* length= */ 0); - List fakeHolders = createFakeHolders(); + List fakeHolders = createFakeHolders(); - Timeline timeline = playlist.setMediaSources(fakeHolders, shuffleOrder); + Timeline timeline = mediaSourceList.setMediaSources(fakeHolders, shuffleOrder); assertNotSame(timeline, Timeline.EMPTY); // Remove all media sources. timeline = - playlist.removeMediaSourceRange( + mediaSourceList.removeMediaSourceRange( /* fromIndex= */ 0, /* toIndex= */ timeline.getWindowCount(), shuffleOrder); assertSame(timeline, Timeline.EMPTY); - timeline = playlist.setMediaSources(fakeHolders, shuffleOrder); + timeline = mediaSourceList.setMediaSources(fakeHolders, shuffleOrder); assertNotSame(timeline, Timeline.EMPTY); // Clear. - timeline = playlist.clear(shuffleOrder); + timeline = mediaSourceList.clear(shuffleOrder); assertSame(timeline, Timeline.EMPTY); } @Test - public void testPrepareAndReprepareAfterRelease_expectSourcePreparationAfterPlaylistPrepare() { + public void prepareAndReprepareAfterRelease_expectSourcePreparationAfterMediaSourceListPrepare() { MediaSource mockMediaSource1 = mock(MediaSource.class); MediaSource mockMediaSource2 = mock(MediaSource.class); - playlist.setMediaSources( + mediaSourceList.setMediaSources( createFakeHoldersWithSources( /* useLazyPreparation= */ false, mockMediaSource1, mockMediaSource2), new ShuffleOrder.DefaultShuffleOrder(/* length= */ 2)); @@ -88,8 +89,8 @@ public class PlaylistTest { .prepareSource( any(MediaSource.MediaSourceCaller.class), /* mediaTransferListener= */ isNull()); - playlist.prepare(/* mediaTransferListener= */ null); - assertThat(playlist.isPrepared()).isTrue(); + mediaSourceList.prepare(/* mediaTransferListener= */ null); + assertThat(mediaSourceList.isPrepared()).isTrue(); // Verify prepare is called once on prepare. verify(mockMediaSource1, times(1)) .prepareSource( @@ -98,8 +99,8 @@ public class PlaylistTest { .prepareSource( any(MediaSource.MediaSourceCaller.class), /* mediaTransferListener= */ isNull()); - playlist.release(); - playlist.prepare(/* mediaTransferListener= */ null); + mediaSourceList.release(); + mediaSourceList.prepare(/* mediaTransferListener= */ null); // Verify prepare is called a second time on re-prepare. verify(mockMediaSource1, times(2)) .prepareSource( @@ -110,36 +111,36 @@ public class PlaylistTest { } @Test - public void testSetMediaSources_playlistUnprepared_notUsingLazyPreparation() { + public void setMediaSources_mediaSourceListUnprepared_notUsingLazyPreparation() { ShuffleOrder.DefaultShuffleOrder shuffleOrder = new ShuffleOrder.DefaultShuffleOrder(/* length= */ 2); MediaSource mockMediaSource1 = mock(MediaSource.class); MediaSource mockMediaSource2 = mock(MediaSource.class); - List mediaSources = + List mediaSources = createFakeHoldersWithSources( /* useLazyPreparation= */ false, mockMediaSource1, mockMediaSource2); - Timeline timeline = playlist.setMediaSources(mediaSources, shuffleOrder); + Timeline timeline = mediaSourceList.setMediaSources(mediaSources, shuffleOrder); assertThat(timeline.getWindowCount()).isEqualTo(2); - assertThat(playlist.getSize()).isEqualTo(2); + assertThat(mediaSourceList.getSize()).isEqualTo(2); // Assert holder offsets have been set properly for (int i = 0; i < mediaSources.size(); i++) { - Playlist.MediaSourceHolder mediaSourceHolder = mediaSources.get(i); + MediaSourceList.MediaSourceHolder mediaSourceHolder = mediaSources.get(i); assertThat(mediaSourceHolder.isRemoved).isFalse(); assertThat(mediaSourceHolder.firstWindowIndexInChild).isEqualTo(i); } // Set media items again. The second holder is re-used. - List moreMediaSources = + List moreMediaSources = createFakeHoldersWithSources(/* useLazyPreparation= */ false, mock(MediaSource.class)); moreMediaSources.add(mediaSources.get(1)); - timeline = playlist.setMediaSources(moreMediaSources, shuffleOrder); + timeline = mediaSourceList.setMediaSources(moreMediaSources, shuffleOrder); - assertThat(playlist.getSize()).isEqualTo(2); + assertThat(mediaSourceList.getSize()).isEqualTo(2); assertThat(timeline.getWindowCount()).isEqualTo(2); for (int i = 0; i < moreMediaSources.size(); i++) { - Playlist.MediaSourceHolder mediaSourceHolder = moreMediaSources.get(i); + MediaSourceList.MediaSourceHolder mediaSourceHolder = moreMediaSources.get(i); assertThat(mediaSourceHolder.isRemoved).isFalse(); assertThat(mediaSourceHolder.firstWindowIndexInChild).isEqualTo(i); } @@ -152,17 +153,17 @@ public class PlaylistTest { } @Test - public void testSetMediaSources_playlistPrepared_notUsingLazyPreparation() { + public void setMediaSources_mediaSourceListPrepared_notUsingLazyPreparation() { ShuffleOrder.DefaultShuffleOrder shuffleOrder = new ShuffleOrder.DefaultShuffleOrder(/* length= */ 2); MediaSource mockMediaSource1 = mock(MediaSource.class); MediaSource mockMediaSource2 = mock(MediaSource.class); - List mediaSources = + List mediaSources = createFakeHoldersWithSources( /* useLazyPreparation= */ false, mockMediaSource1, mockMediaSource2); - playlist.prepare(/* mediaTransferListener= */ null); - playlist.setMediaSources(mediaSources, shuffleOrder); + mediaSourceList.prepare(/* mediaTransferListener= */ null); + mediaSourceList.setMediaSources(mediaSources, shuffleOrder); // Verify sources are prepared. verify(mockMediaSource1, times(1)) @@ -173,10 +174,10 @@ public class PlaylistTest { any(MediaSource.MediaSourceCaller.class), /* mediaTransferListener= */ isNull()); // Set media items again. The second holder is re-used. - List moreMediaSources = + List moreMediaSources = createFakeHoldersWithSources(/* useLazyPreparation= */ false, mock(MediaSource.class)); moreMediaSources.add(mediaSources.get(1)); - playlist.setMediaSources(moreMediaSources, shuffleOrder); + mediaSourceList.setMediaSources(moreMediaSources, shuffleOrder); // Expect removed holders and sources to be removed and released. verify(mockMediaSource1, times(1)).releaseSource(any(MediaSource.MediaSourceCaller.class)); @@ -190,15 +191,16 @@ public class PlaylistTest { } @Test - public void testAddMediaSources_playlistUnprepared_notUsingLazyPreparation_expectUnprepared() { + public void addMediaSources_mediaSourceListUnprepared_notUsingLazyPreparation_expectUnprepared() { MediaSource mockMediaSource1 = mock(MediaSource.class); MediaSource mockMediaSource2 = mock(MediaSource.class); - List mediaSources = + List mediaSources = createFakeHoldersWithSources( /* useLazyPreparation= */ false, mockMediaSource1, mockMediaSource2); - playlist.addMediaSources(/* index= */ 0, mediaSources, new ShuffleOrder.DefaultShuffleOrder(2)); + mediaSourceList.addMediaSources( + /* index= */ 0, mediaSources, new ShuffleOrder.DefaultShuffleOrder(2)); - assertThat(playlist.getSize()).isEqualTo(2); + assertThat(mediaSourceList.getSize()).isEqualTo(2); // Verify lazy initialization does not call prepare on sources. verify(mockMediaSource1, times(0)) .prepareSource( @@ -213,8 +215,8 @@ public class PlaylistTest { } // Add for more sources in between. - List moreMediaSources = createFakeHolders(); - playlist.addMediaSources( + List moreMediaSources = createFakeHolders(); + mediaSourceList.addMediaSources( /* index= */ 1, moreMediaSources, new ShuffleOrder.DefaultShuffleOrder(/* length= */ 3)); assertThat(mediaSources.get(0).firstWindowIndexInChild).isEqualTo(0); @@ -224,11 +226,11 @@ public class PlaylistTest { } @Test - public void testAddMediaSources_playlistPrepared_notUsingLazyPreparation_expectPrepared() { + public void addMediaSources_mediaSourceListPrepared_notUsingLazyPreparation_expectPrepared() { MediaSource mockMediaSource1 = mock(MediaSource.class); MediaSource mockMediaSource2 = mock(MediaSource.class); - playlist.prepare(/* mediaTransferListener= */ null); - playlist.addMediaSources( + mediaSourceList.prepare(/* mediaTransferListener= */ null); + mediaSourceList.addMediaSources( /* index= */ 0, createFakeHoldersWithSources( /* useLazyPreparation= */ false, mockMediaSource1, mockMediaSource2), @@ -244,46 +246,46 @@ public class PlaylistTest { } @Test - public void testMoveMediaSources() { + public void moveMediaSources() { ShuffleOrder.DefaultShuffleOrder shuffleOrder = new ShuffleOrder.DefaultShuffleOrder(/* length= */ 4); - List holders = createFakeHolders(); - playlist.addMediaSources(/* index= */ 0, holders, shuffleOrder); + List holders = createFakeHolders(); + mediaSourceList.addMediaSources(/* index= */ 0, holders, shuffleOrder); assertDefaultFirstWindowInChildIndexOrder(holders); - playlist.moveMediaSource(/* currentIndex= */ 0, /* newIndex= */ 3, shuffleOrder); + mediaSourceList.moveMediaSource(/* currentIndex= */ 0, /* newIndex= */ 3, shuffleOrder); assertFirstWindowInChildIndices(holders, 3, 0, 1, 2); - playlist.moveMediaSource(/* currentIndex= */ 3, /* newIndex= */ 0, shuffleOrder); + mediaSourceList.moveMediaSource(/* currentIndex= */ 3, /* newIndex= */ 0, shuffleOrder); assertDefaultFirstWindowInChildIndexOrder(holders); - playlist.moveMediaSourceRange( + mediaSourceList.moveMediaSourceRange( /* fromIndex= */ 0, /* toIndex= */ 2, /* newFromIndex= */ 2, shuffleOrder); assertFirstWindowInChildIndices(holders, 2, 3, 0, 1); - playlist.moveMediaSourceRange( + mediaSourceList.moveMediaSourceRange( /* fromIndex= */ 2, /* toIndex= */ 4, /* newFromIndex= */ 0, shuffleOrder); assertDefaultFirstWindowInChildIndexOrder(holders); - playlist.moveMediaSourceRange( + mediaSourceList.moveMediaSourceRange( /* fromIndex= */ 0, /* toIndex= */ 2, /* newFromIndex= */ 2, shuffleOrder); assertFirstWindowInChildIndices(holders, 2, 3, 0, 1); - playlist.moveMediaSourceRange( + mediaSourceList.moveMediaSourceRange( /* fromIndex= */ 2, /* toIndex= */ 3, /* newFromIndex= */ 0, shuffleOrder); assertFirstWindowInChildIndices(holders, 0, 3, 1, 2); - playlist.moveMediaSourceRange( + mediaSourceList.moveMediaSourceRange( /* fromIndex= */ 3, /* toIndex= */ 4, /* newFromIndex= */ 1, shuffleOrder); assertDefaultFirstWindowInChildIndexOrder(holders); // No-ops. - playlist.moveMediaSourceRange( + mediaSourceList.moveMediaSourceRange( /* fromIndex= */ 0, /* toIndex= */ 4, /* newFromIndex= */ 0, shuffleOrder); assertDefaultFirstWindowInChildIndexOrder(holders); - playlist.moveMediaSourceRange( + mediaSourceList.moveMediaSourceRange( /* fromIndex= */ 0, /* toIndex= */ 0, /* newFromIndex= */ 3, shuffleOrder); assertDefaultFirstWindowInChildIndexOrder(holders); } @Test - public void testRemoveMediaSources_whenUnprepared_expectNoRelease() { + public void removeMediaSources_whenUnprepared_expectNoRelease() { MediaSource mockMediaSource1 = mock(MediaSource.class); MediaSource mockMediaSource2 = mock(MediaSource.class); MediaSource mockMediaSource3 = mock(MediaSource.class); @@ -291,19 +293,19 @@ public class PlaylistTest { ShuffleOrder.DefaultShuffleOrder shuffleOrder = new ShuffleOrder.DefaultShuffleOrder(/* length= */ 4); - List holders = + List holders = createFakeHoldersWithSources( /* useLazyPreparation= */ false, mockMediaSource1, mockMediaSource2, mockMediaSource3, mockMediaSource4); - playlist.addMediaSources(/* index= */ 0, holders, shuffleOrder); - playlist.removeMediaSourceRange(/* fromIndex= */ 1, /* toIndex= */ 3, shuffleOrder); + mediaSourceList.addMediaSources(/* index= */ 0, holders, shuffleOrder); + mediaSourceList.removeMediaSourceRange(/* fromIndex= */ 1, /* toIndex= */ 3, shuffleOrder); - assertThat(playlist.getSize()).isEqualTo(2); - Playlist.MediaSourceHolder removedHolder1 = holders.remove(1); - Playlist.MediaSourceHolder removedHolder2 = holders.remove(1); + assertThat(mediaSourceList.getSize()).isEqualTo(2); + MediaSourceList.MediaSourceHolder removedHolder1 = holders.remove(1); + MediaSourceList.MediaSourceHolder removedHolder2 = holders.remove(1); assertDefaultFirstWindowInChildIndexOrder(holders); assertThat(removedHolder1.isRemoved).isTrue(); @@ -315,7 +317,7 @@ public class PlaylistTest { } @Test - public void testRemoveMediaSources_whenPrepared_expectRelease() { + public void removeMediaSources_whenPrepared_expectRelease() { MediaSource mockMediaSource1 = mock(MediaSource.class); MediaSource mockMediaSource2 = mock(MediaSource.class); MediaSource mockMediaSource3 = mock(MediaSource.class); @@ -323,18 +325,18 @@ public class PlaylistTest { ShuffleOrder.DefaultShuffleOrder shuffleOrder = new ShuffleOrder.DefaultShuffleOrder(/* length= */ 4); - List holders = + List holders = createFakeHoldersWithSources( /* useLazyPreparation= */ false, mockMediaSource1, mockMediaSource2, mockMediaSource3, mockMediaSource4); - playlist.prepare(/* mediaTransferListener */ null); - playlist.addMediaSources(/* index= */ 0, holders, shuffleOrder); - playlist.removeMediaSourceRange(/* fromIndex= */ 1, /* toIndex= */ 3, shuffleOrder); + mediaSourceList.prepare(/* mediaTransferListener */ null); + mediaSourceList.addMediaSources(/* index= */ 0, holders, shuffleOrder); + mediaSourceList.removeMediaSourceRange(/* fromIndex= */ 1, /* toIndex= */ 3, shuffleOrder); - assertThat(playlist.getSize()).isEqualTo(2); + assertThat(mediaSourceList.getSize()).isEqualTo(2); holders.remove(2); holders.remove(1); @@ -346,53 +348,53 @@ public class PlaylistTest { } @Test - public void testRelease_playlistUnprepared_expectSourcesNotReleased() { + public void release_mediaSourceListUnprepared_expectSourcesNotReleased() { MediaSource mockMediaSource = mock(MediaSource.class); - Playlist.MediaSourceHolder mediaSourceHolder = - new Playlist.MediaSourceHolder(mockMediaSource, /* useLazyPreparation= */ false); + MediaSourceList.MediaSourceHolder mediaSourceHolder = + new MediaSourceList.MediaSourceHolder(mockMediaSource, /* useLazyPreparation= */ false); - playlist.setMediaSources( + mediaSourceList.setMediaSources( Collections.singletonList(mediaSourceHolder), new ShuffleOrder.DefaultShuffleOrder(/* length= */ 1)); verify(mockMediaSource, times(0)) .prepareSource( any(MediaSource.MediaSourceCaller.class), /* mediaTransferListener= */ isNull()); - playlist.release(); + mediaSourceList.release(); verify(mockMediaSource, times(0)).releaseSource(any(MediaSource.MediaSourceCaller.class)); assertThat(mediaSourceHolder.isRemoved).isFalse(); } @Test - public void testRelease_playlistPrepared_expectSourcesReleasedNotRemoved() { + public void release_mediaSourceListPrepared_expectSourcesReleasedNotRemoved() { MediaSource mockMediaSource = mock(MediaSource.class); - Playlist.MediaSourceHolder mediaSourceHolder = - new Playlist.MediaSourceHolder(mockMediaSource, /* useLazyPreparation= */ false); + MediaSourceList.MediaSourceHolder mediaSourceHolder = + new MediaSourceList.MediaSourceHolder(mockMediaSource, /* useLazyPreparation= */ false); - playlist.prepare(/* mediaTransferListener= */ null); - playlist.setMediaSources( + mediaSourceList.prepare(/* mediaTransferListener= */ null); + mediaSourceList.setMediaSources( Collections.singletonList(mediaSourceHolder), new ShuffleOrder.DefaultShuffleOrder(/* length= */ 1)); verify(mockMediaSource, times(1)) .prepareSource( any(MediaSource.MediaSourceCaller.class), /* mediaTransferListener= */ isNull()); - playlist.release(); + mediaSourceList.release(); verify(mockMediaSource, times(1)).releaseSource(any(MediaSource.MediaSourceCaller.class)); assertThat(mediaSourceHolder.isRemoved).isFalse(); } @Test - public void testClearPlaylist_expectSourcesReleasedAndRemoved() { + public void clearMediaSourceList_expectSourcesReleasedAndRemoved() { ShuffleOrder.DefaultShuffleOrder shuffleOrder = new ShuffleOrder.DefaultShuffleOrder(/* length= */ 4); MediaSource mockMediaSource1 = mock(MediaSource.class); MediaSource mockMediaSource2 = mock(MediaSource.class); - List holders = + List holders = createFakeHoldersWithSources( /* useLazyPreparation= */ false, mockMediaSource1, mockMediaSource2); - playlist.setMediaSources(holders, shuffleOrder); - playlist.prepare(/* mediaTransferListener= */ null); + mediaSourceList.setMediaSources(holders, shuffleOrder); + mediaSourceList.prepare(/* mediaTransferListener= */ null); - Timeline timeline = playlist.clear(shuffleOrder); + Timeline timeline = mediaSourceList.clear(shuffleOrder); assertThat(timeline.isEmpty()).isTrue(); assertThat(holders.get(0).isRemoved).isTrue(); assertThat(holders.get(1).isRemoved).isTrue(); @@ -401,59 +403,63 @@ public class PlaylistTest { } @Test - public void testSetMediaSources_expectTimelineUsesCustomShuffleOrder() { + public void setMediaSources_expectTimelineUsesCustomShuffleOrder() { Timeline timeline = - playlist.setMediaSources(createFakeHolders(), new FakeShuffleOrder(/* length=*/ 4)); + mediaSourceList.setMediaSources(createFakeHolders(), new FakeShuffleOrder(/* length=*/ 4)); assertTimelineUsesFakeShuffleOrder(timeline); } @Test - public void testAddMediaSources_expectTimelineUsesCustomShuffleOrder() { + public void addMediaSources_expectTimelineUsesCustomShuffleOrder() { Timeline timeline = - playlist.addMediaSources( - /* index= */ 0, createFakeHolders(), new FakeShuffleOrder(PLAYLIST_SIZE)); + mediaSourceList.addMediaSources( + /* index= */ 0, createFakeHolders(), new FakeShuffleOrder(MEDIA_SOURCE_LIST_SIZE)); assertTimelineUsesFakeShuffleOrder(timeline); } @Test - public void testMoveMediaSources_expectTimelineUsesCustomShuffleOrder() { - ShuffleOrder shuffleOrder = new ShuffleOrder.DefaultShuffleOrder(/* length= */ PLAYLIST_SIZE); - playlist.addMediaSources(/* index= */ 0, createFakeHolders(), shuffleOrder); + public void moveMediaSources_expectTimelineUsesCustomShuffleOrder() { + ShuffleOrder shuffleOrder = + new ShuffleOrder.DefaultShuffleOrder(/* length= */ MEDIA_SOURCE_LIST_SIZE); + mediaSourceList.addMediaSources(/* index= */ 0, createFakeHolders(), shuffleOrder); Timeline timeline = - playlist.moveMediaSource( - /* currentIndex= */ 0, /* newIndex= */ 1, new FakeShuffleOrder(PLAYLIST_SIZE)); + mediaSourceList.moveMediaSource( + /* currentIndex= */ 0, /* newIndex= */ 1, new FakeShuffleOrder(MEDIA_SOURCE_LIST_SIZE)); assertTimelineUsesFakeShuffleOrder(timeline); } @Test - public void testMoveMediaSourceRange_expectTimelineUsesCustomShuffleOrder() { - ShuffleOrder shuffleOrder = new ShuffleOrder.DefaultShuffleOrder(/* length= */ PLAYLIST_SIZE); - playlist.addMediaSources(/* index= */ 0, createFakeHolders(), shuffleOrder); + public void moveMediaSourceRange_expectTimelineUsesCustomShuffleOrder() { + ShuffleOrder shuffleOrder = + new ShuffleOrder.DefaultShuffleOrder(/* length= */ MEDIA_SOURCE_LIST_SIZE); + mediaSourceList.addMediaSources(/* index= */ 0, createFakeHolders(), shuffleOrder); Timeline timeline = - playlist.moveMediaSourceRange( + mediaSourceList.moveMediaSourceRange( /* fromIndex= */ 0, /* toIndex= */ 2, /* newFromIndex= */ 2, - new FakeShuffleOrder(PLAYLIST_SIZE)); + new FakeShuffleOrder(MEDIA_SOURCE_LIST_SIZE)); assertTimelineUsesFakeShuffleOrder(timeline); } @Test - public void testRemoveMediaSourceRange_expectTimelineUsesCustomShuffleOrder() { - ShuffleOrder shuffleOrder = new ShuffleOrder.DefaultShuffleOrder(/* length= */ PLAYLIST_SIZE); - playlist.addMediaSources(/* index= */ 0, createFakeHolders(), shuffleOrder); + public void removeMediaSourceRange_expectTimelineUsesCustomShuffleOrder() { + ShuffleOrder shuffleOrder = + new ShuffleOrder.DefaultShuffleOrder(/* length= */ MEDIA_SOURCE_LIST_SIZE); + mediaSourceList.addMediaSources(/* index= */ 0, createFakeHolders(), shuffleOrder); Timeline timeline = - playlist.removeMediaSourceRange( + mediaSourceList.removeMediaSourceRange( /* fromIndex= */ 0, /* toIndex= */ 2, new FakeShuffleOrder(/* length= */ 2)); assertTimelineUsesFakeShuffleOrder(timeline); } @Test - public void testSetShuffleOrder_expectTimelineUsesCustomShuffleOrder() { - playlist.setMediaSources( - createFakeHolders(), new ShuffleOrder.DefaultShuffleOrder(/* length= */ PLAYLIST_SIZE)); + public void setShuffleOrder_expectTimelineUsesCustomShuffleOrder() { + mediaSourceList.setMediaSources( + createFakeHolders(), + new ShuffleOrder.DefaultShuffleOrder(/* length= */ MEDIA_SOURCE_LIST_SIZE)); assertTimelineUsesFakeShuffleOrder( - playlist.setShuffleOrder(new FakeShuffleOrder(PLAYLIST_SIZE))); + mediaSourceList.setShuffleOrder(new FakeShuffleOrder(MEDIA_SOURCE_LIST_SIZE))); } // Internal methods. @@ -472,7 +478,7 @@ public class PlaylistTest { } private static void assertDefaultFirstWindowInChildIndexOrder( - List holders) { + List holders) { int[] indices = new int[holders.size()]; for (int i = 0; i < indices.length; i++) { indices[i] = i; @@ -481,28 +487,29 @@ public class PlaylistTest { } private static void assertFirstWindowInChildIndices( - List holders, int... firstWindowInChildIndices) { + List holders, int... firstWindowInChildIndices) { assertThat(holders).hasSize(firstWindowInChildIndices.length); for (int i = 0; i < holders.size(); i++) { assertThat(holders.get(i).firstWindowIndexInChild).isEqualTo(firstWindowInChildIndices[i]); } } - private static List createFakeHolders() { + private static List createFakeHolders() { MediaSource fakeMediaSource = new FakeMediaSource(new FakeTimeline(1)); - List holders = new ArrayList<>(); - for (int i = 0; i < PLAYLIST_SIZE; i++) { - holders.add(new Playlist.MediaSourceHolder(fakeMediaSource, /* useLazyPreparation= */ true)); + List holders = new ArrayList<>(); + for (int i = 0; i < MEDIA_SOURCE_LIST_SIZE; i++) { + holders.add( + new MediaSourceList.MediaSourceHolder(fakeMediaSource, /* useLazyPreparation= */ true)); } return holders; } - private static List createFakeHoldersWithSources( + private static List createFakeHoldersWithSources( boolean useLazyPreparation, MediaSource... sources) { - List holders = new ArrayList<>(); + List holders = new ArrayList<>(); for (MediaSource mediaSource : sources) { holders.add( - new Playlist.MediaSourceHolder( + new MediaSourceList.MediaSourceHolder( mediaSource, /* useLazyPreparation= */ useLazyPreparation)); } return holders; diff --git a/library/core/src/test/java/com/google/android/exoplayer2/PlayerMessageTest.java b/library/core/src/test/java/com/google/android/exoplayer2/PlayerMessageTest.java index a7ef451752..874a8c5a5a 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/PlayerMessageTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/PlayerMessageTest.java @@ -53,7 +53,7 @@ public class PlayerMessageTest { initMocks(this); PlayerMessage.Sender sender = (message) -> {}; PlayerMessage.Target target = (messageType, payload) -> {}; - handlerThread = new HandlerThread("TestHandlerThread"); + handlerThread = new HandlerThread("TestHandler"); handlerThread.start(); Handler handler = new Handler(handlerThread.getLooper()); message = diff --git a/library/core/src/test/java/com/google/android/exoplayer2/TimelineTest.java b/library/core/src/test/java/com/google/android/exoplayer2/TimelineTest.java index 5110ad411c..a151507db4 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/TimelineTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/TimelineTest.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer2; import static com.google.common.truth.Truth.assertThat; +import androidx.annotation.Nullable; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.testutil.FakeTimeline; import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition; @@ -29,12 +30,12 @@ import org.junit.runner.RunWith; public class TimelineTest { @Test - public void testEmptyTimeline() { + public void emptyTimeline() { TimelineAsserts.assertEmpty(Timeline.EMPTY); } @Test - public void testSinglePeriodTimeline() { + public void singlePeriodTimeline() { Timeline timeline = new FakeTimeline(new TimelineWindowDefinition(1, 111)); TimelineAsserts.assertWindowTags(timeline, 111); TimelineAsserts.assertPeriodCounts(timeline, 1); @@ -48,7 +49,7 @@ public class TimelineTest { } @Test - public void testMultiPeriodTimeline() { + public void multiPeriodTimeline() { Timeline timeline = new FakeTimeline(new TimelineWindowDefinition(5, 111)); TimelineAsserts.assertWindowTags(timeline, 111); TimelineAsserts.assertPeriodCounts(timeline, 5); @@ -61,13 +62,19 @@ public class TimelineTest { TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, false, 0); } + @SuppressWarnings("deprecation") // Tests the deprecated window.tag property. @Test - public void testWindowEquals() { + public void windowEquals() { + MediaItem mediaItem = new MediaItem.Builder().setUri("uri").setTag(new Object()).build(); Timeline.Window window = new Timeline.Window(); assertThat(window).isEqualTo(new Timeline.Window()); Timeline.Window otherWindow = new Timeline.Window(); - otherWindow.tag = new Object(); + otherWindow.mediaItem = mediaItem; + assertThat(window).isNotEqualTo(otherWindow); + + otherWindow = new Timeline.Window(); + otherWindow.tag = mediaItem.playbackProperties.tag; assertThat(window).isNotEqualTo(otherWindow); otherWindow = new Timeline.Window(); @@ -94,6 +101,10 @@ public class TimelineTest { otherWindow.isLive = true; assertThat(window).isNotEqualTo(otherWindow); + otherWindow = new Timeline.Window(); + otherWindow.isPlaceholder = true; + assertThat(window).isNotEqualTo(otherWindow); + otherWindow = new Timeline.Window(); otherWindow.defaultPositionUs = C.TIME_UNSET; assertThat(window).isNotEqualTo(otherWindow); @@ -114,19 +125,31 @@ public class TimelineTest { otherWindow.positionInFirstPeriodUs = C.TIME_UNSET; assertThat(window).isNotEqualTo(otherWindow); - window.uid = new Object(); - window.tag = new Object(); - window.manifest = new Object(); - window.presentationStartTimeMs = C.TIME_UNSET; - window.windowStartTimeMs = C.TIME_UNSET; - window.isSeekable = true; - window.isDynamic = true; - window.isLive = true; - window.defaultPositionUs = C.TIME_UNSET; - window.durationUs = C.TIME_UNSET; - window.firstPeriodIndex = 1; - window.lastPeriodIndex = 1; - window.positionInFirstPeriodUs = C.TIME_UNSET; + window = populateWindow(mediaItem, mediaItem.playbackProperties.tag); + otherWindow = + otherWindow.set( + window.uid, + window.mediaItem, + window.manifest, + window.presentationStartTimeMs, + window.windowStartTimeMs, + window.elapsedRealtimeEpochOffsetMs, + window.isSeekable, + window.isDynamic, + window.isLive, + window.defaultPositionUs, + window.durationUs, + window.firstPeriodIndex, + window.lastPeriodIndex, + window.positionInFirstPeriodUs); + assertThat(window).isEqualTo(otherWindow); + } + + @SuppressWarnings("deprecation") + @Test + public void windowSet_withTag() { + Timeline.Window window = populateWindow(/* mediaItem= */ null, new Object()); + Timeline.Window otherWindow = new Timeline.Window(); otherWindow = otherWindow.set( window.uid, @@ -134,6 +157,7 @@ public class TimelineTest { window.manifest, window.presentationStartTimeMs, window.windowStartTimeMs, + window.elapsedRealtimeEpochOffsetMs, window.isSeekable, window.isDynamic, window.isLive, @@ -146,19 +170,19 @@ public class TimelineTest { } @Test - public void testWindowHashCode() { + public void windowHashCode() { Timeline.Window window = new Timeline.Window(); Timeline.Window otherWindow = new Timeline.Window(); assertThat(window.hashCode()).isEqualTo(otherWindow.hashCode()); - window.tag = new Object(); + window.mediaItem = new MediaItem.Builder().setMediaId("mediaId").setTag(new Object()).build(); assertThat(window.hashCode()).isNotEqualTo(otherWindow.hashCode()); - otherWindow.tag = window.tag; + otherWindow.mediaItem = window.mediaItem; assertThat(window.hashCode()).isEqualTo(otherWindow.hashCode()); } @Test - public void testPeriodEquals() { + public void periodEquals() { Timeline.Period period = new Timeline.Period(); assertThat(period).isEqualTo(new Timeline.Period()); @@ -194,7 +218,7 @@ public class TimelineTest { } @Test - public void testPeriodHashCode() { + public void periodHashCode() { Timeline.Period period = new Timeline.Period(); Timeline.Period otherPeriod = new Timeline.Period(); assertThat(period.hashCode()).isEqualTo(otherPeriod.hashCode()); @@ -204,4 +228,25 @@ public class TimelineTest { otherPeriod.windowIndex = period.windowIndex; assertThat(period.hashCode()).isEqualTo(otherPeriod.hashCode()); } + + @SuppressWarnings("deprecation") // Populates the deprecated window.tag property. + private static Timeline.Window populateWindow( + @Nullable MediaItem mediaItem, @Nullable Object tag) { + Timeline.Window window = new Timeline.Window(); + window.uid = new Object(); + window.tag = tag; + window.mediaItem = mediaItem; + window.manifest = new Object(); + window.presentationStartTimeMs = C.TIME_UNSET; + window.windowStartTimeMs = C.TIME_UNSET; + window.isSeekable = true; + window.isDynamic = true; + window.isLive = true; + window.defaultPositionUs = C.TIME_UNSET; + window.durationUs = C.TIME_UNSET; + window.firstPeriodIndex = 1; + window.lastPeriodIndex = 1; + window.positionInFirstPeriodUs = C.TIME_UNSET; + return window; + } } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/WakeLockManagerTest.java b/library/core/src/test/java/com/google/android/exoplayer2/WakeLockManagerTest.java new file mode 100644 index 0000000000..da9fedd230 --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/WakeLockManagerTest.java @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2020 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; + +import static com.google.common.truth.Truth.assertThat; + +import android.content.Context; +import android.os.PowerManager.WakeLock; +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.shadows.ShadowPowerManager; + +/** Unit tests for {@link WakeLockManager} */ +@RunWith(AndroidJUnit4.class) +public class WakeLockManagerTest { + + private Context context; + + @Before + public void setUp() { + context = ApplicationProvider.getApplicationContext(); + } + + @Test + public void stayAwakeFalse_wakeLockIsNeverHeld() { + WakeLockManager wakeLockManager = new WakeLockManager(context); + wakeLockManager.setEnabled(true); + wakeLockManager.setStayAwake(false); + + WakeLock wakeLock = ShadowPowerManager.getLatestWakeLock(); + assertThat(wakeLock.isHeld()).isFalse(); + + wakeLockManager.setEnabled(false); + + assertThat(wakeLock.isHeld()).isFalse(); + } + + @Test + public void stayAwakeTrue_wakeLockIsOnlyHeldWhenEnabled() { + WakeLockManager wakeLockManager = new WakeLockManager(context); + wakeLockManager.setEnabled(true); + wakeLockManager.setStayAwake(true); + + WakeLock wakeLock = ShadowPowerManager.getLatestWakeLock(); + + assertThat(wakeLock.isHeld()).isTrue(); + + wakeLockManager.setEnabled(false); + + assertThat(wakeLock.isHeld()).isFalse(); + } +} 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 2e26529a81..1d22984f84 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 @@ -17,22 +17,19 @@ package com.google.android.exoplayer2.analytics; import static com.google.common.truth.Truth.assertThat; -import android.os.Handler; -import android.os.SystemClock; import android.view.Surface; import androidx.annotation.Nullable; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.Format; -import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Renderer; import com.google.android.exoplayer2.RenderersFactory; import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.Timeline.Window; -import com.google.android.exoplayer2.audio.AudioRendererEventListener; import com.google.android.exoplayer2.decoder.DecoderCounters; import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.source.ConcatenatingMediaSource; @@ -41,20 +38,23 @@ import com.google.android.exoplayer2.source.MediaLoadData; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; 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.ExoPlayerTestRunner; -import com.google.android.exoplayer2.testutil.ExoPlayerTestRunner.Builder; +import com.google.android.exoplayer2.testutil.FakeAudioRenderer; import com.google.android.exoplayer2.testutil.FakeMediaSource; -import com.google.android.exoplayer2.testutil.FakeRenderer; import com.google.android.exoplayer2.testutil.FakeTimeline; +import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition; +import com.google.android.exoplayer2.testutil.FakeVideoRenderer; import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.util.Util; -import com.google.android.exoplayer2.video.VideoRendererEventListener; import java.io.IOException; import java.util.ArrayList; import java.util.Iterator; import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.annotation.LooperMode; @@ -65,12 +65,14 @@ import org.robolectric.annotation.LooperMode.Mode; @LooperMode(Mode.PAUSED) public final class AnalyticsCollectorTest { + private static final String TAG = "AnalyticsCollectorTest"; + private static final int EVENT_PLAYER_STATE_CHANGED = 0; private static final int EVENT_TIMELINE_CHANGED = 1; private static final int EVENT_POSITION_DISCONTINUITY = 2; private static final int EVENT_SEEK_STARTED = 3; private static final int EVENT_SEEK_PROCESSED = 4; - private static final int EVENT_PLAYBACK_PARAMETERS_CHANGED = 5; + private static final int EVENT_PLAYBACK_SPEED_CHANGED = 5; private static final int EVENT_REPEAT_MODE_CHANGED = 6; private static final int EVENT_SHUFFLE_MODE_CHANGED = 7; private static final int EVENT_LOADING_CHANGED = 8; @@ -103,6 +105,7 @@ public final class AnalyticsCollectorTest { private static final int EVENT_DRM_KEYS_REMOVED = 36; private static final int EVENT_DRM_SESSION_ACQUIRED = 37; private static final int EVENT_DRM_SESSION_RELEASED = 38; + private static final int EVENT_VIDEO_FRAME_PROCESSING_OFFSET = 39; private static final int TIMEOUT_MS = 10000; private static final Timeline SINGLE_PERIOD_TIMELINE = new FakeTimeline(/* windowCount= */ 1); @@ -122,35 +125,38 @@ public final class AnalyticsCollectorTest { private EventWindowAndPeriodId window1Period0Seq1; @Test - public void testEmptyTimeline() throws Exception { + public void emptyTimeline() throws Exception { FakeMediaSource mediaSource = new FakeMediaSource( - Timeline.EMPTY, - ExoPlayerTestRunner.Builder.VIDEO_FORMAT, - ExoPlayerTestRunner.Builder.AUDIO_FORMAT); + Timeline.EMPTY, ExoPlayerTestRunner.VIDEO_FORMAT, ExoPlayerTestRunner.AUDIO_FORMAT); TestAnalyticsListener listener = runAnalyticsTest(mediaSource); assertThat(listener.getEvents(EVENT_PLAYER_STATE_CHANGED)) .containsExactly( WINDOW_0 /* setPlayWhenReady */, WINDOW_0 /* BUFFERING */, WINDOW_0 /* ENDED */); - assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED)).containsExactly(WINDOW_0); + assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED)) + .containsExactly(WINDOW_0 /* PLAYLIST_CHANGED */, WINDOW_0 /* SOURCE_UPDATE */); listener.assertNoMoreEvents(); } @Test - public void testSinglePeriod() throws Exception { + public void singlePeriod() throws Exception { FakeMediaSource mediaSource = - new FakeMediaSource(SINGLE_PERIOD_TIMELINE, Builder.VIDEO_FORMAT, Builder.AUDIO_FORMAT); + new FakeMediaSource( + SINGLE_PERIOD_TIMELINE, + ExoPlayerTestRunner.VIDEO_FORMAT, + ExoPlayerTestRunner.AUDIO_FORMAT); TestAnalyticsListener listener = runAnalyticsTest(mediaSource); - populateEventIds(SINGLE_PERIOD_TIMELINE); + populateEventIds(listener.lastReportedTimeline); assertThat(listener.getEvents(EVENT_PLAYER_STATE_CHANGED)) .containsExactly( WINDOW_0 /* setPlayWhenReady */, WINDOW_0 /* BUFFERING */, period0 /* READY */, period0 /* ENDED */); - assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED)).containsExactly(WINDOW_0); + assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED)) + .containsExactly(WINDOW_0 /* PLAYLIST_CHANGED */, WINDOW_0 /* SOURCE_UPDATE */); assertThat(listener.getEvents(EVENT_LOADING_CHANGED)) .containsExactly(period0 /* started */, period0 /* stopped */); assertThat(listener.getEvents(EVENT_TRACKS_CHANGED)).containsExactly(period0); @@ -172,16 +178,22 @@ public final class AnalyticsCollectorTest { assertThat(listener.getEvents(EVENT_DROPPED_VIDEO_FRAMES)).containsExactly(period0); assertThat(listener.getEvents(EVENT_VIDEO_SIZE_CHANGED)).containsExactly(period0); assertThat(listener.getEvents(EVENT_RENDERED_FIRST_FRAME)).containsExactly(period0); + assertThat(listener.getEvents(EVENT_VIDEO_FRAME_PROCESSING_OFFSET)).containsExactly(period0); listener.assertNoMoreEvents(); } @Test - public void testAutomaticPeriodTransition() throws Exception { + public void automaticPeriodTransition() throws Exception { MediaSource mediaSource = new ConcatenatingMediaSource( - new FakeMediaSource(SINGLE_PERIOD_TIMELINE, Builder.VIDEO_FORMAT, Builder.AUDIO_FORMAT), new FakeMediaSource( - SINGLE_PERIOD_TIMELINE, Builder.VIDEO_FORMAT, Builder.AUDIO_FORMAT)); + SINGLE_PERIOD_TIMELINE, + ExoPlayerTestRunner.VIDEO_FORMAT, + ExoPlayerTestRunner.AUDIO_FORMAT), + new FakeMediaSource( + SINGLE_PERIOD_TIMELINE, + ExoPlayerTestRunner.VIDEO_FORMAT, + ExoPlayerTestRunner.AUDIO_FORMAT)); TestAnalyticsListener listener = runAnalyticsTest(mediaSource); populateEventIds(listener.lastReportedTimeline); @@ -191,7 +203,8 @@ public final class AnalyticsCollectorTest { WINDOW_0 /* BUFFERING */, period0 /* READY */, period1 /* ENDED */); - assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED)).containsExactly(WINDOW_0); + assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED)) + .containsExactly(WINDOW_0 /* PLAYLIST_CHANGED */, period0 /* SOURCE_UPDATE */); assertThat(listener.getEvents(EVENT_POSITION_DISCONTINUITY)).containsExactly(period1); assertThat(listener.getEvents(EVENT_LOADING_CHANGED)) .containsExactly(period0, period0, period0, period0); @@ -224,17 +237,18 @@ public final class AnalyticsCollectorTest { period0 /* audio */, period0 /* video */, period1 /* audio */, period1 /* video */); assertThat(listener.getEvents(EVENT_AUDIO_SESSION_ID)).containsExactly(period0); assertThat(listener.getEvents(EVENT_DROPPED_VIDEO_FRAMES)).containsExactly(period1); - assertThat(listener.getEvents(EVENT_VIDEO_SIZE_CHANGED)).containsExactly(period0); - assertThat(listener.getEvents(EVENT_RENDERED_FIRST_FRAME)).containsExactly(period0); + assertThat(listener.getEvents(EVENT_VIDEO_SIZE_CHANGED)).containsExactly(period0, period1); + assertThat(listener.getEvents(EVENT_RENDERED_FIRST_FRAME)).containsExactly(period0, period1); + assertThat(listener.getEvents(EVENT_VIDEO_FRAME_PROCESSING_OFFSET)).containsExactly(period1); listener.assertNoMoreEvents(); } @Test - public void testPeriodTransitionWithRendererChange() throws Exception { + public void periodTransitionWithRendererChange() throws Exception { MediaSource mediaSource = new ConcatenatingMediaSource( - new FakeMediaSource(SINGLE_PERIOD_TIMELINE, Builder.VIDEO_FORMAT), - new FakeMediaSource(SINGLE_PERIOD_TIMELINE, Builder.AUDIO_FORMAT)); + new FakeMediaSource(SINGLE_PERIOD_TIMELINE, ExoPlayerTestRunner.VIDEO_FORMAT), + new FakeMediaSource(SINGLE_PERIOD_TIMELINE, ExoPlayerTestRunner.AUDIO_FORMAT)); TestAnalyticsListener listener = runAnalyticsTest(mediaSource); populateEventIds(listener.lastReportedTimeline); @@ -243,10 +257,9 @@ public final class AnalyticsCollectorTest { WINDOW_0 /* setPlayWhenReady */, WINDOW_0 /* BUFFERING */, period0 /* READY */, - period1 /* BUFFERING */, - period1 /* READY */, period1 /* ENDED */); - assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED)).containsExactly(WINDOW_0); + assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED)) + .containsExactly(WINDOW_0 /* PLAYLIST_CHANGED */, period0 /* SOURCE_UPDATE */); assertThat(listener.getEvents(EVENT_POSITION_DISCONTINUITY)).containsExactly(period1); assertThat(listener.getEvents(EVENT_LOADING_CHANGED)) .containsExactly(period0, period0, period0, period0); @@ -279,19 +292,27 @@ public final class AnalyticsCollectorTest { assertThat(listener.getEvents(EVENT_DROPPED_VIDEO_FRAMES)).containsExactly(period0); assertThat(listener.getEvents(EVENT_VIDEO_SIZE_CHANGED)).containsExactly(period0); assertThat(listener.getEvents(EVENT_RENDERED_FIRST_FRAME)).containsExactly(period0); + assertThat(listener.getEvents(EVENT_VIDEO_FRAME_PROCESSING_OFFSET)).containsExactly(period0); listener.assertNoMoreEvents(); } @Test - public void testSeekToOtherPeriod() throws Exception { + public void seekToOtherPeriod() throws Exception { MediaSource mediaSource = new ConcatenatingMediaSource( - new FakeMediaSource(SINGLE_PERIOD_TIMELINE, Builder.VIDEO_FORMAT), - new FakeMediaSource(SINGLE_PERIOD_TIMELINE, Builder.AUDIO_FORMAT)); + new FakeMediaSource( + SINGLE_PERIOD_TIMELINE, + ExoPlayerTestRunner.VIDEO_FORMAT, + ExoPlayerTestRunner.AUDIO_FORMAT), + new FakeMediaSource(SINGLE_PERIOD_TIMELINE, ExoPlayerTestRunner.AUDIO_FORMAT)); ActionSchedule actionSchedule = - new ActionSchedule.Builder("AnalyticsCollectorTest") + new ActionSchedule.Builder(TAG) .pause() - .waitForPlaybackState(Player.STATE_READY) + // Wait until second period has fully loaded to assert loading events without flakiness. + .waitForIsLoading(true) + .waitForIsLoading(false) + .waitForIsLoading(true) + .waitForIsLoading(false) .seek(/* windowIndex= */ 1, /* positionMs= */ 0) .play() .build(); @@ -305,10 +326,11 @@ public final class AnalyticsCollectorTest { WINDOW_0 /* setPlayWhenReady=false */, period0 /* READY */, period1 /* BUFFERING */, - period1 /* READY */, period1 /* setPlayWhenReady=true */, + period1 /* READY */, period1 /* ENDED */); - assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED)).containsExactly(WINDOW_0); + assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED)) + .containsExactly(WINDOW_0 /* PLAYLIST_CHANGED */, period0 /* SOURCE_UPDATE */); assertThat(listener.getEvents(EVENT_POSITION_DISCONTINUITY)).containsExactly(period1); assertThat(listener.getEvents(EVENT_SEEK_STARTED)).containsExactly(period0); assertThat(listener.getEvents(EVENT_SEEK_PROCESSED)).containsExactly(period1); @@ -329,34 +351,37 @@ public final class AnalyticsCollectorTest { WINDOW_1 /* manifest */, period1 /* media */); assertThat(listener.getEvents(EVENT_DOWNSTREAM_FORMAT_CHANGED)) - .containsExactly(period0 /* video */, period1 /* audio */); + .containsExactly(period0 /* video */, period0 /* audio */, period1 /* audio */); assertThat(listener.getEvents(EVENT_MEDIA_PERIOD_CREATED)).containsExactly(period0, period1); assertThat(listener.getEvents(EVENT_MEDIA_PERIOD_RELEASED)).containsExactly(period0); assertThat(listener.getEvents(EVENT_READING_STARTED)).containsExactly(period0, period1); assertThat(listener.getEvents(EVENT_DECODER_ENABLED)) - .containsExactly(period0 /* video */, period1 /* audio */); + .containsExactly(period0 /* video */, period0 /* audio */, period1 /* audio */); assertThat(listener.getEvents(EVENT_DECODER_INIT)) - .containsExactly(period0 /* video */, period1 /* audio */); + .containsExactly(period0 /* video */, period0 /* audio */, period1 /* audio */); assertThat(listener.getEvents(EVENT_DECODER_FORMAT_CHANGED)) - .containsExactly(period0 /* video */, period1 /* audio */); - assertThat(listener.getEvents(EVENT_DECODER_DISABLED)).containsExactly(period0); - assertThat(listener.getEvents(EVENT_AUDIO_SESSION_ID)).containsExactly(period1); + .containsExactly(period0 /* video */, period0 /* audio */, period1 /* audio */); + assertThat(listener.getEvents(EVENT_DECODER_DISABLED)) + .containsExactly(period0 /* video */, period0 /* audio */); + assertThat(listener.getEvents(EVENT_AUDIO_SESSION_ID)).containsExactly(period0, period1); assertThat(listener.getEvents(EVENT_VIDEO_SIZE_CHANGED)).containsExactly(period0); assertThat(listener.getEvents(EVENT_RENDERED_FIRST_FRAME)).containsExactly(period0); listener.assertNoMoreEvents(); } @Test - public void testSeekBackAfterReadingAhead() throws Exception { + public void seekBackAfterReadingAhead() throws Exception { MediaSource mediaSource = new ConcatenatingMediaSource( - new FakeMediaSource(SINGLE_PERIOD_TIMELINE, Builder.VIDEO_FORMAT), + new FakeMediaSource(SINGLE_PERIOD_TIMELINE, ExoPlayerTestRunner.VIDEO_FORMAT), new FakeMediaSource( - SINGLE_PERIOD_TIMELINE, Builder.VIDEO_FORMAT, Builder.AUDIO_FORMAT)); + SINGLE_PERIOD_TIMELINE, + ExoPlayerTestRunner.VIDEO_FORMAT, + ExoPlayerTestRunner.AUDIO_FORMAT)); long periodDurationMs = SINGLE_PERIOD_TIMELINE.getWindow(/* windowIndex= */ 0, new Window()).getDurationMs(); ActionSchedule actionSchedule = - new ActionSchedule.Builder("AnalyticsCollectorTest") + new ActionSchedule.Builder(TAG) .pause() .waitForPlaybackState(Player.STATE_READY) .playUntilPosition(/* windowIndex= */ 0, periodDurationMs) @@ -377,10 +402,9 @@ public final class AnalyticsCollectorTest { period0 /* BUFFERING */, period0 /* READY */, period0 /* setPlayWhenReady=true */, - period1Seq2 /* BUFFERING */, - period1Seq2 /* READY */, period1Seq2 /* ENDED */); - assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED)).containsExactly(WINDOW_0); + assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED)) + .containsExactly(WINDOW_0 /* PLAYLIST_CHANGED */, period0 /* SOURCE_UPDATE */); assertThat(listener.getEvents(EVENT_POSITION_DISCONTINUITY)) .containsExactly(period0, period1Seq2); assertThat(listener.getEvents(EVENT_SEEK_STARTED)).containsExactly(period0); @@ -403,7 +427,7 @@ public final class AnalyticsCollectorTest { period1Seq1 /* media */, period1Seq2 /* media */); assertThat(listener.getEvents(EVENT_DOWNSTREAM_FORMAT_CHANGED)) - .containsExactly(period0, period1Seq1, period1Seq2, period1Seq2); + .containsExactly(period0, period1Seq1, period1Seq1, period1Seq2, period1Seq2); assertThat(listener.getEvents(EVENT_MEDIA_PERIOD_CREATED)) .containsExactly(period0, period1Seq1, period1Seq2); assertThat(listener.getEvents(EVENT_MEDIA_PERIOD_RELEASED)) @@ -411,35 +435,52 @@ public final class AnalyticsCollectorTest { assertThat(listener.getEvents(EVENT_READING_STARTED)) .containsExactly(period0, period1Seq1, period1Seq2); assertThat(listener.getEvents(EVENT_DECODER_ENABLED)) - .containsExactly(period0, period0, period1Seq2); + .containsExactly(period0, period1, period0, period1Seq2); assertThat(listener.getEvents(EVENT_DECODER_INIT)) - .containsExactly(period0, period1Seq1, period1Seq2, period1Seq2); + .containsExactly(period0, period1Seq1, period1Seq1, period1Seq2, period1Seq2); assertThat(listener.getEvents(EVENT_DECODER_FORMAT_CHANGED)) - .containsExactly(period0, period1Seq1, period1Seq2, period1Seq2); - assertThat(listener.getEvents(EVENT_DECODER_DISABLED)).containsExactly(period0); - assertThat(listener.getEvents(EVENT_AUDIO_SESSION_ID)).containsExactly(period1Seq2); + .containsExactly(period0, period1Seq1, period1Seq1, period1Seq2, period1Seq2); + assertThat(listener.getEvents(EVENT_DECODER_DISABLED)).containsExactly(period0, period0); + assertThat(listener.getEvents(EVENT_AUDIO_SESSION_ID)) + .containsExactly(period1Seq1, period1Seq2); assertThat(listener.getEvents(EVENT_DROPPED_VIDEO_FRAMES)) - .containsExactly(period0, period0, period1Seq2); - assertThat(listener.getEvents(EVENT_VIDEO_SIZE_CHANGED)).containsExactly(period0, period1Seq2); + .containsExactly(period0, period1Seq2); + assertThat(listener.getEvents(EVENT_VIDEO_SIZE_CHANGED)) + .containsExactly(period0, period1Seq1, period0, period1Seq2); assertThat(listener.getEvents(EVENT_RENDERED_FIRST_FRAME)) + .containsExactly(period0, period1Seq1, period0, period1Seq2); + assertThat(listener.getEvents(EVENT_VIDEO_FRAME_PROCESSING_OFFSET)) .containsExactly(period0, period1Seq2); listener.assertNoMoreEvents(); } @Test - public void testPrepareNewSource() throws Exception { - MediaSource mediaSource1 = new FakeMediaSource(SINGLE_PERIOD_TIMELINE, Builder.VIDEO_FORMAT); - MediaSource mediaSource2 = new FakeMediaSource(SINGLE_PERIOD_TIMELINE, Builder.VIDEO_FORMAT); + public void prepareNewSource() throws Exception { + MediaSource mediaSource1 = + new FakeMediaSource(SINGLE_PERIOD_TIMELINE, ExoPlayerTestRunner.VIDEO_FORMAT); + MediaSource mediaSource2 = + new FakeMediaSource(SINGLE_PERIOD_TIMELINE, ExoPlayerTestRunner.VIDEO_FORMAT); ActionSchedule actionSchedule = - new ActionSchedule.Builder("AnalyticsCollectorTest") + new ActionSchedule.Builder(TAG) .pause() .waitForPlaybackState(Player.STATE_READY) - .prepareSource(mediaSource2) + .setMediaSources(/* resetPosition= */ false, mediaSource2) + .waitForTimelineChanged() + // Wait until loading started to prevent flakiness caused by loading finishing too fast. + .waitForIsLoading(true) .play() .build(); TestAnalyticsListener listener = runAnalyticsTest(mediaSource1, actionSchedule); - populateEventIds(SINGLE_PERIOD_TIMELINE); + // Populate all event ids with last timeline (after second prepare). + populateEventIds(listener.lastReportedTimeline); + // Populate event id of period 0, sequence 0 with timeline of initial preparation. + period0Seq0 = + new EventWindowAndPeriodId( + /* windowIndex= */ 0, + new MediaPeriodId( + listener.reportedTimelines.get(1).getUidOfPeriod(/* periodIndex= */ 0), + /* windowSequenceNumber= */ 0)); assertThat(listener.getEvents(EVENT_PLAYER_STATE_CHANGED)) .containsExactly( WINDOW_0 /* setPlayWhenReady=true */, @@ -447,16 +488,20 @@ public final class AnalyticsCollectorTest { WINDOW_0 /* setPlayWhenReady=false */, period0Seq0 /* READY */, WINDOW_0 /* BUFFERING */, - WINDOW_0 /* setPlayWhenReady=true */, + period0Seq1 /* setPlayWhenReady=true */, period0Seq1 /* READY */, period0Seq1 /* ENDED */); assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED)) - .containsExactly(WINDOW_0 /* prepared */, WINDOW_0 /* reset */, WINDOW_0 /* prepared */); + .containsExactly( + WINDOW_0 /* PLAYLIST_CHANGE */, + WINDOW_0 /* SOURCE_UPDATE */, + WINDOW_0 /* PLAYLIST_CHANGE */, + WINDOW_0 /* SOURCE_UPDATE */); assertThat(listener.getEvents(EVENT_LOADING_CHANGED)) .containsExactly(period0Seq0, period0Seq0, period0Seq1, period0Seq1); assertThat(listener.getEvents(EVENT_TRACKS_CHANGED)) .containsExactly( - period0Seq0 /* prepared */, WINDOW_0 /* reset */, period0Seq1 /* prepared */); + period0Seq0 /* prepared */, WINDOW_0 /* setMediaSources */, period0Seq1 /* prepared */); assertThat(listener.getEvents(EVENT_LOAD_STARTED)) .containsExactly( WINDOW_0 /* manifest */, @@ -485,38 +530,47 @@ public final class AnalyticsCollectorTest { .containsExactly(period0Seq0, period0Seq1); assertThat(listener.getEvents(EVENT_RENDERED_FIRST_FRAME)) .containsExactly(period0Seq0, period0Seq1); + assertThat(listener.getEvents(EVENT_VIDEO_FRAME_PROCESSING_OFFSET)) + .containsExactly(period0Seq1); listener.assertNoMoreEvents(); } @Test - public void testReprepareAfterError() throws Exception { - MediaSource mediaSource = new FakeMediaSource(SINGLE_PERIOD_TIMELINE, Builder.VIDEO_FORMAT); + public void reprepareAfterError() throws Exception { + MediaSource mediaSource = + new FakeMediaSource(SINGLE_PERIOD_TIMELINE, ExoPlayerTestRunner.VIDEO_FORMAT); ActionSchedule actionSchedule = - new ActionSchedule.Builder("AnalyticsCollectorTest") + new ActionSchedule.Builder(TAG) + .pause() .waitForPlaybackState(Player.STATE_READY) .throwPlaybackException(ExoPlaybackException.createForSource(new IOException())) .waitForPlaybackState(Player.STATE_IDLE) .seek(/* positionMs= */ 0) - .prepareSource(mediaSource, /* resetPosition= */ false, /* resetState= */ false) + .prepare() + // Wait until loading started to assert loading events without flakiness. + .waitForIsLoading(true) + .play() .waitForPlaybackState(Player.STATE_ENDED) .build(); TestAnalyticsListener listener = runAnalyticsTest(mediaSource, actionSchedule); - populateEventIds(SINGLE_PERIOD_TIMELINE); + populateEventIds(listener.lastReportedTimeline); assertThat(listener.getEvents(EVENT_PLAYER_STATE_CHANGED)) .containsExactly( WINDOW_0 /* setPlayWhenReady=true */, + WINDOW_0 /* setPlayWhenReady=false */, WINDOW_0 /* BUFFERING */, period0Seq0 /* READY */, - WINDOW_0 /* IDLE */, - WINDOW_0 /* BUFFERING */, + period0Seq0 /* IDLE */, + period0Seq0 /* BUFFERING */, + period0Seq0 /* setPlayWhenReady=true */, period0Seq0 /* READY */, period0Seq0 /* ENDED */); assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED)) .containsExactly(WINDOW_0 /* prepared */, WINDOW_0 /* prepared */); - assertThat(listener.getEvents(EVENT_POSITION_DISCONTINUITY)).containsExactly(WINDOW_0); - assertThat(listener.getEvents(EVENT_SEEK_STARTED)).containsExactly(WINDOW_0); - assertThat(listener.getEvents(EVENT_SEEK_PROCESSED)).containsExactly(WINDOW_0); + assertThat(listener.getEvents(EVENT_POSITION_DISCONTINUITY)).containsExactly(period0Seq0); + assertThat(listener.getEvents(EVENT_SEEK_STARTED)).containsExactly(period0Seq0); + assertThat(listener.getEvents(EVENT_SEEK_PROCESSED)).containsExactly(period0Seq0); assertThat(listener.getEvents(EVENT_LOADING_CHANGED)) .containsExactly(period0Seq0, period0Seq0, period0Seq0, period0Seq0); assertThat(listener.getEvents(EVENT_PLAYER_ERROR)).containsExactly(period0Seq0); @@ -544,25 +598,26 @@ public final class AnalyticsCollectorTest { assertThat(listener.getEvents(EVENT_DECODER_FORMAT_CHANGED)) .containsExactly(period0Seq0, period0Seq0); assertThat(listener.getEvents(EVENT_DECODER_DISABLED)).containsExactly(period0Seq0); - assertThat(listener.getEvents(EVENT_DROPPED_VIDEO_FRAMES)) - .containsExactly(period0Seq0, period0Seq0); + assertThat(listener.getEvents(EVENT_DROPPED_VIDEO_FRAMES)).containsExactly(period0Seq0); assertThat(listener.getEvents(EVENT_VIDEO_SIZE_CHANGED)) .containsExactly(period0Seq0, period0Seq0); assertThat(listener.getEvents(EVENT_RENDERED_FIRST_FRAME)) .containsExactly(period0Seq0, period0Seq0); + assertThat(listener.getEvents(EVENT_VIDEO_FRAME_PROCESSING_OFFSET)) + .containsExactly(period0Seq0); listener.assertNoMoreEvents(); } @Test - public void testDynamicTimelineChange() throws Exception { + public void dynamicTimelineChange() throws Exception { MediaSource childMediaSource = - new FakeMediaSource(SINGLE_PERIOD_TIMELINE, Builder.VIDEO_FORMAT); + new FakeMediaSource(SINGLE_PERIOD_TIMELINE, ExoPlayerTestRunner.VIDEO_FORMAT); final ConcatenatingMediaSource concatenatedMediaSource = new ConcatenatingMediaSource(childMediaSource, childMediaSource); long periodDurationMs = SINGLE_PERIOD_TIMELINE.getWindow(/* windowIndex= */ 0, new Window()).getDurationMs(); ActionSchedule actionSchedule = - new ActionSchedule.Builder("AnalyticsCollectorTest") + new ActionSchedule.Builder(TAG) .pause() .waitForPlaybackState(Player.STATE_READY) // Ensure second period is already being read from. @@ -572,6 +627,7 @@ public final class AnalyticsCollectorTest { concatenatedMediaSource.moveMediaSource( /* currentIndex= */ 0, /* newIndex= */ 1)) .waitForTimelineChanged() + .waitForPlaybackState(Player.STATE_READY) .play() .build(); TestAnalyticsListener listener = runAnalyticsTest(concatenatedMediaSource, actionSchedule); @@ -587,8 +643,13 @@ public final class AnalyticsCollectorTest { window0Period1Seq0 /* setPlayWhenReady=false */, period1Seq0 /* setPlayWhenReady=true */, period1Seq0 /* BUFFERING */, + period1Seq0 /* READY */, period1Seq0 /* ENDED */); - assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED)).containsExactly(WINDOW_0, period1Seq0); + assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED)) + .containsExactly( + WINDOW_0 /* PLAYLIST_CHANGED */, + window0Period1Seq0 /* SOURCE_UPDATE (concatenated timeline replaces dummy) */, + period1Seq0 /* SOURCE_UPDATE (child sources in concatenating source moved) */); assertThat(listener.getEvents(EVENT_LOADING_CHANGED)) .containsExactly( window0Period1Seq0, window0Period1Seq0, window0Period1Seq0, window0Period1Seq0); @@ -617,17 +678,467 @@ public final class AnalyticsCollectorTest { assertThat(listener.getEvents(EVENT_DECODER_FORMAT_CHANGED)) .containsExactly(window0Period1Seq0, window1Period0Seq1); assertThat(listener.getEvents(EVENT_DECODER_DISABLED)).containsExactly(window0Period1Seq0); - assertThat(listener.getEvents(EVENT_DROPPED_VIDEO_FRAMES)).containsExactly(window0Period1Seq0); - assertThat(listener.getEvents(EVENT_VIDEO_SIZE_CHANGED)).containsExactly(window0Period1Seq0); - assertThat(listener.getEvents(EVENT_RENDERED_FIRST_FRAME)).containsExactly(window0Period1Seq0); + assertThat(listener.getEvents(EVENT_DROPPED_VIDEO_FRAMES)) + .containsExactly(window0Period1Seq0, period1Seq0); + assertThat(listener.getEvents(EVENT_VIDEO_SIZE_CHANGED)) + .containsExactly(window0Period1Seq0, window1Period0Seq1, period1Seq0); + assertThat(listener.getEvents(EVENT_RENDERED_FIRST_FRAME)) + .containsExactly(window0Period1Seq0, window1Period0Seq1, period1Seq0); + assertThat(listener.getEvents(EVENT_VIDEO_FRAME_PROCESSING_OFFSET)) + .containsExactly(window0Period1Seq0, period1Seq0); listener.assertNoMoreEvents(); } @Test - public void testNotifyExternalEvents() throws Exception { + public void playlistOperations() throws Exception { + MediaSource fakeMediaSource = + new FakeMediaSource(SINGLE_PERIOD_TIMELINE, ExoPlayerTestRunner.VIDEO_FORMAT); + ActionSchedule actionSchedule = + new ActionSchedule.Builder(TAG) + .pause() + .waitForPlaybackState(Player.STATE_READY) + .addMediaSources(fakeMediaSource) + // Wait until second period has fully loaded to assert loading events without flakiness. + .waitForIsLoading(true) + .waitForIsLoading(false) + .removeMediaItem(/* index= */ 0) + .waitForPlaybackState(Player.STATE_BUFFERING) + .waitForPlaybackState(Player.STATE_READY) + .play() + .build(); + TestAnalyticsListener listener = runAnalyticsTest(fakeMediaSource, actionSchedule); + + // Populate event ids with second to last timeline that still contained both periods. + populateEventIds(listener.reportedTimelines.get(listener.reportedTimelines.size() - 2)); + // Expect the second period with window index 0 and increased window sequence after the removal + // moved the period to another window index. + period0Seq1 = + new EventWindowAndPeriodId( + /* windowIndex= */ 0, + new MediaPeriodId( + listener.lastReportedTimeline.getUidOfPeriod(/* periodIndex= */ 0), + /* windowSequenceNumber= */ 1)); + assertThat(listener.getEvents(EVENT_PLAYER_STATE_CHANGED)) + .containsExactly( + WINDOW_0 /* setPlayWhenReady=true */, + WINDOW_0 /* setPlayWhenReady=false */, + WINDOW_0 /* BUFFERING */, + period0Seq0 /* READY */, + period0Seq1 /* BUFFERING */, + period0Seq1 /* READY */, + period0Seq1 /* setPlayWhenReady=true */, + period0Seq1 /* ENDED */); + assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED)) + .containsExactly( + WINDOW_0 /* PLAYLIST_CHANGED */, + WINDOW_0 /* SOURCE_UPDATE (first item) */, + period0Seq0 /* PLAYLIST_CHANGED (add) */, + period0Seq0 /* SOURCE_UPDATE (second item) */, + period0Seq1 /* PLAYLIST_CHANGED (remove) */); + assertThat(listener.getEvents(EVENT_LOADING_CHANGED)) + .containsExactly(period0Seq0, period0Seq0, period0Seq0, period0Seq0); + assertThat(listener.getEvents(EVENT_TRACKS_CHANGED)).containsExactly(period0Seq0, period0Seq1); + assertThat(listener.getEvents(EVENT_LOAD_STARTED)) + .containsExactly(WINDOW_0 /* manifest */, period0Seq0 /* media */, period1Seq1 /* media */); + assertThat(listener.getEvents(EVENT_LOAD_COMPLETED)) + .containsExactly(WINDOW_0 /* manifest */, period0Seq0 /* media */, period1Seq1 /* media */); + assertThat(listener.getEvents(EVENT_DOWNSTREAM_FORMAT_CHANGED)) + .containsExactly(period0Seq0, period0Seq1); + assertThat(listener.getEvents(EVENT_MEDIA_PERIOD_CREATED)) + .containsExactly(period0Seq0, period1Seq1); + assertThat(listener.getEvents(EVENT_MEDIA_PERIOD_RELEASED)).containsExactly(period0Seq0); + assertThat(listener.getEvents(EVENT_READING_STARTED)).containsExactly(period0Seq0, period0Seq1); + assertThat(listener.getEvents(EVENT_DECODER_ENABLED)) + .containsExactly(period0Seq0, period0Seq1, period0Seq1); + assertThat(listener.getEvents(EVENT_DECODER_INIT)).containsExactly(period0Seq0, period0Seq1); + assertThat(listener.getEvents(EVENT_DECODER_FORMAT_CHANGED)) + .containsExactly(period0Seq0, period0Seq1); + assertThat(listener.getEvents(EVENT_DECODER_DISABLED)) + .containsExactly(period0Seq0, period0Seq0); + assertThat(listener.getEvents(EVENT_DROPPED_VIDEO_FRAMES)).containsExactly(period0Seq1); + assertThat(listener.getEvents(EVENT_VIDEO_SIZE_CHANGED)) + .containsExactly(period0Seq0, period0Seq1); + assertThat(listener.getEvents(EVENT_RENDERED_FIRST_FRAME)) + .containsExactly(period0Seq0, period0Seq1); + assertThat(listener.getEvents(EVENT_VIDEO_FRAME_PROCESSING_OFFSET)) + .containsExactly(period0Seq1); + listener.assertNoMoreEvents(); + } + + @Test + public void adPlayback() throws Exception { + long contentDurationsUs = 10 * C.MICROS_PER_SECOND; + AtomicReference adPlaybackState = + new AtomicReference<>( + FakeTimeline.createAdPlaybackState( + /* adsPerAdGroup= */ 1, /* adGroupTimesUs...= */ + 0, + TimelineWindowDefinition.DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US + + 5 * C.MICROS_PER_SECOND, + C.TIME_END_OF_SOURCE)); + AtomicInteger playedAdCount = new AtomicInteger(0); + Timeline adTimeline = + new FakeTimeline( + new TimelineWindowDefinition( + /* periodCount= */ 1, + /* id= */ 0, + /* isSeekable= */ true, + /* isDynamic= */ false, + contentDurationsUs, + adPlaybackState.get())); + FakeMediaSource fakeMediaSource = + new FakeMediaSource(adTimeline, ExoPlayerTestRunner.VIDEO_FORMAT); + ActionSchedule actionSchedule = + new ActionSchedule.Builder(TAG) + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + player.addListener( + new Player.EventListener() { + @Override + public void onPositionDiscontinuity( + @Player.DiscontinuityReason int reason) { + if (!player.isPlayingAd() + && reason == Player.DISCONTINUITY_REASON_AD_INSERTION) { + // Finished playing ad. Marked as played. + adPlaybackState.set( + adPlaybackState + .get() + .withPlayedAd( + playedAdCount.getAndIncrement(), + /* adIndexInAdGroup= */ 0)); + fakeMediaSource.setNewSourceInfo( + new FakeTimeline( + new TimelineWindowDefinition( + /* periodCount= */ 1, + /* id= */ 0, + /* isSeekable= */ true, + /* isDynamic= */ false, + /* durationUs =*/ 10 * C.MICROS_PER_SECOND, + adPlaybackState.get()))); + } + } + }); + } + }) + .pause() + // Ensure everything is preloaded. + .waitForIsLoading(true) + .waitForIsLoading(false) + .waitForIsLoading(true) + .waitForIsLoading(false) + .waitForIsLoading(true) + .waitForIsLoading(false) + .waitForIsLoading(true) + .waitForIsLoading(false) + .waitForIsLoading(true) + .waitForIsLoading(false) + .waitForIsLoading(true) + .waitForIsLoading(false) + .waitForPlaybackState(Player.STATE_READY) + // Wait in each content part to ensure previously triggered events get a chance to be + // delivered. This prevents flakiness caused by playback progressing too fast. + .playUntilPosition(/* windowIndex= */ 0, /* positionMs= */ 3_000) + .playUntilPosition(/* windowIndex= */ 0, /* positionMs= */ 8_000) + .play() + .waitForPlaybackState(Player.STATE_ENDED) + // Wait for final timeline change that marks post-roll played. + .waitForTimelineChanged() + .build(); + TestAnalyticsListener listener = runAnalyticsTest(fakeMediaSource, actionSchedule); + + Object periodUid = listener.lastReportedTimeline.getUidOfPeriod(/* periodIndex= */ 0); + EventWindowAndPeriodId prerollAd = + new EventWindowAndPeriodId( + /* windowIndex= */ 0, + new MediaPeriodId( + periodUid, + /* adGroupIndex= */ 0, + /* adIndexInAdGroup= */ 0, + /* windowSequenceNumber= */ 0)); + EventWindowAndPeriodId midrollAd = + new EventWindowAndPeriodId( + /* windowIndex= */ 0, + new MediaPeriodId( + periodUid, + /* adGroupIndex= */ 1, + /* adIndexInAdGroup= */ 0, + /* windowSequenceNumber= */ 0)); + EventWindowAndPeriodId postrollAd = + new EventWindowAndPeriodId( + /* windowIndex= */ 0, + new MediaPeriodId( + periodUid, + /* adGroupIndex= */ 2, + /* adIndexInAdGroup= */ 0, + /* windowSequenceNumber= */ 0)); + EventWindowAndPeriodId contentAfterPreroll = + new EventWindowAndPeriodId( + /* windowIndex= */ 0, + new MediaPeriodId(periodUid, /* windowSequenceNumber= */ 0, /* nextAdGroupIndex= */ 1)); + EventWindowAndPeriodId contentAfterMidroll = + new EventWindowAndPeriodId( + /* windowIndex= */ 0, + new MediaPeriodId(periodUid, /* windowSequenceNumber= */ 0, /* nextAdGroupIndex= */ 2)); + EventWindowAndPeriodId contentAfterPostroll = + new EventWindowAndPeriodId( + /* windowIndex= */ 0, + new MediaPeriodId( + periodUid, /* windowSequenceNumber= */ 0, /* nextAdGroupIndex= */ C.INDEX_UNSET)); + assertThat(listener.getEvents(EVENT_PLAYER_STATE_CHANGED)) + .containsExactly( + WINDOW_0 /* setPlayWhenReady=true */, + WINDOW_0 /* setPlayWhenReady=false */, + WINDOW_0 /* BUFFERING */, + prerollAd /* READY */, + prerollAd /* setPlayWhenReady=true */, + contentAfterPreroll /* setPlayWhenReady=false */, + contentAfterPreroll /* setPlayWhenReady=true */, + contentAfterMidroll /* setPlayWhenReady=false */, + contentAfterMidroll /* setPlayWhenReady=true */, + contentAfterPostroll /* ENDED */); + assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED)) + .containsExactly( + WINDOW_0 /* PLAYLIST_CHANGED */, + WINDOW_0 /* SOURCE_UPDATE (initial) */, + contentAfterPreroll /* SOURCE_UPDATE (played preroll) */, + contentAfterMidroll /* SOURCE_UPDATE (played midroll) */, + contentAfterPostroll /* SOURCE_UPDATE (played postroll) */); + assertThat(listener.getEvents(EVENT_POSITION_DISCONTINUITY)) + .containsExactly( + contentAfterPreroll, midrollAd, contentAfterMidroll, postrollAd, contentAfterPostroll); + assertThat(listener.getEvents(EVENT_LOADING_CHANGED)) + .containsExactly( + prerollAd, prerollAd, prerollAd, prerollAd, prerollAd, prerollAd, prerollAd, prerollAd, + prerollAd, prerollAd, prerollAd, prerollAd); + assertThat(listener.getEvents(EVENT_TRACKS_CHANGED)) + .containsExactly( + prerollAd, + contentAfterPreroll, + midrollAd, + contentAfterMidroll, + postrollAd, + contentAfterPostroll); + assertThat(listener.getEvents(EVENT_LOAD_STARTED)) + .containsExactly( + WINDOW_0 /* content manifest */, + WINDOW_0 /* preroll manifest */, + prerollAd, + contentAfterPreroll, + WINDOW_0 /* midroll manifest */, + midrollAd, + contentAfterMidroll, + WINDOW_0 /* postroll manifest */, + postrollAd, + contentAfterPostroll); + assertThat(listener.getEvents(EVENT_LOAD_COMPLETED)) + .containsExactly( + WINDOW_0 /* content manifest */, + WINDOW_0 /* preroll manifest */, + prerollAd, + contentAfterPreroll, + WINDOW_0 /* midroll manifest */, + midrollAd, + contentAfterMidroll, + WINDOW_0 /* postroll manifest */, + postrollAd, + contentAfterPostroll); + assertThat(listener.getEvents(EVENT_DOWNSTREAM_FORMAT_CHANGED)) + .containsExactly( + prerollAd, + contentAfterPreroll, + midrollAd, + contentAfterMidroll, + postrollAd, + contentAfterPostroll); + assertThat(listener.getEvents(EVENT_MEDIA_PERIOD_CREATED)) + .containsExactly( + prerollAd, + contentAfterPreroll, + midrollAd, + contentAfterMidroll, + postrollAd, + contentAfterPostroll); + assertThat(listener.getEvents(EVENT_MEDIA_PERIOD_RELEASED)) + .containsExactly( + prerollAd, contentAfterPreroll, midrollAd, contentAfterMidroll, postrollAd); + assertThat(listener.getEvents(EVENT_READING_STARTED)) + .containsExactly( + prerollAd, + contentAfterPreroll, + midrollAd, + contentAfterMidroll, + postrollAd, + contentAfterPostroll); + assertThat(listener.getEvents(EVENT_DECODER_ENABLED)).containsExactly(prerollAd); + assertThat(listener.getEvents(EVENT_DECODER_INIT)) + .containsExactly( + prerollAd, + contentAfterPreroll, + midrollAd, + contentAfterMidroll, + postrollAd, + contentAfterPostroll); + assertThat(listener.getEvents(EVENT_DECODER_FORMAT_CHANGED)) + .containsExactly( + prerollAd, + contentAfterPreroll, + midrollAd, + contentAfterMidroll, + postrollAd, + contentAfterPostroll); + assertThat(listener.getEvents(EVENT_DROPPED_VIDEO_FRAMES)) + .containsExactly(contentAfterPreroll, contentAfterMidroll, contentAfterPostroll); + assertThat(listener.getEvents(EVENT_VIDEO_SIZE_CHANGED)) + .containsExactly( + prerollAd, + contentAfterPreroll, + midrollAd, + contentAfterMidroll, + postrollAd, + contentAfterPostroll); + assertThat(listener.getEvents(EVENT_RENDERED_FIRST_FRAME)) + .containsExactly( + prerollAd, + contentAfterPreroll, + midrollAd, + contentAfterMidroll, + postrollAd, + contentAfterPostroll); + assertThat(listener.getEvents(EVENT_VIDEO_FRAME_PROCESSING_OFFSET)) + .containsExactly(contentAfterPreroll, contentAfterMidroll, contentAfterPostroll); + listener.assertNoMoreEvents(); + } + + @Test + public void seekAfterMidroll() throws Exception { + Timeline adTimeline = + new FakeTimeline( + new TimelineWindowDefinition( + /* periodCount= */ 1, + /* id= */ 0, + /* isSeekable= */ true, + /* isDynamic= */ false, + 10 * C.MICROS_PER_SECOND, + FakeTimeline.createAdPlaybackState( + /* adsPerAdGroup= */ 1, /* adGroupTimesUs...= */ + TimelineWindowDefinition.DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US + + 5 * C.MICROS_PER_SECOND))); + FakeMediaSource fakeMediaSource = + new FakeMediaSource(adTimeline, ExoPlayerTestRunner.VIDEO_FORMAT); + ActionSchedule actionSchedule = + new ActionSchedule.Builder(TAG) + .pause() + // Ensure everything is preloaded. + .waitForIsLoading(true) + .waitForIsLoading(false) + .waitForIsLoading(true) + .waitForIsLoading(false) + .waitForIsLoading(true) + .waitForIsLoading(false) + // Seek behind the midroll. + .seek(6 * C.MICROS_PER_SECOND) + // Wait until loading started again to assert loading events without flakiness. + .waitForIsLoading(true) + .play() + .waitForPlaybackState(Player.STATE_ENDED) + .build(); + TestAnalyticsListener listener = runAnalyticsTest(fakeMediaSource, actionSchedule); + + Object periodUid = listener.lastReportedTimeline.getUidOfPeriod(/* periodIndex= */ 0); + EventWindowAndPeriodId midrollAd = + new EventWindowAndPeriodId( + /* windowIndex= */ 0, + new MediaPeriodId( + periodUid, + /* adGroupIndex= */ 0, + /* adIndexInAdGroup= */ 0, + /* windowSequenceNumber= */ 0)); + EventWindowAndPeriodId contentBeforeMidroll = + new EventWindowAndPeriodId( + /* windowIndex= */ 0, + new MediaPeriodId(periodUid, /* windowSequenceNumber= */ 0, /* nextAdGroupIndex= */ 0)); + EventWindowAndPeriodId contentAfterMidroll = + new EventWindowAndPeriodId( + /* windowIndex= */ 0, + new MediaPeriodId( + periodUid, /* windowSequenceNumber= */ 0, /* nextAdGroupIndex= */ C.INDEX_UNSET)); + assertThat(listener.getEvents(EVENT_PLAYER_STATE_CHANGED)) + .containsExactly( + WINDOW_0 /* setPlayWhenReady=true */, + WINDOW_0 /* setPlayWhenReady=false */, + WINDOW_0 /* BUFFERING */, + contentBeforeMidroll /* READY */, + contentAfterMidroll /* BUFFERING */, + midrollAd /* setPlayWhenReady=true */, + midrollAd /* READY */, + contentAfterMidroll /* ENDED */); + assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED)) + .containsExactly(WINDOW_0 /* PLAYLIST_CHANGED */, WINDOW_0 /* SOURCE_UPDATE */); + assertThat(listener.getEvents(EVENT_POSITION_DISCONTINUITY)) + .containsExactly( + contentAfterMidroll /* seek */, + midrollAd /* seek adjustment */, + contentAfterMidroll /* ad transition */); + assertThat(listener.getEvents(EVENT_SEEK_STARTED)).containsExactly(contentBeforeMidroll); + assertThat(listener.getEvents(EVENT_SEEK_PROCESSED)).containsExactly(contentAfterMidroll); + assertThat(listener.getEvents(EVENT_LOADING_CHANGED)) + .containsExactly( + contentBeforeMidroll, + contentBeforeMidroll, + contentBeforeMidroll, + contentBeforeMidroll, + contentBeforeMidroll, + contentBeforeMidroll, + midrollAd, + midrollAd); + assertThat(listener.getEvents(EVENT_TRACKS_CHANGED)) + .containsExactly(contentBeforeMidroll, midrollAd, contentAfterMidroll); + assertThat(listener.getEvents(EVENT_LOAD_STARTED)) + .containsExactly( + WINDOW_0 /* content manifest */, + contentBeforeMidroll, + midrollAd, + contentAfterMidroll, + contentAfterMidroll); + assertThat(listener.getEvents(EVENT_LOAD_COMPLETED)) + .containsExactly( + WINDOW_0 /* content manifest */, + contentBeforeMidroll, + midrollAd, + contentAfterMidroll, + contentAfterMidroll); + assertThat(listener.getEvents(EVENT_DOWNSTREAM_FORMAT_CHANGED)) + .containsExactly(contentBeforeMidroll, midrollAd, contentAfterMidroll); + assertThat(listener.getEvents(EVENT_MEDIA_PERIOD_CREATED)) + .containsExactly(contentBeforeMidroll, midrollAd, contentAfterMidroll, contentAfterMidroll); + assertThat(listener.getEvents(EVENT_MEDIA_PERIOD_RELEASED)) + .containsExactly(contentBeforeMidroll, midrollAd, contentAfterMidroll); + assertThat(listener.getEvents(EVENT_READING_STARTED)) + .containsExactly(contentBeforeMidroll, midrollAd, contentAfterMidroll); + assertThat(listener.getEvents(EVENT_DECODER_ENABLED)) + .containsExactly(contentBeforeMidroll, midrollAd); + assertThat(listener.getEvents(EVENT_DECODER_INIT)) + .containsExactly(contentBeforeMidroll, midrollAd, contentAfterMidroll); + assertThat(listener.getEvents(EVENT_DECODER_FORMAT_CHANGED)) + .containsExactly(contentBeforeMidroll, midrollAd, contentAfterMidroll); + assertThat(listener.getEvents(EVENT_DECODER_DISABLED)).containsExactly(contentBeforeMidroll); + assertThat(listener.getEvents(EVENT_DROPPED_VIDEO_FRAMES)).containsExactly(contentAfterMidroll); + assertThat(listener.getEvents(EVENT_VIDEO_SIZE_CHANGED)) + .containsExactly(contentBeforeMidroll, midrollAd, contentAfterMidroll); + assertThat(listener.getEvents(EVENT_RENDERED_FIRST_FRAME)) + .containsExactly(contentBeforeMidroll, midrollAd, contentAfterMidroll); + assertThat(listener.getEvents(EVENT_VIDEO_FRAME_PROCESSING_OFFSET)) + .containsExactly(contentAfterMidroll); + listener.assertNoMoreEvents(); + } + + @Test + public void notifyExternalEvents() throws Exception { MediaSource mediaSource = new FakeMediaSource(SINGLE_PERIOD_TIMELINE); ActionSchedule actionSchedule = - new ActionSchedule.Builder("AnalyticsCollectorTest") + new ActionSchedule.Builder(TAG) .pause() .waitForPlaybackState(Player.STATE_READY) .executeRunnable( @@ -642,7 +1153,7 @@ public final class AnalyticsCollectorTest { .build(); TestAnalyticsListener listener = runAnalyticsTest(mediaSource, actionSchedule); - populateEventIds(SINGLE_PERIOD_TIMELINE); + populateEventIds(listener.lastReportedTimeline); assertThat(listener.getEvents(EVENT_SEEK_STARTED)).containsExactly(period0); assertThat(listener.getEvents(EVENT_SEEK_PROCESSED)).containsExactly(period0); } @@ -700,20 +1211,19 @@ public final class AnalyticsCollectorTest { videoRendererEventListener, audioRendererEventListener, textRendererOutput, - metadataRendererOutput, - drmSessionManager) -> + metadataRendererOutput) -> new Renderer[] { new FakeVideoRenderer(eventHandler, videoRendererEventListener), new FakeAudioRenderer(eventHandler, audioRendererEventListener) }; TestAnalyticsListener listener = new TestAnalyticsListener(); try { - new ExoPlayerTestRunner.Builder() - .setMediaSource(mediaSource) + new ExoPlayerTestRunner.Builder(ApplicationProvider.getApplicationContext()) + .setMediaSources(mediaSource) .setRenderersFactory(renderersFactory) .setAnalyticsListener(listener) .setActionSchedule(actionSchedule) - .build(ApplicationProvider.getApplicationContext()) + .build() .start() .blockUntilActionScheduleFinished(TIMEOUT_MS) .blockUntilEnded(TIMEOUT_MS); @@ -723,113 +1233,6 @@ public final class AnalyticsCollectorTest { return listener; } - private static final class FakeVideoRenderer extends FakeRenderer { - - private final VideoRendererEventListener.EventDispatcher eventDispatcher; - private final DecoderCounters decoderCounters; - private Format format; - private boolean renderedFirstFrame; - - public FakeVideoRenderer(Handler handler, VideoRendererEventListener eventListener) { - super(Builder.VIDEO_FORMAT); - eventDispatcher = new VideoRendererEventListener.EventDispatcher(handler, eventListener); - decoderCounters = new DecoderCounters(); - } - - @Override - protected void onEnabled(boolean joining) throws ExoPlaybackException { - super.onEnabled(joining); - eventDispatcher.enabled(decoderCounters); - renderedFirstFrame = false; - } - - @Override - protected void onStopped() throws ExoPlaybackException { - super.onStopped(); - eventDispatcher.droppedFrames(/* droppedFrameCount= */ 0, /* elapsedMs= */ 0); - } - - @Override - protected void onDisabled() { - super.onDisabled(); - eventDispatcher.disabled(decoderCounters); - } - - @Override - protected void onPositionReset(long positionUs, boolean joining) throws ExoPlaybackException { - super.onPositionReset(positionUs, joining); - renderedFirstFrame = false; - } - - @Override - protected void onFormatChanged(Format format) { - eventDispatcher.inputFormatChanged(format); - eventDispatcher.decoderInitialized( - /* decoderName= */ "fake.video.decoder", - /* initializedTimestampMs= */ SystemClock.elapsedRealtime(), - /* initializationDurationMs= */ 0); - this.format = format; - } - - @Override - protected void onBufferRead() { - if (!renderedFirstFrame) { - eventDispatcher.videoSizeChanged( - format.width, format.height, format.rotationDegrees, format.pixelWidthHeightRatio); - eventDispatcher.renderedFirstFrame(/* surface= */ null); - renderedFirstFrame = true; - } - } - } - - private static final class FakeAudioRenderer extends FakeRenderer { - - private final AudioRendererEventListener.EventDispatcher eventDispatcher; - private final DecoderCounters decoderCounters; - private boolean notifiedAudioSessionId; - - public FakeAudioRenderer(Handler handler, AudioRendererEventListener eventListener) { - super(Builder.AUDIO_FORMAT); - eventDispatcher = new AudioRendererEventListener.EventDispatcher(handler, eventListener); - decoderCounters = new DecoderCounters(); - } - - @Override - protected void onEnabled(boolean joining) throws ExoPlaybackException { - super.onEnabled(joining); - eventDispatcher.enabled(decoderCounters); - notifiedAudioSessionId = false; - } - - @Override - protected void onDisabled() { - super.onDisabled(); - eventDispatcher.disabled(decoderCounters); - } - - @Override - protected void onPositionReset(long positionUs, boolean joining) throws ExoPlaybackException { - super.onPositionReset(positionUs, joining); - } - - @Override - protected void onFormatChanged(Format format) { - eventDispatcher.inputFormatChanged(format); - eventDispatcher.decoderInitialized( - /* decoderName= */ "fake.audio.decoder", - /* initializedTimestampMs= */ SystemClock.elapsedRealtime(), - /* initializationDurationMs= */ 0); - } - - @Override - protected void onBufferRead() { - if (!notifiedAudioSessionId) { - eventDispatcher.audioSessionId(/* audioSessionId= */ 1); - notifiedAudioSessionId = true; - } - } - } - private static final class EventWindowAndPeriodId { private final int windowIndex; @@ -873,10 +1276,12 @@ public final class AnalyticsCollectorTest { public Timeline lastReportedTimeline; + private final List reportedTimelines; private final ArrayList reportedEvents; public TestAnalyticsListener() { reportedEvents = new ArrayList<>(); + reportedTimelines = new ArrayList<>(); lastReportedTimeline = Timeline.EMPTY; } @@ -906,6 +1311,7 @@ public final class AnalyticsCollectorTest { @Override public void onTimelineChanged(EventTime eventTime, int reason) { lastReportedTimeline = eventTime.timeline; + reportedTimelines.add(eventTime.timeline); reportedEvents.add(new ReportedEvent(EVENT_TIMELINE_CHANGED, eventTime)); } @@ -924,10 +1330,10 @@ public final class AnalyticsCollectorTest { reportedEvents.add(new ReportedEvent(EVENT_SEEK_PROCESSED, eventTime)); } + @SuppressWarnings("deprecation") @Override - public void onPlaybackParametersChanged( - EventTime eventTime, PlaybackParameters playbackParameters) { - reportedEvents.add(new ReportedEvent(EVENT_PLAYBACK_PARAMETERS_CHANGED, eventTime)); + public void onPlaybackSpeedChanged(EventTime eventTime, float playbackSpeed) { + reportedEvents.add(new ReportedEvent(EVENT_PLAYBACK_SPEED_CHANGED, eventTime)); } @Override @@ -941,7 +1347,7 @@ public final class AnalyticsCollectorTest { } @Override - public void onLoadingChanged(EventTime eventTime, boolean isLoading) { + public void onIsLoadingChanged(EventTime eventTime, boolean isLoading) { reportedEvents.add(new ReportedEvent(EVENT_LOADING_CHANGED, eventTime)); } @@ -1109,6 +1515,12 @@ public final class AnalyticsCollectorTest { reportedEvents.add(new ReportedEvent(EVENT_DRM_SESSION_RELEASED, eventTime)); } + @Override + public void onVideoFrameProcessingOffset( + EventTime eventTime, long totalProcessingOffsetUs, int frameCount, Format format) { + reportedEvents.add(new ReportedEvent(EVENT_VIDEO_FRAME_PROCESSING_OFFSET, eventTime)); + } + private static final class ReportedEvent { public final int eventType; @@ -1119,6 +1531,16 @@ public final class AnalyticsCollectorTest { this.eventWindowAndPeriodId = new EventWindowAndPeriodId(eventTime.windowIndex, eventTime.mediaPeriodId); } + + @Override + public String toString() { + return "ReportedEvent{" + + "type=" + + eventType + + ", windowAndPeriodId=" + + eventWindowAndPeriodId + + '}'; + } } } } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/analytics/DefaultPlaybackSessionManagerTest.java b/library/core/src/test/java/com/google/android/exoplayer2/analytics/DefaultPlaybackSessionManagerTest.java index f0b18b4a20..b24135152e 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/analytics/DefaultPlaybackSessionManagerTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/analytics/DefaultPlaybackSessionManagerTest.java @@ -21,6 +21,7 @@ import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; @@ -501,6 +502,7 @@ public final class DefaultPlaybackSessionManagerTest { createEventTime(timeline, /* windowIndex= */ 0, /* mediaPeriodId= */ null); sessionManager.handleTimelineUpdate(newTimelineEventTime); + sessionManager.updateSessions(newTimelineEventTime); ArgumentCaptor sessionId1 = ArgumentCaptor.forClass(String.class); ArgumentCaptor sessionId2 = ArgumentCaptor.forClass(String.class); @@ -608,7 +610,7 @@ public final class DefaultPlaybackSessionManagerTest { /* isSeekable= */ true, /* isDynamic= */ false, /* durationUs =*/ 10 * C.MICROS_PER_SECOND, - new AdPlaybackState(/* adGroupTimesUs= */ C.TIME_END_OF_SOURCE) + new AdPlaybackState(/* adGroupTimesUs=... */ C.TIME_END_OF_SOURCE) .withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 1))); EventTime adEventTime = createEventTime( @@ -657,6 +659,7 @@ public final class DefaultPlaybackSessionManagerTest { sessionManager.handlePositionDiscontinuity( eventTime2, Player.DISCONTINUITY_REASON_PERIOD_TRANSITION); + sessionManager.updateSessions(eventTime2); verify(mockListener).onSessionCreated(eq(eventTime1), anyString()); verify(mockListener).onSessionActive(eq(eventTime1), anyString()); @@ -688,6 +691,7 @@ public final class DefaultPlaybackSessionManagerTest { sessionManager.handlePositionDiscontinuity( eventTime2, Player.DISCONTINUITY_REASON_PERIOD_TRANSITION); + sessionManager.updateSessions(eventTime2); verify(mockListener).onSessionCreated(eventTime1, sessionId1); verify(mockListener).onSessionActive(eventTime1, sessionId1); @@ -722,6 +726,7 @@ public final class DefaultPlaybackSessionManagerTest { sessionManager.getSessionForMediaPeriodId(timeline, eventTime2.mediaPeriodId); sessionManager.handlePositionDiscontinuity(eventTime2, Player.DISCONTINUITY_REASON_SEEK); + sessionManager.updateSessions(eventTime2); verify(mockListener).onSessionCreated(eventTime1, sessionId1); verify(mockListener).onSessionActive(eventTime1, sessionId1); @@ -748,6 +753,7 @@ public final class DefaultPlaybackSessionManagerTest { sessionManager.updateSessions(eventTime2); sessionManager.handlePositionDiscontinuity(eventTime2, Player.DISCONTINUITY_REASON_SEEK); + sessionManager.updateSessions(eventTime2); verify(mockListener, never()).onSessionFinished(any(), anyString(), anyBoolean()); } @@ -790,6 +796,7 @@ public final class DefaultPlaybackSessionManagerTest { sessionManager.getSessionForMediaPeriodId(timeline, eventTime2.mediaPeriodId); sessionManager.handlePositionDiscontinuity(eventTime3, Player.DISCONTINUITY_REASON_SEEK); + sessionManager.updateSessions(eventTime3); verify(mockListener).onSessionCreated(eventTime1, sessionId1); verify(mockListener).onSessionActive(eventTime1, sessionId1); @@ -814,7 +821,7 @@ public final class DefaultPlaybackSessionManagerTest { /* isSeekable= */ true, /* isDynamic= */ false, /* durationUs =*/ 10 * C.MICROS_PER_SECOND, - new AdPlaybackState(/* adGroupTimesUs= */ 0, 5 * C.MICROS_PER_SECOND) + new AdPlaybackState(/* adGroupTimesUs=... */ 0, 5 * C.MICROS_PER_SECOND) .withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 1) .withAdCount(/* adGroupIndex= */ 1, /* adCount= */ 1))); EventTime adEventTime1 = @@ -851,6 +858,7 @@ public final class DefaultPlaybackSessionManagerTest { sessionManager.handlePositionDiscontinuity( contentEventTime, Player.DISCONTINUITY_REASON_AD_INSERTION); + sessionManager.updateSessions(contentEventTime); verify(mockListener).onSessionCreated(adEventTime1, adSessionId1); verify(mockListener).onSessionActive(adEventTime1, adSessionId1); @@ -858,6 +866,8 @@ public final class DefaultPlaybackSessionManagerTest { verify(mockListener) .onSessionFinished( contentEventTime, adSessionId1, /* automaticTransitionToNextPlayback= */ true); + verify(mockListener).onSessionCreated(eq(contentEventTime), anyString()); + verify(mockListener).onSessionActive(eq(contentEventTime), anyString()); verifyNoMoreInteractions(mockListener); } @@ -872,7 +882,7 @@ public final class DefaultPlaybackSessionManagerTest { /* isDynamic= */ false, /* durationUs =*/ 10 * C.MICROS_PER_SECOND, new AdPlaybackState( - /* adGroupTimesUs= */ 2 * C.MICROS_PER_SECOND, 5 * C.MICROS_PER_SECOND) + /* adGroupTimesUs=... */ 2 * C.MICROS_PER_SECOND, 5 * C.MICROS_PER_SECOND) .withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 1) .withAdCount(/* adGroupIndex= */ 1, /* adCount= */ 1))); EventTime adEventTime1 = @@ -908,6 +918,7 @@ public final class DefaultPlaybackSessionManagerTest { sessionManager.handlePositionDiscontinuity( adEventTime1, Player.DISCONTINUITY_REASON_AD_INSERTION); + sessionManager.updateSessions(adEventTime1); verify(mockListener, never()).onSessionFinished(any(), anyString(), anyBoolean()); } @@ -922,7 +933,7 @@ public final class DefaultPlaybackSessionManagerTest { /* isSeekable= */ true, /* isDynamic= */ false, /* durationUs =*/ 10 * C.MICROS_PER_SECOND, - new AdPlaybackState(/* adGroupTimesUs= */ 0, 5 * C.MICROS_PER_SECOND) + new AdPlaybackState(/* adGroupTimesUs=... */ 0, 5 * C.MICROS_PER_SECOND) .withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 1) .withAdCount(/* adGroupIndex= */ 1, /* adCount= */ 1))); EventTime adEventTime1 = @@ -964,7 +975,9 @@ public final class DefaultPlaybackSessionManagerTest { sessionManager.handlePositionDiscontinuity( adEventTime1, Player.DISCONTINUITY_REASON_AD_INSERTION); + sessionManager.updateSessions(adEventTime1); sessionManager.handlePositionDiscontinuity(adEventTime2, Player.DISCONTINUITY_REASON_SEEK); + sessionManager.updateSessions(adEventTime2); verify(mockListener).onSessionCreated(eq(contentEventTime), anyString()); verify(mockListener).onSessionActive(eq(contentEventTime), anyString()); @@ -992,7 +1005,7 @@ public final class DefaultPlaybackSessionManagerTest { /* isDynamic= */ false, /* durationUs =*/ 10 * C.MICROS_PER_SECOND, new AdPlaybackState( - /* adGroupTimesUs= */ 2 * C.MICROS_PER_SECOND, 5 * C.MICROS_PER_SECOND) + /* adGroupTimesUs=... */ 2 * C.MICROS_PER_SECOND, 5 * C.MICROS_PER_SECOND) .withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 1) .withAdCount(/* adGroupIndex= */ 1, /* adCount= */ 1))); EventTime adEventTime1 = @@ -1034,8 +1047,10 @@ public final class DefaultPlaybackSessionManagerTest { sessionManager.updateSessions(adEventTime1); sessionManager.handlePositionDiscontinuity( adEventTime1, Player.DISCONTINUITY_REASON_AD_INSERTION); + sessionManager.updateSessions(adEventTime1); sessionManager.handlePositionDiscontinuity( contentEventTime2, Player.DISCONTINUITY_REASON_AD_INSERTION); + sessionManager.updateSessions(contentEventTime2); String adSessionId2 = sessionManager.getSessionForMediaPeriodId(adTimeline, adEventTime2.mediaPeriodId); @@ -1044,6 +1059,31 @@ public final class DefaultPlaybackSessionManagerTest { verify(mockListener, never()).onSessionActive(any(), eq(adSessionId2)); } + @Test + public void finishAllSessions_callsOnSessionFinishedForAllCreatedSessions() { + Timeline timeline = new FakeTimeline(/* windowCount= */ 4); + EventTime eventTimeWindow0 = + createEventTime(timeline, /* windowIndex= */ 0, /* mediaPeriodId= */ null); + EventTime eventTimeWindow2 = + createEventTime(timeline, /* windowIndex= */ 2, /* mediaPeriodId= */ null); + // Actually create sessions for window 0 and 2. + sessionManager.updateSessions(eventTimeWindow0); + sessionManager.updateSessions(eventTimeWindow2); + // Query information about session for window 1, but don't create it. + sessionManager.getSessionForMediaPeriodId( + timeline, + new MediaPeriodId( + timeline.getPeriod(/* periodIndex= */ 1, new Timeline.Period(), /* setIds= */ true).uid, + /* windowSequenceNumber= */ 123)); + verify(mockListener, times(2)).onSessionCreated(any(), anyString()); + + EventTime finishEventTime = + createEventTime(Timeline.EMPTY, /* windowIndex= */ 0, /* mediaPeriodId= */ null); + sessionManager.finishAllSessions(finishEventTime); + + verify(mockListener, times(2)).onSessionFinished(eq(finishEventTime), anyString(), eq(false)); + } + private static EventTime createEventTime( Timeline timeline, int windowIndex, @Nullable MediaPeriodId mediaPeriodId) { return new EventTime( diff --git a/library/core/src/test/java/com/google/android/exoplayer2/analytics/PlaybackStatsListenerTest.java b/library/core/src/test/java/com/google/android/exoplayer2/analytics/PlaybackStatsListenerTest.java new file mode 100644 index 0000000000..c6d4a597ed --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/analytics/PlaybackStatsListenerTest.java @@ -0,0 +1,193 @@ +/* + * Copyright 2020 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.analytics; + +import static com.google.common.truth.Truth.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +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 android.os.SystemClock; +import androidx.annotation.Nullable; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.testutil.FakeTimeline; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Unit test for {@link PlaybackStatsListener}. */ +@RunWith(AndroidJUnit4.class) +public final class PlaybackStatsListenerTest { + + private static final AnalyticsListener.EventTime EMPTY_TIMELINE_EVENT_TIME = + new AnalyticsListener.EventTime( + /* realtimeMs= */ 500, + Timeline.EMPTY, + /* windowIndex= */ 0, + /* mediaPeriodId= */ null, + /* eventPlaybackPositionMs= */ 0, + /* currentPlaybackPositionMs= */ 0, + /* totalBufferedDurationMs= */ 0); + private static final Timeline TEST_TIMELINE = new FakeTimeline(/* windowCount= */ 1); + private static final AnalyticsListener.EventTime TEST_EVENT_TIME = + new AnalyticsListener.EventTime( + /* realtimeMs= */ 500, + TEST_TIMELINE, + /* windowIndex= */ 0, + new MediaSource.MediaPeriodId( + TEST_TIMELINE.getPeriod( + /* periodIndex= */ 0, new Timeline.Period(), /* setIds= */ true) + .uid, + /* windowSequenceNumber= */ 42), + /* eventPlaybackPositionMs= */ 123, + /* currentPlaybackPositionMs= */ 123, + /* totalBufferedDurationMs= */ 456); + + @Test + public void events_duringInitialIdleState_dontCreateNewPlaybackStats() { + PlaybackStatsListener playbackStatsListener = + new PlaybackStatsListener(/* keepHistory= */ true, /* callback= */ null); + + playbackStatsListener.onPositionDiscontinuity( + EMPTY_TIMELINE_EVENT_TIME, Player.DISCONTINUITY_REASON_SEEK); + playbackStatsListener.onPlaybackSpeedChanged( + EMPTY_TIMELINE_EVENT_TIME, /* playbackSpeed= */ 2.0f); + playbackStatsListener.onPlayWhenReadyChanged( + EMPTY_TIMELINE_EVENT_TIME, + /* playWhenReady= */ true, + Player.PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST); + + assertThat(playbackStatsListener.getPlaybackStats()).isNull(); + } + + @Test + public void stateChangeEvent_toNonIdle_createsInitialPlaybackStats() { + PlaybackStatsListener playbackStatsListener = + new PlaybackStatsListener(/* keepHistory= */ true, /* callback= */ null); + + playbackStatsListener.onPlaybackStateChanged(EMPTY_TIMELINE_EVENT_TIME, Player.STATE_BUFFERING); + + assertThat(playbackStatsListener.getPlaybackStats()).isNotNull(); + } + + @Test + public void timelineChangeEvent_toNonEmpty_createsInitialPlaybackStats() { + PlaybackStatsListener playbackStatsListener = + new PlaybackStatsListener(/* keepHistory= */ true, /* callback= */ null); + + playbackStatsListener.onTimelineChanged( + TEST_EVENT_TIME, Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED); + + assertThat(playbackStatsListener.getPlaybackStats()).isNotNull(); + } + + @Test + public void playback_withKeepHistory_updatesStats() { + PlaybackStatsListener playbackStatsListener = + new PlaybackStatsListener(/* keepHistory= */ true, /* callback= */ null); + + playbackStatsListener.onPlaybackStateChanged(TEST_EVENT_TIME, Player.STATE_BUFFERING); + playbackStatsListener.onPlaybackStateChanged(TEST_EVENT_TIME, Player.STATE_READY); + playbackStatsListener.onPlaybackStateChanged(TEST_EVENT_TIME, Player.STATE_ENDED); + + @Nullable PlaybackStats playbackStats = playbackStatsListener.getPlaybackStats(); + assertThat(playbackStats).isNotNull(); + assertThat(playbackStats.endedCount).isEqualTo(1); + } + + @Test + public void playback_withoutKeepHistory_updatesStats() { + PlaybackStatsListener playbackStatsListener = + new PlaybackStatsListener(/* keepHistory= */ false, /* callback= */ null); + + playbackStatsListener.onPlaybackStateChanged(TEST_EVENT_TIME, Player.STATE_BUFFERING); + playbackStatsListener.onPlaybackStateChanged(TEST_EVENT_TIME, Player.STATE_READY); + playbackStatsListener.onPlaybackStateChanged(TEST_EVENT_TIME, Player.STATE_ENDED); + + @Nullable PlaybackStats playbackStats = playbackStatsListener.getPlaybackStats(); + assertThat(playbackStats).isNotNull(); + assertThat(playbackStats.endedCount).isEqualTo(1); + } + + @Test + public void finishedSession_callsCallback() { + PlaybackStatsListener.Callback callback = mock(PlaybackStatsListener.Callback.class); + PlaybackStatsListener playbackStatsListener = + new PlaybackStatsListener(/* keepHistory= */ true, callback); + + // Create session with an event and finish it by simulating removal from playlist. + playbackStatsListener.onPlaybackStateChanged(TEST_EVENT_TIME, Player.STATE_BUFFERING); + verify(callback, never()).onPlaybackStatsReady(any(), any()); + playbackStatsListener.onTimelineChanged( + EMPTY_TIMELINE_EVENT_TIME, Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED); + + verify(callback).onPlaybackStatsReady(eq(TEST_EVENT_TIME), any()); + } + + @Test + public void finishAllSessions_callsAllPendingCallbacks() { + AnalyticsListener.EventTime eventTimeWindow0 = + new AnalyticsListener.EventTime( + /* realtimeMs= */ 0, + Timeline.EMPTY, + /* windowIndex= */ 0, + /* mediaPeriodId= */ null, + /* eventPlaybackPositionMs= */ 0, + /* currentPlaybackPositionMs= */ 0, + /* totalBufferedDurationMs= */ 0); + AnalyticsListener.EventTime eventTimeWindow1 = + new AnalyticsListener.EventTime( + /* realtimeMs= */ 0, + Timeline.EMPTY, + /* windowIndex= */ 1, + /* mediaPeriodId= */ null, + /* eventPlaybackPositionMs= */ 0, + /* currentPlaybackPositionMs= */ 0, + /* totalBufferedDurationMs= */ 0); + PlaybackStatsListener.Callback callback = mock(PlaybackStatsListener.Callback.class); + PlaybackStatsListener playbackStatsListener = + new PlaybackStatsListener(/* keepHistory= */ true, callback); + playbackStatsListener.onPlaybackStateChanged(eventTimeWindow0, Player.STATE_BUFFERING); + playbackStatsListener.onPlaybackStateChanged(eventTimeWindow1, Player.STATE_BUFFERING); + + playbackStatsListener.finishAllSessions(); + + verify(callback, times(2)).onPlaybackStatsReady(any(), any()); + verify(callback).onPlaybackStatsReady(eq(eventTimeWindow0), any()); + verify(callback).onPlaybackStatsReady(eq(eventTimeWindow1), any()); + } + + @Test + public void finishAllSessions_doesNotCallCallbackAgainWhenSessionWouldBeAutomaticallyFinished() { + PlaybackStatsListener.Callback callback = mock(PlaybackStatsListener.Callback.class); + PlaybackStatsListener playbackStatsListener = + new PlaybackStatsListener(/* keepHistory= */ true, callback); + playbackStatsListener.onPlaybackStateChanged(TEST_EVENT_TIME, Player.STATE_BUFFERING); + SystemClock.setCurrentTimeMillis(TEST_EVENT_TIME.realtimeMs + 100); + + playbackStatsListener.finishAllSessions(); + // Simulate removing the playback item to ensure the session would finish if it hadn't already. + playbackStatsListener.onTimelineChanged( + EMPTY_TIMELINE_EVENT_TIME, Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED); + + verify(callback).onPlaybackStatsReady(any(), any()); + } +} diff --git a/library/core/src/test/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRendererTest.java b/library/core/src/test/java/com/google/android/exoplayer2/audio/DecoderAudioRendererTest.java similarity index 74% rename from library/core/src/test/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRendererTest.java rename to library/core/src/test/java/com/google/android/exoplayer2/audio/DecoderAudioRendererTest.java index 6769f5049b..bfc657aaf4 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRendererTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/audio/DecoderAudioRendererTest.java @@ -29,10 +29,10 @@ import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.RendererConfiguration; +import com.google.android.exoplayer2.decoder.DecoderException; 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.drm.DrmSessionManager; import com.google.android.exoplayer2.drm.ExoMediaCrypto; import com.google.android.exoplayer2.testutil.FakeSampleStream; import com.google.android.exoplayer2.util.MimeTypes; @@ -43,60 +43,71 @@ import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.robolectric.annotation.Config; -/** Unit test for {@link SimpleDecoderAudioRenderer}. */ +/** Unit test for {@link DecoderAudioRenderer}. */ @RunWith(AndroidJUnit4.class) -public class SimpleDecoderAudioRendererTest { +public class DecoderAudioRendererTest { - private static final Format FORMAT = Format.createSampleFormat(null, MimeTypes.AUDIO_RAW, 0); + private static final Format FORMAT = + new Format.Builder().setSampleMimeType(MimeTypes.AUDIO_RAW).build(); @Mock private AudioSink mockAudioSink; - private SimpleDecoderAudioRenderer audioRenderer; + private DecoderAudioRenderer audioRenderer; @Before public void setUp() throws Exception { MockitoAnnotations.initMocks(this); audioRenderer = - new SimpleDecoderAudioRenderer(null, null, null, false, mockAudioSink) { + new DecoderAudioRenderer(null, null, mockAudioSink) { @Override - protected int supportsFormatInternal( - @Nullable DrmSessionManager drmSessionManager, Format format) { + public String getName() { + return "TestAudioRenderer"; + } + + @Override + @FormatSupport + protected int supportsFormatInternal(Format format) { return FORMAT_HANDLED; } @Override protected SimpleDecoder< - DecoderInputBuffer, ? extends SimpleOutputBuffer, ? extends AudioDecoderException> - createDecoder(Format format, @Nullable ExoMediaCrypto mediaCrypto) - throws AudioDecoderException { + DecoderInputBuffer, ? extends SimpleOutputBuffer, ? extends DecoderException> + createDecoder(Format format, @Nullable ExoMediaCrypto mediaCrypto) { return new FakeDecoder(); } + + @Override + protected Format getOutputFormat() { + return FORMAT; + } }; } @Config(sdk = 19) @Test - public void testSupportsFormatAtApi19() { + public void supportsFormatAtApi19() { assertThat(audioRenderer.supportsFormat(FORMAT)) .isEqualTo(ADAPTIVE_NOT_SEAMLESS | TUNNELING_NOT_SUPPORTED | FORMAT_HANDLED); } @Config(sdk = 21) @Test - public void testSupportsFormatAtApi21() { + public void supportsFormatAtApi21() { // From API 21, tunneling is supported. assertThat(audioRenderer.supportsFormat(FORMAT)) .isEqualTo(ADAPTIVE_NOT_SEAMLESS | TUNNELING_SUPPORTED | FORMAT_HANDLED); } @Test - public void testImmediatelyReadEndOfStreamPlaysAudioSinkToEndOfStream() throws Exception { + public void immediatelyReadEndOfStreamPlaysAudioSinkToEndOfStream() throws Exception { audioRenderer.enable( RendererConfiguration.DEFAULT, new Format[] {FORMAT}, new FakeSampleStream(FORMAT, /* eventDispatcher= */ null, /* shouldOutputSample= */ false), - 0, - false, - 0); + /* positionUs= */ 0, + /* joining= */ false, + /* mayRenderStartOfStream= */ true, + /* offsetUs= */ 0); audioRenderer.setCurrentStreamFinal(); when(mockAudioSink.isEnded()).thenReturn(true); while (!audioRenderer.isEnded()) { @@ -109,7 +120,7 @@ public class SimpleDecoderAudioRendererTest { } private static final class FakeDecoder - extends SimpleDecoder { + extends SimpleDecoder { public FakeDecoder() { super(new DecoderInputBuffer[1], new SimpleOutputBuffer[1]); @@ -127,17 +138,17 @@ public class SimpleDecoderAudioRendererTest { @Override protected SimpleOutputBuffer createOutputBuffer() { - return new SimpleOutputBuffer(this); + return new SimpleOutputBuffer(this::releaseOutputBuffer); } @Override - protected AudioDecoderException createUnexpectedDecodeException(Throwable error) { - return new AudioDecoderException("Unexpected decode error", error); + protected DecoderException createUnexpectedDecodeException(Throwable error) { + return new DecoderException("Unexpected decode error", error); } @Override - protected AudioDecoderException decode(DecoderInputBuffer inputBuffer, - SimpleOutputBuffer outputBuffer, boolean reset) { + protected DecoderException decode( + DecoderInputBuffer inputBuffer, SimpleOutputBuffer outputBuffer, boolean reset) { if (inputBuffer.isEndOfStream()) { outputBuffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM); } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/audio/DefaultAudioSinkTest.java b/library/core/src/test/java/com/google/android/exoplayer2/audio/DefaultAudioSinkTest.java index 7982163ee8..9689a326e7 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/audio/DefaultAudioSinkTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/audio/DefaultAudioSinkTest.java @@ -21,7 +21,6 @@ import static org.robolectric.annotation.Config.TARGET_SDK; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.PlaybackParameters; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.util.Arrays; @@ -38,7 +37,7 @@ import org.robolectric.annotation.Config; * data (i.e., the {@link android.media.AudioTrack#write} methods just return 0), so these tests are * currently limited to verifying behavior that doesn't rely on consuming data, and the position * will stay at its initial value. For example, we can't verify {@link - * AudioSink#handleBuffer(ByteBuffer, long)} handling a complete buffer, or queueing audio then + * AudioSink#handleBuffer(ByteBuffer, long, int)} handling a complete buffer, or queueing audio then * draining to the end of the stream. This could be worked around by having a test-only mode where * {@link DefaultAudioSink} automatically treats audio as consumed. */ @@ -77,51 +76,57 @@ public final class DefaultAudioSinkTest { @Test public void handlesBufferAfterReset() throws Exception { configureDefaultAudioSink(CHANNEL_COUNT_STEREO); - defaultAudioSink.handleBuffer(createDefaultSilenceBuffer(), /* presentationTimeUs= */ 0); + defaultAudioSink.handleBuffer( + createDefaultSilenceBuffer(), /* presentationTimeUs= */ 0, /* encodedAccessUnitCount= */ 1); // After reset and re-configure we can successfully queue more input. defaultAudioSink.reset(); configureDefaultAudioSink(CHANNEL_COUNT_STEREO); - defaultAudioSink.handleBuffer(createDefaultSilenceBuffer(), /* presentationTimeUs= */ 0); + defaultAudioSink.handleBuffer( + createDefaultSilenceBuffer(), /* presentationTimeUs= */ 0, /* encodedAccessUnitCount= */ 1); } @Test public void handlesBufferAfterReset_withPlaybackParameters() throws Exception { - PlaybackParameters playbackParameters = new PlaybackParameters(1.5f); - defaultAudioSink.setPlaybackParameters(playbackParameters); + defaultAudioSink.setPlaybackSpeed(/* playbackSpeed= */ 1.5f); configureDefaultAudioSink(CHANNEL_COUNT_STEREO); - defaultAudioSink.handleBuffer(createDefaultSilenceBuffer(), /* presentationTimeUs= */ 0); + defaultAudioSink.handleBuffer( + createDefaultSilenceBuffer(), /* presentationTimeUs= */ 0, /* encodedAccessUnitCount= */ 1); // After reset and re-configure we can successfully queue more input. defaultAudioSink.reset(); configureDefaultAudioSink(CHANNEL_COUNT_STEREO); - defaultAudioSink.handleBuffer(createDefaultSilenceBuffer(), /* presentationTimeUs= */ 0); - assertThat(defaultAudioSink.getPlaybackParameters()).isEqualTo(playbackParameters); + defaultAudioSink.handleBuffer( + createDefaultSilenceBuffer(), /* presentationTimeUs= */ 0, /* encodedAccessUnitCount= */ 1); + assertThat(defaultAudioSink.getPlaybackSpeed()).isEqualTo(1.5f); } @Test public void handlesBufferAfterReset_withFormatChange() throws Exception { configureDefaultAudioSink(CHANNEL_COUNT_STEREO); - defaultAudioSink.handleBuffer(createDefaultSilenceBuffer(), /* presentationTimeUs= */ 0); + defaultAudioSink.handleBuffer( + createDefaultSilenceBuffer(), /* presentationTimeUs= */ 0, /* encodedAccessUnitCount= */ 1); // After reset and re-configure we can successfully queue more input. defaultAudioSink.reset(); configureDefaultAudioSink(CHANNEL_COUNT_MONO); - defaultAudioSink.handleBuffer(createDefaultSilenceBuffer(), /* presentationTimeUs= */ 0); + defaultAudioSink.handleBuffer( + createDefaultSilenceBuffer(), /* presentationTimeUs= */ 0, /* encodedAccessUnitCount= */ 1); } @Test public void handlesBufferAfterReset_withFormatChangeAndPlaybackParameters() throws Exception { - PlaybackParameters playbackParameters = new PlaybackParameters(1.5f); - defaultAudioSink.setPlaybackParameters(playbackParameters); + defaultAudioSink.setPlaybackSpeed(/* playbackSpeed= */ 1.5f); configureDefaultAudioSink(CHANNEL_COUNT_STEREO); - defaultAudioSink.handleBuffer(createDefaultSilenceBuffer(), /* presentationTimeUs= */ 0); + defaultAudioSink.handleBuffer( + createDefaultSilenceBuffer(), /* presentationTimeUs= */ 0, /* encodedAccessUnitCount= */ 1); // After reset and re-configure we can successfully queue more input. defaultAudioSink.reset(); configureDefaultAudioSink(CHANNEL_COUNT_MONO); - defaultAudioSink.handleBuffer(createDefaultSilenceBuffer(), /* presentationTimeUs= */ 0); - assertThat(defaultAudioSink.getPlaybackParameters()).isEqualTo(playbackParameters); + defaultAudioSink.handleBuffer( + createDefaultSilenceBuffer(), /* presentationTimeUs= */ 0, /* encodedAccessUnitCount= */ 1); + assertThat(defaultAudioSink.getPlaybackSpeed()).isEqualTo(1.5f); } @Test @@ -130,7 +135,8 @@ public final class DefaultAudioSinkTest { CHANNEL_COUNT_STEREO, /* trimStartFrames= */ TRIM_100_MS_FRAME_COUNT, /* trimEndFrames= */ 0); - defaultAudioSink.handleBuffer(createDefaultSilenceBuffer(), /* presentationTimeUs= */ 0); + defaultAudioSink.handleBuffer( + createDefaultSilenceBuffer(), /* presentationTimeUs= */ 0, /* encodedAccessUnitCount= */ 1); assertThat(arrayAudioBufferSink.output) .hasLength( @@ -145,7 +151,8 @@ public final class DefaultAudioSinkTest { CHANNEL_COUNT_STEREO, /* trimStartFrames= */ 0, /* trimEndFrames= */ TRIM_10_MS_FRAME_COUNT); - defaultAudioSink.handleBuffer(createDefaultSilenceBuffer(), /* presentationTimeUs= */ 0); + defaultAudioSink.handleBuffer( + createDefaultSilenceBuffer(), /* presentationTimeUs= */ 0, /* encodedAccessUnitCount= */ 1); assertThat(arrayAudioBufferSink.output) .hasLength( @@ -160,7 +167,8 @@ public final class DefaultAudioSinkTest { CHANNEL_COUNT_STEREO, /* trimStartFrames= */ TRIM_100_MS_FRAME_COUNT, /* trimEndFrames= */ TRIM_10_MS_FRAME_COUNT); - defaultAudioSink.handleBuffer(createDefaultSilenceBuffer(), /* presentationTimeUs= */ 0); + defaultAudioSink.handleBuffer( + createDefaultSilenceBuffer(), /* presentationTimeUs= */ 0, /* encodedAccessUnitCount= */ 1); assertThat(arrayAudioBufferSink.output) .hasLength( @@ -173,14 +181,18 @@ public final class DefaultAudioSinkTest { public void getCurrentPosition_returnsPositionFromFirstBuffer() throws Exception { configureDefaultAudioSink(CHANNEL_COUNT_STEREO); defaultAudioSink.handleBuffer( - createDefaultSilenceBuffer(), /* presentationTimeUs= */ 5 * C.MICROS_PER_SECOND); + createDefaultSilenceBuffer(), + /* presentationTimeUs= */ 5 * C.MICROS_PER_SECOND, + /* encodedAccessUnitCount= */ 1); assertThat(defaultAudioSink.getCurrentPositionUs(/* sourceEnded= */ false)) .isEqualTo(5 * C.MICROS_PER_SECOND); defaultAudioSink.reset(); configureDefaultAudioSink(CHANNEL_COUNT_STEREO); defaultAudioSink.handleBuffer( - createDefaultSilenceBuffer(), /* presentationTimeUs= */ 8 * C.MICROS_PER_SECOND); + createDefaultSilenceBuffer(), + /* presentationTimeUs= */ 8 * C.MICROS_PER_SECOND, + /* encodedAccessUnitCount= */ 1); assertThat(defaultAudioSink.getCurrentPositionUs(/* sourceEnded= */ false)) .isEqualTo(8 * C.MICROS_PER_SECOND); } 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 e8eb530d99..fac1c4e322 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 @@ -49,7 +49,7 @@ public final class SilenceSkippingAudioProcessorTest { } @Test - public void testEnabledProcessor_isActive() throws Exception { + public void enabledProcessor_isActive() throws Exception { // Given an enabled processor. silenceSkippingAudioProcessor.setEnabled(true); @@ -61,7 +61,7 @@ public final class SilenceSkippingAudioProcessorTest { } @Test - public void testDisabledProcessor_isNotActive() throws Exception { + public void disabledProcessor_isNotActive() throws Exception { // Given a disabled processor. silenceSkippingAudioProcessor.setEnabled(false); @@ -73,7 +73,7 @@ public final class SilenceSkippingAudioProcessorTest { } @Test - public void testDefaultProcessor_isNotEnabled() throws Exception { + public void defaultProcessor_isNotEnabled() throws Exception { // Given a processor in its default state. // When reconfigured. silenceSkippingAudioProcessor.configure(AUDIO_FORMAT); @@ -83,7 +83,7 @@ public final class SilenceSkippingAudioProcessorTest { } @Test - public void testSkipInSilentSignal_skipsEverything() throws Exception { + public void skipInSilentSignal_skipsEverything() throws Exception { // Given a signal with only noise. InputBufferProvider inputBufferProvider = getInputBufferProviderForAlternatingSilenceAndNoise( @@ -105,7 +105,7 @@ public final class SilenceSkippingAudioProcessorTest { } @Test - public void testSkipInNoisySignal_skipsNothing() throws Exception { + public void skipInNoisySignal_skipsNothing() throws Exception { // Given a signal with only silence. InputBufferProvider inputBufferProvider = getInputBufferProviderForAlternatingSilenceAndNoise( @@ -129,8 +129,7 @@ public final class SilenceSkippingAudioProcessorTest { } @Test - public void testSkipInAlternatingTestSignal_hasCorrectOutputAndSkippedFrameCounts() - throws Exception { + public void skipInAlternatingTestSignal_hasCorrectOutputAndSkippedFrameCounts() throws Exception { // Given a signal that alternates between silence and noise. InputBufferProvider inputBufferProvider = getInputBufferProviderForAlternatingSilenceAndNoise( @@ -154,7 +153,7 @@ public final class SilenceSkippingAudioProcessorTest { } @Test - public void testSkipWithSmallerInputBufferSize_hasCorrectOutputAndSkippedFrameCounts() + public void skipWithSmallerInputBufferSize_hasCorrectOutputAndSkippedFrameCounts() throws Exception { // Given a signal that alternates between silence and noise. InputBufferProvider inputBufferProvider = @@ -179,7 +178,7 @@ public final class SilenceSkippingAudioProcessorTest { } @Test - public void testSkipWithLargerInputBufferSize_hasCorrectOutputAndSkippedFrameCounts() + public void skipWithLargerInputBufferSize_hasCorrectOutputAndSkippedFrameCounts() throws Exception { // Given a signal that alternates between silence and noise. InputBufferProvider inputBufferProvider = @@ -204,7 +203,7 @@ public final class SilenceSkippingAudioProcessorTest { } @Test - public void testSkipThenFlush_resetsSkippedFrameCount() throws Exception { + public void skipThenFlush_resetsSkippedFrameCount() throws Exception { // Given a signal that alternates between silence and noise. InputBufferProvider inputBufferProvider = getInputBufferProviderForAlternatingSilenceAndNoise( @@ -268,10 +267,11 @@ public final class SilenceSkippingAudioProcessorTest { Pcm16BitAudioBuilder audioBuilder = new Pcm16BitAudioBuilder(channelCount, totalFrameCount); while (!audioBuilder.isFull()) { int silenceDurationFrames = (silenceDurationMs * sampleRate) / 1000; - audioBuilder.appendFrames(/* count= */ silenceDurationFrames, /* channelLevels= */ (short) 0); + audioBuilder.appendFrames( + /* count= */ silenceDurationFrames, /* channelLevels...= */ (short) 0); int noiseDurationFrames = (noiseDurationMs * sampleRate) / 1000; audioBuilder.appendFrames( - /* count= */ noiseDurationFrames, /* channelLevels= */ Short.MAX_VALUE); + /* count= */ noiseDurationFrames, /* channelLevels...= */ Short.MAX_VALUE); } return new InputBufferProvider(audioBuilder.build()); } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/audio/SonicAudioProcessorTest.java b/library/core/src/test/java/com/google/android/exoplayer2/audio/SonicAudioProcessorTest.java index e6b448774c..46e179456c 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/audio/SonicAudioProcessorTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/audio/SonicAudioProcessorTest.java @@ -48,7 +48,7 @@ public final class SonicAudioProcessorTest { } @Test - public void testReconfigureWithSameSampleRate() throws Exception { + public void reconfigureWithSameSampleRate() throws Exception { // When configured for resampling from 44.1 kHz to 48 kHz, the output sample rate is correct. sonicAudioProcessor.setOutputSampleRateHz(48000); AudioFormat outputAudioFormat = sonicAudioProcessor.configure(AUDIO_FORMAT_44100_HZ); @@ -65,7 +65,7 @@ public final class SonicAudioProcessorTest { } @Test - public void testNoSampleRateChange() throws Exception { + public void noSampleRateChange() throws Exception { // Configure for resampling 44.1 kHz to 48 kHz. sonicAudioProcessor.setOutputSampleRateHz(48000); sonicAudioProcessor.configure(AUDIO_FORMAT_44100_HZ); @@ -78,7 +78,7 @@ public final class SonicAudioProcessorTest { } @Test - public void testIsActiveWithSpeedChange() throws Exception { + public void isActiveWithSpeedChange() throws Exception { sonicAudioProcessor.setSpeed(1.5f); sonicAudioProcessor.configure(AUDIO_FORMAT_44100_HZ); sonicAudioProcessor.flush(); @@ -86,21 +86,13 @@ public final class SonicAudioProcessorTest { } @Test - public void testIsActiveWithPitchChange() throws Exception { - sonicAudioProcessor.setPitch(1.5f); - sonicAudioProcessor.configure(AUDIO_FORMAT_44100_HZ); - sonicAudioProcessor.flush(); - assertThat(sonicAudioProcessor.isActive()).isTrue(); - } - - @Test - public void testIsNotActiveWithNoChange() throws Exception { + public void isNotActiveWithNoChange() throws Exception { sonicAudioProcessor.configure(AUDIO_FORMAT_44100_HZ); assertThat(sonicAudioProcessor.isActive()).isFalse(); } @Test - public void testDoesNotSupportNon16BitInput() throws Exception { + public void doesNotSupportNon16BitInput() throws Exception { try { sonicAudioProcessor.configure( new AudioFormat( diff --git a/library/core/src/test/java/com/google/android/exoplayer2/audio/TeeAudioProcessorTest.java b/library/core/src/test/java/com/google/android/exoplayer2/audio/TeeAudioProcessorTest.java new file mode 100644 index 0000000000..6f0a87e97b --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/audio/TeeAudioProcessorTest.java @@ -0,0 +1,58 @@ +/* + * Copyright 2020 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 org.mockito.Mockito.verify; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.audio.AudioProcessor.AudioFormat; +import com.google.android.exoplayer2.audio.TeeAudioProcessor.AudioBufferSink; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +/** Unit tests for {@link TeeAudioProcessorTest}. */ +@RunWith(AndroidJUnit4.class) +public final class TeeAudioProcessorTest { + + private static final AudioFormat AUDIO_FORMAT = + new AudioFormat(/* sampleRate= */ 44100, /* channelCount= */ 2, C.ENCODING_PCM_16BIT); + + @Rule public final MockitoRule mockito = MockitoJUnit.rule(); + + private TeeAudioProcessor teeAudioProcessor; + + @Mock private AudioBufferSink mockAudioBufferSink; + + @Before + public void setUp() { + teeAudioProcessor = new TeeAudioProcessor(mockAudioBufferSink); + } + + @Test + public void initialFlush_flushesSink() throws Exception { + teeAudioProcessor.configure(AUDIO_FORMAT); + teeAudioProcessor.flush(); + + verify(mockAudioBufferSink) + .flush(AUDIO_FORMAT.sampleRate, AUDIO_FORMAT.channelCount, AUDIO_FORMAT.encoding); + } +} diff --git a/library/core/src/test/java/com/google/android/exoplayer2/audio/TrimmingAudioProcessorTest.java b/library/core/src/test/java/com/google/android/exoplayer2/audio/TrimmingAudioProcessorTest.java new file mode 100644 index 0000000000..19a1ad19c3 --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/audio/TrimmingAudioProcessorTest.java @@ -0,0 +1,101 @@ +/* + * Copyright 2020 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 androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.audio.AudioProcessor.AudioFormat; +import java.nio.ByteBuffer; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Unit tests for {@link TrimmingAudioProcessor}. */ +@RunWith(AndroidJUnit4.class) +public final class TrimmingAudioProcessorTest { + + private static final AudioFormat AUDIO_FORMAT = + new AudioFormat(/* sampleRate= */ 44100, /* channelCount= */ 2, C.ENCODING_PCM_16BIT); + private static final int TRACK_ONE_UNTRIMMED_FRAME_COUNT = 1024; + private static final int TRACK_ONE_TRIM_START_FRAME_COUNT = 64; + private static final int TRACK_ONE_TRIM_END_FRAME_COUNT = 32; + private static final int TRACK_TWO_TRIM_START_FRAME_COUNT = 128; + private static final int TRACK_TWO_TRIM_END_FRAME_COUNT = 16; + + private static final int TRACK_ONE_BUFFER_SIZE_BYTES = + AUDIO_FORMAT.bytesPerFrame * TRACK_ONE_UNTRIMMED_FRAME_COUNT; + private static final int TRACK_ONE_TRIMMED_BUFFER_SIZE_BYTES = + TRACK_ONE_BUFFER_SIZE_BYTES + - AUDIO_FORMAT.bytesPerFrame + * (TRACK_ONE_TRIM_START_FRAME_COUNT + TRACK_ONE_TRIM_END_FRAME_COUNT); + + private TrimmingAudioProcessor trimmingAudioProcessor; + + @Before + public void setUp() { + trimmingAudioProcessor = new TrimmingAudioProcessor(); + } + + @After + public void tearDown() { + trimmingAudioProcessor.reset(); + } + + @Test + public void flushTwice_trimsStartAndEnd() throws Exception { + trimmingAudioProcessor.setTrimFrameCount( + TRACK_ONE_TRIM_START_FRAME_COUNT, TRACK_ONE_TRIM_END_FRAME_COUNT); + trimmingAudioProcessor.configure(AUDIO_FORMAT); + trimmingAudioProcessor.flush(); + trimmingAudioProcessor.flush(); + + int outputSizeBytes = feedAndDrainAudioProcessorToEndOfTrackOne(); + + assertThat(trimmingAudioProcessor.getTrimmedFrameCount()) + .isEqualTo(TRACK_ONE_TRIM_START_FRAME_COUNT + TRACK_ONE_TRIM_END_FRAME_COUNT); + assertThat(outputSizeBytes).isEqualTo(TRACK_ONE_TRIMMED_BUFFER_SIZE_BYTES); + } + + /** + * Feeds and drains the audio processor up to the end of track one, returning the total output + * size in bytes. + */ + private int feedAndDrainAudioProcessorToEndOfTrackOne() throws Exception { + // Feed and drain the processor, simulating a gapless transition to another track. + ByteBuffer inputBuffer = ByteBuffer.allocate(TRACK_ONE_BUFFER_SIZE_BYTES); + int outputSize = 0; + while (!trimmingAudioProcessor.isEnded()) { + if (inputBuffer.hasRemaining()) { + trimmingAudioProcessor.queueInput(inputBuffer); + if (!inputBuffer.hasRemaining()) { + // Reconfigure for a next track then begin draining. + trimmingAudioProcessor.setTrimFrameCount( + TRACK_TWO_TRIM_START_FRAME_COUNT, TRACK_TWO_TRIM_END_FRAME_COUNT); + trimmingAudioProcessor.configure(AUDIO_FORMAT); + trimmingAudioProcessor.queueEndOfStream(); + } + } + ByteBuffer outputBuffer = trimmingAudioProcessor.getOutput(); + outputSize += outputBuffer.remaining(); + outputBuffer.clear(); + } + trimmingAudioProcessor.reset(); + return outputSize; + } +} diff --git a/library/core/src/test/java/com/google/android/exoplayer2/drm/ClearKeyUtilTest.java b/library/core/src/test/java/com/google/android/exoplayer2/drm/ClearKeyUtilTest.java index d12319ad46..57fab8bb65 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/drm/ClearKeyUtilTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/drm/ClearKeyUtilTest.java @@ -69,7 +69,7 @@ public final class ClearKeyUtilTest { @Config(sdk = 26) @Test - public void testAdjustSingleKeyResponseDataV26() { + public void adjustSingleKeyResponseDataV26() { // Everything but the keys should be removed. Within each key only the k, kid and kty parameters // should remain. Any "-" and "_" characters in the k and kid values should be replaced with "+" // and "/". @@ -87,7 +87,7 @@ public final class ClearKeyUtilTest { @Config(sdk = 26) @Test - public void testAdjustMultiKeyResponseDataV26() { + public void adjustMultiKeyResponseDataV26() { // Everything but the keys should be removed. Within each key only the k, kid and kty parameters // should remain. Any "-" and "_" characters in the k and kid values should be replaced with "+" // and "/". @@ -107,14 +107,14 @@ public final class ClearKeyUtilTest { @Config(sdk = 27) @Test - public void testAdjustResponseDataV27() { + public void adjustResponseDataV27() { // Response should be unchanged. assertThat(ClearKeyUtil.adjustResponseData(SINGLE_KEY_RESPONSE)).isEqualTo(SINGLE_KEY_RESPONSE); } @Config(sdk = 26) @Test - public void testAdjustRequestDataV26() { + public void adjustRequestDataV26() { // We expect "+" and "/" to be replaced with "-" and "_" respectively, for "kids". byte[] expected = Util.getUtf8Bytes( @@ -130,7 +130,7 @@ public final class ClearKeyUtilTest { @Config(sdk = 27) @Test - public void testAdjustRequestDataV27() { + public void adjustRequestDataV27() { // Request should be unchanged. assertThat(ClearKeyUtil.adjustRequestData(KEY_REQUEST)).isEqualTo(KEY_REQUEST); } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/drm/OfflineLicenseHelperTest.java b/library/core/src/test/java/com/google/android/exoplayer2/drm/OfflineLicenseHelperTest.java index e3dd679b92..c36c6cff38 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/drm/OfflineLicenseHelperTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/drm/OfflineLicenseHelperTest.java @@ -25,6 +25,7 @@ import android.util.Pair; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.drm.DrmInitData.SchemeData; +import com.google.android.exoplayer2.util.MediaSourceEventDispatcher; import java.util.HashMap; import org.junit.After; import org.junit.Before; @@ -39,9 +40,9 @@ import org.robolectric.annotation.LooperMode; @LooperMode(LooperMode.Mode.PAUSED) public class OfflineLicenseHelperTest { - private OfflineLicenseHelper offlineLicenseHelper; + private OfflineLicenseHelper offlineLicenseHelper; @Mock private MediaDrmCallback mediaDrmCallback; - @Mock private ExoMediaDrm mediaDrm; + @Mock private ExoMediaDrm mediaDrm; @Before public void setUp() throws Exception { @@ -51,11 +52,12 @@ public class OfflineLicenseHelperTest { .thenReturn( new ExoMediaDrm.KeyRequest(/* data= */ new byte[0], /* licenseServerUrl= */ "")); offlineLicenseHelper = - new OfflineLicenseHelper<>( + new OfflineLicenseHelper( C.WIDEVINE_UUID, - new ExoMediaDrm.AppManagedProvider<>(mediaDrm), + new ExoMediaDrm.AppManagedProvider(mediaDrm), mediaDrmCallback, - null); + /* optionalKeyRequestParameters= */ null, + new MediaSourceEventDispatcher()); } @After @@ -65,7 +67,7 @@ public class OfflineLicenseHelperTest { } @Test - public void testDownloadRenewReleaseKey() throws Exception { + public void downloadRenewReleaseKey() throws Exception { setStubLicenseAndPlaybackDurationValues(1000, 200); byte[] keySetId = {2, 5, 8}; @@ -86,7 +88,7 @@ public class OfflineLicenseHelperTest { } @Test - public void testDownloadLicenseFailsIfNullInitData() throws Exception { + public void downloadLicenseFailsIfNullInitData() throws Exception { try { offlineLicenseHelper.downloadLicense(null); fail(); @@ -96,7 +98,7 @@ public class OfflineLicenseHelperTest { } @Test - public void testDownloadLicenseFailsIfNoKeySetIdIsReturned() throws Exception { + public void downloadLicenseFailsIfNoKeySetIdIsReturned() throws Exception { setStubLicenseAndPlaybackDurationValues(1000, 200); try { @@ -108,7 +110,7 @@ public class OfflineLicenseHelperTest { } @Test - public void testDownloadLicenseDoesNotFailIfDurationNotAvailable() throws Exception { + public void downloadLicenseDoesNotFailIfDurationNotAvailable() throws Exception { setDefaultStubKeySetId(); byte[] offlineLicenseKeySetId = offlineLicenseHelper.downloadLicense(newDrmInitData()); @@ -117,7 +119,7 @@ public class OfflineLicenseHelperTest { } @Test - public void testGetLicenseDurationRemainingSec() throws Exception { + public void getLicenseDurationRemainingSec() throws Exception { long licenseDuration = 1000; int playbackDuration = 200; setStubLicenseAndPlaybackDurationValues(licenseDuration, playbackDuration); @@ -133,7 +135,7 @@ public class OfflineLicenseHelperTest { } @Test - public void testGetLicenseDurationRemainingSecExpiredLicense() throws Exception { + public void getLicenseDurationRemainingSecExpiredLicense() throws Exception { long licenseDuration = 0; int playbackDuration = 0; setStubLicenseAndPlaybackDurationValues(licenseDuration, playbackDuration); diff --git a/library/core/src/test/java/com/google/android/exoplayer2/extractor/flac/FlacExtractorTest.java b/library/core/src/test/java/com/google/android/exoplayer2/extractor/flac/FlacExtractorTest.java deleted file mode 100644 index c4fd9e21ec..0000000000 --- a/library/core/src/test/java/com/google/android/exoplayer2/extractor/flac/FlacExtractorTest.java +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright (C) 2019 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.flac; - -import androidx.test.ext.junit.runners.AndroidJUnit4; -import com.google.android.exoplayer2.testutil.ExtractorAsserts; -import org.junit.Test; -import org.junit.runner.RunWith; - -/** Unit tests for {@link FlacExtractor}. */ -@RunWith(AndroidJUnit4.class) -public class FlacExtractorTest { - - @Test - public void testSample() throws Exception { - ExtractorAsserts.assertBehavior(FlacExtractor::new, "flac/bear.flac"); - } - - @Test - public void testSampleWithId3HeaderAndId3Enabled() throws Exception { - ExtractorAsserts.assertBehavior(FlacExtractor::new, "flac/bear_with_id3.flac"); - } - - @Test - public void testSampleWithId3HeaderAndId3Disabled() throws Exception { - // The same file is used for testing the extractor with and without ID3 enabled as the test does - // not check the metadata outputted. It only checks that the file is parsed correctly in both - // cases. - ExtractorAsserts.assertBehavior( - () -> new FlacExtractor(FlacExtractor.FLAG_DISABLE_ID3_METADATA), - "flac/bear_with_id3.flac"); - } - - @Test - public void testSampleWithVorbisComments() throws Exception { - ExtractorAsserts.assertBehavior(FlacExtractor::new, "flac/bear_with_vorbis_comments.flac"); - } - - @Test - public void testSampleWithPicture() throws Exception { - ExtractorAsserts.assertBehavior(FlacExtractor::new, "flac/bear_with_picture.flac"); - } - - @Test - public void testOneMetadataBlock() throws Exception { - ExtractorAsserts.assertBehavior(FlacExtractor::new, "flac/bear_one_metadata_block.flac"); - } - - @Test - public void testNoMinMaxFrameSize() throws Exception { - ExtractorAsserts.assertBehavior(FlacExtractor::new, "flac/bear_no_min_max_frame_size.flac"); - } - - @Test - public void testNoNumSamples() throws Exception { - ExtractorAsserts.assertBehavior(FlacExtractor::new, "flac/bear_no_num_samples.flac"); - } - - @Test - public void testUncommonSampleRate() throws Exception { - ExtractorAsserts.assertBehavior(FlacExtractor::new, "flac/bear_uncommon_sample_rate.flac"); - } -} diff --git a/library/core/src/test/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractorTest.java b/library/core/src/test/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractorTest.java deleted file mode 100644 index 9c20a9668f..0000000000 --- a/library/core/src/test/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractorTest.java +++ /dev/null @@ -1,42 +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.extractor.mkv; - -import androidx.test.ext.junit.runners.AndroidJUnit4; -import com.google.android.exoplayer2.testutil.ExtractorAsserts; -import org.junit.Test; -import org.junit.runner.RunWith; - -/** Tests for {@link MatroskaExtractor}. */ -@RunWith(AndroidJUnit4.class) -public final class MatroskaExtractorTest { - - @Test - public void testMkvSample() throws Exception { - ExtractorAsserts.assertBehavior(MatroskaExtractor::new, "mkv/sample.mkv"); - } - - @Test - public void testWebmSubsampleEncryption() throws Exception { - ExtractorAsserts.assertBehavior( - MatroskaExtractor::new, "mkv/subsample_encrypted_noaltref.webm"); - } - - @Test - public void testWebmSubsampleEncryptionWithAltrefFrames() throws Exception { - ExtractorAsserts.assertBehavior(MatroskaExtractor::new, "mkv/subsample_encrypted_altref.webm"); - } -} 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 deleted file mode 100644 index a29dfcc310..0000000000 --- a/library/core/src/test/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4ExtractorTest.java +++ /dev/null @@ -1,57 +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.extractor.mp4; - -import androidx.test.ext.junit.runners.AndroidJUnit4; -import com.google.android.exoplayer2.Format; -import com.google.android.exoplayer2.testutil.ExtractorAsserts; -import com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory; -import com.google.android.exoplayer2.util.MimeTypes; -import java.util.Collections; -import java.util.List; -import org.junit.Test; -import org.junit.runner.RunWith; - -/** Unit test for {@link FragmentedMp4Extractor}. */ -@RunWith(AndroidJUnit4.class) -public final class FragmentedMp4ExtractorTest { - - @Test - public void testSample() throws Exception { - ExtractorAsserts.assertBehavior( - getExtractorFactory(Collections.emptyList()), "mp4/sample_fragmented.mp4"); - } - - @Test - public void testSampleSeekable() throws Exception { - ExtractorAsserts.assertBehavior( - getExtractorFactory(Collections.emptyList()), "mp4/sample_fragmented_seekable.mp4"); - } - - @Test - public void testSampleWithSeiPayloadParsing() throws Exception { - // Enabling the CEA-608 track enables SEI payload parsing. - ExtractorFactory extractorFactory = - getExtractorFactory( - Collections.singletonList( - Format.createTextSampleFormat(null, MimeTypes.APPLICATION_CEA608, 0, null))); - ExtractorAsserts.assertBehavior(extractorFactory, "mp4/sample_fragmented_sei.mp4"); - } - - private static ExtractorFactory getExtractorFactory(final List closedCaptionFormats) { - return () -> new FragmentedMp4Extractor(0, null, null, null, closedCaptionFormats); - } -} diff --git a/library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/OggExtractorTest.java b/library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/OggExtractorTest.java deleted file mode 100644 index b80c8d3892..0000000000 --- a/library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/OggExtractorTest.java +++ /dev/null @@ -1,115 +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.extractor.ogg; - - -import androidx.test.ext.junit.runners.AndroidJUnit4; -import com.google.android.exoplayer2.testutil.ExtractorAsserts; -import com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory; -import com.google.android.exoplayer2.testutil.FakeExtractorInput; -import com.google.android.exoplayer2.testutil.TestUtil; -import java.io.IOException; -import org.junit.Test; -import org.junit.runner.RunWith; - -/** Unit test for {@link OggExtractor}. */ -@RunWith(AndroidJUnit4.class) -public final class OggExtractorTest { - - private static final ExtractorFactory OGG_EXTRACTOR_FACTORY = OggExtractor::new; - - @Test - public void testOpus() throws Exception { - ExtractorAsserts.assertBehavior(OGG_EXTRACTOR_FACTORY, "ogg/bear.opus"); - } - - @Test - public void testFlac() throws Exception { - ExtractorAsserts.assertBehavior(OGG_EXTRACTOR_FACTORY, "ogg/bear_flac.ogg"); - } - - @Test - public void testFlacNoSeektable() throws Exception { - ExtractorAsserts.assertBehavior(OGG_EXTRACTOR_FACTORY, "ogg/bear_flac_noseektable.ogg"); - } - - @Test - public void testVorbis() throws Exception { - ExtractorAsserts.assertBehavior(OGG_EXTRACTOR_FACTORY, "ogg/bear_vorbis.ogg"); - } - - @Test - public void testSniffVorbis() throws Exception { - byte[] data = - TestUtil.joinByteArrays( - OggTestData.buildOggHeader(0x02, 0, 1000, 1), - TestUtil.createByteArray(7), // Laces - new byte[] {0x01, 'v', 'o', 'r', 'b', 'i', 's'}); - assertSniff(data, /* expectedResult= */ true); - } - - @Test - public void testSniffFlac() throws Exception { - byte[] data = - TestUtil.joinByteArrays( - OggTestData.buildOggHeader(0x02, 0, 1000, 1), - TestUtil.createByteArray(5), // Laces - new byte[] {0x7F, 'F', 'L', 'A', 'C'}); - assertSniff(data, /* expectedResult= */ true); - } - - @Test - public void testSniffFailsOpusFile() throws Exception { - byte[] data = - TestUtil.joinByteArrays( - OggTestData.buildOggHeader(0x02, 0, 1000, 0x00), new byte[] {'O', 'p', 'u', 's'}); - assertSniff(data, /* expectedResult= */ false); - } - - @Test - public void testSniffFailsInvalidOggHeader() throws Exception { - byte[] data = OggTestData.buildOggHeader(0x00, 0, 1000, 0x00); - assertSniff(data, /* expectedResult= */ false); - } - - @Test - public void testSniffInvalidHeader() throws Exception { - byte[] data = - TestUtil.joinByteArrays( - OggTestData.buildOggHeader(0x02, 0, 1000, 1), - TestUtil.createByteArray(7), // Laces - new byte[] {0x7F, 'X', 'o', 'r', 'b', 'i', 's'}); - assertSniff(data, /* expectedResult= */ false); - } - - @Test - public void testSniffFailsEOF() throws Exception { - byte[] data = OggTestData.buildOggHeader(0x02, 0, 1000, 0x00); - assertSniff(data, /* expectedResult= */ false); - } - - private void assertSniff(byte[] data, boolean expectedResult) - throws InterruptedException, IOException { - FakeExtractorInput input = - new FakeExtractorInput.Builder() - .setData(data) - .setSimulateIOErrors(true) - .setSimulateUnknownLength(true) - .setSimulatePartialReads(true) - .build(); - ExtractorAsserts.assertSniff(OGG_EXTRACTOR_FACTORY.create(), input, expectedResult); - } -} diff --git a/library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/OggTestData.java b/library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/OggTestData.java deleted file mode 100644 index 1cd3d5e5d2..0000000000 --- a/library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/OggTestData.java +++ /dev/null @@ -1,65 +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.extractor.ogg; - -import com.google.android.exoplayer2.testutil.FakeExtractorInput; -import com.google.android.exoplayer2.testutil.TestUtil; - -/** Provides ogg/vorbis test data in bytes for unit tests. */ -/* package */ final class OggTestData { - - public static FakeExtractorInput createInput(byte[] data, boolean simulateUnknownLength) { - return new FakeExtractorInput.Builder() - .setData(data) - .setSimulateIOErrors(true) - .setSimulateUnknownLength(simulateUnknownLength) - .setSimulatePartialReads(true) - .build(); - } - - public static byte[] buildOggHeader( - int headerType, long granule, int pageSequenceCounter, int pageSegmentCount) { - return TestUtil.createByteArray( - 0x4F, - 0x67, - 0x67, - 0x53, // Oggs. - 0x00, // Stream revision. - headerType, - (int) (granule) & 0xFF, - (int) (granule >> 8) & 0xFF, - (int) (granule >> 16) & 0xFF, - (int) (granule >> 24) & 0xFF, - (int) (granule >> 32) & 0xFF, - (int) (granule >> 40) & 0xFF, - (int) (granule >> 48) & 0xFF, - (int) (granule >> 56) & 0xFF, - 0x00, // LSB of data serial number. - 0x10, - 0x00, - 0x00, // MSB of data serial number. - (pageSequenceCounter) & 0xFF, - (pageSequenceCounter >> 8) & 0xFF, - (pageSequenceCounter >> 16) & 0xFF, - (pageSequenceCounter >> 24) & 0xFF, - 0x00, // LSB of page checksum. - 0x00, - 0x10, - 0x00, // MSB of page checksum. - pageSegmentCount); - } - -} diff --git a/library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/OggTestFile.java b/library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/OggTestFile.java deleted file mode 100644 index a334c5128e..0000000000 --- a/library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/OggTestFile.java +++ /dev/null @@ -1,143 +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.extractor.ogg; - -import static org.junit.Assert.fail; - -import com.google.android.exoplayer2.testutil.TestUtil; -import java.util.ArrayList; -import java.util.Random; - -/** Generates test data. */ -/* package */ final class OggTestFile { - - private static final int MAX_PACKET_LENGTH = 2048; - private static final int MAX_SEGMENT_COUNT = 10; - private static final int MAX_GRANULES_IN_PAGE = 100000; - - public final byte[] data; - public final int granuleCount; - public final int pageCount; - public final int firstPayloadPageSize; - public final int firstPayloadPageGranuleCount; - public final int lastPayloadPageSize; - public final int lastPayloadPageGranuleCount; - - private OggTestFile( - byte[] data, - int granuleCount, - int pageCount, - int firstPayloadPageSize, - int firstPayloadPageGranuleCount, - int lastPayloadPageSize, - int lastPayloadPageGranuleCount) { - this.data = data; - this.granuleCount = granuleCount; - this.pageCount = pageCount; - this.firstPayloadPageSize = firstPayloadPageSize; - this.firstPayloadPageGranuleCount = firstPayloadPageGranuleCount; - this.lastPayloadPageSize = lastPayloadPageSize; - this.lastPayloadPageGranuleCount = lastPayloadPageGranuleCount; - } - - public static OggTestFile generate(Random random, int pageCount) { - ArrayList fileData = new ArrayList<>(); - int fileSize = 0; - int granuleCount = 0; - int firstPayloadPageSize = 0; - int firstPayloadPageGranuleCount = 0; - int lastPageloadPageSize = 0; - int lastPayloadPageGranuleCount = 0; - int packetLength = -1; - - for (int i = 0; i < pageCount; i++) { - int headerType = 0x00; - if (packetLength >= 0) { - headerType |= 1; - } - if (i == 0) { - headerType |= 2; - } - if (i == pageCount - 1) { - headerType |= 4; - } - int pageGranuleCount = random.nextInt(MAX_GRANULES_IN_PAGE - 1) + 1; - int pageSegmentCount = random.nextInt(MAX_SEGMENT_COUNT); - granuleCount += pageGranuleCount; - byte[] header = OggTestData.buildOggHeader(headerType, granuleCount, 0, pageSegmentCount); - fileData.add(header); - int pageSize = header.length; - - byte[] laces = new byte[pageSegmentCount]; - int bodySize = 0; - for (int j = 0; j < pageSegmentCount; j++) { - if (packetLength < 0) { - if (i < pageCount - 1) { - packetLength = random.nextInt(MAX_PACKET_LENGTH); - } else { - int maxPacketLength = 255 * (pageSegmentCount - j) - 1; - packetLength = random.nextInt(maxPacketLength); - } - } else if (i == pageCount - 1 && j == pageSegmentCount - 1) { - packetLength = Math.min(packetLength, 254); - } - laces[j] = (byte) Math.min(packetLength, 255); - bodySize += laces[j] & 0xFF; - packetLength -= 255; - } - fileData.add(laces); - pageSize += laces.length; - - byte[] payload = TestUtil.buildTestData(bodySize, random); - fileData.add(payload); - pageSize += payload.length; - - fileSize += pageSize; - if (i == 0) { - firstPayloadPageSize = pageSize; - firstPayloadPageGranuleCount = pageGranuleCount; - } else if (i == pageCount - 1) { - lastPageloadPageSize = pageSize; - lastPayloadPageGranuleCount = pageGranuleCount; - } - } - - byte[] file = new byte[fileSize]; - int position = 0; - for (byte[] data : fileData) { - System.arraycopy(data, 0, file, position, data.length); - position += data.length; - } - return new OggTestFile( - file, - granuleCount, - pageCount, - firstPayloadPageSize, - firstPayloadPageGranuleCount, - lastPageloadPageSize, - lastPayloadPageGranuleCount); - } - - public int findPreviousPageStart(long position) { - for (int i = (int) (position - 4); i >= 0; i--) { - if (data[i] == 'O' && data[i + 1] == 'g' && data[i + 2] == 'g' && data[i + 3] == 'S') { - return i; - } - } - fail(); - return -1; - } -} diff --git a/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/AsynchronousMediaCodecAdapterTest.java b/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/AsynchronousMediaCodecAdapterTest.java new file mode 100644 index 0000000000..f816d1d11b --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/AsynchronousMediaCodecAdapterTest.java @@ -0,0 +1,242 @@ +/* + * Copyright (C) 2019 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.mediacodec; + +import static com.google.android.exoplayer2.testutil.TestUtil.assertBufferInfosEqual; +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; +import static org.robolectric.annotation.LooperMode.Mode.LEGACY; + +import android.media.MediaCodec; +import android.media.MediaFormat; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Looper; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import java.io.IOException; +import java.util.concurrent.atomic.AtomicInteger; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.Shadows; +import org.robolectric.annotation.LooperMode; + +/** Unit tests for {@link AsynchronousMediaCodecAdapter}. */ +@LooperMode(LEGACY) +@RunWith(AndroidJUnit4.class) +public class AsynchronousMediaCodecAdapterTest { + private AsynchronousMediaCodecAdapter adapter; + private MediaCodec codec; + private HandlerThread handlerThread; + private Looper looper; + private MediaCodec.BufferInfo bufferInfo; + + @Before + public void setUp() throws IOException { + handlerThread = new HandlerThread("TestHandler"); + handlerThread.start(); + looper = handlerThread.getLooper(); + codec = MediaCodec.createByCodecName("h264"); + adapter = new AsynchronousMediaCodecAdapter(codec, looper); + adapter.setCodecStartRunnable(() -> {}); + bufferInfo = new MediaCodec.BufferInfo(); + } + + @After + public void tearDown() { + adapter.shutdown(); + handlerThread.quit(); + } + + @Test + public void dequeueInputBufferIndex_withoutInputBuffer_returnsTryAgainLater() { + adapter.start(); + + assertThat(adapter.dequeueInputBufferIndex()).isEqualTo(MediaCodec.INFO_TRY_AGAIN_LATER); + } + + @Test + public void dequeueInputBufferIndex_withInputBuffer_returnsInputBuffer() { + adapter.start(); + adapter.getMediaCodecCallback().onInputBufferAvailable(codec, /* index=*/ 0); + + assertThat(adapter.dequeueInputBufferIndex()).isEqualTo(0); + } + + @Test + public void dequeueInputBufferIndex_whileFlushing_returnsTryAgainLater() { + adapter.start(); + adapter.getMediaCodecCallback().onInputBufferAvailable(codec, /* index=*/ 0); + adapter.flush(); + adapter.getMediaCodecCallback().onInputBufferAvailable(codec, /* index=*/ 1); + + assertThat(adapter.dequeueInputBufferIndex()).isEqualTo(MediaCodec.INFO_TRY_AGAIN_LATER); + } + + @Test + public void dequeueInputBufferIndex_afterFlushCompletes_returnsNextInputBuffer() { + adapter.start(); + Handler handler = new Handler(looper); + handler.post( + () -> adapter.getMediaCodecCallback().onInputBufferAvailable(codec, /* index=*/ 0)); + adapter.flush(); // enqueues a flush event on the looper + handler.post( + () -> adapter.getMediaCodecCallback().onInputBufferAvailable(codec, /* index=*/ 1)); + + // Wait until all tasks have been handled. + Shadows.shadowOf(looper).idle(); + assertThat(adapter.dequeueInputBufferIndex()).isEqualTo(1); + } + + @Test + public void dequeueInputBufferIndex_afterFlushCompletesWithError_throwsException() { + AtomicInteger calls = new AtomicInteger(0); + adapter.setCodecStartRunnable( + () -> { + if (calls.incrementAndGet() == 2) { + throw new IllegalStateException(); + } + }); + adapter.start(); + adapter.flush(); + + // Wait until all tasks have been handled. + Shadows.shadowOf(looper).idle(); + assertThrows( + IllegalStateException.class, + () -> { + adapter.dequeueInputBufferIndex(); + }); + } + + @Test + public void dequeueOutputBufferIndex_withoutOutputBuffer_returnsTryAgainLater() { + adapter.start(); + + assertThat(adapter.dequeueOutputBufferIndex(bufferInfo)) + .isEqualTo(MediaCodec.INFO_TRY_AGAIN_LATER); + } + + @Test + public void dequeueOutputBufferIndex_withOutputBuffer_returnsOutputBuffer() { + adapter.start(); + MediaCodec.BufferInfo outBufferInfo = new MediaCodec.BufferInfo(); + outBufferInfo.presentationTimeUs = 10; + adapter.getMediaCodecCallback().onOutputBufferAvailable(codec, /* index=*/ 0, outBufferInfo); + + assertThat(adapter.dequeueOutputBufferIndex(bufferInfo)).isEqualTo(0); + assertBufferInfosEqual(bufferInfo, outBufferInfo); + } + + @Test + public void dequeueOutputBufferIndex_whileFlushing_returnsTryAgainLater() { + adapter.start(); + adapter.getMediaCodecCallback().onOutputBufferAvailable(codec, /* index=*/ 0, bufferInfo); + adapter.flush(); + adapter.getMediaCodecCallback().onOutputBufferAvailable(codec, /* index=*/ 1, bufferInfo); + + assertThat(adapter.dequeueOutputBufferIndex(bufferInfo)) + .isEqualTo(MediaCodec.INFO_TRY_AGAIN_LATER); + } + + @Test + public void dequeueOutputBufferIndex_afterFlushCompletes_returnsNextOutputBuffer() { + adapter.start(); + Handler handler = new Handler(looper); + MediaCodec.BufferInfo info0 = new MediaCodec.BufferInfo(); + handler.post( + () -> adapter.getMediaCodecCallback().onOutputBufferAvailable(codec, /* index=*/ 0, info0)); + adapter.flush(); // enqueues a flush event on the looper + MediaCodec.BufferInfo info1 = new MediaCodec.BufferInfo(); + info1.presentationTimeUs = 1; + handler.post( + () -> adapter.getMediaCodecCallback().onOutputBufferAvailable(codec, /* index=*/ 1, info1)); + + // Wait until all tasks have been handled. + Shadows.shadowOf(looper).idle(); + assertThat(adapter.dequeueOutputBufferIndex(bufferInfo)).isEqualTo(1); + assertBufferInfosEqual(info1, bufferInfo); + } + + @Test + public void dequeueOutputBufferIndex_afterFlushCompletesWithError_throwsException() { + AtomicInteger calls = new AtomicInteger(0); + adapter.setCodecStartRunnable( + () -> { + if (calls.incrementAndGet() == 2) { + throw new RuntimeException("codec#start() exception"); + } + }); + adapter.start(); + adapter.flush(); + + // Wait until all tasks have been handled. + Shadows.shadowOf(looper).idle(); + assertThrows(IllegalStateException.class, () -> adapter.dequeueOutputBufferIndex(bufferInfo)); + } + + @Test + public void getOutputFormat_withMultipleFormats_returnsFormatsInCorrectOrder() { + adapter.start(); + MediaFormat[] formats = new MediaFormat[10]; + MediaCodec.Callback mediaCodecCallback = adapter.getMediaCodecCallback(); + for (int i = 0; i < formats.length; i++) { + formats[i] = new MediaFormat(); + mediaCodecCallback.onOutputFormatChanged(codec, formats[i]); + } + + for (MediaFormat format : formats) { + assertThat(adapter.dequeueOutputBufferIndex(bufferInfo)) + .isEqualTo(MediaCodec.INFO_OUTPUT_FORMAT_CHANGED); + assertThat(adapter.getOutputFormat()).isEqualTo(format); + // Call it again to ensure same format is returned + assertThat(adapter.getOutputFormat()).isEqualTo(format); + } + // Obtain next output buffer + assertThat(adapter.dequeueOutputBufferIndex(bufferInfo)) + .isEqualTo(MediaCodec.INFO_TRY_AGAIN_LATER); + // Format should remain as is + assertThat(adapter.getOutputFormat()).isEqualTo(formats[formats.length - 1]); + } + + @Test + public void getOutputFormat_afterFlush_returnsPreviousFormat() { + adapter.start(); + MediaFormat format = new MediaFormat(); + adapter.getMediaCodecCallback().onOutputFormatChanged(codec, format); + adapter.dequeueOutputBufferIndex(bufferInfo); + adapter.flush(); + + // Wait until all tasks have been handled. + Shadows.shadowOf(looper).idle(); + assertThat(adapter.getOutputFormat()).isEqualTo(format); + } + + @Test + public void shutdown_withPendingFlush_cancelsFlush() { + AtomicInteger onCodecStartCalled = new AtomicInteger(0); + adapter.setCodecStartRunnable(() -> onCodecStartCalled.incrementAndGet()); + adapter.start(); + adapter.flush(); + adapter.shutdown(); + + // Wait until all tasks have been handled. + Shadows.shadowOf(looper).idle(); + assertThat(onCodecStartCalled.get()).isEqualTo(1); + } +} diff --git a/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/AsynchronousMediaCodecBufferEnqueuerTest.java b/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/AsynchronousMediaCodecBufferEnqueuerTest.java new file mode 100644 index 0000000000..c7020b4169 --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/AsynchronousMediaCodecBufferEnqueuerTest.java @@ -0,0 +1,283 @@ +/* + * Copyright (C) 2020 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.mediacodec; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; +import static org.mockito.Mockito.doAnswer; + +import android.media.MediaCodec; +import android.os.HandlerThread; +import android.os.Looper; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.decoder.CryptoInfo; +import com.google.android.exoplayer2.util.ConditionVariable; +import java.io.IOException; +import java.util.concurrent.atomic.AtomicLong; +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; +import org.robolectric.Shadows; +import org.robolectric.shadows.ShadowLooper; + +/** Unit tests for {@link AsynchronousMediaCodecBufferEnqueuer}. */ +@RunWith(AndroidJUnit4.class) +public class AsynchronousMediaCodecBufferEnqueuerTest { + @Rule public final MockitoRule mockito = MockitoJUnit.rule(); + + private AsynchronousMediaCodecBufferEnqueuer enqueuer; + private TestHandlerThread handlerThread; + @Mock private ConditionVariable mockConditionVariable; + + @Before + public void setUp() throws IOException { + MediaCodec codec = MediaCodec.createByCodecName("h264"); + handlerThread = new TestHandlerThread("TestHandlerThread"); + enqueuer = + new AsynchronousMediaCodecBufferEnqueuer(codec, handlerThread, mockConditionVariable); + } + + @After + public void tearDown() { + enqueuer.shutdown(); + + assertThat(TestHandlerThread.INSTANCES_STARTED.get()).isEqualTo(0); + } + + @Test + public void queueInputBuffer_withPendingCryptoExceptionSet_throwsCryptoException() { + enqueuer.setPendingRuntimeException( + new MediaCodec.CryptoException(/* errorCode= */ 0, /* detailMessage= */ null)); + enqueuer.start(); + + assertThrows( + MediaCodec.CryptoException.class, + () -> + enqueuer.queueInputBuffer( + /* index= */ 0, + /* offset= */ 0, + /* size= */ 0, + /* presentationTimeUs= */ 0, + /* flags= */ 0)); + } + + @Test + public void queueInputBuffer_withPendingIllegalStateExceptionSet_throwsIllegalStateException() { + enqueuer.start(); + enqueuer.setPendingRuntimeException(new IllegalStateException()); + assertThrows( + IllegalStateException.class, + () -> + enqueuer.queueInputBuffer( + /* index= */ 0, + /* offset= */ 0, + /* size= */ 0, + /* presentationTimeUs= */ 0, + /* flags= */ 0)); + } + + @Test + public void queueInputBuffer_multipleTimes_limitsObjectsAllocation() { + enqueuer.start(); + Looper looper = handlerThread.getLooper(); + ShadowLooper shadowLooper = Shadows.shadowOf(looper); + + for (int cycle = 0; cycle < 100; cycle++) { + // Enqueue 10 messages to looper. + for (int i = 0; i < 10; i++) { + enqueuer.queueInputBuffer( + /* index= */ i, + /* offset= */ 0, + /* size= */ 0, + /* presentationTimeUs= */ i, + /* flags= */ 0); + } + // Execute all messages. + shadowLooper.idle(); + } + + assertThat(AsynchronousMediaCodecBufferEnqueuer.getInstancePoolSize()).isEqualTo(10); + } + + @Test + public void queueSecureInputBuffer_withPendingCryptoException_throwsCryptoException() { + enqueuer.setPendingRuntimeException( + new MediaCodec.CryptoException(/* errorCode= */ 0, /* detailMessage= */ null)); + enqueuer.start(); + CryptoInfo info = createCryptoInfo(); + + assertThrows( + MediaCodec.CryptoException.class, + () -> + enqueuer.queueSecureInputBuffer( + /* index= */ 0, + /* offset= */ 0, + /* info= */ info, + /* presentationTimeUs= */ 0, + /* flags= */ 0)); + } + + @Test + public void queueSecureInputBuffer_codecThrewIllegalStateException_throwsIllegalStateException() { + enqueuer.setPendingRuntimeException(new IllegalStateException()); + enqueuer.start(); + CryptoInfo info = createCryptoInfo(); + + assertThrows( + IllegalStateException.class, + () -> + enqueuer.queueSecureInputBuffer( + /* index= */ 0, + /* offset= */ 0, + /* info= */ info, + /* presentationTimeUs= */ 0, + /* flags= */ 0)); + } + + @Test + public void queueSecureInputBuffer_multipleTimes_limitsObjectsAllocation() { + enqueuer.start(); + Looper looper = handlerThread.getLooper(); + CryptoInfo info = createCryptoInfo(); + ShadowLooper shadowLooper = Shadows.shadowOf(looper); + + for (int cycle = 0; cycle < 100; cycle++) { + // Enqueue 10 messages to looper. + for (int i = 0; i < 10; i++) { + enqueuer.queueSecureInputBuffer( + /* index= */ i, + /* offset= */ 0, + /* info= */ info, + /* presentationTimeUs= */ i, + /* flags= */ 0); + } + // Execute all messages. + shadowLooper.idle(); + } + + assertThat(AsynchronousMediaCodecBufferEnqueuer.getInstancePoolSize()).isEqualTo(10); + } + + @Test + public void flush_withoutStart_works() { + enqueuer.flush(); + } + + @Test + public void flush_onInterruptedException_throwsIllegalStateException() + throws InterruptedException { + doAnswer( + invocation -> { + throw new InterruptedException(); + }) + .doNothing() + .when(mockConditionVariable) + .block(); + + enqueuer.start(); + + assertThrows(IllegalStateException.class, () -> enqueuer.flush()); + } + + @Test + public void flush_multipleTimes_works() { + enqueuer.start(); + + enqueuer.flush(); + enqueuer.flush(); + } + + @Test + public void shutdown_withoutStart_works() { + enqueuer.shutdown(); + } + + @Test + public void shutdown_multipleTimes_works() { + enqueuer.start(); + + enqueuer.shutdown(); + enqueuer.shutdown(); + } + + @Test + public void shutdown_onInterruptedException_throwsIllegalStateException() + throws InterruptedException { + doAnswer( + invocation -> { + throw new InterruptedException(); + }) + .doNothing() + .when(mockConditionVariable) + .block(); + + enqueuer.start(); + + assertThrows(IllegalStateException.class, () -> enqueuer.shutdown()); + } + + private static class TestHandlerThread extends HandlerThread { + private static final AtomicLong INSTANCES_STARTED = new AtomicLong(0); + + TestHandlerThread(String name) { + super(name); + } + + @Override + public synchronized void start() { + super.start(); + INSTANCES_STARTED.incrementAndGet(); + } + + @Override + public boolean quit() { + boolean quit = super.quit(); + if (quit) { + INSTANCES_STARTED.decrementAndGet(); + } + return quit; + } + } + + private static CryptoInfo createCryptoInfo() { + CryptoInfo info = new CryptoInfo(); + int numSubSamples = 5; + int[] numBytesOfClearData = new int[] {0, 1, 2, 3}; + int[] numBytesOfEncryptedData = new int[] {4, 5, 6, 7}; + byte[] key = new byte[] {0, 1, 2, 3}; + byte[] iv = new byte[] {4, 5, 6, 7}; + @C.CryptoMode int mode = C.CRYPTO_MODE_AES_CBC; + int encryptedBlocks = 16; + int clearBlocks = 8; + info.set( + numSubSamples, + numBytesOfClearData, + numBytesOfEncryptedData, + key, + iv, + mode, + encryptedBlocks, + clearBlocks); + return info; + } +} diff --git a/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/BatchBufferTest.java b/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/BatchBufferTest.java new file mode 100644 index 0000000000..ac40b4b39a --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/BatchBufferTest.java @@ -0,0 +1,251 @@ +/* + * Copyright (C) 2020 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.mediacodec; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.decoder.DecoderInputBuffer; +import com.google.android.exoplayer2.testutil.TestUtil; +import java.nio.ByteBuffer; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Unit tests for {@link BatchBuffer}. */ +@RunWith(AndroidJUnit4.class) +public final class BatchBufferTest { + + /** Bigger than {@link BatchBuffer#BATCH_SIZE_BYTES} */ + private static final int BUFFER_SIZE_LARGER_THAN_BATCH_SIZE_BYTES = 100 * 1000 * 1000; + /** Smaller than {@link BatchBuffer#BATCH_SIZE_BYTES} */ + private static final int BUFFER_SIZE_MUCH_SMALLER_THAN_BATCH_SIZE_BYTES = 100; + + private static final byte[] TEST_ACCESS_UNIT = + TestUtil.buildTestData(BUFFER_SIZE_MUCH_SMALLER_THAN_BATCH_SIZE_BYTES); + private static final byte[] TEST_HUGE_ACCESS_UNIT = + TestUtil.buildTestData(BUFFER_SIZE_LARGER_THAN_BATCH_SIZE_BYTES); + + private final BatchBuffer batchBuffer = new BatchBuffer(); + + @Test + public void newBatchBuffer_isEmpty() { + assertIsCleared(batchBuffer); + } + + @Test + public void clear_empty_isEmpty() { + batchBuffer.clear(); + + assertIsCleared(batchBuffer); + } + + @Test + public void clear_afterInsertingAccessUnit_isEmpty() { + batchBuffer.commitNextAccessUnit(); + + batchBuffer.clear(); + + assertIsCleared(batchBuffer); + } + + @Test + public void commitNextAccessUnit_addsAccessUnit() { + batchBuffer.commitNextAccessUnit(); + + assertThat(batchBuffer.getAccessUnitCount()).isEqualTo(1); + } + + @Test + public void commitNextAccessUnit_untilFull_isFullAndNotEmpty() { + fillBatchBuffer(batchBuffer); + + assertThat(batchBuffer.isEmpty()).isFalse(); + assertThat(batchBuffer.isFull()).isTrue(); + } + + @Test + public void commitNextAccessUnit_whenFull_throws() { + batchBuffer.setMaxAccessUnitCount(1); + batchBuffer.commitNextAccessUnit(); + + assertThrows(IllegalStateException.class, batchBuffer::commitNextAccessUnit); + } + + @Test + public void commitNextAccessUnit_whenAccessUnitIsDecodeOnly_isDecodeOnly() { + batchBuffer.getNextAccessUnitBuffer().setFlags(C.BUFFER_FLAG_DECODE_ONLY); + + batchBuffer.commitNextAccessUnit(); + + assertThat(batchBuffer.isDecodeOnly()).isTrue(); + } + + @Test + public void commitNextAccessUnit_whenAccessUnitIsEndOfStream_isEndOfSteam() { + batchBuffer.getNextAccessUnitBuffer().setFlags(C.BUFFER_FLAG_END_OF_STREAM); + + batchBuffer.commitNextAccessUnit(); + + assertThat(batchBuffer.isEndOfStream()).isTrue(); + } + + @Test + public void commitNextAccessUnit_whenAccessUnitIsKeyFrame_isKeyFrame() { + batchBuffer.getNextAccessUnitBuffer().setFlags(C.BUFFER_FLAG_KEY_FRAME); + + batchBuffer.commitNextAccessUnit(); + + assertThat(batchBuffer.isKeyFrame()).isTrue(); + } + + @Test + public void commitNextAccessUnit_withData_dataIsCopiedInTheBatch() { + batchBuffer.getNextAccessUnitBuffer().ensureSpaceForWrite(TEST_ACCESS_UNIT.length); + batchBuffer.getNextAccessUnitBuffer().data.put(TEST_ACCESS_UNIT); + + batchBuffer.commitNextAccessUnit(); + batchBuffer.flip(); + + assertThat(batchBuffer.getAccessUnitCount()).isEqualTo(1); + assertThat(batchBuffer.data).isEqualTo(ByteBuffer.wrap(TEST_ACCESS_UNIT)); + } + + @Test + public void commitNextAccessUnit_nextAccessUnit_isClear() { + batchBuffer.getNextAccessUnitBuffer().ensureSpaceForWrite(TEST_ACCESS_UNIT.length); + batchBuffer.getNextAccessUnitBuffer().data.put(TEST_ACCESS_UNIT); + batchBuffer.getNextAccessUnitBuffer().setFlags(C.BUFFER_FLAG_KEY_FRAME); + + batchBuffer.commitNextAccessUnit(); + + DecoderInputBuffer nextAccessUnit = batchBuffer.getNextAccessUnitBuffer(); + assertThat(nextAccessUnit.data).isNotNull(); + assertThat(nextAccessUnit.data.position()).isEqualTo(0); + assertThat(nextAccessUnit.isKeyFrame()).isFalse(); + } + + @Test + public void commitNextAccessUnit_twice_bothAccessUnitAreConcatenated() { + // Commit TEST_ACCESS_UNIT + batchBuffer.getNextAccessUnitBuffer().ensureSpaceForWrite(TEST_ACCESS_UNIT.length); + batchBuffer.getNextAccessUnitBuffer().data.put(TEST_ACCESS_UNIT); + batchBuffer.commitNextAccessUnit(); + // Commit TEST_ACCESS_UNIT again + batchBuffer.getNextAccessUnitBuffer().ensureSpaceForWrite(TEST_ACCESS_UNIT.length); + batchBuffer.getNextAccessUnitBuffer().data.put(TEST_ACCESS_UNIT); + + batchBuffer.commitNextAccessUnit(); + batchBuffer.flip(); + + byte[] expected = TestUtil.joinByteArrays(TEST_ACCESS_UNIT, TEST_ACCESS_UNIT); + assertThat(batchBuffer.data).isEqualTo(ByteBuffer.wrap(expected)); + } + + @Test + public void commitNextAccessUnit_whenAccessUnitIsHugeAndBatchBufferNotEmpty_isMarkedPending() { + batchBuffer.getNextAccessUnitBuffer().ensureSpaceForWrite(TEST_ACCESS_UNIT.length); + batchBuffer.getNextAccessUnitBuffer().data.put(TEST_ACCESS_UNIT); + batchBuffer.commitNextAccessUnit(); + batchBuffer.getNextAccessUnitBuffer().ensureSpaceForWrite(TEST_HUGE_ACCESS_UNIT.length); + batchBuffer.getNextAccessUnitBuffer().data.put(TEST_HUGE_ACCESS_UNIT); + batchBuffer.commitNextAccessUnit(); + + batchBuffer.batchWasConsumed(); + batchBuffer.flip(); + + assertThat(batchBuffer.getAccessUnitCount()).isEqualTo(1); + assertThat(batchBuffer.data).isEqualTo(ByteBuffer.wrap(TEST_HUGE_ACCESS_UNIT)); + } + + @Test + public void batchWasConsumed_whenNotEmpty_isEmpty() { + fillBatchBuffer(batchBuffer); + + batchBuffer.batchWasConsumed(); + + assertIsCleared(batchBuffer); + } + + @Test + public void batchWasConsumed_whenFull_isEmpty() { + fillBatchBuffer(batchBuffer); + + batchBuffer.batchWasConsumed(); + + assertIsCleared(batchBuffer); + } + + @Test + public void getMaxAccessUnitCount_whenSetToAPositiveValue_returnsIt() { + batchBuffer.setMaxAccessUnitCount(20); + + assertThat(batchBuffer.getMaxAccessUnitCount()).isEqualTo(20); + } + + @Test + public void setMaxAccessUnitCount_whenSetToNegative_throws() { + assertThrows(IllegalArgumentException.class, () -> batchBuffer.setMaxAccessUnitCount(-19)); + } + + @Test + public void setMaxAccessUnitCount_whenSetToZero_throws() { + assertThrows(IllegalArgumentException.class, () -> batchBuffer.setMaxAccessUnitCount(0)); + } + + @Test + public void setMaxAccessUnitCount_whenSetToTheNumberOfAccessUnitInTheBatch_isFull() { + batchBuffer.commitNextAccessUnit(); + + batchBuffer.setMaxAccessUnitCount(1); + + assertThat(batchBuffer.isFull()).isTrue(); + } + + @Test + public void batchWasConsumed_whenAccessUnitIsPending_pendingAccessUnitIsInTheBatch() { + batchBuffer.commitNextAccessUnit(); + batchBuffer.getNextAccessUnitBuffer().setFlags(C.BUFFER_FLAG_DECODE_ONLY); + batchBuffer.getNextAccessUnitBuffer().ensureSpaceForWrite(TEST_ACCESS_UNIT.length); + batchBuffer.getNextAccessUnitBuffer().data.put(TEST_ACCESS_UNIT); + batchBuffer.commitNextAccessUnit(); + + batchBuffer.batchWasConsumed(); + batchBuffer.flip(); + + assertThat(batchBuffer.getAccessUnitCount()).isEqualTo(1); + assertThat(batchBuffer.isDecodeOnly()).isTrue(); + assertThat(batchBuffer.data).isEqualTo(ByteBuffer.wrap(TEST_ACCESS_UNIT)); + } + + private static void fillBatchBuffer(BatchBuffer batchBuffer) { + int maxAccessUnit = batchBuffer.getMaxAccessUnitCount(); + while (!batchBuffer.isFull()) { + assertThat(maxAccessUnit--).isNotEqualTo(0); + batchBuffer.commitNextAccessUnit(); + } + } + + private static void assertIsCleared(BatchBuffer batchBuffer) { + assertThat(batchBuffer.getFirstAccessUnitTimeUs()).isEqualTo(C.TIME_UNSET); + assertThat(batchBuffer.getLastAccessUnitTimeUs()).isEqualTo(C.TIME_UNSET); + assertThat(batchBuffer.getAccessUnitCount()).isEqualTo(0); + assertThat(batchBuffer.isEmpty()).isTrue(); + assertThat(batchBuffer.isFull()).isFalse(); + } +} diff --git a/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/DedicatedThreadAsyncMediaCodecAdapterTest.java b/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/DedicatedThreadAsyncMediaCodecAdapterTest.java new file mode 100644 index 0000000000..7ea55b1d82 --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/DedicatedThreadAsyncMediaCodecAdapterTest.java @@ -0,0 +1,336 @@ +/* + * Copyright (C) 2019 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.mediacodec; + +import static com.google.android.exoplayer2.testutil.TestUtil.assertBufferInfosEqual; +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; +import static org.robolectric.Shadows.shadowOf; +import static org.robolectric.annotation.LooperMode.Mode.LEGACY; + +import android.media.MediaCodec; +import android.media.MediaFormat; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Looper; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.android.exoplayer2.C; +import java.io.IOException; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.Shadows; +import org.robolectric.annotation.LooperMode; +import org.robolectric.shadows.ShadowLooper; + +/** Unit tests for {@link DedicatedThreadAsyncMediaCodecAdapter}. */ +@LooperMode(LEGACY) +@RunWith(AndroidJUnit4.class) +public class DedicatedThreadAsyncMediaCodecAdapterTest { + private DedicatedThreadAsyncMediaCodecAdapter adapter; + private MediaCodec codec; + private TestHandlerThread handlerThread; + private MediaCodec.BufferInfo bufferInfo; + + @Before + public void setUp() throws IOException { + codec = MediaCodec.createByCodecName("h264"); + handlerThread = new TestHandlerThread("TestHandlerThread"); + adapter = + new DedicatedThreadAsyncMediaCodecAdapter( + codec, + /* enableAsynchronousQueueing= */ false, + /* trackType= */ C.TRACK_TYPE_VIDEO, + handlerThread); + adapter.setCodecStartRunnable(() -> {}); + bufferInfo = new MediaCodec.BufferInfo(); + } + + @After + public void tearDown() { + adapter.shutdown(); + + assertThat(TestHandlerThread.INSTANCES_STARTED.get()).isEqualTo(0); + } + + @Test + public void startAndShutdown_works() { + adapter.start(); + adapter.shutdown(); + } + + @Test + public void dequeueInputBufferIndex_withAfterFlushFailed_throwsException() { + AtomicInteger codecStartCalls = new AtomicInteger(0); + adapter.setCodecStartRunnable( + () -> { + if (codecStartCalls.incrementAndGet() == 2) { + throw new IllegalStateException("codec#start() exception"); + } + }); + adapter.start(); + adapter.flush(); + + // Wait until all tasks have been handled. + Shadows.shadowOf(handlerThread.getLooper()).idle(); + assertThrows(IllegalStateException.class, () -> adapter.dequeueInputBufferIndex()); + } + + @Test + public void dequeueInputBufferIndex_withoutInputBuffer_returnsTryAgainLater() { + adapter.start(); + + assertThat(adapter.dequeueInputBufferIndex()).isEqualTo(MediaCodec.INFO_TRY_AGAIN_LATER); + } + + @Test + public void dequeueInputBufferIndex_withInputBuffer_returnsInputBuffer() { + adapter.start(); + adapter.onInputBufferAvailable(codec, 0); + + assertThat(adapter.dequeueInputBufferIndex()).isEqualTo(0); + } + + @Test + public void dequeueInputBufferIndex_withPendingFlush_returnsTryAgainLater() { + adapter.start(); + adapter.onInputBufferAvailable(codec, 0); + adapter.flush(); + + assertThat(adapter.dequeueInputBufferIndex()).isEqualTo(MediaCodec.INFO_TRY_AGAIN_LATER); + } + + @Test + public void dequeueInputBufferIndex_withFlushCompletedAndInputBuffer_returnsInputBuffer() { + adapter.start(); + Looper looper = handlerThread.getLooper(); + Handler handler = new Handler(looper); + // Enqueue 10 callbacks from codec + for (int i = 0; i < 10; i++) { + int bufferIndex = i; + handler.post(() -> adapter.onInputBufferAvailable(codec, bufferIndex)); + } + adapter.flush(); // Enqueues a flush event after the onInputBufferAvailable callbacks + // Enqueue another onInputBufferAvailable after the flush event + handler.post(() -> adapter.onInputBufferAvailable(codec, 10)); + + // Wait until all tasks have been handled. + Shadows.shadowOf(handlerThread.getLooper()).idle(); + assertThat(adapter.dequeueInputBufferIndex()).isEqualTo(10); + } + + @Test + public void dequeueInputBufferIndex_withMediaCodecError_throwsException() { + adapter.start(); + adapter.onMediaCodecError(new IllegalStateException("error from codec")); + + assertThrows(IllegalStateException.class, () -> adapter.dequeueInputBufferIndex()); + } + + @Test + public void dequeueOutputBufferIndex_withInternalException_throwsException() { + AtomicInteger codecStartCalls = new AtomicInteger(0); + adapter.setCodecStartRunnable( + () -> { + if (codecStartCalls.incrementAndGet() == 2) { + throw new RuntimeException("codec#start() exception"); + } + }); + adapter.start(); + adapter.flush(); + + // Wait until all tasks have been handled. + Shadows.shadowOf(handlerThread.getLooper()).idle(); + assertThrows(IllegalStateException.class, () -> adapter.dequeueOutputBufferIndex(bufferInfo)); + } + + @Test + public void dequeueOutputBufferIndex_withoutInputBuffer_returnsTryAgainLater() { + adapter.start(); + + assertThat(adapter.dequeueOutputBufferIndex(bufferInfo)) + .isEqualTo(MediaCodec.INFO_TRY_AGAIN_LATER); + } + + @Test + public void dequeueOutputBufferIndex_withOutputBuffer_returnsOutputBuffer() { + adapter.start(); + MediaCodec.BufferInfo enqueuedBufferInfo = new MediaCodec.BufferInfo(); + adapter.onOutputBufferAvailable(codec, 0, enqueuedBufferInfo); + + assertThat(adapter.dequeueOutputBufferIndex((bufferInfo))).isEqualTo(0); + assertBufferInfosEqual(enqueuedBufferInfo, bufferInfo); + } + + @Test + public void dequeueOutputBufferIndex_withPendingFlush_returnsTryAgainLater() { + adapter.start(); + adapter.dequeueOutputBufferIndex(bufferInfo); + adapter.flush(); + + assertThat(adapter.dequeueOutputBufferIndex(bufferInfo)) + .isEqualTo(MediaCodec.INFO_TRY_AGAIN_LATER); + } + + @Test + public void dequeueOutputBufferIndex_withFlushCompletedAndOutputBuffer_returnsOutputBuffer() { + adapter.start(); + Looper looper = handlerThread.getLooper(); + Handler handler = new Handler(looper); + // Enqueue 10 callbacks from codec + for (int i = 0; i < 10; i++) { + int bufferIndex = i; + MediaCodec.BufferInfo outBufferInfo = new MediaCodec.BufferInfo(); + outBufferInfo.presentationTimeUs = i; + handler.post(() -> adapter.onOutputBufferAvailable(codec, bufferIndex, outBufferInfo)); + } + adapter.flush(); // Enqueues a flush event after the onOutputBufferAvailable callbacks + // Enqueue another onOutputBufferAvailable after the flush event + MediaCodec.BufferInfo lastBufferInfo = new MediaCodec.BufferInfo(); + lastBufferInfo.presentationTimeUs = 10; + handler.post(() -> adapter.onOutputBufferAvailable(codec, 10, lastBufferInfo)); + + // Wait until all tasks have been handled. + Shadows.shadowOf(handlerThread.getLooper()).idle(); + assertThat(adapter.dequeueOutputBufferIndex(bufferInfo)).isEqualTo(10); + assertBufferInfosEqual(lastBufferInfo, bufferInfo); + } + + @Test + public void dequeueOutputBufferIndex_withMediaCodecError_throwsException() { + adapter.start(); + adapter.onMediaCodecError(new IllegalStateException("error from codec")); + + assertThrows(IllegalStateException.class, () -> adapter.dequeueOutputBufferIndex(bufferInfo)); + } + + @Test + public void getOutputFormat_withoutFormatReceived_throwsException() { + adapter.start(); + + assertThrows(IllegalStateException.class, () -> adapter.getOutputFormat()); + } + + @Test + public void getOutputFormat_withMultipleFormats_returnsCorrectFormat() { + adapter.start(); + MediaFormat[] formats = new MediaFormat[10]; + for (int i = 0; i < formats.length; i++) { + formats[i] = new MediaFormat(); + adapter.onOutputFormatChanged(codec, formats[i]); + } + + for (int i = 0; i < 10; i++) { + assertThat(adapter.dequeueOutputBufferIndex(bufferInfo)) + .isEqualTo(MediaCodec.INFO_OUTPUT_FORMAT_CHANGED); + assertThat(adapter.getOutputFormat()).isEqualTo(formats[i]); + // A subsequent call to getOutputFormat() should return the previously fetched format + assertThat(adapter.getOutputFormat()).isEqualTo(formats[i]); + } + assertThat(adapter.dequeueOutputBufferIndex(bufferInfo)) + .isEqualTo(MediaCodec.INFO_TRY_AGAIN_LATER); + } + + @Test + public void getOutputFormat_afterFlush_returnsPreviousFormat() { + MediaFormat format = new MediaFormat(); + adapter.start(); + adapter.onOutputFormatChanged(codec, format); + + assertThat(adapter.dequeueOutputBufferIndex(bufferInfo)) + .isEqualTo(MediaCodec.INFO_OUTPUT_FORMAT_CHANGED); + assertThat(adapter.getOutputFormat()).isEqualTo(format); + + adapter.flush(); + + // Wait until all tasks have been handled. + Shadows.shadowOf(handlerThread.getLooper()).idle(); + assertThat(adapter.getOutputFormat()).isEqualTo(format); + } + + @Test + public void flush_multipleTimes_onlyLastFlushExecutes() { + AtomicInteger codecStartCalls = new AtomicInteger(0); + adapter.setCodecStartRunnable(() -> codecStartCalls.incrementAndGet()); + adapter.start(); + Looper looper = handlerThread.getLooper(); + Handler handler = new Handler(looper); + handler.post(() -> adapter.onInputBufferAvailable(codec, 0)); + adapter.flush(); // Enqueues a flush event + handler.post(() -> adapter.onInputBufferAvailable(codec, 2)); + AtomicInteger milestoneCount = new AtomicInteger(0); + handler.post(() -> milestoneCount.incrementAndGet()); + adapter.flush(); // Enqueues a second flush event + handler.post(() -> adapter.onInputBufferAvailable(codec, 3)); + + // Progress the looper until the milestoneCount is increased. + // adapter.start() will call codec.start(). First flush event should not call codec.start(). + ShadowLooper shadowLooper = shadowOf(looper); + while (milestoneCount.get() < 1) { + shadowLooper.runOneTask(); + } + assertThat(codecStartCalls.get()).isEqualTo(1); + + // Wait until all tasks have been handled. + shadowLooper.idle(); + assertThat(adapter.dequeueInputBufferIndex()).isEqualTo(3); + assertThat(codecStartCalls.get()).isEqualTo(2); + } + + @Test + public void flush_andImmediatelyShutdown_flushIsNoOp() { + AtomicInteger onCodecStartCount = new AtomicInteger(0); + adapter.setCodecStartRunnable(() -> onCodecStartCount.incrementAndGet()); + adapter.start(); + // Grab reference to Looper before shutting down the adapter otherwise handlerThread.getLooper() + // might return null. + Looper looper = handlerThread.getLooper(); + adapter.flush(); + adapter.shutdown(); + + // Wait until all tasks have been handled. + Shadows.shadowOf(looper).idle(); + // Only adapter.start() calls onCodecStart. + assertThat(onCodecStartCount.get()).isEqualTo(1); + } + + private static class TestHandlerThread extends HandlerThread { + private static final AtomicLong INSTANCES_STARTED = new AtomicLong(0); + + public TestHandlerThread(String name) { + super(name); + } + + @Override + public synchronized void start() { + super.start(); + INSTANCES_STARTED.incrementAndGet(); + } + + @Override + public boolean quit() { + boolean quit = super.quit(); + if (quit) { + INSTANCES_STARTED.decrementAndGet(); + } + return quit; + } + } +} diff --git a/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/MediaCodecAsyncCallbackTest.java b/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/MediaCodecAsyncCallbackTest.java index 1ada9f8583..0161b541f1 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/MediaCodecAsyncCallbackTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/MediaCodecAsyncCallbackTest.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.mediacodec; +import static com.google.android.exoplayer2.testutil.TestUtil.assertBufferInfosEqual; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.fail; @@ -100,9 +101,9 @@ public class MediaCodecAsyncCallbackTest { MediaCodec.BufferInfo outBufferInfo = new MediaCodec.BufferInfo(); assertThat(mediaCodecAsyncCallback.dequeueOutputBufferIndex(outBufferInfo)).isEqualTo(0); - assertThat(areEqual(outBufferInfo, bufferInfo1)).isTrue(); + assertBufferInfosEqual(bufferInfo1, outBufferInfo); assertThat(mediaCodecAsyncCallback.dequeueOutputBufferIndex(outBufferInfo)).isEqualTo(1); - assertThat(areEqual(outBufferInfo, bufferInfo2)).isTrue(); + assertBufferInfosEqual(bufferInfo2, outBufferInfo); assertThat(mediaCodecAsyncCallback.dequeueOutputBufferIndex(outBufferInfo)) .isEqualTo(MediaCodec.INFO_TRY_AGAIN_LATER); } @@ -204,17 +205,4 @@ public class MediaCodecAsyncCallbackTest { mediaCodecAsyncCallback.maybeThrowMediaCodecException(); } - - /** - * Compares if two {@link android.media.MediaCodec.BufferInfo} are equal by inspecting {@link - * android.media.MediaCodec.BufferInfo#flags}, {@link android.media.MediaCodec.BufferInfo#size}, - * {@link android.media.MediaCodec.BufferInfo#presentationTimeUs} and {@link - * android.media.MediaCodec.BufferInfo#offset}. - */ - private static boolean areEqual(MediaCodec.BufferInfo lhs, MediaCodec.BufferInfo rhs) { - return lhs.flags == rhs.flags - && lhs.offset == rhs.offset - && lhs.presentationTimeUs == rhs.presentationTimeUs - && lhs.size == rhs.size; - } } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtilTest.java b/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtilTest.java index 3693e494d4..587e2f2202 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtilTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtilTest.java @@ -96,22 +96,11 @@ public final class MediaCodecUtilTest { /* colorTransfer= */ C.COLOR_TRANSFER_SDR, /* hdrStaticInfo= */ new byte[] {1, 2, 3, 4, 5, 6, 7}); Format format = - Format.createVideoSampleFormat( - /* id= */ null, - MimeTypes.VIDEO_AV1, - /* codecs= */ "av01.0.21M.10", - /* bitrate= */ Format.NO_VALUE, - /* maxInputSize= */ Format.NO_VALUE, - /* width= */ 1024, - /* height= */ 768, - /* frameRate= */ Format.NO_VALUE, - /* initializationData= */ null, - /* rotationDegrees= */ Format.NO_VALUE, - /* pixelWidthHeightRatio= */ 0, - /* projectionData= */ null, - /* stereoMode= */ Format.NO_VALUE, - /* colorInfo= */ colorInfo, - /* drmInitData */ null); + new Format.Builder() + .setSampleMimeType(MimeTypes.VIDEO_AV1) + .setCodecs("av01.0.21M.10") + .setColorInfo(colorInfo) + .build(); assertCodecProfileAndLevelForFormat( format, MediaCodecInfo.CodecProfileLevel.AV1ProfileMain10HDR10, @@ -127,22 +116,11 @@ public final class MediaCodecUtilTest { /* colorTransfer= */ C.COLOR_TRANSFER_HLG, /* hdrStaticInfo= */ null); Format format = - Format.createVideoSampleFormat( - /* id= */ null, - MimeTypes.VIDEO_AV1, - /* codecs= */ "av01.0.21M.10", - /* bitrate= */ Format.NO_VALUE, - /* maxInputSize= */ Format.NO_VALUE, - /* width= */ 1024, - /* height= */ 768, - /* frameRate= */ Format.NO_VALUE, - /* initializationData= */ null, - /* rotationDegrees= */ Format.NO_VALUE, - /* pixelWidthHeightRatio= */ 0, - /* projectionData= */ null, - /* stereoMode= */ Format.NO_VALUE, - /* colorInfo= */ colorInfo, - /* drmInitData */ null); + new Format.Builder() + .setSampleMimeType(MimeTypes.VIDEO_AV1) + .setCodecs("av01.0.21M.10") + .setColorInfo(colorInfo) + .build(); assertCodecProfileAndLevelForFormat( format, MediaCodecInfo.CodecProfileLevel.AV1ProfileMain10HDR10, @@ -161,52 +139,20 @@ public final class MediaCodecUtilTest { @Test public void getCodecProfileAndLevel_rejectsNullCodecString() { - Format format = - Format.createVideoSampleFormat( - /* id= */ null, - /* sampleMimeType= */ MimeTypes.VIDEO_UNKNOWN, - /* codecs= */ null, - /* bitrate= */ Format.NO_VALUE, - /* maxInputSize= */ Format.NO_VALUE, - /* width= */ 1024, - /* height= */ 768, - /* frameRate= */ Format.NO_VALUE, - /* initializationData= */ null, - /* drmInitData= */ null); + Format format = new Format.Builder().setCodecs(null).build(); assertThat(MediaCodecUtil.getCodecProfileAndLevel(format)).isNull(); } @Test public void getCodecProfileAndLevel_rejectsEmptyCodecString() { - Format format = - Format.createVideoSampleFormat( - /* id= */ null, - /* sampleMimeType= */ MimeTypes.VIDEO_UNKNOWN, - /* codecs= */ "", - /* bitrate= */ Format.NO_VALUE, - /* maxInputSize= */ Format.NO_VALUE, - /* width= */ 1024, - /* height= */ 768, - /* frameRate= */ Format.NO_VALUE, - /* initializationData= */ null, - /* drmInitData= */ null); + Format format = new Format.Builder().setCodecs("").build(); assertThat(MediaCodecUtil.getCodecProfileAndLevel(format)).isNull(); } private static void assertCodecProfileAndLevelForCodecsString( - String mimeType, String codecs, int profile, int level) { + String sampleMimeType, String codecs, int profile, int level) { Format format = - Format.createVideoSampleFormat( - /* id= */ null, - mimeType, - /* codecs= */ codecs, - /* bitrate= */ Format.NO_VALUE, - /* maxInputSize= */ Format.NO_VALUE, - /* width= */ 1024, - /* height= */ 768, - /* frameRate= */ Format.NO_VALUE, - /* initializationData= */ null, - /* drmInitData= */ null); + new Format.Builder().setSampleMimeType(sampleMimeType).setCodecs(codecs).build(); assertCodecProfileAndLevelForFormat(format, profile, level); } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/MultiLockAsyncMediaCodecAdapterTest.java b/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/MultiLockAsyncMediaCodecAdapterTest.java new file mode 100644 index 0000000000..cfe9cf2900 --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/MultiLockAsyncMediaCodecAdapterTest.java @@ -0,0 +1,331 @@ +/* + * Copyright (C) 2019 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.mediacodec; + +import static com.google.android.exoplayer2.testutil.TestUtil.assertBufferInfosEqual; +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; +import static org.robolectric.Shadows.shadowOf; +import static org.robolectric.annotation.LooperMode.Mode.LEGACY; + +import android.media.MediaCodec; +import android.media.MediaFormat; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Looper; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.android.exoplayer2.C; +import java.io.IOException; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.Shadows; +import org.robolectric.annotation.LooperMode; +import org.robolectric.shadows.ShadowLooper; + +/** Unit tests for {@link MultiLockAsyncMediaCodecAdapter}. */ +@LooperMode(LEGACY) +@RunWith(AndroidJUnit4.class) +public class MultiLockAsyncMediaCodecAdapterTest { + private MultiLockAsyncMediaCodecAdapter adapter; + private MediaCodec codec; + private MediaCodec.BufferInfo bufferInfo; + private TestHandlerThread handlerThread; + + @Before + public void setUp() throws IOException { + codec = MediaCodec.createByCodecName("h264"); + handlerThread = new TestHandlerThread("TestHandlerThread"); + adapter = + new MultiLockAsyncMediaCodecAdapter( + codec, /* enableAsynchronousQueueing= */ false, C.TRACK_TYPE_VIDEO, handlerThread); + adapter.setCodecStartRunnable(() -> {}); + bufferInfo = new MediaCodec.BufferInfo(); + } + + @After + public void tearDown() { + adapter.shutdown(); + + assertThat(TestHandlerThread.INSTANCES_STARTED.get()).isEqualTo(0); + } + + @Test + public void startAndShutdown_works() { + adapter.start(); + adapter.shutdown(); + } + + @Test + public void dequeueInputBufferIndex_withAfterFlushFailed_throwsException() + throws InterruptedException { + AtomicInteger codecStartCalls = new AtomicInteger(0); + adapter.setCodecStartRunnable( + () -> { + if (codecStartCalls.incrementAndGet() == 2) { + throw new IllegalStateException("codec#start() exception"); + } + }); + adapter.start(); + adapter.flush(); + + Shadows.shadowOf(handlerThread.getLooper()).idle(); + assertThrows(IllegalStateException.class, () -> adapter.dequeueInputBufferIndex()); + } + + @Test + public void dequeueInputBufferIndex_withoutInputBuffer_returnsTryAgainLater() { + adapter.start(); + + assertThat(adapter.dequeueInputBufferIndex()).isEqualTo(MediaCodec.INFO_TRY_AGAIN_LATER); + } + + @Test + public void dequeueInputBufferIndex_withInputBuffer_returnsInputBuffer() { + adapter.start(); + adapter.onInputBufferAvailable(codec, 0); + + assertThat(adapter.dequeueInputBufferIndex()).isEqualTo(0); + } + + @Test + public void dequeueInputBufferIndex_withPendingFlush_returnsTryAgainLater() { + adapter.start(); + adapter.onInputBufferAvailable(codec, 0); + adapter.flush(); + + assertThat(adapter.dequeueInputBufferIndex()).isEqualTo(MediaCodec.INFO_TRY_AGAIN_LATER); + } + + @Test + public void dequeueInputBufferIndex_withFlushCompletedAndInputBuffer_returnsInputBuffer() + throws InterruptedException { + adapter.start(); + Looper looper = handlerThread.getLooper(); + Handler handler = new Handler(looper); + // Enqueue 10 callbacks from codec + for (int i = 0; i < 10; i++) { + int bufferIndex = i; + handler.post(() -> adapter.onInputBufferAvailable(codec, bufferIndex)); + } + adapter.flush(); // Enqueues a flush event after the onInputBufferAvailable callbacks + // Enqueue another onInputBufferAvailable after the flush event + handler.post(() -> adapter.onInputBufferAvailable(codec, 10)); + + // Wait until all tasks have been handled + Shadows.shadowOf(handlerThread.getLooper()).idle(); + assertThat(adapter.dequeueInputBufferIndex()).isEqualTo(10); + } + + @Test + public void dequeueInputBufferIndex_withMediaCodecError_throwsException() { + adapter.start(); + adapter.onMediaCodecError(new IllegalStateException("error from codec")); + + assertThrows(IllegalStateException.class, () -> adapter.dequeueInputBufferIndex()); + } + + + @Test + public void dequeueOutputBufferIndex_withInternalException_throwsException() + throws InterruptedException { + AtomicInteger codecStartCalls = new AtomicInteger(0); + adapter.setCodecStartRunnable( + () -> { + if (codecStartCalls.incrementAndGet() == 2) { + throw new RuntimeException("codec#start() exception"); + } + }); + adapter.start(); + adapter.flush(); + + Shadows.shadowOf(handlerThread.getLooper()).idle(); + assertThrows(IllegalStateException.class, () -> adapter.dequeueOutputBufferIndex(bufferInfo)); + } + + @Test + public void dequeueOutputBufferIndex_withoutInputBuffer_returnsTryAgainLater() { + adapter.start(); + + assertThat(adapter.dequeueOutputBufferIndex(bufferInfo)) + .isEqualTo(MediaCodec.INFO_TRY_AGAIN_LATER); + } + + @Test + public void dequeueOutputBufferIndex_withOutputBuffer_returnsOutputBuffer() { + adapter.start(); + MediaCodec.BufferInfo enqueuedBufferInfo = new MediaCodec.BufferInfo(); + adapter.onOutputBufferAvailable(codec, 0, enqueuedBufferInfo); + + assertThat(adapter.dequeueOutputBufferIndex((bufferInfo))).isEqualTo(0); + assertBufferInfosEqual(enqueuedBufferInfo, bufferInfo); + } + + @Test + public void dequeueOutputBufferIndex_withPendingFlush_returnsTryAgainLater() { + adapter.start(); + adapter.dequeueOutputBufferIndex(bufferInfo); + adapter.flush(); + + assertThat(adapter.dequeueOutputBufferIndex(bufferInfo)) + .isEqualTo(MediaCodec.INFO_TRY_AGAIN_LATER); + } + + @Test + public void dequeueOutputBufferIndex_withFlushCompletedAndOutputBuffer_returnsOutputBuffer() + throws InterruptedException { + adapter.start(); + Looper looper = handlerThread.getLooper(); + Handler handler = new Handler(looper); + // Enqueue 10 callbacks from codec + for (int i = 0; i < 10; i++) { + int bufferIndex = i; + MediaCodec.BufferInfo outBufferInfo = new MediaCodec.BufferInfo(); + outBufferInfo.presentationTimeUs = i; + handler.post(() -> adapter.onOutputBufferAvailable(codec, bufferIndex, outBufferInfo)); + } + adapter.flush(); // Enqueues a flush event after the onOutputBufferAvailable callbacks + // Enqueue another onOutputBufferAvailable after the flush event + MediaCodec.BufferInfo lastBufferInfo = new MediaCodec.BufferInfo(); + lastBufferInfo.presentationTimeUs = 10; + handler.post(() -> adapter.onOutputBufferAvailable(codec, 10, lastBufferInfo)); + + // Wait until all tasks have been handled + Shadows.shadowOf(handlerThread.getLooper()).idle(); + assertThat(adapter.dequeueOutputBufferIndex(bufferInfo)).isEqualTo(10); + assertBufferInfosEqual(lastBufferInfo, bufferInfo); + } + + @Test + public void dequeueOutputBufferIndex_withMediaCodecError_throwsException() { + adapter.start(); + adapter.onMediaCodecError(new IllegalStateException("error from codec")); + + assertThrows(IllegalStateException.class, () -> adapter.dequeueOutputBufferIndex(bufferInfo)); + } + + @Test + public void getOutputFormat_withoutFormatReceived_throwsException() { + adapter.start(); + + assertThrows(IllegalStateException.class, () -> adapter.getOutputFormat()); + } + + @Test + public void getOutputFormat_withMultipleFormats_returnsCorrectFormat() { + adapter.start(); + MediaFormat[] formats = new MediaFormat[10]; + for (int i = 0; i < formats.length; i++) { + formats[i] = new MediaFormat(); + adapter.onOutputFormatChanged(codec, formats[i]); + } + + for (int i = 0; i < 10; i++) { + assertThat(adapter.dequeueOutputBufferIndex(bufferInfo)) + .isEqualTo(MediaCodec.INFO_OUTPUT_FORMAT_CHANGED); + assertThat(adapter.getOutputFormat()).isEqualTo(formats[i]); + // A subsequent call to getOutputFormat() should return the previously fetched format + assertThat(adapter.getOutputFormat()).isEqualTo(formats[i]); + } + assertThat(adapter.dequeueOutputBufferIndex(bufferInfo)) + .isEqualTo(MediaCodec.INFO_TRY_AGAIN_LATER); + } + + @Test + public void getOutputFormat_afterFlush_returnsPreviousFormat() { + MediaFormat format = new MediaFormat(); + adapter.start(); + adapter.onOutputFormatChanged(codec, format); + + assertThat(adapter.dequeueOutputBufferIndex(bufferInfo)) + .isEqualTo(MediaCodec.INFO_OUTPUT_FORMAT_CHANGED); + assertThat(adapter.getOutputFormat()).isEqualTo(format); + + adapter.flush(); + Shadows.shadowOf(handlerThread.getLooper()).idle(); + assertThat(adapter.getOutputFormat()).isEqualTo(format); + } + + @Test + public void flush_multipleTimes_onlyLastFlushExecutes() { + AtomicInteger codecStartCalls = new AtomicInteger(0); + adapter.setCodecStartRunnable(() -> codecStartCalls.incrementAndGet()); + adapter.start(); + Looper looper = handlerThread.getLooper(); + Handler handler = new Handler(looper); + handler.post(() -> adapter.onInputBufferAvailable(codec, 0)); + adapter.flush(); // Enqueues a flush event + handler.post(() -> adapter.onInputBufferAvailable(codec, 2)); + AtomicInteger milestoneCount = new AtomicInteger(0); + handler.post(() -> milestoneCount.incrementAndGet()); + adapter.flush(); // Enqueues a second flush event + handler.post(() -> adapter.onInputBufferAvailable(codec, 3)); + + // Progress the looper until the milestoneCount is increased: + // adapter.start() called codec.start() but first flush event should have been a no-op + ShadowLooper shadowLooper = shadowOf(looper); + while (milestoneCount.get() < 1) { + shadowLooper.runOneTask(); + } + assertThat(codecStartCalls.get()).isEqualTo(1); + + shadowLooper.idle(); + assertThat(adapter.dequeueInputBufferIndex()).isEqualTo(3); + assertThat(codecStartCalls.get()).isEqualTo(2); + } + + @Test + public void flush_andImmediatelyShutdown_flushIsNoOp() { + AtomicInteger codecStartCalls = new AtomicInteger(0); + adapter.setCodecStartRunnable(() -> codecStartCalls.incrementAndGet()); + adapter.start(); + // Grab reference to Looper before shutting down the adapter otherwise handlerThread.getLooper() + // might return null. + Looper looper = handlerThread.getLooper(); + adapter.flush(); + adapter.shutdown(); + + Shadows.shadowOf(looper).idle(); + // Only adapter.start() called codec#start() + assertThat(codecStartCalls.get()).isEqualTo(1); + } + + private static class TestHandlerThread extends HandlerThread { + + private static final AtomicLong INSTANCES_STARTED = new AtomicLong(0); + + public TestHandlerThread(String name) { + super(name); + } + + @Override + public synchronized void start() { + super.start(); + INSTANCES_STARTED.incrementAndGet(); + } + + @Override + public boolean quit() { + boolean quit = super.quit(); + INSTANCES_STARTED.decrementAndGet(); + return quit; + } + } +} diff --git a/library/core/src/test/java/com/google/android/exoplayer2/metadata/MetadataRendererTest.java b/library/core/src/test/java/com/google/android/exoplayer2/metadata/MetadataRendererTest.java index 1ad0ce6b79..4d1b4f601b 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/metadata/MetadataRendererTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/metadata/MetadataRendererTest.java @@ -12,7 +12,6 @@ * 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; @@ -28,6 +27,7 @@ import com.google.android.exoplayer2.metadata.emsg.EventMessageEncoder; import com.google.android.exoplayer2.metadata.id3.TextInformationFrame; import com.google.android.exoplayer2.metadata.scte35.TimeSignalCommand; import com.google.android.exoplayer2.testutil.FakeSampleStream; +import com.google.android.exoplayer2.testutil.FakeSampleStream.FakeSampleStreamItem; import com.google.android.exoplayer2.testutil.TestUtil; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.MimeTypes; @@ -64,7 +64,7 @@ public class MetadataRendererTest { 0x00, 0x00, 0x00, 0x00)); // CRC_32 (ignored, check happens at extraction). private static final Format EMSG_FORMAT = - Format.createSampleFormat(null, MimeTypes.APPLICATION_EMSG, Format.OFFSET_SAMPLE_RELATIVE); + new Format.Builder().setSampleMimeType(MimeTypes.APPLICATION_EMSG).build(); private final EventMessageEncoder eventMessageEncoder = new EventMessageEncoder(); @@ -142,7 +142,13 @@ public class MetadataRendererTest { MetadataRenderer renderer = new MetadataRenderer(metadata::add, /* outputLooper= */ null); renderer.replaceStream( new Format[] {EMSG_FORMAT}, - new FakeSampleStream(EMSG_FORMAT, /* eventDispatcher= */ null, input), + new FakeSampleStream( + EMSG_FORMAT, + /* eventDispatcher= */ null, + /* firstSampleTimeUs= */ 0, + /* timeUsIncrement= */ 0, + new FakeSampleStreamItem(input), + FakeSampleStreamItem.END_OF_STREAM_ITEM), /* offsetUs= */ 0L); renderer.render(/* positionUs= */ 0, /* elapsedRealtimeUs= */ 0); // Read the format renderer.render(/* positionUs= */ 0, /* elapsedRealtimeUs= */ 0); // Read the data diff --git a/library/core/src/test/java/com/google/android/exoplayer2/metadata/dvbsi/AppInfoTableDecoderTest.java b/library/core/src/test/java/com/google/android/exoplayer2/metadata/dvbsi/AppInfoTableDecoderTest.java new file mode 100644 index 0000000000..39de14b893 --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/metadata/dvbsi/AppInfoTableDecoderTest.java @@ -0,0 +1,105 @@ +/* + * Copyright (C) 2020 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.dvbsi; + +import static com.google.android.exoplayer2.testutil.TestUtil.createByteArray; +import static com.google.android.exoplayer2.testutil.TestUtil.createMetadataInputBuffer; +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; + +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.android.exoplayer2.metadata.Metadata; +import com.google.android.exoplayer2.metadata.MetadataInputBuffer; +import com.google.android.exoplayer2.testutil.TestUtil; +import java.io.IOException; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Tests for {@link AppInfoTableDecoder}. */ +@RunWith(AndroidJUnit4.class) +public final class AppInfoTableDecoderTest { + + private static final String TYPICAL_FILE = "dvbsi/ait_typical.bin"; + private static final String NO_URL_BASE_FILE = "dvbsi/ait_no_url_base.bin"; + private static final String NO_URL_PATH_FILE = "dvbsi/ait_no_url_path.bin"; + + @Test + public void decode_typical() throws Exception { + AppInfoTableDecoder decoder = new AppInfoTableDecoder(); + Metadata metadata = decoder.decode(createMetadataInputBuffer(readTestFile(TYPICAL_FILE))); + + assertThat(metadata.length()).isEqualTo(2); + Metadata.Entry firstEntry = metadata.get(0); + assertThat(firstEntry).isInstanceOf(AppInfoTable.class); + assertThat(((AppInfoTable) firstEntry).controlCode) + .isEqualTo(AppInfoTable.CONTROL_CODE_AUTOSTART); + assertThat(((AppInfoTable) firstEntry).url).isEqualTo("http://example.com/path/foo"); + Metadata.Entry secondEntry = metadata.get(1); + assertThat(secondEntry).isInstanceOf(AppInfoTable.class); + assertThat(((AppInfoTable) secondEntry).controlCode) + .isEqualTo(AppInfoTable.CONTROL_CODE_PRESENT); + assertThat(((AppInfoTable) secondEntry).url).isEqualTo("http://google.com/path/bar"); + } + + @Test + public void decode_noUrlBase() throws Exception { + AppInfoTableDecoder decoder = new AppInfoTableDecoder(); + Metadata metadata = decoder.decode(createMetadataInputBuffer(readTestFile(NO_URL_BASE_FILE))); + + assertThat(metadata).isNull(); + } + + @Test + public void decode_noUrlPath() throws Exception { + AppInfoTableDecoder decoder = new AppInfoTableDecoder(); + Metadata metadata = decoder.decode(createMetadataInputBuffer(readTestFile(NO_URL_PATH_FILE))); + + assertThat(metadata).isNull(); + } + + @Test + public void decode_failsIfPositionNonZero() { + AppInfoTableDecoder decoder = new AppInfoTableDecoder(); + MetadataInputBuffer buffer = createMetadataInputBuffer(createByteArray(1, 2, 3)); + buffer.data.position(1); + + assertThrows(IllegalArgumentException.class, () -> decoder.decode(buffer)); + } + + @Test + public void decode_failsIfBufferHasNoArray() { + AppInfoTableDecoder decoder = new AppInfoTableDecoder(); + MetadataInputBuffer buffer = createMetadataInputBuffer(createByteArray(1, 2, 3)); + buffer.data = buffer.data.asReadOnlyBuffer(); + + assertThrows(IllegalArgumentException.class, () -> decoder.decode(buffer)); + } + + @Test + public void decode_failsIfArrayOffsetNonZero() { + AppInfoTableDecoder decoder = new AppInfoTableDecoder(); + MetadataInputBuffer buffer = createMetadataInputBuffer(createByteArray(1, 2, 3)); + buffer.data.position(1); + buffer.data = buffer.data.slice(); + + assertThrows(IllegalArgumentException.class, () -> decoder.decode(buffer)); + } + + private static byte[] readTestFile(String name) throws IOException { + return TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), name); + } +} diff --git a/library/core/src/test/java/com/google/android/exoplayer2/metadata/icy/IcyDecoderTest.java b/library/core/src/test/java/com/google/android/exoplayer2/metadata/icy/IcyDecoderTest.java index 72237d665c..49cca0367d 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/metadata/icy/IcyDecoderTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/metadata/icy/IcyDecoderTest.java @@ -15,10 +15,18 @@ */ package com.google.android.exoplayer2.metadata.icy; +import static com.google.android.exoplayer2.testutil.TestUtil.createByteArray; +import static com.google.android.exoplayer2.testutil.TestUtil.createMetadataInputBuffer; import static com.google.common.truth.Truth.assertThat; +import static java.nio.charset.StandardCharsets.ISO_8859_1; +import static java.nio.charset.StandardCharsets.UTF_16; +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.junit.Assert.assertThrows; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.metadata.Metadata; +import com.google.android.exoplayer2.metadata.MetadataInputBuffer; +import com.google.android.exoplayer2.testutil.TestUtil; import org.junit.Test; import org.junit.runner.RunWith; @@ -26,11 +34,13 @@ import org.junit.runner.RunWith; @RunWith(AndroidJUnit4.class) public final class IcyDecoderTest { + private final IcyDecoder decoder = new IcyDecoder(); + @Test public void decode() { - IcyDecoder decoder = new IcyDecoder(); - String icyContent = "StreamTitle='test title';StreamURL='test_url';"; - Metadata metadata = decoder.decode(icyContent); + byte[] icyContent = "StreamTitle='test title';StreamURL='test_url';".getBytes(UTF_8); + + Metadata metadata = decoder.decode(createMetadataInputBuffer(icyContent)); assertThat(metadata.length()).isEqualTo(1); IcyInfo streamInfo = (IcyInfo) metadata.get(0); @@ -39,11 +49,29 @@ public final class IcyDecoderTest { assertThat(streamInfo.url).isEqualTo("test_url"); } + @Test + // Check the decoder is reading MetadataInputBuffer.data.limit() correctly. + public void decode_respectsLimit() { + byte[] icyTitle = "StreamTitle='test title';".getBytes(UTF_8); + byte[] icyUrl = "StreamURL='test_url';".getBytes(UTF_8); + byte[] paddedRawBytes = TestUtil.joinByteArrays(icyTitle, icyUrl); + MetadataInputBuffer metadataBuffer = createMetadataInputBuffer(paddedRawBytes); + // Stop before the stream URL. + metadataBuffer.data.limit(icyTitle.length); + Metadata metadata = decoder.decode(metadataBuffer); + + assertThat(metadata.length()).isEqualTo(1); + IcyInfo streamInfo = (IcyInfo) metadata.get(0); + assertThat(streamInfo.rawMetadata).isEqualTo(icyTitle); + assertThat(streamInfo.title).isEqualTo("test title"); + assertThat(streamInfo.url).isNull(); + } + @Test public void decode_titleOnly() { - IcyDecoder decoder = new IcyDecoder(); - String icyContent = "StreamTitle='test title';"; - Metadata metadata = decoder.decode(icyContent); + byte[] icyContent = "StreamTitle='test title';".getBytes(UTF_8); + + Metadata metadata = decoder.decode(createMetadataInputBuffer(icyContent)); assertThat(metadata.length()).isEqualTo(1); IcyInfo streamInfo = (IcyInfo) metadata.get(0); @@ -54,10 +82,11 @@ public final class IcyDecoderTest { @Test public void decode_extraTags() { - String icyContent = - "StreamTitle='test title';StreamURL='test_url';CustomTag|withWeirdSeparator"; - IcyDecoder decoder = new IcyDecoder(); - Metadata metadata = decoder.decode(icyContent); + byte[] icyContent = + "StreamTitle='test title';StreamURL='test_url';CustomTag|withWeirdSeparator" + .getBytes(UTF_8); + + Metadata metadata = decoder.decode(createMetadataInputBuffer(icyContent)); assertThat(metadata.length()).isEqualTo(1); IcyInfo streamInfo = (IcyInfo) metadata.get(0); @@ -68,9 +97,9 @@ public final class IcyDecoderTest { @Test public void decode_emptyTitle() { - IcyDecoder decoder = new IcyDecoder(); - String icyContent = "StreamTitle='';StreamURL='test_url';"; - Metadata metadata = decoder.decode(icyContent); + byte[] icyContent = "StreamTitle='';StreamURL='test_url';".getBytes(UTF_8); + + Metadata metadata = decoder.decode(createMetadataInputBuffer(icyContent)); assertThat(metadata.length()).isEqualTo(1); IcyInfo streamInfo = (IcyInfo) metadata.get(0); @@ -81,9 +110,9 @@ public final class IcyDecoderTest { @Test public void decode_semiColonInTitle() { - IcyDecoder decoder = new IcyDecoder(); - String icyContent = "StreamTitle='test; title';StreamURL='test_url';"; - Metadata metadata = decoder.decode(icyContent); + byte[] icyContent = "StreamTitle='test; title';StreamURL='test_url';".getBytes(UTF_8); + + Metadata metadata = decoder.decode(createMetadataInputBuffer(icyContent)); assertThat(metadata.length()).isEqualTo(1); IcyInfo streamInfo = (IcyInfo) metadata.get(0); @@ -94,9 +123,9 @@ public final class IcyDecoderTest { @Test public void decode_quoteInTitle() { - IcyDecoder decoder = new IcyDecoder(); - String icyContent = "StreamTitle='test' title';StreamURL='test_url';"; - Metadata metadata = decoder.decode(icyContent); + byte[] icyContent = "StreamTitle='test' title';StreamURL='test_url';".getBytes(UTF_8); + + Metadata metadata = decoder.decode(createMetadataInputBuffer(icyContent)); assertThat(metadata.length()).isEqualTo(1); IcyInfo streamInfo = (IcyInfo) metadata.get(0); @@ -107,9 +136,9 @@ public final class IcyDecoderTest { @Test public void decode_lineTerminatorInTitle() { - IcyDecoder decoder = new IcyDecoder(); - String icyContent = "StreamTitle='test\r\ntitle';StreamURL='test_url';"; - Metadata metadata = decoder.decode(icyContent); + byte[] icyContent = "StreamTitle='test\r\ntitle';StreamURL='test_url';".getBytes(UTF_8); + + Metadata metadata = decoder.decode(createMetadataInputBuffer(icyContent)); assertThat(metadata.length()).isEqualTo(1); IcyInfo streamInfo = (IcyInfo) metadata.get(0); @@ -119,14 +148,68 @@ public final class IcyDecoderTest { } @Test - public void decode_noReconisedHeaders() { - IcyDecoder decoder = new IcyDecoder(); - Metadata metadata = decoder.decode("NotIcyData"); + public void decode_iso885911() { + // Create an invalid UTF-8 string by using 'é'. + byte[] icyContent = "StreamTitle='tést';StreamURL='tést_url';".getBytes(ISO_8859_1); + + Metadata metadata = decoder.decode(createMetadataInputBuffer(icyContent)); assertThat(metadata.length()).isEqualTo(1); IcyInfo streamInfo = (IcyInfo) metadata.get(0); - assertThat(streamInfo.rawMetadata).isEqualTo("NotIcyData"); + assertThat(streamInfo.rawMetadata).isEqualTo(icyContent); + assertThat(streamInfo.title).isEqualTo("tést"); + assertThat(streamInfo.url).isEqualTo("tést_url"); + } + + @Test + public void decode_unrecognisedEncoding() { + // Create an invalid UTF-8 and ISO-88591-1 string by using 'é'. + byte[] icyContent = "StreamTitle='tést';StreamURL='tést_url';".getBytes(UTF_16); + + Metadata metadata = decoder.decode(createMetadataInputBuffer(icyContent)); + + assertThat(metadata.length()).isEqualTo(1); + IcyInfo streamInfo = (IcyInfo) metadata.get(0); + assertThat(streamInfo.rawMetadata).isEqualTo(icyContent); assertThat(streamInfo.title).isNull(); assertThat(streamInfo.url).isNull(); } + + @Test + public void decode_noRecognisedHeaders() { + byte[] icyContent = "NotIcyData".getBytes(UTF_8); + + Metadata metadata = decoder.decode(createMetadataInputBuffer(icyContent)); + + assertThat(metadata.length()).isEqualTo(1); + IcyInfo streamInfo = (IcyInfo) metadata.get(0); + assertThat(streamInfo.rawMetadata).isEqualTo(icyContent); + assertThat(streamInfo.title).isNull(); + assertThat(streamInfo.url).isNull(); + } + + @Test + public void decode_failsIfPositionNonZero() { + MetadataInputBuffer buffer = createMetadataInputBuffer(createByteArray(1, 2, 3)); + buffer.data.position(1); + + assertThrows(IllegalArgumentException.class, () -> decoder.decode(buffer)); + } + + @Test + public void decode_failsIfBufferHasNoArray() { + MetadataInputBuffer buffer = createMetadataInputBuffer(createByteArray(1, 2, 3)); + buffer.data = buffer.data.asReadOnlyBuffer(); + + assertThrows(IllegalArgumentException.class, () -> decoder.decode(buffer)); + } + + @Test + public void decode_failsIfArrayOffsetNonZero() { + MetadataInputBuffer buffer = createMetadataInputBuffer(createByteArray(1, 2, 3)); + buffer.data.position(1); + buffer.data = buffer.data.slice(); + + assertThrows(IllegalArgumentException.class, () -> decoder.decode(buffer)); + } } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/metadata/icy/IcyInfoTest.java b/library/core/src/test/java/com/google/android/exoplayer2/metadata/icy/IcyInfoTest.java index 2c8e6616c9..5f9b1a931f 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/metadata/icy/IcyInfoTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/metadata/icy/IcyInfoTest.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.metadata.icy; import static com.google.common.truth.Truth.assertThat; +import static java.nio.charset.StandardCharsets.UTF_8; import android.os.Parcel; import androidx.test.ext.junit.runners.AndroidJUnit4; @@ -28,7 +29,8 @@ public final class IcyInfoTest { @Test public void parcelEquals() { - IcyInfo streamInfo = new IcyInfo("StreamName='name';StreamUrl='url'", "name", "url"); + IcyInfo streamInfo = + new IcyInfo("StreamName='name';StreamUrl='url'".getBytes(UTF_8), "name", "url"); // Write to parcel. Parcel parcel = Parcel.obtain(); streamInfo.writeToParcel(parcel, 0); 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 92fa147c30..90c2e7d386 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 @@ -16,7 +16,10 @@ package com.google.android.exoplayer2.metadata.scte35; import static com.google.android.exoplayer2.C.TIME_UNSET; +import static com.google.android.exoplayer2.testutil.TestUtil.createByteArray; +import static com.google.android.exoplayer2.testutil.TestUtil.createMetadataInputBuffer; import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.metadata.Metadata; @@ -42,7 +45,7 @@ public final class SpliceInfoDecoderTest { } @Test - public void testWrappedAroundTimeSignalCommand() { + public void wrappedAroundTimeSignalCommand() { byte[] rawTimeSignalSection = new byte[] { 0, // table_id. (byte) 0x80, // section_syntax_indicator, private_indicator, reserved, section_length(4). @@ -162,9 +165,35 @@ public final class SpliceInfoDecoderTest { assertThat(command.availsExpected).isEqualTo(2); } + @Test + public void decodeFailsIfPositionNonZero() { + MetadataInputBuffer buffer = createMetadataInputBuffer(createByteArray(1, 2, 3)); + buffer.data.position(1); + + assertThrows(IllegalArgumentException.class, () -> decoder.decode(buffer)); + } + + @Test + public void decodeFailsIfBufferHasNoArray() { + MetadataInputBuffer buffer = createMetadataInputBuffer(createByteArray(1, 2, 3)); + buffer.data = buffer.data.asReadOnlyBuffer(); + + assertThrows(IllegalArgumentException.class, () -> decoder.decode(buffer)); + } + + @Test + public void decodeFailsIfArrayOffsetNonZero() { + MetadataInputBuffer buffer = createMetadataInputBuffer(createByteArray(1, 2, 3)); + buffer.data.position(1); + buffer.data = buffer.data.slice(); + + assertThrows(IllegalArgumentException.class, () -> decoder.decode(buffer)); + } + private Metadata feedInputBuffer(byte[] data, long timeUs, long subsampleOffset) { inputBuffer.clear(); inputBuffer.data = ByteBuffer.allocate(data.length).put(data); + inputBuffer.data.flip(); inputBuffer.timeUs = timeUs; inputBuffer.subsampleOffsetUs = subsampleOffset; return decoder.decode(inputBuffer); 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 7abfa44886..cec0d07688 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 @@ -56,7 +56,7 @@ public class ActionFileTest { } @Test - public void testLoadNoDataThrowsIOException() throws Exception { + public void loadNoDataThrowsIOException() throws Exception { ActionFile actionFile = getActionFile("offline/action_file_no_data.exi"); try { actionFile.load(); @@ -67,7 +67,7 @@ public class ActionFileTest { } @Test - public void testLoadIncompleteHeaderThrowsIOException() throws Exception { + public void loadIncompleteHeaderThrowsIOException() throws Exception { ActionFile actionFile = getActionFile("offline/action_file_incomplete_header.exi"); try { actionFile.load(); @@ -78,7 +78,7 @@ public class ActionFileTest { } @Test - public void testLoadZeroActions() throws Exception { + public void loadZeroActions() throws Exception { ActionFile actionFile = getActionFile("offline/action_file_zero_actions.exi"); DownloadRequest[] actions = actionFile.load(); assertThat(actions).isNotNull(); @@ -86,7 +86,7 @@ public class ActionFileTest { } @Test - public void testLoadOneAction() throws Exception { + public void loadOneAction() throws Exception { ActionFile actionFile = getActionFile("offline/action_file_one_action.exi"); DownloadRequest[] actions = actionFile.load(); assertThat(actions).hasLength(1); @@ -94,7 +94,7 @@ public class ActionFileTest { } @Test - public void testLoadTwoActions() throws Exception { + public void loadTwoActions() throws Exception { ActionFile actionFile = getActionFile("offline/action_file_two_actions.exi"); DownloadRequest[] actions = actionFile.load(); assertThat(actions).hasLength(2); @@ -103,7 +103,7 @@ public class ActionFileTest { } @Test - public void testLoadUnsupportedVersion() throws Exception { + public void loadUnsupportedVersion() throws Exception { ActionFile actionFile = getActionFile("offline/action_file_unsupported_version.exi"); try { actionFile.load(); diff --git a/library/core/src/test/java/com/google/android/exoplayer2/offline/DefaultDownloadIndexTest.java b/library/core/src/test/java/com/google/android/exoplayer2/offline/DefaultDownloadIndexTest.java index f42a1c6086..cc1ae4b71b 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/offline/DefaultDownloadIndexTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/offline/DefaultDownloadIndexTest.java @@ -23,6 +23,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.database.DatabaseIOException; import com.google.android.exoplayer2.database.ExoDatabaseProvider; import com.google.android.exoplayer2.database.VersionTable; +import com.google.android.exoplayer2.testutil.DownloadBuilder; import org.junit.After; import org.junit.Before; import org.junit.Test; @@ -248,6 +249,23 @@ public class DefaultDownloadIndexTest { assertEqual(readDownload, download); } + @Test + public void setStatesToRemoving_setsStateAndClearsFailureReason() throws Exception { + String id = "id"; + DownloadBuilder downloadBuilder = + new DownloadBuilder(id) + .setState(Download.STATE_FAILED) + .setFailureReason(Download.FAILURE_REASON_UNKNOWN); + Download download = downloadBuilder.build(); + downloadIndex.putDownload(download); + + downloadIndex.setStatesToRemoving(); + + download = downloadIndex.getDownload(id); + assertThat(download.state).isEqualTo(Download.STATE_REMOVING); + assertThat(download.failureReason).isEqualTo(Download.FAILURE_REASON_NONE); + } + @Test public void setSingleDownloadStopReason_setReasonToNone() throws Exception { String id = "id"; diff --git a/library/core/src/test/java/com/google/android/exoplayer2/offline/DefaultDownloaderFactoryTest.java b/library/core/src/test/java/com/google/android/exoplayer2/offline/DefaultDownloaderFactoryTest.java index c3d23c7d22..5955a9491e 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/offline/DefaultDownloaderFactoryTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/offline/DefaultDownloaderFactoryTest.java @@ -21,6 +21,7 @@ import android.net.Uri; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.upstream.DummyDataSource; import com.google.android.exoplayer2.upstream.cache.Cache; +import com.google.android.exoplayer2.upstream.cache.CacheDataSource; import java.util.Collections; import org.junit.Test; import org.junit.runner.RunWith; @@ -32,9 +33,11 @@ public final class DefaultDownloaderFactoryTest { @Test public void createProgressiveDownloader() throws Exception { - DownloaderConstructorHelper constructorHelper = - new DownloaderConstructorHelper(Mockito.mock(Cache.class), DummyDataSource.FACTORY); - DownloaderFactory factory = new DefaultDownloaderFactory(constructorHelper); + CacheDataSource.Factory cacheDataSourceFactory = + new CacheDataSource.Factory() + .setCache(Mockito.mock(Cache.class)) + .setUpstreamDataSourceFactory(DummyDataSource.FACTORY); + DownloaderFactory factory = new DefaultDownloaderFactory(cacheDataSourceFactory); Downloader downloader = factory.createDownloader( diff --git a/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadHelperTest.java b/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadHelperTest.java index f99864440d..5fa9ae082f 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadHelperTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadHelperTest.java @@ -41,7 +41,6 @@ import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedT import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.util.MimeTypes; -import com.google.android.exoplayer2.util.Util; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; @@ -50,6 +49,7 @@ import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; import org.junit.Before; +import org.junit.BeforeClass; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.annotation.LooperMode; @@ -69,43 +69,61 @@ public class DownloadHelperTest { private static final Format VIDEO_FORMAT_LOW = createVideoFormat(/* bitrate= */ 200_000); private static final Format VIDEO_FORMAT_HIGH = createVideoFormat(/* bitrate= */ 800_000); - private static final Format AUDIO_FORMAT_US = createAudioFormat(/* language= */ "US"); - private static final Format AUDIO_FORMAT_ZH = createAudioFormat(/* language= */ "ZH"); - private static final Format TEXT_FORMAT_US = createTextFormat(/* language= */ "US"); - private static final Format TEXT_FORMAT_ZH = createTextFormat(/* language= */ "ZH"); + private static Format audioFormatUs; + private static Format audioFormatZh; + private static Format textFormatUs; + private static Format textFormatZh; private static final TrackGroup TRACK_GROUP_VIDEO_BOTH = new TrackGroup(VIDEO_FORMAT_LOW, VIDEO_FORMAT_HIGH); private static final TrackGroup TRACK_GROUP_VIDEO_SINGLE = new TrackGroup(VIDEO_FORMAT_LOW); - private static final TrackGroup TRACK_GROUP_AUDIO_US = new TrackGroup(AUDIO_FORMAT_US); - private static final TrackGroup TRACK_GROUP_AUDIO_ZH = new TrackGroup(AUDIO_FORMAT_ZH); - private static final TrackGroup TRACK_GROUP_TEXT_US = new TrackGroup(TEXT_FORMAT_US); - private static final TrackGroup TRACK_GROUP_TEXT_ZH = new TrackGroup(TEXT_FORMAT_ZH); - private static final TrackGroupArray TRACK_GROUP_ARRAY_ALL = - new TrackGroupArray( - TRACK_GROUP_VIDEO_BOTH, - TRACK_GROUP_AUDIO_US, - TRACK_GROUP_AUDIO_ZH, - TRACK_GROUP_TEXT_US, - TRACK_GROUP_TEXT_ZH); - private static final TrackGroupArray TRACK_GROUP_ARRAY_SINGLE = - new TrackGroupArray(TRACK_GROUP_VIDEO_SINGLE, TRACK_GROUP_AUDIO_US); - private static final TrackGroupArray[] TRACK_GROUP_ARRAYS = - new TrackGroupArray[] {TRACK_GROUP_ARRAY_ALL, TRACK_GROUP_ARRAY_SINGLE}; + private static TrackGroup trackGroupAudioUs; + private static TrackGroup trackGroupAudioZh; + private static TrackGroup trackGroupTextUs; + private static TrackGroup trackGroupTextZh; - private Uri testUri; + private static TrackGroupArray trackGroupArrayAll; + private static TrackGroupArray trackGroupArraySingle; + private static TrackGroupArray[] trackGroupArrays; + + private static Uri testUri; private DownloadHelper downloadHelper; + @BeforeClass + public static void staticSetUp() { + audioFormatUs = createAudioFormat(/* language= */ "US"); + audioFormatZh = createAudioFormat(/* language= */ "ZH"); + textFormatUs = createTextFormat(/* language= */ "US"); + textFormatZh = createTextFormat(/* language= */ "ZH"); + + trackGroupAudioUs = new TrackGroup(audioFormatUs); + trackGroupAudioZh = new TrackGroup(audioFormatZh); + trackGroupTextUs = new TrackGroup(textFormatUs); + trackGroupTextZh = new TrackGroup(textFormatZh); + + trackGroupArrayAll = + new TrackGroupArray( + TRACK_GROUP_VIDEO_BOTH, + trackGroupAudioUs, + trackGroupAudioZh, + trackGroupTextUs, + trackGroupTextZh); + trackGroupArraySingle = + new TrackGroupArray(TRACK_GROUP_VIDEO_SINGLE, trackGroupAudioUs); + trackGroupArrays = + new TrackGroupArray[] {trackGroupArrayAll, trackGroupArraySingle}; + + testUri = Uri.parse("http://test.uri"); + } + @Before public void setUp() { - testUri = Uri.parse("http://test.uri"); - - FakeRenderer videoRenderer = new FakeRenderer(VIDEO_FORMAT_LOW, VIDEO_FORMAT_HIGH); - FakeRenderer audioRenderer = new FakeRenderer(AUDIO_FORMAT_US, AUDIO_FORMAT_ZH); - FakeRenderer textRenderer = new FakeRenderer(TEXT_FORMAT_US, TEXT_FORMAT_ZH); + FakeRenderer videoRenderer = new FakeRenderer(C.TRACK_TYPE_VIDEO); + FakeRenderer audioRenderer = new FakeRenderer(C.TRACK_TYPE_AUDIO); + FakeRenderer textRenderer = new FakeRenderer(C.TRACK_TYPE_TEXT); RenderersFactory renderersFactory = - (handler, videoListener, audioListener, metadata, text, drm) -> + (handler, videoListener, audioListener, metadata, text) -> new Renderer[] {textRenderer, audioRenderer, videoRenderer}; downloadHelper = @@ -115,7 +133,7 @@ public class DownloadHelperTest { TEST_CACHE_KEY, new TestMediaSource(), DownloadHelper.DEFAULT_TRACK_SELECTOR_PARAMETERS_WITHOUT_VIEWPORT, - Util.getRendererCapabilities(renderersFactory, /* drmSessionManager= */ null)); + DownloadHelper.getRendererCapabilities(renderersFactory)); } @Test @@ -143,8 +161,8 @@ public class DownloadHelperTest { TrackGroupArray trackGroupArrayPeriod0 = downloadHelper.getTrackGroups(/* periodIndex= */ 0); TrackGroupArray trackGroupArrayPeriod1 = downloadHelper.getTrackGroups(/* periodIndex= */ 1); - assertThat(trackGroupArrayPeriod0).isEqualTo(TRACK_GROUP_ARRAYS[0]); - assertThat(trackGroupArrayPeriod1).isEqualTo(TRACK_GROUP_ARRAYS[1]); + assertThat(trackGroupArrayPeriod0).isEqualTo(trackGroupArrays[0]); + assertThat(trackGroupArrayPeriod1).isEqualTo(trackGroupArrays[1]); } @Test @@ -162,13 +180,13 @@ public class DownloadHelperTest { assertThat(mappedTracks0.getTrackGroups(/* rendererIndex= */ 1).length).isEqualTo(2); assertThat(mappedTracks0.getTrackGroups(/* rendererIndex= */ 2).length).isEqualTo(1); assertThat(mappedTracks0.getTrackGroups(/* rendererIndex= */ 0).get(/* index= */ 0)) - .isEqualTo(TRACK_GROUP_TEXT_US); + .isEqualTo(trackGroupTextUs); assertThat(mappedTracks0.getTrackGroups(/* rendererIndex= */ 0).get(/* index= */ 1)) - .isEqualTo(TRACK_GROUP_TEXT_ZH); + .isEqualTo(trackGroupTextZh); assertThat(mappedTracks0.getTrackGroups(/* rendererIndex= */ 1).get(/* index= */ 0)) - .isEqualTo(TRACK_GROUP_AUDIO_US); + .isEqualTo(trackGroupAudioUs); assertThat(mappedTracks0.getTrackGroups(/* rendererIndex= */ 1).get(/* index= */ 1)) - .isEqualTo(TRACK_GROUP_AUDIO_ZH); + .isEqualTo(trackGroupAudioZh); assertThat(mappedTracks0.getTrackGroups(/* rendererIndex= */ 2).get(/* index= */ 0)) .isEqualTo(TRACK_GROUP_VIDEO_BOTH); @@ -180,7 +198,7 @@ public class DownloadHelperTest { assertThat(mappedTracks1.getTrackGroups(/* rendererIndex= */ 1).length).isEqualTo(1); assertThat(mappedTracks1.getTrackGroups(/* rendererIndex= */ 2).length).isEqualTo(1); assertThat(mappedTracks1.getTrackGroups(/* rendererIndex= */ 1).get(/* index= */ 0)) - .isEqualTo(TRACK_GROUP_AUDIO_US); + .isEqualTo(trackGroupAudioUs); assertThat(mappedTracks1.getTrackGroups(/* rendererIndex= */ 2).get(/* index= */ 0)) .isEqualTo(TRACK_GROUP_VIDEO_SINGLE); } @@ -202,12 +220,12 @@ public class DownloadHelperTest { List selectedVideo1 = downloadHelper.getTrackSelections(/* periodIndex= */ 1, /* rendererIndex= */ 2); - assertSingleTrackSelectionEquals(selectedText0, TRACK_GROUP_TEXT_US, 0); - assertSingleTrackSelectionEquals(selectedAudio0, TRACK_GROUP_AUDIO_US, 0); + assertSingleTrackSelectionEquals(selectedText0, trackGroupTextUs, 0); + assertSingleTrackSelectionEquals(selectedAudio0, trackGroupAudioUs, 0); assertSingleTrackSelectionEquals(selectedVideo0, TRACK_GROUP_VIDEO_BOTH, 1); assertThat(selectedText1).isEmpty(); - assertSingleTrackSelectionEquals(selectedAudio1, TRACK_GROUP_AUDIO_US, 0); + assertSingleTrackSelectionEquals(selectedAudio1, trackGroupAudioUs, 0); assertSingleTrackSelectionEquals(selectedVideo1, TRACK_GROUP_VIDEO_SINGLE, 0); } @@ -236,7 +254,7 @@ public class DownloadHelperTest { // Verify assertThat(selectedText1).isEmpty(); - assertSingleTrackSelectionEquals(selectedAudio1, TRACK_GROUP_AUDIO_US, 0); + assertSingleTrackSelectionEquals(selectedAudio1, trackGroupAudioUs, 0); assertSingleTrackSelectionEquals(selectedVideo1, TRACK_GROUP_VIDEO_SINGLE, 0); } @@ -266,12 +284,12 @@ public class DownloadHelperTest { List selectedVideo1 = downloadHelper.getTrackSelections(/* periodIndex= */ 1, /* rendererIndex= */ 2); - assertSingleTrackSelectionEquals(selectedText0, TRACK_GROUP_TEXT_ZH, 0); - assertSingleTrackSelectionEquals(selectedAudio0, TRACK_GROUP_AUDIO_ZH, 0); + assertSingleTrackSelectionEquals(selectedText0, trackGroupTextZh, 0); + assertSingleTrackSelectionEquals(selectedAudio0, trackGroupAudioZh, 0); assertThat(selectedVideo0).isEmpty(); assertThat(selectedText1).isEmpty(); - assertSingleTrackSelectionEquals(selectedAudio1, TRACK_GROUP_AUDIO_US, 0); + assertSingleTrackSelectionEquals(selectedAudio1, trackGroupAudioUs, 0); assertSingleTrackSelectionEquals(selectedVideo1, TRACK_GROUP_VIDEO_SINGLE, 0); } @@ -302,14 +320,14 @@ public class DownloadHelperTest { List selectedVideo1 = downloadHelper.getTrackSelections(/* periodIndex= */ 1, /* rendererIndex= */ 2); - assertSingleTrackSelectionEquals(selectedText0, TRACK_GROUP_TEXT_US, 0); + assertSingleTrackSelectionEquals(selectedText0, trackGroupTextUs, 0); assertThat(selectedAudio0).hasSize(2); - assertTrackSelectionEquals(selectedAudio0.get(0), TRACK_GROUP_AUDIO_US, 0); - assertTrackSelectionEquals(selectedAudio0.get(1), TRACK_GROUP_AUDIO_ZH, 0); + assertTrackSelectionEquals(selectedAudio0.get(0), trackGroupAudioUs, 0); + assertTrackSelectionEquals(selectedAudio0.get(1), trackGroupAudioZh, 0); assertSingleTrackSelectionEquals(selectedVideo0, TRACK_GROUP_VIDEO_BOTH, 0, 1); assertThat(selectedText1).isEmpty(); - assertSingleTrackSelectionEquals(selectedAudio1, TRACK_GROUP_AUDIO_US, 0); + assertSingleTrackSelectionEquals(selectedAudio1, trackGroupAudioUs, 0); assertSingleTrackSelectionEquals(selectedVideo1, TRACK_GROUP_VIDEO_SINGLE, 0); } @@ -338,12 +356,12 @@ public class DownloadHelperTest { assertThat(selectedVideo0).isEmpty(); assertThat(selectedText0).isEmpty(); assertThat(selectedAudio0).hasSize(2); - assertTrackSelectionEquals(selectedAudio0.get(0), TRACK_GROUP_AUDIO_ZH, 0); - assertTrackSelectionEquals(selectedAudio0.get(1), TRACK_GROUP_AUDIO_US, 0); + assertTrackSelectionEquals(selectedAudio0.get(0), trackGroupAudioZh, 0); + assertTrackSelectionEquals(selectedAudio0.get(1), trackGroupAudioUs, 0); assertThat(selectedVideo1).isEmpty(); assertThat(selectedText1).isEmpty(); - assertSingleTrackSelectionEquals(selectedAudio1, TRACK_GROUP_AUDIO_US, 0); + assertSingleTrackSelectionEquals(selectedAudio1, trackGroupAudioUs, 0); } @Test @@ -372,8 +390,8 @@ public class DownloadHelperTest { assertThat(selectedVideo0).isEmpty(); assertThat(selectedAudio0).isEmpty(); assertThat(selectedText0).hasSize(2); - assertTrackSelectionEquals(selectedText0.get(0), TRACK_GROUP_TEXT_ZH, 0); - assertTrackSelectionEquals(selectedText0.get(1), TRACK_GROUP_TEXT_US, 0); + assertTrackSelectionEquals(selectedText0.get(0), trackGroupTextZh, 0); + assertTrackSelectionEquals(selectedText0.get(1), trackGroupTextUs, 0); assertThat(selectedVideo1).isEmpty(); assertThat(selectedAudio1).isEmpty(); @@ -436,40 +454,25 @@ public class DownloadHelperTest { } private static Format createVideoFormat(int bitrate) { - return Format.createVideoSampleFormat( - /* id= */ null, - /* sampleMimeType= */ MimeTypes.VIDEO_H264, - /* codecs= */ null, - /* bitrate= */ bitrate, - /* maxInputSize= */ Format.NO_VALUE, - /* width= */ 480, - /* height= */ 360, - /* frameRate= */ Format.NO_VALUE, - /* initializationData= */ null, - /* drmInitData= */ null); + return new Format.Builder() + .setSampleMimeType(MimeTypes.VIDEO_H264) + .setAverageBitrate(bitrate) + .build(); } private static Format createAudioFormat(String language) { - return Format.createAudioSampleFormat( - /* id= */ null, - /* sampleMimeType= */ MimeTypes.AUDIO_AAC, - /* codecs= */ null, - /* bitrate= */ 48000, - /* maxInputSize= */ Format.NO_VALUE, - /* channelCount= */ 2, - /* sampleRate */ 44100, - /* initializationData= */ null, - /* drmInitData= */ null, - /* selectionFlags= */ C.SELECTION_FLAG_DEFAULT, - /* language= */ language); + return new Format.Builder() + .setSampleMimeType(MimeTypes.AUDIO_AAC) + .setLanguage(language) + .build(); } private static Format createTextFormat(String language) { - return Format.createTextSampleFormat( - /* id= */ null, - /* sampleMimeType= */ MimeTypes.TEXT_VTT, - /* selectionFlags= */ C.SELECTION_FLAG_DEFAULT, - /* language= */ language); + return new Format.Builder() + .setSampleMimeType(MimeTypes.TEXT_VTT) + .setSelectionFlags(C.SELECTION_FLAG_DEFAULT) + .setLanguage(language) + .build(); } private static void assertSingleTrackSelectionEquals( @@ -501,7 +504,7 @@ public class DownloadHelperTest { public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs) { int periodIndex = TEST_TIMELINE.getIndexOfPeriod(id.periodUid); return new FakeMediaPeriod( - TRACK_GROUP_ARRAYS[periodIndex], + trackGroupArrays[periodIndex], new EventDispatcher() .withParameters(/* windowIndex= */ 0, id, /* mediaTimeOffsetMs= */ 0)) { @Override @@ -509,7 +512,7 @@ public class DownloadHelperTest { List result = new ArrayList<>(); for (TrackSelection trackSelection : trackSelections) { int groupIndex = - TRACK_GROUP_ARRAYS[periodIndex].indexOf(trackSelection.getTrackGroup()); + trackGroupArrays[periodIndex].indexOf(trackSelection.getTrackGroup()); for (int i = 0; i < trackSelection.length(); i++) { result.add( new StreamKey(periodIndex, groupIndex, trackSelection.getIndexInTrackGroup(i))); 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 452f20e957..0f4abf2f89 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 @@ -18,28 +18,29 @@ package com.google.android.exoplayer2.offline; import static com.google.common.truth.Truth.assertThat; import android.net.Uri; +import androidx.annotation.GuardedBy; +import androidx.annotation.Nullable; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.offline.Download.State; import com.google.android.exoplayer2.scheduler.Requirements; +import com.google.android.exoplayer2.testutil.DownloadBuilder; import com.google.android.exoplayer2.testutil.DummyMainThread; import com.google.android.exoplayer2.testutil.DummyMainThread.TestRunnable; import com.google.android.exoplayer2.testutil.TestDownloadManagerListener; import com.google.android.exoplayer2.testutil.TestUtil; +import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.ConditionVariable; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; import java.util.List; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; -import org.mockito.MockitoAnnotations; import org.robolectric.annotation.LooperMode; import org.robolectric.annotation.LooperMode.Mode; import org.robolectric.shadows.ShadowLog; @@ -49,41 +50,31 @@ import org.robolectric.shadows.ShadowLog; @LooperMode(Mode.PAUSED) public class DownloadManagerTest { - /** Used to check if condition becomes true in this time interval. */ - private static final int ASSERT_TRUE_TIMEOUT = 10000; - /** Used to check if condition stays false for this time interval. */ - private static final int ASSERT_FALSE_TIME = 1000; - /** Maximum retry delay in DownloadManager. */ - private static final int MAX_RETRY_DELAY = 5000; - /** Maximum number of times a downloader can be restarted before doing a released check. */ - private static final int MAX_STARTS_BEFORE_RELEASED = 1; - /** A stop reason. */ + /** Timeout to use when blocking on conditions that we expect to become unblocked. */ + private static final int TIMEOUT_MS = 10_000; + /** An application provided stop reason. */ private static final int APP_STOP_REASON = 1; - /** The minimum number of times a task must be retried before failing. */ + /** The minimum number of times a download must be retried before failing. */ private static final int MIN_RETRY_COUNT = 3; /** Dummy value for the current time. */ private static final long NOW_MS = 1234; - private Uri uri1; - private Uri uri2; - private Uri uri3; - private DummyMainThread dummyMainThread; - private DefaultDownloadIndex downloadIndex; - private TestDownloadManagerListener downloadManagerListener; - private FakeDownloaderFactory downloaderFactory; + private static final String ID1 = "id1"; + private static final String ID2 = "id2"; + private static final String ID3 = "id3"; + + @GuardedBy("downloaders") + private final List downloaders = new ArrayList<>(); + private DownloadManager downloadManager; + private TestDownloadManagerListener downloadManagerListener; + private DummyMainThread dummyMainThread; @Before public void setUp() throws Exception { ShadowLog.stream = System.out; - MockitoAnnotations.initMocks(this); - uri1 = Uri.parse("http://abc.com/media1"); - uri2 = Uri.parse("http://abc.com/media2"); - uri3 = Uri.parse("http://abc.com/media3"); dummyMainThread = new DummyMainThread(); - downloadIndex = new DefaultDownloadIndex(TestUtil.getInMemoryDatabaseProvider()); - downloaderFactory = new FakeDownloaderFactory(); - setUpDownloadManager(100); + setupDownloadManager(/* maxParallelDownloads= */ 100); } @After @@ -93,395 +84,510 @@ public class DownloadManagerTest { } @Test - public void downloadRunner_multipleInstancePerContent_throwsException() { - boolean exceptionThrown = false; - try { - new DownloadRunner(uri1); - new DownloadRunner(uri1); - // can't put fail() here as it would be caught in the catch below. - } catch (Throwable e) { - exceptionThrown = true; - } - assertThat(exceptionThrown).isTrue(); + public void downloadRequest_downloads() throws Throwable { + postDownloadRequest(ID1); + assertDownloading(ID1); + + FakeDownloader downloader = getDownloaderAt(0); + downloader.assertId(ID1); + downloader.assertDownloadStarted(); + downloader.finish(); + assertCompleted(ID1); + + downloadManagerListener.blockUntilIdleAndThrowAnyFailure(); + assertDownloaderCount(1); + assertDownloadIndexSize(1); + assertCurrentDownloadCount(0); } @Test - public void multipleRequestsForTheSameContent_executedOnTheSameTask() { - // Two download requests on first task - new DownloadRunner(uri1).postDownloadRequest().postDownloadRequest(); - // One download, one remove requests on second task - new DownloadRunner(uri2).postDownloadRequest().postRemoveRequest(); - // Two remove requests on third task - new DownloadRunner(uri3).postRemoveRequest().postRemoveRequest(); + public void removeRequest_cancelsAndRemovesDownload() throws Throwable { + postDownloadRequest(ID1); + + FakeDownloader downloader0 = getDownloaderAt(0); + downloader0.assertId(ID1); + downloader0.assertDownloadStarted(); + assertDownloading(ID1); + + // The download will be canceled by the remove request. + postRemoveRequest(ID1); + downloader0.assertCanceled(); + assertRemoving(ID1); + + // The download will be removed. + FakeDownloader downloader1 = getDownloaderAt(1); + downloader1.assertId(ID1); + downloader1.assertRemoveStarted(); + downloader1.finish(); + assertRemoved(ID1); + + downloadManagerListener.blockUntilIdleAndThrowAnyFailure(); + assertDownloaderCount(2); + assertDownloadIndexSize(0); + assertCurrentDownloadCount(0); } @Test - public void requestsForDifferentContent_executedOnDifferentTasks() { - TaskWrapper task1 = new DownloadRunner(uri1).postDownloadRequest().getTask(); - TaskWrapper task2 = new DownloadRunner(uri2).postDownloadRequest().getTask(); - TaskWrapper task3 = new DownloadRunner(uri3).postRemoveRequest().getTask(); - - assertThat(task1).isNoneOf(task2, task3); - assertThat(task2).isNotEqualTo(task3); - } - - @Test - public void postDownloadRequest_downloads() throws Throwable { - DownloadRunner runner = new DownloadRunner(uri1); - TaskWrapper task = runner.postDownloadRequest().getTask(); - task.assertDownloading(); - runner.getDownloader(0).unblock().assertReleased().assertStartCount(1); - task.assertCompleted(); - runner.assertCreatedDownloaderCount(1); - downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); - assertThat(downloadManager.getCurrentDownloads()).isEmpty(); - } - - @Test - public void postRemoveRequest_removes() throws Throwable { - DownloadRunner runner = new DownloadRunner(uri1); - TaskWrapper task = runner.postDownloadRequest().postRemoveRequest().getTask(); - task.assertRemoving(); - runner.getDownloader(1).unblock().assertReleased().assertStartCount(1); - task.assertRemoved(); - runner.assertCreatedDownloaderCount(2); - downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); - assertThat(downloadManager.getCurrentDownloads()).isEmpty(); - } - - @Test - public void downloadFails_retriesThenTaskFails() throws Throwable { - DownloadRunner runner = new DownloadRunner(uri1); - runner.postDownloadRequest(); - FakeDownloader downloader = runner.getDownloader(0); + public void download_retryUntilMinRetryCount_withoutProgress_thenFails() throws Throwable { + postDownloadRequest(ID1); + FakeDownloader downloader = getDownloaderAt(0); + downloader.assertId(ID1); for (int i = 0; i <= MIN_RETRY_COUNT; i++) { - downloader.assertStarted(MAX_RETRY_DELAY).fail(); + downloader.assertDownloadStarted(); + downloader.fail(); } + assertFailed(ID1); - downloader.assertReleased().assertStartCount(MIN_RETRY_COUNT + 1); - runner.getTask().assertFailed(); - downloadManagerListener.blockUntilTasksComplete(); - assertThat(downloadManager.getCurrentDownloads()).isEmpty(); + downloadManagerListener.blockUntilIdle(); + assertDownloaderCount(1); + assertDownloadIndexSize(1); + assertCurrentDownloadCount(0); } @Test - public void downloadFails_retries() throws Throwable { - DownloadRunner runner = new DownloadRunner(uri1); - runner.postDownloadRequest(); - FakeDownloader downloader = runner.getDownloader(0); + public void download_retryUntilMinRetryCountMinusOne_thenSucceeds() throws Throwable { + postDownloadRequest(ID1); + FakeDownloader downloader = getDownloaderAt(0); + downloader.assertId(ID1); for (int i = 0; i < MIN_RETRY_COUNT; i++) { - downloader.assertStarted(MAX_RETRY_DELAY).fail(); + downloader.assertDownloadStarted(); + downloader.fail(); } - downloader.assertStarted(MAX_RETRY_DELAY).unblock(); + downloader.assertDownloadStarted(); + downloader.finish(); + assertCompleted(ID1); - downloader.assertReleased().assertStartCount(MIN_RETRY_COUNT + 1); - runner.getTask().assertCompleted(); - downloadManagerListener.blockUntilTasksComplete(); - assertThat(downloadManager.getCurrentDownloads()).isEmpty(); + downloadManagerListener.blockUntilIdle(); + assertDownloaderCount(1); + assertDownloadIndexSize(1); + assertCurrentDownloadCount(0); } @Test - public void downloadProgressOnRetry_retryCountResets() throws Throwable { - DownloadRunner runner = new DownloadRunner(uri1); - runner.postDownloadRequest(); - FakeDownloader downloader = runner.getDownloader(0); + public void download_retryMakesProgress_resetsRetryCount() throws Throwable { + postDownloadRequest(ID1); - int tooManyRetries = MIN_RETRY_COUNT + 10; - for (int i = 0; i < tooManyRetries; i++) { - downloader.incrementBytesDownloaded(); - downloader.assertStarted(MAX_RETRY_DELAY).fail(); + FakeDownloader downloader = getDownloaderAt(0); + downloader.assertId(ID1); + for (int i = 0; i <= MIN_RETRY_COUNT; i++) { + downloader.assertDownloadStarted(); + downloader.incrementBytesDownloaded(); // Make some progress. + downloader.fail(); } - downloader.assertStarted(MAX_RETRY_DELAY).unblock(); + // Since previous attempts all made progress the current error count should be 1. Therefore we + // should be able to fail (MIN_RETRY_COUNT - 1) more times and then still complete the download + // successfully. + for (int i = 0; i < MIN_RETRY_COUNT - 1; i++) { + downloader.assertDownloadStarted(); + downloader.fail(); + } + downloader.assertDownloadStarted(); + downloader.finish(); + assertCompleted(ID1); - downloader.assertReleased().assertStartCount(tooManyRetries + 1); - runner.getTask().assertCompleted(); - downloadManagerListener.blockUntilTasksComplete(); + downloadManagerListener.blockUntilIdle(); + assertDownloaderCount(1); + assertDownloadIndexSize(1); + assertCurrentDownloadCount(0); } @Test - public void removeCancelsDownload() throws Throwable { - DownloadRunner runner = new DownloadRunner(uri1); - FakeDownloader downloader1 = runner.getDownloader(0); + public void download_retryMakesProgress_resetsRetryCount_thenFails() throws Throwable { + postDownloadRequest(ID1); - runner.postDownloadRequest(); - downloader1.assertStarted(); - runner.postRemoveRequest(); + FakeDownloader downloader = getDownloaderAt(0); + downloader.assertId(ID1); + for (int i = 0; i <= MIN_RETRY_COUNT; i++) { + downloader.assertDownloadStarted(); + downloader.incrementBytesDownloaded(); // Make some progress. + downloader.fail(); + } + // Since previous attempts all made progress the current error count should be 1. Therefore we + // should fail after MIN_RETRY_COUNT more attempts without making any progress. + for (int i = 0; i < MIN_RETRY_COUNT; i++) { + downloader.assertDownloadStarted(); + downloader.fail(); + } + assertFailed(ID1); - downloader1.assertCanceled().assertStartCount(1); - runner.getDownloader(1).unblock().assertNotCanceled(); - downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); + downloadManagerListener.blockUntilIdle(); + assertDownloaderCount(1); + assertDownloadIndexSize(1); + assertCurrentDownloadCount(0); } @Test - public void downloadNotCancelRemove() throws Throwable { - DownloadRunner runner = new DownloadRunner(uri1); - FakeDownloader downloader1 = runner.getDownloader(1); + public void download_WhenRemovalInProgress_doesNotCancelRemoval() throws Throwable { + postDownloadRequest(ID1); + postRemoveRequest(ID1); + assertRemoving(ID1); - runner.postDownloadRequest().postRemoveRequest(); - downloader1.assertStarted(); - runner.postDownloadRequest(); + FakeDownloader downloader1 = getDownloaderAt(1); + downloader1.assertId(ID1); + downloader1.assertRemoveStarted(); - downloader1.unblock().assertNotCanceled(); - runner.getDownloader(2).unblock().assertNotCanceled(); - downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); + postDownloadRequest(ID1); + // The removal should still complete. + downloader1.finish(); + + // The download should then start and complete. + FakeDownloader downloader2 = getDownloaderAt(2); + downloader2.assertId(ID1); + downloader2.assertDownloadStarted(); + downloader2.finish(); + assertCompleted(ID1); + + downloadManagerListener.blockUntilIdleAndThrowAnyFailure(); + assertDownloaderCount(3); + assertDownloadIndexSize(1); + assertCurrentDownloadCount(0); } @Test - public void secondSameRemoveRequestIgnored() throws Throwable { - DownloadRunner runner = new DownloadRunner(uri1); - FakeDownloader downloader1 = runner.getDownloader(1); + public void remove_WhenRemovalInProgress_doesNothing() throws Throwable { + postDownloadRequest(ID1); + postRemoveRequest(ID1); + assertRemoving(ID1); - runner.postDownloadRequest().postRemoveRequest(); - downloader1.assertStarted(); - runner.postRemoveRequest(); + FakeDownloader downloader1 = getDownloaderAt(1); + downloader1.assertId(ID1); + downloader1.assertRemoveStarted(); - downloader1.unblock().assertNotCanceled(); - runner.getTask().assertRemoved(); - runner.assertCreatedDownloaderCount(2); - downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); + postRemoveRequest(ID1); + // The existing removal should still complete. + downloader1.finish(); + assertRemoved(ID1); + + downloadManagerListener.blockUntilIdleAndThrowAnyFailure(); + assertDownloaderCount(2); + assertDownloadIndexSize(0); + assertCurrentDownloadCount(0); } @Test public void removeAllDownloads_removesAllDownloads() throws Throwable { - // Finish one download and keep one running. - DownloadRunner runner1 = new DownloadRunner(uri1); - DownloadRunner runner2 = new DownloadRunner(uri2); - runner1.postDownloadRequest(); - runner1.getDownloader(0).unblock(); - downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); - runner2.postDownloadRequest(); + // Finish one download. + postDownloadRequest(ID1); + FakeDownloader downloader0 = getDownloaderAt(0); + downloader0.assertId(ID1); + downloader0.assertDownloadStarted(); + downloader0.finish(); + assertCompleted(ID1); - runner1.postRemoveAllRequest(); - runner1.getDownloader(1).unblock(); - runner2.getDownloader(1).unblock(); - downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); + // Start a second download. + postDownloadRequest(ID2); + FakeDownloader downloader1 = getDownloaderAt(1); + downloader1.assertId(ID2); + downloader1.assertDownloadStarted(); - runner1.getTask().assertRemoved(); - runner2.getTask().assertRemoved(); - assertThat(downloadManager.getCurrentDownloads()).isEmpty(); - assertThat(downloadIndex.getDownloads().getCount()).isEqualTo(0); + postRemoveAllRequest(); + // Both downloads should be removed. + FakeDownloader downloader2 = getDownloaderAt(2); + FakeDownloader downloader3 = getDownloaderAt(3); + downloader2.assertId(ID1); + downloader3.assertId(ID2); + downloader2.assertRemoveStarted(); + downloader3.assertRemoveStarted(); + downloader2.finish(); + downloader3.finish(); + assertRemoved(ID1); + assertRemoved(ID2); + + downloadManagerListener.blockUntilIdleAndThrowAnyFailure(); + assertDownloaderCount(4); + assertDownloadIndexSize(0); + assertCurrentDownloadCount(0); } @Test - public void differentDownloadRequestsMerged() throws Throwable { - DownloadRunner runner = new DownloadRunner(uri1); - FakeDownloader downloader1 = runner.getDownloader(0); - + public void downloads_withSameIdsAndDifferentStreamKeys_areMerged() throws Throwable { StreamKey streamKey1 = new StreamKey(/* groupIndex= */ 0, /* trackIndex= */ 0); + postDownloadRequest(ID1, streamKey1); + FakeDownloader downloader0 = getDownloaderAt(0); + downloader0.assertId(ID1); + downloader0.assertDownloadStarted(); + StreamKey streamKey2 = new StreamKey(/* groupIndex= */ 1, /* trackIndex= */ 1); + postDownloadRequest(ID1, streamKey2); + // The request for streamKey2 will cause the downloader for streamKey1 to be canceled and + // replaced with a new downloader for both keys. + downloader0.assertCanceled(); + FakeDownloader downloader1 = getDownloaderAt(1); + downloader1.assertId(ID1); + downloader1.assertStreamKeys(streamKey1, streamKey2); + downloader1.assertDownloadStarted(); + downloader1.finish(); + assertCompleted(ID1); - runner.postDownloadRequest(streamKey1); - downloader1.assertStarted(); - runner.postDownloadRequest(streamKey2); - - downloader1.assertCanceled(); - - FakeDownloader downloader2 = runner.getDownloader(1); - downloader2.assertStarted(); - assertThat(downloader2.request.streamKeys).containsExactly(streamKey1, streamKey2); - downloader2.unblock(); - - runner.getTask().assertCompleted(); - runner.assertCreatedDownloaderCount(2); - downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); + downloadManagerListener.blockUntilIdleAndThrowAnyFailure(); + assertDownloaderCount(2); + assertDownloadIndexSize(1); + assertCurrentDownloadCount(0); } @Test - public void requestsForDifferentContent_executedInParallel() throws Throwable { - DownloadRunner runner1 = new DownloadRunner(uri1).postDownloadRequest(); - DownloadRunner runner2 = new DownloadRunner(uri2).postDownloadRequest(); - FakeDownloader downloader1 = runner1.getDownloader(0); - FakeDownloader downloader2 = runner2.getDownloader(0); + public void downloads_withDifferentIds_executeInParallel() throws Throwable { + postDownloadRequest(ID1); + postDownloadRequest(ID2); - downloader1.assertStarted(); - downloader2.assertStarted(); - downloader1.unblock(); - downloader2.unblock(); + FakeDownloader downloader0 = getDownloaderAt(0); + FakeDownloader downloader1 = getDownloaderAt(1); + downloader0.assertId(ID1); + downloader1.assertId(ID2); + downloader0.assertDownloadStarted(); + downloader1.assertDownloadStarted(); + downloader0.finish(); + downloader1.finish(); + assertCompleted(ID1); + assertCompleted(ID2); - runner1.getTask().assertCompleted(); - runner2.getTask().assertCompleted(); - downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); + downloadManagerListener.blockUntilIdleAndThrowAnyFailure(); + assertDownloaderCount(2); + assertDownloadIndexSize(2); + assertCurrentDownloadCount(0); } @Test - public void requestsForDifferentContent_ifMaxDownloadIs1_executedSequentially() throws Throwable { - setUpDownloadManager(1); - DownloadRunner runner1 = new DownloadRunner(uri1).postDownloadRequest(); - DownloadRunner runner2 = new DownloadRunner(uri2).postDownloadRequest(); - FakeDownloader downloader1 = runner1.getDownloader(0); - FakeDownloader downloader2 = runner2.getDownloader(0); + public void downloads_withDifferentIds_maxDownloadsIsOne_executedSequentially() throws Throwable { + setupDownloadManager(/* maxParallelDownloads= */ 1); + postDownloadRequest(ID1); + postDownloadRequest(ID2); + FakeDownloader downloader0 = getDownloaderAt(0); + downloader0.assertId(ID1); + downloader0.assertDownloadStarted(); - downloader1.assertStarted(); - downloader2.assertDoesNotStart(); - runner2.getTask().assertQueued(); - downloader1.unblock(); - downloader2.assertStarted(); - downloader2.unblock(); + // The second download should be queued and the first one should be able to complete. + assertNoDownloaderAt(1); + assertQueued(ID2); + downloader0.finish(); + assertCompleted(ID1); - runner1.getTask().assertCompleted(); - runner2.getTask().assertCompleted(); - downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); + // The second download can start once the first one has completed. + FakeDownloader downloader1 = getDownloaderAt(1); + downloader1.assertId(ID2); + downloader1.assertDownloadStarted(); + downloader1.finish(); + assertCompleted(ID2); + + downloadManagerListener.blockUntilIdleAndThrowAnyFailure(); + assertDownloaderCount(2); + assertDownloadIndexSize(2); + assertCurrentDownloadCount(0); } @Test - public void removeRequestForDifferentContent_ifMaxDownloadIs1_executedInParallel() + public void downloadAndRemove_withDifferentIds_maxDownloadsIsOne_executeInParallel() throws Throwable { - setUpDownloadManager(1); - DownloadRunner runner1 = new DownloadRunner(uri1).postDownloadRequest(); - DownloadRunner runner2 = new DownloadRunner(uri2).postDownloadRequest().postRemoveRequest(); - FakeDownloader downloader1 = runner1.getDownloader(0); - FakeDownloader downloader2 = runner2.getDownloader(0); + setupDownloadManager(/* maxParallelDownloads= */ 1); - downloader1.assertStarted(); - downloader2.assertStarted(); - downloader1.unblock(); - downloader2.unblock(); + // Complete a download so that we can remove it. + postDownloadRequest(ID1); + FakeDownloader downloader0 = getDownloaderAt(0); + downloader0.assertId(ID1); + downloader0.assertDownloadStarted(); + downloader0.finish(); - runner1.getTask().assertCompleted(); - runner2.getTask().assertRemoved(); - downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); + // Request removal of the first download, and downloading of a second download. + postRemoveRequest(ID1); + postDownloadRequest(ID2); + + // The removal and download should proceed in parallel. + FakeDownloader downloader1 = getDownloaderAt(1); + FakeDownloader downloader2 = getDownloaderAt(2); + downloader1.assertId(ID1); + downloader2.assertId(ID2); + downloader1.assertRemoveStarted(); + downloader2.assertDownloadStarted(); + downloader1.finish(); + downloader2.finish(); + assertRemoved(ID1); + assertCompleted(ID2); + + downloadManagerListener.blockUntilIdleAndThrowAnyFailure(); + assertDownloaderCount(3); + assertDownloadIndexSize(1); + assertCurrentDownloadCount(0); } @Test - public void downloadRequestFollowingRemove_ifMaxDownloadIs1_isNotStarted() throws Throwable { - setUpDownloadManager(1); - DownloadRunner runner1 = new DownloadRunner(uri1).postDownloadRequest(); - DownloadRunner runner2 = new DownloadRunner(uri2).postDownloadRequest().postRemoveRequest(); - runner2.postDownloadRequest(); - FakeDownloader downloader1 = runner1.getDownloader(0); - FakeDownloader downloader2 = runner2.getDownloader(0); - FakeDownloader downloader3 = runner2.getDownloader(1); + public void downloadAfterRemove_maxDownloadIsOne_isNotStarted() throws Throwable { + setupDownloadManager(/* maxParallelDownloads= */ 1); + postDownloadRequest(ID1); + postDownloadRequest(ID2); + postRemoveRequest(ID2); + postDownloadRequest(ID2); - downloader1.assertStarted(); - downloader2.assertStarted(); - downloader2.unblock(); - downloader3.assertDoesNotStart(); - downloader1.unblock(); - downloader3.assertStarted(); - downloader3.unblock(); + FakeDownloader downloader0 = getDownloaderAt(0); + downloader0.assertId(ID1); + downloader0.assertDownloadStarted(); - runner1.getTask().assertCompleted(); - runner2.getTask().assertCompleted(); - downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); + // The second download shouldn't have been started, so the second downloader is for removal. + FakeDownloader downloader1 = getDownloaderAt(1); + downloader1.assertId(ID2); + downloader1.assertRemoveStarted(); + downloader1.finish(); + // A downloader to re-download the second download should not be started. + assertNoDownloaderAt(2); + // The first download should be able to complete. + downloader0.finish(); + assertCompleted(ID1); + + // Now the first download has completed, the second download should start. + FakeDownloader downloader2 = getDownloaderAt(2); + downloader2.assertId(ID2); + downloader2.assertDownloadStarted(); + downloader2.finish(); + assertCompleted(ID2); + + downloadManagerListener.blockUntilIdleAndThrowAnyFailure(); + assertDownloaderCount(3); + assertDownloadIndexSize(2); + assertCurrentDownloadCount(0); } @Test - public void getCurrentDownloads_returnsCurrentDownloads() { - TaskWrapper task1 = new DownloadRunner(uri1).postDownloadRequest().getTask(); - TaskWrapper task2 = new DownloadRunner(uri2).postDownloadRequest().getTask(); - TaskWrapper task3 = - new DownloadRunner(uri3).postDownloadRequest().postRemoveRequest().getTask(); + public void pauseAndResume_pausesAndResumesDownload() throws Throwable { + postDownloadRequest(ID1); + FakeDownloader downloader0 = getDownloaderAt(0); + downloader0.assertId(ID1); + downloader0.assertDownloadStarted(); - task3.assertRemoving(); - List downloads = downloadManager.getCurrentDownloads(); + postPauseDownloads(); + downloader0.assertCanceled(); + assertQueued(ID1); + postResumeDownloads(); + FakeDownloader downloader1 = getDownloaderAt(1); + downloader1.assertId(ID1); + downloader1.assertDownloadStarted(); + downloader1.finish(); + assertCompleted(ID1); + + downloadManagerListener.blockUntilIdleAndThrowAnyFailure(); + assertDownloaderCount(2); + assertDownloadIndexSize(1); + assertCurrentDownloadCount(0); + } + + @Test + public void pause_doesNotCancelRemove() throws Throwable { + postDownloadRequest(ID1); + postRemoveRequest(ID1); + FakeDownloader downloader = getDownloaderAt(1); + downloader.assertId(ID1); + downloader.assertRemoveStarted(); + + postPauseDownloads(); + downloader.finish(); + assertRemoved(ID1); + + downloadManagerListener.blockUntilIdleAndThrowAnyFailure(); + assertDownloaderCount(2); + assertDownloadIndexSize(0); + assertCurrentDownloadCount(0); + } + + @Test + public void setAndClearStopReason_stopsAndRestartsDownload() throws Throwable { + postDownloadRequest(ID1); + FakeDownloader downloader0 = getDownloaderAt(0); + downloader0.assertId(ID1); + downloader0.assertDownloadStarted(); + + postSetStopReason(ID1, APP_STOP_REASON); + downloader0.assertCanceled(); + assertStopped(ID1); + + postSetStopReason(ID1, Download.STOP_REASON_NONE); + FakeDownloader downloader1 = getDownloaderAt(1); + downloader1.assertId(ID1); + downloader1.assertDownloadStarted(); + downloader1.finish(); + + downloadManagerListener.blockUntilIdleAndThrowAnyFailure(); + assertDownloaderCount(2); + assertDownloadIndexSize(1); + assertCurrentDownloadCount(0); + } + + @Test + public void setStopReason_doesNotStopOtherDownload() throws Throwable { + postDownloadRequest(ID1); + postDownloadRequest(ID2); + + FakeDownloader downloader0 = getDownloaderAt(0); + FakeDownloader downloader1 = getDownloaderAt(1); + downloader0.assertId(ID1); + downloader1.assertId(ID2); + downloader0.assertDownloadStarted(); + downloader1.assertDownloadStarted(); + + postSetStopReason(ID1, APP_STOP_REASON); + downloader0.assertCanceled(); + assertStopped(ID1); + + // The second download should still complete. + downloader1.finish(); + assertCompleted(ID2); + + downloadManagerListener.blockUntilIdleAndThrowAnyFailure(); + assertDownloaderCount(2); + assertDownloadIndexSize(2); + assertCurrentDownloadCount(1); + } + + @Test + public void remove_removesStoppedDownload() throws Throwable { + postDownloadRequest(ID1); + FakeDownloader downloader0 = getDownloaderAt(0); + downloader0.assertId(ID1); + downloader0.assertDownloadStarted(); + + postSetStopReason(ID1, APP_STOP_REASON); + downloader0.assertCanceled(); + assertStopped(ID1); + + postRemoveRequest(ID1); + FakeDownloader downloader1 = getDownloaderAt(1); + downloader1.assertId(ID1); + downloader1.assertRemoveStarted(); + downloader1.finish(); + assertRemoved(ID1); + + downloadManagerListener.blockUntilIdleAndThrowAnyFailure(); + assertDownloaderCount(2); + assertDownloadIndexSize(0); + assertCurrentDownloadCount(0); + } + + @Test + public void getCurrentDownloads_returnsCurrentDownloads() throws Throwable { + setupDownloadManager(/* maxParallelDownloads= */ 1); + postDownloadRequest(ID1); + postDownloadRequest(ID2); + postDownloadRequest(ID3); + postRemoveRequest(ID3); + + assertRemoving(ID3); // Blocks until the downloads will be visible. + + List downloads = postGetCurrentDownloads(); assertThat(downloads).hasSize(3); - String[] taskIds = {task1.taskId, task2.taskId, task3.taskId}; - String[] downloadIds = { - downloads.get(0).request.id, downloads.get(1).request.id, downloads.get(2).request.id - }; - assertThat(downloadIds).isEqualTo(taskIds); - } - - @Test - public void pauseAndResume() throws Throwable { - DownloadRunner runner1 = new DownloadRunner(uri1); - DownloadRunner runner2 = new DownloadRunner(uri2); - DownloadRunner runner3 = new DownloadRunner(uri3); - - runner1.postDownloadRequest().getTask().assertDownloading(); - runner2.postDownloadRequest().postRemoveRequest().getTask().assertRemoving(); - runner2.postDownloadRequest(); - - runOnMainThread(() -> downloadManager.pauseDownloads()); - - runner1.getTask().assertQueued(); - - // remove requests aren't stopped. - runner2.getDownloader(1).unblock().assertReleased(); - runner2.getTask().assertQueued(); - // Although remove2 is finished, download2 doesn't start. - runner2.getDownloader(2).assertDoesNotStart(); - - // When a new remove request is added, it cancels stopped download requests with the same media. - runner1.postRemoveRequest(); - runner1.getDownloader(1).assertStarted().unblock(); - runner1.getTask().assertRemoved(); - - // New download requests can be added but they don't start. - runner3.postDownloadRequest().getDownloader(0).assertDoesNotStart(); - - runOnMainThread(() -> downloadManager.resumeDownloads()); - - runner2.getDownloader(2).assertStarted().unblock(); - runner3.getDownloader(0).assertStarted().unblock(); - - downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); - } - - @Test - public void setAndClearSingleDownloadStopReason() throws Throwable { - DownloadRunner runner = new DownloadRunner(uri1).postDownloadRequest(); - TaskWrapper task = runner.getTask(); - - task.assertDownloading(); - - runOnMainThread(() -> downloadManager.setStopReason(task.taskId, APP_STOP_REASON)); - - task.assertStopped(); - - runOnMainThread(() -> downloadManager.setStopReason(task.taskId, Download.STOP_REASON_NONE)); - - runner.getDownloader(1).assertStarted().unblock(); - - downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); - } - - @Test - public void setSingleDownloadStopReasonThenRemove_removesDownload() throws Throwable { - DownloadRunner runner = new DownloadRunner(uri1).postDownloadRequest(); - TaskWrapper task = runner.getTask(); - - task.assertDownloading(); - - runOnMainThread(() -> downloadManager.setStopReason(task.taskId, APP_STOP_REASON)); - - task.assertStopped(); - - runner.postRemoveRequest(); - runner.getDownloader(1).assertStarted().unblock(); - task.assertRemoved(); - - downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); - } - - @Test - public void setSingleDownloadStopReason_doesNotAffectOtherDownloads() throws Throwable { - DownloadRunner runner1 = new DownloadRunner(uri1); - DownloadRunner runner2 = new DownloadRunner(uri2); - DownloadRunner runner3 = new DownloadRunner(uri3); - - runner1.postDownloadRequest().getTask().assertDownloading(); - runner2.postDownloadRequest().postRemoveRequest().getTask().assertRemoving(); - - runOnMainThread(() -> downloadManager.setStopReason(runner1.getTask().taskId, APP_STOP_REASON)); - - runner1.getTask().assertStopped(); - - // Other downloads aren't affected. - runner2.getDownloader(1).unblock().assertReleased(); - - // New download requests can be added and they start. - runner3.postDownloadRequest().getDownloader(0).assertStarted().unblock(); - - downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); + Download download0 = downloads.get(0); + assertThat(download0.request.id).isEqualTo(ID1); + assertThat(download0.state).isEqualTo(Download.STATE_DOWNLOADING); + Download download1 = downloads.get(1); + assertThat(download1.request.id).isEqualTo(ID2); + assertThat(download1.state).isEqualTo(Download.STATE_QUEUED); + Download download2 = downloads.get(2); + assertThat(download2.request.id).isEqualTo(ID3); + assertThat(download2.state).isEqualTo(Download.STATE_REMOVING); } @Test public void mergeRequest_removing_becomesRestarting() { - DownloadRequest downloadRequest = createDownloadRequest(); + DownloadRequest downloadRequest = createDownloadRequest(ID1); DownloadBuilder downloadBuilder = new DownloadBuilder(downloadRequest).setState(Download.STATE_REMOVING); Download download = downloadBuilder.build(); @@ -496,7 +602,7 @@ public class DownloadManagerTest { @Test public void mergeRequest_failed_becomesQueued() { - DownloadRequest downloadRequest = createDownloadRequest(); + DownloadRequest downloadRequest = createDownloadRequest(ID1); DownloadBuilder downloadBuilder = new DownloadBuilder(downloadRequest) .setState(Download.STATE_FAILED) @@ -517,7 +623,7 @@ public class DownloadManagerTest { @Test public void mergeRequest_stopped_staysStopped() { - DownloadRequest downloadRequest = createDownloadRequest(); + DownloadRequest downloadRequest = createDownloadRequest(ID1); DownloadBuilder downloadBuilder = new DownloadBuilder(downloadRequest) .setState(Download.STATE_STOPPED) @@ -532,7 +638,7 @@ public class DownloadManagerTest { @Test public void mergeRequest_completedWithStopReason_becomesStopped() { - DownloadRequest downloadRequest = createDownloadRequest(); + DownloadRequest downloadRequest = createDownloadRequest(ID1); DownloadBuilder downloadBuilder = new DownloadBuilder(downloadRequest) .setState(Download.STATE_COMPLETED) @@ -547,7 +653,7 @@ public class DownloadManagerTest { assertEqualIgnoringUpdateTime(mergedDownload, expectedDownload); } - private void setUpDownloadManager(final int maxParallelDownloads) throws Exception { + private void setupDownloadManager(int maxParallelDownloads) throws Exception { if (downloadManager != null) { releaseDownloadManager(); } @@ -556,15 +662,16 @@ public class DownloadManagerTest { () -> { downloadManager = new DownloadManager( - ApplicationProvider.getApplicationContext(), downloadIndex, downloaderFactory); + ApplicationProvider.getApplicationContext(), + new DefaultDownloadIndex(TestUtil.getInMemoryDatabaseProvider()), + new FakeDownloaderFactory()); downloadManager.setMaxParallelDownloads(maxParallelDownloads); downloadManager.setMinRetryCount(MIN_RETRY_COUNT); downloadManager.setRequirements(new Requirements(0)); downloadManager.resumeDownloads(); - downloadManagerListener = - new TestDownloadManagerListener(downloadManager, dummyMainThread); + downloadManagerListener = new TestDownloadManagerListener(downloadManager); }); - downloadManagerListener.waitUntilInitialized(); + downloadManagerListener.blockUntilInitialized(); } catch (Throwable throwable) { throw new Exception(throwable); } @@ -578,10 +685,103 @@ public class DownloadManagerTest { } } + private void postRemoveRequest(String id) { + runOnMainThread(() -> downloadManager.removeDownload(id)); + } + + private void postRemoveAllRequest() { + runOnMainThread(() -> downloadManager.removeAllDownloads()); + } + + private void postPauseDownloads() { + runOnMainThread(() -> downloadManager.pauseDownloads()); + } + + private void postResumeDownloads() { + runOnMainThread(() -> downloadManager.resumeDownloads()); + } + + private void postSetStopReason(String id, int reason) { + runOnMainThread(() -> downloadManager.setStopReason(id, reason)); + } + + private void postDownloadRequest(String id, StreamKey... keys) { + runOnMainThread(() -> downloadManager.addDownload(createDownloadRequest(id, keys))); + } + + private List postGetCurrentDownloads() { + AtomicReference> currentDownloadsReference = new AtomicReference<>(); + runOnMainThread( + () -> { + currentDownloadsReference.set(downloadManager.getCurrentDownloads()); + }); + return currentDownloadsReference.get(); + } + + private DownloadIndex postGetDownloadIndex() { + AtomicReference downloadIndexReference = new AtomicReference<>(); + runOnMainThread( + () -> { + downloadIndexReference.set(downloadManager.getDownloadIndex()); + }); + return downloadIndexReference.get(); + } + private void runOnMainThread(TestRunnable r) { dummyMainThread.runTestOnMainThread(r); } + private FakeDownloader getDownloaderAt(int index) throws InterruptedException { + return Assertions.checkNotNull(getDownloaderInternal(index, TIMEOUT_MS)); + } + + private void assertNoDownloaderAt(int index) throws InterruptedException { + // We use a timeout shorter than TIMEOUT_MS because timing out is expected in this case. + assertThat(getDownloaderInternal(index, /* timeoutMs= */ 1_000)).isNull(); + } + + private void assertDownloading(String id) { + downloadManagerListener.assertState(id, Download.STATE_DOWNLOADING); + } + + private void assertCompleted(String id) { + downloadManagerListener.assertState(id, Download.STATE_COMPLETED); + } + + private void assertRemoving(String id) { + downloadManagerListener.assertState(id, Download.STATE_REMOVING); + } + + private void assertFailed(String id) { + downloadManagerListener.assertState(id, Download.STATE_FAILED); + } + + private void assertQueued(String id) { + downloadManagerListener.assertState(id, Download.STATE_QUEUED); + } + + private void assertStopped(String id) { + downloadManagerListener.assertState(id, Download.STATE_STOPPED); + } + + private void assertRemoved(String id) { + downloadManagerListener.assertRemoved(id); + } + + private void assertDownloaderCount(int expectedCount) { + synchronized (downloaders) { + assertThat(downloaders).hasSize(expectedCount); + } + } + + private void assertCurrentDownloadCount(int expectedCount) { + assertThat(postGetCurrentDownloads()).hasSize(expectedCount); + } + + private void assertDownloadIndexSize(int expectedSize) throws IOException { + assertThat(postGetDownloadIndex().getDownloads().getCount()).isEqualTo(expectedSize); + } + private static void assertEqualIgnoringUpdateTime(Download download, Download that) { assertThat(download.request).isEqualTo(that.request); assertThat(download.state).isEqualTo(that.state); @@ -593,277 +793,163 @@ public class DownloadManagerTest { assertThat(download.getBytesDownloaded()).isEqualTo(that.getBytesDownloaded()); } - private static DownloadRequest createDownloadRequest() { + private static DownloadRequest createDownloadRequest(String id, StreamKey... keys) { return new DownloadRequest( - "id", + id, DownloadRequest.TYPE_DASH, - Uri.parse("https://www.test.com/download"), - Collections.emptyList(), + Uri.parse("http://abc.com/ " + id), + Arrays.asList(keys), /* customCacheKey= */ null, /* data= */ null); } - private final class DownloadRunner { + // Internal methods. - private final Uri uri; - private final String id; - private final ArrayList downloaders; - private int createdDownloaderCount = 0; - private FakeDownloader downloader; - private final TaskWrapper taskWrapper; - - private DownloadRunner(Uri uri) { - this.uri = uri; - id = uri.toString(); - downloaders = new ArrayList<>(); - downloader = addDownloader(); - downloaderFactory.registerDownloadRunner(this); - taskWrapper = new TaskWrapper(id); - } - - private DownloadRunner postRemoveRequest() { - runOnMainThread(() -> downloadManager.removeDownload(id)); - return this; - } - - private DownloadRunner postRemoveAllRequest() { - runOnMainThread(() -> downloadManager.removeAllDownloads()); - return this; - } - - private DownloadRunner postDownloadRequest(StreamKey... keys) { - DownloadRequest downloadRequest = - new DownloadRequest( - id, - DownloadRequest.TYPE_PROGRESSIVE, - uri, - Arrays.asList(keys), - /* customCacheKey= */ null, - /* data= */ null); - runOnMainThread(() -> downloadManager.addDownload(downloadRequest)); - return this; - } - - private synchronized FakeDownloader addDownloader() { - FakeDownloader fakeDownloader = new FakeDownloader(); - downloaders.add(fakeDownloader); - return fakeDownloader; - } - - private synchronized FakeDownloader getDownloader(int index) { - while (downloaders.size() <= index) { - addDownloader(); + @Nullable + private FakeDownloader getDownloaderInternal(int index, long timeoutMs) + throws InterruptedException { + long nowMs = System.currentTimeMillis(); + long endMs = nowMs + timeoutMs; + synchronized (downloaders) { + while (downloaders.size() <= index && nowMs < endMs) { + downloaders.wait(endMs - nowMs); + nowMs = System.currentTimeMillis(); } - return downloaders.get(index); - } - - private synchronized Downloader createDownloader(DownloadRequest request) { - downloader = getDownloader(createdDownloaderCount++); - downloader.request = request; - return downloader; - } - - private TaskWrapper getTask() { - return taskWrapper; - } - - private void assertCreatedDownloaderCount(int count) { - assertThat(createdDownloaderCount).isEqualTo(count); + return downloaders.size() <= index ? null : downloaders.get(index); } } - private final class TaskWrapper { - private final String taskId; - - private TaskWrapper(String taskId) { - this.taskId = taskId; - } - - private TaskWrapper assertDownloading() { - return assertState(Download.STATE_DOWNLOADING); - } - - private TaskWrapper assertCompleted() { - return assertState(Download.STATE_COMPLETED); - } - - private TaskWrapper assertRemoving() { - return assertState(Download.STATE_REMOVING); - } - - private TaskWrapper assertFailed() { - return assertState(Download.STATE_FAILED); - } - - private TaskWrapper assertQueued() { - return assertState(Download.STATE_QUEUED); - } - - private TaskWrapper assertStopped() { - return assertState(Download.STATE_STOPPED); - } - - private TaskWrapper assertState(@State int expectedState) { - downloadManagerListener.assertState(taskId, expectedState, ASSERT_TRUE_TIMEOUT); - return this; - } - - private TaskWrapper assertRemoved() { - downloadManagerListener.assertRemoved(taskId, ASSERT_TRUE_TIMEOUT); - return this; - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - return taskId.equals(((TaskWrapper) o).taskId); - } - - @Override - public int hashCode() { - return taskId.hashCode(); - } - } - - private static final class FakeDownloaderFactory implements DownloaderFactory { - - private final HashMap downloaders; - - public FakeDownloaderFactory() { - downloaders = new HashMap<>(); - } - - public void registerDownloadRunner(DownloadRunner downloadRunner) { - assertThat(downloaders.put(downloadRunner.uri, downloadRunner)).isNull(); - } + private final class FakeDownloaderFactory implements DownloaderFactory { @Override public Downloader createDownloader(DownloadRequest request) { - return downloaders.get(request.uri).createDownloader(request); + FakeDownloader fakeDownloader = new FakeDownloader(request); + synchronized (downloaders) { + downloaders.add(fakeDownloader); + downloaders.notifyAll(); + } + return fakeDownloader; } } private static final class FakeDownloader implements Downloader { - private final com.google.android.exoplayer2.util.ConditionVariable blocker; + private final DownloadRequest request; + private final ConditionVariable downloadStarted; + private final ConditionVariable removeStarted; + private final ConditionVariable finished; + private final ConditionVariable blocker; + private final AtomicInteger startCount; + private final AtomicInteger bytesDownloaded; - private DownloadRequest request; - private CountDownLatch started; - private volatile boolean interrupted; - private volatile boolean cancelled; + private volatile boolean canceled; private volatile boolean enableDownloadIOException; - private volatile int startCount; - private volatile int bytesDownloaded; - private FakeDownloader() { - this.started = new CountDownLatch(1); - this.blocker = new com.google.android.exoplayer2.util.ConditionVariable(); - } - - @SuppressWarnings({"NonAtomicOperationOnVolatileField", "NonAtomicVolatileUpdate"}) - @Override - public void download(ProgressListener listener) throws InterruptedException, IOException { - // It's ok to update this directly as no other thread will update it. - startCount++; - started.countDown(); - block(); - if (bytesDownloaded > 0) { - listener.onProgress(C.LENGTH_UNSET, bytesDownloaded, C.PERCENTAGE_UNSET); - } - if (enableDownloadIOException) { - enableDownloadIOException = false; - throw new IOException(); - } + private FakeDownloader(DownloadRequest request) { + this.request = request; + downloadStarted = TestUtil.createRobolectricConditionVariable(); + removeStarted = TestUtil.createRobolectricConditionVariable(); + finished = TestUtil.createRobolectricConditionVariable(); + blocker = TestUtil.createRobolectricConditionVariable(); + startCount = new AtomicInteger(); + bytesDownloaded = new AtomicInteger(); } @Override public void cancel() { - cancelled = true; + canceled = true; + blocker.open(); } - @SuppressWarnings({"NonAtomicOperationOnVolatileField", "NonAtomicVolatileUpdate"}) @Override - public void remove() throws InterruptedException { - // It's ok to update this directly as no other thread will update it. - startCount++; - started.countDown(); - block(); + public void download(ProgressListener listener) throws IOException { + startCount.incrementAndGet(); + downloadStarted.open(); + try { + block(); + if (canceled) { + return; + } + int bytesDownloaded = this.bytesDownloaded.get(); + if (listener != null && bytesDownloaded > 0) { + listener.onProgress(C.LENGTH_UNSET, bytesDownloaded, C.PERCENTAGE_UNSET); + } + if (enableDownloadIOException) { + enableDownloadIOException = false; + throw new IOException(); + } + } finally { + finished.open(); + } } - private void block() throws InterruptedException { + @Override + public void remove() { + startCount.incrementAndGet(); + removeStarted.open(); + try { + block(); + } finally { + finished.open(); + } + } + + /** Finishes the {@link #download} or {@link #remove} without an error. */ + public void finish() throws InterruptedException { + blocker.open(); + blockUntilFinished(); + } + + /** Fails {@link #download} or {@link #remove} with an error. */ + public void fail() throws InterruptedException { + enableDownloadIOException = true; + blocker.open(); + blockUntilFinished(); + } + + /** Increments the number of bytes that the fake downloader has downloaded. */ + public void incrementBytesDownloaded() { + bytesDownloaded.incrementAndGet(); + } + + public void assertId(String id) { + assertThat(request.id).isEqualTo(id); + } + + public void assertStreamKeys(StreamKey... streamKeys) { + assertThat(request.streamKeys).containsExactly(streamKeys); + } + + public void assertDownloadStarted() throws InterruptedException { + assertThat(downloadStarted.block(TIMEOUT_MS)).isTrue(); + downloadStarted.close(); + } + + public void assertRemoveStarted() throws InterruptedException { + assertThat(removeStarted.block(TIMEOUT_MS)).isTrue(); + removeStarted.close(); + } + + public void assertCanceled() throws InterruptedException { + blockUntilFinished(); + assertThat(canceled).isTrue(); + } + + // Internal methods. + + private void block() { try { blocker.block(); } catch (InterruptedException e) { - interrupted = true; - throw e; + throw new IllegalStateException(e); // Never happens. } finally { blocker.close(); } } - private FakeDownloader assertStarted() throws InterruptedException { - return assertStarted(ASSERT_TRUE_TIMEOUT); - } - - private FakeDownloader assertStarted(int timeout) throws InterruptedException { - assertThat(started.await(timeout, TimeUnit.MILLISECONDS)).isTrue(); - started = new CountDownLatch(1); - return this; - } - - private FakeDownloader assertStartCount(int count) { - assertThat(startCount).isEqualTo(count); - return this; - } - - private FakeDownloader assertReleased() throws InterruptedException { - int count = 0; - while (started.await(ASSERT_TRUE_TIMEOUT, TimeUnit.MILLISECONDS)) { - if (count++ >= MAX_STARTS_BEFORE_RELEASED) { - fail(); - } - started = new CountDownLatch(1); - } - return this; - } - - private FakeDownloader assertCanceled() throws InterruptedException { - assertReleased(); - assertThat(interrupted).isTrue(); - assertThat(cancelled).isTrue(); - return this; - } - - private FakeDownloader assertNotCanceled() throws InterruptedException { - assertReleased(); - assertThat(interrupted).isFalse(); - assertThat(cancelled).isFalse(); - return this; - } - - private FakeDownloader unblock() { - blocker.open(); - return this; - } - - private FakeDownloader fail() { - enableDownloadIOException = true; - return unblock(); - } - - private void assertDoesNotStart() throws InterruptedException { - Thread.sleep(ASSERT_FALSE_TIME); - assertThat(started.getCount()).isEqualTo(1); - } - - @SuppressWarnings({"NonAtomicOperationOnVolatileField", "NonAtomicVolatileUpdate"}) - private void incrementBytesDownloaded() { - bytesDownloaded++; + private void blockUntilFinished() throws InterruptedException { + assertThat(finished.block(TIMEOUT_MS)).isTrue(); + finished.close(); } } } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadRequestTest.java b/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadRequestTest.java index a55b1e1283..c5b00b02d6 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadRequestTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadRequestTest.java @@ -45,7 +45,7 @@ public class DownloadRequestTest { } @Test - public void testMergeRequests_withDifferentIds_fails() { + public void mergeRequests_withDifferentIds_fails() { DownloadRequest request1 = new DownloadRequest( "id1", @@ -71,7 +71,7 @@ public class DownloadRequestTest { } @Test - public void testMergeRequests_withDifferentTypes_fails() { + public void mergeRequests_withDifferentTypes_fails() { DownloadRequest request1 = new DownloadRequest( "id1", @@ -97,7 +97,7 @@ public class DownloadRequestTest { } @Test - public void testMergeRequest_withSameRequest() { + public void mergeRequest_withSameRequest() { DownloadRequest request1 = createRequest(uri1, new StreamKey(0, 0, 0)); DownloadRequest mergedRequest = request1.copyWithMergedRequest(request1); @@ -105,7 +105,7 @@ public class DownloadRequestTest { } @Test - public void testMergeRequests_withEmptyStreamKeys() { + public void mergeRequests_withEmptyStreamKeys() { DownloadRequest request1 = createRequest(uri1, new StreamKey(0, 0, 0)); DownloadRequest request2 = createRequest(uri1); @@ -118,7 +118,7 @@ public class DownloadRequestTest { } @Test - public void testMergeRequests_withOverlappingStreamKeys() { + public void mergeRequests_withOverlappingStreamKeys() { StreamKey streamKey1 = new StreamKey(0, 1, 2); StreamKey streamKey2 = new StreamKey(3, 4, 5); StreamKey streamKey3 = new StreamKey(6, 7, 8); @@ -134,7 +134,7 @@ public class DownloadRequestTest { } @Test - public void testMergeRequests_withDifferentFields() { + public void mergeRequests_withDifferentFields() { byte[] data1 = new byte[] {0, 1, 2}; byte[] data2 = new byte[] {3, 4, 5}; DownloadRequest request1 = @@ -167,7 +167,7 @@ public class DownloadRequestTest { } @Test - public void testParcelable() { + public void parcelable() { ArrayList streamKeys = new ArrayList<>(); streamKeys.add(new StreamKey(1, 2, 3)); streamKeys.add(new StreamKey(4, 5, 6)); @@ -191,7 +191,7 @@ public class DownloadRequestTest { @SuppressWarnings("EqualsWithItself") @Test - public void testEquals() { + public void equals() { DownloadRequest request1 = createRequest(uri1); assertThat(request1.equals(request1)).isTrue(); 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 57ed9332c2..ae0c431bd3 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 @@ -26,6 +26,7 @@ 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.source.ClippingMediaSource.IllegalClippingException; +import com.google.android.exoplayer2.source.MaskingMediaSource.DummyTimeline; import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; import com.google.android.exoplayer2.testutil.FakeMediaPeriod; @@ -62,7 +63,7 @@ public final class ClippingMediaSourceTest { } @Test - public void testNoClipping() throws IOException { + public void noClipping() throws IOException { Timeline timeline = new SinglePeriodTimeline( TEST_PERIOD_DURATION_US, @@ -81,7 +82,7 @@ public final class ClippingMediaSourceTest { } @Test - public void testClippingUnseekableWindowThrows() throws IOException { + public void clippingUnseekableWindowThrows() throws IOException { Timeline timeline = new SinglePeriodTimeline( TEST_PERIOD_DURATION_US, @@ -101,7 +102,27 @@ public final class ClippingMediaSourceTest { } @Test - public void testClippingStart() throws IOException { + public void clippingUnseekableWindowWithUnknownDurationThrows() throws IOException { + Timeline timeline = + new SinglePeriodTimeline( + /* durationUs= */ C.TIME_UNSET, + /* isSeekable= */ false, + /* isDynamic= */ false, + /* isLive= */ false); + + // If the unseekable window isn't clipped, clipping succeeds. + getClippedTimeline(timeline, /* startUs= */ 0, TEST_PERIOD_DURATION_US); + try { + // If the unseekable window is clipped, clipping fails. + getClippedTimeline(timeline, /* startUs= */ 1, TEST_PERIOD_DURATION_US); + fail("Expected clipping to fail."); + } catch (IllegalClippingException e) { + assertThat(e.reason).isEqualTo(IllegalClippingException.REASON_NOT_SEEKABLE_TO_START); + } + } + + @Test + public void clippingStart() throws IOException { Timeline timeline = new SinglePeriodTimeline( TEST_PERIOD_DURATION_US, @@ -118,7 +139,7 @@ public final class ClippingMediaSourceTest { } @Test - public void testClippingEnd() throws IOException { + public void clippingEnd() throws IOException { Timeline timeline = new SinglePeriodTimeline( TEST_PERIOD_DURATION_US, @@ -135,13 +156,11 @@ public final class ClippingMediaSourceTest { } @Test - public void testClippingStartAndEndInitial() throws IOException { + public void clippingStartAndEndInitial() throws IOException { // Timeline that's dynamic and not seekable. A child source might report such a timeline prior // to it having loaded sufficient data to establish its duration and seekability. Such timelines // should not result in clipping failure. - Timeline timeline = - new SinglePeriodTimeline( - C.TIME_UNSET, /* isSeekable= */ false, /* isDynamic= */ true, /* isLive= */ true); + Timeline timeline = new DummyTimeline(/* tag= */ null); Timeline clippedTimeline = getClippedTimeline( @@ -153,7 +172,7 @@ public final class ClippingMediaSourceTest { } @Test - public void testClippingToEndOfSourceWithDurationSetsDuration() throws IOException { + public void clippingToEndOfSourceWithDurationSetsDuration() throws IOException { // Create a child timeline that has a known duration. Timeline timeline = new SinglePeriodTimeline( @@ -170,7 +189,7 @@ public final class ClippingMediaSourceTest { } @Test - public void testClippingToEndOfSourceWithUnsetDurationDoesNotSetDuration() throws IOException { + public void clippingToEndOfSourceWithUnsetDurationDoesNotSetDuration() throws IOException { // Create a child timeline that has an unknown duration. Timeline timeline = new SinglePeriodTimeline( @@ -187,7 +206,7 @@ public final class ClippingMediaSourceTest { } @Test - public void testClippingStartAndEnd() throws IOException { + public void clippingStartAndEnd() throws IOException { Timeline timeline = new SinglePeriodTimeline( TEST_PERIOD_DURATION_US, @@ -205,7 +224,7 @@ public final class ClippingMediaSourceTest { } @Test - public void testClippingFromDefaultPosition() throws IOException { + public void clippingFromDefaultPosition() throws IOException { Timeline timeline = new SinglePeriodTimeline( /* periodDurationUs= */ 3 * TEST_PERIOD_DURATION_US, @@ -228,7 +247,7 @@ public final class ClippingMediaSourceTest { } @Test - public void testAllowDynamicUpdatesWithOverlappingLiveWindow() throws IOException { + public void allowDynamicUpdatesWithOverlappingLiveWindow() throws IOException { Timeline timeline1 = new SinglePeriodTimeline( /* periodDurationUs= */ 2 * TEST_PERIOD_DURATION_US, @@ -279,7 +298,7 @@ public final class ClippingMediaSourceTest { } @Test - public void testAllowDynamicUpdatesWithNonOverlappingLiveWindow() throws IOException { + public void allowDynamicUpdatesWithNonOverlappingLiveWindow() throws IOException { Timeline timeline1 = new SinglePeriodTimeline( /* periodDurationUs= */ 2 * TEST_PERIOD_DURATION_US, @@ -330,7 +349,7 @@ public final class ClippingMediaSourceTest { } @Test - public void testDisallowDynamicUpdatesWithOverlappingLiveWindow() throws IOException { + public void disallowDynamicUpdatesWithOverlappingLiveWindow() throws IOException { Timeline timeline1 = new SinglePeriodTimeline( /* periodDurationUs= */ 2 * TEST_PERIOD_DURATION_US, @@ -382,7 +401,7 @@ public final class ClippingMediaSourceTest { } @Test - public void testDisallowDynamicUpdatesWithNonOverlappingLiveWindow() throws IOException { + public void disallowDynamicUpdatesWithNonOverlappingLiveWindow() throws IOException { Timeline timeline1 = new SinglePeriodTimeline( /* periodDurationUs= */ 2 * TEST_PERIOD_DURATION_US, @@ -432,7 +451,7 @@ public final class ClippingMediaSourceTest { } @Test - public void testWindowAndPeriodIndices() throws IOException { + public void windowAndPeriodIndices() throws IOException { Timeline timeline = new FakeTimeline( new TimelineWindowDefinition(1, 111, true, false, TEST_PERIOD_DURATION_US)); @@ -452,7 +471,7 @@ public final class ClippingMediaSourceTest { } @Test - public void testEventTimeWithinClippedRange() throws IOException { + public void eventTimeWithinClippedRange() throws IOException { MediaLoadData mediaLoadData = getClippingMediaSourceMediaLoadData( /* clippingStartUs= */ TEST_CLIP_AMOUNT_US, @@ -465,7 +484,7 @@ public final class ClippingMediaSourceTest { } @Test - public void testEventTimeOutsideClippedRange() throws IOException { + public void eventTimeOutsideClippedRange() throws IOException { MediaLoadData mediaLoadData = getClippingMediaSourceMediaLoadData( /* clippingStartUs= */ TEST_CLIP_AMOUNT_US, @@ -478,7 +497,7 @@ public final class ClippingMediaSourceTest { } @Test - public void testUnsetEventTime() throws IOException { + public void unsetEventTime() throws IOException { MediaLoadData mediaLoadData = getClippingMediaSourceMediaLoadData( /* clippingStartUs= */ TEST_CLIP_AMOUNT_US, @@ -490,7 +509,7 @@ public final class ClippingMediaSourceTest { } @Test - public void testEventTimeWithUnsetDuration() throws IOException { + public void eventTimeWithUnsetDuration() throws IOException { MediaLoadData mediaLoadData = getClippingMediaSourceMediaLoadData( /* clippingStartUs= */ TEST_CLIP_AMOUNT_US, @@ -635,7 +654,7 @@ public final class ClippingMediaSourceTest { clippedTimelines[0].getUidOfPeriod(/* periodIndex= */ 0), /* windowSequenceNumber= */ 0)); for (int i = 0; i < additionalTimelines.length; i++) { - fakeMediaSource.setNewSourceInfo(additionalTimelines[i], /* newManifest= */ null); + fakeMediaSource.setNewSourceInfo(additionalTimelines[i]); clippedTimelines[i + 1] = testRunner.assertTimelineChangeBlocking(); } testRunner.releasePeriod(mediaPeriod); diff --git a/library/core/src/test/java/com/google/android/exoplayer2/source/CompositeSequenceableLoaderTest.java b/library/core/src/test/java/com/google/android/exoplayer2/source/CompositeSequenceableLoaderTest.java index c996aadddb..c2f9ad07f7 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/source/CompositeSequenceableLoaderTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/source/CompositeSequenceableLoaderTest.java @@ -31,7 +31,7 @@ public final class CompositeSequenceableLoaderTest { * position among all sub-loaders. */ @Test - public void testGetBufferedPositionUsReturnsMinimumLoaderBufferedPosition() { + public void getBufferedPositionUsReturnsMinimumLoaderBufferedPosition() { FakeSequenceableLoader loader1 = new FakeSequenceableLoader(/* bufferedPositionUs */ 1000, /* nextLoadPositionUs */ 2000); FakeSequenceableLoader loader2 = @@ -46,7 +46,7 @@ public final class CompositeSequenceableLoaderTest { * position that is not {@link C#TIME_END_OF_SOURCE} among all sub-loaders. */ @Test - public void testGetBufferedPositionUsReturnsMinimumNonEndOfSourceLoaderBufferedPosition() { + public void getBufferedPositionUsReturnsMinimumNonEndOfSourceLoaderBufferedPosition() { FakeSequenceableLoader loader1 = new FakeSequenceableLoader(/* bufferedPositionUs */ 1000, /* nextLoadPositionUs */ 2000); FakeSequenceableLoader loader2 = @@ -61,11 +61,11 @@ public final class CompositeSequenceableLoaderTest { } /** - * Tests that {@link CompositeSequenceableLoader#getBufferedPositionUs()} returns - * {@link C#TIME_END_OF_SOURCE} when all sub-loaders have buffered till end-of-source. + * Tests that {@link CompositeSequenceableLoader#getBufferedPositionUs()} returns {@link + * C#TIME_END_OF_SOURCE} when all sub-loaders have buffered till end-of-source. */ @Test - public void testGetBufferedPositionUsReturnsEndOfSourceWhenAllLoaderBufferedTillEndOfSource() { + public void getBufferedPositionUsReturnsEndOfSourceWhenAllLoaderBufferedTillEndOfSource() { FakeSequenceableLoader loader1 = new FakeSequenceableLoader( /* bufferedPositionUs */ C.TIME_END_OF_SOURCE, @@ -84,7 +84,7 @@ public final class CompositeSequenceableLoaderTest { * load position among all sub-loaders. */ @Test - public void testGetNextLoadPositionUsReturnMinimumLoaderNextLoadPositionUs() { + public void getNextLoadPositionUsReturnMinimumLoaderNextLoadPositionUs() { FakeSequenceableLoader loader1 = new FakeSequenceableLoader(/* bufferedPositionUs */ 1000, /* nextLoadPositionUs */ 2001); FakeSequenceableLoader loader2 = @@ -99,7 +99,7 @@ public final class CompositeSequenceableLoaderTest { * load position that is not {@link C#TIME_END_OF_SOURCE} among all sub-loaders. */ @Test - public void testGetNextLoadPositionUsReturnMinimumNonEndOfSourceLoaderNextLoadPositionUs() { + public void getNextLoadPositionUsReturnMinimumNonEndOfSourceLoaderNextLoadPositionUs() { FakeSequenceableLoader loader1 = new FakeSequenceableLoader(/* bufferedPositionUs */ 1000, /* nextLoadPositionUs */ 2000); FakeSequenceableLoader loader2 = @@ -113,11 +113,11 @@ public final class CompositeSequenceableLoaderTest { } /** - * Tests that {@link CompositeSequenceableLoader#getNextLoadPositionUs()} returns - * {@link C#TIME_END_OF_SOURCE} when all sub-loaders have next load position at end-of-source. + * Tests that {@link CompositeSequenceableLoader#getNextLoadPositionUs()} returns {@link + * C#TIME_END_OF_SOURCE} when all sub-loaders have next load position at end-of-source. */ @Test - public void testGetNextLoadPositionUsReturnsEndOfSourceWhenAllLoaderLoadingLastChunk() { + public void getNextLoadPositionUsReturnsEndOfSourceWhenAllLoaderLoadingLastChunk() { FakeSequenceableLoader loader1 = new FakeSequenceableLoader( /* bufferedPositionUs */ 1000, /* nextLoadPositionUs */ C.TIME_END_OF_SOURCE); @@ -135,7 +135,7 @@ public final class CompositeSequenceableLoaderTest { * current playback position. */ @Test - public void testContinueLoadingOnlyAllowFurthestBehindLoaderToLoadIfNotBehindPlaybackPosition() { + public void continueLoadingOnlyAllowFurthestBehindLoaderToLoadIfNotBehindPlaybackPosition() { FakeSequenceableLoader loader1 = new FakeSequenceableLoader(/* bufferedPositionUs */ 1000, /* nextLoadPositionUs */ 2000); FakeSequenceableLoader loader2 = @@ -149,11 +149,11 @@ public final class CompositeSequenceableLoaderTest { } /** - * Tests that {@link CompositeSequenceableLoader#continueLoading(long)} allows all loaders - * with next load position behind current playback position to continue loading. + * Tests that {@link CompositeSequenceableLoader#continueLoading(long)} allows all loaders with + * next load position behind current playback position to continue loading. */ @Test - public void testContinueLoadingReturnAllowAllLoadersBehindPlaybackPositionToLoad() { + public void continueLoadingReturnAllowAllLoadersBehindPlaybackPositionToLoad() { FakeSequenceableLoader loader1 = new FakeSequenceableLoader(/* bufferedPositionUs */ 1000, /* nextLoadPositionUs */ 2000); FakeSequenceableLoader loader2 = @@ -170,11 +170,11 @@ public final class CompositeSequenceableLoaderTest { } /** - * Tests that {@link CompositeSequenceableLoader#continueLoading(long)} does not allow loader - * with next load position at end-of-source to continue loading. + * Tests that {@link CompositeSequenceableLoader#continueLoading(long)} does not allow loader with + * next load position at end-of-source to continue loading. */ @Test - public void testContinueLoadingOnlyNotAllowEndOfSourceLoaderToLoad() { + public void continueLoadingOnlyNotAllowEndOfSourceLoaderToLoad() { FakeSequenceableLoader loader1 = new FakeSequenceableLoader( /* bufferedPositionUs */ 1000, /* nextLoadPositionUs */ C.TIME_END_OF_SOURCE); @@ -191,11 +191,11 @@ public final class CompositeSequenceableLoaderTest { /** * Tests that {@link CompositeSequenceableLoader#continueLoading(long)} returns true if the loader - * with minimum next load position can make progress if next load positions are not behind - * current playback position. + * with minimum next load position can make progress if next load positions are not behind current + * playback position. */ @Test - public void testContinueLoadingReturnTrueIfFurthestBehindLoaderCanMakeProgress() { + public void continueLoadingReturnTrueIfFurthestBehindLoaderCanMakeProgress() { FakeSequenceableLoader loader1 = new FakeSequenceableLoader(/* bufferedPositionUs */ 1000, /* nextLoadPositionUs */ 2000); FakeSequenceableLoader loader2 = @@ -214,7 +214,7 @@ public final class CompositeSequenceableLoaderTest { * minimum next load position. */ @Test - public void testContinueLoadingReturnTrueIfLoaderBehindPlaybackPositionCanMakeProgress() { + public void continueLoadingReturnTrueIfLoaderBehindPlaybackPositionCanMakeProgress() { FakeSequenceableLoader loader1 = new FakeSequenceableLoader(/* bufferedPositionUs */ 1000, /* nextLoadPositionUs */ 2000); FakeSequenceableLoader loader2 = 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 d2330f200e..cf2e3e879d 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 @@ -65,7 +65,7 @@ public final class ConcatenatingMediaSourceTest { } @Test - public void testPlaylistChangesAfterPreparation() throws IOException, InterruptedException { + public void playlistChangesAfterPreparation() throws IOException, InterruptedException { Timeline timeline = testRunner.prepareSource(); TimelineAsserts.assertEmpty(timeline); @@ -187,7 +187,7 @@ public final class ConcatenatingMediaSourceTest { } @Test - public void testPlaylistChangesBeforePreparation() throws IOException, InterruptedException { + public void playlistChangesBeforePreparation() throws IOException, InterruptedException { FakeMediaSource[] childSources = createMediaSources(4); mediaSource.addMediaSource(childSources[0]); mediaSource.addMediaSource(childSources[1]); @@ -220,7 +220,7 @@ public final class ConcatenatingMediaSourceTest { } @Test - public void testPlaylistWithLazyMediaSource() throws IOException, InterruptedException { + public void playlistWithLazyMediaSource() throws IOException, InterruptedException { // Create some normal (immediately preparing) sources and some lazy sources whose timeline // updates need to be triggered. FakeMediaSource[] fastSources = createMediaSources(2); @@ -246,8 +246,7 @@ public final class ConcatenatingMediaSourceTest { // Trigger source info refresh for lazy source and check that the timeline now contains all // information for all windows. - testRunner.runOnPlaybackThread( - () -> lazySources[1].setNewSourceInfo(createFakeTimeline(8), null)); + testRunner.runOnPlaybackThread(() -> lazySources[1].setNewSourceInfo(createFakeTimeline(8))); timeline = testRunner.assertTimelineChangeBlocking(); TimelineAsserts.assertPeriodCounts(timeline, 1, 9); TimelineAsserts.assertWindowTags(timeline, 111, 999); @@ -281,8 +280,7 @@ public final class ConcatenatingMediaSourceTest { // Trigger source info refresh for lazy media source. Assert that now all information is // available again and the previously created period now also finished preparing. - testRunner.runOnPlaybackThread( - () -> lazySources[3].setNewSourceInfo(createFakeTimeline(7), null)); + testRunner.runOnPlaybackThread(() -> lazySources[3].setNewSourceInfo(createFakeTimeline(7))); timeline = testRunner.assertTimelineChangeBlocking(); TimelineAsserts.assertPeriodCounts(timeline, 8, 1, 2, 9); TimelineAsserts.assertWindowTags(timeline, 888, 111, 222, 999); @@ -303,7 +301,7 @@ public final class ConcatenatingMediaSourceTest { } @Test - public void testEmptyTimelineMediaSource() throws IOException, InterruptedException { + public void emptyTimelineMediaSource() throws IOException, InterruptedException { Timeline timeline = testRunner.prepareSource(); TimelineAsserts.assertEmpty(timeline); @@ -359,7 +357,7 @@ public final class ConcatenatingMediaSourceTest { } @Test - public void testDynamicChangeOfEmptyTimelines() throws IOException { + public void dynamicChangeOfEmptyTimelines() throws IOException { FakeMediaSource[] childSources = new FakeMediaSource[] { new FakeMediaSource(Timeline.EMPTY), @@ -372,21 +370,21 @@ public final class ConcatenatingMediaSourceTest { Timeline timeline = testRunner.prepareSource(); TimelineAsserts.assertEmpty(timeline); - childSources[0].setNewSourceInfo(nonEmptyTimeline, /* newManifest== */ null); + childSources[0].setNewSourceInfo(nonEmptyTimeline); timeline = testRunner.assertTimelineChangeBlocking(); TimelineAsserts.assertPeriodCounts(timeline, 1); - childSources[2].setNewSourceInfo(nonEmptyTimeline, /* newManifest== */ null); + childSources[2].setNewSourceInfo(nonEmptyTimeline); timeline = testRunner.assertTimelineChangeBlocking(); TimelineAsserts.assertPeriodCounts(timeline, 1, 1); - childSources[1].setNewSourceInfo(nonEmptyTimeline, /* newManifest== */ null); + childSources[1].setNewSourceInfo(nonEmptyTimeline); timeline = testRunner.assertTimelineChangeBlocking(); TimelineAsserts.assertPeriodCounts(timeline, 1, 1, 1); } @Test - public void testIllegalArguments() { + public void illegalArguments() { MediaSource validSource = new FakeMediaSource(createFakeTimeline(1)); // Null sources. @@ -407,7 +405,7 @@ public final class ConcatenatingMediaSourceTest { } @Test - public void testCustomCallbackBeforePreparationAddSingle() throws Exception { + public void customCallbackBeforePreparationAddSingle() throws Exception { CountDownLatch runnableInvoked = new CountDownLatch(1); DummyMainThread dummyMainThread = new DummyMainThread(); @@ -422,7 +420,7 @@ public final class ConcatenatingMediaSourceTest { } @Test - public void testCustomCallbackBeforePreparationAddMultiple() throws Exception { + public void customCallbackBeforePreparationAddMultiple() throws Exception { CountDownLatch runnableInvoked = new CountDownLatch(1); DummyMainThread dummyMainThread = new DummyMainThread(); @@ -439,7 +437,7 @@ public final class ConcatenatingMediaSourceTest { } @Test - public void testCustomCallbackBeforePreparationAddSingleWithIndex() throws Exception { + public void customCallbackBeforePreparationAddSingleWithIndex() throws Exception { CountDownLatch runnableInvoked = new CountDownLatch(1); DummyMainThread dummyMainThread = new DummyMainThread(); @@ -457,7 +455,7 @@ public final class ConcatenatingMediaSourceTest { } @Test - public void testCustomCallbackBeforePreparationAddMultipleWithIndex() throws Exception { + public void customCallbackBeforePreparationAddMultipleWithIndex() throws Exception { CountDownLatch runnableInvoked = new CountDownLatch(1); DummyMainThread dummyMainThread = new DummyMainThread(); @@ -475,7 +473,7 @@ public final class ConcatenatingMediaSourceTest { } @Test - public void testCustomCallbackBeforePreparationRemove() throws Exception { + public void customCallbackBeforePreparationRemove() throws Exception { CountDownLatch runnableInvoked = new CountDownLatch(1); DummyMainThread dummyMainThread = new DummyMainThread(); @@ -492,7 +490,7 @@ public final class ConcatenatingMediaSourceTest { } @Test - public void testCustomCallbackBeforePreparationMove() throws Exception { + public void customCallbackBeforePreparationMove() throws Exception { CountDownLatch runnableInvoked = new CountDownLatch(1); DummyMainThread dummyMainThread = new DummyMainThread(); @@ -510,7 +508,7 @@ public final class ConcatenatingMediaSourceTest { } @Test - public void testCustomCallbackAfterPreparationAddSingle() throws Exception { + public void customCallbackAfterPreparationAddSingle() throws Exception { DummyMainThread dummyMainThread = new DummyMainThread(); try { testRunner.prepareSource(); @@ -527,7 +525,7 @@ public final class ConcatenatingMediaSourceTest { } @Test - public void testCustomCallbackAfterPreparationAddMultiple() throws Exception { + public void customCallbackAfterPreparationAddMultiple() throws Exception { DummyMainThread dummyMainThread = new DummyMainThread(); try { testRunner.prepareSource(); @@ -547,7 +545,7 @@ public final class ConcatenatingMediaSourceTest { } @Test - public void testCustomCallbackAfterPreparationAddSingleWithIndex() throws Exception { + public void customCallbackAfterPreparationAddSingleWithIndex() throws Exception { DummyMainThread dummyMainThread = new DummyMainThread(); try { testRunner.prepareSource(); @@ -564,7 +562,7 @@ public final class ConcatenatingMediaSourceTest { } @Test - public void testCustomCallbackAfterPreparationAddMultipleWithIndex() throws Exception { + public void customCallbackAfterPreparationAddMultipleWithIndex() throws Exception { DummyMainThread dummyMainThread = new DummyMainThread(); try { testRunner.prepareSource(); @@ -585,7 +583,7 @@ public final class ConcatenatingMediaSourceTest { } @Test - public void testCustomCallbackAfterPreparationRemove() throws Exception { + public void customCallbackAfterPreparationRemove() throws Exception { DummyMainThread dummyMainThread = new DummyMainThread(); try { testRunner.prepareSource(); @@ -604,7 +602,7 @@ public final class ConcatenatingMediaSourceTest { } @Test - public void testCustomCallbackAfterPreparationMove() throws Exception { + public void customCallbackAfterPreparationMove() throws Exception { DummyMainThread dummyMainThread = new DummyMainThread(); try { testRunner.prepareSource(); @@ -628,7 +626,7 @@ public final class ConcatenatingMediaSourceTest { } @Test - public void testCustomCallbackIsCalledAfterRelease() throws Exception { + public void customCallbackIsCalledAfterRelease() throws Exception { DummyMainThread dummyMainThread = new DummyMainThread(); CountDownLatch callbackCalledCondition = new CountDownLatch(1); try { @@ -654,7 +652,7 @@ public final class ConcatenatingMediaSourceTest { } @Test - public void testPeriodCreationWithAds() throws IOException, InterruptedException { + public void periodCreationWithAds() throws IOException, InterruptedException { // Create concatenated media source with ad child source. Timeline timelineContentOnly = new FakeTimeline( @@ -668,7 +666,7 @@ public final class ConcatenatingMediaSourceTest { false, 10 * C.MICROS_PER_SECOND, FakeTimeline.createAdPlaybackState( - /* adsPerAdGroup= */ 1, /* adGroupTimesUs= */ 0))); + /* adsPerAdGroup= */ 1, /* adGroupTimesUs=... */ 0))); FakeMediaSource mediaSourceContentOnly = new FakeMediaSource(timelineContentOnly); FakeMediaSource mediaSourceWithAds = new FakeMediaSource(timelineWithAds); mediaSource.addMediaSource(mediaSourceContentOnly); @@ -710,7 +708,7 @@ public final class ConcatenatingMediaSourceTest { } @Test - public void testAtomicTimelineWindowOrder() throws IOException { + public void atomicTimelineWindowOrder() throws IOException { // Release default test runner with non-atomic media source and replace with new test runner. testRunner.release(); ConcatenatingMediaSource mediaSource = @@ -751,7 +749,7 @@ public final class ConcatenatingMediaSourceTest { } @Test - public void testNestedTimeline() throws IOException { + public void nestedTimeline() throws IOException { ConcatenatingMediaSource nestedSource1 = new ConcatenatingMediaSource(/* isAtomic= */ false, new FakeShuffleOrder(0)); ConcatenatingMediaSource nestedSource2 = @@ -798,7 +796,7 @@ public final class ConcatenatingMediaSourceTest { } @Test - public void testRemoveChildSourceWithActiveMediaPeriod() throws IOException { + public void removeChildSourceWithActiveMediaPeriod() throws IOException { FakeMediaSource childSource = createFakeMediaSource(); mediaSource.addMediaSource(childSource); Timeline timeline = testRunner.prepareSource(); @@ -814,7 +812,7 @@ public final class ConcatenatingMediaSourceTest { } @Test - public void testDuplicateMediaSources() throws IOException, InterruptedException { + public void duplicateMediaSources() throws IOException, InterruptedException { Timeline childTimeline = new FakeTimeline(/* windowCount= */ 2); FakeMediaSource childSource = new FakeMediaSource(childTimeline); @@ -839,7 +837,7 @@ public final class ConcatenatingMediaSourceTest { new MediaPeriodId(childPeriodUid1, /* windowSequenceNumber= */ 5), new MediaPeriodId(childPeriodUid1, /* windowSequenceNumber= */ 7)); // Assert that only one manifest load is reported because the source is reused. - testRunner.assertCompletedManifestLoads(/* windowIndices= */ 0); + testRunner.assertCompletedManifestLoads(/* windowIndices=... */ 0); assertCompletedAllMediaPeriodLoads(timeline); testRunner.releaseSource(); @@ -847,7 +845,7 @@ public final class ConcatenatingMediaSourceTest { } @Test - public void testDuplicateNestedMediaSources() throws IOException, InterruptedException { + public void duplicateNestedMediaSources() throws IOException, InterruptedException { Timeline childTimeline = new FakeTimeline(/* windowCount= */ 1); FakeMediaSource childSource = new FakeMediaSource(childTimeline); ConcatenatingMediaSource nestedConcatenation = new ConcatenatingMediaSource(); @@ -872,7 +870,7 @@ public final class ConcatenatingMediaSourceTest { new MediaPeriodId(childPeriodUid, /* windowSequenceNumber= */ 3), new MediaPeriodId(childPeriodUid, /* windowSequenceNumber= */ 4)); // Assert that only one manifest load is needed because the source is reused. - testRunner.assertCompletedManifestLoads(/* windowIndices= */ 0); + testRunner.assertCompletedManifestLoads(/* windowIndices=... */ 0); assertCompletedAllMediaPeriodLoads(timeline); testRunner.releaseSource(); @@ -880,7 +878,7 @@ public final class ConcatenatingMediaSourceTest { } @Test - public void testClear() throws Exception { + public void clear() throws Exception { DummyMainThread dummyMainThread = new DummyMainThread(); final FakeMediaSource preparedChildSource = createFakeMediaSource(); final FakeMediaSource unpreparedChildSource = new FakeMediaSource(/* timeline= */ null); @@ -901,7 +899,7 @@ public final class ConcatenatingMediaSourceTest { } @Test - public void testReleaseAndReprepareSource() throws IOException { + public void releaseAndReprepareSource() throws IOException { FakeMediaSource[] fakeMediaSources = createMediaSources(/* count= */ 2); mediaSource.addMediaSource(fakeMediaSources[0]); // Child source with 1 period. mediaSource.addMediaSource(fakeMediaSources[1]); // Child source with 2 periods. @@ -922,7 +920,7 @@ public final class ConcatenatingMediaSourceTest { } @Test - public void testChildTimelineChangeWithActiveMediaPeriod() throws IOException { + public void childTimelineChangeWithActiveMediaPeriod() throws IOException { FakeMediaSource[] nestedChildSources = createMediaSources(/* count= */ 2); ConcatenatingMediaSource childSource = new ConcatenatingMediaSource(nestedChildSources); mediaSource.addMediaSource(childSource); @@ -942,7 +940,7 @@ public final class ConcatenatingMediaSourceTest { } @Test - public void testChildSourceIsNotPreparedWithLazyPreparation() throws IOException { + public void childSourceIsNotPreparedWithLazyPreparation() throws IOException { FakeMediaSource[] childSources = createMediaSources(/* count= */ 2); mediaSource = new ConcatenatingMediaSource( @@ -958,7 +956,7 @@ public final class ConcatenatingMediaSourceTest { } @Test - public void testChildSourceIsPreparedWithLazyPreparationAfterPeriodCreation() throws IOException { + public void childSourceIsPreparedWithLazyPreparationAfterPeriodCreation() throws IOException { FakeMediaSource[] childSources = createMediaSources(/* count= */ 2); mediaSource = new ConcatenatingMediaSource( @@ -977,7 +975,7 @@ public final class ConcatenatingMediaSourceTest { } @Test - public void testChildSourceWithLazyPreparationOnlyPreparesSourceOnce() throws IOException { + public void childSourceWithLazyPreparationOnlyPreparesSourceOnce() throws IOException { FakeMediaSource[] childSources = createMediaSources(/* count= */ 2); mediaSource = new ConcatenatingMediaSource( @@ -999,7 +997,7 @@ public final class ConcatenatingMediaSourceTest { } @Test - public void testRemoveUnpreparedChildSourceWithLazyPreparation() throws IOException { + public void removeUnpreparedChildSourceWithLazyPreparation() throws IOException { FakeMediaSource[] childSources = createMediaSources(/* count= */ 2); mediaSource = new ConcatenatingMediaSource( @@ -1015,7 +1013,7 @@ public final class ConcatenatingMediaSourceTest { } @Test - public void testSetShuffleOrderBeforePreparation() throws Exception { + public void setShuffleOrderBeforePreparation() throws Exception { mediaSource.setShuffleOrder(new ShuffleOrder.UnshuffledShuffleOrder(/* length= */ 0)); mediaSource.addMediaSources( Arrays.asList(createFakeMediaSource(), createFakeMediaSource(), createFakeMediaSource())); @@ -1025,7 +1023,7 @@ public final class ConcatenatingMediaSourceTest { } @Test - public void testSetShuffleOrderAfterPreparation() throws Exception { + public void setShuffleOrderAfterPreparation() throws Exception { mediaSource.addMediaSources( Arrays.asList(createFakeMediaSource(), createFakeMediaSource(), createFakeMediaSource())); testRunner.prepareSource(); @@ -1036,7 +1034,7 @@ public final class ConcatenatingMediaSourceTest { } @Test - public void testCustomCallbackBeforePreparationSetShuffleOrder() throws Exception { + public void customCallbackBeforePreparationSetShuffleOrder() throws Exception { CountDownLatch runnableInvoked = new CountDownLatch(1); DummyMainThread dummyMainThread = new DummyMainThread(); @@ -1053,7 +1051,7 @@ public final class ConcatenatingMediaSourceTest { } @Test - public void testCustomCallbackAfterPreparationSetShuffleOrder() throws Exception { + public void customCallbackAfterPreparationSetShuffleOrder() throws Exception { DummyMainThread dummyMainThread = new DummyMainThread(); try { mediaSource.addMediaSources( diff --git a/library/core/src/test/java/com/google/android/exoplayer2/source/DefaultMediaSourceFactoryTest.java b/library/core/src/test/java/com/google/android/exoplayer2/source/DefaultMediaSourceFactoryTest.java new file mode 100644 index 0000000000..0e723a0263 --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/source/DefaultMediaSourceFactoryTest.java @@ -0,0 +1,249 @@ +/* + * Copyright 2020 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; + +import static com.google.common.truth.Truth.assertThat; +import static org.mockito.Mockito.mock; + +import android.content.Context; +import android.net.Uri; +import androidx.annotation.Nullable; +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.MediaItem; +import com.google.android.exoplayer2.source.ads.AdsLoader; +import com.google.android.exoplayer2.source.ads.AdsMediaSource; +import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; +import com.google.android.exoplayer2.util.MimeTypes; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Unit test for {@link DefaultMediaSourceFactory}. */ +@RunWith(AndroidJUnit4.class) +public final class DefaultMediaSourceFactoryTest { + + private static final String URI_MEDIA = "http://exoplayer.dev/video"; + private static final String URI_TEXT = "http://exoplayer.dev/text"; + + @Test + public void createMediaSource_withoutMimeType_progressiveSource() { + DefaultMediaSourceFactory defaultMediaSourceFactory = + DefaultMediaSourceFactory.newInstance(ApplicationProvider.getApplicationContext()); + MediaItem mediaItem = new MediaItem.Builder().setUri(URI_MEDIA).build(); + + MediaSource mediaSource = defaultMediaSourceFactory.createMediaSource(mediaItem); + + assertThat(mediaSource).isInstanceOf(ProgressiveMediaSource.class); + } + + @Test + public void createMediaSource_withTag_tagInSource() { + Object tag = new Object(); + DefaultMediaSourceFactory defaultMediaSourceFactory = + DefaultMediaSourceFactory.newInstance(ApplicationProvider.getApplicationContext()); + MediaItem mediaItem = new MediaItem.Builder().setUri(URI_MEDIA).setTag(tag).build(); + + MediaSource mediaSource = defaultMediaSourceFactory.createMediaSource(mediaItem); + + assertThat(mediaSource.getTag()).isEqualTo(tag); + } + + @Test + public void createMediaSource_withPath_progressiveSource() { + DefaultMediaSourceFactory defaultMediaSourceFactory = + DefaultMediaSourceFactory.newInstance(ApplicationProvider.getApplicationContext()); + MediaItem mediaItem = new MediaItem.Builder().setUri(URI_MEDIA + "/file.mp3").build(); + + MediaSource mediaSource = defaultMediaSourceFactory.createMediaSource(mediaItem); + + assertThat(mediaSource).isInstanceOf(ProgressiveMediaSource.class); + } + + @Test + public void createMediaSource_withNull_usesNonNullDefaults() { + DefaultMediaSourceFactory defaultMediaSourceFactory = + DefaultMediaSourceFactory.newInstance(ApplicationProvider.getApplicationContext()); + MediaItem mediaItem = new MediaItem.Builder().setUri(URI_MEDIA).build(); + + MediaSource mediaSource = + defaultMediaSourceFactory + .setDrmSessionManager(null) + .setDrmHttpDataSourceFactory(null) + .setLoadErrorHandlingPolicy(null) + .createMediaSource(mediaItem); + + assertThat(mediaSource).isNotNull(); + } + + @Test + public void createMediaSource_withSubtitle_isMergingMediaSource() { + DefaultMediaSourceFactory defaultMediaSourceFactory = + DefaultMediaSourceFactory.newInstance(ApplicationProvider.getApplicationContext()); + List subtitles = + Arrays.asList( + new MediaItem.Subtitle(Uri.parse(URI_TEXT), MimeTypes.APPLICATION_TTML, "en"), + new MediaItem.Subtitle( + Uri.parse(URI_TEXT), MimeTypes.APPLICATION_TTML, "de", C.SELECTION_FLAG_DEFAULT)); + MediaItem mediaItem = new MediaItem.Builder().setUri(URI_MEDIA).setSubtitles(subtitles).build(); + + MediaSource mediaSource = defaultMediaSourceFactory.createMediaSource(mediaItem); + + assertThat(mediaSource).isInstanceOf(MergingMediaSource.class); + } + + @Test + public void createMediaSource_withSubtitle_hasTag() { + DefaultMediaSourceFactory defaultMediaSourceFactory = + DefaultMediaSourceFactory.newInstance(ApplicationProvider.getApplicationContext()); + Object tag = new Object(); + MediaItem mediaItem = + new MediaItem.Builder() + .setTag(tag) + .setUri(URI_MEDIA) + .setSubtitles( + Collections.singletonList( + new MediaItem.Subtitle(Uri.parse(URI_TEXT), MimeTypes.APPLICATION_TTML, "en"))) + .build(); + + MediaSource mediaSource = defaultMediaSourceFactory.createMediaSource(mediaItem); + + assertThat(mediaSource.getTag()).isEqualTo(tag); + } + + @Test + public void createMediaSource_withStartPosition_isClippingMediaSource() { + DefaultMediaSourceFactory defaultMediaSourceFactory = + DefaultMediaSourceFactory.newInstance(ApplicationProvider.getApplicationContext()); + MediaItem mediaItem = + new MediaItem.Builder().setUri(URI_MEDIA).setClipStartPositionMs(1000L).build(); + + MediaSource mediaSource = defaultMediaSourceFactory.createMediaSource(mediaItem); + + assertThat(mediaSource).isInstanceOf(ClippingMediaSource.class); + } + + @Test + public void createMediaSource_withEndPosition_isClippingMediaSource() { + DefaultMediaSourceFactory defaultMediaSourceFactory = + DefaultMediaSourceFactory.newInstance(ApplicationProvider.getApplicationContext()); + MediaItem mediaItem = + new MediaItem.Builder().setUri(URI_MEDIA).setClipEndPositionMs(1000L).build(); + + MediaSource mediaSource = defaultMediaSourceFactory.createMediaSource(mediaItem); + + assertThat(mediaSource).isInstanceOf(ClippingMediaSource.class); + } + + @Test + public void createMediaSource_relativeToDefaultPosition_isClippingMediaSource() { + DefaultMediaSourceFactory defaultMediaSourceFactory = + DefaultMediaSourceFactory.newInstance(ApplicationProvider.getApplicationContext()); + MediaItem mediaItem = + new MediaItem.Builder().setUri(URI_MEDIA).setClipRelativeToDefaultPosition(true).build(); + + MediaSource mediaSource = defaultMediaSourceFactory.createMediaSource(mediaItem); + + assertThat(mediaSource).isInstanceOf(ClippingMediaSource.class); + } + + @Test + public void createMediaSource_defaultToEnd_isNotClippingMediaSource() { + DefaultMediaSourceFactory defaultMediaSourceFactory = + DefaultMediaSourceFactory.newInstance(ApplicationProvider.getApplicationContext()); + MediaItem mediaItem = + new MediaItem.Builder() + .setUri(URI_MEDIA) + .setClipEndPositionMs(C.TIME_END_OF_SOURCE) + .build(); + + MediaSource mediaSource = defaultMediaSourceFactory.createMediaSource(mediaItem); + + assertThat(mediaSource).isInstanceOf(ProgressiveMediaSource.class); + } + + @Test + public void getSupportedTypes_coreModule_onlyOther() { + int[] supportedTypes = + DefaultMediaSourceFactory.newInstance(ApplicationProvider.getApplicationContext()) + .getSupportedTypes(); + + assertThat(supportedTypes).asList().containsExactly(C.TYPE_OTHER); + } + + @Test + public void createMediaSource_withAdTagUri_callsAdsLoader() { + Context applicationContext = ApplicationProvider.getApplicationContext(); + Uri adTagUri = Uri.parse(URI_MEDIA); + MediaItem mediaItem = new MediaItem.Builder().setUri(URI_MEDIA).setAdTagUri(adTagUri).build(); + DefaultMediaSourceFactory defaultMediaSourceFactory = + new DefaultMediaSourceFactory( + applicationContext, + new DefaultDataSourceFactory(applicationContext, "userAgent"), + createAdSupportProvider(mock(AdsLoader.class), mock(AdsLoader.AdViewProvider.class))); + + MediaSource mediaSource = defaultMediaSourceFactory.createMediaSource(mediaItem); + + assertThat(mediaSource).isInstanceOf(AdsMediaSource.class); + } + + @Test + public void createMediaSource_withAdTagUriAdsLoaderNull_playsWithoutAdNoException() { + Context applicationContext = ApplicationProvider.getApplicationContext(); + MediaItem mediaItem = + new MediaItem.Builder().setUri(URI_MEDIA).setAdTagUri(Uri.parse(URI_MEDIA)).build(); + DefaultMediaSourceFactory defaultMediaSourceFactory = + new DefaultMediaSourceFactory( + applicationContext, + new DefaultDataSourceFactory(applicationContext, "userAgent"), + createAdSupportProvider(/* adsLoader= */ null, mock(AdsLoader.AdViewProvider.class))); + + MediaSource mediaSource = defaultMediaSourceFactory.createMediaSource(mediaItem); + + assertThat(mediaSource).isNotInstanceOf(AdsMediaSource.class); + } + + @Test + public void createMediaSource_withAdTagUriProvidersNull_playsWithoutAdNoException() { + Context applicationContext = ApplicationProvider.getApplicationContext(); + MediaItem mediaItem = + new MediaItem.Builder().setUri(URI_MEDIA).setAdTagUri(Uri.parse(URI_MEDIA)).build(); + + MediaSource mediaSource = + DefaultMediaSourceFactory.newInstance(applicationContext).createMediaSource(mediaItem); + + assertThat(mediaSource).isNotInstanceOf(AdsMediaSource.class); + } + + private static DefaultMediaSourceFactory.AdSupportProvider createAdSupportProvider( + @Nullable AdsLoader adsLoader, AdsLoader.AdViewProvider adViewProvider) { + return new DefaultMediaSourceFactory.AdSupportProvider() { + @Nullable + @Override + public AdsLoader getAdsLoader(Uri adTagUri) { + return adsLoader; + } + + @Override + public AdsLoader.AdViewProvider getAdViewProvider() { + return adViewProvider; + } + }; + } +} 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 fa7c2f0614..f938ffe370 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 @@ -47,7 +47,7 @@ public class LoopingMediaSourceTest { } @Test - public void testSingleLoopTimeline() throws IOException { + public void singleLoopTimeline() throws IOException { Timeline timeline = getLoopingTimeline(multiWindowTimeline, 1); TimelineAsserts.assertWindowTags(timeline, 111, 222, 333); TimelineAsserts.assertPeriodCounts(timeline, 1, 1, 1); @@ -66,7 +66,7 @@ public class LoopingMediaSourceTest { } @Test - public void testMultiLoopTimeline() throws IOException { + public void multiLoopTimeline() 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); @@ -87,7 +87,7 @@ public class LoopingMediaSourceTest { } @Test - public void testInfiniteLoopTimeline() throws IOException { + public void infiniteLoopTimeline() throws IOException { Timeline timeline = getLoopingTimeline(multiWindowTimeline, Integer.MAX_VALUE); TimelineAsserts.assertWindowTags(timeline, 111, 222, 333); TimelineAsserts.assertPeriodCounts(timeline, 1, 1, 1); @@ -105,7 +105,7 @@ public class LoopingMediaSourceTest { } @Test - public void testEmptyTimelineLoop() throws IOException { + public void emptyTimelineLoop() throws IOException { Timeline timeline = getLoopingTimeline(Timeline.EMPTY, 1); TimelineAsserts.assertEmpty(timeline); @@ -117,17 +117,17 @@ public class LoopingMediaSourceTest { } @Test - public void testSingleLoopPeriodCreation() throws Exception { + public void singleLoopPeriodCreation() throws Exception { testMediaPeriodCreation(multiWindowTimeline, /* loopCount= */ 1); } @Test - public void testMultiLoopPeriodCreation() throws Exception { + public void multiLoopPeriodCreation() throws Exception { testMediaPeriodCreation(multiWindowTimeline, /* loopCount= */ 3); } @Test - public void testInfiniteLoopPeriodCreation() throws Exception { + public void infiniteLoopPeriodCreation() throws Exception { testMediaPeriodCreation(multiWindowTimeline, /* loopCount= */ Integer.MAX_VALUE); } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/source/MergingMediaPeriodTest.java b/library/core/src/test/java/com/google/android/exoplayer2/source/MergingMediaPeriodTest.java new file mode 100644 index 0000000000..d201782b53 --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/source/MergingMediaPeriodTest.java @@ -0,0 +1,204 @@ +/* + * Copyright (C) 2020 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; + +import static com.google.common.truth.Truth.assertThat; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.FormatHolder; +import com.google.android.exoplayer2.decoder.DecoderInputBuffer; +import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; +import com.google.android.exoplayer2.testutil.FakeMediaPeriod; +import com.google.android.exoplayer2.trackselection.FixedTrackSelection; +import com.google.android.exoplayer2.trackselection.TrackSelection; +import java.util.concurrent.CountDownLatch; +import org.checkerframework.checker.nullness.compatqual.NullableType; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.annotation.LooperMode; + +/** Unit test for {@link MergingMediaPeriod}. */ +@RunWith(AndroidJUnit4.class) +@LooperMode(LooperMode.Mode.PAUSED) +public final class MergingMediaPeriodTest { + + private static final Format childFormat11 = new Format.Builder().setId("1_1").build(); + private static final Format childFormat12 = new Format.Builder().setId("1_2").build(); + private static final Format childFormat21 = new Format.Builder().setId("2_1").build(); + private static final Format childFormat22 = new Format.Builder().setId("2_2").build(); + + @Test + public void getTrackGroups_returnsAllChildTrackGroups() throws Exception { + MergingMediaPeriod mergingMediaPeriod = + prepareMergingPeriod( + new MergingPeriodDefinition(/* timeOffsetUs= */ 0, childFormat11, childFormat12), + new MergingPeriodDefinition(/* timeOffsetUs= */ 0, childFormat21, childFormat22)); + + assertThat(mergingMediaPeriod.getTrackGroups().length).isEqualTo(4); + assertThat(mergingMediaPeriod.getTrackGroups().get(0).getFormat(0)).isEqualTo(childFormat11); + assertThat(mergingMediaPeriod.getTrackGroups().get(1).getFormat(0)).isEqualTo(childFormat12); + assertThat(mergingMediaPeriod.getTrackGroups().get(2).getFormat(0)).isEqualTo(childFormat21); + assertThat(mergingMediaPeriod.getTrackGroups().get(3).getFormat(0)).isEqualTo(childFormat22); + } + + @Test + public void selectTracks_createsSampleStreamsFromChildPeriods() throws Exception { + MergingMediaPeriod mergingMediaPeriod = + prepareMergingPeriod( + new MergingPeriodDefinition(/* timeOffsetUs= */ 0, childFormat11, childFormat12), + new MergingPeriodDefinition(/* timeOffsetUs= */ 0, childFormat21, childFormat22)); + + TrackSelection selectionForChild1 = + new FixedTrackSelection(mergingMediaPeriod.getTrackGroups().get(1), /* track= */ 0); + TrackSelection selectionForChild2 = + new FixedTrackSelection(mergingMediaPeriod.getTrackGroups().get(2), /* track= */ 0); + SampleStream[] streams = new SampleStream[4]; + mergingMediaPeriod.selectTracks( + /* selections= */ new TrackSelection[] {null, selectionForChild1, selectionForChild2, null}, + /* mayRetainStreamFlags= */ new boolean[] {false, false, false, false}, + streams, + /* streamResetFlags= */ new boolean[] {false, false, false, false}, + /* positionUs= */ 0); + + assertThat(streams[0]).isNull(); + assertThat(streams[3]).isNull(); + + FormatHolder formatHolder = new FormatHolder(); + DecoderInputBuffer inputBuffer = + new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_NORMAL); + assertThat(streams[1].readData(formatHolder, inputBuffer, /* formatRequired= */ true)) + .isEqualTo(C.RESULT_FORMAT_READ); + assertThat(formatHolder.format).isEqualTo(childFormat12); + + assertThat(streams[2].readData(formatHolder, inputBuffer, /* formatRequired= */ true)) + .isEqualTo(C.RESULT_FORMAT_READ); + assertThat(formatHolder.format).isEqualTo(childFormat21); + } + + @Test + public void + selectTracks_withPeriodOffsets_selectTracksWithOffset_andCreatesSampleStreamsCorrectingOffset() + throws Exception { + MergingMediaPeriod mergingMediaPeriod = + prepareMergingPeriod( + new MergingPeriodDefinition(/* timeOffsetUs= */ 0, childFormat11, childFormat12), + new MergingPeriodDefinition(/* timeOffsetUs= */ -3000, childFormat21, childFormat22)); + + TrackSelection selectionForChild1 = + new FixedTrackSelection(mergingMediaPeriod.getTrackGroups().get(0), /* track= */ 0); + TrackSelection selectionForChild2 = + new FixedTrackSelection(mergingMediaPeriod.getTrackGroups().get(2), /* track= */ 0); + SampleStream[] streams = new SampleStream[2]; + mergingMediaPeriod.selectTracks( + /* selections= */ new TrackSelection[] {selectionForChild1, selectionForChild2}, + /* mayRetainStreamFlags= */ new boolean[] {false, false}, + streams, + /* streamResetFlags= */ new boolean[] {false, false}, + /* positionUs= */ 0); + FormatHolder formatHolder = new FormatHolder(); + DecoderInputBuffer inputBuffer = + new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_NORMAL); + streams[0].readData(formatHolder, inputBuffer, /* formatRequired= */ true); + streams[1].readData(formatHolder, inputBuffer, /* formatRequired= */ true); + + FakeMediaPeriodWithSelectTracksPosition childMediaPeriod1 = + (FakeMediaPeriodWithSelectTracksPosition) mergingMediaPeriod.getChildPeriod(0); + assertThat(childMediaPeriod1.selectTracksPositionUs).isEqualTo(0); + assertThat(streams[0].readData(formatHolder, inputBuffer, /* formatRequired= */ false)) + .isEqualTo(C.RESULT_BUFFER_READ); + assertThat(inputBuffer.timeUs).isEqualTo(0L); + + FakeMediaPeriodWithSelectTracksPosition childMediaPeriod2 = + (FakeMediaPeriodWithSelectTracksPosition) mergingMediaPeriod.getChildPeriod(1); + assertThat(childMediaPeriod2.selectTracksPositionUs).isEqualTo(3000L); + assertThat(streams[1].readData(formatHolder, inputBuffer, /* formatRequired= */ false)) + .isEqualTo(C.RESULT_BUFFER_READ); + assertThat(inputBuffer.timeUs).isEqualTo(0L); + } + + private MergingMediaPeriod prepareMergingPeriod(MergingPeriodDefinition... definitions) + throws Exception { + MediaPeriod[] mediaPeriods = new MediaPeriod[definitions.length]; + long[] timeOffsetsUs = new long[definitions.length]; + for (int i = 0; i < definitions.length; i++) { + timeOffsetsUs[i] = definitions[i].timeOffsetUs; + TrackGroup[] trackGroups = new TrackGroup[definitions[i].formats.length]; + for (int j = 0; j < definitions[i].formats.length; j++) { + trackGroups[j] = new TrackGroup(definitions[i].formats[j]); + } + mediaPeriods[i] = + new FakeMediaPeriodWithSelectTracksPosition( + new TrackGroupArray(trackGroups), new EventDispatcher()); + } + MergingMediaPeriod mergingMediaPeriod = + new MergingMediaPeriod( + new DefaultCompositeSequenceableLoaderFactory(), timeOffsetsUs, mediaPeriods); + + CountDownLatch prepareCountDown = new CountDownLatch(1); + mergingMediaPeriod.prepare( + new MediaPeriod.Callback() { + @Override + public void onPrepared(MediaPeriod mediaPeriod) { + prepareCountDown.countDown(); + } + + @Override + public void onContinueLoadingRequested(MediaPeriod source) { + mergingMediaPeriod.continueLoading(/* positionUs= */ 0); + } + }, + /* positionUs= */ 0); + prepareCountDown.await(); + + return mergingMediaPeriod; + } + + private static final class FakeMediaPeriodWithSelectTracksPosition extends FakeMediaPeriod { + + public long selectTracksPositionUs; + + public FakeMediaPeriodWithSelectTracksPosition( + TrackGroupArray trackGroupArray, EventDispatcher eventDispatcher) { + super(trackGroupArray, eventDispatcher); + selectTracksPositionUs = C.TIME_UNSET; + } + + @Override + public long selectTracks( + @NullableType TrackSelection[] selections, + boolean[] mayRetainStreamFlags, + @NullableType SampleStream[] streams, + boolean[] streamResetFlags, + long positionUs) { + selectTracksPositionUs = positionUs; + return super.selectTracks( + selections, mayRetainStreamFlags, streams, streamResetFlags, positionUs); + } + } + + private static final class MergingPeriodDefinition { + + public long timeOffsetUs; + public Format[] formats; + + public MergingPeriodDefinition(long timeOffsetUs, Format... formats) { + this.timeOffsetUs = timeOffsetUs; + this.formats = formats; + } + } +} 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 1434d28500..4d91b7a34c 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 @@ -37,7 +37,7 @@ import org.robolectric.annotation.LooperMode; public class MergingMediaSourceTest { @Test - public void testMergingDynamicTimelines() throws IOException { + public void mergingDynamicTimelines() throws IOException { FakeTimeline firstTimeline = new FakeTimeline(new TimelineWindowDefinition(true, true, C.TIME_UNSET)); FakeTimeline secondTimeline = @@ -46,14 +46,14 @@ public class MergingMediaSourceTest { } @Test - public void testMergingStaticTimelines() throws IOException { + public void mergingStaticTimelines() throws IOException { FakeTimeline firstTimeline = new FakeTimeline(new TimelineWindowDefinition(true, false, 20)); FakeTimeline secondTimeline = new FakeTimeline(new TimelineWindowDefinition(true, false, 10)); testMergingMediaSourcePrepare(firstTimeline, secondTimeline); } @Test - public void testMergingTimelinesWithDifferentPeriodCounts() throws IOException { + public void mergingTimelinesWithDifferentPeriodCounts() throws IOException { FakeTimeline firstTimeline = new FakeTimeline(new TimelineWindowDefinition(1, null)); FakeTimeline secondTimeline = new FakeTimeline(new TimelineWindowDefinition(2, null)); try { @@ -65,7 +65,7 @@ public class MergingMediaSourceTest { } @Test - public void testMergingMediaSourcePeriodCreation() throws Exception { + public void mergingMediaSourcePeriodCreation() throws Exception { FakeMediaSource[] mediaSources = new FakeMediaSource[2]; for (int i = 0; i < mediaSources.length; i++) { mediaSources[i] = new FakeMediaSource(new FakeTimeline(/* windowCount= */ 2)); diff --git a/library/core/src/test/java/com/google/android/exoplayer2/source/SampleQueueTest.java b/library/core/src/test/java/com/google/android/exoplayer2/source/SampleQueueTest.java index 441ac9e05a..41b953a0d2 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/source/SampleQueueTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/source/SampleQueueTest.java @@ -15,15 +15,18 @@ */ package com.google.android.exoplayer2.source; +import static com.google.android.exoplayer2.C.BUFFER_FLAG_ENCRYPTED; +import static com.google.android.exoplayer2.C.BUFFER_FLAG_KEY_FRAME; import static com.google.android.exoplayer2.C.RESULT_BUFFER_READ; import static com.google.android.exoplayer2.C.RESULT_FORMAT_READ; import static com.google.android.exoplayer2.C.RESULT_NOTHING_READ; -import static com.google.android.exoplayer2.source.SampleQueue.ADVANCE_FAILED; import static com.google.common.truth.Truth.assertThat; import static java.lang.Long.MIN_VALUE; import static java.util.Arrays.copyOfRange; +import static org.junit.Assert.assertArrayEquals; import static org.mockito.Mockito.when; +import android.os.Looper; import androidx.annotation.Nullable; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.C; @@ -33,14 +36,16 @@ import com.google.android.exoplayer2.decoder.DecoderInputBuffer; import com.google.android.exoplayer2.drm.DrmInitData; import com.google.android.exoplayer2.drm.DrmSession; import com.google.android.exoplayer2.drm.DrmSessionManager; -import com.google.android.exoplayer2.drm.ExoMediaCrypto; import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.testutil.TestUtil; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.DefaultAllocator; +import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.MediaSourceEventDispatcher; import com.google.android.exoplayer2.util.ParsableByteArray; import java.io.IOException; import java.util.Arrays; +import java.util.concurrent.atomic.AtomicReference; import org.junit.After; import org.junit.Assert; import org.junit.Before; @@ -55,17 +60,12 @@ public final class SampleQueueTest { private static final int ALLOCATION_SIZE = 16; - private static final Format FORMAT_1 = Format.createSampleFormat("1", "mimeType", 0); - private static final Format FORMAT_2 = Format.createSampleFormat("2", "mimeType", 0); - private static final Format FORMAT_1_COPY = Format.createSampleFormat("1", "mimeType", 0); - private static final Format FORMAT_SPLICED = Format.createSampleFormat("spliced", "mimeType", 0); + private static final Format FORMAT_1 = buildFormat(/* id= */ "1"); + private static final Format FORMAT_2 = buildFormat(/* id= */ "2"); + private static final Format FORMAT_1_COPY = buildFormat(/* id= */ "1"); + private static final Format FORMAT_SPLICED = buildFormat(/* id= */ "spliced"); private static final Format FORMAT_ENCRYPTED = - Format.createSampleFormat( - /* id= */ "encrypted", - "mimeType", - /* codecs= */ null, - /* bitrate= */ Format.NO_VALUE, - new DrmInitData()); + new Format.Builder().setId(/* id= */ "encrypted").setDrmInitData(new DrmInitData()).build(); private static final byte[] DATA = TestUtil.buildTestData(ALLOCATION_SIZE * 10); /* @@ -113,44 +113,46 @@ public final class SampleQueueTest { C.BUFFER_FLAG_KEY_FRAME, C.BUFFER_FLAG_ENCRYPTED, 0, C.BUFFER_FLAG_ENCRYPTED, }; private static final long[] ENCRYPTED_SAMPLE_TIMESTAMPS = new long[] {0, 1000, 2000, 3000}; - private static final Format[] ENCRYPTED_SAMPLES_FORMATS = + private static final Format[] ENCRYPTED_SAMPLE_FORMATS = new Format[] {FORMAT_ENCRYPTED, FORMAT_ENCRYPTED, FORMAT_1, FORMAT_ENCRYPTED}; /** Encrypted samples require the encryption preamble. */ - private static final int[] ENCRYPTED_SAMPLES_SIZES = new int[] {1, 3, 1, 3}; + private static final int[] ENCRYPTED_SAMPLE_SIZES = new int[] {1, 3, 1, 3}; - private static final int[] ENCRYPTED_SAMPLES_OFFSETS = new int[] {7, 4, 3, 0}; - private static final byte[] ENCRYPTED_SAMPLES_DATA = new byte[8]; - - static { - Arrays.fill(ENCRYPTED_SAMPLES_DATA, (byte) 1); - } + private static final int[] ENCRYPTED_SAMPLE_OFFSETS = new int[] {7, 4, 3, 0}; + private static final byte[] ENCRYPTED_SAMPLE_DATA = new byte[] {1, 1, 1, 1, 1, 1, 1, 1}; private static final TrackOutput.CryptoData DUMMY_CRYPTO_DATA = new TrackOutput.CryptoData(C.CRYPTO_MODE_AES_CTR, new byte[16], 0, 0); private Allocator allocator; - private DrmSessionManager mockDrmSessionManager; - private DrmSession mockDrmSession; + private DrmSessionManager mockDrmSessionManager; + private DrmSession mockDrmSession; + private MediaSourceEventDispatcher eventDispatcher; private SampleQueue sampleQueue; private FormatHolder formatHolder; private DecoderInputBuffer inputBuffer; @Before - @SuppressWarnings("unchecked") - public void setUp() throws Exception { + public void setUp() { allocator = new DefaultAllocator(false, ALLOCATION_SIZE); - mockDrmSessionManager = - (DrmSessionManager) Mockito.mock(DrmSessionManager.class); - mockDrmSession = (DrmSession) Mockito.mock(DrmSession.class); - when(mockDrmSessionManager.acquireSession(ArgumentMatchers.any(), ArgumentMatchers.any())) + mockDrmSessionManager = Mockito.mock(DrmSessionManager.class); + mockDrmSession = Mockito.mock(DrmSession.class); + when(mockDrmSessionManager.acquireSession( + ArgumentMatchers.any(), ArgumentMatchers.any(), ArgumentMatchers.any())) .thenReturn(mockDrmSession); - sampleQueue = new SampleQueue(allocator, mockDrmSessionManager); + eventDispatcher = new MediaSourceEventDispatcher(); + sampleQueue = + new SampleQueue( + allocator, + /* playbackLooper= */ Assertions.checkNotNull(Looper.myLooper()), + mockDrmSessionManager, + eventDispatcher); formatHolder = new FormatHolder(); inputBuffer = new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_NORMAL); } @After - public void tearDown() throws Exception { + public void tearDown() { allocator = null; sampleQueue = null; formatHolder = null; @@ -158,7 +160,35 @@ public final class SampleQueueTest { } @Test - public void testResetReleasesAllocations() { + public void capacityIncreases() { + int numberOfSamplesToInput = 3 * SampleQueue.SAMPLE_CAPACITY_INCREMENT + 1; + sampleQueue.format(FORMAT_1); + sampleQueue.sampleData( + new ParsableByteArray(numberOfSamplesToInput), /* length= */ numberOfSamplesToInput); + for (int i = 0; i < numberOfSamplesToInput; i++) { + sampleQueue.sampleMetadata( + /* timeUs= */ i * 1000, + /* flags= */ C.BUFFER_FLAG_KEY_FRAME, + /* size= */ 1, + /* offset= */ numberOfSamplesToInput - i - 1, + /* cryptoData= */ null); + } + + assertReadFormat(/* formatRequired= */ false, FORMAT_1); + for (int i = 0; i < numberOfSamplesToInput; i++) { + assertReadSample( + /* timeUs= */ i * 1000, + /* isKeyFrame= */ true, + /* isEncrypted= */ false, + /* sampleData= */ new byte[1], + /* offset= */ 0, + /* length= */ 1); + } + assertReadNothing(/* formatRequired= */ false); + } + + @Test + public void resetReleasesAllocations() { writeTestData(); assertAllocationCount(10); sampleQueue.reset(); @@ -166,12 +196,12 @@ public final class SampleQueueTest { } @Test - public void testReadWithoutWrite() { + public void readWithoutWrite() { assertNoSamplesToRead(null); } @Test - public void testEqualFormatsDeduplicated() { + public void equalFormatsDeduplicated() { sampleQueue.format(FORMAT_1); assertReadFormat(false, FORMAT_1); // If the same format is written then it should not cause a format change on the read side. @@ -183,7 +213,7 @@ public final class SampleQueueTest { } @Test - public void testMultipleFormatsDeduplicated() { + public void multipleFormatsDeduplicated() { sampleQueue.format(FORMAT_1); sampleQueue.sampleData(new ParsableByteArray(DATA), ALLOCATION_SIZE); sampleQueue.sampleMetadata(0, C.BUFFER_FLAG_KEY_FRAME, ALLOCATION_SIZE, 0, null); @@ -210,7 +240,7 @@ public final class SampleQueueTest { } @Test - public void testReadSingleSamples() { + public void readSingleSamples() { sampleQueue.sampleData(new ParsableByteArray(DATA), ALLOCATION_SIZE); assertAllocationCount(1); @@ -269,7 +299,7 @@ public final class SampleQueueTest { } @Test - public void testReadMultiSamples() { + public void readMultiSamples() { writeTestData(); assertThat(sampleQueue.getLargestQueuedTimestampUs()).isEqualTo(LAST_SAMPLE_TIMESTAMP); assertAllocationCount(10); @@ -280,7 +310,7 @@ public final class SampleQueueTest { } @Test - public void testReadMultiSamplesTwice() { + public void readMultiSamplesTwice() { writeTestData(); writeTestData(); assertAllocationCount(20); @@ -292,14 +322,14 @@ public final class SampleQueueTest { } @Test - public void testReadMultiWithRewind() { + public void readMultiWithSeek() { writeTestData(); assertReadTestData(); assertThat(sampleQueue.getFirstIndex()).isEqualTo(0); assertThat(sampleQueue.getReadIndex()).isEqualTo(8); assertAllocationCount(10); - // Rewind. - sampleQueue.rewind(); + + sampleQueue.seekTo(0); assertAllocationCount(10); // Read again. assertThat(sampleQueue.getFirstIndex()).isEqualTo(0); @@ -308,20 +338,20 @@ public final class SampleQueueTest { } @Test - public void testEmptyQueueReturnsLoadingFinished() { + public void emptyQueueReturnsLoadingFinished() { sampleQueue.sampleData(new ParsableByteArray(DATA), DATA.length); assertThat(sampleQueue.isReady(/* loadingFinished= */ false)).isFalse(); assertThat(sampleQueue.isReady(/* loadingFinished= */ true)).isTrue(); } @Test - public void testIsReadyWithUpstreamFormatOnlyReturnsTrue() { + public void isReadyWithUpstreamFormatOnlyReturnsTrue() { sampleQueue.format(FORMAT_ENCRYPTED); assertThat(sampleQueue.isReady(/* loadingFinished= */ false)).isTrue(); } @Test - public void testIsReadyReturnsTrueForValidDrmSession() { + public void isReadyReturnsTrueForValidDrmSession() { writeTestDataWithEncryptedSections(); assertReadFormat(/* formatRequired= */ false, FORMAT_ENCRYPTED); assertThat(sampleQueue.isReady(/* loadingFinished= */ false)).isFalse(); @@ -330,27 +360,34 @@ public final class SampleQueueTest { } @Test - public void testIsReadyReturnsTrueForClearSampleAndPlayClearSamplesWithoutKeysIsTrue() { + public void isReadyReturnsTrueForClearSampleAndPlayClearSamplesWithoutKeysIsTrue() { when(mockDrmSession.playClearSamplesWithoutKeys()).thenReturn(true); // We recreate the queue to ensure the mock DRM session manager flags are taken into account. - sampleQueue = new SampleQueue(allocator, mockDrmSessionManager); + sampleQueue = + new SampleQueue( + allocator, + /* playbackLooper= */ Assertions.checkNotNull(Looper.myLooper()), + mockDrmSessionManager, + eventDispatcher); writeTestDataWithEncryptedSections(); assertThat(sampleQueue.isReady(/* loadingFinished= */ false)).isTrue(); } @Test - public void testReadEncryptedSectionsWaitsForKeys() { + public void readEncryptedSectionsWaitsForKeys() { when(mockDrmSession.getState()).thenReturn(DrmSession.STATE_OPENED); writeTestDataWithEncryptedSections(); assertReadFormat(/* formatRequired= */ false, FORMAT_ENCRYPTED); assertReadNothing(/* formatRequired= */ false); + assertThat(inputBuffer.waitingForKeys).isTrue(); when(mockDrmSession.getState()).thenReturn(DrmSession.STATE_OPENED_WITH_KEYS); assertReadEncryptedSample(/* sampleIndex= */ 0); + assertThat(inputBuffer.waitingForKeys).isFalse(); } @Test - public void testReadEncryptedSectionsPopulatesDrmSession() { + public void readEncryptedSectionsPopulatesDrmSession() { when(mockDrmSession.getState()).thenReturn(DrmSession.STATE_OPENED_WITH_KEYS); writeTestDataWithEncryptedSections(); @@ -389,11 +426,9 @@ public final class SampleQueueTest { } @Test - @SuppressWarnings("unchecked") - public void testAllowPlaceholderSessionPopulatesDrmSession() { + public void allowPlaceholderSessionPopulatesDrmSession() { when(mockDrmSession.getState()).thenReturn(DrmSession.STATE_OPENED_WITH_KEYS); - DrmSession mockPlaceholderDrmSession = - (DrmSession) Mockito.mock(DrmSession.class); + DrmSession mockPlaceholderDrmSession = Mockito.mock(DrmSession.class); when(mockPlaceholderDrmSession.getState()).thenReturn(DrmSession.STATE_OPENED_WITH_KEYS); when(mockDrmSessionManager.acquirePlaceholderSession( ArgumentMatchers.any(), ArgumentMatchers.anyInt())) @@ -432,10 +467,62 @@ public final class SampleQueueTest { /* decodeOnlyUntilUs= */ 0); assertThat(result).isEqualTo(RESULT_FORMAT_READ); assertThat(formatHolder.drmSession).isSameInstanceAs(mockDrmSession); + assertReadEncryptedSample(/* sampleIndex= */ 3); } @Test - public void testReadWithErrorSessionReadsNothingAndThrows() throws IOException { + public void trailingCryptoInfoInitializationVectorBytesZeroed() { + when(mockDrmSession.getState()).thenReturn(DrmSession.STATE_OPENED_WITH_KEYS); + DrmSession mockPlaceholderDrmSession = Mockito.mock(DrmSession.class); + when(mockPlaceholderDrmSession.getState()).thenReturn(DrmSession.STATE_OPENED_WITH_KEYS); + when(mockDrmSessionManager.acquirePlaceholderSession( + ArgumentMatchers.any(), ArgumentMatchers.anyInt())) + .thenReturn(mockPlaceholderDrmSession); + + writeFormat(ENCRYPTED_SAMPLE_FORMATS[0]); + byte[] sampleData = new byte[] {0, 1, 2}; + byte[] initializationVector = new byte[] {7, 6, 5, 4, 3, 2, 1, 0}; + byte[] encryptedSampleData = + TestUtil.joinByteArrays( + new byte[] { + 0x08, // subsampleEncryption = false (1 bit), ivSize = 8 (7 bits). + }, + initializationVector, + sampleData); + writeSample( + encryptedSampleData, /* timestampUs= */ 0, BUFFER_FLAG_KEY_FRAME | BUFFER_FLAG_ENCRYPTED); + + int result = + sampleQueue.read( + formatHolder, + inputBuffer, + /* formatRequired= */ false, + /* loadingFinished= */ false, + /* decodeOnlyUntilUs= */ 0); + assertThat(result).isEqualTo(RESULT_FORMAT_READ); + + // Fill cryptoInfo.iv with non-zero data. When the 8 byte initialization vector is written into + // it, we expect the trailing 8 bytes to be zeroed. + inputBuffer.cryptoInfo.iv = new byte[16]; + Arrays.fill(inputBuffer.cryptoInfo.iv, (byte) 1); + + result = + sampleQueue.read( + formatHolder, + inputBuffer, + /* formatRequired= */ false, + /* loadingFinished= */ false, + /* decodeOnlyUntilUs= */ 0); + assertThat(result).isEqualTo(RESULT_BUFFER_READ); + + // Assert cryptoInfo.iv contains the 8-byte initialization vector and that the trailing 8 bytes + // have been zeroed. + byte[] expectedInitializationVector = Arrays.copyOf(initializationVector, 16); + assertArrayEquals(expectedInitializationVector, inputBuffer.cryptoInfo.iv); + } + + @Test + public void readWithErrorSessionReadsNothingAndThrows() throws IOException { when(mockDrmSession.getState()).thenReturn(DrmSession.STATE_OPENED); writeTestDataWithEncryptedSections(); @@ -456,10 +543,15 @@ public final class SampleQueueTest { } @Test - public void testAllowPlayClearSamplesWithoutKeysReadsClearSamples() { + public void allowPlayClearSamplesWithoutKeysReadsClearSamples() { when(mockDrmSession.playClearSamplesWithoutKeys()).thenReturn(true); // We recreate the queue to ensure the mock DRM session manager flags are taken into account. - sampleQueue = new SampleQueue(allocator, mockDrmSessionManager); + sampleQueue = + new SampleQueue( + allocator, + /* playbackLooper= */ Assertions.checkNotNull(Looper.myLooper()), + mockDrmSessionManager, + eventDispatcher); when(mockDrmSession.getState()).thenReturn(DrmSession.STATE_OPENED); writeTestDataWithEncryptedSections(); @@ -468,15 +560,15 @@ public final class SampleQueueTest { } @Test - public void testRewindAfterDiscard() { + public void seekAfterDiscard() { writeTestData(); assertReadTestData(); sampleQueue.discardToRead(); assertThat(sampleQueue.getFirstIndex()).isEqualTo(8); assertThat(sampleQueue.getReadIndex()).isEqualTo(8); assertAllocationCount(0); - // Rewind. - sampleQueue.rewind(); + + sampleQueue.seekTo(0); assertAllocationCount(0); // Can't read again. assertThat(sampleQueue.getFirstIndex()).isEqualTo(8); @@ -485,7 +577,7 @@ public final class SampleQueueTest { } @Test - public void testAdvanceToEnd() { + public void advanceToEnd() { writeTestData(); sampleQueue.advanceToEnd(); assertAllocationCount(10); @@ -499,7 +591,7 @@ public final class SampleQueueTest { } @Test - public void testAdvanceToEndRetainsUnassignedData() { + public void advanceToEndRetainsUnassignedData() { sampleQueue.format(FORMAT_1); sampleQueue.sampleData(new ParsableByteArray(DATA), ALLOCATION_SIZE); sampleQueue.advanceToEnd(); @@ -523,57 +615,113 @@ public final class SampleQueueTest { } @Test - public void testAdvanceToBeforeBuffer() { + public void advanceToBeforeBuffer() { writeTestData(); - int skipCount = sampleQueue.advanceTo(SAMPLE_TIMESTAMPS[0] - 1, true, false); - // Should fail and have no effect. - assertThat(skipCount).isEqualTo(ADVANCE_FAILED); - assertReadTestData(); - assertNoSamplesToRead(FORMAT_2); - } - - @Test - public void testAdvanceToStartOfBuffer() { - writeTestData(); - int skipCount = sampleQueue.advanceTo(SAMPLE_TIMESTAMPS[0], true, false); - // Should succeed but have no effect (we're already at the first frame). + int skipCount = sampleQueue.advanceTo(SAMPLE_TIMESTAMPS[0] - 1); + // Should have no effect (we're already at the first frame). assertThat(skipCount).isEqualTo(0); assertReadTestData(); assertNoSamplesToRead(FORMAT_2); } @Test - public void testAdvanceToEndOfBuffer() { + public void advanceToStartOfBuffer() { writeTestData(); - int skipCount = sampleQueue.advanceTo(LAST_SAMPLE_TIMESTAMP, true, false); - // Should succeed and skip to 2nd keyframe (the 4th frame). - assertThat(skipCount).isEqualTo(4); - assertReadTestData(null, DATA_SECOND_KEYFRAME_INDEX); - assertNoSamplesToRead(FORMAT_2); - } - - @Test - public void testAdvanceToAfterBuffer() { - writeTestData(); - int skipCount = sampleQueue.advanceTo(LAST_SAMPLE_TIMESTAMP + 1, true, false); - // Should fail and have no effect. - assertThat(skipCount).isEqualTo(ADVANCE_FAILED); + int skipCount = sampleQueue.advanceTo(SAMPLE_TIMESTAMPS[0]); + // Should have no effect (we're already at the first frame). + assertThat(skipCount).isEqualTo(0); assertReadTestData(); assertNoSamplesToRead(FORMAT_2); } @Test - public void testAdvanceToAfterBufferAllowed() { + public void advanceToEndOfBuffer() { writeTestData(); - int skipCount = sampleQueue.advanceTo(LAST_SAMPLE_TIMESTAMP + 1, true, true); - // Should succeed and skip to 2nd keyframe (the 4th frame). + int skipCount = sampleQueue.advanceTo(LAST_SAMPLE_TIMESTAMP); + // Should advance to 2nd keyframe (the 4th frame). assertThat(skipCount).isEqualTo(4); assertReadTestData(null, DATA_SECOND_KEYFRAME_INDEX); assertNoSamplesToRead(FORMAT_2); } @Test - public void testDiscardToEnd() { + public void advanceToAfterBuffer() { + writeTestData(); + int skipCount = sampleQueue.advanceTo(LAST_SAMPLE_TIMESTAMP + 1); + // Should advance to 2nd keyframe (the 4th frame). + assertThat(skipCount).isEqualTo(4); + assertReadTestData(null, DATA_SECOND_KEYFRAME_INDEX); + assertNoSamplesToRead(FORMAT_2); + } + + @Test + public void seekToBeforeBuffer() { + writeTestData(); + boolean success = sampleQueue.seekTo(SAMPLE_TIMESTAMPS[0] - 1, false); + assertThat(success).isFalse(); + assertThat(sampleQueue.getReadIndex()).isEqualTo(0); + assertReadTestData(); + assertNoSamplesToRead(FORMAT_2); + } + + @Test + public void seekToStartOfBuffer() { + writeTestData(); + boolean success = sampleQueue.seekTo(SAMPLE_TIMESTAMPS[0], false); + assertThat(success).isTrue(); + assertThat(sampleQueue.getReadIndex()).isEqualTo(0); + assertReadTestData(); + assertNoSamplesToRead(FORMAT_2); + } + + @Test + public void seekToEndOfBuffer() { + writeTestData(); + boolean success = sampleQueue.seekTo(LAST_SAMPLE_TIMESTAMP, false); + assertThat(success).isTrue(); + assertThat(sampleQueue.getReadIndex()).isEqualTo(4); + assertReadTestData(null, DATA_SECOND_KEYFRAME_INDEX); + assertNoSamplesToRead(FORMAT_2); + } + + @Test + public void seekToAfterBuffer() { + writeTestData(); + boolean success = sampleQueue.seekTo(LAST_SAMPLE_TIMESTAMP + 1, false); + assertThat(success).isFalse(); + assertThat(sampleQueue.getReadIndex()).isEqualTo(0); + assertReadTestData(); + assertNoSamplesToRead(FORMAT_2); + } + + @Test + public void seekToAfterBufferAllowed() { + writeTestData(); + boolean success = sampleQueue.seekTo(LAST_SAMPLE_TIMESTAMP + 1, true); + assertThat(success).isTrue(); + assertThat(sampleQueue.getReadIndex()).isEqualTo(4); + assertReadTestData(null, DATA_SECOND_KEYFRAME_INDEX); + assertNoSamplesToRead(FORMAT_2); + } + + @Test + public void seekToEndAndBackToStart() { + writeTestData(); + boolean success = sampleQueue.seekTo(LAST_SAMPLE_TIMESTAMP, false); + assertThat(success).isTrue(); + assertThat(sampleQueue.getReadIndex()).isEqualTo(4); + assertReadTestData(null, DATA_SECOND_KEYFRAME_INDEX); + assertNoSamplesToRead(FORMAT_2); + // Seek back to the start. + success = sampleQueue.seekTo(SAMPLE_TIMESTAMPS[0], false); + assertThat(success).isTrue(); + assertThat(sampleQueue.getReadIndex()).isEqualTo(0); + assertReadTestData(); + assertNoSamplesToRead(FORMAT_2); + } + + @Test + public void discardToEnd() { writeTestData(); // Should discard everything. sampleQueue.discardToEnd(); @@ -588,7 +736,7 @@ public final class SampleQueueTest { } @Test - public void testDiscardToStopAtReadPosition() { + public void discardToStopAtReadPosition() { writeTestData(); // Shouldn't discard anything. sampleQueue.discardTo(LAST_SAMPLE_TIMESTAMP, false, true); @@ -629,7 +777,7 @@ public final class SampleQueueTest { } @Test - public void testDiscardToDontStopAtReadPosition() { + public void discardToDontStopAtReadPosition() { writeTestData(); // Shouldn't discard anything. sampleQueue.discardTo(SAMPLE_TIMESTAMPS[1] - 1, false, false); @@ -646,7 +794,7 @@ public final class SampleQueueTest { } @Test - public void testDiscardUpstream() { + public void discardUpstream() { writeTestData(); sampleQueue.discardUpstreamSamples(8); assertAllocationCount(10); @@ -671,7 +819,7 @@ public final class SampleQueueTest { } @Test - public void testDiscardUpstreamMulti() { + public void discardUpstreamMulti() { writeTestData(); sampleQueue.discardUpstreamSamples(4); assertAllocationCount(4); @@ -682,7 +830,7 @@ public final class SampleQueueTest { } @Test - public void testDiscardUpstreamBeforeRead() { + public void discardUpstreamBeforeRead() { writeTestData(); sampleQueue.discardUpstreamSamples(4); assertAllocationCount(4); @@ -692,7 +840,7 @@ public final class SampleQueueTest { } @Test - public void testDiscardUpstreamAfterRead() { + public void discardUpstreamAfterRead() { writeTestData(); assertReadTestData(null, 0, 3); sampleQueue.discardUpstreamSamples(8); @@ -714,7 +862,7 @@ public final class SampleQueueTest { } @Test - public void testLargestQueuedTimestampWithDiscardUpstream() { + public void largestQueuedTimestampWithDiscardUpstream() { writeTestData(); assertThat(sampleQueue.getLargestQueuedTimestampUs()).isEqualTo(LAST_SAMPLE_TIMESTAMP); sampleQueue.discardUpstreamSamples(SAMPLE_TIMESTAMPS.length - 1); @@ -727,7 +875,7 @@ public final class SampleQueueTest { } @Test - public void testLargestQueuedTimestampWithDiscardUpstreamDecodeOrder() { + public void largestQueuedTimestampWithDiscardUpstreamDecodeOrder() { long[] decodeOrderTimestamps = new long[] {0, 3000, 2000, 1000, 4000, 7000, 6000, 5000}; writeTestData( DATA, SAMPLE_SIZES, SAMPLE_OFFSETS, decodeOrderTimestamps, SAMPLE_FORMATS, SAMPLE_FLAGS); @@ -743,9 +891,9 @@ public final class SampleQueueTest { // Discarding everything from upstream without reading should unset the largest timestamp. assertThat(sampleQueue.getLargestQueuedTimestampUs()).isEqualTo(MIN_VALUE); } - + @Test - public void testLargestQueuedTimestampWithRead() { + public void largestQueuedTimestampWithRead() { writeTestData(); assertThat(sampleQueue.getLargestQueuedTimestampUs()).isEqualTo(LAST_SAMPLE_TIMESTAMP); assertReadTestData(); @@ -754,21 +902,112 @@ public final class SampleQueueTest { } @Test - public void testSetSampleOffset() { + public void setSampleOffsetBeforeData() { long sampleOffsetUs = 1000; sampleQueue.setSampleOffsetUs(sampleOffsetUs); writeTestData(); - assertReadTestData(null, 0, 8, sampleOffsetUs); - assertReadEndOfStream(false); + assertReadTestData( + /* startFormat= */ null, /* firstSampleIndex= */ 0, /* sampleCount= */ 8, sampleOffsetUs); + assertReadEndOfStream(/* formatRequired= */ false); } @Test - public void testSplice() { + public void setSampleOffsetBetweenSamples() { + writeTestData(); + long sampleOffsetUs = 1000; + sampleQueue.setSampleOffsetUs(sampleOffsetUs); + + // Write a final sample now the offset is set. + long unadjustedTimestampUs = LAST_SAMPLE_TIMESTAMP + 1234; + writeSample(DATA, unadjustedTimestampUs, /* sampleFlags= */ 0); + + assertReadTestData(); + // We expect to read the format adjusted to account for the sample offset, followed by the final + // sample and then the end of stream. + assertReadFormat( + /* formatRequired= */ false, + FORMAT_2.buildUpon().setSubsampleOffsetUs(sampleOffsetUs).build()); + assertReadSample( + unadjustedTimestampUs + sampleOffsetUs, + /* isKeyFrame= */ false, + /* isEncrypted= */ false, + DATA, + /* offset= */ 0, + DATA.length); + assertReadEndOfStream(/* formatRequired= */ false); + } + + @Test + public void adjustUpstreamFormat() { + String label = "label"; + sampleQueue = + new SampleQueue( + allocator, + /* playbackLooper= */ Assertions.checkNotNull(Looper.myLooper()), + mockDrmSessionManager, + eventDispatcher) { + @Override + public Format getAdjustedUpstreamFormat(Format format) { + return super.getAdjustedUpstreamFormat(copyWithLabel(format, label)); + } + }; + + writeFormat(FORMAT_1); + assertReadFormat(/* formatRequired= */ false, copyWithLabel(FORMAT_1, label)); + assertReadEndOfStream(/* formatRequired= */ false); + } + + @Test + public void invalidateUpstreamFormatAdjustment() { + AtomicReference label = new AtomicReference<>("label1"); + sampleQueue = + new SampleQueue( + allocator, + /* playbackLooper= */ Assertions.checkNotNull(Looper.myLooper()), + mockDrmSessionManager, + eventDispatcher) { + @Override + public Format getAdjustedUpstreamFormat(Format format) { + return super.getAdjustedUpstreamFormat(copyWithLabel(format, label.get())); + } + }; + + writeFormat(FORMAT_1); + writeSample(DATA, /* timestampUs= */ 0, BUFFER_FLAG_KEY_FRAME); + + // Make a change that'll affect the SampleQueue's format adjustment, and invalidate it. + label.set("label2"); + sampleQueue.invalidateUpstreamFormatAdjustment(); + + writeSample(DATA, /* timestampUs= */ 1, /* sampleFlags= */ 0); + + assertReadFormat(/* formatRequired= */ false, copyWithLabel(FORMAT_1, "label1")); + assertReadSample( + /* timeUs= */ 0, + /* isKeyFrame= */ true, + /* isEncrypted= */ false, + DATA, + /* offset= */ 0, + DATA.length); + assertReadFormat(/* formatRequired= */ false, copyWithLabel(FORMAT_1, "label2")); + assertReadSample( + /* timeUs= */ 1, + /* isKeyFrame= */ false, + /* isEncrypted= */ false, + DATA, + /* offset= */ 0, + DATA.length); + assertReadEndOfStream(/* formatRequired= */ false); + } + + @Test + public void splice() { writeTestData(); sampleQueue.splice(); // Splice should succeed, replacing the last 4 samples with the sample being written. long spliceSampleTimeUs = SAMPLE_TIMESTAMPS[4]; - writeSample(DATA, spliceSampleTimeUs, FORMAT_SPLICED, C.BUFFER_FLAG_KEY_FRAME); + writeFormat(FORMAT_SPLICED); + writeSample(DATA, spliceSampleTimeUs, C.BUFFER_FLAG_KEY_FRAME); assertReadTestData(null, 0, 4); assertReadFormat(false, FORMAT_SPLICED); assertReadSample(spliceSampleTimeUs, true, /* isEncrypted= */ false, DATA, 0, DATA.length); @@ -776,38 +1015,42 @@ public final class SampleQueueTest { } @Test - public void testSpliceAfterRead() { + public void spliceAfterRead() { writeTestData(); assertReadTestData(null, 0, 4); sampleQueue.splice(); // Splice should fail, leaving the last 4 samples unchanged. long spliceSampleTimeUs = SAMPLE_TIMESTAMPS[3]; - writeSample(DATA, spliceSampleTimeUs, FORMAT_SPLICED, C.BUFFER_FLAG_KEY_FRAME); + writeFormat(FORMAT_SPLICED); + writeSample(DATA, spliceSampleTimeUs, C.BUFFER_FLAG_KEY_FRAME); assertReadTestData(SAMPLE_FORMATS[3], 4, 4); assertReadEndOfStream(false); - sampleQueue.rewind(); + sampleQueue.seekTo(0); assertReadTestData(null, 0, 4); sampleQueue.splice(); // Splice should succeed, replacing the last 4 samples with the sample being written spliceSampleTimeUs = SAMPLE_TIMESTAMPS[3] + 1; - writeSample(DATA, spliceSampleTimeUs, FORMAT_SPLICED, C.BUFFER_FLAG_KEY_FRAME); + writeFormat(FORMAT_SPLICED); + writeSample(DATA, spliceSampleTimeUs, C.BUFFER_FLAG_KEY_FRAME); assertReadFormat(false, FORMAT_SPLICED); assertReadSample(spliceSampleTimeUs, true, /* isEncrypted= */ false, DATA, 0, DATA.length); assertReadEndOfStream(false); } @Test - public void testSpliceWithSampleOffset() { + public void spliceWithSampleOffset() { long sampleOffsetUs = 30000; sampleQueue.setSampleOffsetUs(sampleOffsetUs); writeTestData(); sampleQueue.splice(); // Splice should succeed, replacing the last 4 samples with the sample being written. long spliceSampleTimeUs = SAMPLE_TIMESTAMPS[4]; - writeSample(DATA, spliceSampleTimeUs, FORMAT_SPLICED, C.BUFFER_FLAG_KEY_FRAME); + writeFormat(FORMAT_SPLICED); + writeSample(DATA, spliceSampleTimeUs, C.BUFFER_FLAG_KEY_FRAME); assertReadTestData(null, 0, 4, sampleOffsetUs); - assertReadFormat(false, FORMAT_SPLICED.copyWithSubsampleOffsetUs(sampleOffsetUs)); + assertReadFormat( + false, FORMAT_SPLICED.buildUpon().setSubsampleOffsetUs(sampleOffsetUs).build()); assertReadSample( spliceSampleTimeUs + sampleOffsetUs, true, /* isEncrypted= */ false, DATA, 0, DATA.length); assertReadEndOfStream(false); @@ -825,11 +1068,11 @@ public final class SampleQueueTest { private void writeTestDataWithEncryptedSections() { writeTestData( - ENCRYPTED_SAMPLES_DATA, - ENCRYPTED_SAMPLES_SIZES, - ENCRYPTED_SAMPLES_OFFSETS, + ENCRYPTED_SAMPLE_DATA, + ENCRYPTED_SAMPLE_SIZES, + ENCRYPTED_SAMPLE_OFFSETS, ENCRYPTED_SAMPLE_TIMESTAMPS, - ENCRYPTED_SAMPLES_FORMATS, + ENCRYPTED_SAMPLE_FORMATS, ENCRYPTED_SAMPLES_FLAGS); } @@ -855,11 +1098,20 @@ public final class SampleQueueTest { } } - /** Writes a single sample to {@code sampleQueue}. */ - private void writeSample(byte[] data, long timestampUs, Format format, int sampleFlags) { + /** Writes a {@link Format} to the {@code sampleQueue}. */ + private void writeFormat(Format format) { sampleQueue.format(format); + } + + /** Writes a single sample to {@code sampleQueue}. */ + private void writeSample(byte[] data, long timestampUs, int sampleFlags) { sampleQueue.sampleData(new ParsableByteArray(data), data.length); - sampleQueue.sampleMetadata(timestampUs, sampleFlags, data.length, 0, null); + sampleQueue.sampleMetadata( + timestampUs, + sampleFlags, + data.length, + /* offset= */ 0, + (sampleFlags & C.BUFFER_FLAG_ENCRYPTED) != 0 ? DUMMY_CRYPTO_DATA : null); } /** @@ -1032,7 +1284,7 @@ public final class SampleQueueTest { } private void assertReadEncryptedSample(int sampleIndex) { - byte[] sampleData = new byte[ENCRYPTED_SAMPLES_SIZES[sampleIndex]]; + byte[] sampleData = new byte[ENCRYPTED_SAMPLE_SIZES[sampleIndex]]; Arrays.fill(sampleData, (byte) 1); boolean isKeyFrame = (ENCRYPTED_SAMPLES_FLAGS[sampleIndex] & C.BUFFER_FLAG_KEY_FRAME) != 0; boolean isEncrypted = (ENCRYPTED_SAMPLES_FLAGS[sampleIndex] & C.BUFFER_FLAG_ENCRYPTED) != 0; @@ -1042,7 +1294,7 @@ public final class SampleQueueTest { isEncrypted, sampleData, /* offset= */ 0, - ENCRYPTED_SAMPLES_SIZES[sampleIndex] - (isEncrypted ? 2 : 0)); + ENCRYPTED_SAMPLE_SIZES[sampleIndex] - (isEncrypted ? 2 : 0)); } /** @@ -1050,7 +1302,7 @@ public final class SampleQueueTest { * filled with the specified sample data. * * @param timeUs The expected buffer timestamp. - * @param isKeyframe The expected keyframe flag. + * @param isKeyFrame The expected keyframe flag. * @param isEncrypted The expected encrypted flag. * @param sampleData An array containing the expected sample data. * @param offset The offset in {@code sampleData} of the expected sample data. @@ -1058,7 +1310,7 @@ public final class SampleQueueTest { */ private void assertReadSample( long timeUs, - boolean isKeyframe, + boolean isKeyFrame, boolean isEncrypted, byte[] sampleData, int offset, @@ -1076,7 +1328,7 @@ public final class SampleQueueTest { assertThat(formatHolder.format).isNull(); // inputBuffer should be populated. assertThat(inputBuffer.timeUs).isEqualTo(timeUs); - assertThat(inputBuffer.isKeyFrame()).isEqualTo(isKeyframe); + assertThat(inputBuffer.isKeyFrame()).isEqualTo(isKeyFrame); assertThat(inputBuffer.isDecodeOnly()).isFalse(); assertThat(inputBuffer.isEncrypted()).isEqualTo(isEncrypted); inputBuffer.flip(); @@ -1086,18 +1338,6 @@ public final class SampleQueueTest { assertThat(readData).isEqualTo(copyOfRange(sampleData, offset, offset + length)); } - /** Asserts {@link SampleQueue#read} returns the given result. */ - private void assertResult(int expectedResult, boolean allowOnlyClearBuffers) { - int obtainedResult = - sampleQueue.read( - formatHolder, - inputBuffer, - allowOnlyClearBuffers, - /* loadingFinished= */ false, - /* decodeOnlyUntilUs= */ 0); - assertThat(obtainedResult).isEqualTo(expectedResult); - } - /** * Asserts the number of allocations currently in use by {@code sampleQueue}. * @@ -1132,6 +1372,14 @@ public final class SampleQueueTest { private static Format adjustFormat(@Nullable Format format, long sampleOffsetUs) { return format == null || sampleOffsetUs == 0 ? format - : format.copyWithSubsampleOffsetUs(sampleOffsetUs); + : format.buildUpon().setSubsampleOffsetUs(sampleOffsetUs).build(); + } + + private static Format buildFormat(String id) { + return new Format.Builder().setId(id).setSubsampleOffsetUs(0).build(); + } + + private static Format copyWithLabel(Format format, String label) { + return format.buildUpon().setLabel(label).build(); } } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/source/ShuffleOrderTest.java b/library/core/src/test/java/com/google/android/exoplayer2/source/ShuffleOrderTest.java index 17b0996387..bfa4dbd049 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/source/ShuffleOrderTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/source/ShuffleOrderTest.java @@ -32,7 +32,7 @@ public final class ShuffleOrderTest { public static final long RANDOM_SEED = 1234567890L; @Test - public void testDefaultShuffleOrder() { + public void defaultShuffleOrder() { assertShuffleOrderCorrectness(new DefaultShuffleOrder(0, RANDOM_SEED), 0); assertShuffleOrderCorrectness(new DefaultShuffleOrder(1, RANDOM_SEED), 1); assertShuffleOrderCorrectness(new DefaultShuffleOrder(5, RANDOM_SEED), 5); @@ -55,7 +55,7 @@ public final class ShuffleOrderTest { } @Test - public void testDefaultShuffleOrderSideloaded() { + public void defaultShuffleOrderSideloaded() { int[] shuffledIndices = new int[] {2, 1, 0, 4, 3}; ShuffleOrder shuffleOrder = new DefaultShuffleOrder(shuffledIndices, RANDOM_SEED); assertThat(shuffleOrder.getFirstIndex()).isEqualTo(2); @@ -72,7 +72,7 @@ public final class ShuffleOrderTest { } @Test - public void testUnshuffledShuffleOrder() { + public void unshuffledShuffleOrder() { assertShuffleOrderCorrectness(new UnshuffledShuffleOrder(0), 0); assertShuffleOrderCorrectness(new UnshuffledShuffleOrder(1), 1); assertShuffleOrderCorrectness(new UnshuffledShuffleOrder(5), 5); @@ -95,7 +95,7 @@ public final class ShuffleOrderTest { } @Test - public void testUnshuffledShuffleOrderIsUnshuffled() { + public void unshuffledShuffleOrderIsUnshuffled() { ShuffleOrder shuffleOrder = new UnshuffledShuffleOrder(5); assertThat(shuffleOrder.getFirstIndex()).isEqualTo(0); assertThat(shuffleOrder.getLastIndex()).isEqualTo(4); diff --git a/library/core/src/test/java/com/google/android/exoplayer2/source/SinglePeriodTimelineTest.java b/library/core/src/test/java/com/google/android/exoplayer2/source/SinglePeriodTimelineTest.java index 6ff4f78fa2..fe4255c631 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/source/SinglePeriodTimelineTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/source/SinglePeriodTimelineTest.java @@ -40,7 +40,7 @@ public final class SinglePeriodTimelineTest { } @Test - public void testGetPeriodPositionDynamicWindowUnknownDuration() { + public void getPeriodPositionDynamicWindowUnknownDuration() { SinglePeriodTimeline timeline = new SinglePeriodTimeline( C.TIME_UNSET, /* isSeekable= */ false, /* isDynamic= */ true, /* isLive= */ true); @@ -54,7 +54,7 @@ public final class SinglePeriodTimelineTest { } @Test - public void testGetPeriodPositionDynamicWindowKnownDuration() { + public void getPeriodPositionDynamicWindowKnownDuration() { long windowDurationUs = 1000; SinglePeriodTimeline timeline = new SinglePeriodTimeline( diff --git a/library/core/src/test/java/com/google/android/exoplayer2/source/TrackGroupArrayTest.java b/library/core/src/test/java/com/google/android/exoplayer2/source/TrackGroupArrayTest.java index 36adc97147..75653e86b6 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/source/TrackGroupArrayTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/source/TrackGroupArrayTest.java @@ -29,10 +29,11 @@ import org.junit.runner.RunWith; public final class TrackGroupArrayTest { @Test - public void testParcelable() { - Format format1 = Format.createSampleFormat("1", MimeTypes.VIDEO_H264, 0); - Format format2 = Format.createSampleFormat("2", MimeTypes.AUDIO_AAC, 0); - Format format3 = Format.createSampleFormat("3", MimeTypes.VIDEO_H264, 0); + public void parcelable() { + Format.Builder formatBuilder = new Format.Builder(); + Format format1 = formatBuilder.setSampleMimeType(MimeTypes.VIDEO_H264).build(); + Format format2 = formatBuilder.setSampleMimeType(MimeTypes.AUDIO_AAC).build(); + Format format3 = formatBuilder.setSampleMimeType(MimeTypes.VIDEO_H264).build(); TrackGroup trackGroup1 = new TrackGroup(format1, format2); TrackGroup trackGroup2 = new TrackGroup(format3); diff --git a/library/core/src/test/java/com/google/android/exoplayer2/source/TrackGroupTest.java b/library/core/src/test/java/com/google/android/exoplayer2/source/TrackGroupTest.java index 4de1f8eb84..ba42463279 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/source/TrackGroupTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/source/TrackGroupTest.java @@ -29,9 +29,10 @@ import org.junit.runner.RunWith; public final class TrackGroupTest { @Test - public void testParcelable() { - Format format1 = Format.createSampleFormat("1", MimeTypes.VIDEO_H264, 0); - Format format2 = Format.createSampleFormat("2", MimeTypes.AUDIO_AAC, 0); + public void parcelable() { + Format.Builder formatBuilder = new Format.Builder(); + Format format1 = formatBuilder.setSampleMimeType(MimeTypes.VIDEO_H264).build(); + Format format2 = formatBuilder.setSampleMimeType(MimeTypes.AUDIO_AAC).build(); TrackGroup trackGroupToParcel = new TrackGroup(format1, format2); 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 0cd27a90c0..5b7713a835 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 @@ -40,14 +40,14 @@ public final class AdPlaybackStateTest { } @Test - public void testSetAdCount() { + public void setAdCount() { assertThat(state.adGroups[0].count).isEqualTo(C.LENGTH_UNSET); state = state.withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 1); assertThat(state.adGroups[0].count).isEqualTo(1); } @Test - public void testSetAdUriBeforeAdCount() { + public void setAdUriBeforeAdCount() { state = state.withAdUri(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 1, TEST_URI); state = state.withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 2); @@ -58,7 +58,7 @@ public final class AdPlaybackStateTest { } @Test - public void testSetAdErrorBeforeAdCount() { + public void setAdErrorBeforeAdCount() { state = state.withAdLoadError(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0); state = state.withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 2); @@ -68,7 +68,7 @@ public final class AdPlaybackStateTest { } @Test - public void testGetFirstAdIndexToPlayIsZero() { + public void getFirstAdIndexToPlayIsZero() { 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); @@ -77,7 +77,7 @@ public final class AdPlaybackStateTest { } @Test - public void testGetFirstAdIndexToPlaySkipsPlayedAd() { + public void getFirstAdIndexToPlaySkipsPlayedAd() { 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); @@ -90,7 +90,7 @@ public final class AdPlaybackStateTest { } @Test - public void testGetFirstAdIndexToPlaySkipsSkippedAd() { + public void getFirstAdIndexToPlaySkipsSkippedAd() { 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); @@ -103,7 +103,7 @@ public final class AdPlaybackStateTest { } @Test - public void testGetFirstAdIndexToPlaySkipsErrorAds() { + public void getFirstAdIndexToPlaySkipsErrorAds() { 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); @@ -115,7 +115,7 @@ public final class AdPlaybackStateTest { } @Test - public void testGetNextAdIndexToPlaySkipsErrorAds() { + public void getNextAdIndexToPlaySkipsErrorAds() { state = state.withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 3); state = state.withAdUri(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 1, TEST_URI); @@ -125,7 +125,7 @@ public final class AdPlaybackStateTest { } @Test - public void testSetAdStateTwiceThrows() { + public void setAdStateTwiceThrows() { state = state.withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 1); state = state.withPlayedAd(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0); try { @@ -137,7 +137,7 @@ public final class AdPlaybackStateTest { } @Test - public void testSkipAllWithoutAdCount() { + public void skipAllWithoutAdCount() { state = state.withSkippedAdGroup(0); state = state.withSkippedAdGroup(1); assertThat(state.adGroups[0].count).isEqualTo(0); diff --git a/library/core/src/test/java/com/google/android/exoplayer2/source/ads/AdsMediaSourceTest.java b/library/core/src/test/java/com/google/android/exoplayer2/source/ads/AdsMediaSourceTest.java new file mode 100644 index 0000000000..255d1298b7 --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/source/ads/AdsMediaSourceTest.java @@ -0,0 +1,216 @@ +/* + * Copyright 2020 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.ads; + +import static com.google.common.truth.Truth.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.robolectric.Shadows.shadowOf; +import static org.robolectric.annotation.LooperMode.Mode.PAUSED; + +import android.net.Uri; +import android.os.Looper; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.source.MediaPeriod; +import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; +import com.google.android.exoplayer2.source.MediaSource.MediaSourceCaller; +import com.google.android.exoplayer2.source.MediaSourceFactory; +import com.google.android.exoplayer2.source.SinglePeriodTimeline; +import com.google.android.exoplayer2.source.ads.AdsLoader.AdViewProvider; +import com.google.android.exoplayer2.source.ads.AdsLoader.EventListener; +import com.google.android.exoplayer2.testutil.FakeMediaSource; +import com.google.android.exoplayer2.upstream.Allocator; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; +import org.robolectric.annotation.LooperMode; + +/** Unit tests for {@link AdsMediaSource}. */ +@RunWith(AndroidJUnit4.class) +@LooperMode(PAUSED) +public final class AdsMediaSourceTest { + + private static final long PREROLL_AD_DURATION_US = 10 * C.MICROS_PER_SECOND; + private static final Timeline PREROLL_AD_TIMELINE = + new SinglePeriodTimeline( + PREROLL_AD_DURATION_US, + /* isSeekable= */ true, + /* isDynamic= */ false, + /* isLive= */ false); + private static final Object PREROLL_AD_PERIOD_UID = + PREROLL_AD_TIMELINE.getUidOfPeriod(/* periodIndex= */ 0); + + private static final long CONTENT_DURATION_US = 30 * C.MICROS_PER_SECOND; + private static final Timeline CONTENT_TIMELINE = + new SinglePeriodTimeline( + CONTENT_DURATION_US, /* isSeekable= */ true, /* isDynamic= */ false, /* isLive= */ false); + private static final Object CONTENT_PERIOD_UID = + CONTENT_TIMELINE.getUidOfPeriod(/* periodIndex= */ 0); + + private static final AdPlaybackState AD_PLAYBACK_STATE = + new AdPlaybackState(/* adGroupTimesUs...= */ 0) + .withContentDurationUs(CONTENT_DURATION_US) + .withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 1) + .withAdUri(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0, Uri.EMPTY) + .withPlayedAd(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0) + .withAdResumePositionUs(/* adResumePositionUs= */ 0); + + @Rule public final MockitoRule mockito = MockitoJUnit.rule(); + + private FakeMediaSource contentMediaSource; + private FakeMediaSource prerollAdMediaSource; + @Mock private MediaSourceCaller mockMediaSourceCaller; + private AdsMediaSource adsMediaSource; + + @Before + public void setUp() { + // Set up content and ad media sources, passing a null timeline so tests can simulate setting it + // later. + contentMediaSource = new FakeMediaSource(/* timeline= */ null); + prerollAdMediaSource = new FakeMediaSource(/* timeline= */ null); + MediaSourceFactory adMediaSourceFactory = mock(MediaSourceFactory.class); + when(adMediaSourceFactory.createMediaSource(any(Uri.class))).thenReturn(prerollAdMediaSource); + + // Prepare the AdsMediaSource and capture its ads loader listener. + AdsLoader mockAdsLoader = mock(AdsLoader.class); + AdViewProvider mockAdViewProvider = mock(AdViewProvider.class); + ArgumentCaptor eventListenerArgumentCaptor = + ArgumentCaptor.forClass(AdsLoader.EventListener.class); + adsMediaSource = + new AdsMediaSource( + contentMediaSource, adMediaSourceFactory, mockAdsLoader, mockAdViewProvider); + adsMediaSource.prepareSource(mockMediaSourceCaller, /* mediaTransferListener= */ null); + shadowOf(Looper.getMainLooper()).idle(); + verify(mockAdsLoader).start(eventListenerArgumentCaptor.capture(), eq(mockAdViewProvider)); + + // Simulate loading a preroll ad. + AdsLoader.EventListener adsLoaderEventListener = eventListenerArgumentCaptor.getValue(); + adsLoaderEventListener.onAdPlaybackState(AD_PLAYBACK_STATE); + shadowOf(Looper.getMainLooper()).idle(); + } + + @Test + public void createPeriod_preparesChildAdMediaSourceAndRefreshesSourceInfo() { + contentMediaSource.setNewSourceInfo(CONTENT_TIMELINE); + adsMediaSource.createPeriod( + new MediaPeriodId( + CONTENT_PERIOD_UID, + /* adGroupIndex= */ 0, + /* adIndexInAdGroup= */ 0, + /* windowSequenceNumber= */ 0), + mock(Allocator.class), + /* startPositionUs= */ 0); + shadowOf(Looper.getMainLooper()).idle(); + + assertThat(prerollAdMediaSource.isPrepared()).isTrue(); + verify(mockMediaSourceCaller) + .onSourceInfoRefreshed( + adsMediaSource, new SinglePeriodAdTimeline(CONTENT_TIMELINE, AD_PLAYBACK_STATE)); + } + + @Test + public void createPeriod_preparesChildAdMediaSourceAndRefreshesSourceInfoWithAdMediaSourceInfo() { + contentMediaSource.setNewSourceInfo(CONTENT_TIMELINE); + adsMediaSource.createPeriod( + new MediaPeriodId( + CONTENT_PERIOD_UID, + /* adGroupIndex= */ 0, + /* adIndexInAdGroup= */ 0, + /* windowSequenceNumber= */ 0), + mock(Allocator.class), + /* startPositionUs= */ 0); + prerollAdMediaSource.setNewSourceInfo(PREROLL_AD_TIMELINE); + shadowOf(Looper.getMainLooper()).idle(); + + verify(mockMediaSourceCaller) + .onSourceInfoRefreshed( + adsMediaSource, + new SinglePeriodAdTimeline( + CONTENT_TIMELINE, + AD_PLAYBACK_STATE.withAdDurationsUs(new long[][] {{PREROLL_AD_DURATION_US}}))); + } + + @Test + public void createPeriod_createsChildPrerollAdMediaPeriod() { + contentMediaSource.setNewSourceInfo(CONTENT_TIMELINE); + adsMediaSource.createPeriod( + new MediaPeriodId( + CONTENT_PERIOD_UID, + /* adGroupIndex= */ 0, + /* adIndexInAdGroup= */ 0, + /* windowSequenceNumber= */ 0), + mock(Allocator.class), + /* startPositionUs= */ 0); + prerollAdMediaSource.setNewSourceInfo(PREROLL_AD_TIMELINE); + shadowOf(Looper.getMainLooper()).idle(); + + prerollAdMediaSource.assertMediaPeriodCreated( + new MediaPeriodId(PREROLL_AD_PERIOD_UID, /* windowSequenceNumber= */ 0)); + } + + @Test + public void createPeriod_createsChildContentMediaPeriod() { + contentMediaSource.setNewSourceInfo(CONTENT_TIMELINE); + shadowOf(Looper.getMainLooper()).idle(); + adsMediaSource.createPeriod( + new MediaPeriodId(CONTENT_PERIOD_UID, /* windowSequenceNumber= */ 0), + mock(Allocator.class), + /* startPositionUs= */ 0); + + contentMediaSource.assertMediaPeriodCreated( + new MediaPeriodId(CONTENT_PERIOD_UID, /* windowSequenceNumber= */ 0)); + } + + @Test + public void releasePeriod_releasesChildMediaPeriodsAndSources() { + contentMediaSource.setNewSourceInfo(CONTENT_TIMELINE); + MediaPeriod prerollAdMediaPeriod = + adsMediaSource.createPeriod( + new MediaPeriodId( + CONTENT_PERIOD_UID, + /* adGroupIndex= */ 0, + /* adIndexInAdGroup= */ 0, + /* windowSequenceNumber= */ 0), + mock(Allocator.class), + /* startPositionUs= */ 0); + prerollAdMediaSource.setNewSourceInfo(PREROLL_AD_TIMELINE); + shadowOf(Looper.getMainLooper()).idle(); + MediaPeriod contentMediaPeriod = + adsMediaSource.createPeriod( + new MediaPeriodId(CONTENT_PERIOD_UID, /* windowSequenceNumber= */ 0), + mock(Allocator.class), + /* startPositionUs= */ 0); + adsMediaSource.releasePeriod(prerollAdMediaPeriod); + + prerollAdMediaSource.assertReleased(); + + adsMediaSource.releasePeriod(contentMediaPeriod); + adsMediaSource.releaseSource(mockMediaSourceCaller); + shadowOf(Looper.getMainLooper()).idle(); + prerollAdMediaSource.assertReleased(); + contentMediaSource.assertReleased(); + } +} diff --git a/library/core/src/test/java/com/google/android/exoplayer2/source/chunk/MediaChunkListIteratorTest.java b/library/core/src/test/java/com/google/android/exoplayer2/source/chunk/MediaChunkListIteratorTest.java deleted file mode 100644 index d2169d0a38..0000000000 --- a/library/core/src/test/java/com/google/android/exoplayer2/source/chunk/MediaChunkListIteratorTest.java +++ /dev/null @@ -1,76 +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.chunk; - -import static com.google.common.truth.Truth.assertThat; - -import androidx.test.ext.junit.runners.AndroidJUnit4; -import com.google.android.exoplayer2.Format; -import com.google.android.exoplayer2.testutil.FakeMediaChunk; -import java.util.Arrays; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; - -/** Tests for {@link MediaChunkListIterator}. */ -@RunWith(AndroidJUnit4.class) -public class MediaChunkListIteratorTest { - - private static final Format TEST_FORMAT = Format.createSampleFormat(null, null, 0); - - private FakeMediaChunk testChunk1; - private FakeMediaChunk testChunk2; - - @Before - public void setUp() { - testChunk1 = new FakeMediaChunk(TEST_FORMAT, 0, 10); - testChunk2 = new FakeMediaChunk(TEST_FORMAT, 10, 20); - } - - @Test - public void iterator_reverseOrderFalse_returnsItemsInNormalOrder() { - MediaChunkListIterator iterator = - new MediaChunkListIterator( - Arrays.asList(testChunk1, testChunk2), /* reverseOrder= */ false); - assertThat(iterator.isEnded()).isFalse(); - assertThat(iterator.next()).isTrue(); - assertEqual(iterator, testChunk1); - assertThat(iterator.next()).isTrue(); - assertEqual(iterator, testChunk2); - assertThat(iterator.next()).isFalse(); - assertThat(iterator.isEnded()).isTrue(); - } - - @Test - public void iterator_reverseOrderTrue_returnsItemsInReverseOrder() { - MediaChunkListIterator iterator = - new MediaChunkListIterator( - Arrays.asList(testChunk1, testChunk2), /* reverseOrder= */ true); - assertThat(iterator.isEnded()).isFalse(); - assertThat(iterator.next()).isTrue(); - assertEqual(iterator, testChunk2); - assertThat(iterator.next()).isTrue(); - assertEqual(iterator, testChunk1); - assertThat(iterator.next()).isFalse(); - assertThat(iterator.isEnded()).isTrue(); - } - - private static void assertEqual(MediaChunkListIterator iterator, FakeMediaChunk chunk) { - assertThat(iterator.getChunkStartTimeUs()).isEqualTo(chunk.startTimeUs); - assertThat(iterator.getChunkEndTimeUs()).isEqualTo(chunk.endTimeUs); - assertThat(iterator.getDataSpec()).isEqualTo(chunk.dataSpec); - } -} diff --git a/library/core/src/test/java/com/google/android/exoplayer2/text/CueTest.java b/library/core/src/test/java/com/google/android/exoplayer2/text/CueTest.java new file mode 100644 index 0000000000..8c66d35253 --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/text/CueTest.java @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2019 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; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; + +import android.graphics.Bitmap; +import android.graphics.Color; +import android.text.Layout; +import android.text.SpannedString; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Tests for {@link Cue}. */ +@RunWith(AndroidJUnit4.class) +public class CueTest { + + @Test + public void buildAndBuildUponWorkAsExpected() { + Cue cue = + new Cue.Builder() + .setText(SpannedString.valueOf("text")) + .setTextAlignment(Layout.Alignment.ALIGN_CENTER) + .setLine(5, Cue.LINE_TYPE_NUMBER) + .setLineAnchor(Cue.ANCHOR_TYPE_END) + .setPosition(0.4f) + .setPositionAnchor(Cue.ANCHOR_TYPE_MIDDLE) + .setTextSize(0.2f, Cue.TEXT_SIZE_TYPE_FRACTIONAL) + .setSize(0.8f) + .setWindowColor(Color.CYAN) + .setVerticalType(Cue.VERTICAL_TYPE_RL) + .build(); + + Cue modifiedCue = cue.buildUpon().build(); + + assertThat(cue.text.toString()).isEqualTo("text"); + assertThat(cue.textAlignment).isEqualTo(Layout.Alignment.ALIGN_CENTER); + assertThat(cue.line).isEqualTo(5); + assertThat(cue.lineType).isEqualTo(Cue.LINE_TYPE_NUMBER); + assertThat(cue.position).isEqualTo(0.4f); + assertThat(cue.positionAnchor).isEqualTo(Cue.ANCHOR_TYPE_MIDDLE); + assertThat(cue.textSize).isEqualTo(0.2f); + assertThat(cue.textSizeType).isEqualTo(Cue.TEXT_SIZE_TYPE_FRACTIONAL); + assertThat(cue.size).isEqualTo(0.8f); + assertThat(cue.windowColor).isEqualTo(Color.CYAN); + assertThat(cue.windowColorSet).isTrue(); + assertThat(cue.verticalType).isEqualTo(Cue.VERTICAL_TYPE_RL); + + assertThat(modifiedCue.text).isSameInstanceAs(cue.text); + assertThat(modifiedCue.textAlignment).isEqualTo(cue.textAlignment); + assertThat(modifiedCue.line).isEqualTo(cue.line); + assertThat(modifiedCue.lineType).isEqualTo(cue.lineType); + assertThat(modifiedCue.position).isEqualTo(cue.position); + assertThat(modifiedCue.positionAnchor).isEqualTo(cue.positionAnchor); + assertThat(modifiedCue.textSize).isEqualTo(cue.textSize); + assertThat(modifiedCue.textSizeType).isEqualTo(cue.textSizeType); + assertThat(modifiedCue.size).isEqualTo(cue.size); + assertThat(modifiedCue.windowColor).isEqualTo(cue.windowColor); + assertThat(modifiedCue.windowColorSet).isEqualTo(cue.windowColorSet); + assertThat(modifiedCue.verticalType).isEqualTo(cue.verticalType); + } + + @Test + public void buildWithNoTextOrBitmapFails() { + assertThrows(RuntimeException.class, () -> new Cue.Builder().build()); + } + + @Test + public void buildWithBothTextAndBitmapFails() { + assertThrows( + RuntimeException.class, + () -> + new Cue.Builder() + .setText(SpannedString.valueOf("text")) + .setBitmap(Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888)) + .build()); + } +} diff --git a/library/core/src/test/java/com/google/android/exoplayer2/text/span/SpanUtilTest.java b/library/core/src/test/java/com/google/android/exoplayer2/text/span/SpanUtilTest.java new file mode 100644 index 0000000000..cdccf045a5 --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/text/span/SpanUtilTest.java @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2020 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.span; + +import static com.google.common.truth.Truth.assertThat; + +import android.graphics.Color; +import android.text.Spannable; +import android.text.SpannableString; +import android.text.Spanned; +import android.text.style.BackgroundColorSpan; +import android.text.style.ForegroundColorSpan; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Tests for {@link SpanUtil}. */ +@RunWith(AndroidJUnit4.class) +public class SpanUtilTest { + + @Test + public void addOrReplaceSpan_replacesSameTypeAndIndexes() { + Spannable spannable = SpannableString.valueOf("test text"); + spannable.setSpan( + new ForegroundColorSpan(Color.CYAN), + /* start= */ 2, + /* end= */ 5, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + + ForegroundColorSpan newSpan = new ForegroundColorSpan(Color.BLUE); + SpanUtil.addOrReplaceSpan( + spannable, newSpan, /* start= */ 2, /* end= */ 5, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + + Object[] spans = spannable.getSpans(0, spannable.length(), Object.class); + assertThat(spans).asList().containsExactly(newSpan); + } + + @Test + public void addOrReplaceSpan_ignoresDifferentType() { + Spannable spannable = SpannableString.valueOf("test text"); + ForegroundColorSpan originalSpan = new ForegroundColorSpan(Color.CYAN); + spannable.setSpan(originalSpan, /* start= */ 2, /* end= */ 5, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + + BackgroundColorSpan newSpan = new BackgroundColorSpan(Color.BLUE); + SpanUtil.addOrReplaceSpan(spannable, newSpan, 2, 5, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + + Object[] spans = spannable.getSpans(0, spannable.length(), Object.class); + assertThat(spans).asList().containsExactly(originalSpan, newSpan).inOrder(); + } + + @Test + public void addOrReplaceSpan_ignoresDifferentStartEndAndFlags() { + Spannable spannable = SpannableString.valueOf("test text"); + ForegroundColorSpan originalSpan = new ForegroundColorSpan(Color.CYAN); + spannable.setSpan(originalSpan, /* start= */ 2, /* end= */ 5, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + + ForegroundColorSpan differentStart = new ForegroundColorSpan(Color.GREEN); + SpanUtil.addOrReplaceSpan( + spannable, differentStart, /* start= */ 3, /* end= */ 5, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + ForegroundColorSpan differentEnd = new ForegroundColorSpan(Color.BLUE); + SpanUtil.addOrReplaceSpan( + spannable, differentEnd, /* start= */ 2, /* end= */ 6, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + ForegroundColorSpan differentFlags = new ForegroundColorSpan(Color.GREEN); + SpanUtil.addOrReplaceSpan( + spannable, differentFlags, /* start= */ 2, /* end= */ 5, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + + Object[] spans = spannable.getSpans(0, spannable.length(), Object.class); + assertThat(spans) + .asList() + .containsExactly(originalSpan, differentStart, differentEnd, differentFlags) + .inOrder(); + } +} diff --git a/library/core/src/test/java/com/google/android/exoplayer2/text/ssa/SsaDecoderTest.java b/library/core/src/test/java/com/google/android/exoplayer2/text/ssa/SsaDecoderTest.java index 3c48aa61dd..379e189db9 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/text/ssa/SsaDecoderTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/text/ssa/SsaDecoderTest.java @@ -16,11 +16,15 @@ package com.google.android.exoplayer2.text.ssa; import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.Truth.assertWithMessage; +import android.text.Layout; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.testutil.TestUtil; +import com.google.android.exoplayer2.text.Cue; import com.google.android.exoplayer2.text.Subtitle; +import com.google.common.collect.Iterables; import java.io.IOException; import java.util.ArrayList; import org.junit.Test; @@ -35,10 +39,14 @@ public final class SsaDecoderTest { private static final String TYPICAL_HEADER_ONLY = "ssa/typical_header"; private static final String TYPICAL_DIALOGUE_ONLY = "ssa/typical_dialogue"; private static final String TYPICAL_FORMAT_ONLY = "ssa/typical_format"; + private static final String OVERLAPPING_TIMECODES = "ssa/overlapping_timecodes"; + private static final String POSITIONS = "ssa/positioning"; private static final String INVALID_TIMECODES = "ssa/invalid_timecodes"; + private static final String INVALID_POSITIONS = "ssa/invalid_positioning"; + private static final String POSITIONS_WITHOUT_PLAYRES = "ssa/positioning_without_playres"; @Test - public void testDecodeEmpty() throws IOException { + public void decodeEmpty() throws IOException { SsaDecoder decoder = new SsaDecoder(); byte[] bytes = TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), EMPTY); Subtitle subtitle = decoder.decode(bytes, bytes.length, false); @@ -48,19 +56,32 @@ public final class SsaDecoderTest { } @Test - public void testDecodeTypical() throws IOException { + public void decodeTypical() throws IOException { SsaDecoder decoder = new SsaDecoder(); byte[] bytes = TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), TYPICAL); Subtitle subtitle = decoder.decode(bytes, bytes.length, false); assertThat(subtitle.getEventTimeCount()).isEqualTo(6); + // Check position, line, anchors & alignment are set from Alignment Style (2 - bottom-center). + Cue firstCue = subtitle.getCues(subtitle.getEventTime(0)).get(0); + assertWithMessage("Cue.textAlignment") + .that(firstCue.textAlignment) + .isEqualTo(Layout.Alignment.ALIGN_CENTER); + assertWithMessage("Cue.positionAnchor") + .that(firstCue.positionAnchor) + .isEqualTo(Cue.ANCHOR_TYPE_MIDDLE); + assertThat(firstCue.position).isEqualTo(0.5f); + assertThat(firstCue.lineAnchor).isEqualTo(Cue.ANCHOR_TYPE_END); + assertThat(firstCue.lineType).isEqualTo(Cue.LINE_TYPE_FRACTION); + assertThat(firstCue.line).isEqualTo(0.95f); + assertTypicalCue1(subtitle, 0); assertTypicalCue2(subtitle, 2); assertTypicalCue3(subtitle, 4); } @Test - public void testDecodeTypicalWithInitializationData() throws IOException { + public void decodeTypicalWithInitializationData() throws IOException { byte[] headerBytes = TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), TYPICAL_HEADER_ONLY); byte[] formatBytes = @@ -80,7 +101,162 @@ public final class SsaDecoderTest { } @Test - public void testDecodeInvalidTimecodes() throws IOException { + public void decodeOverlappingTimecodes() throws IOException { + SsaDecoder decoder = new SsaDecoder(); + byte[] bytes = + TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), OVERLAPPING_TIMECODES); + Subtitle subtitle = decoder.decode(bytes, bytes.length, false); + + assertThat(subtitle.getEventTime(0)).isEqualTo(1_000_000); + assertThat(subtitle.getEventTime(1)).isEqualTo(2_000_000); + assertThat(subtitle.getEventTime(2)).isEqualTo(4_230_000); + assertThat(subtitle.getEventTime(3)).isEqualTo(5_230_000); + assertThat(subtitle.getEventTime(4)).isEqualTo(6_000_000); + assertThat(subtitle.getEventTime(5)).isEqualTo(8_440_000); + assertThat(subtitle.getEventTime(6)).isEqualTo(9_440_000); + assertThat(subtitle.getEventTime(7)).isEqualTo(10_720_000); + assertThat(subtitle.getEventTime(8)).isEqualTo(13_220_000); + assertThat(subtitle.getEventTime(9)).isEqualTo(14_220_000); + assertThat(subtitle.getEventTime(10)).isEqualTo(15_650_000); + + String firstSubtitleText = "First subtitle - end overlaps second"; + String secondSubtitleText = "Second subtitle - beginning overlaps first"; + String thirdSubtitleText = "Third subtitle - out of order"; + String fourthSubtitleText = "Fourth subtitle - same timings as fifth"; + String fifthSubtitleText = "Fifth subtitle - same timings as fourth"; + String sixthSubtitleText = "Sixth subtitle - fully encompasses seventh"; + String seventhSubtitleText = "Seventh subtitle - nested fully inside sixth"; + assertThat(Iterables.transform(subtitle.getCues(1_000_010), cue -> cue.text.toString())) + .containsExactly(firstSubtitleText); + assertThat(Iterables.transform(subtitle.getCues(2_000_010), cue -> cue.text.toString())) + .containsExactly(firstSubtitleText, secondSubtitleText); + assertThat(Iterables.transform(subtitle.getCues(4_230_010), cue -> cue.text.toString())) + .containsExactly(secondSubtitleText); + assertThat(Iterables.transform(subtitle.getCues(5_230_010), cue -> cue.text.toString())) + .isEmpty(); + assertThat(Iterables.transform(subtitle.getCues(6_000_010), cue -> cue.text.toString())) + .containsExactly(thirdSubtitleText); + assertThat(Iterables.transform(subtitle.getCues(8_440_010), cue -> cue.text.toString())) + .containsExactly(fourthSubtitleText, fifthSubtitleText); + assertThat(Iterables.transform(subtitle.getCues(9_440_010), cue -> cue.text.toString())) + .isEmpty(); + assertThat(Iterables.transform(subtitle.getCues(10_720_010), cue -> cue.text.toString())) + .containsExactly(sixthSubtitleText); + assertThat(Iterables.transform(subtitle.getCues(13_220_010), cue -> cue.text.toString())) + .containsExactly(sixthSubtitleText, seventhSubtitleText); + assertThat(Iterables.transform(subtitle.getCues(14_220_010), cue -> cue.text.toString())) + .containsExactly(sixthSubtitleText); + assertThat(Iterables.transform(subtitle.getCues(15_650_010), cue -> cue.text.toString())) + .isEmpty(); + } + + @Test + public void decodePositions() throws IOException { + SsaDecoder decoder = new SsaDecoder(); + byte[] bytes = TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), POSITIONS); + Subtitle subtitle = decoder.decode(bytes, bytes.length, false); + + // Check \pos() sets position & line + Cue firstCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(0))); + assertThat(firstCue.position).isEqualTo(0.5f); + assertThat(firstCue.lineType).isEqualTo(Cue.LINE_TYPE_FRACTION); + assertThat(firstCue.line).isEqualTo(0.25f); + + // Check the \pos() doesn't need to be at the start of the line. + Cue secondCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(2))); + assertThat(secondCue.position).isEqualTo(0.25f); + assertThat(secondCue.line).isEqualTo(0.25f); + + // Check only the last \pos() value is used. + Cue thirdCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(4))); + assertThat(thirdCue.position).isEqualTo(0.25f); + + // Check \move() is treated as \pos() + Cue fourthCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(6))); + assertThat(fourthCue.position).isEqualTo(0.5f); + assertThat(fourthCue.line).isEqualTo(0.25f); + + // Check alignment override in a separate brace (to bottom-center) affects textAlignment and + // both line & position anchors. + Cue fifthCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(8))); + assertThat(fifthCue.position).isEqualTo(0.5f); + assertThat(fifthCue.line).isEqualTo(0.5f); + assertWithMessage("Cue.positionAnchor") + .that(fifthCue.positionAnchor) + .isEqualTo(Cue.ANCHOR_TYPE_MIDDLE); + assertThat(fifthCue.lineAnchor).isEqualTo(Cue.ANCHOR_TYPE_END); + assertWithMessage("Cue.textAlignment") + .that(fifthCue.textAlignment) + .isEqualTo(Layout.Alignment.ALIGN_CENTER); + + // Check alignment override in the same brace (to top-right) affects textAlignment and both line + // & position anchors. + Cue sixthCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(10))); + assertThat(sixthCue.position).isEqualTo(0.5f); + assertThat(sixthCue.line).isEqualTo(0.5f); + assertWithMessage("Cue.positionAnchor") + .that(sixthCue.positionAnchor) + .isEqualTo(Cue.ANCHOR_TYPE_END); + assertThat(sixthCue.lineAnchor).isEqualTo(Cue.ANCHOR_TYPE_START); + assertWithMessage("Cue.textAlignment") + .that(sixthCue.textAlignment) + .isEqualTo(Layout.Alignment.ALIGN_OPPOSITE); + } + + @Test + public void decodeInvalidPositions() throws IOException { + SsaDecoder decoder = new SsaDecoder(); + byte[] bytes = + TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), INVALID_POSITIONS); + Subtitle subtitle = decoder.decode(bytes, bytes.length, false); + + // Negative parameter to \pos() - fall back to the positions implied by middle-left alignment. + Cue firstCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(0))); + assertThat(firstCue.position).isEqualTo(0.05f); + assertThat(firstCue.lineType).isEqualTo(Cue.LINE_TYPE_FRACTION); + assertThat(firstCue.line).isEqualTo(0.5f); + + // Negative parameter to \move() - fall back to the positions implied by middle-left alignment. + Cue secondCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(2))); + assertThat(secondCue.position).isEqualTo(0.05f); + assertThat(secondCue.lineType).isEqualTo(Cue.LINE_TYPE_FRACTION); + assertThat(secondCue.line).isEqualTo(0.5f); + + // Check invalid alignment override (11) is skipped and style-provided one is used (4). + Cue thirdCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(4))); + assertWithMessage("Cue.positionAnchor") + .that(thirdCue.positionAnchor) + .isEqualTo(Cue.ANCHOR_TYPE_START); + assertThat(thirdCue.lineAnchor).isEqualTo(Cue.ANCHOR_TYPE_MIDDLE); + assertWithMessage("Cue.textAlignment") + .that(thirdCue.textAlignment) + .isEqualTo(Layout.Alignment.ALIGN_NORMAL); + + // No braces - fall back to the positions implied by middle-left alignment + Cue fourthCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(6))); + assertThat(fourthCue.position).isEqualTo(0.05f); + assertThat(fourthCue.lineType).isEqualTo(Cue.LINE_TYPE_FRACTION); + assertThat(fourthCue.line).isEqualTo(0.5f); + } + + @Test + public void decodePositionsWithMissingPlayResY() throws IOException { + SsaDecoder decoder = new SsaDecoder(); + byte[] bytes = + TestUtil.getByteArray( + ApplicationProvider.getApplicationContext(), POSITIONS_WITHOUT_PLAYRES); + Subtitle subtitle = decoder.decode(bytes, bytes.length, false); + + // The dialogue line has a valid \pos() override, but it's ignored because PlayResY isn't + // set (so we don't know the denominator). + Cue firstCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(0))); + assertThat(firstCue.position).isEqualTo(Cue.DIMEN_UNSET); + assertThat(firstCue.lineType).isEqualTo(Cue.LINE_TYPE_FRACTION); + assertThat(firstCue.line).isEqualTo(Cue.DIMEN_UNSET); + } + + @Test + public void decodeInvalidTimecodes() throws IOException { // Parsing should succeed, parsing the third cue only. SsaDecoder decoder = new SsaDecoder(); byte[] bytes = diff --git a/library/core/src/test/java/com/google/android/exoplayer2/text/subrip/SubripDecoderTest.java b/library/core/src/test/java/com/google/android/exoplayer2/text/subrip/SubripDecoderTest.java index 9f66f65a56..e233d8d1b5 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/text/subrip/SubripDecoderTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/text/subrip/SubripDecoderTest.java @@ -39,9 +39,10 @@ public final class SubripDecoderTest { private static final String TYPICAL_NEGATIVE_TIMESTAMPS = "subrip/typical_negative_timestamps"; private static final String TYPICAL_UNEXPECTED_END = "subrip/typical_unexpected_end"; private static final String TYPICAL_WITH_TAGS = "subrip/typical_with_tags"; + private static final String TYPICAL_NO_HOURS_AND_MILLIS = "subrip/typical_no_hours_and_millis"; @Test - public void testDecodeEmpty() throws IOException { + public void decodeEmpty() throws IOException { SubripDecoder decoder = new SubripDecoder(); byte[] bytes = TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), EMPTY_FILE); Subtitle subtitle = decoder.decode(bytes, bytes.length, false); @@ -51,7 +52,7 @@ public final class SubripDecoderTest { } @Test - public void testDecodeTypical() throws IOException { + public void decodeTypical() throws IOException { SubripDecoder decoder = new SubripDecoder(); byte[] bytes = TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), TYPICAL_FILE); Subtitle subtitle = decoder.decode(bytes, bytes.length, false); @@ -63,7 +64,7 @@ public final class SubripDecoderTest { } @Test - public void testDecodeTypicalWithByteOrderMark() throws IOException { + public void decodeTypicalWithByteOrderMark() throws IOException { SubripDecoder decoder = new SubripDecoder(); byte[] bytes = TestUtil.getByteArray( @@ -77,7 +78,7 @@ public final class SubripDecoderTest { } @Test - public void testDecodeTypicalExtraBlankLine() throws IOException { + public void decodeTypicalExtraBlankLine() throws IOException { SubripDecoder decoder = new SubripDecoder(); byte[] bytes = TestUtil.getByteArray( @@ -91,7 +92,7 @@ public final class SubripDecoderTest { } @Test - public void testDecodeTypicalMissingTimecode() throws IOException { + public void decodeTypicalMissingTimecode() throws IOException { // Parsing should succeed, parsing the first and third cues only. SubripDecoder decoder = new SubripDecoder(); byte[] bytes = @@ -105,7 +106,7 @@ public final class SubripDecoderTest { } @Test - public void testDecodeTypicalMissingSequence() throws IOException { + public void decodeTypicalMissingSequence() throws IOException { // Parsing should succeed, parsing the first and third cues only. SubripDecoder decoder = new SubripDecoder(); byte[] bytes = @@ -119,7 +120,7 @@ public final class SubripDecoderTest { } @Test - public void testDecodeTypicalNegativeTimestamps() throws IOException { + public void decodeTypicalNegativeTimestamps() throws IOException { // Parsing should succeed, parsing the third cue only. SubripDecoder decoder = new SubripDecoder(); byte[] bytes = @@ -132,7 +133,7 @@ public final class SubripDecoderTest { } @Test - public void testDecodeTypicalUnexpectedEnd() throws IOException { + public void decodeTypicalUnexpectedEnd() throws IOException { // Parsing should succeed, parsing the first and second cues only. SubripDecoder decoder = new SubripDecoder(); byte[] bytes = @@ -145,15 +146,20 @@ public final class SubripDecoderTest { } @Test - public void testDecodeCueWithTag() throws IOException { + public void decodeCueWithTag() throws IOException { SubripDecoder decoder = new SubripDecoder(); byte[] bytes = TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), TYPICAL_WITH_TAGS); Subtitle subtitle = decoder.decode(bytes, bytes.length, false); - assertTypicalCue1(subtitle, 0); - assertTypicalCue2(subtitle, 2); - assertTypicalCue3(subtitle, 4); + assertThat(subtitle.getCues(subtitle.getEventTime(0)).get(0).text.toString()) + .isEqualTo("This is the first subtitle."); + + assertThat(subtitle.getCues(subtitle.getEventTime(2)).get(0).text.toString()) + .isEqualTo("This is the second subtitle.\nSecond subtitle with second line."); + + assertThat(subtitle.getCues(subtitle.getEventTime(4)).get(0).text.toString()) + .isEqualTo("This is the third subtitle."); assertThat(subtitle.getCues(subtitle.getEventTime(6)).get(0).text.toString()) .isEqualTo("This { \\an2} is not a valid tag due to the space after the opening bracket."); @@ -172,6 +178,21 @@ public final class SubripDecoderTest { assertAlignmentCue(subtitle, 26, Cue.ANCHOR_TYPE_START, Cue.ANCHOR_TYPE_END); // {/an9} } + @Test + public void decodeTypicalNoHoursAndMillis() throws IOException { + SubripDecoder decoder = new SubripDecoder(); + byte[] bytes = + TestUtil.getByteArray( + ApplicationProvider.getApplicationContext(), TYPICAL_NO_HOURS_AND_MILLIS); + Subtitle subtitle = decoder.decode(bytes, bytes.length, false); + + assertThat(subtitle.getEventTimeCount()).isEqualTo(6); + assertTypicalCue1(subtitle, 0); + assertThat(subtitle.getEventTime(2)).isEqualTo(2_000_000); + assertThat(subtitle.getEventTime(3)).isEqualTo(3_000_000); + assertTypicalCue3(subtitle, 4); + } + private static void assertTypicalCue1(Subtitle subtitle, int eventIndex) { assertThat(subtitle.getEventTime(eventIndex)).isEqualTo(0); assertThat(subtitle.getCues(subtitle.getEventTime(eventIndex)).get(0).text.toString()) @@ -187,10 +208,12 @@ public final class SubripDecoderTest { } private static void assertTypicalCue3(Subtitle subtitle, int eventIndex) { - assertThat(subtitle.getEventTime(eventIndex)).isEqualTo(4567000); + long expectedStartTimeUs = (((2L * 60L * 60L) + 4L) * 1000L + 567L) * 1000L; + assertThat(subtitle.getEventTime(eventIndex)).isEqualTo(expectedStartTimeUs); assertThat(subtitle.getCues(subtitle.getEventTime(eventIndex)).get(0).text.toString()) .isEqualTo("This is the third subtitle."); - assertThat(subtitle.getEventTime(eventIndex + 1)).isEqualTo(8901000); + long expectedEndTimeUs = (((2L * 60L * 60L) + 8L) * 1000L + 901L) * 1000L; + assertThat(subtitle.getEventTime(eventIndex + 1)).isEqualTo(expectedEndTimeUs); } private static void assertAlignmentCue( diff --git a/library/core/src/test/java/com/google/android/exoplayer2/text/ttml/TtmlDecoderTest.java b/library/core/src/test/java/com/google/android/exoplayer2/text/ttml/TtmlDecoderTest.java index 22c7288340..071d34e5d0 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/text/ttml/TtmlDecoderTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/text/ttml/TtmlDecoderTest.java @@ -15,26 +15,20 @@ */ package com.google.android.exoplayer2.text.ttml; +import static com.google.android.exoplayer2.testutil.truth.SpannedSubject.assertThat; import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertWithMessage; import android.text.Layout; -import android.text.Spannable; -import android.text.SpannableStringBuilder; -import android.text.style.AbsoluteSizeSpan; -import android.text.style.AlignmentSpan; -import android.text.style.BackgroundColorSpan; -import android.text.style.ForegroundColorSpan; -import android.text.style.RelativeSizeSpan; -import android.text.style.StrikethroughSpan; -import android.text.style.StyleSpan; -import android.text.style.TypefaceSpan; -import android.text.style.UnderlineSpan; +import android.text.Spanned; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.testutil.TestUtil; import com.google.android.exoplayer2.text.Cue; +import com.google.android.exoplayer2.text.Subtitle; import com.google.android.exoplayer2.text.SubtitleDecoderException; +import com.google.android.exoplayer2.text.span.RubySpan; +import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.ColorParser; import java.io.IOException; import java.util.List; @@ -66,39 +60,44 @@ public final class TtmlDecoderTest { private static final String BITMAP_REGION_FILE = "ttml/bitmap_percentage_region.xml"; private static final String BITMAP_PIXEL_REGION_FILE = "ttml/bitmap_pixel_region.xml"; private static final String BITMAP_UNSUPPORTED_REGION_FILE = "ttml/bitmap_unsupported_region.xml"; + private static final String VERTICAL_TEXT_FILE = "ttml/vertical_text.xml"; + private static final String TEXT_COMBINE_FILE = "ttml/text_combine.xml"; + private static final String RUBIES_FILE = "ttml/rubies.xml"; @Test - public void testInlineAttributes() throws IOException, SubtitleDecoderException { + public void inlineAttributes() throws IOException, SubtitleDecoderException { TtmlSubtitle subtitle = getSubtitle(INLINE_ATTRIBUTES_TTML_FILE); + assertThat(subtitle.getEventTimeCount()).isEqualTo(4); - TtmlNode root = subtitle.getRoot(); - - TtmlNode body = queryChildrenForTag(root, TtmlNode.TAG_BODY, 0); - TtmlNode firstDiv = queryChildrenForTag(body, TtmlNode.TAG_DIV, 0); - TtmlStyle firstPStyle = queryChildrenForTag(firstDiv, TtmlNode.TAG_P, 0).style; - assertThat(firstPStyle.getFontColor()).isEqualTo(ColorParser.parseTtmlColor("yellow")); - assertThat(firstPStyle.getBackgroundColor()).isEqualTo(ColorParser.parseTtmlColor("blue")); - assertThat(firstPStyle.getFontFamily()).isEqualTo("serif"); - assertThat(firstPStyle.getStyle()).isEqualTo(TtmlStyle.STYLE_BOLD_ITALIC); - assertThat(firstPStyle.isUnderline()).isTrue(); + Spanned spanned = getOnlyCueTextAtTimeUs(subtitle, 10_000_000); + assertThat(spanned.toString()).isEqualTo("text 1"); + assertThat(spanned).hasTypefaceSpanBetween(0, spanned.length()).withFamily("serif"); + assertThat(spanned).hasBoldItalicSpanBetween(0, spanned.length()); + assertThat(spanned).hasUnderlineSpanBetween(0, spanned.length()); + assertThat(spanned) + .hasBackgroundColorSpanBetween(0, spanned.length()) + .withColor(ColorParser.parseTtmlColor("blue")); + assertThat(spanned) + .hasForegroundColorSpanBetween(0, spanned.length()) + .withColor(ColorParser.parseTtmlColor("yellow")); } @Test - public void testInheritInlineAttributes() throws IOException, SubtitleDecoderException { + public void inheritInlineAttributes() throws IOException, SubtitleDecoderException { TtmlSubtitle subtitle = getSubtitle(INLINE_ATTRIBUTES_TTML_FILE); + assertThat(subtitle.getEventTimeCount()).isEqualTo(4); - assertSpans( - subtitle, - 20, - "text 2", - "sansSerif", - TtmlStyle.STYLE_ITALIC, - 0xFF00FFFF, - ColorParser.parseTtmlColor("lime"), - false, - true, - null); + + Spanned spanned = getOnlyCueTextAtTimeUs(subtitle, 20_000_000); + assertThat(spanned.toString()).isEqualTo("text 2"); + assertThat(spanned).hasTypefaceSpanBetween(0, spanned.length()).withFamily("sansSerif"); + assertThat(spanned).hasItalicSpanBetween(0, spanned.length()); + assertThat(spanned).hasStrikethroughSpanBetween(0, spanned.length()); + assertThat(spanned).hasBackgroundColorSpanBetween(0, spanned.length()).withColor(0xFF00FFFF); + assertThat(spanned) + .hasForegroundColorSpanBetween(0, spanned.length()) + .withColor(ColorParser.parseTtmlColor("lime")); } /** @@ -114,155 +113,172 @@ public final class TtmlDecoderTest { * @throws IOException thrown if reading subtitle file fails. */ @Test - public void testLime() throws IOException, SubtitleDecoderException { + public void lime() throws IOException, SubtitleDecoderException { TtmlSubtitle subtitle = getSubtitle(INLINE_ATTRIBUTES_TTML_FILE); + assertThat(subtitle.getEventTimeCount()).isEqualTo(4); - assertSpans( - subtitle, - 20, - "text 2", - "sansSerif", - TtmlStyle.STYLE_ITALIC, - 0xFF00FFFF, - 0xFF00FF00, - false, - true, - null); + + Spanned spanned = getOnlyCueTextAtTimeUs(subtitle, 20_000_000); + assertThat(spanned.toString()).isEqualTo("text 2"); + assertThat(spanned).hasTypefaceSpanBetween(0, spanned.length()).withFamily("sansSerif"); + assertThat(spanned).hasItalicSpanBetween(0, spanned.length()); + assertThat(spanned).hasStrikethroughSpanBetween(0, spanned.length()); + assertThat(spanned).hasBackgroundColorSpanBetween(0, spanned.length()).withColor(0xFF00FFFF); + assertThat(spanned).hasForegroundColorSpanBetween(0, spanned.length()).withColor(0xFF00FF00); } @Test - public void testInheritGlobalStyle() throws IOException, SubtitleDecoderException { + public void inheritGlobalStyle() throws IOException, SubtitleDecoderException { TtmlSubtitle subtitle = getSubtitle(INHERIT_STYLE_TTML_FILE); + assertThat(subtitle.getEventTimeCount()).isEqualTo(2); - assertSpans( - subtitle, - 10, - "text 1", - "serif", - TtmlStyle.STYLE_BOLD_ITALIC, - 0xFF0000FF, - 0xFFFFFF00, - true, - false, - null); + + Spanned spanned = getOnlyCueTextAtTimeUs(subtitle, 10_000_000); + assertThat(spanned.toString()).isEqualTo("text 1"); + assertThat(spanned).hasTypefaceSpanBetween(0, spanned.length()).withFamily("serif"); + assertThat(spanned).hasBoldItalicSpanBetween(0, spanned.length()); + assertThat(spanned).hasUnderlineSpanBetween(0, spanned.length()); + assertThat(spanned).hasBackgroundColorSpanBetween(0, spanned.length()).withColor(0xFF0000FF); + assertThat(spanned).hasForegroundColorSpanBetween(0, spanned.length()).withColor(0xFFFFFF00); } @Test - public void testInheritGlobalStyleOverriddenByInlineAttributes() + public void inheritGlobalStyleOverriddenByInlineAttributes() throws IOException, SubtitleDecoderException { TtmlSubtitle subtitle = getSubtitle(INHERIT_STYLE_OVERRIDE_TTML_FILE); + assertThat(subtitle.getEventTimeCount()).isEqualTo(4); - assertSpans( - subtitle, - 10, - "text 1", - "serif", - TtmlStyle.STYLE_BOLD_ITALIC, - 0xFF0000FF, - 0xFFFFFF00, - true, - false, - null); - assertSpans( - subtitle, - 20, - "text 2", - "sansSerif", - TtmlStyle.STYLE_ITALIC, - 0xFFFF0000, - 0xFFFFFF00, - true, - false, - null); + Spanned firstCueText = getOnlyCueTextAtTimeUs(subtitle, 10_000_000); + assertThat(firstCueText.toString()).isEqualTo("text 1"); + assertThat(firstCueText).hasTypefaceSpanBetween(0, firstCueText.length()).withFamily("serif"); + assertThat(firstCueText).hasBoldItalicSpanBetween(0, firstCueText.length()); + assertThat(firstCueText).hasUnderlineSpanBetween(0, firstCueText.length()); + assertThat(firstCueText) + .hasBackgroundColorSpanBetween(0, firstCueText.length()) + .withColor(0xFF0000FF); + assertThat(firstCueText) + .hasForegroundColorSpanBetween(0, firstCueText.length()) + .withColor(0xFFFFFF00); + + Spanned secondCueText = getOnlyCueTextAtTimeUs(subtitle, 20_000_000); + assertThat(secondCueText.toString()).isEqualTo("text 2"); + assertThat(secondCueText) + .hasTypefaceSpanBetween(0, secondCueText.length()) + .withFamily("sansSerif"); + assertThat(secondCueText).hasItalicSpanBetween(0, secondCueText.length()); + assertThat(secondCueText).hasUnderlineSpanBetween(0, secondCueText.length()); + assertThat(secondCueText) + .hasBackgroundColorSpanBetween(0, secondCueText.length()) + .withColor(0xFFFF0000); + assertThat(secondCueText) + .hasForegroundColorSpanBetween(0, secondCueText.length()) + .withColor(0xFFFFFF00); } @Test - public void testInheritGlobalAndParent() throws IOException, SubtitleDecoderException { + public void inheritGlobalAndParent() throws IOException, SubtitleDecoderException { TtmlSubtitle subtitle = getSubtitle(INHERIT_GLOBAL_AND_PARENT_TTML_FILE); + assertThat(subtitle.getEventTimeCount()).isEqualTo(4); - assertSpans( - subtitle, - 10, - "text 1", - "sansSerif", - TtmlStyle.STYLE_NORMAL, - 0xFFFF0000, - ColorParser.parseTtmlColor("lime"), - false, - true, - Layout.Alignment.ALIGN_CENTER); - assertSpans( - subtitle, - 20, - "text 2", - "serif", - TtmlStyle.STYLE_BOLD_ITALIC, - 0xFF0000FF, - 0xFFFFFF00, - true, - true, - Layout.Alignment.ALIGN_CENTER); + Spanned firstCueText = getOnlyCueTextAtTimeUs(subtitle, 10_000_000); + assertThat(firstCueText.toString()).isEqualTo("text 1"); + assertThat(firstCueText) + .hasTypefaceSpanBetween(0, firstCueText.length()) + .withFamily("sansSerif"); + assertThat(firstCueText).hasStrikethroughSpanBetween(0, firstCueText.length()); + assertThat(firstCueText) + .hasBackgroundColorSpanBetween(0, firstCueText.length()) + .withColor(0xFFFF0000); + assertThat(firstCueText) + .hasForegroundColorSpanBetween(0, firstCueText.length()) + .withColor(ColorParser.parseTtmlColor("lime")); + assertThat(firstCueText) + .hasAlignmentSpanBetween(0, firstCueText.length()) + .withAlignment(Layout.Alignment.ALIGN_CENTER); + + Spanned secondCueText = getOnlyCueTextAtTimeUs(subtitle, 20_000_000); + assertThat(secondCueText.toString()).isEqualTo("text 2"); + assertThat(secondCueText).hasTypefaceSpanBetween(0, secondCueText.length()).withFamily("serif"); + assertThat(secondCueText).hasBoldItalicSpanBetween(0, secondCueText.length()); + assertThat(secondCueText).hasUnderlineSpanBetween(0, secondCueText.length()); + assertThat(secondCueText).hasStrikethroughSpanBetween(0, secondCueText.length()); + assertThat(secondCueText) + .hasBackgroundColorSpanBetween(0, secondCueText.length()) + .withColor(0xFF0000FF); + assertThat(secondCueText) + .hasForegroundColorSpanBetween(0, secondCueText.length()) + .withColor(0xFFFFFF00); + assertThat(secondCueText) + .hasAlignmentSpanBetween(0, secondCueText.length()) + .withAlignment(Layout.Alignment.ALIGN_CENTER); } @Test - public void testInheritMultipleStyles() throws IOException, SubtitleDecoderException { + public void inheritMultipleStyles() throws IOException, SubtitleDecoderException { TtmlSubtitle subtitle = getSubtitle(INHERIT_MULTIPLE_STYLES_TTML_FILE); + assertThat(subtitle.getEventTimeCount()).isEqualTo(12); - assertSpans( - subtitle, - 10, - "text 1", - "sansSerif", - TtmlStyle.STYLE_BOLD_ITALIC, - 0xFF0000FF, - 0xFFFFFF00, - false, - true, - null); + + Spanned spanned = getOnlyCueTextAtTimeUs(subtitle, 10_000_000); + assertThat(spanned.toString()).isEqualTo("text 1"); + assertThat(spanned).hasTypefaceSpanBetween(0, spanned.length()).withFamily("sansSerif"); + assertThat(spanned).hasBoldItalicSpanBetween(0, spanned.length()); + assertThat(spanned).hasStrikethroughSpanBetween(0, spanned.length()); + assertThat(spanned).hasBackgroundColorSpanBetween(0, spanned.length()).withColor(0xFF0000FF); + assertThat(spanned).hasForegroundColorSpanBetween(0, spanned.length()).withColor(0xFFFFFF00); } @Test - public void testInheritMultipleStylesWithoutLocalAttributes() + public void inheritMultipleStylesWithoutLocalAttributes() throws IOException, SubtitleDecoderException { TtmlSubtitle subtitle = getSubtitle(INHERIT_MULTIPLE_STYLES_TTML_FILE); + assertThat(subtitle.getEventTimeCount()).isEqualTo(12); - assertSpans( - subtitle, - 20, - "text 2", - "sansSerif", - TtmlStyle.STYLE_BOLD_ITALIC, - 0xFF0000FF, - 0xFF000000, - false, - true, - null); + + Spanned secondCueText = getOnlyCueTextAtTimeUs(subtitle, 20_000_000); + assertThat(secondCueText.toString()).isEqualTo("text 2"); + assertThat(secondCueText) + .hasTypefaceSpanBetween(0, secondCueText.length()) + .withFamily("sansSerif"); + assertThat(secondCueText).hasBoldItalicSpanBetween(0, secondCueText.length()); + assertThat(secondCueText).hasStrikethroughSpanBetween(0, secondCueText.length()); + assertThat(secondCueText) + .hasBackgroundColorSpanBetween(0, secondCueText.length()) + .withColor(0xFF0000FF); + assertThat(secondCueText) + .hasForegroundColorSpanBetween(0, secondCueText.length()) + .withColor(0xFF000000); } @Test - public void testMergeMultipleStylesWithParentStyle() - throws IOException, SubtitleDecoderException { + public void mergeMultipleStylesWithParentStyle() throws IOException, SubtitleDecoderException { TtmlSubtitle subtitle = getSubtitle(INHERIT_MULTIPLE_STYLES_TTML_FILE); + assertThat(subtitle.getEventTimeCount()).isEqualTo(12); - assertSpans( - subtitle, - 30, - "text 2.5", - "sansSerifInline", - TtmlStyle.STYLE_ITALIC, - 0xFFFF0000, - 0xFFFFFF00, - true, - true, - null); + + Spanned thirdCueText = getOnlyCueTextAtTimeUs(subtitle, 30_000_000); + assertThat(thirdCueText.toString()).isEqualTo("text 2.5"); + assertThat(thirdCueText) + .hasTypefaceSpanBetween(0, thirdCueText.length()) + .withFamily("sansSerifInline"); + assertThat(thirdCueText).hasItalicSpanBetween(0, thirdCueText.length()); + assertThat(thirdCueText).hasUnderlineSpanBetween(0, thirdCueText.length()); + assertThat(thirdCueText).hasStrikethroughSpanBetween(0, thirdCueText.length()); + assertThat(thirdCueText) + .hasBackgroundColorSpanBetween(0, thirdCueText.length()) + .withColor(0xFFFF0000); + assertThat(thirdCueText) + .hasForegroundColorSpanBetween(0, thirdCueText.length()) + .withColor(0xFFFFFF00); } @Test - public void testMultipleRegions() throws IOException, SubtitleDecoderException { + public void multipleRegions() throws IOException, SubtitleDecoderException { TtmlSubtitle subtitle = getSubtitle(MULTIPLE_REGIONS_TTML_FILE); - List cues = subtitle.getCues(1000000); + + List cues = subtitle.getCues(1_000_000); assertThat(cues).hasSize(2); Cue cue = cues.get(0); assertThat(cue.text.toString()).isEqualTo("lorem"); @@ -276,17 +292,13 @@ public final class TtmlDecoderTest { assertThat(cue.line).isEqualTo(10f / 100f); assertThat(cue.size).isEqualTo(20f / 100f); - cues = subtitle.getCues(5000000); - assertThat(cues).hasSize(1); - cue = cues.get(0); + cue = getOnlyCueAtTimeUs(subtitle, 5_000_000); assertThat(cue.text.toString()).isEqualTo("ipsum"); assertThat(cue.position).isEqualTo(40f / 100f); assertThat(cue.line).isEqualTo(40f / 100f); assertThat(cue.size).isEqualTo(20f / 100f); - cues = subtitle.getCues(9000000); - assertThat(cues).hasSize(1); - cue = cues.get(0); + cue = getOnlyCueAtTimeUs(subtitle, 9_000_000); assertThat(cue.text.toString()).isEqualTo("dolor"); assertThat(cue.position).isEqualTo(Cue.DIMEN_UNSET); assertThat(cue.line).isEqualTo(Cue.DIMEN_UNSET); @@ -296,27 +308,25 @@ public final class TtmlDecoderTest { // assertEquals(80f / 100f, cue.line); // assertEquals(1f, cue.size); - cues = subtitle.getCues(21000000); - assertThat(cues).hasSize(1); - cue = cues.get(0); + cue = getOnlyCueAtTimeUs(subtitle, 21_000_000); assertThat(cue.text.toString()).isEqualTo("She first said this"); assertThat(cue.position).isEqualTo(45f / 100f); assertThat(cue.line).isEqualTo(45f / 100f); assertThat(cue.size).isEqualTo(35f / 100f); - cues = subtitle.getCues(25000000); - cue = cues.get(0); + + cue = getOnlyCueAtTimeUs(subtitle, 25_000_000); assertThat(cue.text.toString()).isEqualTo("She first said this\nThen this"); - cues = subtitle.getCues(29000000); - assertThat(cues).hasSize(1); - cue = cues.get(0); + + cue = getOnlyCueAtTimeUs(subtitle, 29_000_000); assertThat(cue.text.toString()).isEqualTo("She first said this\nThen this\nFinally this"); assertThat(cue.position).isEqualTo(45f / 100f); assertThat(cue.line).isEqualTo(45f / 100f); } @Test - public void testEmptyStyleAttribute() throws IOException, SubtitleDecoderException { + public void emptyStyleAttribute() throws IOException, SubtitleDecoderException { TtmlSubtitle subtitle = getSubtitle(INHERIT_MULTIPLE_STYLES_TTML_FILE); + assertThat(subtitle.getEventTimeCount()).isEqualTo(12); TtmlNode root = subtitle.getRoot(); @@ -327,8 +337,9 @@ public final class TtmlDecoderTest { } @Test - public void testNonexistingStyleId() throws IOException, SubtitleDecoderException { + public void nonexistingStyleId() throws IOException, SubtitleDecoderException { TtmlSubtitle subtitle = getSubtitle(INHERIT_MULTIPLE_STYLES_TTML_FILE); + assertThat(subtitle.getEventTimeCount()).isEqualTo(12); TtmlNode root = subtitle.getRoot(); @@ -339,9 +350,10 @@ public final class TtmlDecoderTest { } @Test - public void testNonExistingAndExistingStyleIdWithRedundantSpaces() + public void nonExistingAndExistingStyleIdWithRedundantSpaces() throws IOException, SubtitleDecoderException { TtmlSubtitle subtitle = getSubtitle(INHERIT_MULTIPLE_STYLES_TTML_FILE); + assertThat(subtitle.getEventTimeCount()).isEqualTo(12); TtmlNode root = subtitle.getRoot(); @@ -353,8 +365,9 @@ public final class TtmlDecoderTest { } @Test - public void testMultipleChaining() throws IOException, SubtitleDecoderException { + public void multipleChaining() throws IOException, SubtitleDecoderException { TtmlSubtitle subtitle = getSubtitle(CHAIN_MULTIPLE_STYLES_TTML_FILE); + assertThat(subtitle.getEventTimeCount()).isEqualTo(2); Map globalStyles = subtitle.getGlobalStyles(); @@ -376,8 +389,9 @@ public final class TtmlDecoderTest { } @Test - public void testNoUnderline() throws IOException, SubtitleDecoderException { + public void noUnderline() throws IOException, SubtitleDecoderException { TtmlSubtitle subtitle = getSubtitle(NO_UNDERLINE_LINETHROUGH_TTML_FILE); + assertThat(subtitle.getEventTimeCount()).isEqualTo(4); TtmlNode root = subtitle.getRoot(); @@ -391,8 +405,9 @@ public final class TtmlDecoderTest { } @Test - public void testNoLinethrough() throws IOException, SubtitleDecoderException { + public void noLinethrough() throws IOException, SubtitleDecoderException { TtmlSubtitle subtitle = getSubtitle(NO_UNDERLINE_LINETHROUGH_TTML_FILE); + assertThat(subtitle.getEventTimeCount()).isEqualTo(4); TtmlNode root = subtitle.getRoot(); @@ -406,95 +421,82 @@ public final class TtmlDecoderTest { } @Test - public void testFontSizeSpans() throws IOException, SubtitleDecoderException { + public void fontSizeSpans() throws IOException, SubtitleDecoderException { TtmlSubtitle subtitle = getSubtitle(FONT_SIZE_TTML_FILE); + assertThat(subtitle.getEventTimeCount()).isEqualTo(10); - List cues = subtitle.getCues(10 * 1000000); - assertThat(cues).hasSize(1); - SpannableStringBuilder spannable = (SpannableStringBuilder) cues.get(0).text; - assertThat(String.valueOf(spannable)).isEqualTo("text 1"); - assertAbsoluteFontSize(spannable, 32); + Spanned spanned = getOnlyCueTextAtTimeUs(subtitle, 10_000_000); + assertThat(String.valueOf(spanned)).isEqualTo("text 1"); + assertThat(spanned).hasAbsoluteSizeSpanBetween(0, spanned.length()).withAbsoluteSize(32); - cues = subtitle.getCues(20 * 1000000); - assertThat(cues).hasSize(1); - spannable = (SpannableStringBuilder) cues.get(0).text; - assertThat(String.valueOf(cues.get(0).text)).isEqualTo("text 2"); - assertRelativeFontSize(spannable, 2.2f); + spanned = getOnlyCueTextAtTimeUs(subtitle, 20_000_000); + assertThat(spanned.toString()).isEqualTo("text 2"); + assertThat(spanned).hasRelativeSizeSpanBetween(0, spanned.length()).withSizeChange(2.2f); - cues = subtitle.getCues(30 * 1000000); - assertThat(cues).hasSize(1); - spannable = (SpannableStringBuilder) cues.get(0).text; - assertThat(String.valueOf(cues.get(0).text)).isEqualTo("text 3"); - assertRelativeFontSize(spannable, 1.5f); + spanned = getOnlyCueTextAtTimeUs(subtitle, 30_000_000); + assertThat(spanned.toString()).isEqualTo("text 3"); + assertThat(spanned).hasRelativeSizeSpanBetween(0, spanned.length()).withSizeChange(1.5f); - cues = subtitle.getCues(40 * 1000000); - assertThat(cues).hasSize(1); - spannable = (SpannableStringBuilder) cues.get(0).text; - assertThat(String.valueOf(cues.get(0).text)).isEqualTo("two values"); - assertAbsoluteFontSize(spannable, 16); + spanned = getOnlyCueTextAtTimeUs(subtitle, 40_000_000); + assertThat(spanned.toString()).isEqualTo("two values"); + assertThat(spanned).hasAbsoluteSizeSpanBetween(0, spanned.length()).withAbsoluteSize(16); - cues = subtitle.getCues(50 * 1000000); - assertThat(cues).hasSize(1); - spannable = (SpannableStringBuilder) cues.get(0).text; - assertThat(String.valueOf(cues.get(0).text)).isEqualTo("leading dot"); - assertRelativeFontSize(spannable, 0.5f); + spanned = getOnlyCueTextAtTimeUs(subtitle, 50_000_000); + assertThat(spanned.toString()).isEqualTo("leading dot"); + assertThat(spanned).hasRelativeSizeSpanBetween(0, spanned.length()).withSizeChange(0.5f); } @Test - public void testFontSizeWithMissingUnitIsIgnored() throws IOException, SubtitleDecoderException { + public void fontSizeWithMissingUnitIsIgnored() throws IOException, SubtitleDecoderException { TtmlSubtitle subtitle = getSubtitle(FONT_SIZE_MISSING_UNIT_TTML_FILE); + assertThat(subtitle.getEventTimeCount()).isEqualTo(2); - List cues = subtitle.getCues(10 * 1000000); - assertThat(cues).hasSize(1); - SpannableStringBuilder spannable = (SpannableStringBuilder) cues.get(0).text; - assertThat(String.valueOf(spannable)).isEqualTo("no unit"); - assertThat(spannable.getSpans(0, spannable.length(), RelativeSizeSpan.class)).hasLength(0); - assertThat(spannable.getSpans(0, spannable.length(), AbsoluteSizeSpan.class)).hasLength(0); + + Spanned spanned = getOnlyCueTextAtTimeUs(subtitle, 10_000_000); + assertThat(spanned.toString()).isEqualTo("no unit"); + assertThat(spanned).hasNoRelativeSizeSpanBetween(0, spanned.length()); + assertThat(spanned).hasNoAbsoluteSizeSpanBetween(0, spanned.length()); } @Test - public void testFontSizeWithInvalidValueIsIgnored() throws IOException, SubtitleDecoderException { + public void fontSizeWithInvalidValueIsIgnored() throws IOException, SubtitleDecoderException { TtmlSubtitle subtitle = getSubtitle(FONT_SIZE_INVALID_TTML_FILE); + assertThat(subtitle.getEventTimeCount()).isEqualTo(6); - List cues = subtitle.getCues(10 * 1000000); - assertThat(cues).hasSize(1); - SpannableStringBuilder spannable = (SpannableStringBuilder) cues.get(0).text; - assertThat(String.valueOf(spannable)).isEqualTo("invalid"); - assertThat(spannable.getSpans(0, spannable.length(), RelativeSizeSpan.class)).hasLength(0); - assertThat(spannable.getSpans(0, spannable.length(), AbsoluteSizeSpan.class)).hasLength(0); + Spanned spanned = getOnlyCueTextAtTimeUs(subtitle, 10_000_000); + assertThat(String.valueOf(spanned)).isEqualTo("invalid"); + assertThat(spanned).hasNoRelativeSizeSpanBetween(0, spanned.length()); + assertThat(spanned).hasNoAbsoluteSizeSpanBetween(0, spanned.length()); - cues = subtitle.getCues(20 * 1000000); - assertThat(cues).hasSize(1); - spannable = (SpannableStringBuilder) cues.get(0).text; - assertThat(String.valueOf(spannable)).isEqualTo("invalid"); - assertThat(spannable.getSpans(0, spannable.length(), RelativeSizeSpan.class)).hasLength(0); - assertThat(spannable.getSpans(0, spannable.length(), AbsoluteSizeSpan.class)).hasLength(0); + spanned = getOnlyCueTextAtTimeUs(subtitle, 20_000_000); + assertThat(String.valueOf(spanned)).isEqualTo("invalid"); + assertThat(spanned).hasNoRelativeSizeSpanBetween(0, spanned.length()); + assertThat(spanned).hasNoAbsoluteSizeSpanBetween(0, spanned.length()); - cues = subtitle.getCues(30 * 1000000); - assertThat(cues).hasSize(1); - spannable = (SpannableStringBuilder) cues.get(0).text; - assertThat(String.valueOf(spannable)).isEqualTo("invalid dot"); - assertThat(spannable.getSpans(0, spannable.length(), RelativeSizeSpan.class)).hasLength(0); - assertThat(spannable.getSpans(0, spannable.length(), AbsoluteSizeSpan.class)).hasLength(0); + spanned = getOnlyCueTextAtTimeUs(subtitle, 30_000_000); + assertThat(String.valueOf(spanned)).isEqualTo("invalid dot"); + assertThat(spanned).hasNoRelativeSizeSpanBetween(0, spanned.length()); + assertThat(spanned).hasNoAbsoluteSizeSpanBetween(0, spanned.length()); } @Test - public void testFontSizeWithEmptyValueIsIgnored() throws IOException, SubtitleDecoderException { + public void fontSizeWithEmptyValueIsIgnored() throws IOException, SubtitleDecoderException { TtmlSubtitle subtitle = getSubtitle(FONT_SIZE_EMPTY_TTML_FILE); + assertThat(subtitle.getEventTimeCount()).isEqualTo(2); - List cues = subtitle.getCues(10 * 1000000); - assertThat(cues).hasSize(1); - SpannableStringBuilder spannable = (SpannableStringBuilder) cues.get(0).text; - assertThat(String.valueOf(spannable)).isEqualTo("empty"); - assertThat(spannable.getSpans(0, spannable.length(), RelativeSizeSpan.class)).hasLength(0); - assertThat(spannable.getSpans(0, spannable.length(), AbsoluteSizeSpan.class)).hasLength(0); + + Spanned spanned = getOnlyCueTextAtTimeUs(subtitle, 10_000_000); + assertThat(String.valueOf(spanned)).isEqualTo("empty"); + assertThat(spanned).hasNoRelativeSizeSpanBetween(0, spanned.length()); + assertThat(spanned).hasNoAbsoluteSizeSpanBetween(0, spanned.length()); } @Test - public void testFrameRate() throws IOException, SubtitleDecoderException { + public void frameRate() throws IOException, SubtitleDecoderException { TtmlSubtitle subtitle = getSubtitle(FRAME_RATE_TTML_FILE); + assertThat(subtitle.getEventTimeCount()).isEqualTo(4); assertThat(subtitle.getEventTime(0)).isEqualTo(1_000_000); assertThat(subtitle.getEventTime(1)).isEqualTo(1_010_000); @@ -503,12 +505,10 @@ public final class TtmlDecoderTest { } @Test - public void testBitmapPercentageRegion() throws IOException, SubtitleDecoderException { + public void bitmapPercentageRegion() throws IOException, SubtitleDecoderException { TtmlSubtitle subtitle = getSubtitle(BITMAP_REGION_FILE); - List cues = subtitle.getCues(1000000); - assertThat(cues).hasSize(1); - Cue cue = cues.get(0); + Cue cue = getOnlyCueAtTimeUs(subtitle, 1_000_000); assertThat(cue.text).isNull(); assertThat(cue.bitmap).isNotNull(); assertThat(cue.position).isEqualTo(24f / 100f); @@ -516,9 +516,7 @@ public final class TtmlDecoderTest { assertThat(cue.size).isEqualTo(51f / 100f); assertThat(cue.bitmapHeight).isEqualTo(12f / 100f); - cues = subtitle.getCues(4000000); - assertThat(cues).hasSize(1); - cue = cues.get(0); + cue = getOnlyCueAtTimeUs(subtitle, 4_000_000); assertThat(cue.text).isNull(); assertThat(cue.bitmap).isNotNull(); assertThat(cue.position).isEqualTo(21f / 100f); @@ -526,9 +524,7 @@ public final class TtmlDecoderTest { assertThat(cue.size).isEqualTo(57f / 100f); assertThat(cue.bitmapHeight).isEqualTo(6f / 100f); - cues = subtitle.getCues(7500000); - assertThat(cues).hasSize(1); - cue = cues.get(0); + cue = getOnlyCueAtTimeUs(subtitle, 7_500_000); assertThat(cue.text).isNull(); assertThat(cue.bitmap).isNotNull(); assertThat(cue.position).isEqualTo(24f / 100f); @@ -538,12 +534,10 @@ public final class TtmlDecoderTest { } @Test - public void testBitmapPixelRegion() throws IOException, SubtitleDecoderException { + public void bitmapPixelRegion() throws IOException, SubtitleDecoderException { TtmlSubtitle subtitle = getSubtitle(BITMAP_PIXEL_REGION_FILE); - List cues = subtitle.getCues(1000000); - assertThat(cues).hasSize(1); - Cue cue = cues.get(0); + Cue cue = getOnlyCueAtTimeUs(subtitle, 1_000_000); assertThat(cue.text).isNull(); assertThat(cue.bitmap).isNotNull(); assertThat(cue.position).isEqualTo(307f / 1280f); @@ -551,9 +545,7 @@ public final class TtmlDecoderTest { assertThat(cue.size).isEqualTo(653f / 1280f); assertThat(cue.bitmapHeight).isEqualTo(86f / 720f); - cues = subtitle.getCues(4000000); - assertThat(cues).hasSize(1); - cue = cues.get(0); + cue = getOnlyCueAtTimeUs(subtitle, 4_000_000); assertThat(cue.text).isNull(); assertThat(cue.bitmap).isNotNull(); assertThat(cue.position).isEqualTo(269f / 1280f); @@ -563,12 +555,10 @@ public final class TtmlDecoderTest { } @Test - public void testBitmapUnsupportedRegion() throws IOException, SubtitleDecoderException { + public void bitmapUnsupportedRegion() throws IOException, SubtitleDecoderException { TtmlSubtitle subtitle = getSubtitle(BITMAP_UNSUPPORTED_REGION_FILE); - List cues = subtitle.getCues(1000000); - assertThat(cues).hasSize(1); - Cue cue = cues.get(0); + Cue cue = getOnlyCueAtTimeUs(subtitle, 1_000_000); assertThat(cue.text).isNull(); assertThat(cue.bitmap).isNotNull(); assertThat(cue.position).isEqualTo(Cue.DIMEN_UNSET); @@ -576,9 +566,7 @@ public final class TtmlDecoderTest { assertThat(cue.size).isEqualTo(Cue.DIMEN_UNSET); assertThat(cue.bitmapHeight).isEqualTo(Cue.DIMEN_UNSET); - cues = subtitle.getCues(4000000); - assertThat(cues).hasSize(1); - cue = cues.get(0); + cue = getOnlyCueAtTimeUs(subtitle, 4_000_000); assertThat(cue.text).isNull(); assertThat(cue.bitmap).isNotNull(); assertThat(cue.position).isEqualTo(Cue.DIMEN_UNSET); @@ -587,106 +575,83 @@ public final class TtmlDecoderTest { assertThat(cue.bitmapHeight).isEqualTo(Cue.DIMEN_UNSET); } - private void assertSpans( - TtmlSubtitle subtitle, - int second, - String text, - String font, - int fontStyle, - int backgroundColor, - int color, - boolean isUnderline, - boolean isLinethrough, - Layout.Alignment alignment) { + @Test + public void verticalText() throws IOException, SubtitleDecoderException { + TtmlSubtitle subtitle = getSubtitle(VERTICAL_TEXT_FILE); - long timeUs = second * 1000000L; + Cue firstCue = getOnlyCueAtTimeUs(subtitle, 10_000_000); + assertThat(firstCue.verticalType).isEqualTo(Cue.VERTICAL_TYPE_RL); + + Cue secondCue = getOnlyCueAtTimeUs(subtitle, 20_000_000); + assertThat(secondCue.verticalType).isEqualTo(Cue.VERTICAL_TYPE_LR); + + Cue thirdCue = getOnlyCueAtTimeUs(subtitle, 30_000_000); + assertThat(thirdCue.verticalType).isEqualTo(Cue.TYPE_UNSET); + } + + @Test + public void textCombine() throws IOException, SubtitleDecoderException { + TtmlSubtitle subtitle = getSubtitle(TEXT_COMBINE_FILE); + + Spanned firstCue = getOnlyCueTextAtTimeUs(subtitle, 10_000_000); + assertThat(firstCue) + .hasHorizontalTextInVerticalContextSpanBetween( + "text with ".length(), "text with combined".length()); + + Spanned secondCue = getOnlyCueTextAtTimeUs(subtitle, 20_000_000); + assertThat(secondCue) + .hasNoHorizontalTextInVerticalContextSpanBetween( + "text with ".length(), "text with un-combined".length()); + + Spanned thirdCue = getOnlyCueTextAtTimeUs(subtitle, 30_000_000); + assertThat(thirdCue).hasNoHorizontalTextInVerticalContextSpanBetween(0, thirdCue.length()); + } + + @Test + public void rubies() throws IOException, SubtitleDecoderException { + TtmlSubtitle subtitle = getSubtitle(RUBIES_FILE); + + Spanned firstCue = getOnlyCueTextAtTimeUs(subtitle, 10_000_000); + assertThat(firstCue.toString()).isEqualTo("Cue with annotated text."); + assertThat(firstCue) + .hasRubySpanBetween("Cue with ".length(), "Cue with annotated".length()) + .withTextAndPosition("1st rubies", RubySpan.POSITION_OVER); + assertThat(firstCue) + .hasRubySpanBetween("Cue with annotated ".length(), "Cue with annotated text".length()) + .withTextAndPosition("2nd rubies", RubySpan.POSITION_UNKNOWN); + + Spanned secondCue = getOnlyCueTextAtTimeUs(subtitle, 20_000_000); + assertThat(secondCue.toString()).isEqualTo("Cue with annotated text."); + assertThat(secondCue) + .hasRubySpanBetween("Cue with ".length(), "Cue with annotated".length()) + .withTextAndPosition("rubies", RubySpan.POSITION_UNKNOWN); + + Spanned thirdCue = getOnlyCueTextAtTimeUs(subtitle, 30_000_000); + assertThat(thirdCue.toString()).isEqualTo("Cue with annotated text."); + assertThat(thirdCue).hasNoRubySpanBetween(0, thirdCue.length()); + + Spanned fourthCue = getOnlyCueTextAtTimeUs(subtitle, 40_000_000); + assertThat(fourthCue.toString()).isEqualTo("Cue with text."); + assertThat(fourthCue).hasNoRubySpanBetween(0, fourthCue.length()); + + Spanned fifthCue = getOnlyCueTextAtTimeUs(subtitle, 50_000_000); + assertThat(fifthCue.toString()).isEqualTo("Cue with annotated text."); + assertThat(fifthCue).hasNoRubySpanBetween(0, fifthCue.length()); + } + + private static Spanned getOnlyCueTextAtTimeUs(Subtitle subtitle, long timeUs) { + Cue cue = getOnlyCueAtTimeUs(subtitle, timeUs); + assertThat(cue.text).isInstanceOf(Spanned.class); + return (Spanned) Assertions.checkNotNull(cue.text); + } + + private static Cue getOnlyCueAtTimeUs(Subtitle subtitle, long timeUs) { List cues = subtitle.getCues(timeUs); - assertThat(cues).hasSize(1); - assertThat(String.valueOf(cues.get(0).text)).isEqualTo(text); - assertWithMessage("single cue expected for timeUs: " + timeUs).that(cues.size()).isEqualTo(1); - SpannableStringBuilder spannable = (SpannableStringBuilder) cues.get(0).text; - - assertFont(spannable, font); - assertStyle(spannable, fontStyle); - assertUnderline(spannable, isUnderline); - assertStrikethrough(spannable, isLinethrough); - assertUnderline(spannable, isUnderline); - assertBackground(spannable, backgroundColor); - assertForeground(spannable, color); - assertAlignment(spannable, alignment); + return cues.get(0); } - private void assertAbsoluteFontSize(Spannable spannable, int absoluteFontSize) { - AbsoluteSizeSpan[] absoluteSizeSpans = - spannable.getSpans(0, spannable.length(), AbsoluteSizeSpan.class); - assertThat(absoluteSizeSpans).hasLength(1); - assertThat(absoluteSizeSpans[0].getSize()).isEqualTo(absoluteFontSize); - } - - private void assertRelativeFontSize(Spannable spannable, float relativeFontSize) { - RelativeSizeSpan[] relativeSizeSpans = - spannable.getSpans(0, spannable.length(), RelativeSizeSpan.class); - assertThat(relativeSizeSpans).hasLength(1); - assertThat(relativeSizeSpans[0].getSizeChange()).isEqualTo(relativeFontSize); - } - - private void assertFont(Spannable spannable, String font) { - TypefaceSpan[] typefaceSpans = spannable.getSpans(0, spannable.length(), TypefaceSpan.class); - assertThat(typefaceSpans[typefaceSpans.length - 1].getFamily()).isEqualTo(font); - } - - private void assertStyle(Spannable spannable, int fontStyle) { - StyleSpan[] styleSpans = spannable.getSpans(0, spannable.length(), StyleSpan.class); - assertThat(styleSpans[styleSpans.length - 1].getStyle()).isEqualTo(fontStyle); - } - - private void assertUnderline(Spannable spannable, boolean isUnderline) { - UnderlineSpan[] underlineSpans = spannable.getSpans(0, spannable.length(), UnderlineSpan.class); - assertWithMessage(isUnderline ? "must be underlined" : "must not be underlined") - .that(underlineSpans) - .hasLength(isUnderline ? 1 : 0); - } - - private void assertStrikethrough(Spannable spannable, boolean isStrikethrough) { - StrikethroughSpan[] striketroughSpans = - spannable.getSpans(0, spannable.length(), StrikethroughSpan.class); - assertWithMessage(isStrikethrough ? "must be strikethrough" : "must not be strikethrough") - .that(striketroughSpans) - .hasLength(isStrikethrough ? 1 : 0); - } - - private void assertBackground(Spannable spannable, int backgroundColor) { - BackgroundColorSpan[] backgroundColorSpans = - spannable.getSpans(0, spannable.length(), BackgroundColorSpan.class); - if (backgroundColor != 0) { - assertThat(backgroundColorSpans[backgroundColorSpans.length - 1].getBackgroundColor()) - .isEqualTo(backgroundColor); - } else { - assertThat(backgroundColorSpans).hasLength(0); - } - } - - private void assertForeground(Spannable spannable, int foregroundColor) { - ForegroundColorSpan[] foregroundColorSpans = - spannable.getSpans(0, spannable.length(), ForegroundColorSpan.class); - assertThat(foregroundColorSpans[foregroundColorSpans.length - 1].getForegroundColor()) - .isEqualTo(foregroundColor); - } - - private void assertAlignment(Spannable spannable, Layout.Alignment alignment) { - if (alignment != null) { - AlignmentSpan.Standard[] alignmentSpans = - spannable.getSpans(0, spannable.length(), AlignmentSpan.Standard.class); - assertThat(alignmentSpans).hasLength(1); - assertThat(alignmentSpans[0].getAlignment()).isEqualTo(alignment); - } else { - assertThat(spannable.getSpans(0, spannable.length(), AlignmentSpan.Standard.class)) - .hasLength(0); - } - } - - private TtmlNode queryChildrenForTag(TtmlNode node, String tag, int pos) { + private static TtmlNode queryChildrenForTag(TtmlNode node, String tag, int pos) { int count = 0; for (int i = 0; i < node.getChildCount(); i++) { if (tag.equals(node.getChild(i).tag)) { @@ -698,7 +663,8 @@ public final class TtmlDecoderTest { throw new IllegalStateException("tag not found"); } - private TtmlSubtitle getSubtitle(String file) throws IOException, SubtitleDecoderException { + private static TtmlSubtitle getSubtitle(String file) + throws IOException, SubtitleDecoderException { TtmlDecoder ttmlDecoder = new TtmlDecoder(); byte[] bytes = TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), file); return (TtmlSubtitle) ttmlDecoder.decode(bytes, bytes.length, false); diff --git a/library/core/src/test/java/com/google/android/exoplayer2/text/ttml/TtmlRenderUtilTest.java b/library/core/src/test/java/com/google/android/exoplayer2/text/ttml/TtmlRenderUtilTest.java index 8785229b7c..f9d12aefd1 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/text/ttml/TtmlRenderUtilTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/text/ttml/TtmlRenderUtilTest.java @@ -35,12 +35,12 @@ import org.junit.runner.RunWith; public final class TtmlRenderUtilTest { @Test - public void testResolveStyleNoStyleAtAll() { + public void resolveStyleNoStyleAtAll() { assertThat(resolveStyle(null, null, null)).isNull(); } @Test - public void testResolveStyleSingleReferentialStyle() { + public void resolveStyleSingleReferentialStyle() { Map globalStyles = getGlobalStyles(); String[] styleIds = {"s0"}; @@ -49,7 +49,7 @@ public final class TtmlRenderUtilTest { } @Test - public void testResolveStyleMultipleReferentialStyles() { + public void resolveStyleMultipleReferentialStyles() { Map globalStyles = getGlobalStyles(); String[] styleIds = {"s0", "s1"}; @@ -67,7 +67,7 @@ public final class TtmlRenderUtilTest { } @Test - public void testResolveMergeSingleReferentialStyleIntoInlineStyle() { + public void resolveMergeSingleReferentialStyleIntoInlineStyle() { Map globalStyles = getGlobalStyles(); String[] styleIds = {"s0"}; TtmlStyle style = new TtmlStyle(); @@ -83,7 +83,7 @@ public final class TtmlRenderUtilTest { } @Test - public void testResolveMergeMultipleReferentialStylesIntoInlineStyle() { + public void resolveMergeMultipleReferentialStylesIntoInlineStyle() { Map globalStyles = getGlobalStyles(); String[] styleIds = {"s0", "s1"}; TtmlStyle style = new TtmlStyle(); @@ -99,7 +99,7 @@ public final class TtmlRenderUtilTest { } @Test - public void testResolveStyleOnlyInlineStyle() { + public void resolveStyleOnlyInlineStyle() { TtmlStyle inlineStyle = new TtmlStyle(); assertThat(TtmlRenderUtil.resolveStyle(inlineStyle, null, null)).isSameInstanceAs(inlineStyle); } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/text/ttml/TtmlStyleTest.java b/library/core/src/test/java/com/google/android/exoplayer2/text/ttml/TtmlStyleTest.java index 24b5ca678f..4f75c50b12 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/text/ttml/TtmlStyleTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/text/ttml/TtmlStyleTest.java @@ -16,7 +16,6 @@ package com.google.android.exoplayer2.text.ttml; import static android.graphics.Color.BLACK; -import static android.graphics.Color.WHITE; import static com.google.android.exoplayer2.text.ttml.TtmlStyle.STYLE_BOLD; import static com.google.android.exoplayer2.text.ttml.TtmlStyle.STYLE_BOLD_ITALIC; import static com.google.android.exoplayer2.text.ttml.TtmlStyle.STYLE_ITALIC; @@ -26,8 +25,11 @@ import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertWithMessage; import android.graphics.Color; +import android.text.Layout; +import androidx.annotation.ColorInt; import androidx.test.ext.junit.runners.AndroidJUnit4; -import org.junit.Before; +import com.google.android.exoplayer2.text.Cue; +import com.google.android.exoplayer2.text.span.RubySpan; import org.junit.Test; import org.junit.runner.RunWith; @@ -35,58 +37,93 @@ import org.junit.runner.RunWith; @RunWith(AndroidJUnit4.class) public final class TtmlStyleTest { - private static final String FONT_FAMILY = "serif"; private static final String ID = "id"; - public static final int FOREGROUND_COLOR = Color.WHITE; - public static final int BACKGROUND_COLOR = Color.BLACK; - private TtmlStyle style; + private static final String FONT_FAMILY = "serif"; + @ColorInt private static final int FONT_COLOR = Color.WHITE; + private static final float FONT_SIZE = 12.5f; + @TtmlStyle.FontSizeUnit private static final int FONT_SIZE_UNIT = TtmlStyle.FONT_SIZE_UNIT_EM; + @ColorInt private static final int BACKGROUND_COLOR = Color.BLACK; + private static final int RUBY_TYPE = TtmlStyle.RUBY_TYPE_TEXT; + private static final int RUBY_POSITION = RubySpan.POSITION_UNDER; + private static final Layout.Alignment TEXT_ALIGN = Layout.Alignment.ALIGN_CENTER; + private static final boolean TEXT_COMBINE = true; + @Cue.VerticalType private static final int VERTICAL_TYPE = Cue.VERTICAL_TYPE_RL; - @Before - public void setUp() throws Exception { - style = new TtmlStyle(); - } + private final TtmlStyle populatedStyle = + new TtmlStyle() + .setId(ID) + .setItalic(true) + .setBold(true) + .setBackgroundColor(BACKGROUND_COLOR) + .setFontColor(FONT_COLOR) + .setLinethrough(true) + .setUnderline(true) + .setFontFamily(FONT_FAMILY) + .setFontSize(FONT_SIZE) + .setFontSizeUnit(FONT_SIZE_UNIT) + .setRubyType(RUBY_TYPE) + .setRubyPosition(RUBY_POSITION) + .setTextAlign(TEXT_ALIGN) + .setTextCombine(TEXT_COMBINE) + .setVerticalType(VERTICAL_TYPE); @Test - public void testInheritStyle() { - style.inherit(createAncestorStyle()); + public void inheritStyle() { + TtmlStyle style = new TtmlStyle(); + style.inherit(populatedStyle); + assertWithMessage("id must not be inherited").that(style.getId()).isNull(); assertThat(style.isUnderline()).isTrue(); assertThat(style.isLinethrough()).isTrue(); assertThat(style.getStyle()).isEqualTo(STYLE_BOLD_ITALIC); assertThat(style.getFontFamily()).isEqualTo(FONT_FAMILY); - assertThat(style.getFontColor()).isEqualTo(WHITE); - assertWithMessage("do not inherit backgroundColor").that(style.hasBackgroundColor()).isFalse(); + assertThat(style.getFontColor()).isEqualTo(FONT_COLOR); + assertThat(style.getFontSize()).isEqualTo(FONT_SIZE); + assertThat(style.getFontSizeUnit()).isEqualTo(FONT_SIZE_UNIT); + assertThat(style.getRubyPosition()).isEqualTo(RUBY_POSITION); + assertThat(style.getTextAlign()).isEqualTo(TEXT_ALIGN); + assertThat(style.getTextCombine()).isEqualTo(TEXT_COMBINE); + assertWithMessage("rubyType should not be inherited") + .that(style.getRubyType()) + .isEqualTo(UNSPECIFIED); + assertWithMessage("backgroundColor should not be inherited") + .that(style.hasBackgroundColor()) + .isFalse(); + assertWithMessage("verticalType should not be inherited") + .that(style.getVerticalType()) + .isEqualTo(Cue.TYPE_UNSET); } @Test - public void testChainStyle() { - style.chain(createAncestorStyle()); + public void chainStyle() { + TtmlStyle style = new TtmlStyle(); + + style.chain(populatedStyle); + assertWithMessage("id must not be inherited").that(style.getId()).isNull(); assertThat(style.isUnderline()).isTrue(); assertThat(style.isLinethrough()).isTrue(); assertThat(style.getStyle()).isEqualTo(STYLE_BOLD_ITALIC); assertThat(style.getFontFamily()).isEqualTo(FONT_FAMILY); - assertThat(style.getFontColor()).isEqualTo(FOREGROUND_COLOR); - // do inherit backgroundColor when chaining - assertWithMessage("do not inherit backgroundColor when chaining") - .that(style.getBackgroundColor()).isEqualTo(BACKGROUND_COLOR); - } - - private static TtmlStyle createAncestorStyle() { - TtmlStyle ancestor = new TtmlStyle(); - ancestor.setId(ID); - ancestor.setItalic(true); - ancestor.setBold(true); - ancestor.setBackgroundColor(BACKGROUND_COLOR); - ancestor.setFontColor(FOREGROUND_COLOR); - ancestor.setLinethrough(true); - ancestor.setUnderline(true); - ancestor.setFontFamily(FONT_FAMILY); - return ancestor; + assertThat(style.getFontColor()).isEqualTo(FONT_COLOR); + assertThat(style.getFontSize()).isEqualTo(FONT_SIZE); + assertThat(style.getFontSizeUnit()).isEqualTo(FONT_SIZE_UNIT); + assertThat(style.getRubyPosition()).isEqualTo(RUBY_POSITION); + assertThat(style.getTextAlign()).isEqualTo(TEXT_ALIGN); + assertThat(style.getTextCombine()).isEqualTo(TEXT_COMBINE); + assertWithMessage("backgroundColor should be chained") + .that(style.getBackgroundColor()) + .isEqualTo(BACKGROUND_COLOR); + assertWithMessage("rubyType should be chained").that(style.getRubyType()).isEqualTo(RUBY_TYPE); + assertWithMessage("verticalType should be chained") + .that(style.getVerticalType()) + .isEqualTo(VERTICAL_TYPE); } @Test - public void testStyle() { + public void style() { + TtmlStyle style = new TtmlStyle(); + assertThat(style.getStyle()).isEqualTo(UNSPECIFIED); style.setItalic(true); assertThat(style.getStyle()).isEqualTo(STYLE_ITALIC); @@ -99,7 +136,9 @@ public final class TtmlStyleTest { } @Test - public void testLinethrough() { + public void linethrough() { + TtmlStyle style = new TtmlStyle(); + assertThat(style.isLinethrough()).isFalse(); style.setLinethrough(true); assertThat(style.isLinethrough()).isTrue(); @@ -108,7 +147,9 @@ public final class TtmlStyleTest { } @Test - public void testUnderline() { + public void underline() { + TtmlStyle style = new TtmlStyle(); + assertThat(style.isUnderline()).isFalse(); style.setUnderline(true); assertThat(style.isUnderline()).isTrue(); @@ -117,7 +158,9 @@ public final class TtmlStyleTest { } @Test - public void testFontFamily() { + public void fontFamily() { + TtmlStyle style = new TtmlStyle(); + assertThat(style.getFontFamily()).isNull(); style.setFontFamily(FONT_FAMILY); assertThat(style.getFontFamily()).isEqualTo(FONT_FAMILY); @@ -126,23 +169,47 @@ public final class TtmlStyleTest { } @Test - public void testColor() { + public void fontColor() { + TtmlStyle style = new TtmlStyle(); + assertThat(style.hasFontColor()).isFalse(); style.setFontColor(Color.BLACK); - assertThat(style.getFontColor()).isEqualTo(BLACK); assertThat(style.hasFontColor()).isTrue(); + assertThat(style.getFontColor()).isEqualTo(BLACK); } @Test - public void testBackgroundColor() { + public void fontSize() { + TtmlStyle style = new TtmlStyle(); + + assertThat(style.getFontSize()).isEqualTo(0); + style.setFontSize(10.5f); + assertThat(style.getFontSize()).isEqualTo(10.5f); + } + + @Test + public void fontSizeUnit() { + TtmlStyle style = new TtmlStyle(); + + assertThat(style.getFontSizeUnit()).isEqualTo(UNSPECIFIED); + style.setFontSizeUnit(TtmlStyle.FONT_SIZE_UNIT_EM); + assertThat(style.getFontSizeUnit()).isEqualTo(TtmlStyle.FONT_SIZE_UNIT_EM); + } + + @Test + public void backgroundColor() { + TtmlStyle style = new TtmlStyle(); + assertThat(style.hasBackgroundColor()).isFalse(); style.setBackgroundColor(Color.BLACK); - assertThat(style.getBackgroundColor()).isEqualTo(BLACK); assertThat(style.hasBackgroundColor()).isTrue(); + assertThat(style.getBackgroundColor()).isEqualTo(BLACK); } @Test - public void testId() { + public void id() { + TtmlStyle style = new TtmlStyle(); + assertThat(style.getId()).isNull(); style.setId(ID); assertThat(style.getId()).isEqualTo(ID); @@ -150,4 +217,41 @@ public final class TtmlStyleTest { assertThat(style.getId()).isNull(); } + @Test + public void rubyType() { + TtmlStyle style = new TtmlStyle(); + + assertThat(style.getRubyType()).isEqualTo(UNSPECIFIED); + style.setRubyType(TtmlStyle.RUBY_TYPE_BASE); + assertThat(style.getRubyType()).isEqualTo(TtmlStyle.RUBY_TYPE_BASE); + } + + @Test + public void rubyPosition() { + TtmlStyle style = new TtmlStyle(); + + assertThat(style.getRubyPosition()).isEqualTo(RubySpan.POSITION_UNKNOWN); + style.setRubyPosition(RubySpan.POSITION_OVER); + assertThat(style.getRubyPosition()).isEqualTo(RubySpan.POSITION_OVER); + } + + @Test + public void textAlign() { + TtmlStyle style = new TtmlStyle(); + + assertThat(style.getTextAlign()).isNull(); + style.setTextAlign(Layout.Alignment.ALIGN_OPPOSITE); + assertThat(style.getTextAlign()).isEqualTo(Layout.Alignment.ALIGN_OPPOSITE); + style.setTextAlign(null); + assertThat(style.getTextAlign()).isNull(); + } + + @Test + public void textCombine() { + TtmlStyle style = new TtmlStyle(); + + assertThat(style.getTextCombine()).isFalse(); + style.setTextCombine(true); + assertThat(style.getTextCombine()).isTrue(); + } } 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 16b997e117..143326583c 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 @@ -54,7 +54,7 @@ public final class Tx3gDecoderTest { private static final String INITIALIZATION_ALL_DEFAULTS = "tx3g/initialization_all_defaults"; @Test - public void testDecodeNoSubtitle() throws IOException, SubtitleDecoderException { + public void decodeNoSubtitle() throws IOException, SubtitleDecoderException { Tx3gDecoder decoder = new Tx3gDecoder(Collections.emptyList()); byte[] bytes = TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), NO_SUBTITLE); Subtitle subtitle = decoder.decode(bytes, bytes.length, false); @@ -62,7 +62,7 @@ public final class Tx3gDecoderTest { } @Test - public void testDecodeJustText() throws IOException, SubtitleDecoderException { + public void decodeJustText() throws IOException, SubtitleDecoderException { Tx3gDecoder decoder = new Tx3gDecoder(Collections.emptyList()); byte[] bytes = TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), SAMPLE_JUST_TEXT); @@ -74,7 +74,7 @@ public final class Tx3gDecoderTest { } @Test - public void testDecodeWithStyl() throws IOException, SubtitleDecoderException { + public void decodeWithStyl() throws IOException, SubtitleDecoderException { Tx3gDecoder decoder = new Tx3gDecoder(Collections.emptyList()); byte[] bytes = TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), SAMPLE_WITH_STYL); @@ -91,7 +91,7 @@ public final class Tx3gDecoderTest { } @Test - public void testDecodeWithStylAllDefaults() throws IOException, SubtitleDecoderException { + public void decodeWithStylAllDefaults() throws IOException, SubtitleDecoderException { Tx3gDecoder decoder = new Tx3gDecoder(Collections.emptyList()); byte[] bytes = TestUtil.getByteArray( @@ -104,7 +104,7 @@ public final class Tx3gDecoderTest { } @Test - public void testDecodeUtf16BeNoStyl() throws IOException, SubtitleDecoderException { + public void decodeUtf16BeNoStyl() throws IOException, SubtitleDecoderException { Tx3gDecoder decoder = new Tx3gDecoder(Collections.emptyList()); byte[] bytes = TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), SAMPLE_UTF16_BE_NO_STYL); @@ -116,7 +116,7 @@ public final class Tx3gDecoderTest { } @Test - public void testDecodeUtf16LeNoStyl() throws IOException, SubtitleDecoderException { + public void decodeUtf16LeNoStyl() throws IOException, SubtitleDecoderException { Tx3gDecoder decoder = new Tx3gDecoder(Collections.emptyList()); byte[] bytes = TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), SAMPLE_UTF16_LE_NO_STYL); @@ -128,7 +128,7 @@ public final class Tx3gDecoderTest { } @Test - public void testDecodeWithMultipleStyl() throws IOException, SubtitleDecoderException { + public void decodeWithMultipleStyl() throws IOException, SubtitleDecoderException { Tx3gDecoder decoder = new Tx3gDecoder(Collections.emptyList()); byte[] bytes = TestUtil.getByteArray( @@ -148,7 +148,7 @@ public final class Tx3gDecoderTest { } @Test - public void testDecodeWithOtherExtension() throws IOException, SubtitleDecoderException { + public void decodeWithOtherExtension() throws IOException, SubtitleDecoderException { Tx3gDecoder decoder = new Tx3gDecoder(Collections.emptyList()); byte[] bytes = TestUtil.getByteArray( @@ -165,7 +165,7 @@ public final class Tx3gDecoderTest { } @Test - public void testInitializationDecodeWithStyl() throws IOException, SubtitleDecoderException { + public void initializationDecodeWithStyl() throws IOException, SubtitleDecoderException { byte[] initBytes = TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), INITIALIZATION); Tx3gDecoder decoder = new Tx3gDecoder(Collections.singletonList(initBytes)); @@ -188,7 +188,7 @@ public final class Tx3gDecoderTest { } @Test - public void testInitializationDecodeWithTbox() throws IOException, SubtitleDecoderException { + public void initializationDecodeWithTbox() throws IOException, SubtitleDecoderException { byte[] initBytes = TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), INITIALIZATION); Tx3gDecoder decoder = new Tx3gDecoder(Collections.singletonList(initBytes)); @@ -209,7 +209,7 @@ public final class Tx3gDecoderTest { } @Test - public void testInitializationAllDefaultsDecodeWithStyl() + public void initializationAllDefaultsDecodeWithStyl() throws IOException, SubtitleDecoderException { byte[] initBytes = TestUtil.getByteArray( diff --git a/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/CssParserTest.java b/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/CssParserTest.java index 72be083181..7dc41eda82 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/CssParserTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/CssParserTest.java @@ -38,7 +38,7 @@ public final class CssParserTest { } @Test - public void testSkipWhitespacesAndComments() { + public void skipWhitespacesAndComments() { // Skip only whitespaces String skipOnlyWhitespaces = " \t\r\n\f End of skip\n /* */"; assertSkipsToEndOfSkip("End of skip", skipOnlyWhitespaces); @@ -61,7 +61,7 @@ public final class CssParserTest { } @Test - public void testGetInputLimit() { + public void getInputLimit() { // \r After 3 lines. String threeLinesThen3Cr = "One Line\nThen other\rAnd finally\r\r\r"; assertInputLimit("", threeLinesThen3Cr); @@ -87,7 +87,7 @@ public final class CssParserTest { } @Test - public void testParseMethodSimpleInput() { + public void parseMethodSimpleInput() { WebvttCssStyle expectedStyle = new WebvttCssStyle(); String styleBlock1 = " ::cue { color : black; background-color: PapayaWhip }"; expectedStyle.setFontColor(0xFF000000); @@ -106,7 +106,7 @@ public final class CssParserTest { } @Test - public void testParseMethodMultipleRulesInBlockInput() { + public void parseMethodMultipleRulesInBlockInput() { String styleBlock = "::cue {\n background-color\n:#00fFFe} \n::cue {\n background-color\n:#00000000}\n"; WebvttCssStyle expectedStyle = new WebvttCssStyle(); @@ -117,7 +117,7 @@ public final class CssParserTest { } @Test - public void testMultiplePropertiesInBlock() { + public void multiplePropertiesInBlock() { String styleBlock = "::cue(#id){text-decoration:underline; background-color:green;" + "color:red; font-family:Courier; font-weight:bold}"; WebvttCssStyle expectedStyle = new WebvttCssStyle(); @@ -132,7 +132,7 @@ public final class CssParserTest { } @Test - public void testRgbaColorExpression() { + public void rgbaColorExpression() { String styleBlock = "::cue(#rgb){background-color: rgba(\n10/* Ugly color */,11\t, 12\n,.1);" + "color:rgb(1,1,\n1)}"; WebvttCssStyle expectedStyle = new WebvttCssStyle(); @@ -144,7 +144,7 @@ public final class CssParserTest { } @Test - public void testGetNextToken() { + public void getNextToken() { String stringInput = " lorem:ipsum\n{dolor}#sit,amet;lorem:ipsum\r\t\f\ndolor(())\n"; ParsableByteArray input = new ParsableByteArray(Util.getUtf8Bytes(stringInput)); StringBuilder builder = new StringBuilder(); @@ -170,7 +170,7 @@ public final class CssParserTest { } @Test - public void testStyleScoreSystem() { + public void styleScoreSystem() { WebvttCssStyle style = new WebvttCssStyle(); // Universal selector. assertThat(style.getSpecificityScore("", "", new String[0], "")).isEqualTo(1); diff --git a/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/Mp4WebvttDecoderTest.java b/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/Mp4WebvttDecoderTest.java index 015ee7e23a..18a76c1a57 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/Mp4WebvttDecoderTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/Mp4WebvttDecoderTest.java @@ -88,25 +88,25 @@ public final class Mp4WebvttDecoderTest { // Positive tests. @Test - public void testSingleCueSample() throws SubtitleDecoderException { + public void singleCueSample() throws SubtitleDecoderException { Mp4WebvttDecoder decoder = new Mp4WebvttDecoder(); Subtitle result = decoder.decode(SINGLE_CUE_SAMPLE, SINGLE_CUE_SAMPLE.length, false); // Line feed must be trimmed by the decoder - Cue expectedCue = new WebvttCue.Builder().setText("Hello World").build(); + Cue expectedCue = WebvttCueParser.newCueForText("Hello World"); assertMp4WebvttSubtitleEquals(result, expectedCue); } @Test - public void testTwoCuesSample() throws SubtitleDecoderException { + public void twoCuesSample() throws SubtitleDecoderException { Mp4WebvttDecoder decoder = new Mp4WebvttDecoder(); Subtitle result = decoder.decode(DOUBLE_CUE_SAMPLE, DOUBLE_CUE_SAMPLE.length, false); - Cue firstExpectedCue = new WebvttCue.Builder().setText("Hello World").build(); - Cue secondExpectedCue = new WebvttCue.Builder().setText("Bye Bye").build(); + Cue firstExpectedCue = WebvttCueParser.newCueForText("Hello World"); + Cue secondExpectedCue = WebvttCueParser.newCueForText("Bye Bye"); assertMp4WebvttSubtitleEquals(result, firstExpectedCue, secondExpectedCue); } @Test - public void testNoCueSample() throws SubtitleDecoderException { + public void noCueSample() throws SubtitleDecoderException { Mp4WebvttDecoder decoder = new Mp4WebvttDecoder(); Subtitle result = decoder.decode(NO_CUE_SAMPLE, NO_CUE_SAMPLE.length, false); assertThat(result.getEventTimeCount()).isEqualTo(1); @@ -117,7 +117,7 @@ public final class Mp4WebvttDecoderTest { // Negative tests. @Test - public void testSampleWithIncompleteHeader() { + public void sampleWithIncompleteHeader() { Mp4WebvttDecoder decoder = new Mp4WebvttDecoder(); try { decoder.decode(INCOMPLETE_HEADER_SAMPLE, INCOMPLETE_HEADER_SAMPLE.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 32d2dc2060..f500029885 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 @@ -15,15 +15,13 @@ */ package com.google.android.exoplayer2.text.webvtt; -import static android.graphics.Typeface.BOLD; -import static android.graphics.Typeface.ITALIC; +import static com.google.android.exoplayer2.testutil.truth.SpannedSubject.assertThat; import static com.google.common.truth.Truth.assertThat; -import android.graphics.Typeface; +import android.graphics.Color; import android.text.Spanned; -import android.text.style.StyleSpan; -import android.text.style.UnderlineSpan; import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.android.exoplayer2.text.span.RubySpan; import java.util.Collections; import org.junit.Test; import org.junit.runner.RunWith; @@ -33,180 +31,230 @@ import org.junit.runner.RunWith; public final class WebvttCueParserTest { @Test - public void testParseStrictValidClassesAndTrailingTokens() throws Exception { + public void parseStrictValidClassesAndTrailingTokens() throws Exception { Spanned text = parseCueText("" + "This is text with html tags"); assertThat(text.toString()).isEqualTo("This is text with html tags"); - - UnderlineSpan[] underlineSpans = getSpans(text, UnderlineSpan.class); - StyleSpan[] styleSpans = getSpans(text, StyleSpan.class); - assertThat(underlineSpans).hasLength(1); - assertThat(styleSpans).hasLength(2); - assertThat(styleSpans[0].getStyle()).isEqualTo(ITALIC); - assertThat(styleSpans[1].getStyle()).isEqualTo(BOLD); - - assertThat(text.getSpanStart(underlineSpans[0])).isEqualTo(5); - assertThat(text.getSpanEnd(underlineSpans[0])).isEqualTo(7); - assertThat(text.getSpanStart(styleSpans[0])).isEqualTo(18); - assertThat(text.getSpanStart(styleSpans[1])).isEqualTo(18); - assertThat(text.getSpanEnd(styleSpans[0])).isEqualTo(22); - assertThat(text.getSpanEnd(styleSpans[1])).isEqualTo(22); + assertThat(text).hasUnderlineSpanBetween("This ".length(), "This is".length()); + assertThat(text) + .hasBoldItalicSpanBetween("This is text with ".length(), "This is text with html".length()); } @Test - public void testParseStrictValidUnsupportedTagsStrippedOut() throws Exception { + public void parseStrictValidUnsupportedTagsStrippedOut() throws Exception { Spanned text = parseCueText("This is text with " + "html tags"); + assertThat(text.toString()).isEqualTo("This is text with html tags"); - assertThat(getSpans(text, UnderlineSpan.class)).hasLength(0); - assertThat(getSpans(text, StyleSpan.class)).hasLength(0); + assertThat(text).hasNoSpans(); } @Test - public void testParseWellFormedUnclosedEndAtCueEnd() throws Exception { + public void parseRubyTag() throws Exception { + Spanned text = + parseCueText("Some base textwith ruby and undecorated text"); + + // The text between the tags is stripped from Cue.text and only present on the RubySpan. + assertThat(text.toString()).isEqualTo("Some base text and undecorated text"); + assertThat(text) + .hasRubySpanBetween("Some ".length(), "Some base text".length()) + .withTextAndPosition("with ruby", RubySpan.POSITION_OVER); + } + + @Test + public void parseSingleRubyTagWithMultipleRts() throws Exception { + Spanned text = parseCueText("A1B2C3"); + + // The text between the tags is stripped from Cue.text and only present on the RubySpan. + assertThat(text.toString()).isEqualTo("ABC"); + assertThat(text).hasRubySpanBetween(0, 1).withTextAndPosition("1", RubySpan.POSITION_OVER); + assertThat(text).hasRubySpanBetween(1, 2).withTextAndPosition("2", RubySpan.POSITION_OVER); + assertThat(text).hasRubySpanBetween(2, 3).withTextAndPosition("3", RubySpan.POSITION_OVER); + } + + @Test + public void parseMultipleRubyTagsWithSingleRtEach() throws Exception { + Spanned text = + parseCueText("A1B2C3"); + + // The text between the tags is stripped from Cue.text and only present on the RubySpan. + assertThat(text.toString()).isEqualTo("ABC"); + assertThat(text).hasRubySpanBetween(0, 1).withTextAndPosition("1", RubySpan.POSITION_OVER); + assertThat(text).hasRubySpanBetween(1, 2).withTextAndPosition("2", RubySpan.POSITION_OVER); + assertThat(text).hasRubySpanBetween(2, 3).withTextAndPosition("3", RubySpan.POSITION_OVER); + } + + @Test + public void parseRubyTagWithNoTextTag() throws Exception { + Spanned text = parseCueText("Some base text with no ruby text"); + + assertThat(text.toString()).isEqualTo("Some base text with no ruby text"); + assertThat(text).hasNoSpans(); + } + + @Test + public void parseRubyTagWithEmptyTextTag() throws Exception { + Spanned text = parseCueText("Some base text with empty ruby text"); + + assertThat(text.toString()).isEqualTo("Some base text with empty ruby text"); + assertThat(text) + .hasRubySpanBetween("Some ".length(), "Some base text with".length()) + .withTextAndPosition("", RubySpan.POSITION_OVER); + } + + @Test + public void parseDefaultTextColor() throws Exception { + Spanned text = parseCueText("In this sentence this text is red"); + + assertThat(text.toString()).isEqualTo("In this sentence this text is red"); + assertThat(text) + .hasForegroundColorSpanBetween( + "In this sentence ".length(), "In this sentence this text".length()) + .withColor(Color.RED); + } + + @Test + public void parseUnsupportedDefaultTextColor() throws Exception { + Spanned text = parseCueText("In this sentence this text is not papaya"); + + assertThat(text.toString()).isEqualTo("In this sentence this text is not papaya"); + assertThat(text).hasNoSpans(); + } + + @Test + public void parseDefaultBackgroundColor() throws Exception { + Spanned text = parseCueText("In this sentence this text has a cyan background"); + + assertThat(text.toString()).isEqualTo("In this sentence this text has a cyan background"); + assertThat(text) + .hasBackgroundColorSpanBetween( + "In this sentence ".length(), "In this sentence this text".length()) + .withColor(Color.CYAN); + } + + @Test + public void parseUnsupportedDefaultBackgroundColor() throws Exception { + Spanned text = + parseCueText( + "In this sentence this text doesn't have a papaya background"); + + assertThat(text.toString()) + .isEqualTo("In this sentence this text doesn't have a papaya background"); + assertThat(text).hasNoSpans(); + } + + @Test + public void parseWellFormedUnclosedEndAtCueEnd() throws Exception { Spanned text = parseCueText("An unclosed u tag with " + "italic inside"); assertThat(text.toString()).isEqualTo("An unclosed u tag with italic inside"); - - UnderlineSpan[] underlineSpans = getSpans(text, UnderlineSpan.class); - StyleSpan[] styleSpans = getSpans(text, StyleSpan.class); - assertThat(underlineSpans).hasLength(1); - assertThat(styleSpans).hasLength(1); - assertThat(styleSpans[0].getStyle()).isEqualTo(ITALIC); - - assertThat(text.getSpanStart(underlineSpans[0])).isEqualTo(3); - assertThat(text.getSpanStart(styleSpans[0])).isEqualTo(23); - assertThat(text.getSpanEnd(styleSpans[0])).isEqualTo(29); - assertThat(text.getSpanEnd(underlineSpans[0])).isEqualTo(36); + assertThat(text) + .hasUnderlineSpanBetween("An ".length(), "An unclosed u tag with italic inside".length()); + assertThat(text) + .hasItalicSpanBetween( + "An unclosed u tag with ".length(), "An unclosed u tag with italic".length()); } @Test - public void testParseWellFormedUnclosedEndAtParent() throws Exception { - Spanned text = parseCueText("An unclosed u tag with underline and italic inside"); + public void parseWellFormedUnclosedEndAtParent() throws Exception { + Spanned text = parseCueText("An italic tag with unclosed underline inside"); - assertThat(text.toString()).isEqualTo("An unclosed u tag with underline and italic inside"); - - UnderlineSpan[] underlineSpans = getSpans(text, UnderlineSpan.class); - StyleSpan[] styleSpans = getSpans(text, StyleSpan.class); - assertThat(underlineSpans).hasLength(1); - assertThat(styleSpans).hasLength(1); - - assertThat(text.getSpanStart(underlineSpans[0])).isEqualTo(23); - assertThat(text.getSpanStart(styleSpans[0])).isEqualTo(23); - assertThat(text.getSpanEnd(underlineSpans[0])).isEqualTo(43); - assertThat(text.getSpanEnd(styleSpans[0])).isEqualTo(43); - - assertThat(styleSpans[0].getStyle()).isEqualTo(ITALIC); + assertThat(text.toString()).isEqualTo("An italic tag with unclosed underline inside"); + assertThat(text) + .hasItalicSpanBetween( + "An italic tag with unclosed ".length(), + "An italic tag with unclosed underline".length()); + assertThat(text) + .hasUnderlineSpanBetween( + "An italic tag with unclosed ".length(), + "An italic tag with unclosed underline".length()); } @Test - public void testParseMalformedNestedElements() throws Exception { - Spanned text = parseCueText("An unclosed u tag with italic inside"); - assertThat(text.toString()).isEqualTo("An unclosed u tag with italic inside"); + public void parseMalformedNestedElements() throws Exception { + Spanned text = parseCueText("Overlapping u and i tags"); - UnderlineSpan[] underlineSpans = getSpans(text, UnderlineSpan.class); - StyleSpan[] styleSpans = getSpans(text, StyleSpan.class); - assertThat(underlineSpans).hasLength(1); - assertThat(styleSpans).hasLength(2); - - // all tags applied until matching start tag found - assertThat(text.getSpanStart(underlineSpans[0])).isEqualTo(0); - assertThat(text.getSpanEnd(underlineSpans[0])).isEqualTo(29); - if (styleSpans[0].getStyle() == Typeface.BOLD) { - assertThat(text.getSpanStart(styleSpans[0])).isEqualTo(0); - assertThat(text.getSpanStart(styleSpans[1])).isEqualTo(23); - assertThat(text.getSpanEnd(styleSpans[1])).isEqualTo(29); - assertThat(text.getSpanEnd(styleSpans[0])).isEqualTo(36); - } else { - assertThat(text.getSpanStart(styleSpans[1])).isEqualTo(0); - assertThat(text.getSpanStart(styleSpans[0])).isEqualTo(23); - assertThat(text.getSpanEnd(styleSpans[0])).isEqualTo(29); - assertThat(text.getSpanEnd(styleSpans[1])).isEqualTo(36); - } + String expectedText = "Overlapping u and i tags"; + assertThat(text.toString()).isEqualTo(expectedText); + assertThat(text).hasBoldSpanBetween(0, expectedText.length()); + // Text between the tags is underlined. + assertThat(text).hasUnderlineSpanBetween(0, "Overlapping u and".length()); + // Only text from to <\\u> is italic (unexpected - but simplifies the parsing). + assertThat(text).hasItalicSpanBetween("Overlapping u ".length(), "Overlapping u and".length()); } @Test - public void testParseCloseNonExistingTag() throws Exception { - Spanned text = parseCueText("blahblahblahblah"); - assertThat(text.toString()).isEqualTo("blahblahblahblah"); + public void parseCloseNonExistingTag() throws Exception { + Spanned text = parseCueText("foobarbazbuzz"); + assertThat(text.toString()).isEqualTo("foobarbazbuzz"); - StyleSpan[] spans = getSpans(text, StyleSpan.class); - assertThat(spans).hasLength(1); - assertThat(spans[0].getStyle()).isEqualTo(BOLD); - assertThat(text.getSpanStart(spans[0])).isEqualTo(4); - assertThat(text.getSpanEnd(spans[0])).isEqualTo(8); // should be 12 when valid + // endIndex should be 9 when valid (i.e. "foobarbaz".length() + assertThat(text).hasBoldSpanBetween("foo".length(), "foobar".length()); } @Test - public void testParseEmptyTagName() throws Exception { - Spanned text = parseCueText("An unclosed u tag with <>italic inside"); - assertThat(text.toString()).isEqualTo("An unclosed u tag with italic inside"); + public void parseEmptyTagName() throws Exception { + Spanned text = parseCueText("An empty <>tag"); + assertThat(text.toString()).isEqualTo("An empty tag"); } @Test - public void testParseEntities() throws Exception { + public void parseEntities() throws Exception { Spanned text = parseCueText("& > <  "); assertThat(text.toString()).isEqualTo("& > < "); } @Test - public void testParseEntitiesUnsupported() throws Exception { + public void parseEntitiesUnsupported() throws Exception { Spanned text = parseCueText("&noway; &sure;"); assertThat(text.toString()).isEqualTo(" "); } @Test - public void testParseEntitiesNotTerminated() throws Exception { + public void parseEntitiesNotTerminated() throws Exception { Spanned text = parseCueText("& here comes text"); assertThat(text.toString()).isEqualTo("& here comes text"); } @Test - public void testParseEntitiesNotTerminatedUnsupported() throws Exception { + public void parseEntitiesNotTerminatedUnsupported() throws Exception { Spanned text = parseCueText("&surenot here comes text"); assertThat(text.toString()).isEqualTo(" here comes text"); } @Test - public void testParseEntitiesNotTerminatedNoSpace() throws Exception { + public void parseEntitiesNotTerminatedNoSpace() throws Exception { Spanned text = parseCueText("&surenot"); assertThat(text.toString()).isEqualTo("&surenot"); } @Test - public void testParseVoidTag() throws Exception { + public void parseVoidTag() throws Exception { Spanned text = parseCueText("here comes
        text
        "); assertThat(text.toString()).isEqualTo("here comes text"); } @Test - public void testParseMultipleTagsOfSameKind() { + public void parseMultipleTagsOfSameKind() { Spanned text = parseCueText("blah blah blah foo"); assertThat(text.toString()).isEqualTo("blah blah blah foo"); - StyleSpan[] spans = getSpans(text, StyleSpan.class); - assertThat(spans).hasLength(2); - assertThat(text.getSpanStart(spans[0])).isEqualTo(5); - assertThat(text.getSpanEnd(spans[0])).isEqualTo(9); - assertThat(text.getSpanStart(spans[1])).isEqualTo(15); - assertThat(text.getSpanEnd(spans[1])).isEqualTo(18); - assertThat(spans[0].getStyle()).isEqualTo(BOLD); - assertThat(spans[1].getStyle()).isEqualTo(BOLD); + assertThat(text).hasBoldSpanBetween("blah ".length(), "blah blah".length()); + assertThat(text).hasBoldSpanBetween("blah blah blah ".length(), "blah blah blah foo".length()); } @Test - public void testParseInvalidVoidSlash() { + public void parseInvalidVoidSlash() { Spanned text = parseCueText("blah blah"); assertThat(text.toString()).isEqualTo("blah blah"); - StyleSpan[] spans = getSpans(text, StyleSpan.class); - assertThat(spans).hasLength(0); + assertThat(text).hasNoSpans(); } @Test - public void testParseMonkey() throws Exception { + public void parseMonkey() throws Exception { Spanned text = parseCueText("< u>An unclosed u tag with <<<<< i>italic
        " + " inside"); assertThat(text.toString()).isEqualTo("An unclosed u tag with italic inside"); @@ -216,7 +264,7 @@ public final class WebvttCueParserTest { } @Test - public void testParseCornerCases() throws Exception { + public void parseCornerCases() throws Exception { Spanned text = parseCueText(">"); assertThat(text.toString()).isEqualTo(">"); @@ -243,13 +291,7 @@ public final class WebvttCueParserTest { } private static Spanned parseCueText(String string) { - WebvttCue.Builder builder = new WebvttCue.Builder(); - WebvttCueParser.parseCueText(null, string, builder, Collections.emptyList()); - return (Spanned) builder.build().text; + return WebvttCueParser.parseCueText( + /* id= */ null, string, /* styles= */ Collections.emptyList()); } - - private static T[] getSpans(Spanned text, Class spanType) { - return text.getSpans(0, text.length(), spanType); - } - } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttDecoderTest.java b/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttDecoderTest.java index 06ac4d825c..3de75a249f 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttDecoderTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttDecoderTest.java @@ -15,23 +15,20 @@ */ package com.google.android.exoplayer2.text.webvtt; +import static com.google.android.exoplayer2.testutil.truth.SpannedSubject.assertThat; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.fail; -import android.graphics.Typeface; import android.text.Layout.Alignment; import android.text.Spanned; -import android.text.style.BackgroundColorSpan; -import android.text.style.ForegroundColorSpan; -import android.text.style.StyleSpan; -import android.text.style.TypefaceSpan; -import android.text.style.UnderlineSpan; -import androidx.annotation.Nullable; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.testutil.TestUtil; import com.google.android.exoplayer2.text.Cue; import com.google.android.exoplayer2.text.SubtitleDecoderException; +import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.ColorParser; +import com.google.common.collect.Iterables; import com.google.common.truth.Expect; import java.io.IOException; import java.util.List; @@ -48,17 +45,22 @@ public class WebvttDecoderTest { private static final String TYPICAL_WITH_IDS_FILE = "webvtt/typical_with_identifiers"; private static final String TYPICAL_WITH_COMMENTS_FILE = "webvtt/typical_with_comments"; private static final String WITH_POSITIONING_FILE = "webvtt/with_positioning"; + private static final String WITH_OVERLAPPING_TIMESTAMPS_FILE = + "webvtt/with_overlapping_timestamps"; + private static final String WITH_VERTICAL_FILE = "webvtt/with_vertical"; private static final String WITH_BAD_CUE_HEADER_FILE = "webvtt/with_bad_cue_header"; private static final String WITH_TAGS_FILE = "webvtt/with_tags"; private static final String WITH_CSS_STYLES = "webvtt/with_css_styles"; private static final String WITH_CSS_COMPLEX_SELECTORS = "webvtt/with_css_complex_selectors"; + private static final String WITH_CSS_TEXT_COMBINE_UPRIGHT = + "webvtt/with_css_text_combine_upright"; private static final String WITH_BOM = "webvtt/with_bom"; private static final String EMPTY_FILE = "webvtt/empty"; @Rule public final Expect expect = Expect.create(); @Test - public void testDecodeEmpty() throws IOException { + public void decodeEmpty() throws IOException { WebvttDecoder decoder = new WebvttDecoder(); byte[] bytes = TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), EMPTY_FILE); try { @@ -70,330 +72,379 @@ public class WebvttDecoderTest { } @Test - public void testDecodeTypical() throws Exception { + public void decodeTypical() throws Exception { WebvttSubtitle subtitle = getSubtitleForTestAsset(TYPICAL_FILE); - // Test event count. assertThat(subtitle.getEventTimeCount()).isEqualTo(4); - // Test cues. - assertCue( - subtitle, - /* eventTimeIndex= */ 0, - /* startTimeUs= */ 0, - /* endTimeUs= */ 1234000, - "This is the first subtitle."); - assertCue( - subtitle, - /* eventTimeIndex= */ 2, - /* startTimeUs= */ 2345000, - /* endTimeUs= */ 3456000, - "This is the second subtitle."); + assertThat(subtitle.getEventTime(0)).isEqualTo(0L); + assertThat(subtitle.getEventTime(1)).isEqualTo(1_234_000L); + Cue firstCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(0))); + assertThat(firstCue.text.toString()).isEqualTo("This is the first subtitle."); + + assertThat(subtitle.getEventTime(2)).isEqualTo(2_345_000L); + assertThat(subtitle.getEventTime(3)).isEqualTo(3_456_000L); + Cue secondCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(2))); + assertThat(secondCue.text.toString()).isEqualTo("This is the second subtitle."); } @Test - public void testDecodeWithBom() throws Exception { + public void decodeWithBom() throws Exception { WebvttSubtitle subtitle = getSubtitleForTestAsset(WITH_BOM); - // Test event count. assertThat(subtitle.getEventTimeCount()).isEqualTo(4); - // Test cues. - assertCue( - subtitle, - /* eventTimeIndex= */ 0, - /* startTimeUs= */ 0, - /* endTimeUs= */ 1234000, - "This is the first subtitle."); - assertCue( - subtitle, - /* eventTimeIndex= */ 2, - /* startTimeUs= */ 2345000, - /* endTimeUs= */ 3456000, - "This is the second subtitle."); + assertThat(subtitle.getEventTime(0)).isEqualTo(0L); + assertThat(subtitle.getEventTime(1)).isEqualTo(1_234_000L); + Cue firstCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(0))); + assertThat(firstCue.text.toString()).isEqualTo("This is the first subtitle."); + + assertThat(subtitle.getEventTime(2)).isEqualTo(2_345_000L); + assertThat(subtitle.getEventTime(3)).isEqualTo(3_456_000L); + Cue secondCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(2))); + assertThat(secondCue.text.toString()).isEqualTo("This is the second subtitle."); } @Test - public void testDecodeTypicalWithBadTimestamps() throws Exception { + public void decodeTypicalWithBadTimestamps() throws Exception { WebvttSubtitle subtitle = getSubtitleForTestAsset(TYPICAL_WITH_BAD_TIMESTAMPS); - // Test event count. assertThat(subtitle.getEventTimeCount()).isEqualTo(4); - // Test cues. - assertCue( - subtitle, - /* eventTimeIndex= */ 0, - /* startTimeUs= */ 0, - /* endTimeUs= */ 1234000, - "This is the first subtitle."); - assertCue( - subtitle, - /* eventTimeIndex= */ 2, - /* startTimeUs= */ 2345000, - /* endTimeUs= */ 3456000, - "This is the second subtitle."); + assertThat(subtitle.getEventTime(0)).isEqualTo(0L); + assertThat(subtitle.getEventTime(1)).isEqualTo(1_234_000L); + Cue firstCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(0))); + assertThat(firstCue.text.toString()).isEqualTo("This is the first subtitle."); + + assertThat(subtitle.getEventTime(2)).isEqualTo(2_345_000L); + assertThat(subtitle.getEventTime(3)).isEqualTo(3_456_000L); + Cue secondCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(2))); + assertThat(secondCue.text.toString()).isEqualTo("This is the second subtitle."); } @Test - public void testDecodeTypicalWithIds() throws Exception { + public void decodeTypicalWithIds() throws Exception { WebvttSubtitle subtitle = getSubtitleForTestAsset(TYPICAL_WITH_IDS_FILE); - // Test event count. assertThat(subtitle.getEventTimeCount()).isEqualTo(4); - // Test cues. - assertCue( - subtitle, - /* eventTimeIndex= */ 0, - /* startTimeUs= */ 0, - /* endTimeUs= */ 1234000, - "This is the first subtitle."); - assertCue( - subtitle, - /* eventTimeIndex= */ 2, - /* startTimeUs= */ 2345000, - /* endTimeUs= */ 3456000, - "This is the second subtitle."); + assertThat(subtitle.getEventTime(0)).isEqualTo(0L); + assertThat(subtitle.getEventTime(1)).isEqualTo(1_234_000L); + Cue firstCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(0))); + assertThat(firstCue.text.toString()).isEqualTo("This is the first subtitle."); + + assertThat(subtitle.getEventTime(2)).isEqualTo(2_345_000L); + assertThat(subtitle.getEventTime(3)).isEqualTo(3_456_000L); + Cue secondCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(2))); + assertThat(secondCue.text.toString()).isEqualTo("This is the second subtitle."); } @Test - public void testDecodeTypicalWithComments() throws Exception { + public void decodeTypicalWithComments() throws Exception { WebvttSubtitle subtitle = getSubtitleForTestAsset(TYPICAL_WITH_COMMENTS_FILE); - // test event count assertThat(subtitle.getEventTimeCount()).isEqualTo(4); - // test cues - assertCue( - subtitle, - /* eventTimeIndex= */ 0, - /* startTimeUs= */ 0, - /* endTimeUs= */ 1234000, - "This is the first subtitle."); - assertCue( - subtitle, - /* eventTimeIndex= */ 2, - /* startTimeUs= */ 2345000, - /* endTimeUs= */ 3456000, - "This is the second subtitle."); + assertThat(subtitle.getEventTime(0)).isEqualTo(0L); + assertThat(subtitle.getEventTime(0 + 1)).isEqualTo(1_234_000L); + Cue firstCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(0))); + assertThat(firstCue.text.toString()).isEqualTo("This is the first subtitle."); + + assertThat(subtitle.getEventTime(2)).isEqualTo(2_345_000L); + assertThat(subtitle.getEventTime(2 + 1)).isEqualTo(3_456_000L); + Cue secondCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(2))); + assertThat(secondCue.text.toString()).isEqualTo("This is the second subtitle."); } @Test - public void testDecodeWithTags() throws Exception { + public void decodeWithTags() throws Exception { WebvttSubtitle subtitle = getSubtitleForTestAsset(WITH_TAGS_FILE); - // Test event count. assertThat(subtitle.getEventTimeCount()).isEqualTo(8); - // Test cues. - assertCue( - subtitle, - /* eventTimeIndex= */ 0, - /* startTimeUs= */ 0, - /* endTimeUs= */ 1234000, - "This is the first subtitle."); - assertCue( - subtitle, - /* eventTimeIndex= */ 2, - /* startTimeUs= */ 2345000, - /* endTimeUs= */ 3456000, - "This is the second subtitle."); - assertCue( - subtitle, - /* eventTimeIndex= */ 4, - /* startTimeUs= */ 4000000, - /* endTimeUs= */ 5000000, - "This is the third subtitle."); - assertCue( - subtitle, - /* eventTimeIndex= */ 6, - /* startTimeUs= */ 6000000, - /* endTimeUs= */ 7000000, - "This is the &subtitle."); + assertThat(subtitle.getEventTime(0)).isEqualTo(0L); + assertThat(subtitle.getEventTime(1)).isEqualTo(1_234_000L); + Cue firstCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(0))); + assertThat(firstCue.text.toString()).isEqualTo("This is the first subtitle."); + + assertThat(subtitle.getEventTime(2)).isEqualTo(2_345_000L); + assertThat(subtitle.getEventTime(3)).isEqualTo(3_456_000L); + Cue secondCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(2))); + assertThat(secondCue.text.toString()).isEqualTo("This is the second subtitle."); + + assertThat(subtitle.getEventTime(4)).isEqualTo(4_000_000L); + assertThat(subtitle.getEventTime(5)).isEqualTo(5_000_000L); + Cue thirdCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(4))); + assertThat(thirdCue.text.toString()).isEqualTo("This is the third subtitle."); + + assertThat(subtitle.getEventTime(6)).isEqualTo(6_000_000L); + assertThat(subtitle.getEventTime(7)).isEqualTo(7_000_000L); + Cue fourthCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(6))); + assertThat(fourthCue.text.toString()).isEqualTo("This is the &subtitle."); } @Test - public void testDecodeWithPositioning() throws Exception { + public void decodeWithPositioning() throws Exception { WebvttSubtitle subtitle = getSubtitleForTestAsset(WITH_POSITIONING_FILE); - // Test event count. - assertThat(subtitle.getEventTimeCount()).isEqualTo(12); - // Test cues. - assertCue( - subtitle, - /* eventTimeIndex= */ 0, - /* startTimeUs= */ 0, - /* endTimeUs= */ 1234000, - "This is the first subtitle.", - Alignment.ALIGN_NORMAL, - /* line= */ Cue.DIMEN_UNSET, - /* lineType= */ Cue.LINE_TYPE_NUMBER, - /* lineAnchor= */ Cue.ANCHOR_TYPE_START, - /* position= */ 0.1f, - /* positionAnchor= */ Cue.ANCHOR_TYPE_START, - /* size= */ 0.35f); - assertCue( - subtitle, - /* eventTimeIndex= */ 2, - /* startTimeUs= */ 2345000, - /* endTimeUs= */ 3456000, - "This is the second subtitle.", - Alignment.ALIGN_OPPOSITE, - /* line= */ Cue.DIMEN_UNSET, - /* lineType= */ Cue.LINE_TYPE_NUMBER, - /* lineAnchor= */ Cue.ANCHOR_TYPE_START, - /* position= */ 0.5f, - /* positionAnchor= */ Cue.ANCHOR_TYPE_END, - /* size= */ 0.35f); - assertCue( - subtitle, - /* eventTimeIndex= */ 4, - /* startTimeUs= */ 4000000, - /* endTimeUs= */ 5000000, - "This is the third subtitle.", - Alignment.ALIGN_CENTER, - /* line= */ 0.45f, - /* lineType= */ Cue.LINE_TYPE_FRACTION, - /* lineAnchor= */ Cue.ANCHOR_TYPE_END, - /* position= */ 0.5f, - /* positionAnchor= */ Cue.ANCHOR_TYPE_MIDDLE, - /* size= */ 0.35f); - assertCue( - subtitle, - /* eventTimeIndex= */ 6, - /* startTimeUs= */ 6000000, - /* endTimeUs= */ 7000000, - "This is the fourth subtitle.", - Alignment.ALIGN_CENTER, - /* line= */ -11.0f, - /* lineType= */ Cue.LINE_TYPE_NUMBER, - /* lineAnchor= */ Cue.ANCHOR_TYPE_START, - /* position= */ 0.5f, - /* positionAnchor= */ Cue.ANCHOR_TYPE_MIDDLE, - /* size= */ 1.0f); - assertCue( - subtitle, - /* eventTimeIndex= */ 8, - /* startTimeUs= */ 7000000, - /* endTimeUs= */ 8000000, - "This is the fifth subtitle.", - Alignment.ALIGN_OPPOSITE, - /* line= */ Cue.DIMEN_UNSET, - /* lineType= */ Cue.LINE_TYPE_NUMBER, - /* lineAnchor= */ Cue.ANCHOR_TYPE_START, - /* position= */ 1.0f, - /* positionAnchor= */ Cue.ANCHOR_TYPE_END, - /* size= */ 1.0f); - assertCue( - subtitle, - /* eventTimeIndex= */ 10, - /* startTimeUs= */ 10000000, - /* endTimeUs= */ 11000000, - "This is the sixth subtitle.", - Alignment.ALIGN_CENTER, - /* line= */ 0.45f, - /* lineType= */ Cue.LINE_TYPE_FRACTION, - /* lineAnchor= */ Cue.ANCHOR_TYPE_END, - /* position= */ 0.5f, - /* positionAnchor= */ Cue.ANCHOR_TYPE_MIDDLE, - /* size= */ 0.35f); + + assertThat(subtitle.getEventTimeCount()).isEqualTo(16); + + assertThat(subtitle.getEventTime(0)).isEqualTo(0L); + assertThat(subtitle.getEventTime(1)).isEqualTo(1_234_000L); + Cue firstCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(0))); + assertThat(firstCue.text.toString()).isEqualTo("This is the first subtitle."); + assertThat(firstCue.position).isEqualTo(0.6f); + assertThat(firstCue.positionAnchor).isEqualTo(Cue.ANCHOR_TYPE_END); + assertThat(firstCue.textAlignment).isEqualTo(Alignment.ALIGN_NORMAL); + assertThat(firstCue.size).isEqualTo(0.35f); + + // Unspecified values should use WebVTT defaults + assertThat(firstCue.line).isEqualTo(-1f); + assertThat(firstCue.lineType).isEqualTo(Cue.LINE_TYPE_NUMBER); + // WebVTT specifies START as the default, but it doesn't expect this to be used if + // lineType=NUMBER so we have to override it to END in this case, otherwise the Cue will be + // displayed off the bottom of the screen. + assertThat(firstCue.lineAnchor).isEqualTo(Cue.ANCHOR_TYPE_END); + assertThat(firstCue.verticalType).isEqualTo(Cue.TYPE_UNSET); + + assertThat(subtitle.getEventTime(2)).isEqualTo(2_345_000L); + assertThat(subtitle.getEventTime(3)).isEqualTo(3_456_000L); + Cue secondCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(2))); + assertThat(secondCue.text.toString()).isEqualTo("This is the second subtitle."); + // Position is invalid so defaults to 0.5 + assertThat(secondCue.position).isEqualTo(0.5f); + assertThat(secondCue.textAlignment).isEqualTo(Alignment.ALIGN_OPPOSITE); + + assertThat(subtitle.getEventTime(4)).isEqualTo(4_000_000L); + assertThat(subtitle.getEventTime(5)).isEqualTo(5_000_000L); + Cue thirdCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(4))); + assertThat(thirdCue.text.toString()).isEqualTo("This is the third subtitle."); + assertThat(thirdCue.line).isEqualTo(0.45f); + assertThat(thirdCue.lineType).isEqualTo(Cue.LINE_TYPE_FRACTION); + assertThat(thirdCue.lineAnchor).isEqualTo(Cue.ANCHOR_TYPE_END); + assertThat(thirdCue.textAlignment).isEqualTo(Alignment.ALIGN_CENTER); + // Derived from `align:middle`: + assertThat(thirdCue.positionAnchor).isEqualTo(Cue.ANCHOR_TYPE_MIDDLE); + + assertThat(subtitle.getEventTime(6)).isEqualTo(6_000_000L); + assertThat(subtitle.getEventTime(7)).isEqualTo(7_000_000L); + Cue fourthCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(6))); + assertThat(fourthCue.text.toString()).isEqualTo("This is the fourth subtitle."); + assertThat(fourthCue.line).isEqualTo(-11f); + assertThat(fourthCue.lineAnchor).isEqualTo(Cue.ANCHOR_TYPE_START); + assertThat(fourthCue.textAlignment).isEqualTo(Alignment.ALIGN_CENTER); + // Derived from `align:middle`: + assertThat(fourthCue.position).isEqualTo(0.5f); + assertThat(fourthCue.positionAnchor).isEqualTo(Cue.ANCHOR_TYPE_MIDDLE); + + assertThat(subtitle.getEventTime(8)).isEqualTo(8_000_000L); + assertThat(subtitle.getEventTime(9)).isEqualTo(9_000_000L); + Cue fifthCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(8))); + assertThat(fifthCue.text.toString()).isEqualTo("This is the fifth subtitle."); + assertThat(fifthCue.textAlignment).isEqualTo(Alignment.ALIGN_OPPOSITE); + // Derived from `align:right`: + assertThat(fifthCue.position).isEqualTo(1.0f); + assertThat(fifthCue.positionAnchor).isEqualTo(Cue.ANCHOR_TYPE_END); + + assertThat(subtitle.getEventTime(10)).isEqualTo(10_000_000L); + assertThat(subtitle.getEventTime(11)).isEqualTo(11_000_000L); + Cue sixthCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(10))); + assertThat(sixthCue.text.toString()).isEqualTo("This is the sixth subtitle."); + assertThat(sixthCue.textAlignment).isEqualTo(Alignment.ALIGN_CENTER); + // Derived from `align:center`: + assertThat(sixthCue.position).isEqualTo(0.5f); + assertThat(sixthCue.positionAnchor).isEqualTo(Cue.ANCHOR_TYPE_MIDDLE); + + assertThat(subtitle.getEventTime(12)).isEqualTo(12_000_000L); + assertThat(subtitle.getEventTime(13)).isEqualTo(13_000_000L); + Cue seventhCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(12))); + assertThat(seventhCue.text.toString()).isEqualTo("This is the seventh subtitle."); + assertThat(seventhCue.positionAnchor).isEqualTo(Cue.ANCHOR_TYPE_START); + + assertThat(subtitle.getEventTime(14)).isEqualTo(14_000_000L); + assertThat(subtitle.getEventTime(15)).isEqualTo(15_000_000L); + Cue eighthCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(14))); + assertThat(eighthCue.text.toString()).isEqualTo("This is the eighth subtitle."); + assertThat(eighthCue.positionAnchor).isEqualTo(Cue.ANCHOR_TYPE_END); } @Test - public void testDecodeWithBadCueHeader() throws Exception { + public void decodeWithOverlappingTimestamps() throws Exception { + WebvttSubtitle subtitle = getSubtitleForTestAsset(WITH_OVERLAPPING_TIMESTAMPS_FILE); + + assertThat(subtitle.getEventTimeCount()).isEqualTo(8); + + Cue firstCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(0))); + assertThat(firstCue.text.toString()).isEqualTo("Displayed at the bottom for 3 seconds."); + assertThat(firstCue.line).isEqualTo(-1f); + assertThat(firstCue.lineType).isEqualTo(Cue.LINE_TYPE_NUMBER); + assertThat(firstCue.lineAnchor).isEqualTo(Cue.ANCHOR_TYPE_END); + + List firstAndSecondCue = subtitle.getCues(subtitle.getEventTime(1)); + assertThat(firstAndSecondCue).hasSize(2); + assertThat(firstAndSecondCue.get(0).text.toString()) + .isEqualTo("Displayed at the bottom for 3 seconds."); + assertThat(firstAndSecondCue.get(0).line).isEqualTo(-1f); + assertThat(firstAndSecondCue.get(0).lineType).isEqualTo(Cue.LINE_TYPE_NUMBER); + assertThat(firstAndSecondCue.get(0).lineAnchor).isEqualTo(Cue.ANCHOR_TYPE_END); + assertThat(firstAndSecondCue.get(1).text.toString()) + .isEqualTo("Appears directly above for 1 second."); + assertThat(firstAndSecondCue.get(1).line).isEqualTo(-2f); + assertThat(firstAndSecondCue.get(1).lineType).isEqualTo(Cue.LINE_TYPE_NUMBER); + assertThat(firstAndSecondCue.get(1).lineAnchor).isEqualTo(Cue.ANCHOR_TYPE_END); + + Cue thirdCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(4))); + assertThat(thirdCue.text.toString()).isEqualTo("Displayed at the bottom for 2 seconds."); + assertThat(thirdCue.line).isEqualTo(-1f); + assertThat(thirdCue.lineType).isEqualTo(Cue.LINE_TYPE_NUMBER); + assertThat(thirdCue.lineAnchor).isEqualTo(Cue.ANCHOR_TYPE_END); + + List thirdAndFourthCue = subtitle.getCues(subtitle.getEventTime(5)); + assertThat(thirdAndFourthCue).hasSize(2); + assertThat(thirdAndFourthCue.get(0).text.toString()) + .isEqualTo("Displayed at the bottom for 2 seconds."); + assertThat(thirdAndFourthCue.get(0).line).isEqualTo(-1f); + assertThat(thirdAndFourthCue.get(0).lineType).isEqualTo(Cue.LINE_TYPE_NUMBER); + assertThat(thirdAndFourthCue.get(0).lineAnchor).isEqualTo(Cue.ANCHOR_TYPE_END); + assertThat(thirdAndFourthCue.get(1).text.toString()) + .isEqualTo("Appears directly above the previous cue, then replaces it after 1 second."); + assertThat(thirdAndFourthCue.get(1).line).isEqualTo(-2f); + assertThat(thirdAndFourthCue.get(1).lineType).isEqualTo(Cue.LINE_TYPE_NUMBER); + assertThat(thirdAndFourthCue.get(1).lineAnchor).isEqualTo(Cue.ANCHOR_TYPE_END); + + Cue fourthCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(6))); + assertThat(fourthCue.text.toString()) + .isEqualTo("Appears directly above the previous cue, then replaces it after 1 second."); + assertThat(fourthCue.line).isEqualTo(-1f); + assertThat(fourthCue.lineType).isEqualTo(Cue.LINE_TYPE_NUMBER); + assertThat(fourthCue.lineAnchor).isEqualTo(Cue.ANCHOR_TYPE_END); + } + + @Test + public void decodeWithVertical() throws Exception { + WebvttSubtitle subtitle = getSubtitleForTestAsset(WITH_VERTICAL_FILE); + + assertThat(subtitle.getEventTimeCount()).isEqualTo(6); + + assertThat(subtitle.getEventTime(0)).isEqualTo(0L); + assertThat(subtitle.getEventTime(1)).isEqualTo(1_234_000L); + Cue firstCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(0))); + assertThat(firstCue.text.toString()).isEqualTo("Vertical right-to-left (e.g. Japanese)"); + assertThat(firstCue.verticalType).isEqualTo(Cue.VERTICAL_TYPE_RL); + + assertThat(subtitle.getEventTime(2)).isEqualTo(2_345_000L); + assertThat(subtitle.getEventTime(3)).isEqualTo(3_456_000L); + Cue secondCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(2))); + assertThat(secondCue.text.toString()).isEqualTo("Vertical left-to-right (e.g. Mongolian)"); + assertThat(secondCue.verticalType).isEqualTo(Cue.VERTICAL_TYPE_LR); + + assertThat(subtitle.getEventTime(4)).isEqualTo(4_000_000L); + assertThat(subtitle.getEventTime(5)).isEqualTo(5_000_000L); + Cue thirdCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(4))); + assertThat(thirdCue.text.toString()).isEqualTo("No vertical setting (i.e. horizontal)"); + assertThat(thirdCue.verticalType).isEqualTo(Cue.TYPE_UNSET); + } + + @Test + public void decodeWithBadCueHeader() throws Exception { WebvttSubtitle subtitle = getSubtitleForTestAsset(WITH_BAD_CUE_HEADER_FILE); - // Test event count. assertThat(subtitle.getEventTimeCount()).isEqualTo(4); - // Test cues. - assertCue( - subtitle, - /* eventTimeIndex= */ 0, - /* startTimeUs= */ 0, - /* endTimeUs= */ 1234000, - "This is the first subtitle."); - assertCue( - subtitle, - /* eventTimeIndex= */ 2, - /* startTimeUs= */ 4000000, - /* endTimeUs= */ 5000000, - "This is the third subtitle."); + assertThat(subtitle.getEventTime(0)).isEqualTo(0L); + assertThat(subtitle.getEventTime(1)).isEqualTo(1_234_000L); + Cue firstCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(0))); + assertThat(firstCue.text.toString()).isEqualTo("This is the first subtitle."); + + assertThat(subtitle.getEventTime(2)).isEqualTo(4_000_000L); + assertThat(subtitle.getEventTime(3)).isEqualTo(5_000_000L); + Cue secondCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(2))); + assertThat(secondCue.text.toString()).isEqualTo("This is the third subtitle."); } @Test - public void testWebvttWithCssStyle() throws Exception { + public void webvttWithCssStyle() throws Exception { WebvttSubtitle subtitle = getSubtitleForTestAsset(WITH_CSS_STYLES); - // Test event count. - assertThat(subtitle.getEventTimeCount()).isEqualTo(8); + Spanned firstCueText = getUniqueSpanTextAt(subtitle, 0); + assertThat(firstCueText.toString()).isEqualTo("This is the first subtitle."); + assertThat(firstCueText) + .hasForegroundColorSpanBetween(0, firstCueText.length()) + .withColor(ColorParser.parseCssColor("papayawhip")); + assertThat(firstCueText) + .hasBackgroundColorSpanBetween(0, firstCueText.length()) + .withColor(ColorParser.parseCssColor("green")); - // Test cues. - assertCue( - subtitle, - /* eventTimeIndex= */ 0, - /* startTimeUs= */ 0, - /* endTimeUs= */ 1234000, - "This is the first subtitle."); - assertCue( - subtitle, - /* eventTimeIndex= */ 2, - /* startTimeUs= */ 2345000, - /* endTimeUs= */ 3456000, - "This is the second subtitle."); + Spanned secondCueText = getUniqueSpanTextAt(subtitle, 2_345_000); + assertThat(secondCueText.toString()).isEqualTo("This is the second subtitle."); + assertThat(secondCueText) + .hasForegroundColorSpanBetween(0, secondCueText.length()) + .withColor(ColorParser.parseCssColor("peachpuff")); - Spanned s1 = getUniqueSpanTextAt(subtitle, /* timeUs= */ 0); - Spanned s2 = getUniqueSpanTextAt(subtitle, /* timeUs= */ 2345000); - Spanned s3 = getUniqueSpanTextAt(subtitle, /* timeUs= */ 20000000); - Spanned s4 = getUniqueSpanTextAt(subtitle, /* timeUs= */ 25000000); - assertThat(s1.getSpans(/* start= */ 0, s1.length(), ForegroundColorSpan.class)).hasLength(1); - assertThat(s1.getSpans(/* start= */ 0, s1.length(), BackgroundColorSpan.class)).hasLength(1); - assertThat(s2.getSpans(/* start= */ 0, s2.length(), ForegroundColorSpan.class)).hasLength(2); - assertThat(s3.getSpans(/* start= */ 10, s3.length(), UnderlineSpan.class)).hasLength(1); - assertThat(s4.getSpans(/* start= */ 0, /* end= */ 16, BackgroundColorSpan.class)).hasLength(2); - assertThat(s4.getSpans(/* start= */ 17, s4.length(), StyleSpan.class)).hasLength(1); - assertThat(s4.getSpans(/* start= */ 17, s4.length(), StyleSpan.class)[0].getStyle()) - .isEqualTo(Typeface.BOLD); + Spanned thirdCueText = getUniqueSpanTextAt(subtitle, 20_000_000); + assertThat(thirdCueText.toString()).isEqualTo("This is a reference by element"); + assertThat(thirdCueText).hasUnderlineSpanBetween("This is a ".length(), thirdCueText.length()); + + Spanned fourthCueText = getUniqueSpanTextAt(subtitle, 25_000_000); + assertThat(fourthCueText.toString()).isEqualTo("You are an idiot\nYou don't have the guts"); + assertThat(fourthCueText) + .hasBackgroundColorSpanBetween(0, "You are an idiot".length()) + .withColor(ColorParser.parseCssColor("lime")); + assertThat(fourthCueText) + .hasBoldSpanBetween("You are an idiot\n".length(), fourthCueText.length()); } @Test - public void testWithComplexCssSelectors() throws Exception { + public void withComplexCssSelectors() throws Exception { WebvttSubtitle subtitle = getSubtitleForTestAsset(WITH_CSS_COMPLEX_SELECTORS); - Spanned text = getUniqueSpanTextAt(subtitle, /* timeUs= */ 0); - assertThat(text.getSpans(/* start= */ 30, text.length(), ForegroundColorSpan.class)) - .hasLength(1); - assertThat( - text.getSpans(/* start= */ 30, text.length(), ForegroundColorSpan.class)[0] - .getForegroundColor()) - .isEqualTo(0xFFEE82EE); - assertThat(text.getSpans(/* start= */ 30, text.length(), TypefaceSpan.class)).hasLength(1); - assertThat(text.getSpans(/* start= */ 30, text.length(), TypefaceSpan.class)[0].getFamily()) - .isEqualTo("courier"); + Spanned firstCueText = getUniqueSpanTextAt(subtitle, /* timeUs= */ 0); + assertThat(firstCueText).hasUnderlineSpanBetween(0, firstCueText.length()); + assertThat(firstCueText) + .hasForegroundColorSpanBetween( + "This should be underlined and ".length(), firstCueText.length()) + .withColor(ColorParser.parseCssColor("violet")); + assertThat(firstCueText) + .hasTypefaceSpanBetween("This should be underlined and ".length(), firstCueText.length()) + .withFamily("courier"); - text = getUniqueSpanTextAt(subtitle, /* timeUs= */ 2000000); - assertThat(text.getSpans(/* start= */ 5, text.length(), TypefaceSpan.class)).hasLength(1); - assertThat(text.getSpans(/* start= */ 5, text.length(), TypefaceSpan.class)[0].getFamily()) - .isEqualTo("courier"); + Spanned secondCueText = getUniqueSpanTextAt(subtitle, /* timeUs= */ 2_000_000); + assertThat(secondCueText) + .hasTypefaceSpanBetween("This ".length(), secondCueText.length()) + .withFamily("courier"); + assertThat(secondCueText) + .hasNoForegroundColorSpanBetween("This ".length(), secondCueText.length()); - text = getUniqueSpanTextAt(subtitle, /* timeUs= */ 2500000); - assertThat(text.getSpans(/* start= */ 5, text.length(), StyleSpan.class)).hasLength(1); - assertThat(text.getSpans(/* start= */ 5, text.length(), StyleSpan.class)[0].getStyle()) - .isEqualTo(Typeface.BOLD); - assertThat(text.getSpans(/* start= */ 5, text.length(), TypefaceSpan.class)).hasLength(1); - assertThat(text.getSpans(/* start= */ 5, text.length(), TypefaceSpan.class)[0].getFamily()) - .isEqualTo("courier"); + Spanned thirdCueText = getUniqueSpanTextAt(subtitle, /* timeUs= */ 2_500_000); + assertThat(thirdCueText).hasBoldSpanBetween("This ".length(), thirdCueText.length()); + assertThat(thirdCueText) + .hasTypefaceSpanBetween("This ".length(), thirdCueText.length()) + .withFamily("courier"); - text = getUniqueSpanTextAt(subtitle, /* timeUs= */ 4000000); - assertThat(text.getSpans(/* start= */ 6, /* end= */ 22, StyleSpan.class)).hasLength(0); - assertThat(text.getSpans(/* start= */ 30, text.length(), StyleSpan.class)).hasLength(1); - assertThat(text.getSpans(/* start= */ 30, text.length(), StyleSpan.class)[0].getStyle()) - .isEqualTo(Typeface.BOLD); + Spanned fourthCueText = getUniqueSpanTextAt(subtitle, /* timeUs= */ 4_000_000); + assertThat(fourthCueText) + .hasNoStyleSpanBetween("This ".length(), "shouldn't be bold.".length()); + assertThat(fourthCueText) + .hasBoldSpanBetween("This shouldn't be bold.\nThis ".length(), fourthCueText.length()); - text = getUniqueSpanTextAt(subtitle, /* timeUs= */ 5000000); - assertThat(text.getSpans(/* start= */ 9, /* end= */ 17, StyleSpan.class)).hasLength(0); - assertThat(text.getSpans(/* start= */ 19, text.length(), StyleSpan.class)).hasLength(1); - assertThat(text.getSpans(/* start= */ 19, text.length(), StyleSpan.class)[0].getStyle()) - .isEqualTo(Typeface.ITALIC); + Spanned fifthCueText = getUniqueSpanTextAt(subtitle, /* timeUs= */ 5_000_000); + assertThat(fifthCueText) + .hasNoStyleSpanBetween("This is ".length(), "This is specific".length()); + assertThat(fifthCueText) + .hasItalicSpanBetween("This is specific\n".length(), fifthCueText.length()); + } + + @Test + public void webvttWithCssTextCombineUpright() throws Exception { + WebvttSubtitle subtitle = getSubtitleForTestAsset(WITH_CSS_TEXT_COMBINE_UPRIGHT); + + Spanned firstCueText = getUniqueSpanTextAt(subtitle, 500_000); + assertThat(firstCueText) + .hasHorizontalTextInVerticalContextSpanBetween("Combine ".length(), "Combine all".length()); + + Spanned secondCueText = getUniqueSpanTextAt(subtitle, 3_500_000); + assertThat(secondCueText) + .hasHorizontalTextInVerticalContextSpanBetween( + "Combine ".length(), "Combine 0004".length()); } private WebvttSubtitle getSubtitleForTestAsset(String asset) @@ -404,60 +455,6 @@ public class WebvttDecoderTest { } private Spanned getUniqueSpanTextAt(WebvttSubtitle sub, long timeUs) { - return (Spanned) sub.getCues(timeUs).get(0).text; - } - - private void assertCue( - WebvttSubtitle subtitle, int eventTimeIndex, long startTimeUs, long endTimeUs, String text) { - assertCue( - subtitle, - eventTimeIndex, - startTimeUs, - endTimeUs, - text, - /* textAlignment= */ Alignment.ALIGN_CENTER, - /* line= */ Cue.DIMEN_UNSET, - /* lineType= */ Cue.LINE_TYPE_NUMBER, - /* lineAnchor= */ Cue.ANCHOR_TYPE_START, - /* position= */ 0.5f, - /* positionAnchor= */ Cue.ANCHOR_TYPE_MIDDLE, - /* size= */ 1.0f); - } - - private void assertCue( - WebvttSubtitle subtitle, - int eventTimeIndex, - long startTimeUs, - long endTimeUs, - String text, - @Nullable Alignment textAlignment, - float line, - @Cue.LineType int lineType, - @Cue.AnchorType int lineAnchor, - float position, - @Cue.AnchorType int positionAnchor, - float size) { - expect - .withMessage("startTimeUs") - .that(subtitle.getEventTime(eventTimeIndex)) - .isEqualTo(startTimeUs); - expect - .withMessage("endTimeUs") - .that(subtitle.getEventTime(eventTimeIndex + 1)) - .isEqualTo(endTimeUs); - List cues = subtitle.getCues(subtitle.getEventTime(eventTimeIndex)); - assertThat(cues).hasSize(1); - // Assert cue properties. - Cue cue = cues.get(0); - expect.withMessage("cue.text").that(cue.text.toString()).isEqualTo(text); - expect.withMessage("cue.textAlignment").that(cue.textAlignment).isEqualTo(textAlignment); - expect.withMessage("cue.line").that(cue.line).isEqualTo(line); - expect.withMessage("cue.lineType").that(cue.lineType).isEqualTo(lineType); - expect.withMessage("cue.lineAnchor").that(cue.lineAnchor).isEqualTo(lineAnchor); - expect.withMessage("cue.position").that(cue.position).isEqualTo(position); - expect.withMessage("cue.positionAnchor").that(cue.positionAnchor).isEqualTo(positionAnchor); - expect.withMessage("cue.size").that(cue.size).isEqualTo(size); - - assertThat(expect.hasFailures()).isFalse(); + return (Spanned) Assertions.checkNotNull(sub.getCues(timeUs).get(0).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 71927cbceb..a9b8c32e5b 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 @@ -22,6 +22,7 @@ import static java.lang.Long.MAX_VALUE; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.text.Cue; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.List; import org.junit.Test; @@ -33,76 +34,47 @@ public class WebvttSubtitleTest { private static final String FIRST_SUBTITLE_STRING = "This is the first subtitle."; private static final String SECOND_SUBTITLE_STRING = "This is the second subtitle."; - 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 simpleSubtitle; + private static final WebvttSubtitle simpleSubtitle = + new WebvttSubtitle( + Arrays.asList( + new WebvttCueInfo( + WebvttCueParser.newCueForText(FIRST_SUBTITLE_STRING), + /* startTimeUs= */ 1_000_000, + /* endTimeUs= */ 2_000_000), + new WebvttCueInfo( + WebvttCueParser.newCueForText(SECOND_SUBTITLE_STRING), + /* startTimeUs= */ 3_000_000, + /* endTimeUs= */ 4_000_000))); - static { - ArrayList simpleSubtitleCues = new ArrayList<>(); - WebvttCue firstCue = - new WebvttCue.Builder() - .setStartTime(1000000) - .setEndTime(2000000) - .setText(FIRST_SUBTITLE_STRING) - .build(); - simpleSubtitleCues.add(firstCue); - WebvttCue secondCue = - new WebvttCue.Builder() - .setStartTime(3000000) - .setEndTime(4000000) - .setText(SECOND_SUBTITLE_STRING) - .build(); - simpleSubtitleCues.add(secondCue); - simpleSubtitle = new WebvttSubtitle(simpleSubtitleCues); - } + private static final WebvttSubtitle overlappingSubtitle = + new WebvttSubtitle( + Arrays.asList( + new WebvttCueInfo( + WebvttCueParser.newCueForText(FIRST_SUBTITLE_STRING), + /* startTimeUs= */ 1_000_000, + /* endTimeUs= */ 3_000_000), + new WebvttCueInfo( + WebvttCueParser.newCueForText(SECOND_SUBTITLE_STRING), + /* startTimeUs= */ 2_000_000, + /* endTimeUs= */ 4_000_000))); - private static final WebvttSubtitle overlappingSubtitle; - - static { - ArrayList overlappingSubtitleCues = new ArrayList<>(); - WebvttCue firstCue = - new WebvttCue.Builder() - .setStartTime(1000000) - .setEndTime(3000000) - .setText(FIRST_SUBTITLE_STRING) - .build(); - overlappingSubtitleCues.add(firstCue); - WebvttCue secondCue = - new WebvttCue.Builder() - .setStartTime(2000000) - .setEndTime(4000000) - .setText(SECOND_SUBTITLE_STRING) - .build(); - overlappingSubtitleCues.add(secondCue); - overlappingSubtitle = new WebvttSubtitle(overlappingSubtitleCues); - } - - private static final WebvttSubtitle nestedSubtitle; - - static { - ArrayList nestedSubtitleCues = new ArrayList<>(); - WebvttCue firstCue = - new WebvttCue.Builder() - .setStartTime(1000000) - .setEndTime(4000000) - .setText(FIRST_SUBTITLE_STRING) - .build(); - nestedSubtitleCues.add(firstCue); - WebvttCue secondCue = - new WebvttCue.Builder() - .setStartTime(2000000) - .setEndTime(3000000) - .setText(SECOND_SUBTITLE_STRING) - .build(); - nestedSubtitleCues.add(secondCue); - nestedSubtitle = new WebvttSubtitle(nestedSubtitleCues); - } + private static final WebvttSubtitle nestedSubtitle = + new WebvttSubtitle( + Arrays.asList( + new WebvttCueInfo( + WebvttCueParser.newCueForText(FIRST_SUBTITLE_STRING), + /* startTimeUs= */ 1_000_000, + /* endTimeUs= */ 4_000_000), + new WebvttCueInfo( + WebvttCueParser.newCueForText(SECOND_SUBTITLE_STRING), + /* startTimeUs= */ 2_000_000, + /* endTimeUs= */ 3_000_000))); @Test - public void testEventCount() { + public void eventCount() { assertThat(emptySubtitle.getEventTimeCount()).isEqualTo(0); assertThat(simpleSubtitle.getEventTimeCount()).isEqualTo(4); assertThat(overlappingSubtitle.getEventTimeCount()).isEqualTo(4); @@ -110,163 +82,226 @@ public class WebvttSubtitleTest { } @Test - public void testSimpleSubtitleEventTimes() { - testSubtitleEventTimesHelper(simpleSubtitle); + public void simpleSubtitleEventTimes() { + assertThat(simpleSubtitle.getEventTime(0)).isEqualTo(1_000_000); + assertThat(simpleSubtitle.getEventTime(1)).isEqualTo(2_000_000); + assertThat(simpleSubtitle.getEventTime(2)).isEqualTo(3_000_000); + assertThat(simpleSubtitle.getEventTime(3)).isEqualTo(4_000_000); } @Test - public void testSimpleSubtitleEventIndices() { - testSubtitleEventIndicesHelper(simpleSubtitle); - } - - @Test - public void testSimpleSubtitleText() { - // Test before first subtitle - assertSingleCueEmpty(simpleSubtitle.getCues(0)); - assertSingleCueEmpty(simpleSubtitle.getCues(500000)); - assertSingleCueEmpty(simpleSubtitle.getCues(999999)); - - // Test first subtitle - assertSingleCueTextEquals(FIRST_SUBTITLE_STRING, simpleSubtitle.getCues(1000000)); - assertSingleCueTextEquals(FIRST_SUBTITLE_STRING, simpleSubtitle.getCues(1500000)); - assertSingleCueTextEquals(FIRST_SUBTITLE_STRING, simpleSubtitle.getCues(1999999)); - - // Test after first subtitle, before second subtitle - assertSingleCueEmpty(simpleSubtitle.getCues(2000000)); - assertSingleCueEmpty(simpleSubtitle.getCues(2500000)); - assertSingleCueEmpty(simpleSubtitle.getCues(2999999)); - - // Test second subtitle - assertSingleCueTextEquals(SECOND_SUBTITLE_STRING, simpleSubtitle.getCues(3000000)); - assertSingleCueTextEquals(SECOND_SUBTITLE_STRING, simpleSubtitle.getCues(3500000)); - assertSingleCueTextEquals(SECOND_SUBTITLE_STRING, simpleSubtitle.getCues(3999999)); - - // Test after second subtitle - assertSingleCueEmpty(simpleSubtitle.getCues(4000000)); - assertSingleCueEmpty(simpleSubtitle.getCues(4500000)); - assertSingleCueEmpty(simpleSubtitle.getCues(Long.MAX_VALUE)); - } - - @Test - public void testOverlappingSubtitleEventTimes() { - testSubtitleEventTimesHelper(overlappingSubtitle); - } - - @Test - public void testOverlappingSubtitleEventIndices() { - testSubtitleEventIndicesHelper(overlappingSubtitle); - } - - @Test - public void testOverlappingSubtitleText() { - // Test before first subtitle - assertSingleCueEmpty(overlappingSubtitle.getCues(0)); - assertSingleCueEmpty(overlappingSubtitle.getCues(500000)); - assertSingleCueEmpty(overlappingSubtitle.getCues(999999)); - - // Test first subtitle - assertSingleCueTextEquals(FIRST_SUBTITLE_STRING, overlappingSubtitle.getCues(1000000)); - assertSingleCueTextEquals(FIRST_SUBTITLE_STRING, overlappingSubtitle.getCues(1500000)); - assertSingleCueTextEquals(FIRST_SUBTITLE_STRING, overlappingSubtitle.getCues(1999999)); - - // Test after first and second subtitle - assertSingleCueTextEquals(FIRST_AND_SECOND_SUBTITLE_STRING, - overlappingSubtitle.getCues(2000000)); - assertSingleCueTextEquals(FIRST_AND_SECOND_SUBTITLE_STRING, - overlappingSubtitle.getCues(2500000)); - assertSingleCueTextEquals(FIRST_AND_SECOND_SUBTITLE_STRING, - overlappingSubtitle.getCues(2999999)); - - // Test second subtitle - assertSingleCueTextEquals(SECOND_SUBTITLE_STRING, overlappingSubtitle.getCues(3000000)); - assertSingleCueTextEquals(SECOND_SUBTITLE_STRING, overlappingSubtitle.getCues(3500000)); - assertSingleCueTextEquals(SECOND_SUBTITLE_STRING, overlappingSubtitle.getCues(3999999)); - - // Test after second subtitle - assertSingleCueEmpty(overlappingSubtitle.getCues(4000000)); - assertSingleCueEmpty(overlappingSubtitle.getCues(4500000)); - assertSingleCueEmpty(overlappingSubtitle.getCues(Long.MAX_VALUE)); - } - - @Test - public void testNestedSubtitleEventTimes() { - testSubtitleEventTimesHelper(nestedSubtitle); - } - - @Test - public void testNestedSubtitleEventIndices() { - testSubtitleEventIndicesHelper(nestedSubtitle); - } - - @Test - public void testNestedSubtitleText() { - // Test before first subtitle - assertSingleCueEmpty(nestedSubtitle.getCues(0)); - assertSingleCueEmpty(nestedSubtitle.getCues(500000)); - assertSingleCueEmpty(nestedSubtitle.getCues(999999)); - - // Test first subtitle - assertSingleCueTextEquals(FIRST_SUBTITLE_STRING, nestedSubtitle.getCues(1000000)); - assertSingleCueTextEquals(FIRST_SUBTITLE_STRING, nestedSubtitle.getCues(1500000)); - assertSingleCueTextEquals(FIRST_SUBTITLE_STRING, nestedSubtitle.getCues(1999999)); - - // Test after first and second subtitle - assertSingleCueTextEquals(FIRST_AND_SECOND_SUBTITLE_STRING, nestedSubtitle.getCues(2000000)); - assertSingleCueTextEquals(FIRST_AND_SECOND_SUBTITLE_STRING, nestedSubtitle.getCues(2500000)); - assertSingleCueTextEquals(FIRST_AND_SECOND_SUBTITLE_STRING, nestedSubtitle.getCues(2999999)); - - // Test first subtitle - assertSingleCueTextEquals(FIRST_SUBTITLE_STRING, nestedSubtitle.getCues(3000000)); - assertSingleCueTextEquals(FIRST_SUBTITLE_STRING, nestedSubtitle.getCues(3500000)); - assertSingleCueTextEquals(FIRST_SUBTITLE_STRING, nestedSubtitle.getCues(3999999)); - - // Test after second subtitle - assertSingleCueEmpty(nestedSubtitle.getCues(4000000)); - assertSingleCueEmpty(nestedSubtitle.getCues(4500000)); - assertSingleCueEmpty(nestedSubtitle.getCues(Long.MAX_VALUE)); - } - - private void testSubtitleEventTimesHelper(WebvttSubtitle subtitle) { - assertThat(subtitle.getEventTime(0)).isEqualTo(1000000); - assertThat(subtitle.getEventTime(1)).isEqualTo(2000000); - assertThat(subtitle.getEventTime(2)).isEqualTo(3000000); - assertThat(subtitle.getEventTime(3)).isEqualTo(4000000); - } - - private void testSubtitleEventIndicesHelper(WebvttSubtitle subtitle) { + public void simpleSubtitleEventIndices() { // Test first event - assertThat(subtitle.getNextEventTimeIndex(0)).isEqualTo(0); - assertThat(subtitle.getNextEventTimeIndex(500000)).isEqualTo(0); - assertThat(subtitle.getNextEventTimeIndex(999999)).isEqualTo(0); + assertThat(simpleSubtitle.getNextEventTimeIndex(0)).isEqualTo(0); + assertThat(simpleSubtitle.getNextEventTimeIndex(500_000)).isEqualTo(0); + assertThat(simpleSubtitle.getNextEventTimeIndex(999_999)).isEqualTo(0); // Test second event - assertThat(subtitle.getNextEventTimeIndex(1000000)).isEqualTo(1); - assertThat(subtitle.getNextEventTimeIndex(1500000)).isEqualTo(1); - assertThat(subtitle.getNextEventTimeIndex(1999999)).isEqualTo(1); + assertThat(simpleSubtitle.getNextEventTimeIndex(1_000_000)).isEqualTo(1); + assertThat(simpleSubtitle.getNextEventTimeIndex(1_500_000)).isEqualTo(1); + assertThat(simpleSubtitle.getNextEventTimeIndex(1_999_999)).isEqualTo(1); // Test third event - assertThat(subtitle.getNextEventTimeIndex(2000000)).isEqualTo(2); - assertThat(subtitle.getNextEventTimeIndex(2500000)).isEqualTo(2); - assertThat(subtitle.getNextEventTimeIndex(2999999)).isEqualTo(2); + assertThat(simpleSubtitle.getNextEventTimeIndex(2_000_000)).isEqualTo(2); + assertThat(simpleSubtitle.getNextEventTimeIndex(2_500_000)).isEqualTo(2); + assertThat(simpleSubtitle.getNextEventTimeIndex(2_999_999)).isEqualTo(2); // Test fourth event - assertThat(subtitle.getNextEventTimeIndex(3000000)).isEqualTo(3); - assertThat(subtitle.getNextEventTimeIndex(3500000)).isEqualTo(3); - assertThat(subtitle.getNextEventTimeIndex(3999999)).isEqualTo(3); + assertThat(simpleSubtitle.getNextEventTimeIndex(3_000_000)).isEqualTo(3); + assertThat(simpleSubtitle.getNextEventTimeIndex(3_500_000)).isEqualTo(3); + assertThat(simpleSubtitle.getNextEventTimeIndex(3_999_999)).isEqualTo(3); // Test null event (i.e. look for events after the last event) - assertThat(subtitle.getNextEventTimeIndex(4000000)).isEqualTo(INDEX_UNSET); - assertThat(subtitle.getNextEventTimeIndex(4500000)).isEqualTo(INDEX_UNSET); - assertThat(subtitle.getNextEventTimeIndex(MAX_VALUE)).isEqualTo(INDEX_UNSET); + assertThat(simpleSubtitle.getNextEventTimeIndex(4_000_000)).isEqualTo(INDEX_UNSET); + assertThat(simpleSubtitle.getNextEventTimeIndex(4_500_000)).isEqualTo(INDEX_UNSET); + assertThat(simpleSubtitle.getNextEventTimeIndex(MAX_VALUE)).isEqualTo(INDEX_UNSET); } - private void assertSingleCueEmpty(List cues) { - assertThat(cues).isEmpty(); + @Test + public void simpleSubtitleText() { + // Test before first subtitle + assertThat(simpleSubtitle.getCues(0)).isEmpty(); + assertThat(simpleSubtitle.getCues(500_000)).isEmpty(); + assertThat(simpleSubtitle.getCues(999_999)).isEmpty(); + + // Test first subtitle + assertThat(getCueTexts(simpleSubtitle.getCues(1_000_000))) + .containsExactly(FIRST_SUBTITLE_STRING); + assertThat(getCueTexts(simpleSubtitle.getCues(1_500_000))) + .containsExactly(FIRST_SUBTITLE_STRING); + assertThat(getCueTexts(simpleSubtitle.getCues(1_999_999))) + .containsExactly(FIRST_SUBTITLE_STRING); + + // Test after first subtitle, before second subtitle + assertThat(simpleSubtitle.getCues(2_000_000)).isEmpty(); + assertThat(simpleSubtitle.getCues(2_500_000)).isEmpty(); + assertThat(simpleSubtitle.getCues(2_999_999)).isEmpty(); + + // Test second subtitle + assertThat(getCueTexts(simpleSubtitle.getCues(3_000_000))) + .containsExactly(SECOND_SUBTITLE_STRING); + assertThat(getCueTexts(simpleSubtitle.getCues(3_500_000))) + .containsExactly(SECOND_SUBTITLE_STRING); + assertThat(getCueTexts(simpleSubtitle.getCues(3_999_999))) + .containsExactly(SECOND_SUBTITLE_STRING); + + // Test after second subtitle + assertThat(simpleSubtitle.getCues(4_000_000)).isEmpty(); + assertThat(simpleSubtitle.getCues(4_500_000)).isEmpty(); + assertThat(simpleSubtitle.getCues(Long.MAX_VALUE)).isEmpty(); } - private void assertSingleCueTextEquals(String expected, List cues) { - assertThat(cues).hasSize(1); - assertThat(cues.get(0).text.toString()).isEqualTo(expected); + @Test + public void overlappingSubtitleEventTimes() { + assertThat(overlappingSubtitle.getEventTime(0)).isEqualTo(1_000_000); + assertThat(overlappingSubtitle.getEventTime(1)).isEqualTo(2_000_000); + assertThat(overlappingSubtitle.getEventTime(2)).isEqualTo(3_000_000); + assertThat(overlappingSubtitle.getEventTime(3)).isEqualTo(4_000_000); } + @Test + public void overlappingSubtitleEventIndices() { + // Test first event + assertThat(overlappingSubtitle.getNextEventTimeIndex(0)).isEqualTo(0); + assertThat(overlappingSubtitle.getNextEventTimeIndex(500_000)).isEqualTo(0); + assertThat(overlappingSubtitle.getNextEventTimeIndex(999_999)).isEqualTo(0); + + // Test second event + assertThat(overlappingSubtitle.getNextEventTimeIndex(1_000_000)).isEqualTo(1); + assertThat(overlappingSubtitle.getNextEventTimeIndex(1_500_000)).isEqualTo(1); + assertThat(overlappingSubtitle.getNextEventTimeIndex(1_999_999)).isEqualTo(1); + + // Test third event + assertThat(overlappingSubtitle.getNextEventTimeIndex(2_000_000)).isEqualTo(2); + assertThat(overlappingSubtitle.getNextEventTimeIndex(2_500_000)).isEqualTo(2); + assertThat(overlappingSubtitle.getNextEventTimeIndex(2_999_999)).isEqualTo(2); + + // Test fourth event + assertThat(overlappingSubtitle.getNextEventTimeIndex(3_000_000)).isEqualTo(3); + assertThat(overlappingSubtitle.getNextEventTimeIndex(3_500_000)).isEqualTo(3); + assertThat(overlappingSubtitle.getNextEventTimeIndex(3_999_999)).isEqualTo(3); + + // Test null event (i.e. look for events after the last event) + assertThat(overlappingSubtitle.getNextEventTimeIndex(4_000_000)).isEqualTo(INDEX_UNSET); + assertThat(overlappingSubtitle.getNextEventTimeIndex(4_500_000)).isEqualTo(INDEX_UNSET); + assertThat(overlappingSubtitle.getNextEventTimeIndex(MAX_VALUE)).isEqualTo(INDEX_UNSET); + } + + @Test + public void overlappingSubtitleText() { + // Test before first subtitle + assertThat(overlappingSubtitle.getCues(0)).isEmpty(); + assertThat(overlappingSubtitle.getCues(500_000)).isEmpty(); + assertThat(overlappingSubtitle.getCues(999_999)).isEmpty(); + + // Test first subtitle + assertThat(getCueTexts(overlappingSubtitle.getCues(1_000_000))) + .containsExactly(FIRST_SUBTITLE_STRING); + assertThat(getCueTexts(overlappingSubtitle.getCues(1_500_000))) + .containsExactly(FIRST_SUBTITLE_STRING); + assertThat(getCueTexts(overlappingSubtitle.getCues(1_999_999))) + .containsExactly(FIRST_SUBTITLE_STRING); + + // Test after first and second subtitle + assertThat(getCueTexts(overlappingSubtitle.getCues(2_000_000))) + .containsExactly(FIRST_SUBTITLE_STRING, SECOND_SUBTITLE_STRING); + assertThat(getCueTexts(overlappingSubtitle.getCues(2_500_000))) + .containsExactly(FIRST_SUBTITLE_STRING, SECOND_SUBTITLE_STRING); + assertThat(getCueTexts(overlappingSubtitle.getCues(2_999_999))) + .containsExactly(FIRST_SUBTITLE_STRING, SECOND_SUBTITLE_STRING); + + // Test second subtitle + assertThat(getCueTexts(overlappingSubtitle.getCues(3_000_000))) + .containsExactly(SECOND_SUBTITLE_STRING); + assertThat(getCueTexts(overlappingSubtitle.getCues(3_500_000))) + .containsExactly(SECOND_SUBTITLE_STRING); + assertThat(getCueTexts(overlappingSubtitle.getCues(3_999_999))) + .containsExactly(SECOND_SUBTITLE_STRING); + + // Test after second subtitle + assertThat(overlappingSubtitle.getCues(4_000_000)).isEmpty(); + assertThat(overlappingSubtitle.getCues(4_500_000)).isEmpty(); + assertThat(overlappingSubtitle.getCues(Long.MAX_VALUE)).isEmpty(); + } + + @Test + public void nestedSubtitleEventTimes() { + assertThat(nestedSubtitle.getEventTime(0)).isEqualTo(1_000_000); + assertThat(nestedSubtitle.getEventTime(1)).isEqualTo(2_000_000); + assertThat(nestedSubtitle.getEventTime(2)).isEqualTo(3_000_000); + assertThat(nestedSubtitle.getEventTime(3)).isEqualTo(4_000_000); + } + + @Test + public void nestedSubtitleEventIndices() { + // Test first event + assertThat(nestedSubtitle.getNextEventTimeIndex(0)).isEqualTo(0); + assertThat(nestedSubtitle.getNextEventTimeIndex(500_000)).isEqualTo(0); + assertThat(nestedSubtitle.getNextEventTimeIndex(999_999)).isEqualTo(0); + + // Test second event + assertThat(nestedSubtitle.getNextEventTimeIndex(1_000_000)).isEqualTo(1); + assertThat(nestedSubtitle.getNextEventTimeIndex(1_500_000)).isEqualTo(1); + assertThat(nestedSubtitle.getNextEventTimeIndex(1_999_999)).isEqualTo(1); + + // Test third event + assertThat(nestedSubtitle.getNextEventTimeIndex(2_000_000)).isEqualTo(2); + assertThat(nestedSubtitle.getNextEventTimeIndex(2_500_000)).isEqualTo(2); + assertThat(nestedSubtitle.getNextEventTimeIndex(2_999_999)).isEqualTo(2); + + // Test fourth event + assertThat(nestedSubtitle.getNextEventTimeIndex(3_000_000)).isEqualTo(3); + assertThat(nestedSubtitle.getNextEventTimeIndex(3_500_000)).isEqualTo(3); + assertThat(nestedSubtitle.getNextEventTimeIndex(3_999_999)).isEqualTo(3); + + // Test null event (i.e. look for events after the last event) + assertThat(nestedSubtitle.getNextEventTimeIndex(4_000_000)).isEqualTo(INDEX_UNSET); + assertThat(nestedSubtitle.getNextEventTimeIndex(4_500_000)).isEqualTo(INDEX_UNSET); + assertThat(nestedSubtitle.getNextEventTimeIndex(MAX_VALUE)).isEqualTo(INDEX_UNSET); + } + + @Test + public void nestedSubtitleText() { + // Test before first subtitle + assertThat(nestedSubtitle.getCues(0)).isEmpty(); + assertThat(nestedSubtitle.getCues(500_000)).isEmpty(); + assertThat(nestedSubtitle.getCues(999_999)).isEmpty(); + + // Test first subtitle + assertThat(getCueTexts(nestedSubtitle.getCues(1_000_000))) + .containsExactly(FIRST_SUBTITLE_STRING); + assertThat(getCueTexts(nestedSubtitle.getCues(1_500_000))) + .containsExactly(FIRST_SUBTITLE_STRING); + assertThat(getCueTexts(nestedSubtitle.getCues(1_999_999))) + .containsExactly(FIRST_SUBTITLE_STRING); + + // Test after first and second subtitle + assertThat(getCueTexts(nestedSubtitle.getCues(2_000_000))) + .containsExactly(FIRST_SUBTITLE_STRING, SECOND_SUBTITLE_STRING); + assertThat(getCueTexts(nestedSubtitle.getCues(2_500_000))) + .containsExactly(FIRST_SUBTITLE_STRING, SECOND_SUBTITLE_STRING); + assertThat(getCueTexts(nestedSubtitle.getCues(2_999_999))) + .containsExactly(FIRST_SUBTITLE_STRING, SECOND_SUBTITLE_STRING); + + // Test first subtitle + assertThat(getCueTexts(nestedSubtitle.getCues(3_000_000))) + .containsExactly(FIRST_SUBTITLE_STRING); + assertThat(getCueTexts(nestedSubtitle.getCues(3_500_000))) + .containsExactly(FIRST_SUBTITLE_STRING); + assertThat(getCueTexts(nestedSubtitle.getCues(3_999_999))) + .containsExactly(FIRST_SUBTITLE_STRING); + + // Test after second subtitle + assertThat(nestedSubtitle.getCues(4_000_000)).isEmpty(); + assertThat(nestedSubtitle.getCues(4_500_000)).isEmpty(); + assertThat(nestedSubtitle.getCues(Long.MAX_VALUE)).isEmpty(); + } + + private static List getCueTexts(List cues) { + List cueTexts = new ArrayList<>(); + for (int i = 0; i < cues.size(); i++) { + cueTexts.add(cues.get(i).text.toString()); + } + return cueTexts; + } } 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 af935048e8..b14e4b123e 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 @@ -63,7 +63,7 @@ public final class AdaptiveTrackSelectionTest { @Test @SuppressWarnings("deprecation") - public void testFactoryUsesInitiallyProvidedBandwidthMeter() { + public void factoryUsesInitiallyProvidedBandwidthMeter() { BandwidthMeter initialBandwidthMeter = mock(BandwidthMeter.class); BandwidthMeter injectedBandwidthMeter = mock(BandwidthMeter.class); Format format1 = videoFormat(/* bitrate= */ 500, /* width= */ 320, /* height= */ 240); @@ -72,7 +72,7 @@ public final class AdaptiveTrackSelectionTest { new AdaptiveTrackSelection.Factory(initialBandwidthMeter) .createTrackSelections( new Definition[] { - new Definition(new TrackGroup(format1, format2), /* tracks= */ 0, 1) + new Definition(new TrackGroup(format1, format2), /* tracks=... */ 0, 1) }, injectedBandwidthMeter); trackSelections[0].updateSelectedTrack( @@ -87,7 +87,7 @@ public final class AdaptiveTrackSelectionTest { } @Test - public void testSelectInitialIndexUseMaxInitialBitrateIfNoBandwidthEstimate() { + public void selectInitialIndexUseMaxInitialBitrateIfNoBandwidthEstimate() { Format format1 = videoFormat(/* bitrate= */ 500, /* width= */ 320, /* height= */ 240); Format format2 = videoFormat(/* bitrate= */ 1000, /* width= */ 640, /* height= */ 480); Format format3 = videoFormat(/* bitrate= */ 2000, /* width= */ 960, /* height= */ 720); @@ -101,7 +101,7 @@ public final class AdaptiveTrackSelectionTest { } @Test - public void testSelectInitialIndexUseBandwidthEstimateIfAvailable() { + public void selectInitialIndexUseBandwidthEstimateIfAvailable() { Format format1 = videoFormat(/* bitrate= */ 500, /* width= */ 320, /* height= */ 240); Format format2 = videoFormat(/* bitrate= */ 1000, /* width= */ 640, /* height= */ 480); Format format3 = videoFormat(/* bitrate= */ 2000, /* width= */ 960, /* height= */ 720); @@ -115,7 +115,7 @@ public final class AdaptiveTrackSelectionTest { } @Test - public void testUpdateSelectedTrackDoNotSwitchUpIfNotBufferedEnough() { + public void updateSelectedTrackDoNotSwitchUpIfNotBufferedEnough() { Format format1 = videoFormat(/* bitrate= */ 500, /* width= */ 320, /* height= */ 240); Format format2 = videoFormat(/* bitrate= */ 1000, /* width= */ 640, /* height= */ 480); Format format3 = videoFormat(/* bitrate= */ 2000, /* width= */ 960, /* height= */ 720); @@ -143,7 +143,7 @@ public final class AdaptiveTrackSelectionTest { } @Test - public void testUpdateSelectedTrackSwitchUpIfBufferedEnough() { + public void updateSelectedTrackSwitchUpIfBufferedEnough() { Format format1 = videoFormat(/* bitrate= */ 500, /* width= */ 320, /* height= */ 240); Format format2 = videoFormat(/* bitrate= */ 1000, /* width= */ 640, /* height= */ 480); Format format3 = videoFormat(/* bitrate= */ 2000, /* width= */ 960, /* height= */ 720); @@ -171,7 +171,7 @@ public final class AdaptiveTrackSelectionTest { } @Test - public void testUpdateSelectedTrackDoNotSwitchDownIfBufferedEnough() { + public void updateSelectedTrackDoNotSwitchDownIfBufferedEnough() { Format format1 = videoFormat(/* bitrate= */ 500, /* width= */ 320, /* height= */ 240); Format format2 = videoFormat(/* bitrate= */ 1000, /* width= */ 640, /* height= */ 480); Format format3 = videoFormat(/* bitrate= */ 2000, /* width= */ 960, /* height= */ 720); @@ -199,7 +199,7 @@ public final class AdaptiveTrackSelectionTest { } @Test - public void testUpdateSelectedTrackSwitchDownIfNotBufferedEnough() { + public void updateSelectedTrackSwitchDownIfNotBufferedEnough() { Format format1 = videoFormat(/* bitrate= */ 500, /* width= */ 320, /* height= */ 240); Format format2 = videoFormat(/* bitrate= */ 1000, /* width= */ 640, /* height= */ 480); Format format3 = videoFormat(/* bitrate= */ 2000, /* width= */ 960, /* height= */ 720); @@ -227,7 +227,7 @@ public final class AdaptiveTrackSelectionTest { } @Test - public void testEvaluateQueueSizeReturnQueueSizeIfBandwidthIsNotImproved() { + public void evaluateQueueSizeReturnQueueSizeIfBandwidthIsNotImproved() { Format format1 = videoFormat(/* bitrate= */ 500, /* width= */ 320, /* height= */ 240); Format format2 = videoFormat(/* bitrate= */ 1000, /* width= */ 640, /* height= */ 480); Format format3 = videoFormat(/* bitrate= */ 2000, /* width= */ 960, /* height= */ 720); @@ -252,7 +252,7 @@ public final class AdaptiveTrackSelectionTest { } @Test - public void testEvaluateQueueSizeDoNotReevaluateUntilAfterMinTimeBetweenBufferReevaluation() { + public void evaluateQueueSizeDoNotReevaluateUntilAfterMinTimeBetweenBufferReevaluation() { Format format1 = videoFormat(/* bitrate= */ 500, /* width= */ 320, /* height= */ 240); Format format2 = videoFormat(/* bitrate= */ 1000, /* width= */ 640, /* height= */ 480); Format format3 = videoFormat(/* bitrate= */ 2000, /* width= */ 960, /* height= */ 720); @@ -289,7 +289,7 @@ public final class AdaptiveTrackSelectionTest { } @Test - public void testEvaluateQueueSizeRetainMoreThanMinimumDurationAfterDiscard() { + public void evaluateQueueSizeRetainMoreThanMinimumDurationAfterDiscard() { Format format1 = videoFormat(/* bitrate= */ 500, /* width= */ 320, /* height= */ 240); Format format2 = videoFormat(/* bitrate= */ 1000, /* width= */ 640, /* height= */ 480); Format format3 = videoFormat(/* bitrate= */ 2000, /* width= */ 960, /* height= */ 720); @@ -406,17 +406,11 @@ public final class AdaptiveTrackSelectionTest { } private static Format videoFormat(int bitrate, int width, int height) { - return Format.createVideoSampleFormat( - /* id= */ null, - /* sampleMimeType= */ MimeTypes.VIDEO_H264, - /* codecs= */ null, - /* bitrate= */ bitrate, - /* maxInputSize= */ Format.NO_VALUE, - /* width= */ width, - /* height= */ height, - /* frameRate= */ Format.NO_VALUE, - /* initializationData= */ null, - /* drmInitData= */ null); + return new Format.Builder() + .setSampleMimeType(MimeTypes.VIDEO_H264) + .setAverageBitrate(bitrate) + .setWidth(width) + .setHeight(height) + .build(); } - } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/trackselection/BufferSizeAdaptiveTrackSelectionTest.java b/library/core/src/test/java/com/google/android/exoplayer2/trackselection/BufferSizeAdaptiveTrackSelectionTest.java deleted file mode 100644 index 8b20630a23..0000000000 --- a/library/core/src/test/java/com/google/android/exoplayer2/trackselection/BufferSizeAdaptiveTrackSelectionTest.java +++ /dev/null @@ -1,248 +0,0 @@ -/* - * Copyright (C) 2019 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.mockito.Mockito.when; -import static org.mockito.MockitoAnnotations.initMocks; - -import android.util.Pair; -import androidx.test.ext.junit.runners.AndroidJUnit4; -import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.Format; -import com.google.android.exoplayer2.LoadControl; -import com.google.android.exoplayer2.source.TrackGroup; -import com.google.android.exoplayer2.source.chunk.MediaChunkIterator; -import com.google.android.exoplayer2.upstream.BandwidthMeter; -import com.google.android.exoplayer2.util.MimeTypes; -import java.util.Collections; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; - -/** Unit test for the track selection created by {@link BufferSizeAdaptationBuilder}. */ -@RunWith(AndroidJUnit4.class) -public final class BufferSizeAdaptiveTrackSelectionTest { - - private static final int MIN_BUFFER_MS = 15_000; - private static final int MAX_BUFFER_MS = 50_000; - private static final int HYSTERESIS_BUFFER_MS = 10_000; - private static final float BANDWIDTH_FRACTION = 0.5f; - private static final int MIN_BUFFER_FOR_QUALITY_INCREASE_MS = 10_000; - - /** - * Factor between bitrates is always the same (=2.2). That means buffer levels should be linearly - * distributed between MIN_BUFFER=15s and MAX_BUFFER-HYSTERESIS=50s-10s=40s. - */ - private static final Format format1 = - createVideoFormat(/* bitrate= */ 500, /* width= */ 320, /* height= */ 240); - - private static final Format format2 = - createVideoFormat(/* bitrate= */ 1100, /* width= */ 640, /* height= */ 480); - private static final Format format3 = - createVideoFormat(/* bitrate= */ 2420, /* width= */ 960, /* height= */ 720); - private static final int BUFFER_LEVEL_FORMAT_2 = - (MIN_BUFFER_MS + MAX_BUFFER_MS - HYSTERESIS_BUFFER_MS) / 2; - private static final int BUFFER_LEVEL_FORMAT_3 = MAX_BUFFER_MS - HYSTERESIS_BUFFER_MS; - - @Mock private BandwidthMeter mockBandwidthMeter; - private TrackSelection trackSelection; - - @Before - public void setUp() { - initMocks(this); - Pair trackSelectionFactoryAndLoadControl = - new BufferSizeAdaptationBuilder() - .setBufferDurationsMs( - MIN_BUFFER_MS, - MAX_BUFFER_MS, - /* bufferForPlaybackMs= */ 1000, - /* bufferForPlaybackAfterRebufferMs= */ 1000) - .setHysteresisBufferMs(HYSTERESIS_BUFFER_MS) - .setStartUpTrackSelectionParameters( - BANDWIDTH_FRACTION, MIN_BUFFER_FOR_QUALITY_INCREASE_MS) - .buildPlayerComponents(); - trackSelection = - trackSelectionFactoryAndLoadControl - .first - .createTrackSelections( - new TrackSelection.Definition[] { - new TrackSelection.Definition( - new TrackGroup(format1, format2, format3), /* tracks= */ 0, 1, 2) - }, - mockBandwidthMeter)[0]; - trackSelection.enable(); - } - - @Test - public void updateSelectedTrack_usesBandwidthEstimateForInitialSelection() { - when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(getBitrateEstimateEnoughFor(format2)); - - updateSelectedTrack(/* bufferedDurationMs= */ 0); - - assertThat(trackSelection.getSelectedFormat()).isEqualTo(format2); - assertThat(trackSelection.getSelectionReason()).isEqualTo(C.SELECTION_REASON_INITIAL); - } - - @Test - public void updateSelectedTrack_withLowerBandwidthEstimateDuringStartUp_switchesDown() { - when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(getBitrateEstimateEnoughFor(format2)); - updateSelectedTrack(/* bufferedDurationMs= */ 0); - when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(0L); - - updateSelectedTrack(/* bufferedDurationMs= */ 0); - - assertThat(trackSelection.getSelectedFormat()).isEqualTo(format1); - assertThat(trackSelection.getSelectionReason()).isEqualTo(C.SELECTION_REASON_ADAPTIVE); - } - - @Test - public void - updateSelectedTrack_withHigherBandwidthEstimateDuringStartUp_andLowBuffer_keepsSelection() { - when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(getBitrateEstimateEnoughFor(format2)); - updateSelectedTrack(/* bufferedDurationMs= */ 0); - when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(getBitrateEstimateEnoughFor(format3)); - - updateSelectedTrack(/* bufferedDurationMs= */ MIN_BUFFER_FOR_QUALITY_INCREASE_MS - 1); - - assertThat(trackSelection.getSelectedFormat()).isEqualTo(format2); - assertThat(trackSelection.getSelectionReason()).isEqualTo(C.SELECTION_REASON_INITIAL); - } - - @Test - public void - updateSelectedTrack_withHigherBandwidthEstimateDuringStartUp_andHighBuffer_switchesUp() { - when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(getBitrateEstimateEnoughFor(format2)); - updateSelectedTrack(/* bufferedDurationMs= */ 0); - when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(getBitrateEstimateEnoughFor(format3)); - - updateSelectedTrack(/* bufferedDurationMs= */ MIN_BUFFER_FOR_QUALITY_INCREASE_MS); - - assertThat(trackSelection.getSelectedFormat()).isEqualTo(format3); - assertThat(trackSelection.getSelectionReason()).isEqualTo(C.SELECTION_REASON_ADAPTIVE); - } - - @Test - public void - updateSelectedTrack_withIncreasedBandwidthEstimate_onceSteadyStateBufferIsReached_keepsSelection() { - when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(getBitrateEstimateEnoughFor(format2)); - updateSelectedTrack(/* bufferedDurationMs= */ 0); - updateSelectedTrack(/* bufferedDurationMs= */ BUFFER_LEVEL_FORMAT_2); - when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(getBitrateEstimateEnoughFor(format3)); - - updateSelectedTrack(/* bufferedDurationMs= */ BUFFER_LEVEL_FORMAT_2); - - assertThat(trackSelection.getSelectedFormat()).isEqualTo(format2); - assertThat(trackSelection.getSelectionReason()).isEqualTo(C.SELECTION_REASON_INITIAL); - } - - @Test - public void - updateSelectedTrack_withDecreasedBandwidthEstimate_onceSteadyStateBufferIsReached_keepsSelection() { - when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(getBitrateEstimateEnoughFor(format2)); - updateSelectedTrack(/* bufferedDurationMs= */ 0); - updateSelectedTrack(/* bufferedDurationMs= */ BUFFER_LEVEL_FORMAT_2); - when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(0L); - - updateSelectedTrack(/* bufferedDurationMs= */ BUFFER_LEVEL_FORMAT_2); - - assertThat(trackSelection.getSelectedFormat()).isEqualTo(format2); - assertThat(trackSelection.getSelectionReason()).isEqualTo(C.SELECTION_REASON_INITIAL); - } - - @Test - public void updateSelectedTrack_withIncreasedBufferInSteadyState_switchesUp() { - when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(getBitrateEstimateEnoughFor(format2)); - updateSelectedTrack(/* bufferedDurationMs= */ 0); - updateSelectedTrack(/* bufferedDurationMs= */ BUFFER_LEVEL_FORMAT_2); - - updateSelectedTrack(/* bufferedDurationMs= */ BUFFER_LEVEL_FORMAT_3); - - assertThat(trackSelection.getSelectedFormat()).isEqualTo(format3); - assertThat(trackSelection.getSelectionReason()).isEqualTo(C.SELECTION_REASON_ADAPTIVE); - } - - @Test - public void updateSelectedTrack_withDecreasedBufferInSteadyState_switchesDown() { - when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(getBitrateEstimateEnoughFor(format2)); - updateSelectedTrack(/* bufferedDurationMs= */ 0); - updateSelectedTrack(/* bufferedDurationMs= */ BUFFER_LEVEL_FORMAT_2); - when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(0L); - - updateSelectedTrack(/* bufferedDurationMs= */ BUFFER_LEVEL_FORMAT_2 - HYSTERESIS_BUFFER_MS - 1); - - assertThat(trackSelection.getSelectedFormat()).isEqualTo(format1); - assertThat(trackSelection.getSelectionReason()).isEqualTo(C.SELECTION_REASON_ADAPTIVE); - } - - @Test - public void - updateSelectedTrack_withDecreasedBufferInSteadyState_withinHysteresis_keepsSelection() { - when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(getBitrateEstimateEnoughFor(format2)); - updateSelectedTrack(/* bufferedDurationMs= */ 0); - updateSelectedTrack(/* bufferedDurationMs= */ BUFFER_LEVEL_FORMAT_2); - when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(0L); - - updateSelectedTrack(/* bufferedDurationMs= */ BUFFER_LEVEL_FORMAT_2 - HYSTERESIS_BUFFER_MS); - - assertThat(trackSelection.getSelectedFormat()).isEqualTo(format2); - assertThat(trackSelection.getSelectionReason()).isEqualTo(C.SELECTION_REASON_INITIAL); - } - - @Test - public void onDiscontinuity_switchesBackToStartUpState() { - when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(getBitrateEstimateEnoughFor(format2)); - updateSelectedTrack(/* bufferedDurationMs= */ 0); - updateSelectedTrack(/* bufferedDurationMs= */ BUFFER_LEVEL_FORMAT_2); - when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(0L); - - trackSelection.onDiscontinuity(); - updateSelectedTrack(/* bufferedDurationMs= */ BUFFER_LEVEL_FORMAT_2 - 1); - - assertThat(trackSelection.getSelectedFormat()).isEqualTo(format1); - assertThat(trackSelection.getSelectionReason()).isEqualTo(C.SELECTION_REASON_ADAPTIVE); - } - - private void updateSelectedTrack(long bufferedDurationMs) { - trackSelection.updateSelectedTrack( - /* playbackPositionUs= */ 0, - /* bufferedDurationUs= */ C.msToUs(bufferedDurationMs), - /* availableDurationUs= */ C.TIME_UNSET, - /* queue= */ Collections.emptyList(), - /* mediaChunkIterators= */ new MediaChunkIterator[] { - MediaChunkIterator.EMPTY, MediaChunkIterator.EMPTY, MediaChunkIterator.EMPTY - }); - } - - private static Format createVideoFormat(int bitrate, int width, int height) { - return Format.createVideoSampleFormat( - /* id= */ null, - /* sampleMimeType= */ MimeTypes.VIDEO_H264, - /* codecs= */ null, - /* bitrate= */ bitrate, - /* maxInputSize= */ Format.NO_VALUE, - /* width= */ width, - /* height= */ height, - /* frameRate= */ Format.NO_VALUE, - /* initializationData= */ null, - /* drmInitData= */ null); - } - - private static long getBitrateEstimateEnoughFor(Format format) { - return (long) (format.bitrate / BANDWIDTH_FRACTION) + 1; - } -} 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 292742b527..4304c9af9a 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 @@ -49,6 +49,7 @@ import com.google.android.exoplayer2.trackselection.DefaultTrackSelector.Selecti import com.google.android.exoplayer2.trackselection.TrackSelector.InvalidationListener; import com.google.android.exoplayer2.upstream.BandwidthMeter; import com.google.android.exoplayer2.util.MimeTypes; +import com.google.android.exoplayer2.util.Util; import java.util.HashMap; import java.util.Map; import org.junit.Before; @@ -79,8 +80,21 @@ public final class DefaultTrackSelectorTest { private static final RendererCapabilities[] RENDERER_CAPABILITIES_WITH_NO_SAMPLE_RENDERER = new RendererCapabilities[] {VIDEO_CAPABILITIES, NO_SAMPLE_CAPABILITIES}; - private static final Format VIDEO_FORMAT = buildVideoFormat("video"); - private static final Format AUDIO_FORMAT = buildAudioFormat("audio"); + private static final Format VIDEO_FORMAT = + new Format.Builder() + .setSampleMimeType(MimeTypes.VIDEO_H264) + .setWidth(1024) + .setHeight(768) + .build(); + private static final Format AUDIO_FORMAT = + new Format.Builder() + .setSampleMimeType(MimeTypes.AUDIO_AAC) + .setChannelCount(2) + .setSampleRate(44100) + .build(); + private static final Format TEXT_FORMAT = + new Format.Builder().setSampleMimeType(MimeTypes.TEXT_VTT).build(); + 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 = @@ -119,7 +133,7 @@ public final class DefaultTrackSelectorTest { /** Tests {@link Parameters} {@link android.os.Parcelable} implementation. */ @Test - public void testParametersParcelable() { + public void parametersParcelable() { SparseArray> selectionOverrides = new SparseArray<>(); Map videoOverrides = new HashMap<>(); videoOverrides.put(new TrackGroupArray(VIDEO_TRACK_GROUP), new SelectionOverride(0, 1)); @@ -175,7 +189,7 @@ public final class DefaultTrackSelectorTest { /** Tests {@link SelectionOverride}'s {@link android.os.Parcelable} implementation. */ @Test - public void testSelectionOverrideParcelable() { + public void selectionOverrideParcelable() { int[] tracks = new int[] {2, 3}; SelectionOverride selectionOverrideToParcel = new SelectionOverride(/* groupIndex= */ 1, tracks); @@ -193,7 +207,7 @@ public final class DefaultTrackSelectorTest { /** Tests that a null override clears a track selection. */ @Test - public void testSelectTracksWithNullOverride() throws ExoPlaybackException { + public void selectTracksWithNullOverride() throws ExoPlaybackException { trackSelector.setParameters( trackSelector .buildUponParameters() @@ -207,7 +221,7 @@ public final class DefaultTrackSelectorTest { /** Tests that a null override can be cleared. */ @Test - public void testSelectTracksWithClearedNullOverride() throws ExoPlaybackException { + public void selectTracksWithClearedNullOverride() throws ExoPlaybackException { trackSelector.setParameters( trackSelector .buildUponParameters() @@ -222,7 +236,7 @@ public final class DefaultTrackSelectorTest { /** Tests that an override is not applied for a different set of available track groups. */ @Test - public void testSelectTracksWithNullOverrideForDifferentTracks() throws ExoPlaybackException { + public void selectTracksWithNullOverrideForDifferentTracks() throws ExoPlaybackException { trackSelector.setParameters( trackSelector .buildUponParameters() @@ -240,7 +254,7 @@ public final class DefaultTrackSelectorTest { /** Tests disabling a renderer. */ @Test - public void testSelectTracksWithDisabledRenderer() throws ExoPlaybackException { + public void selectTracksWithDisabledRenderer() throws ExoPlaybackException { trackSelector.setParameters(defaultParameters.buildUpon().setRendererDisabled(1, true)); TrackSelectorResult result = trackSelector.selectTracks(RENDERER_CAPABILITIES, TRACK_GROUPS, periodId, TIMELINE); @@ -251,7 +265,7 @@ public final class DefaultTrackSelectorTest { /** Tests that a disabled renderer can be enabled again. */ @Test - public void testSelectTracksWithClearedDisabledRenderer() throws ExoPlaybackException { + public void selectTracksWithClearedDisabledRenderer() throws ExoPlaybackException { trackSelector.setParameters( trackSelector .buildUponParameters() @@ -266,7 +280,7 @@ public final class DefaultTrackSelectorTest { /** Tests a no-sample renderer is enabled without a track selection by default. */ @Test - public void testSelectTracksWithNoSampleRenderer() throws ExoPlaybackException { + public void selectTracksWithNoSampleRenderer() throws ExoPlaybackException { TrackSelectorResult result = trackSelector.selectTracks( RENDERER_CAPABILITIES_WITH_NO_SAMPLE_RENDERER, TRACK_GROUPS, periodId, TIMELINE); @@ -277,7 +291,7 @@ public final class DefaultTrackSelectorTest { /** Tests disabling a no-sample renderer. */ @Test - public void testSelectTracksWithDisabledNoSampleRenderer() throws ExoPlaybackException { + public void selectTracksWithDisabledNoSampleRenderer() throws ExoPlaybackException { trackSelector.setParameters(defaultParameters.buildUpon().setRendererDisabled(1, true)); TrackSelectorResult result = trackSelector.selectTracks( @@ -293,7 +307,7 @@ public final class DefaultTrackSelectorTest { * {@link Parameters}. */ @Test - public void testSetParameterWithDefaultParametersDoesNotNotifyInvalidationListener() { + public void setParameterWithDefaultParametersDoesNotNotifyInvalidationListener() { trackSelector.setParameters(defaultParameters); verify(invalidationListener, never()).onTrackSelectionsInvalidated(); } @@ -303,7 +317,7 @@ public final class DefaultTrackSelectorTest { * when it's set with non-default values of {@link Parameters}. */ @Test - public void testSetParameterWithNonDefaultParameterNotifyInvalidationListener() { + public void setParameterWithNonDefaultParameterNotifyInvalidationListener() { ParametersBuilder builder = defaultParameters.buildUpon().setPreferredAudioLanguage("eng"); trackSelector.setParameters(builder); verify(invalidationListener).onTrackSelectionsInvalidated(); @@ -315,7 +329,7 @@ public final class DefaultTrackSelectorTest { * of {@link Parameters}. */ @Test - public void testSetParameterWithSameParametersDoesNotNotifyInvalidationListenerAgain() { + public void setParameterWithSameParametersDoesNotNotifyInvalidationListenerAgain() { ParametersBuilder builder = defaultParameters.buildUpon().setPreferredAudioLanguage("eng"); trackSelector.setParameters(builder); trackSelector.setParameters(builder); @@ -323,17 +337,15 @@ public final class DefaultTrackSelectorTest { } /** - * Tests that track selector will select audio track with {@link C#SELECTION_FLAG_DEFAULT} - * given default values of {@link Parameters}. + * Tests that track selector will select audio track with {@link C#SELECTION_FLAG_DEFAULT} given + * default values of {@link Parameters}. */ @Test - public void testSelectTracksSelectTrackWithSelectionFlag() throws Exception { - Format audioFormat = - buildAudioFormatWithLanguageAndFlags( - "audio", /* language= */ null, /* selectionFlags= */ 0); + public void selectTracksSelectTrackWithSelectionFlag() throws Exception { + Format.Builder formatBuilder = AUDIO_FORMAT.buildUpon(); + Format audioFormat = formatBuilder.setSelectionFlags(0).build(); Format formatWithSelectionFlag = - buildAudioFormatWithLanguageAndFlags( - "audio", /* language= */ null, C.SELECTION_FLAG_DEFAULT); + formatBuilder.setSelectionFlags(C.SELECTION_FLAG_DEFAULT).build(); TrackGroupArray trackGroups = wrapFormats(audioFormat, formatWithSelectionFlag); TrackSelectorResult result = @@ -346,46 +358,12 @@ public final class DefaultTrackSelectorTest { } /** Tests that adaptive audio track selections respect the maximum audio bitrate. */ - public void testSelectAdaptiveAudioTrackGroupWithMaxBitrate() throws ExoPlaybackException { - Format format128k = - Format.createAudioSampleFormat( - /* id= */ "128", - /* sampleMimeType= */ MimeTypes.AUDIO_AAC, - /* codecs= */ "mp4a.40.2", - /* bitrate= */ 128 * 1024, - /* maxInputSize= */ Format.NO_VALUE, - /* channelCount= */ 2, - /* sampleRate= */ 44100, - /* initializationData= */ null, - /* drmInitData= */ null, - /* selectionFlags= */ 0, - /* language= */ null); - Format format192k = - Format.createAudioSampleFormat( - /* id= */ "192", - /* sampleMimeType= */ MimeTypes.AUDIO_AAC, - /* codecs= */ "mp4a.40.2", - /* bitrate= */ 192 * 1024, - /* maxInputSize= */ Format.NO_VALUE, - /* channelCount= */ 2, - /* sampleRate= */ 44100, - /* initializationData= */ null, - /* drmInitData= */ null, - /* selectionFlags= */ 0, - /* language= */ null); - Format format256k = - Format.createAudioSampleFormat( - /* id= */ "256", - /* sampleMimeType= */ MimeTypes.AUDIO_AAC, - /* codecs= */ "mp4a.40.2", - /* bitrate= */ 256 * 1024, - /* maxInputSize= */ Format.NO_VALUE, - /* channelCount= */ 2, - /* sampleRate= */ 44100, - /* initializationData= */ null, - /* drmInitData= */ null, - /* selectionFlags= */ 0, - /* language= */ null); + @Test + public void selectAdaptiveAudioTrackGroupWithMaxBitrate() throws ExoPlaybackException { + Format.Builder formatBuilder = AUDIO_FORMAT.buildUpon(); + Format format128k = formatBuilder.setAverageBitrate(128 * 1024).build(); + Format format192k = formatBuilder.setAverageBitrate(192 * 1024).build(); + Format format256k = formatBuilder.setAverageBitrate(256 * 1024).build(); RendererCapabilities[] rendererCapabilities = { ALL_AUDIO_FORMAT_SUPPORTED_RENDERER_CAPABILITIES }; @@ -394,7 +372,7 @@ public final class DefaultTrackSelectorTest { TrackSelectorResult result = trackSelector.selectTracks(rendererCapabilities, trackGroups, periodId, TIMELINE); - assertAdaptiveSelection(result.selections.get(0), trackGroups.get(0), 0, 1, 2); + assertAdaptiveSelection(result.selections.get(0), trackGroups.get(0), 2, 0, 1); trackSelector.setParameters( trackSelector.buildUponParameters().setMaxAudioBitrate(256 * 1024 - 1)); @@ -408,11 +386,11 @@ public final class DefaultTrackSelectorTest { trackSelector.setParameters( trackSelector.buildUponParameters().setMaxAudioBitrate(192 * 1024 - 1)); result = trackSelector.selectTracks(rendererCapabilities, trackGroups, periodId, TIMELINE); - assertAdaptiveSelection(result.selections.get(0), trackGroups.get(0), 1); + assertFixedSelection(result.selections.get(0), trackGroups.get(0), 1); trackSelector.setParameters(trackSelector.buildUponParameters().setMaxAudioBitrate(10)); result = trackSelector.selectTracks(rendererCapabilities, trackGroups, periodId, TIMELINE); - assertAdaptiveSelection(result.selections.get(0), trackGroups.get(0), 1); + assertFixedSelection(result.selections.get(0), trackGroups.get(0), 1); } /** @@ -420,14 +398,10 @@ public final class DefaultTrackSelectorTest { * given by {@link Parameters}. */ @Test - public void testSelectTracksSelectPreferredAudioLanguage() - throws Exception { - Format frAudioFormat = - Format.createAudioSampleFormat("audio", MimeTypes.AUDIO_AAC, null, Format.NO_VALUE, - Format.NO_VALUE, 2, 44100, null, null, 0, "fra"); - Format enAudioFormat = - Format.createAudioSampleFormat("audio", MimeTypes.AUDIO_AAC, null, Format.NO_VALUE, - Format.NO_VALUE, 2, 44100, null, null, 0, "eng"); + public void selectTracksSelectPreferredAudioLanguage() throws Exception { + Format.Builder formatBuilder = AUDIO_FORMAT.buildUpon(); + Format frAudioFormat = formatBuilder.setLanguage("fra").build(); + Format enAudioFormat = formatBuilder.setLanguage("eng").build(); TrackGroupArray trackGroups = wrapFormats(frAudioFormat, enAudioFormat); trackSelector.setParameters(defaultParameters.buildUpon().setPreferredAudioLanguage("eng")); @@ -445,15 +419,12 @@ public final class DefaultTrackSelectorTest { * language given by {@link Parameters} over track with {@link C#SELECTION_FLAG_DEFAULT}. */ @Test - public void testSelectTracksSelectPreferredAudioLanguageOverSelectionFlag() - throws Exception { - Format frAudioFormat = - Format.createAudioSampleFormat("audio", MimeTypes.AUDIO_AAC, null, Format.NO_VALUE, - Format.NO_VALUE, 2, 44100, null, null, C.SELECTION_FLAG_DEFAULT, "fra"); - Format enAudioFormat = - Format.createAudioSampleFormat("audio", MimeTypes.AUDIO_AAC, null, Format.NO_VALUE, - Format.NO_VALUE, 2, 44100, null, null, 0, "eng"); - TrackGroupArray trackGroups = wrapFormats(frAudioFormat, enAudioFormat); + public void selectTracksSelectPreferredAudioLanguageOverSelectionFlag() throws Exception { + Format.Builder formatBuilder = AUDIO_FORMAT.buildUpon(); + Format frDefaultFormat = + formatBuilder.setLanguage("fra").setSelectionFlags(C.SELECTION_FLAG_DEFAULT).build(); + Format enNonDefaultFormat = formatBuilder.setLanguage("eng").setSelectionFlags(0).build(); + TrackGroupArray trackGroups = wrapFormats(frDefaultFormat, enNonDefaultFormat); trackSelector.setParameters(defaultParameters.buildUpon().setPreferredAudioLanguage("eng")); TrackSelectorResult result = @@ -462,17 +433,18 @@ public final class DefaultTrackSelectorTest { trackGroups, periodId, TIMELINE); - assertFixedSelection(result.selections.get(0), trackGroups, enAudioFormat); + assertFixedSelection(result.selections.get(0), trackGroups, enNonDefaultFormat); } /** - * Tests that track selector will prefer tracks that are within renderer's capabilities over - * track that exceed renderer's capabilities. + * Tests that track selector will prefer tracks that are within renderer's capabilities over track + * that exceed renderer's capabilities. */ @Test - public void testSelectTracksPreferTrackWithinCapabilities() throws Exception { - Format supportedFormat = buildAudioFormat("supportedFormat"); - Format exceededFormat = buildAudioFormat("exceededFormat"); + public void selectTracksPreferTrackWithinCapabilities() throws Exception { + Format.Builder formatBuilder = AUDIO_FORMAT.buildUpon(); + Format supportedFormat = formatBuilder.setId("supportedFormat").build(); + Format exceededFormat = formatBuilder.setId("exceededFormat").build(); TrackGroupArray trackGroups = wrapFormats(exceededFormat, supportedFormat); Map mappedCapabilities = new HashMap<>(); @@ -495,12 +467,9 @@ public final class DefaultTrackSelectorTest { * there are no other choice, given the default {@link Parameters}. */ @Test - public void testSelectTracksWithNoTrackWithinCapabilitiesSelectExceededCapabilityTrack() + public void selectTracksWithNoTrackWithinCapabilitiesSelectExceededCapabilityTrack() throws Exception { - Format audioFormat = - Format.createAudioSampleFormat("audio", MimeTypes.AUDIO_AAC, null, Format.NO_VALUE, - Format.NO_VALUE, 2, 44100, null, null, 0, null); - TrackGroupArray trackGroups = singleTrackGroup(audioFormat); + TrackGroupArray trackGroups = singleTrackGroup(AUDIO_FORMAT); TrackSelectorResult result = trackSelector.selectTracks( @@ -508,21 +477,18 @@ public final class DefaultTrackSelectorTest { trackGroups, periodId, TIMELINE); - assertFixedSelection(result.selections.get(0), trackGroups, audioFormat); + assertFixedSelection(result.selections.get(0), trackGroups, AUDIO_FORMAT); } /** - * Tests that track selector will return a null track selection for a renderer when - * all tracks exceed that renderer's capabilities when {@link Parameters} does not allow + * Tests that track selector will return a null track selection for a renderer when all tracks + * exceed that renderer's capabilities when {@link Parameters} does not allow * exceeding-capabilities tracks. */ @Test - public void testSelectTracksWithNoTrackWithinCapabilitiesAndSetByParamsReturnNoSelection() + public void selectTracksWithNoTrackWithinCapabilitiesAndSetByParamsReturnNoSelection() throws Exception { - Format audioFormat = - Format.createAudioSampleFormat("audio", MimeTypes.AUDIO_AAC, null, Format.NO_VALUE, - Format.NO_VALUE, 2, 44100, null, null, 0, null); - TrackGroupArray trackGroups = singleTrackGroup(audioFormat); + TrackGroupArray trackGroups = singleTrackGroup(AUDIO_FORMAT); trackSelector.setParameters( defaultParameters.buildUpon().setExceedRendererCapabilitiesIfNecessary(false)); @@ -540,24 +506,11 @@ public final class DefaultTrackSelectorTest { * tracks that have {@link C#SELECTION_FLAG_DEFAULT} but exceed renderer's capabilities. */ @Test - public void testSelectTracksPreferTrackWithinCapabilitiesOverSelectionFlag() - throws Exception { + public void selectTracksPreferTrackWithinCapabilitiesOverSelectionFlag() throws Exception { + Format.Builder formatBuilder = AUDIO_FORMAT.buildUpon(); Format exceededWithSelectionFlagFormat = - Format.createAudioSampleFormat( - "exceededFormat", - MimeTypes.AUDIO_AAC, - null, - Format.NO_VALUE, - Format.NO_VALUE, - 2, - 44100, - null, - null, - C.SELECTION_FLAG_DEFAULT, - null); - Format supportedFormat = - Format.createAudioSampleFormat("supportedFormat", MimeTypes.AUDIO_AAC, null, - Format.NO_VALUE, Format.NO_VALUE, 2, 44100, null, null, 0, null); + formatBuilder.setId("exceededFormat").setSelectionFlags(C.SELECTION_FLAG_DEFAULT).build(); + Format supportedFormat = formatBuilder.setId("supportedFormat").setSelectionFlags(0).build(); TrackGroupArray trackGroups = wrapFormats(exceededWithSelectionFlagFormat, supportedFormat); Map mappedCapabilities = new HashMap<>(); @@ -576,29 +529,15 @@ public final class DefaultTrackSelectorTest { } /** - * Tests that track selector will prefer tracks that are within renderer's capabilities over - * track that have language matching preferred audio given by {@link Parameters} but exceed - * renderer's capabilities. + * Tests that track selector will prefer tracks that are within renderer's capabilities over track + * that have language matching preferred audio given by {@link Parameters} but exceed renderer's + * capabilities. */ @Test - public void testSelectTracksPreferTrackWithinCapabilitiesOverPreferredLanguage() - throws Exception { - Format exceededEnFormat = - Format.createAudioSampleFormat( - "exceededFormat", - MimeTypes.AUDIO_AAC, - null, - Format.NO_VALUE, - Format.NO_VALUE, - 2, - 44100, - null, - null, - 0, - "eng"); - Format supportedFrFormat = - Format.createAudioSampleFormat("supportedFormat", MimeTypes.AUDIO_AAC, null, - Format.NO_VALUE, Format.NO_VALUE, 2, 44100, null, null, 0, "fra"); + public void selectTracksPreferTrackWithinCapabilitiesOverPreferredLanguage() throws Exception { + Format.Builder formatBuilder = AUDIO_FORMAT.buildUpon(); + Format exceededEnFormat = formatBuilder.setId("exceededFormat").setLanguage("eng").build(); + Format supportedFrFormat = formatBuilder.setId("supportedFormat").setLanguage("fra").build(); TrackGroupArray trackGroups = wrapFormats(exceededEnFormat, supportedFrFormat); Map mappedCapabilities = new HashMap<>(); @@ -618,29 +557,22 @@ public final class DefaultTrackSelectorTest { } /** - * Tests that track selector will prefer tracks that are within renderer's capabilities over - * track that have both language matching preferred audio given by {@link Parameters} and - * {@link C#SELECTION_FLAG_DEFAULT}, but exceed renderer's capabilities. + * Tests that track selector will prefer tracks that are within renderer's capabilities over track + * that have both language matching preferred audio given by {@link Parameters} and {@link + * C#SELECTION_FLAG_DEFAULT}, but exceed renderer's capabilities. */ @Test - public void testSelectTracksPreferTrackWithinCapabilitiesOverSelectionFlagAndPreferredLanguage() + public void selectTracksPreferTrackWithinCapabilitiesOverSelectionFlagAndPreferredLanguage() throws Exception { + Format.Builder formatBuilder = AUDIO_FORMAT.buildUpon(); Format exceededDefaultSelectionEnFormat = - Format.createAudioSampleFormat( - "exceededFormat", - MimeTypes.AUDIO_AAC, - null, - Format.NO_VALUE, - Format.NO_VALUE, - 2, - 44100, - null, - null, - C.SELECTION_FLAG_DEFAULT, - "eng"); + formatBuilder + .setId("exceededFormat") + .setSelectionFlags(C.SELECTION_FLAG_DEFAULT) + .setLanguage("eng") + .build(); Format supportedFrFormat = - Format.createAudioSampleFormat("supportedFormat", MimeTypes.AUDIO_AAC, null, - Format.NO_VALUE, Format.NO_VALUE, 2, 44100, null, null, 0, "fra"); + formatBuilder.setId("supportedFormat").setSelectionFlags(0).setLanguage("fra").build(); TrackGroupArray trackGroups = wrapFormats(exceededDefaultSelectionEnFormat, supportedFrFormat); Map mappedCapabilities = new HashMap<>(); @@ -664,24 +596,10 @@ public final class DefaultTrackSelectorTest { * are the same, and tracks are within renderer's capabilities. */ @Test - public void testSelectTracksWithinCapabilitiesSelectHigherNumChannel() - throws Exception { - Format higherChannelFormat = - Format.createAudioSampleFormat( - "audioFormat", - MimeTypes.AUDIO_AAC, - null, - Format.NO_VALUE, - Format.NO_VALUE, - 6, - 44100, - null, - null, - 0, - null); - Format lowerChannelFormat = - Format.createAudioSampleFormat("audioFormat", MimeTypes.AUDIO_AAC, null, Format.NO_VALUE, - Format.NO_VALUE, 2, 44100, null, null, 0, null); + public void selectTracksWithinCapabilitiesSelectHigherNumChannel() throws Exception { + Format.Builder formatBuilder = AUDIO_FORMAT.buildUpon(); + Format higherChannelFormat = formatBuilder.setChannelCount(6).build(); + Format lowerChannelFormat = formatBuilder.setChannelCount(2).build(); TrackGroupArray trackGroups = wrapFormats(higherChannelFormat, lowerChannelFormat); TrackSelectorResult result = @@ -698,14 +616,10 @@ public final class DefaultTrackSelectorTest { * are the same, and tracks are within renderer's capabilities. */ @Test - public void testSelectTracksWithinCapabilitiesSelectHigherSampleRate() - throws Exception { - Format higherSampleRateFormat = - Format.createAudioSampleFormat("audioFormat", MimeTypes.AUDIO_AAC, null, Format.NO_VALUE, - Format.NO_VALUE, 2, 44100, null, null, 0, null); - Format lowerSampleRateFormat = - Format.createAudioSampleFormat("audioFormat", MimeTypes.AUDIO_AAC, null, Format.NO_VALUE, - Format.NO_VALUE, 2, 22050, null, null, 0, null); + public void selectTracksWithinCapabilitiesSelectHigherSampleRate() throws Exception { + Format.Builder formatBuilder = AUDIO_FORMAT.buildUpon(); + Format higherSampleRateFormat = formatBuilder.setSampleRate(44100).build(); + Format lowerSampleRateFormat = formatBuilder.setSampleRate(22050).build(); TrackGroupArray trackGroups = wrapFormats(higherSampleRateFormat, lowerSampleRateFormat); TrackSelectorResult result = @@ -724,32 +638,9 @@ public final class DefaultTrackSelectorTest { @Test public void selectAudioTracks_withinCapabilities_andSameLanguage_selectsHigherBitrate() throws Exception { - Format lowerBitrateFormat = - Format.createAudioSampleFormat( - "audioFormat", - MimeTypes.AUDIO_AAC, - /* codecs= */ null, - /* bitrate= */ 15000, - /* maxInputSize= */ Format.NO_VALUE, - /* channelCount= */ 2, - /* sampleRate= */ 44100, - /* initializationData= */ null, - /* drmInitData= */ null, - /* selectionFlags= */ 0, - /* language= */ "hi"); - Format higherBitrateFormat = - Format.createAudioSampleFormat( - "audioFormat", - MimeTypes.AUDIO_AAC, - /* codecs= */ null, - /* bitrate= */ 30000, - /* maxInputSize= */ Format.NO_VALUE, - /* channelCount= */ 2, - /* sampleRate= */ 44100, - /* initializationData= */ null, - /* drmInitData= */ null, - /* selectionFlags= */ 0, - /* language= */ "hi"); + Format.Builder formatBuilder = AUDIO_FORMAT.buildUpon().setLanguage("en"); + Format lowerBitrateFormat = formatBuilder.setAverageBitrate(15000).build(); + Format higherBitrateFormat = formatBuilder.setAverageBitrate(30000).build(); TrackGroupArray trackGroups = wrapFormats(lowerBitrateFormat, higherBitrateFormat); TrackSelectorResult result = @@ -769,32 +660,9 @@ public final class DefaultTrackSelectorTest { @Test public void selectAudioTracks_withinCapabilities_andDifferentLanguage_selectsFirstTrack() throws Exception { - Format firstLanguageFormat = - Format.createAudioSampleFormat( - "audioFormat", - MimeTypes.AUDIO_AAC, - /* codecs= */ null, - /* bitrate= */ 15000, - /* maxInputSize= */ Format.NO_VALUE, - /* channelCount= */ 2, - /* sampleRate= */ 44100, - /* initializationData= */ null, - /* drmInitData= */ null, - /* selectionFlags= */ 0, - /* language= */ "hi"); - Format higherBitrateFormat = - Format.createAudioSampleFormat( - "audioFormat", - MimeTypes.AUDIO_AAC, - /* codecs= */ null, - /* bitrate= */ 30000, - /* maxInputSize= */ Format.NO_VALUE, - /* channelCount= */ 2, - /* sampleRate= */ 44100, - /* initializationData= */ null, - /* drmInitData= */ null, - /* selectionFlags= */ 0, - /* language= */ "te"); + Format.Builder formatBuilder = AUDIO_FORMAT.buildUpon(); + Format firstLanguageFormat = formatBuilder.setAverageBitrate(15000).setLanguage("hi").build(); + Format higherBitrateFormat = formatBuilder.setAverageBitrate(30000).setLanguage("te").build(); TrackGroupArray trackGroups = wrapFormats(firstLanguageFormat, higherBitrateFormat); TrackSelectorResult result = @@ -812,23 +680,12 @@ public final class DefaultTrackSelectorTest { * capabilities. */ @Test - public void testSelectTracksPreferHigherNumChannelBeforeSampleRate() throws Exception { + public void selectTracksPreferHigherNumChannelBeforeSampleRate() throws Exception { + Format.Builder formatBuilder = AUDIO_FORMAT.buildUpon(); Format higherChannelLowerSampleRateFormat = - Format.createAudioSampleFormat( - "audioFormat", - MimeTypes.AUDIO_AAC, - null, - Format.NO_VALUE, - Format.NO_VALUE, - 6, - 22050, - null, - null, - 0, - null); + formatBuilder.setChannelCount(6).setSampleRate(22050).build(); Format lowerChannelHigherSampleRateFormat = - Format.createAudioSampleFormat("audioFormat", MimeTypes.AUDIO_AAC, null, Format.NO_VALUE, - Format.NO_VALUE, 2, 44100, null, null, 0, null); + formatBuilder.setChannelCount(2).setSampleRate(44100).build(); TrackGroupArray trackGroups = wrapFormats(higherChannelLowerSampleRateFormat, lowerChannelHigherSampleRateFormat); @@ -843,18 +700,15 @@ public final class DefaultTrackSelectorTest { /** * Tests that track selector will prefer audio tracks with higher sample rate over tracks with - * higher bitrate when other factors are the same, and tracks are within renderer's - * capabilities. + * higher bitrate when other factors are the same, and tracks are within renderer's capabilities. */ @Test - public void testSelectTracksPreferHigherSampleRateBeforeBitrate() - throws Exception { + public void selectTracksPreferHigherSampleRateBeforeBitrate() throws Exception { + Format.Builder formatBuilder = AUDIO_FORMAT.buildUpon(); Format higherSampleRateLowerBitrateFormat = - Format.createAudioSampleFormat("audioFormat", MimeTypes.AUDIO_AAC, null, 15000, - Format.NO_VALUE, 2, 44100, null, null, 0, null); + formatBuilder.setAverageBitrate(15000).setSampleRate(44100).build(); Format lowerSampleRateHigherBitrateFormat = - Format.createAudioSampleFormat("audioFormat", MimeTypes.AUDIO_AAC, null, 30000, - Format.NO_VALUE, 2, 22050, null, null, 0, null); + formatBuilder.setAverageBitrate(30000).setSampleRate(22050).build(); TrackGroupArray trackGroups = wrapFormats(higherSampleRateLowerBitrateFormat, lowerSampleRateHigherBitrateFormat); @@ -872,24 +726,10 @@ public final class DefaultTrackSelectorTest { * are the same, and tracks exceed renderer's capabilities. */ @Test - public void testSelectTracksExceedingCapabilitiesSelectLowerNumChannel() - throws Exception { - Format higherChannelFormat = - Format.createAudioSampleFormat( - "audioFormat", - MimeTypes.AUDIO_AAC, - null, - Format.NO_VALUE, - Format.NO_VALUE, - 6, - 44100, - null, - null, - 0, - null); - Format lowerChannelFormat = - Format.createAudioSampleFormat("audioFormat", MimeTypes.AUDIO_AAC, null, Format.NO_VALUE, - Format.NO_VALUE, 2, 44100, null, null, 0, null); + public void selectTracksExceedingCapabilitiesSelectLowerNumChannel() throws Exception { + Format.Builder formatBuilder = AUDIO_FORMAT.buildUpon(); + Format higherChannelFormat = formatBuilder.setChannelCount(6).build(); + Format lowerChannelFormat = formatBuilder.setChannelCount(2).build(); TrackGroupArray trackGroups = wrapFormats(higherChannelFormat, lowerChannelFormat); TrackSelectorResult result = @@ -906,14 +746,10 @@ public final class DefaultTrackSelectorTest { * are the same, and tracks exceed renderer's capabilities. */ @Test - public void testSelectTracksExceedingCapabilitiesSelectLowerSampleRate() - throws Exception { - Format lowerSampleRateFormat = - Format.createAudioSampleFormat("audioFormat", MimeTypes.AUDIO_AAC, null, Format.NO_VALUE, - Format.NO_VALUE, 2, 22050, null, null, 0, null); - Format higherSampleRateFormat = - Format.createAudioSampleFormat("audioFormat", MimeTypes.AUDIO_AAC, null, Format.NO_VALUE, - Format.NO_VALUE, 2, 44100, null, null, 0, null); + public void selectTracksExceedingCapabilitiesSelectLowerSampleRate() throws Exception { + Format.Builder formatBuilder = AUDIO_FORMAT.buildUpon(); + Format lowerSampleRateFormat = formatBuilder.setSampleRate(22050).build(); + Format higherSampleRateFormat = formatBuilder.setSampleRate(44100).build(); TrackGroupArray trackGroups = wrapFormats(higherSampleRateFormat, lowerSampleRateFormat); TrackSelectorResult result = @@ -926,18 +762,14 @@ public final class DefaultTrackSelectorTest { } /** - * Tests that track selector will select audio tracks with lower bit-rate when other factors - * are the same, and tracks exceed renderer's capabilities. + * Tests that track selector will select audio tracks with lower bit-rate when other factors are + * the same, and tracks exceed renderer's capabilities. */ @Test - public void testSelectTracksExceedingCapabilitiesSelectLowerBitrate() - throws Exception { - Format lowerBitrateFormat = - Format.createAudioSampleFormat("audioFormat", MimeTypes.AUDIO_AAC, null, 15000, - Format.NO_VALUE, 2, 44100, null, null, 0, null); - Format higherBitrateFormat = - Format.createAudioSampleFormat("audioFormat", MimeTypes.AUDIO_AAC, null, 30000, - Format.NO_VALUE, 2, 44100, null, null, 0, null); + public void selectTracksExceedingCapabilitiesSelectLowerBitrate() throws Exception { + Format.Builder formatBuilder = AUDIO_FORMAT.buildUpon(); + Format lowerBitrateFormat = formatBuilder.setAverageBitrate(15000).build(); + Format higherBitrateFormat = formatBuilder.setAverageBitrate(30000).build(); TrackGroupArray trackGroups = wrapFormats(lowerBitrateFormat, higherBitrateFormat); TrackSelectorResult result = @@ -955,14 +787,13 @@ public final class DefaultTrackSelectorTest { * capabilities. */ @Test - public void testSelectTracksExceedingCapabilitiesPreferLowerNumChannelBeforeSampleRate() + public void selectTracksExceedingCapabilitiesPreferLowerNumChannelBeforeSampleRate() throws Exception { + Format.Builder formatBuilder = AUDIO_FORMAT.buildUpon(); Format lowerChannelHigherSampleRateFormat = - Format.createAudioSampleFormat("audioFormat", MimeTypes.AUDIO_AAC, null, Format.NO_VALUE, - Format.NO_VALUE, 2, 44100, null, null, 0, null); + formatBuilder.setChannelCount(2).setSampleRate(44100).build(); Format higherChannelLowerSampleRateFormat = - Format.createAudioSampleFormat("audioFormat", MimeTypes.AUDIO_AAC, null, Format.NO_VALUE, - Format.NO_VALUE, 6, 22050, null, null, 0, null); + formatBuilder.setChannelCount(6).setSampleRate(22050).build(); TrackGroupArray trackGroups = wrapFormats(higherChannelLowerSampleRateFormat, lowerChannelHigherSampleRateFormat); @@ -977,18 +808,16 @@ public final class DefaultTrackSelectorTest { /** * Tests that track selector will prefer audio tracks with lower sample rate over tracks with - * lower bitrate when other factors are the same, and tracks are within renderer's - * capabilities. + * lower bitrate when other factors are the same, and tracks are within renderer's capabilities. */ @Test - public void testSelectTracksExceedingCapabilitiesPreferLowerSampleRateBeforeBitrate() + public void selectTracksExceedingCapabilitiesPreferLowerSampleRateBeforeBitrate() throws Exception { + Format.Builder formatBuilder = AUDIO_FORMAT.buildUpon(); Format higherSampleRateLowerBitrateFormat = - Format.createAudioSampleFormat("audioFormat", MimeTypes.AUDIO_AAC, null, 15000, - Format.NO_VALUE, 2, 44100, null, null, 0, null); + formatBuilder.setAverageBitrate(15000).setSampleRate(44100).build(); Format lowerSampleRateHigherBitrateFormat = - Format.createAudioSampleFormat("audioFormat", MimeTypes.AUDIO_AAC, null, 30000, - Format.NO_VALUE, 2, 22050, null, null, 0, null); + formatBuilder.setAverageBitrate(30000).setSampleRate(22050).build(); TrackGroupArray trackGroups = wrapFormats(higherSampleRateLowerBitrateFormat, lowerSampleRateHigherBitrateFormat); @@ -1003,12 +832,13 @@ public final class DefaultTrackSelectorTest { /** Tests text track selection flags. */ @Test - public void testTextTrackSelectionFlags() throws ExoPlaybackException { - Format forcedOnly = buildTextFormat("forcedOnly", "eng", C.SELECTION_FLAG_FORCED); + public void textTrackSelectionFlags() throws ExoPlaybackException { + Format.Builder formatBuilder = TEXT_FORMAT.buildUpon().setLanguage("eng"); + Format forcedOnly = formatBuilder.setSelectionFlags(C.SELECTION_FLAG_FORCED).build(); Format forcedDefault = - buildTextFormat("forcedDefault", "eng", C.SELECTION_FLAG_FORCED | C.SELECTION_FLAG_DEFAULT); - Format defaultOnly = buildTextFormat("defaultOnly", "eng", C.SELECTION_FLAG_DEFAULT); - Format noFlag = buildTextFormat("noFlag", "eng"); + formatBuilder.setSelectionFlags(C.SELECTION_FLAG_FORCED | C.SELECTION_FLAG_DEFAULT).build(); + Format defaultOnly = formatBuilder.setSelectionFlags(C.SELECTION_FLAG_DEFAULT).build(); + Format noFlag = formatBuilder.setSelectionFlags(0).build(); RendererCapabilities[] textRendererCapabilities = new RendererCapabilities[] {ALL_TEXT_FORMAT_SUPPORTED_RENDERER_CAPABILITIES}; @@ -1068,26 +898,15 @@ public final class DefaultTrackSelectorTest { * audio language when no text language preferences match. */ @Test - public void testSelectingForcedTextTrackMatchesAudioLanguage() throws ExoPlaybackException { - Format forcedEnglish = - buildTextFormat(/* id= */ "forcedEnglish", /* language= */ "eng", C.SELECTION_FLAG_FORCED); - Format forcedGerman = - buildTextFormat(/* id= */ "forcedGerman", /* language= */ "deu", C.SELECTION_FLAG_FORCED); - Format forcedNoLanguage = - buildTextFormat( - /* id= */ "forcedNoLanguage", - /* language= */ C.LANGUAGE_UNDETERMINED, - C.SELECTION_FLAG_FORCED); - Format audio = buildAudioFormat(/* id= */ "audio"); - Format germanAudio = - buildAudioFormat( - /* id= */ "germanAudio", - MimeTypes.AUDIO_AAC, - /* bitrate= */ Format.NO_VALUE, - "deu", - /* selectionFlags= */ 0, - /* channelCount= */ Format.NO_VALUE, - /* sampleRate= */ Format.NO_VALUE); + public void selectingForcedTextTrackMatchesAudioLanguage() throws ExoPlaybackException { + Format.Builder formatBuilder = + TEXT_FORMAT.buildUpon().setSelectionFlags(C.SELECTION_FLAG_FORCED); + Format forcedEnglish = formatBuilder.setLanguage("eng").build(); + Format forcedGerman = formatBuilder.setLanguage("deu").build(); + Format forcedNoLanguage = formatBuilder.setLanguage(C.LANGUAGE_UNDETERMINED).build(); + + Format noLanguageAudio = AUDIO_FORMAT.buildUpon().setLanguage(null).build(); + Format germanAudio = AUDIO_FORMAT.buildUpon().setLanguage("deu").build(); RendererCapabilities[] rendererCapabilities = new RendererCapabilities[] { @@ -1097,14 +916,14 @@ public final class DefaultTrackSelectorTest { // Neither the audio nor the forced text track define a language. We select them both under the // assumption that they have matching language. - TrackGroupArray trackGroups = wrapFormats(audio, forcedNoLanguage); + TrackGroupArray trackGroups = wrapFormats(noLanguageAudio, forcedNoLanguage); TrackSelectorResult result = trackSelector.selectTracks(rendererCapabilities, trackGroups, periodId, TIMELINE); assertFixedSelection(result.selections.get(1), trackGroups, forcedNoLanguage); // No forced text track should be selected because none of the forced text tracks' languages // matches the selected audio language. - trackGroups = wrapFormats(audio, forcedEnglish, forcedGerman); + trackGroups = wrapFormats(noLanguageAudio, forcedEnglish, forcedGerman); result = trackSelector.selectTracks(rendererCapabilities, trackGroups, periodId, TIMELINE); assertNoSelection(result.selections.get(1)); @@ -1121,15 +940,16 @@ public final class DefaultTrackSelectorTest { /** * Tests that the default track selector will select a text track with undetermined language if no - * text track with the preferred language is available but - * {@link Parameters#selectUndeterminedTextLanguage} is true. + * text track with the preferred language is available but {@link + * Parameters#selectUndeterminedTextLanguage} is true. */ @Test - public void testSelectUndeterminedTextLanguageAsFallback() throws ExoPlaybackException{ - Format spanish = buildTextFormat("spanish", "spa"); - Format german = buildTextFormat("german", "de"); - Format undeterminedUnd = buildTextFormat("undeterminedUnd", "und"); - Format undeterminedNull = buildTextFormat("undeterminedNull", null); + public void selectUndeterminedTextLanguageAsFallback() throws ExoPlaybackException { + Format.Builder formatBuilder = TEXT_FORMAT.buildUpon(); + Format spanish = formatBuilder.setLanguage("spa").build(); + Format german = formatBuilder.setLanguage("de").build(); + Format undeterminedUnd = formatBuilder.setLanguage(C.LANGUAGE_UNDETERMINED).build(); + Format undeterminedNull = formatBuilder.setLanguage(null).build(); RendererCapabilities[] textRendererCapabilites = new RendererCapabilities[] {ALL_TEXT_FORMAT_SUPPORTED_RENDERER_CAPABILITIES}; @@ -1169,9 +989,10 @@ public final class DefaultTrackSelectorTest { /** Tests audio track selection when there are multiple audio renderers. */ @Test - public void testSelectPreferredTextTrackMultipleRenderers() throws Exception { - Format english = buildTextFormat("en", "en"); - Format german = buildTextFormat("de", "de"); + public void selectPreferredTextTrackMultipleRenderers() throws Exception { + Format.Builder formatBuilder = TEXT_FORMAT.buildUpon(); + Format english = formatBuilder.setId("en").setLanguage("en").build(); + Format german = formatBuilder.setId("de").setLanguage("de").build(); // First renderer handles english. Map firstRendererMappedCapabilities = new HashMap<>(); @@ -1215,11 +1036,13 @@ public final class DefaultTrackSelectorTest { * Parameters#forceLowestBitrate} is set. */ @Test - public void testSelectTracksWithinCapabilitiesAndForceLowestBitrateSelectLowerBitrate() + public void selectTracksWithinCapabilitiesAndForceLowestBitrateSelectLowerBitrate() throws Exception { - Format unsupportedLowBitrateFormat = buildAudioFormatWithBitrate("unsupportedLowBitrate", 5000); - Format lowerBitrateFormat = buildAudioFormatWithBitrate("lowBitrate", 15000); - Format higherBitrateFormat = buildAudioFormatWithBitrate("highBitrate", 30000); + Format.Builder formatBuilder = AUDIO_FORMAT.buildUpon(); + Format unsupportedLowBitrateFormat = + formatBuilder.setId("unsupported").setAverageBitrate(5000).build(); + Format lowerBitrateFormat = formatBuilder.setId("lower").setAverageBitrate(15000).build(); + Format higherBitrateFormat = formatBuilder.setId("higher").setAverageBitrate(30000).build(); TrackGroupArray trackGroups = wrapFormats(unsupportedLowBitrateFormat, lowerBitrateFormat, higherBitrateFormat); @@ -1245,11 +1068,12 @@ public final class DefaultTrackSelectorTest { * Parameters#forceHighestSupportedBitrate} is set. */ @Test - public void testSelectTracksWithinCapabilitiesAndForceHighestBitrateSelectHigherBitrate() + public void selectTracksWithinCapabilitiesAndForceHighestBitrateSelectHigherBitrate() throws Exception { - Format lowerBitrateFormat = buildAudioFormatWithBitrate("lowerBitrateFormat", 5000); - Format higherBitrateFormat = buildAudioFormatWithBitrate("higherBitrateFormat", 15000); - Format exceedsBitrateFormat = buildAudioFormatWithBitrate("exceedsBitrateFormat", 30000); + Format.Builder formatBuilder = AUDIO_FORMAT.buildUpon(); + Format lowerBitrateFormat = formatBuilder.setId("5000").setAverageBitrate(5000).build(); + Format higherBitrateFormat = formatBuilder.setId("15000").setAverageBitrate(15000).build(); + Format exceedsBitrateFormat = formatBuilder.setId("30000").setAverageBitrate(30000).build(); TrackGroupArray trackGroups = wrapFormats(lowerBitrateFormat, higherBitrateFormat, exceedsBitrateFormat); @@ -1272,8 +1096,10 @@ public final class DefaultTrackSelectorTest { } @Test - public void testSelectTracksWithMultipleAudioTracks() throws Exception { - TrackGroupArray trackGroups = singleTrackGroup(buildAudioFormat("0"), buildAudioFormat("1")); + public void selectTracksWithMultipleAudioTracks() throws Exception { + Format.Builder formatBuilder = AUDIO_FORMAT.buildUpon(); + TrackGroupArray trackGroups = + singleTrackGroup(formatBuilder.setId("0").build(), formatBuilder.setId("1").build()); TrackSelectorResult result = trackSelector.selectTracks( new RendererCapabilities[] {AUDIO_CAPABILITIES}, trackGroups, periodId, TIMELINE); @@ -1283,11 +1109,41 @@ public final class DefaultTrackSelectorTest { } @Test - public void testSelectTracksWithMultipleAudioTracksWithMixedSampleRates() throws Exception { - Format highSampleRateAudioFormat = - buildAudioFormatWithSampleRate("44100", /* sampleRate= */ 44100); - Format lowSampleRateAudioFormat = - buildAudioFormatWithSampleRate("22050", /* sampleRate= */ 22050); + public void selectTracks_multipleAudioTracks_selectsAllTracksInBestConfigurationOnly() + throws Exception { + TrackGroupArray trackGroups = + singleTrackGroup( + buildAudioFormatWithConfiguration( + /* id= */ "0", /* channelCount= */ 6, MimeTypes.AUDIO_AAC, /* sampleRate= */ 44100), + buildAudioFormatWithConfiguration( + /* id= */ "1", /* channelCount= */ 2, MimeTypes.AUDIO_AAC, /* sampleRate= */ 44100), + buildAudioFormatWithConfiguration( + /* id= */ "2", /* channelCount= */ 6, MimeTypes.AUDIO_AC3, /* sampleRate= */ 44100), + buildAudioFormatWithConfiguration( + /* id= */ "3", /* channelCount= */ 6, MimeTypes.AUDIO_AAC, /* sampleRate= */ 22050), + buildAudioFormatWithConfiguration( + /* id= */ "4", /* channelCount= */ 6, MimeTypes.AUDIO_AAC, /* sampleRate= */ 22050), + buildAudioFormatWithConfiguration( + /* id= */ "5", /* channelCount= */ 6, MimeTypes.AUDIO_AAC, /* sampleRate= */ 22050), + buildAudioFormatWithConfiguration( + /* id= */ "6", + /* channelCount= */ 6, + MimeTypes.AUDIO_AAC, + /* sampleRate= */ 44100)); + + TrackSelectorResult result = + trackSelector.selectTracks( + new RendererCapabilities[] {AUDIO_CAPABILITIES}, trackGroups, periodId, TIMELINE); + + assertThat(result.length).isEqualTo(1); + assertAdaptiveSelection(result.selections.get(0), trackGroups.get(0), 0, 6); + } + + @Test + public void selectTracksWithMultipleAudioTracksWithMixedSampleRates() throws Exception { + Format.Builder formatBuilder = AUDIO_FORMAT.buildUpon(); + Format highSampleRateAudioFormat = formatBuilder.setSampleRate(44100).build(); + Format lowSampleRateAudioFormat = formatBuilder.setSampleRate(22050).build(); // Should not adapt between mixed sample rates by default, so we expect a fixed selection // containing the higher sample rate stream. @@ -1318,9 +1174,10 @@ public final class DefaultTrackSelectorTest { } @Test - public void testSelectTracksWithMultipleAudioTracksWithMixedMimeTypes() throws Exception { - Format aacAudioFormat = buildAudioFormatWithMimeType("aac", MimeTypes.AUDIO_AAC); - Format opusAudioFormat = buildAudioFormatWithMimeType("opus", MimeTypes.AUDIO_OPUS); + public void selectTracksWithMultipleAudioTracksWithMixedMimeTypes() throws Exception { + Format.Builder formatBuilder = AUDIO_FORMAT.buildUpon(); + Format aacAudioFormat = formatBuilder.setSampleMimeType(MimeTypes.AUDIO_AAC).build(); + Format opusAudioFormat = formatBuilder.setSampleMimeType(MimeTypes.AUDIO_OPUS).build(); // Should not adapt between mixed mime types by default, so we expect a fixed selection // containing the first stream. @@ -1350,11 +1207,10 @@ public final class DefaultTrackSelectorTest { } @Test - public void testSelectTracksWithMultipleAudioTracksWithMixedChannelCounts() throws Exception { - Format stereoAudioFormat = - buildAudioFormatWithChannelCount("2-channels", /* channelCount= */ 2); - Format surroundAudioFormat = - buildAudioFormatWithChannelCount("5-channels", /* channelCount= */ 5); + public void selectTracksWithMultipleAudioTracksWithMixedChannelCounts() throws Exception { + Format.Builder formatBuilder = AUDIO_FORMAT.buildUpon(); + Format stereoAudioFormat = formatBuilder.setChannelCount(2).build(); + Format surroundAudioFormat = formatBuilder.setChannelCount(5).build(); // Should not adapt between different channel counts, so we expect a fixed selection containing // the track with more channels. @@ -1414,17 +1270,21 @@ public final class DefaultTrackSelectorTest { } @Test - public void testSelectTracksWithMultipleAudioTracksOverrideReturnsAdaptiveTrackSelection() + public void selectTracksWithMultipleAudioTracksOverrideReturnsAdaptiveTrackSelection() throws Exception { + Format.Builder formatBuilder = AUDIO_FORMAT.buildUpon(); TrackGroupArray trackGroups = - singleTrackGroup(buildAudioFormat("0"), buildAudioFormat("1"), buildAudioFormat("2")); + singleTrackGroup( + formatBuilder.setId("0").build(), + formatBuilder.setId("1").build(), + formatBuilder.setId("2").build()); trackSelector.setParameters( trackSelector .buildUponParameters() .setSelectionOverride( /* rendererIndex= */ 0, trackGroups, - new SelectionOverride(/* groupIndex= */ 0, /* tracks= */ 1, 2))); + new SelectionOverride(/* groupIndex= */ 0, /* tracks=... */ 1, 2))); TrackSelectorResult result = trackSelector.selectTracks( new RendererCapabilities[] {AUDIO_CAPABILITIES}, trackGroups, periodId, TIMELINE); @@ -1435,9 +1295,10 @@ public final class DefaultTrackSelectorTest { /** Tests audio track selection when there are multiple audio renderers. */ @Test - public void testSelectPreferredAudioTrackMultipleRenderers() throws Exception { - Format english = buildAudioFormatWithLanguage("en", "en"); - Format german = buildAudioFormatWithLanguage("de", "de"); + public void selectPreferredAudioTrackMultipleRenderers() throws Exception { + Format.Builder formatBuilder = AUDIO_FORMAT.buildUpon(); + Format english = formatBuilder.setId("en").setLanguage("en").build(); + Format german = formatBuilder.setId("de").setLanguage("de").build(); // First renderer handles english. Map firstRendererMappedCapabilities = new HashMap<>(); @@ -1477,8 +1338,10 @@ public final class DefaultTrackSelectorTest { } @Test - public void testSelectTracksWithMultipleVideoTracks() throws Exception { - TrackGroupArray trackGroups = singleTrackGroup(buildVideoFormat("0"), buildVideoFormat("1")); + public void selectTracksWithMultipleVideoTracks() throws Exception { + Format.Builder formatBuilder = VIDEO_FORMAT.buildUpon(); + TrackGroupArray trackGroups = + singleTrackGroup(formatBuilder.setId("0").build(), formatBuilder.setId("1").build()); TrackSelectorResult result = trackSelector.selectTracks( new RendererCapabilities[] {VIDEO_CAPABILITIES}, trackGroups, periodId, TIMELINE); @@ -1488,13 +1351,14 @@ public final class DefaultTrackSelectorTest { } @Test - public void testSelectTracksWithMultipleVideoTracksWithNonSeamlessAdaptiveness() - throws Exception { + public void selectTracksWithMultipleVideoTracksWithNonSeamlessAdaptiveness() throws Exception { FakeRendererCapabilities nonSeamlessVideoCapabilities = new FakeRendererCapabilities(C.TRACK_TYPE_VIDEO, FORMAT_HANDLED | ADAPTIVE_NOT_SEAMLESS); // Should do non-seamless adaptiveness by default, so expect an adaptive selection. - TrackGroupArray trackGroups = singleTrackGroup(buildVideoFormat("0"), buildVideoFormat("1")); + Format.Builder formatBuilder = VIDEO_FORMAT.buildUpon(); + TrackGroupArray trackGroups = + singleTrackGroup(formatBuilder.setId("0").build(), formatBuilder.setId("1").build()); trackSelector.setParameters( defaultParameters.buildUpon().setAllowVideoNonSeamlessAdaptiveness(true)); TrackSelectorResult result = @@ -1520,9 +1384,10 @@ public final class DefaultTrackSelectorTest { } @Test - public void testSelectTracksWithMultipleVideoTracksWithMixedMimeTypes() throws Exception { - Format h264VideoFormat = buildVideoFormatWithMimeType("h264", MimeTypes.VIDEO_H264); - Format h265VideoFormat = buildVideoFormatWithMimeType("h265", MimeTypes.VIDEO_H265); + public void selectTracksWithMultipleVideoTracksWithMixedMimeTypes() throws Exception { + Format.Builder formatBuilder = VIDEO_FORMAT.buildUpon(); + Format h264VideoFormat = formatBuilder.setSampleMimeType(MimeTypes.VIDEO_H264).build(); + Format h265VideoFormat = formatBuilder.setSampleMimeType(MimeTypes.VIDEO_H265).build(); // Should not adapt between mixed mime types by default, so we expect a fixed selection // containing the first stream. @@ -1552,17 +1417,21 @@ public final class DefaultTrackSelectorTest { } @Test - public void testSelectTracksWithMultipleVideoTracksOverrideReturnsAdaptiveTrackSelection() + public void selectTracksWithMultipleVideoTracksOverrideReturnsAdaptiveTrackSelection() throws Exception { + Format.Builder formatBuilder = VIDEO_FORMAT.buildUpon(); TrackGroupArray trackGroups = - singleTrackGroup(buildVideoFormat("0"), buildVideoFormat("1"), buildVideoFormat("2")); + singleTrackGroup( + formatBuilder.setId("0").build(), + formatBuilder.setId("1").build(), + formatBuilder.setId("2").build()); trackSelector.setParameters( trackSelector .buildUponParameters() .setSelectionOverride( /* rendererIndex= */ 0, trackGroups, - new SelectionOverride(/* groupIndex= */ 0, /* tracks= */ 1, 2))); + new SelectionOverride(/* groupIndex= */ 0, /* tracks=... */ 1, 2))); TrackSelectorResult result = trackSelector.selectTracks( new RendererCapabilities[] {VIDEO_CAPABILITIES}, trackGroups, periodId, TIMELINE); @@ -1631,132 +1500,14 @@ public final class DefaultTrackSelectorTest { return new TrackGroupArray(trackGroups); } - private static Format buildVideoFormatWithMimeType(String id, String mimeType) { - return Format.createVideoSampleFormat( - id, - mimeType, - null, - Format.NO_VALUE, - Format.NO_VALUE, - 1024, - 768, - Format.NO_VALUE, - null, - null); - } - - private static Format buildVideoFormat(String id) { - return buildVideoFormatWithMimeType(id, MimeTypes.VIDEO_H264); - } - - private static Format buildAudioFormatWithLanguage(String id, String language) { - return buildAudioFormatWithLanguageAndFlags(id, language, /* selectionFlags= */ 0); - } - - private static Format buildAudioFormatWithLanguageAndFlags( - String id, String language, int selectionFlags) { - return buildAudioFormat( - id, - MimeTypes.AUDIO_AAC, - /* bitrate= */ Format.NO_VALUE, - language, - selectionFlags, - /* channelCount= */ 2, - /* sampleRate= */ 44100); - } - - private static Format buildAudioFormatWithBitrate(String id, int bitrate) { - return buildAudioFormat( - id, - MimeTypes.AUDIO_AAC, - bitrate, - /* language= */ null, - /* selectionFlags= */ 0, - /* channelCount= */ 2, - /* sampleRate= */ 44100); - } - - private static Format buildAudioFormatWithSampleRate(String id, int sampleRate) { - return buildAudioFormat( - id, - MimeTypes.AUDIO_AAC, - /* bitrate= */ Format.NO_VALUE, - /* language= */ null, - /* selectionFlags= */ 0, - /* channelCount= */ 2, - sampleRate); - } - - private static Format buildAudioFormatWithChannelCount(String id, int channelCount) { - return buildAudioFormat( - id, - MimeTypes.AUDIO_AAC, - /* bitrate= */ Format.NO_VALUE, - /* language= */ null, - /* selectionFlags= */ 0, - channelCount, - /* sampleRate= */ 44100); - } - - private static Format buildAudioFormatWithMimeType(String id, String mimeType) { - return buildAudioFormat( - id, - mimeType, - /* bitrate= */ Format.NO_VALUE, - /* language= */ null, - /* selectionFlags= */ 0, - /* channelCount= */ 2, - /* sampleRate= */ 44100); - } - - private static Format buildAudioFormat(String id) { - return buildAudioFormat( - id, - MimeTypes.AUDIO_AAC, - /* bitrate= */ Format.NO_VALUE, - /* language= */ null, - /* selectionFlags= */ 0, - /* channelCount= */ 2, - /* sampleRate= */ 44100); - } - - private static Format buildAudioFormat( - String id, - String mimeType, - int bitrate, - String language, - int selectionFlags, - int channelCount, - int sampleRate) { - return Format.createAudioSampleFormat( - id, - mimeType, - /* codecs= */ null, - bitrate, - /* maxInputSize= */ Format.NO_VALUE, - channelCount, - sampleRate, - /* initializationData= */ null, - /* drmInitData= */ null, - selectionFlags, - language); - } - - 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, - /* roleFlags= */ 0, - language); + private static Format buildAudioFormatWithConfiguration( + String id, int channelCount, String mimeType, int sampleRate) { + return new Format.Builder() + .setId(id) + .setSampleMimeType(mimeType) + .setChannelCount(channelCount) + .setSampleRate(sampleRate) + .build(); } /** @@ -1767,7 +1518,7 @@ public final class DefaultTrackSelectorTest { private static final class FakeRendererCapabilities implements RendererCapabilities { private final int trackType; - private final int supportValue; + @Capabilities private final int supportValue; /** * Returns {@link FakeRendererCapabilities} that advertises adaptive support for all @@ -1777,35 +1528,45 @@ public final class DefaultTrackSelectorTest { * support for. */ FakeRendererCapabilities(int trackType) { - this(trackType, FORMAT_HANDLED | ADAPTIVE_SEAMLESS); + this( + trackType, + RendererCapabilities.create(FORMAT_HANDLED, ADAPTIVE_SEAMLESS, TUNNELING_NOT_SUPPORTED)); } /** - * Returns {@link FakeRendererCapabilities} that advertises support level using given value - * for all tracks of the given type. + * Returns {@link FakeRendererCapabilities} that advertises support level using given value for + * all tracks of the given type. * * @param trackType the track type of all formats that this renderer capabilities advertises - * support for. - * @param supportValue the support level value that will be returned for formats with - * the given type. + * support for. + * @param supportValue the {@link Capabilities} that will be returned for formats with the given + * type. */ - FakeRendererCapabilities(int trackType, int supportValue) { + FakeRendererCapabilities(int trackType, @Capabilities int supportValue) { this.trackType = trackType; this.supportValue = supportValue; } + @Override + public String getName() { + return "FakeRenderer(" + Util.getTrackTypeString(trackType) + ")"; + } + @Override public int getTrackType() { return trackType; } @Override + @Capabilities public int supportsFormat(Format format) { return MimeTypes.getTrackType(format.sampleMimeType) == trackType - ? (supportValue) : FORMAT_UNSUPPORTED_TYPE; + ? supportValue + : RendererCapabilities.create(FORMAT_UNSUPPORTED_TYPE); } @Override + @AdaptiveSupport public int supportsMixedMimeTypeAdaptation() { return ADAPTIVE_SEAMLESS; } @@ -1835,19 +1596,26 @@ public final class DefaultTrackSelectorTest { this.formatToCapability = new HashMap<>(formatToCapability); } + @Override + public String getName() { + return "FakeRenderer(" + Util.getTrackTypeString(trackType) + ")"; + } + @Override public int getTrackType() { return trackType; } @Override + @Capabilities public int supportsFormat(Format format) { return format.id != null && formatToCapability.containsKey(format.id) ? formatToCapability.get(format.id) - : FORMAT_UNSUPPORTED_TYPE; + : RendererCapabilities.create(FORMAT_UNSUPPORTED_TYPE); } @Override + @AdaptiveSupport public int supportsMixedMimeTypeAdaptation() { return ADAPTIVE_SEAMLESS; } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/trackselection/MappingTrackSelectorTest.java b/library/core/src/test/java/com/google/android/exoplayer2/trackselection/MappingTrackSelectorTest.java index efb828fc57..5d5508f3cd 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/trackselection/MappingTrackSelectorTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/trackselection/MappingTrackSelectorTest.java @@ -23,6 +23,8 @@ import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.RendererCapabilities; +import com.google.android.exoplayer2.RendererCapabilities.AdaptiveSupport; +import com.google.android.exoplayer2.RendererCapabilities.Capabilities; import com.google.android.exoplayer2.RendererConfiguration; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; @@ -30,6 +32,7 @@ import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.testutil.FakeTimeline; import com.google.android.exoplayer2.util.MimeTypes; +import com.google.android.exoplayer2.util.Util; import org.junit.BeforeClass; import org.junit.Test; import org.junit.runner.RunWith; @@ -42,17 +45,13 @@ public final class MappingTrackSelectorTest { new FakeRendererCapabilities(C.TRACK_TYPE_VIDEO); private static final RendererCapabilities AUDIO_CAPABILITIES = new FakeRendererCapabilities(C.TRACK_TYPE_AUDIO); - private static final RendererCapabilities[] RENDERER_CAPABILITIES = new RendererCapabilities[] { - VIDEO_CAPABILITIES, AUDIO_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 TrackGroupArray TRACK_GROUPS = new TrackGroupArray( - VIDEO_TRACK_GROUP, AUDIO_TRACK_GROUP); + private static final RendererCapabilities METADATA_CAPABILITIES = + new FakeRendererCapabilities(C.TRACK_TYPE_METADATA); + + private static final TrackGroup VIDEO_TRACK_GROUP = buildTrackGroup(MimeTypes.VIDEO_H264); + private static final TrackGroup AUDIO_TRACK_GROUP = buildTrackGroup(MimeTypes.AUDIO_AAC); + private static final TrackGroup METADATA_TRACK_GROUP = buildTrackGroup(MimeTypes.APPLICATION_ID3); + private static final Timeline TIMELINE = new FakeTimeline(/* windowCount= */ 1); private static MediaPeriodId periodId; @@ -62,43 +61,72 @@ public final class MappingTrackSelectorTest { periodId = new MediaPeriodId(TIMELINE.getUidOfPeriod(/* periodIndex= */ 0)); } - /** - * Tests that the video and audio track groups are mapped onto the correct renderers. - */ @Test - public void testMapping() throws ExoPlaybackException { + public void selectTracks_audioAndVideo_sameOrderAsRenderers_mappedToCorrectRenderer() + throws ExoPlaybackException { FakeMappingTrackSelector trackSelector = new FakeMappingTrackSelector(); - trackSelector.selectTracks(RENDERER_CAPABILITIES, TRACK_GROUPS, periodId, TIMELINE); - trackSelector.assertMappedTrackGroups(0, VIDEO_TRACK_GROUP); - trackSelector.assertMappedTrackGroups(1, AUDIO_TRACK_GROUP); + RendererCapabilities[] rendererCapabilities = + new RendererCapabilities[] {VIDEO_CAPABILITIES, AUDIO_CAPABILITIES}; + TrackGroupArray trackGroups = new TrackGroupArray(VIDEO_TRACK_GROUP, AUDIO_TRACK_GROUP); + + trackSelector.selectTracks(rendererCapabilities, trackGroups, periodId, TIMELINE); + + trackSelector.assertMappedTrackGroups(/* rendererIndex= */ 0, VIDEO_TRACK_GROUP); + trackSelector.assertMappedTrackGroups(/* rendererIndex= */ 1, AUDIO_TRACK_GROUP); } - /** - * Tests that the video and audio track groups are mapped onto the correct renderers when the - * renderer ordering is reversed. - */ @Test - public void testMappingReverseOrder() throws ExoPlaybackException { + public void selectTracks_audioAndVideo_reverseOrderToRenderers_mappedToCorrectRenderer() + throws ExoPlaybackException { FakeMappingTrackSelector trackSelector = new FakeMappingTrackSelector(); - RendererCapabilities[] reverseOrderRendererCapabilities = new RendererCapabilities[] { - AUDIO_CAPABILITIES, VIDEO_CAPABILITIES}; - trackSelector.selectTracks(reverseOrderRendererCapabilities, TRACK_GROUPS, periodId, TIMELINE); - trackSelector.assertMappedTrackGroups(0, AUDIO_TRACK_GROUP); - trackSelector.assertMappedTrackGroups(1, VIDEO_TRACK_GROUP); + TrackGroupArray trackGroups = new TrackGroupArray(VIDEO_TRACK_GROUP, AUDIO_TRACK_GROUP); + RendererCapabilities[] reverseOrderRendererCapabilities = + new RendererCapabilities[] {AUDIO_CAPABILITIES, VIDEO_CAPABILITIES}; + + trackSelector.selectTracks(reverseOrderRendererCapabilities, trackGroups, periodId, TIMELINE); + + trackSelector.assertMappedTrackGroups(/* rendererIndex= */ 0, AUDIO_TRACK_GROUP); + trackSelector.assertMappedTrackGroups(/* rendererIndex= */ 1, VIDEO_TRACK_GROUP); } - /** - * Tests video and audio track groups are mapped onto the correct renderers when there are - * multiple track groups of the same type. - */ @Test - public void testMappingMulti() throws ExoPlaybackException { + public void selectTracks_multipleVideoAndAudioTracks_mappedToSameRenderer() + throws ExoPlaybackException { FakeMappingTrackSelector trackSelector = new FakeMappingTrackSelector(); - TrackGroupArray multiTrackGroups = new TrackGroupArray(VIDEO_TRACK_GROUP, AUDIO_TRACK_GROUP, - VIDEO_TRACK_GROUP); - trackSelector.selectTracks(RENDERER_CAPABILITIES, multiTrackGroups, periodId, TIMELINE); + TrackGroupArray trackGroups = + new TrackGroupArray( + VIDEO_TRACK_GROUP, AUDIO_TRACK_GROUP, AUDIO_TRACK_GROUP, VIDEO_TRACK_GROUP); + RendererCapabilities[] rendererCapabilities = + new RendererCapabilities[] { + VIDEO_CAPABILITIES, AUDIO_CAPABILITIES, VIDEO_CAPABILITIES, AUDIO_CAPABILITIES + }; + + trackSelector.selectTracks(rendererCapabilities, trackGroups, periodId, TIMELINE); + trackSelector.assertMappedTrackGroups(0, VIDEO_TRACK_GROUP, VIDEO_TRACK_GROUP); - trackSelector.assertMappedTrackGroups(1, AUDIO_TRACK_GROUP); + trackSelector.assertMappedTrackGroups(1, AUDIO_TRACK_GROUP, AUDIO_TRACK_GROUP); + } + + @Test + public void selectTracks_multipleMetadataTracks_mappedToDifferentRenderers() + throws ExoPlaybackException { + FakeMappingTrackSelector trackSelector = new FakeMappingTrackSelector(); + TrackGroupArray trackGroups = + new TrackGroupArray(VIDEO_TRACK_GROUP, METADATA_TRACK_GROUP, METADATA_TRACK_GROUP); + RendererCapabilities[] rendererCapabilities = + new RendererCapabilities[] { + VIDEO_CAPABILITIES, METADATA_CAPABILITIES, METADATA_CAPABILITIES + }; + + trackSelector.selectTracks(rendererCapabilities, trackGroups, periodId, TIMELINE); + + trackSelector.assertMappedTrackGroups(0, VIDEO_TRACK_GROUP); + trackSelector.assertMappedTrackGroups(1, METADATA_TRACK_GROUP); + trackSelector.assertMappedTrackGroups(2, METADATA_TRACK_GROUP); + } + + private static TrackGroup buildTrackGroup(String sampleMimeType) { + return new TrackGroup(new Format.Builder().setSampleMimeType(sampleMimeType).build()); } /** @@ -112,8 +140,8 @@ public final class MappingTrackSelectorTest { @Override protected Pair selectTracks( MappedTrackInfo mappedTrackInfo, - int[][][] rendererFormatSupports, - int[] rendererMixedMimeTypeAdaptationSupports) + @Capabilities int[][][] rendererFormatSupports, + @AdaptiveSupport int[] rendererMixedMimeTypeAdaptationSupports) throws ExoPlaybackException { int rendererCount = mappedTrackInfo.getRendererCount(); lastMappedTrackInfo = mappedTrackInfo; @@ -142,22 +170,28 @@ public final class MappingTrackSelectorTest { this.trackType = trackType; } + @Override + public String getName() { + return "FakeRenderer(" + Util.getTrackTypeString(trackType) + ")"; + } + @Override public int getTrackType() { return trackType; } @Override + @Capabilities public int supportsFormat(Format format) throws ExoPlaybackException { return MimeTypes.getTrackType(format.sampleMimeType) == trackType - ? (FORMAT_HANDLED | ADAPTIVE_SEAMLESS) : FORMAT_UNSUPPORTED_TYPE; + ? RendererCapabilities.create(FORMAT_HANDLED, ADAPTIVE_SEAMLESS, TUNNELING_NOT_SUPPORTED) + : RendererCapabilities.create(FORMAT_UNSUPPORTED_TYPE); } @Override + @AdaptiveSupport public int supportsMixedMimeTypeAdaptation() throws ExoPlaybackException { return ADAPTIVE_SEAMLESS; } - } - } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/upstream/AssetDataSourceTest.java b/library/core/src/test/java/com/google/android/exoplayer2/upstream/AssetDataSourceTest.java index 7e25eacfe2..1f4790b8c5 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/upstream/AssetDataSourceTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/upstream/AssetDataSourceTest.java @@ -26,10 +26,10 @@ import org.junit.runner.RunWith; @RunWith(AndroidJUnit4.class) public final class AssetDataSourceTest { - private static final String DATA_PATH = "binary/1024_incrementing_bytes.mp3"; + private static final String DATA_PATH = "mp3/1024_incrementing_bytes.mp3"; @Test - public void testReadFileUri() throws Exception { + public void readFileUri() throws Exception { AssetDataSource dataSource = new AssetDataSource(ApplicationProvider.getApplicationContext()); DataSpec dataSpec = new DataSpec(Uri.parse("file:///android_asset/" + DATA_PATH)); TestUtil.assertDataSourceContent( @@ -40,7 +40,7 @@ public final class AssetDataSourceTest { } @Test - public void testReadAssetUri() throws Exception { + public void readAssetUri() throws Exception { AssetDataSource dataSource = new AssetDataSource(ApplicationProvider.getApplicationContext()); DataSpec dataSpec = new DataSpec(Uri.parse("asset:///" + DATA_PATH)); TestUtil.assertDataSourceContent( diff --git a/library/core/src/test/java/com/google/android/exoplayer2/upstream/ByteArrayDataSourceTest.java b/library/core/src/test/java/com/google/android/exoplayer2/upstream/ByteArrayDataSourceTest.java index ee64e56c51..27cf243030 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/upstream/ByteArrayDataSourceTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/upstream/ByteArrayDataSourceTest.java @@ -18,6 +18,7 @@ package com.google.android.exoplayer2.upstream; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.fail; +import android.net.Uri; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.C; import java.io.IOException; @@ -32,17 +33,17 @@ public final class ByteArrayDataSourceTest { private static final byte[] TEST_DATA_ODD = new byte[] {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; @Test - public void testFullReadSingleBytes() { + public void fullReadSingleBytes() { readTestData(TEST_DATA, 0, C.LENGTH_UNSET, 1, 0, 1, false); } @Test - public void testFullReadAllBytes() { + public void fullReadAllBytes() { readTestData(TEST_DATA, 0, C.LENGTH_UNSET, 100, 0, 100, false); } @Test - public void testLimitReadSingleBytes() { + public void limitReadSingleBytes() { // Limit set to the length of the data. readTestData(TEST_DATA, 0, TEST_DATA.length, 1, 0, 1, false); // And less. @@ -50,7 +51,7 @@ public final class ByteArrayDataSourceTest { } @Test - public void testFullReadTwoBytes() { + public void fullReadTwoBytes() { // Try with the total data length an exact multiple of the size of each individual read. readTestData(TEST_DATA, 0, C.LENGTH_UNSET, 2, 0, 2, false); // And not. @@ -58,7 +59,7 @@ public final class ByteArrayDataSourceTest { } @Test - public void testLimitReadTwoBytes() { + public void limitReadTwoBytes() { // Try with the limit an exact multiple of the size of each individual read. readTestData(TEST_DATA, 0, 6, 2, 0, 2, false); // And not. @@ -66,7 +67,7 @@ public final class ByteArrayDataSourceTest { } @Test - public void testReadFromValidOffsets() { + public void readFromValidOffsets() { // Read from an offset without bound. readTestData(TEST_DATA, 1, C.LENGTH_UNSET, 1, 0, 1, false); // And with bound. @@ -78,7 +79,7 @@ public final class ByteArrayDataSourceTest { } @Test - public void testReadFromInvalidOffsets() { + public void readFromInvalidOffsets() { // Read from first invalid offset and check failure without bound. readTestData(TEST_DATA, TEST_DATA.length, C.LENGTH_UNSET, 1, 0, 1, true); // And with bound. @@ -86,7 +87,7 @@ public final class ByteArrayDataSourceTest { } @Test - public void testReadWithInvalidLength() { + public void readWithInvalidLength() { // Read more data than is available. readTestData(TEST_DATA, 0, TEST_DATA.length + 1, 1, 0, 1, true); // And with bound. @@ -112,7 +113,7 @@ public final class ByteArrayDataSourceTest { boolean opened = false; try { // Open the source. - long length = dataSource.open(new DataSpec(null, dataOffset, dataLength, null)); + long length = dataSource.open(new DataSpec(Uri.EMPTY, dataOffset, dataLength)); opened = true; assertThat(expectFailOnOpen).isFalse(); diff --git a/library/core/src/test/java/com/google/android/exoplayer2/upstream/DataSchemeDataSourceTest.java b/library/core/src/test/java/com/google/android/exoplayer2/upstream/DataSchemeDataSourceTest.java index 8cb142f05d..0acbd74891 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/upstream/DataSchemeDataSourceTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/upstream/DataSchemeDataSourceTest.java @@ -22,6 +22,7 @@ import static org.junit.Assert.fail; import android.net.Uri; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.testutil.TestUtil; import com.google.android.exoplayer2.util.Util; import java.io.IOException; import org.junit.Before; @@ -43,9 +44,9 @@ public final class DataSchemeDataSourceTest { } @Test - public void testBase64Data() throws IOException { + public void base64Data() throws IOException { DataSpec dataSpec = buildDataSpec(DATA_SCHEME_URI); - DataSourceAsserts.assertDataSourceContent( + assertDataSourceContent( schemeDataDataSource, dataSpec, Util.getUtf8Bytes( @@ -54,15 +55,15 @@ public final class DataSchemeDataSourceTest { } @Test - public void testAsciiData() throws IOException { - DataSourceAsserts.assertDataSourceContent( + public void asciiData() throws IOException { + assertDataSourceContent( schemeDataDataSource, buildDataSpec("data:,A%20brief%20note"), Util.getUtf8Bytes("A brief note")); } @Test - public void testPartialReads() throws IOException { + public void partialReads() throws IOException { byte[] buffer = new byte[18]; DataSpec dataSpec = buildDataSpec("data:,012345678901234567"); assertThat(schemeDataDataSource.open(dataSpec)).isEqualTo(18); @@ -75,29 +76,28 @@ public final class DataSchemeDataSourceTest { } @Test - public void testSequentialRangeRequests() throws IOException { + public void sequentialRangeRequests() throws IOException { DataSpec dataSpec = buildDataSpec(DATA_SCHEME_URI, /* position= */ 1, /* length= */ C.LENGTH_UNSET); - DataSourceAsserts.assertDataSourceContent( + assertDataSourceContent( schemeDataDataSource, dataSpec, Util.getUtf8Bytes( "\"provider\":\"widevine_test\",\"content_id\":\"MjAxNV90ZWFycw==\",\"key_ids\":" + "[\"00000000000000000000000000000000\"]}")); dataSpec = buildDataSpec(DATA_SCHEME_URI, /* position= */ 10, /* length= */ C.LENGTH_UNSET); - DataSourceAsserts.assertDataSourceContent( + assertDataSourceContent( schemeDataDataSource, dataSpec, Util.getUtf8Bytes( "\":\"widevine_test\",\"content_id\":\"MjAxNV90ZWFycw==\",\"key_ids\":" + "[\"00000000000000000000000000000000\"]}")); dataSpec = buildDataSpec(DATA_SCHEME_URI, /* position= */ 15, /* length= */ 5); - DataSourceAsserts.assertDataSourceContent( - schemeDataDataSource, dataSpec, Util.getUtf8Bytes("devin")); + assertDataSourceContent(schemeDataDataSource, dataSpec, Util.getUtf8Bytes("devin")); } @Test - public void testInvalidStartPositionRequest() throws IOException { + public void invalidStartPositionRequest() throws IOException { try { // Try to open a range starting one byte beyond the resource's length. schemeDataDataSource.open( @@ -109,7 +109,7 @@ public final class DataSchemeDataSourceTest { } @Test - public void testRangeExceedingResourceLengthRequest() throws IOException { + public void rangeExceedingResourceLengthRequest() throws IOException { try { // Try to open a range exceeding the resource's length. schemeDataDataSource.open( @@ -121,7 +121,7 @@ public final class DataSchemeDataSourceTest { } @Test - public void testIncorrectScheme() { + public void incorrectScheme() { try { schemeDataDataSource.open(buildDataSpec("http://www.google.com")); fail(); @@ -131,7 +131,7 @@ public final class DataSchemeDataSourceTest { } @Test - public void testMalformedData() { + public void malformedData() { try { schemeDataDataSource.open(buildDataSpec("data:text/plain;base64,,This%20is%20Content")); fail(); @@ -151,7 +151,26 @@ public final class DataSchemeDataSourceTest { } private static DataSpec buildDataSpec(String uriString, int position, int length) { - return new DataSpec(Uri.parse(uriString), position, length, /* key= */ null); + return new DataSpec(Uri.parse(uriString), position, length); } + /** + * Asserts that data read from a {@link DataSource} matches {@code expected}. + * + * @param dataSource The {@link DataSource} through which to read. + * @param dataSpec The {@link DataSpec} to use when opening the {@link DataSource}. + * @param expectedData The expected data. + * @throws IOException If an error occurs reading fom the {@link DataSource}. + */ + private static void assertDataSourceContent( + DataSource dataSource, DataSpec dataSpec, byte[] expectedData) throws IOException { + try { + long length = dataSource.open(dataSpec); + assertThat(length).isEqualTo(expectedData.length); + byte[] readData = TestUtil.readToEnd(dataSource); + assertThat(readData).isEqualTo(expectedData); + } finally { + dataSource.close(); + } + } } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/upstream/DataSourceAsserts.java b/library/core/src/test/java/com/google/android/exoplayer2/upstream/DataSourceAsserts.java deleted file mode 100644 index eff3245923..0000000000 --- a/library/core/src/test/java/com/google/android/exoplayer2/upstream/DataSourceAsserts.java +++ /dev/null @@ -1,50 +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.upstream; - -import static com.google.common.truth.Truth.assertThat; - -import com.google.android.exoplayer2.testutil.TestUtil; -import java.io.IOException; - -/** - * Assertions for data source tests. - */ -/* package */ final class DataSourceAsserts { - - /** - * Asserts that data read from a {@link DataSource} matches {@code expected}. - * - * @param dataSource The {@link DataSource} through which to read. - * @param dataSpec The {@link DataSpec} to use when opening the {@link DataSource}. - * @param expectedData The expected data. - * @throws IOException If an error occurs reading fom the {@link DataSource}. - */ - public static void assertDataSourceContent(DataSource dataSource, DataSpec dataSpec, - byte[] expectedData) throws IOException { - try { - long length = dataSource.open(dataSpec); - assertThat(length).isEqualTo(expectedData.length); - byte[] readData = TestUtil.readToEnd(dataSource); - assertThat(readData).isEqualTo(expectedData); - } finally { - dataSource.close(); - } - } - - private DataSourceAsserts() {} - -} diff --git a/library/core/src/test/java/com/google/android/exoplayer2/upstream/DataSourceInputStreamTest.java b/library/core/src/test/java/com/google/android/exoplayer2/upstream/DataSourceInputStreamTest.java index e9823697f7..5bc3e4d04a 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/upstream/DataSourceInputStreamTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/upstream/DataSourceInputStreamTest.java @@ -33,7 +33,7 @@ public final class DataSourceInputStreamTest { private static final byte[] TEST_DATA = TestUtil.buildTestData(16); @Test - public void testReadSingleBytes() throws IOException { + public void readSingleBytes() throws IOException { DataSourceInputStream inputStream = buildTestInputStream(); // No bytes read yet. assertThat(inputStream.bytesRead()).isEqualTo(0); @@ -53,7 +53,7 @@ public final class DataSourceInputStreamTest { } @Test - public void testRead() throws IOException { + public void read() throws IOException { DataSourceInputStream inputStream = buildTestInputStream(); // Read bytes. byte[] readBytes = new byte[TEST_DATA.length]; @@ -76,7 +76,7 @@ public final class DataSourceInputStreamTest { } @Test - public void testSkip() throws IOException { + public void skip() throws IOException { DataSourceInputStream inputStream = buildTestInputStream(); // Skip bytes. long totalBytesSkipped = 0; diff --git a/library/core/src/test/java/com/google/android/exoplayer2/upstream/DataSpecTest.java b/library/core/src/test/java/com/google/android/exoplayer2/upstream/DataSpecTest.java deleted file mode 100644 index 2323dfe965..0000000000 --- a/library/core/src/test/java/com/google/android/exoplayer2/upstream/DataSpecTest.java +++ /dev/null @@ -1,184 +0,0 @@ -/* - * Copyright (C) 2019 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 static junit.framework.TestCase.fail; - -import android.net.Uri; -import androidx.test.ext.junit.runners.AndroidJUnit4; -import java.util.HashMap; -import java.util.Map; -import org.junit.Test; -import org.junit.runner.RunWith; - -/** Unit tests for {@link DataSpec}. */ -@RunWith(AndroidJUnit4.class) -public class DataSpecTest { - - @Test - public void createDataSpec_withDefaultValues_setsEmptyHttpRequestParameters() { - Uri uri = Uri.parse("www.google.com"); - DataSpec dataSpec = new DataSpec(uri); - - assertThat(dataSpec.httpRequestHeaders.isEmpty()).isTrue(); - - dataSpec = new DataSpec(uri, /*flags= */ 0); - assertThat(dataSpec.httpRequestHeaders.isEmpty()).isTrue(); - - dataSpec = - new DataSpec( - uri, - /* httpMethod= */ 0, - /* httpBody= */ new byte[] {0, 0, 0, 0}, - /* absoluteStreamPosition= */ 0, - /* position= */ 0, - /* length= */ 1, - /* key= */ "key", - /* flags= */ 0); - assertThat(dataSpec.httpRequestHeaders.isEmpty()).isTrue(); - } - - @Test - public void createDataSpec_setsHttpRequestParameters() { - Map httpRequestParameters = new HashMap<>(); - httpRequestParameters.put("key1", "value1"); - httpRequestParameters.put("key2", "value2"); - httpRequestParameters.put("key3", "value3"); - - DataSpec dataSpec = - new DataSpec( - Uri.parse("www.google.com"), - /* httpMethod= */ 0, - /* httpBody= */ new byte[] {0, 0, 0, 0}, - /* absoluteStreamPosition= */ 0, - /* position= */ 0, - /* length= */ 1, - /* key= */ "key", - /* flags= */ 0, - httpRequestParameters); - - assertThat(dataSpec.httpRequestHeaders).isEqualTo(httpRequestParameters); - } - - @Test - public void httpRequestParameters_areReadOnly() { - DataSpec dataSpec = - new DataSpec( - Uri.parse("www.google.com"), - /* httpMethod= */ 0, - /* httpBody= */ new byte[] {0, 0, 0, 0}, - /* absoluteStreamPosition= */ 0, - /* position= */ 0, - /* length= */ 1, - /* key= */ "key", - /* flags= */ 0, - /* httpRequestHeaders= */ new HashMap<>()); - - try { - dataSpec.httpRequestHeaders.put("key", "value"); - fail(); - } catch (UnsupportedOperationException expected) { - // Expected - } - } - - @Test - public void withUri_copiesHttpRequestHeaders() { - Map httpRequestProperties = createRequestProperties(5); - DataSpec dataSpec = createDataSpecWithHeaders(httpRequestProperties); - - DataSpec dataSpecCopy = dataSpec.withUri(Uri.parse("www.new-uri.com")); - - assertThat(dataSpecCopy.httpRequestHeaders).isEqualTo(httpRequestProperties); - } - - @Test - public void subrange_copiesHttpRequestHeaders() { - Map httpRequestProperties = createRequestProperties(5); - DataSpec dataSpec = createDataSpecWithHeaders(httpRequestProperties); - - DataSpec dataSpecCopy = dataSpec.subrange(2); - - assertThat(dataSpecCopy.httpRequestHeaders).isEqualTo(httpRequestProperties); - } - - @Test - public void subrange_withOffsetAndLength_copiesHttpRequestHeaders() { - Map httpRequestProperties = createRequestProperties(5); - DataSpec dataSpec = createDataSpecWithHeaders(httpRequestProperties); - - DataSpec dataSpecCopy = dataSpec.subrange(2, 2); - - assertThat(dataSpecCopy.httpRequestHeaders).isEqualTo(httpRequestProperties); - } - - @Test - public void withRequestHeaders_setsCorrectHeaders() { - Map httpRequestProperties = createRequestProperties(5); - DataSpec dataSpec = createDataSpecWithHeaders(httpRequestProperties); - - Map newRequestHeaders = createRequestProperties(5, 10); - DataSpec dataSpecCopy = dataSpec.withRequestHeaders(newRequestHeaders); - - assertThat(dataSpecCopy.httpRequestHeaders).isEqualTo(newRequestHeaders); - } - - @Test - public void withAdditionalHeaders_setsCorrectHeaders() { - Map httpRequestProperties = createRequestProperties(5); - DataSpec dataSpec = createDataSpecWithHeaders(httpRequestProperties); - Map additionalHeaders = createRequestProperties(5, 10); - // additionalHeaders may overwrite a header key - String existingKey = httpRequestProperties.keySet().iterator().next(); - additionalHeaders.put(existingKey, "overwritten"); - Map expectedHeaders = new HashMap<>(httpRequestProperties); - expectedHeaders.putAll(additionalHeaders); - - DataSpec dataSpecCopy = dataSpec.withAdditionalHeaders(additionalHeaders); - - assertThat(dataSpecCopy.httpRequestHeaders).isEqualTo(expectedHeaders); - } - - private static Map createRequestProperties(int howMany) { - return createRequestProperties(0, howMany); - } - - private static Map createRequestProperties(int from, int to) { - assertThat(from).isLessThan(to); - - Map httpRequestParameters = new HashMap<>(); - for (int i = from; i < to; i++) { - httpRequestParameters.put("key-" + i, "value-" + i); - } - - return httpRequestParameters; - } - - private static DataSpec createDataSpecWithHeaders(Map httpRequestProperties) { - return new DataSpec( - Uri.parse("www.google.com"), - /* httpMethod= */ 0, - /* httpBody= */ new byte[] {0, 0, 0, 0}, - /* absoluteStreamPosition= */ 0, - /* position= */ 0, - /* length= */ 1, - /* key= */ "key", - /* flags= */ 0, - httpRequestProperties); - } -} diff --git a/library/core/src/test/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSourceTest.java b/library/core/src/test/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSourceTest.java index d724369d93..405fe6c5ee 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSourceTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSourceTest.java @@ -18,7 +18,6 @@ package com.google.android.exoplayer2.upstream; import static com.google.common.truth.Truth.assertThat; -import android.net.Uri; import androidx.test.ext.junit.runners.AndroidJUnit4; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; @@ -89,16 +88,13 @@ public class DefaultHttpDataSourceTest { dataSpecRequestProperties.put("5", dataSpecParameter); DataSpec dataSpec = - new DataSpec( - /* uri= */ Uri.parse("http://www.google.com"), - /* httpMethod= */ 1, - /* httpBody= */ new byte[] {0, 0, 0, 0}, - /* absoluteStreamPosition= */ 0, - /* position= */ 0, - /* length= */ 1, - /* key= */ "key", - /* flags= */ 0, - dataSpecRequestProperties); + new DataSpec.Builder() + .setUri("http://www.google.com") + .setHttpBody(new byte[] {0, 0, 0, 0}) + .setLength(1) + .setKey("key") + .setHttpRequestHeaders(dataSpecRequestProperties) + .build(); defaultHttpDataSource.open(dataSpec); 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 83104119ad..6562c17183 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 @@ -34,6 +34,8 @@ import com.google.android.exoplayer2.util.Util; import java.io.File; import java.io.IOException; import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; import java.util.NavigableSet; import org.junit.After; import org.junit.Before; @@ -48,20 +50,27 @@ public final class CacheDataSourceTest { private static final int CACHE_FRAGMENT_SIZE = 3; private static final String DATASPEC_KEY = "dataSpecKey"; + // Test data private Uri testDataUri; + private Map httpRequestHeaders; private DataSpec unboundedDataSpec; private DataSpec boundedDataSpec; private DataSpec unboundedDataSpecWithKey; private DataSpec boundedDataSpecWithKey; private String defaultCacheKey; private String customCacheKey; + + // Dependencies of SUT private CacheKeyFactory cacheKeyFactory; private File tempFolder; private SimpleCache cache; + private FakeDataSource upstreamDataSource; @Before public void setUp() throws Exception { testDataUri = Uri.parse("https://www.test.com/data"); + httpRequestHeaders = new HashMap<>(); + httpRequestHeaders.put("Test-key", "Test-val"); unboundedDataSpec = buildDataSpec(/* unbounded= */ true, /* key= */ null); boundedDataSpec = buildDataSpec(/* unbounded= */ false, /* key= */ null); unboundedDataSpecWithKey = buildDataSpec(/* unbounded= */ true, DATASPEC_KEY); @@ -69,9 +78,11 @@ public final class CacheDataSourceTest { defaultCacheKey = CacheUtil.DEFAULT_CACHE_KEY_FACTORY.buildCacheKey(unboundedDataSpec); customCacheKey = "customKey." + defaultCacheKey; cacheKeyFactory = dataSpec -> customCacheKey; + tempFolder = Util.createTempDirectory(ApplicationProvider.getApplicationContext(), "ExoPlayerTest"); cache = new SimpleCache(tempFolder, new NoOpCacheEvictor()); + upstreamDataSource = new FakeDataSource(); } @After @@ -80,7 +91,7 @@ public final class CacheDataSourceTest { } @Test - public void testFragmentSize() throws Exception { + public void fragmentSize() throws Exception { CacheDataSource cacheDataSource = createCacheDataSource(false, false); assertReadDataContentLength(cacheDataSource, boundedDataSpec, false, false); for (String key : cache.getKeys()) { @@ -92,27 +103,40 @@ public final class CacheDataSourceTest { } @Test - public void testCacheAndReadUnboundedRequest() throws Exception { + public void cacheAndReadUnboundedRequest() throws Exception { assertCacheAndRead(unboundedDataSpec, /* unknownLength= */ false); } @Test - public void testCacheAndReadUnknownLength() throws Exception { + public void cacheAndReadUnknownLength() throws Exception { assertCacheAndRead(boundedDataSpec, /* unknownLength= */ true); } @Test - public void testCacheAndReadUnboundedRequestUnknownLength() throws Exception { + public void cacheAndReadUnboundedRequestUnknownLength() throws Exception { assertCacheAndRead(unboundedDataSpec, /* unknownLength= */ true); } @Test - public void testCacheAndRead() throws Exception { + public void cacheAndRead() throws Exception { assertCacheAndRead(boundedDataSpec, /* unknownLength= */ false); } @Test - public void testUnsatisfiableRange() throws Exception { + public void propagatesHttpHeadersUpstream() throws Exception { + CacheDataSource cacheDataSource = + createCacheDataSource(/* setReadException= */ false, /* unknownLength= */ false); + DataSpec dataSpec = buildDataSpec(/* position= */ 2, /* length= */ 5); + cacheDataSource.open(dataSpec); + + DataSpec[] upstreamDataSpecs = upstreamDataSource.getAndClearOpenedDataSpecs(); + + assertThat(upstreamDataSpecs).hasLength(1); + assertThat(upstreamDataSpecs[0].httpRequestHeaders).isEqualTo(this.httpRequestHeaders); + } + + @Test + public void unsatisfiableRange() throws Exception { // Bounded request but the content length is unknown. This forces all data to be cached but not // the length. assertCacheAndRead(boundedDataSpec, /* unknownLength= */ true); @@ -136,13 +160,13 @@ public final class CacheDataSourceTest { } @Test - public void testCacheAndReadUnboundedRequestWithCacheKeyFactoryWithNullDataSpecCacheKey() + public void cacheAndReadUnboundedRequestWithCacheKeyFactoryWithNullDataSpecCacheKey() throws Exception { assertCacheAndRead(unboundedDataSpec, /* unknownLength= */ false, cacheKeyFactory); } @Test - public void testCacheAndReadUnknownLengthWithCacheKeyFactoryOverridingWithNullDataSpecCacheKey() + public void cacheAndReadUnknownLengthWithCacheKeyFactoryOverridingWithNullDataSpecCacheKey() throws Exception { assertCacheAndRead(boundedDataSpec, /* unknownLength= */ true, cacheKeyFactory); } @@ -155,12 +179,12 @@ public final class CacheDataSourceTest { } @Test - public void testCacheAndReadWithCacheKeyFactoryWithNullDataSpecCacheKey() throws Exception { + public void cacheAndReadWithCacheKeyFactoryWithNullDataSpecCacheKey() throws Exception { assertCacheAndRead(boundedDataSpec, /* unknownLength= */ false, cacheKeyFactory); } @Test - public void testUnsatisfiableRangeWithCacheKeyFactoryNullDataSpecCacheKey() throws Exception { + public void unsatisfiableRangeWithCacheKeyFactoryNullDataSpecCacheKey() throws Exception { // Bounded request but the content length is unknown. This forces all data to be cached but not // the length. assertCacheAndRead(boundedDataSpec, /* unknownLength= */ true, cacheKeyFactory); @@ -186,13 +210,13 @@ public final class CacheDataSourceTest { } @Test - public void testCacheAndReadUnboundedRequestWithCacheKeyFactoryOverridingDataSpecCacheKey() + public void cacheAndReadUnboundedRequestWithCacheKeyFactoryOverridingDataSpecCacheKey() throws Exception { assertCacheAndRead(unboundedDataSpecWithKey, false, cacheKeyFactory); } @Test - public void testCacheAndReadUnknownLengthWithCacheKeyFactoryOverridingDataSpecCacheKey() + public void cacheAndReadUnknownLengthWithCacheKeyFactoryOverridingDataSpecCacheKey() throws Exception { assertCacheAndRead(boundedDataSpecWithKey, true, cacheKeyFactory); } @@ -205,13 +229,12 @@ public final class CacheDataSourceTest { } @Test - public void testCacheAndReadWithCacheKeyFactoryOverridingDataSpecCacheKey() throws Exception { + public void cacheAndReadWithCacheKeyFactoryOverridingDataSpecCacheKey() throws Exception { assertCacheAndRead(boundedDataSpecWithKey, /* unknownLength= */ false, cacheKeyFactory); } @Test - public void testUnsatisfiableRangeWithCacheKeyFactoryOverridingDataSpecCacheKey() - throws Exception { + public void unsatisfiableRangeWithCacheKeyFactoryOverridingDataSpecCacheKey() throws Exception { // Bounded request but the content length is unknown. This forces all data to be cached but not // the length. assertCacheAndRead(boundedDataSpecWithKey, /* unknownLength= */ true, cacheKeyFactory); @@ -240,7 +263,7 @@ public final class CacheDataSourceTest { } @Test - public void testContentLengthEdgeCases() throws Exception { + public void contentLengthEdgeCases() throws Exception { DataSpec dataSpec = buildDataSpec(TEST_DATA.length - 2, 2); // Read partial at EOS but don't cross it so length is unknown. @@ -262,16 +285,12 @@ public final class CacheDataSourceTest { // An unbounded request with offset for not cached content. dataSpec = - new DataSpec( - Uri.parse("https://www.test.com/other"), - TEST_DATA.length - 2, - C.LENGTH_UNSET, - /* key= */ null); + new DataSpec(Uri.parse("https://www.test.com/other"), TEST_DATA.length - 2, C.LENGTH_UNSET); assertThat(cacheDataSource.open(dataSpec)).isEqualTo(C.LENGTH_UNSET); } @Test - public void testUnknownLengthContentReadInOneConnectionAndLengthIsResolved() throws Exception { + public void unknownLengthContentReadInOneConnectionAndLengthIsResolved() throws Exception { FakeDataSource upstream = new FakeDataSource(); upstream .getDataSet() @@ -290,7 +309,7 @@ public final class CacheDataSourceTest { } @Test - public void testIgnoreCacheForUnsetLengthRequests() throws Exception { + public void ignoreCacheForUnsetLengthRequests() throws Exception { FakeDataSource upstream = new FakeDataSource(); upstream.getDataSet().setData(testDataUri, TEST_DATA); CacheDataSource cacheDataSource = @@ -305,14 +324,14 @@ public final class CacheDataSourceTest { } @Test - public void testReadOnlyCache() throws Exception { + public void readOnlyCache() throws Exception { CacheDataSource cacheDataSource = createCacheDataSource(false, false, 0, null); assertReadDataContentLength(cacheDataSource, boundedDataSpec, false, false); assertCacheEmpty(cache); } @Test - public void testSwitchToCacheSourceWithReadOnlyCacheDataSource() throws Exception { + public void switchToCacheSourceWithReadOnlyCacheDataSource() throws Exception { // Create a fake data source with a 1 MB default data. FakeDataSource upstream = new FakeDataSource(); FakeData fakeData = upstream.getDataSet().newDefaultData().appendReadData(1024 * 1024 - 1); @@ -339,12 +358,7 @@ public final class CacheDataSourceTest { .appendReadData(1024 * 1024) .endData()); CacheUtil.cache( - unboundedDataSpec, - cache, - /* cacheKeyFactory= */ null, - upstream2, - /* progressListener= */ null, - /* isCanceled= */ null); + cache, unboundedDataSpec, upstream2, /* progressListener= */ null, /* isCanceled= */ null); // Read the rest of the data. TestUtil.readToEnd(cacheDataSource); @@ -352,7 +366,7 @@ public final class CacheDataSourceTest { } @Test - public void testSwitchToCacheSourceWithNonBlockingCacheDataSource() throws Exception { + public void switchToCacheSourceWithNonBlockingCacheDataSource() throws Exception { // Create a fake data source with a 1 MB default data. FakeDataSource upstream = new FakeDataSource(); FakeData fakeData = upstream.getDataSet().newDefaultData().appendReadData(1024 * 1024 - 1); @@ -388,12 +402,7 @@ public final class CacheDataSourceTest { .appendReadData(1024 * 1024) .endData()); CacheUtil.cache( - unboundedDataSpec, - cache, - /* cacheKeyFactory= */ null, - upstream2, - /* progressListener= */ null, - /* isCanceled= */ null); + cache, unboundedDataSpec, upstream2, /* progressListener= */ null, /* isCanceled= */ null); // Read the rest of the data. TestUtil.readToEnd(cacheDataSource); @@ -401,7 +410,7 @@ public final class CacheDataSourceTest { } @Test - public void testDeleteCachedWhileReadingFromUpstreamWithReadOnlyCacheDataSourceDoesNotCrash() + public void deleteCachedWhileReadingFromUpstreamWithReadOnlyCacheDataSourceDoesNotCrash() throws Exception { // Create a fake data source with a 1 KB default data. FakeDataSource upstream = new FakeDataSource(); @@ -412,12 +421,7 @@ public final class CacheDataSourceTest { int halfDataLength = 512; DataSpec dataSpec = buildDataSpec(halfDataLength, C.LENGTH_UNSET); CacheUtil.cache( - dataSpec, - cache, - /* cacheKeyFactory= */ null, - upstream, - /* progressListener= */ null, - /* isCanceled= */ null); + cache, dataSpec, upstream, /* progressListener= */ null, /* isCanceled= */ null); // Create cache read-only CacheDataSource. CacheDataSource cacheDataSource = @@ -437,7 +441,7 @@ public final class CacheDataSourceTest { } @Test - public void testDeleteCachedWhileReadingFromUpstreamWithBlockingCacheDataSourceDoesNotBlock() + public void deleteCachedWhileReadingFromUpstreamWithBlockingCacheDataSourceDoesNotBlock() throws Exception { // Create a fake data source with a 1 KB default data. FakeDataSource upstream = new FakeDataSource(); @@ -448,12 +452,7 @@ public final class CacheDataSourceTest { int halfDataLength = 512; DataSpec dataSpec = buildDataSpec(/* position= */ 0, halfDataLength); CacheUtil.cache( - dataSpec, - cache, - /* cacheKeyFactory= */ null, - upstream, - /* progressListener= */ null, - /* isCanceled= */ null); + cache, dataSpec, upstream, /* progressListener= */ null, /* isCanceled= */ null); // Create blocking CacheDataSource. CacheDataSource cacheDataSource = @@ -522,7 +521,7 @@ public final class CacheDataSourceTest { private void assertReadData( CacheDataSource cacheDataSource, DataSpec dataSpec, boolean unknownLength) throws IOException { - int position = (int) dataSpec.absoluteStreamPosition; + int position = (int) dataSpec.position; int requestLength = (int) dataSpec.length; int readLength = TEST_DATA.length - position; if (requestLength != C.LENGTH_UNSET) { @@ -572,9 +571,8 @@ public final class CacheDataSourceTest { @CacheDataSource.Flags int flags, CacheDataSink cacheWriteDataSink, CacheKeyFactory cacheKeyFactory) { - FakeDataSource upstream = new FakeDataSource(); FakeData fakeData = - upstream + upstreamDataSource .getDataSet() .newDefaultData() .setSimulateUnknownLength(unknownLength) @@ -584,7 +582,7 @@ public final class CacheDataSourceTest { } return new CacheDataSource( cache, - upstream, + upstreamDataSource, new FileDataSource(), cacheWriteDataSink, flags, @@ -601,7 +599,13 @@ public final class CacheDataSourceTest { } private DataSpec buildDataSpec(long position, long length, @Nullable String key) { - return new DataSpec( - testDataUri, position, length, key, DataSpec.FLAG_ALLOW_CACHE_FRAGMENTATION); + return new DataSpec.Builder() + .setUri(testDataUri) + .setPosition(position) + .setLength(length) + .setKey(key) + .setFlags(DataSpec.FLAG_ALLOW_CACHE_FRAGMENTATION) + .setHttpRequestHeaders(httpRequestHeaders) + .build(); } } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest2.java b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest2.java index bb32f47ba8..8702e887f8 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest2.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest2.java @@ -18,6 +18,7 @@ package com.google.android.exoplayer2.upstream.cache; import static com.google.common.truth.Truth.assertThat; import static java.util.Arrays.copyOf; import static java.util.Arrays.copyOfRange; +import static org.robolectric.annotation.LooperMode.Mode.LEGACY; import android.content.Context; import android.net.Uri; @@ -39,8 +40,10 @@ import java.io.IOException; import java.util.Random; import org.junit.Test; import org.junit.runner.RunWith; +import org.robolectric.annotation.LooperMode; /** Additional tests for {@link CacheDataSource}. */ +@LooperMode(LEGACY) @RunWith(AndroidJUnit4.class) public final class CacheDataSourceTest2 { @@ -48,25 +51,24 @@ public final class CacheDataSourceTest2 { private static final int EXO_CACHE_MAX_FILESIZE = 128; private static final Uri URI = Uri.parse("http://test.com/content"); - private static final String KEY = "key"; private static final byte[] DATA = TestUtil.buildTestData(8 * EXO_CACHE_MAX_FILESIZE + 1); // A DataSpec that covers the full file. - private static final DataSpec FULL = new DataSpec(URI, 0, DATA.length, KEY); + private static final DataSpec FULL = new DataSpec(URI, 0, DATA.length); private static final int OFFSET_ON_BOUNDARY = EXO_CACHE_MAX_FILESIZE; // A DataSpec that starts at 0 and extends to a cache file boundary. - private static final DataSpec END_ON_BOUNDARY = new DataSpec(URI, 0, OFFSET_ON_BOUNDARY, KEY); + private static final DataSpec END_ON_BOUNDARY = new DataSpec(URI, 0, OFFSET_ON_BOUNDARY); // A DataSpec that starts on the same boundary and extends to the end of the file. - private static final DataSpec START_ON_BOUNDARY = new DataSpec(URI, OFFSET_ON_BOUNDARY, - DATA.length - OFFSET_ON_BOUNDARY, KEY); + private static final DataSpec START_ON_BOUNDARY = + new DataSpec(URI, OFFSET_ON_BOUNDARY, DATA.length - OFFSET_ON_BOUNDARY); private static final int OFFSET_OFF_BOUNDARY = EXO_CACHE_MAX_FILESIZE * 2 + 1; // A DataSpec that starts at 0 and extends to just past a cache file boundary. - private static final DataSpec END_OFF_BOUNDARY = new DataSpec(URI, 0, OFFSET_OFF_BOUNDARY, KEY); + private static final DataSpec END_OFF_BOUNDARY = new DataSpec(URI, 0, OFFSET_OFF_BOUNDARY); // A DataSpec that starts on the same boundary and extends to the end of the file. - private static final DataSpec START_OFF_BOUNDARY = new DataSpec(URI, OFFSET_OFF_BOUNDARY, - DATA.length - OFFSET_OFF_BOUNDARY, KEY); + private static final DataSpec START_OFF_BOUNDARY = + new DataSpec(URI, OFFSET_OFF_BOUNDARY, DATA.length - OFFSET_OFF_BOUNDARY); @Test public void testWithoutEncryption() throws IOException { @@ -112,7 +114,7 @@ public final class CacheDataSourceTest2 { byte[] scratch = new byte[4096]; Random random = new Random(0); source.open(dataSpec); - int position = (int) dataSpec.absoluteStreamPosition; + int position = (int) dataSpec.position; int bytesRead = 0; while (bytesRead != C.RESULT_END_OF_INPUT) { int maxBytesToRead = random.nextInt(scratch.length) + 1; @@ -134,7 +136,6 @@ public final class CacheDataSourceTest2 { DataSpec[] openedDataSpecs = upstreamSource.getAndClearOpenedDataSpecs(); assertThat(openedDataSpecs).hasLength(1); assertThat(openedDataSpecs[0].position).isEqualTo(start); - assertThat(openedDataSpecs[0].absoluteStreamPosition).isEqualTo(start); assertThat(openedDataSpecs[0].length).isEqualTo(end - start); } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CacheFileMetadataIndexTest.java b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CacheFileMetadataIndexTest.java new file mode 100644 index 0000000000..283487f7ea --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CacheFileMetadataIndexTest.java @@ -0,0 +1,135 @@ +/* + * Copyright (C) 2019 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.cache; + +import static com.google.common.truth.Truth.assertThat; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.android.exoplayer2.database.DatabaseIOException; +import com.google.android.exoplayer2.testutil.TestUtil; +import java.util.HashSet; +import java.util.Map; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Tests {@link CacheFileMetadataIndex}. */ +@RunWith(AndroidJUnit4.class) +public class CacheFileMetadataIndexTest { + + @Test + public void initiallyEmpty() throws DatabaseIOException { + CacheFileMetadataIndex index = newInitializedIndex(); + assertThat(index.getAll()).isEmpty(); + } + + @Test + public void insert() throws DatabaseIOException { + CacheFileMetadataIndex index = newInitializedIndex(); + + index.set("name1", /* length= */ 123, /* lastTouchTimestamp= */ 456); + index.set("name2", /* length= */ 789, /* lastTouchTimestamp= */ 123); + + Map all = index.getAll(); + assertThat(all.size()).isEqualTo(2); + + CacheFileMetadata metadata = all.get("name1"); + assertThat(metadata).isNotNull(); + assertThat(metadata.length).isEqualTo(123); + assertThat(metadata.lastTouchTimestamp).isEqualTo(456); + + metadata = all.get("name2"); + assertThat(metadata).isNotNull(); + assertThat(metadata.length).isEqualTo(789); + assertThat(metadata.lastTouchTimestamp).isEqualTo(123); + + metadata = all.get("name3"); + assertThat(metadata).isNull(); + } + + @Test + public void insertAndRemove() throws DatabaseIOException { + CacheFileMetadataIndex index = newInitializedIndex(); + + index.set("name1", /* length= */ 123, /* lastTouchTimestamp= */ 456); + index.set("name2", /* length= */ 789, /* lastTouchTimestamp= */ 123); + + index.remove("name1"); + + Map all = index.getAll(); + assertThat(all.size()).isEqualTo(1); + + CacheFileMetadata metadata = all.get("name1"); + assertThat(metadata).isNull(); + + metadata = all.get("name2"); + assertThat(metadata).isNotNull(); + assertThat(metadata.length).isEqualTo(789); + assertThat(metadata.lastTouchTimestamp).isEqualTo(123); + + index.remove("name2"); + + all = index.getAll(); + assertThat(all).isEmpty(); + + metadata = all.get("name2"); + assertThat(metadata).isNull(); + } + + @Test + public void insertAndRemoveAll() throws DatabaseIOException { + CacheFileMetadataIndex index = newInitializedIndex(); + + index.set("name1", /* length= */ 123, /* lastTouchTimestamp= */ 456); + index.set("name2", /* length= */ 789, /* lastTouchTimestamp= */ 123); + + HashSet namesToRemove = new HashSet<>(); + namesToRemove.add("name1"); + namesToRemove.add("name2"); + index.removeAll(namesToRemove); + + Map all = index.getAll(); + assertThat(all.isEmpty()).isTrue(); + + CacheFileMetadata metadata = all.get("name1"); + assertThat(metadata).isNull(); + + metadata = all.get("name2"); + assertThat(metadata).isNull(); + } + + @Test + public void insertAndReplace() throws DatabaseIOException { + CacheFileMetadataIndex index = newInitializedIndex(); + + index.set("name1", /* length= */ 123, /* lastTouchTimestamp= */ 456); + index.set("name1", /* length= */ 789, /* lastTouchTimestamp= */ 123); + + Map all = index.getAll(); + assertThat(all.size()).isEqualTo(1); + + CacheFileMetadata metadata = all.get("name1"); + assertThat(metadata).isNotNull(); + assertThat(metadata.length).isEqualTo(789); + assertThat(metadata.lastTouchTimestamp).isEqualTo(123); + } + + private static CacheFileMetadataIndex newInitializedIndex() throws DatabaseIOException { + CacheFileMetadataIndex index = + new CacheFileMetadataIndex(TestUtil.getInMemoryDatabaseProvider()); + index.initialize(/* uid= */ 1234); + return index; + } +} 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 9a449b2ebd..d0a4da4f8c 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 @@ -105,7 +105,7 @@ public final class CacheUtilTest { } @Test - public void testGenerateKey() { + public void generateKey() { assertThat(CacheUtil.generateKey(Uri.EMPTY)).isNotNull(); Uri testUri = Uri.parse("test"); @@ -120,23 +120,23 @@ public final class CacheUtilTest { } @Test - public void testDefaultCacheKeyFactory_buildCacheKey() { + public void defaultCacheKeyFactory_buildCacheKey() { 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))) + new DataSpec.Builder().setUri(testUri).setKey(key).build())) .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))) + new DataSpec(testUri, /* position= */ 0, /* length= */ LENGTH_UNSET))) .isEqualTo(testUri.toString()); } @Test - public void testGetCachedNoData() { + public void getCachedNoData() { Pair contentLengthAndBytesCached = CacheUtil.getCached( new DataSpec(Uri.parse("test")), mockCache, /* cacheKeyFactory= */ null); @@ -146,7 +146,7 @@ public final class CacheUtilTest { } @Test - public void testGetCachedDataUnknownLength() { + public void getCachedDataUnknownLength() { // Mock there is 100 bytes cached at the beginning mockCache.spansAndGaps = new int[] {100}; Pair contentLengthAndBytesCached = @@ -158,7 +158,7 @@ public final class CacheUtilTest { } @Test - public void testGetCachedNoDataKnownLength() { + public void getCachedNoDataKnownLength() { mockCache.contentLength = 1000; Pair contentLengthAndBytesCached = CacheUtil.getCached( @@ -169,7 +169,7 @@ public final class CacheUtilTest { } @Test - public void testGetCached() { + public void getCached() { mockCache.contentLength = 1000; mockCache.spansAndGaps = new int[] {100, 100, 200}; Pair contentLengthAndBytesCached = @@ -181,16 +181,12 @@ public final class CacheUtilTest { } @Test - public void testGetCachedFromNonZeroPosition() { + public void getCachedFromNonZeroPosition() { mockCache.contentLength = 1000; mockCache.spansAndGaps = new int[] {100, 100, 200}; Pair contentLengthAndBytesCached = CacheUtil.getCached( - new DataSpec( - Uri.parse("test"), - /* absoluteStreamPosition= */ 100, - /* length= */ C.LENGTH_UNSET, - /* key= */ null), + new DataSpec(Uri.parse("test"), /* position= */ 100, /* length= */ C.LENGTH_UNSET), mockCache, /* cacheKeyFactory= */ null); @@ -199,51 +195,39 @@ public final class CacheUtilTest { } @Test - public void testCache() throws Exception { + public void cache() throws Exception { FakeDataSet fakeDataSet = new FakeDataSet().setRandomData("test_data", 100); FakeDataSource dataSource = new FakeDataSource(fakeDataSet); CachingCounters counters = new CachingCounters(); CacheUtil.cache( - new DataSpec(Uri.parse("test_data")), - cache, - /* cacheKeyFactory= */ null, - dataSource, - counters, - /* isCanceled= */ null); + cache, new DataSpec(Uri.parse("test_data")), dataSource, counters, /* isCanceled= */ null); counters.assertValues(0, 100, 100); assertCachedData(cache, fakeDataSet); } @Test - public void testCacheSetOffsetAndLength() throws Exception { + public void cacheSetOffsetAndLength() throws Exception { FakeDataSet fakeDataSet = new FakeDataSet().setRandomData("test_data", 100); FakeDataSource dataSource = new FakeDataSource(fakeDataSet); Uri testUri = Uri.parse("test_data"); - DataSpec dataSpec = new DataSpec(testUri, 10, 20, null); + DataSpec dataSpec = new DataSpec(testUri, /* position= */ 10, /* length= */ 20); CachingCounters counters = new CachingCounters(); - CacheUtil.cache( - dataSpec, cache, /* cacheKeyFactory= */ null, dataSource, counters, /* isCanceled= */ null); + CacheUtil.cache(cache, dataSpec, dataSource, counters, /* isCanceled= */ null); counters.assertValues(0, 20, 20); counters.reset(); - CacheUtil.cache( - new DataSpec(testUri), - cache, - /* cacheKeyFactory= */ null, - dataSource, - counters, - /* isCanceled= */ null); + CacheUtil.cache(cache, new DataSpec(testUri), dataSource, counters, /* isCanceled= */ null); counters.assertValues(20, 80, 100); assertCachedData(cache, fakeDataSet); } @Test - public void testCacheUnknownLength() throws Exception { + public void cacheUnknownLength() throws Exception { FakeDataSet fakeDataSet = new FakeDataSet().newData("test_data") .setSimulateUnknownLength(true) .appendReadData(TestUtil.buildTestData(100)).endData(); @@ -251,76 +235,63 @@ public final class CacheUtilTest { DataSpec dataSpec = new DataSpec(Uri.parse("test_data")); CachingCounters counters = new CachingCounters(); - CacheUtil.cache( - dataSpec, cache, /* cacheKeyFactory= */ null, dataSource, counters, /* isCanceled= */ null); + CacheUtil.cache(cache, dataSpec, dataSource, counters, /* isCanceled= */ null); counters.assertValues(0, 100, 100); assertCachedData(cache, fakeDataSet); } @Test - public void testCacheUnknownLengthPartialCaching() throws Exception { + public void cacheUnknownLengthPartialCaching() throws Exception { FakeDataSet fakeDataSet = new FakeDataSet().newData("test_data") .setSimulateUnknownLength(true) .appendReadData(TestUtil.buildTestData(100)).endData(); FakeDataSource dataSource = new FakeDataSource(fakeDataSet); Uri testUri = Uri.parse("test_data"); - DataSpec dataSpec = new DataSpec(testUri, 10, 20, null); + DataSpec dataSpec = new DataSpec(testUri, /* position= */ 10, /* length= */ 20); CachingCounters counters = new CachingCounters(); - CacheUtil.cache( - dataSpec, cache, /* cacheKeyFactory= */ null, dataSource, counters, /* isCanceled= */ null); + CacheUtil.cache(cache, dataSpec, dataSource, counters, /* isCanceled= */ null); counters.assertValues(0, 20, 20); counters.reset(); - CacheUtil.cache( - new DataSpec(testUri), - cache, - /* cacheKeyFactory= */ null, - dataSource, - counters, - /* isCanceled= */ null); + CacheUtil.cache(cache, new DataSpec(testUri), dataSource, counters, /* isCanceled= */ null); counters.assertValues(20, 80, 100); assertCachedData(cache, fakeDataSet); } @Test - public void testCacheLengthExceedsActualDataLength() throws Exception { + public void cacheLengthExceedsActualDataLength() throws Exception { FakeDataSet fakeDataSet = new FakeDataSet().setRandomData("test_data", 100); FakeDataSource dataSource = new FakeDataSource(fakeDataSet); Uri testUri = Uri.parse("test_data"); - DataSpec dataSpec = new DataSpec(testUri, 0, 1000, null); + DataSpec dataSpec = new DataSpec(testUri, /* position= */ 0, /* length= */ 1000); CachingCounters counters = new CachingCounters(); - CacheUtil.cache( - dataSpec, cache, /* cacheKeyFactory= */ null, dataSource, counters, /* isCanceled= */ null); + CacheUtil.cache(cache, dataSpec, dataSource, counters, /* isCanceled= */ null); counters.assertValues(0, 100, 1000); assertCachedData(cache, fakeDataSet); } @Test - public void testCacheThrowEOFException() throws Exception { + public void cacheThrowEOFException() throws Exception { FakeDataSet fakeDataSet = new FakeDataSet().setRandomData("test_data", 100); FakeDataSource dataSource = new FakeDataSource(fakeDataSet); Uri testUri = Uri.parse("test_data"); - DataSpec dataSpec = new DataSpec(testUri, 0, 1000, null); + DataSpec dataSpec = new DataSpec(testUri, /* position= */ 0, /* length= */ 1000); try { CacheUtil.cache( - dataSpec, - cache, - /* cacheKeyFactory= */ null, new CacheDataSource(cache, dataSource), - new byte[CacheUtil.DEFAULT_BUFFER_SIZE_BYTES], - /* priorityTaskManager= */ null, - /* priority= */ 0, + dataSpec, /* progressListener= */ null, /* isCanceled= */ null, - /* enableEOFException= */ true); + /* enableEOFException= */ true, + /* temporaryBuffer= */ new byte[CacheUtil.DEFAULT_BUFFER_SIZE_BYTES]); fail(); } catch (EOFException e) { // Do nothing. @@ -328,7 +299,7 @@ public final class CacheUtilTest { } @Test - public void testCachePolling() throws Exception { + public void cachePolling() throws Exception { final CachingCounters counters = new CachingCounters(); FakeDataSet fakeDataSet = new FakeDataSet() @@ -342,28 +313,23 @@ public final class CacheUtilTest { FakeDataSource dataSource = new FakeDataSource(fakeDataSet); CacheUtil.cache( - new DataSpec(Uri.parse("test_data")), - cache, - /* cacheKeyFactory= */ null, - dataSource, - counters, - /* isCanceled= */ null); + cache, new DataSpec(Uri.parse("test_data")), dataSource, counters, /* isCanceled= */ null); counters.assertValues(0, 300, 300); assertCachedData(cache, fakeDataSet); } @Test - public void testRemove() throws Exception { + public void remove() throws Exception { FakeDataSet fakeDataSet = new FakeDataSet().setRandomData("test_data", 100); FakeDataSource dataSource = new FakeDataSource(fakeDataSet); - Uri uri = Uri.parse("test_data"); - DataSpec dataSpec = new DataSpec(uri, DataSpec.FLAG_ALLOW_CACHE_FRAGMENTATION); + DataSpec dataSpec = + new DataSpec.Builder() + .setUri("test_data") + .setFlags(DataSpec.FLAG_ALLOW_CACHE_FRAGMENTATION) + .build(); CacheUtil.cache( - dataSpec, - cache, - /* cacheKeyFactory= */ null, // Set fragmentSize to 10 to make sure there are multiple spans. new CacheDataSource( cache, @@ -372,12 +338,11 @@ public final class CacheUtilTest { new CacheDataSink(cache, /* fragmentSize= */ 10), /* flags= */ 0, /* eventListener= */ null), - new byte[CacheUtil.DEFAULT_BUFFER_SIZE_BYTES], - /* priorityTaskManager= */ null, - /* priority= */ 0, + dataSpec, /* progressListener= */ null, /* isCanceled= */ null, - true); + /* enableEOFException= */ true, + /* temporaryBuffer= */ new byte[CacheUtil.DEFAULT_BUFFER_SIZE_BYTES]); CacheUtil.remove(dataSpec, cache, /* cacheKeyFactory= */ null); assertCacheEmpty(cache); diff --git a/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndexTest.java b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndexTest.java index fda57cbce4..bbb372b5e2 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndexTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndexTest.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.upstream.cache; +import static com.google.android.exoplayer2.testutil.TestUtil.createTestFile; import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertWithMessage; @@ -88,7 +89,7 @@ public class CachedContentIndexTest { } @Test - public void testAddGetRemove() throws Exception { + public void addGetRemove() throws Exception { final String key1 = "key1"; final String key2 = "key2"; final String key3 = "key3"; @@ -103,12 +104,9 @@ public class CachedContentIndexTest { // add a span int cacheFileLength = 20; File cacheSpanFile = - SimpleCacheSpanTest.createCacheSpanFile( - cacheDir, - cachedContent1.id, - /* offset= */ 10, - cacheFileLength, - /* lastTouchTimestamp= */ 30); + SimpleCacheSpan.getCacheFile( + cacheDir, cachedContent1.id, /* position= */ 10, /* timestamp= */ 30); + createTestFile(cacheSpanFile, cacheFileLength); SimpleCacheSpan span = SimpleCacheSpan.createCacheEntry(cacheSpanFile, cacheFileLength, index); assertThat(span).isNotNull(); cachedContent1.addSpan(span); @@ -146,12 +144,12 @@ public class CachedContentIndexTest { } @Test - public void testLegacyStoreAndLoad() throws Exception { + public void legacyStoreAndLoad() throws Exception { assertStoredAndLoadedEqual(newLegacyInstance(), newLegacyInstance()); } @Test - public void testLegacyLoadV1() throws Exception { + public void legacyLoadV1() throws Exception { CachedContentIndex index = newLegacyInstance(); FileOutputStream fos = @@ -172,7 +170,7 @@ public class CachedContentIndexTest { } @Test - public void testLegacyLoadV2() throws Exception { + public void legacyLoadV2() throws Exception { CachedContentIndex index = newLegacyInstance(); FileOutputStream fos = @@ -194,7 +192,7 @@ public class CachedContentIndexTest { } @Test - public void testAssignIdForKeyAndGetKeyForId() { + public void assignIdForKeyAndGetKeyForId() { CachedContentIndex index = newInstance(); final String key1 = "key1"; final String key2 = "key2"; @@ -208,7 +206,7 @@ public class CachedContentIndexTest { } @Test - public void testGetNewId() { + public void getNewId() { SparseArray idToKey = new SparseArray<>(); assertThat(CachedContentIndex.getNewId(idToKey)).isEqualTo(0); idToKey.put(10, ""); @@ -220,7 +218,7 @@ public class CachedContentIndexTest { } @Test - public void testLegacyEncryption() throws Exception { + public void legacyEncryption() throws Exception { byte[] key = Util.getUtf8Bytes("Bar12345Bar12345"); // 128 bit key byte[] key2 = Util.getUtf8Bytes("Foo12345Foo12345"); // 128 bit key @@ -272,7 +270,7 @@ public class CachedContentIndexTest { } @Test - public void testRemoveEmptyNotLockedCachedContent() { + public void removeEmptyNotLockedCachedContent() { CachedContentIndex index = newInstance(); CachedContent cachedContent = index.getOrAdd("key1"); @@ -282,18 +280,15 @@ public class CachedContentIndexTest { } @Test - public void testCantRemoveNotEmptyCachedContent() throws Exception { + public void cantRemoveNotEmptyCachedContent() throws Exception { CachedContentIndex index = newInstance(); CachedContent cachedContent = index.getOrAdd("key1"); long cacheFileLength = 20; File cacheFile = - SimpleCacheSpanTest.createCacheSpanFile( - cacheDir, - cachedContent.id, - /* offset= */ 10, - cacheFileLength, - /* lastTouchTimestamp= */ 30); + SimpleCacheSpan.getCacheFile( + cacheDir, cachedContent.id, /* position= */ 10, /* timestamp= */ 30); + createTestFile(cacheFile, cacheFileLength); SimpleCacheSpan span = SimpleCacheSpan.createCacheEntry(cacheFile, cacheFileLength, index); cachedContent.addSpan(span); @@ -303,7 +298,7 @@ public class CachedContentIndexTest { } @Test - public void testCantRemoveLockedCachedContent() { + public void cantRemoveLockedCachedContent() { CachedContentIndex index = newInstance(); CachedContent cachedContent = index.getOrAdd("key1"); cachedContent.setLocked(true); diff --git a/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CachedRegionTrackerTest.java b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CachedRegionTrackerTest.java index a1c4d2b59d..a866f20366 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CachedRegionTrackerTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CachedRegionTrackerTest.java @@ -75,13 +75,13 @@ public final class CachedRegionTrackerTest { } @Test - public void testGetRegion_noSpansInCache() { + public void getRegion_noSpansInCache() { assertThat(tracker.getRegionEndTimeMs(100)).isEqualTo(CachedRegionTracker.NOT_CACHED); assertThat(tracker.getRegionEndTimeMs(150)).isEqualTo(CachedRegionTracker.NOT_CACHED); } @Test - public void testGetRegion_fullyCached() throws Exception { + public void getRegion_fullyCached() throws Exception { tracker.onSpanAdded(cache, newCacheSpan(100, 100)); assertThat(tracker.getRegionEndTimeMs(101)).isEqualTo(CachedRegionTracker.CACHED_TO_END); @@ -89,7 +89,7 @@ public final class CachedRegionTrackerTest { } @Test - public void testGetRegion_partiallyCached() throws Exception { + public void getRegion_partiallyCached() throws Exception { tracker.onSpanAdded(cache, newCacheSpan(100, 40)); assertThat(tracker.getRegionEndTimeMs(101)).isEqualTo(200); @@ -97,7 +97,7 @@ public final class CachedRegionTrackerTest { } @Test - public void testGetRegion_multipleSpanAddsJoinedCorrectly() throws Exception { + public void getRegion_multipleSpanAddsJoinedCorrectly() throws Exception { tracker.onSpanAdded(cache, newCacheSpan(100, 20)); tracker.onSpanAdded(cache, newCacheSpan(120, 20)); @@ -106,7 +106,7 @@ public final class CachedRegionTrackerTest { } @Test - public void testGetRegion_fullyCachedThenPartiallyRemoved() throws Exception { + public void getRegion_fullyCachedThenPartiallyRemoved() throws Exception { // Start with the full stream in cache. tracker.onSpanAdded(cache, newCacheSpan(100, 100)); @@ -120,7 +120,7 @@ public final class CachedRegionTrackerTest { } @Test - public void testGetRegion_subchunkEstimation() throws Exception { + public void getRegion_subchunkEstimation() throws Exception { tracker.onSpanAdded(cache, newCacheSpan(100, 10)); assertThat(tracker.getRegionEndTimeMs(101)).isEqualTo(50); diff --git a/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/DefaultContentMetadataTest.java b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/DefaultContentMetadataTest.java index 9c9b7073eb..5dfc4f4a33 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/DefaultContentMetadataTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/DefaultContentMetadataTest.java @@ -34,35 +34,35 @@ public class DefaultContentMetadataTest { } @Test - public void testContainsReturnsFalseWhenEmpty() throws Exception { + public void containsReturnsFalseWhenEmpty() throws Exception { assertThat(contentMetadata.contains("test metadata")).isFalse(); } @Test - public void testContainsReturnsTrueForInitialValue() throws Exception { + public void containsReturnsTrueForInitialValue() throws Exception { contentMetadata = createContentMetadata("metadata name", "value"); assertThat(contentMetadata.contains("metadata name")).isTrue(); } @Test - public void testGetReturnsDefaultValueWhenValueIsNotAvailable() throws Exception { + public void getReturnsDefaultValueWhenValueIsNotAvailable() throws Exception { assertThat(contentMetadata.get("metadata name", "default value")).isEqualTo("default value"); } @Test - public void testGetReturnsInitialValue() throws Exception { + public void getReturnsInitialValue() throws Exception { contentMetadata = createContentMetadata("metadata name", "value"); assertThat(contentMetadata.get("metadata name", "default value")).isEqualTo("value"); } @Test - public void testEmptyMutationDoesNotFail() throws Exception { + public void emptyMutationDoesNotFail() throws Exception { ContentMetadataMutations mutations = new ContentMetadataMutations(); DefaultContentMetadata.EMPTY.copyWithMutationsApplied(mutations); } @Test - public void testAddNewMetadata() throws Exception { + public void addNewMetadata() throws Exception { ContentMetadataMutations mutations = new ContentMetadataMutations(); mutations.set("metadata name", "value"); contentMetadata = contentMetadata.copyWithMutationsApplied(mutations); @@ -70,7 +70,7 @@ public class DefaultContentMetadataTest { } @Test - public void testAddNewIntMetadata() throws Exception { + public void addNewIntMetadata() throws Exception { ContentMetadataMutations mutations = new ContentMetadataMutations(); mutations.set("metadata name", 5); contentMetadata = contentMetadata.copyWithMutationsApplied(mutations); @@ -78,7 +78,7 @@ public class DefaultContentMetadataTest { } @Test - public void testAddNewByteArrayMetadata() throws Exception { + public void addNewByteArrayMetadata() throws Exception { ContentMetadataMutations mutations = new ContentMetadataMutations(); byte[] value = {1, 2, 3}; mutations.set("metadata name", value); @@ -87,14 +87,14 @@ public class DefaultContentMetadataTest { } @Test - public void testNewMetadataNotWrittenBeforeCommitted() throws Exception { + public void newMetadataNotWrittenBeforeCommitted() throws Exception { ContentMetadataMutations mutations = new ContentMetadataMutations(); mutations.set("metadata name", "value"); assertThat(contentMetadata.get("metadata name", "default value")).isEqualTo("default value"); } @Test - public void testEditMetadata() throws Exception { + public void editMetadata() throws Exception { contentMetadata = createContentMetadata("metadata name", "value"); ContentMetadataMutations mutations = new ContentMetadataMutations(); mutations.set("metadata name", "edited value"); @@ -103,7 +103,7 @@ public class DefaultContentMetadataTest { } @Test - public void testRemoveMetadata() throws Exception { + public void removeMetadata() throws Exception { contentMetadata = createContentMetadata("metadata name", "value"); ContentMetadataMutations mutations = new ContentMetadataMutations(); mutations.remove("metadata name"); @@ -112,7 +112,7 @@ public class DefaultContentMetadataTest { } @Test - public void testAddAndRemoveMetadata() throws Exception { + public void addAndRemoveMetadata() throws Exception { ContentMetadataMutations mutations = new ContentMetadataMutations(); mutations.set("metadata name", "value"); mutations.remove("metadata name"); @@ -121,7 +121,7 @@ public class DefaultContentMetadataTest { } @Test - public void testRemoveAndAddMetadata() throws Exception { + public void removeAndAddMetadata() throws Exception { ContentMetadataMutations mutations = new ContentMetadataMutations(); mutations.remove("metadata name"); mutations.set("metadata name", "value"); @@ -130,14 +130,14 @@ public class DefaultContentMetadataTest { } @Test - public void testEqualsStringValues() throws Exception { + public void equalsStringValues() throws Exception { DefaultContentMetadata metadata1 = createContentMetadata("metadata1", "value"); DefaultContentMetadata metadata2 = createContentMetadata("metadata1", "value"); assertThat(metadata1).isEqualTo(metadata2); } @Test - public void testEquals() throws Exception { + public void equals() throws Exception { DefaultContentMetadata metadata1 = createContentMetadata( "metadata1", "value", "metadata2", 12345, "metadata3", new byte[] {1, 2, 3}); @@ -149,7 +149,7 @@ public class DefaultContentMetadataTest { } @Test - public void testNotEquals() throws Exception { + public void notEquals() throws Exception { DefaultContentMetadata metadata1 = createContentMetadata("metadata1", new byte[] {1, 2, 3}); DefaultContentMetadata metadata2 = createContentMetadata("metadata1", new byte[] {3, 2, 1}); assertThat(metadata1).isNotEqualTo(metadata2); diff --git a/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/LeastRecentlyUsedCacheEvictorTest.java b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/LeastRecentlyUsedCacheEvictorTest.java index 482174e3da..4984e71a3e 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/LeastRecentlyUsedCacheEvictorTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/LeastRecentlyUsedCacheEvictorTest.java @@ -32,7 +32,7 @@ public class LeastRecentlyUsedCacheEvictorTest { } @Test - public void testContentBiggerThanMaxSizeDoesNotThrowException() throws Exception { + public void contentBiggerThanMaxSizeDoesNotThrowException() throws Exception { int maxBytes = 100; LeastRecentlyUsedCacheEvictor evictor = new LeastRecentlyUsedCacheEvictor(maxBytes); evictor.onCacheInitialized(); diff --git a/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheSpanTest.java b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheSpanTest.java index 5908c7db20..fa56c31a89 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheSpanTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheSpanTest.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.upstream.cache; +import static com.google.android.exoplayer2.testutil.TestUtil.createTestFile; import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertWithMessage; @@ -24,7 +25,6 @@ import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.testutil.TestUtil; import com.google.android.exoplayer2.util.Util; import java.io.File; -import java.io.FileOutputStream; import java.io.IOException; import java.util.Set; import java.util.TreeSet; @@ -37,13 +37,6 @@ import org.junit.runner.RunWith; @RunWith(AndroidJUnit4.class) public class SimpleCacheSpanTest { - public static File createCacheSpanFile( - File cacheDir, int id, long offset, long length, long lastTouchTimestamp) throws IOException { - File cacheFile = SimpleCacheSpan.getCacheFile(cacheDir, id, offset, lastTouchTimestamp); - createTestFile(cacheFile, length); - return cacheFile; - } - private CachedContentIndex index; private File cacheDir; @@ -60,7 +53,7 @@ public class SimpleCacheSpanTest { } @Test - public void testCacheFile() throws Exception { + public void cacheFile() throws Exception { assertCacheSpan("key1", 0, 0); assertCacheSpan("key2", 1, 2); assertCacheSpan("<>:\"/\\|?*%", 1, 2); @@ -79,13 +72,13 @@ public class SimpleCacheSpanTest { } @Test - public void testUpgradeFileName() throws Exception { + public void upgradeFileName() throws Exception { String key = "abc%def"; int id = index.assignIdForKey(key); - File v3file = createTestFile(id + ".0.1.v3.exo"); - File v2file = createTestFile("abc%25def.1.2.v2.exo"); // %25 is '%' after escaping - File wrongEscapedV2file = createTestFile("abc%2Gdef.3.4.v2.exo"); // 2G is invalid hex - File v1File = createTestFile("abc%def.5.6.v1.exo"); // V1 did not escape + File v3file = createTestFile(cacheDir, id + ".0.1.v3.exo"); + File v2file = createTestFile(cacheDir, "abc%25def.1.2.v2.exo"); // %25 is '%' after escaping + File wrongEscapedV2file = createTestFile(cacheDir, "abc%2Gdef.3.4.v2.exo"); // 2G is invalid hex + File v1File = createTestFile(cacheDir, "abc%def.5.6.v1.exo"); // V1 did not escape for (File file : cacheDir.listFiles()) { SimpleCacheSpan cacheEntry = SimpleCacheSpan.createCacheEntry(file, file.length(), index); @@ -125,26 +118,12 @@ public class SimpleCacheSpanTest { assertThat(cachedPositions.get(5)).isEqualTo(6); } - private static void createTestFile(File file, long length) throws IOException { - FileOutputStream output = new FileOutputStream(file); - for (int i = 0; i < length; i++) { - output.write(i); - } - output.close(); - } - - private File createTestFile(String name) throws IOException { - File file = new File(cacheDir, name); - createTestFile(file, 1); - return file; - } - private void assertCacheSpan(String key, long offset, long lastTouchTimestamp) throws IOException { int id = index.assignIdForKey(key); - long cacheFileLength = 1; - File cacheFile = createCacheSpanFile(cacheDir, id, offset, cacheFileLength, lastTouchTimestamp); - SimpleCacheSpan cacheSpan = SimpleCacheSpan.createCacheEntry(cacheFile, cacheFileLength, index); + File cacheFile = SimpleCacheSpan.getCacheFile(cacheDir, id, offset, lastTouchTimestamp); + createTestFile(cacheFile, /* length= */ 1); + SimpleCacheSpan cacheSpan = SimpleCacheSpan.createCacheEntry(cacheFile, /* length= */ 1, index); String message = cacheFile.toString(); assertWithMessage(message).that(cacheSpan).isNotNull(); assertWithMessage(message).that(cacheFile.getParentFile()).isEqualTo(cacheDir); diff --git a/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheTest.java b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheTest.java index a8dbfe3b42..14222f144d 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheTest.java @@ -63,7 +63,7 @@ public class SimpleCacheTest { } @Test - public void testCacheInitialization() { + public void cacheInitialization() { SimpleCache cache = getSimpleCache(); // Cache initialization should have created a non-negative UID. @@ -79,7 +79,7 @@ public class SimpleCacheTest { } @Test - public void testCacheInitializationError() throws IOException { + public void cacheInitializationError() throws IOException { // Creating a file where the cache should be will cause an error during initialization. assertThat(cacheDir.createNewFile()).isTrue(); @@ -90,7 +90,7 @@ public class SimpleCacheTest { } @Test - public void testCommittingOneFile() throws Exception { + public void committingOneFile() throws Exception { SimpleCache simpleCache = getSimpleCache(); CacheSpan cacheSpan1 = simpleCache.startReadWrite(KEY_1, 0); @@ -122,7 +122,7 @@ public class SimpleCacheTest { } @Test - public void testReadCacheWithoutReleasingWriteCacheSpan() throws Exception { + public void readCacheWithoutReleasingWriteCacheSpan() throws Exception { SimpleCache simpleCache = getSimpleCache(); CacheSpan cacheSpan1 = simpleCache.startReadWrite(KEY_1, 0); @@ -133,7 +133,7 @@ public class SimpleCacheTest { } @Test - public void testSetGetContentMetadata() throws Exception { + public void setGetContentMetadata() throws Exception { SimpleCache simpleCache = getSimpleCache(); assertThat(ContentMetadata.getContentLength(simpleCache.getContentMetadata(KEY_1))) @@ -173,7 +173,7 @@ public class SimpleCacheTest { } @Test - public void testReloadCache() throws Exception { + public void reloadCache() throws Exception { SimpleCache simpleCache = getSimpleCache(); // write data @@ -191,7 +191,7 @@ public class SimpleCacheTest { } @Test - public void testReloadCacheWithoutRelease() throws Exception { + public void reloadCacheWithoutRelease() throws Exception { SimpleCache simpleCache = getSimpleCache(); // Write data for KEY_1. @@ -204,7 +204,7 @@ public class SimpleCacheTest { simpleCache.releaseHoleSpan(cacheSpan2); simpleCache.removeSpan(simpleCache.getCachedSpans(KEY_2).first()); - // Don't release the cache. This means the index file wont have been written to disk after the + // Don't release the cache. This means the index file won't have been written to disk after the // data for KEY_2 was removed. Move the cache instead, so we can reload it without failing the // folder locking check. File cacheDir2 = @@ -226,7 +226,7 @@ public class SimpleCacheTest { } @Test - public void testEncryptedIndex() throws Exception { + public void encryptedIndex() throws Exception { byte[] key = Util.getUtf8Bytes("Bar12345Bar12345"); // 128 bit key SimpleCache simpleCache = getEncryptedSimpleCache(key); @@ -245,7 +245,7 @@ public class SimpleCacheTest { } @Test - public void testEncryptedIndexWrongKey() throws Exception { + public void encryptedIndexWrongKey() throws Exception { byte[] key = Util.getUtf8Bytes("Bar12345Bar12345"); // 128 bit key SimpleCache simpleCache = getEncryptedSimpleCache(key); @@ -265,7 +265,7 @@ public class SimpleCacheTest { } @Test - public void testEncryptedIndexLostKey() throws Exception { + public void encryptedIndexLostKey() throws Exception { byte[] key = Util.getUtf8Bytes("Bar12345Bar12345"); // 128 bit key SimpleCache simpleCache = getEncryptedSimpleCache(key); @@ -284,7 +284,7 @@ public class SimpleCacheTest { } @Test - public void testGetCachedLength() throws Exception { + public void getCachedLength() throws Exception { SimpleCache simpleCache = getSimpleCache(); CacheSpan cacheSpan = simpleCache.startReadWrite(KEY_1, 0); @@ -320,7 +320,7 @@ public class SimpleCacheTest { /* Tests https://github.com/google/ExoPlayer/issues/3260 case. */ @Test - public void testExceptionDuringEvictionByLeastRecentlyUsedCacheEvictorNotHang() throws Exception { + public void exceptionDuringEvictionByLeastRecentlyUsedCacheEvictorNotHang() throws Exception { CachedContentIndex contentIndex = Mockito.spy(new CachedContentIndex(TestUtil.getInMemoryDatabaseProvider())); SimpleCache simpleCache = @@ -357,7 +357,7 @@ public class SimpleCacheTest { } @Test - public void testUsingReleasedSimpleCacheThrowsException() throws Exception { + public void usingReleasedSimpleCacheThrowsException() throws Exception { SimpleCache simpleCache = new SimpleCache(cacheDir, new NoOpCacheEvictor()); simpleCache.release(); @@ -370,7 +370,7 @@ public class SimpleCacheTest { } @Test - public void testMultipleSimpleCacheWithSameCacheDirThrowsException() throws Exception { + public void multipleSimpleCacheWithSameCacheDirThrowsException() throws Exception { new SimpleCache(cacheDir, new NoOpCacheEvictor()); try { @@ -382,7 +382,7 @@ public class SimpleCacheTest { } @Test - public void testMultipleSimpleCacheWithSameCacheDirDoesNotThrowsExceptionAfterRelease() + public void multipleSimpleCacheWithSameCacheDirDoesNotThrowsExceptionAfterRelease() throws Exception { SimpleCache simpleCache = new SimpleCache(cacheDir, new NoOpCacheEvictor()); simpleCache.release(); diff --git a/library/core/src/test/java/com/google/android/exoplayer2/upstream/crypto/AesFlushingCipherTest.java b/library/core/src/test/java/com/google/android/exoplayer2/upstream/crypto/AesFlushingCipherTest.java index fde2bf5a30..17e69db26b 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/upstream/crypto/AesFlushingCipherTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/upstream/crypto/AesFlushingCipherTest.java @@ -76,7 +76,7 @@ public class AesFlushingCipherTest { // Test a single encrypt and decrypt call. @Test - public void testSingle() { + public void single() { byte[] reference = TestUtil.buildTestData(DATA_LENGTH); byte[] data = reference.clone(); @@ -92,7 +92,7 @@ public class AesFlushingCipherTest { // Test several encrypt and decrypt calls, each aligned on a 16 byte block size. @Test - public void testAligned() { + public void aligned() { byte[] reference = TestUtil.buildTestData(DATA_LENGTH); byte[] data = reference.clone(); Random random = new Random(RANDOM_SEED); @@ -125,7 +125,7 @@ public class AesFlushingCipherTest { // Test several encrypt and decrypt calls, not aligned on block boundary. @Test - public void testUnAligned() { + public void unAligned() { byte[] reference = TestUtil.buildTestData(DATA_LENGTH); byte[] data = reference.clone(); Random random = new Random(RANDOM_SEED); @@ -157,7 +157,7 @@ public class AesFlushingCipherTest { // Test decryption starting from the middle of an encrypted block. @Test - public void testMidJoin() { + public void midJoin() { byte[] reference = TestUtil.buildTestData(DATA_LENGTH); byte[] data = reference.clone(); Random random = new Random(RANDOM_SEED); diff --git a/library/core/src/test/java/com/google/android/exoplayer2/util/AtomicFileTest.java b/library/core/src/test/java/com/google/android/exoplayer2/util/AtomicFileTest.java index c0bf459be8..cab14c2c31 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/util/AtomicFileTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/util/AtomicFileTest.java @@ -50,14 +50,14 @@ public final class AtomicFileTest { } @Test - public void testDelete() throws Exception { + public void delete() throws Exception { assertThat(file.createNewFile()).isTrue(); atomicFile.delete(); assertThat(file.exists()).isFalse(); } @Test - public void testWriteRead() throws Exception { + public void writeRead() throws Exception { OutputStream output = atomicFile.startWrite(); output.write(5); atomicFile.endWrite(output); diff --git a/library/core/src/test/java/com/google/android/exoplayer2/util/ColorParserTest.java b/library/core/src/test/java/com/google/android/exoplayer2/util/ColorParserTest.java index 2a1c59e7df..c2f165dec1 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/util/ColorParserTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/util/ColorParserTest.java @@ -35,29 +35,29 @@ public final class ColorParserTest { // Negative tests. @Test(expected = IllegalArgumentException.class) - public void testParseUnknownColor() { + public void parseUnknownColor() { ColorParser.parseTtmlColor("colorOfAnElectron"); } @Test(expected = IllegalArgumentException.class) - public void testParseNull() { + public void parseNull() { ColorParser.parseTtmlColor(null); } @Test(expected = IllegalArgumentException.class) - public void testParseEmpty() { + public void parseEmpty() { ColorParser.parseTtmlColor(""); } @Test(expected = IllegalArgumentException.class) - public void testRgbColorParsingRgbValuesNegative() { + public void rgbColorParsingRgbValuesNegative() { ColorParser.parseTtmlColor("rgb(-4, 55, 209)"); } // Positive tests. @Test - public void testHexCodeParsing() { + public void hexCodeParsing() { assertThat(parseTtmlColor("#FFFFFF")).isEqualTo(WHITE); assertThat(parseTtmlColor("#FFFFFFFF")).isEqualTo(WHITE); assertThat(parseTtmlColor("#123456")).isEqualTo(parseColor("#FF123456")); @@ -67,14 +67,14 @@ public final class ColorParserTest { } @Test - public void testRgbColorParsing() { + public void rgbColorParsing() { assertThat(parseTtmlColor("rgb(255,255,255)")).isEqualTo(WHITE); // Spaces are ignored. assertThat(parseTtmlColor(" rgb ( 255, 255, 255)")).isEqualTo(WHITE); } @Test - public void testRgbColorParsingRgbValuesOutOfBounds() { + public void rgbColorParsingRgbValuesOutOfBounds() { int outOfBounds = ColorParser.parseTtmlColor("rgb(999, 999, 999)"); int color = Color.rgb(999, 999, 999); // Behave like the framework does. @@ -82,7 +82,7 @@ public final class ColorParserTest { } @Test - public void testRgbaColorParsing() { + public void rgbaColorParsing() { assertThat(parseTtmlColor("rgba(255,255,255,255)")).isEqualTo(WHITE); assertThat(parseTtmlColor("rgba(255,255,255,255)")) .isEqualTo(argb(255, 255, 255, 255)); diff --git a/library/core/src/test/java/com/google/android/exoplayer2/util/ConditionVariableTest.java b/library/core/src/test/java/com/google/android/exoplayer2/util/ConditionVariableTest.java new file mode 100644 index 0000000000..8f2fb2ed14 --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/util/ConditionVariableTest.java @@ -0,0 +1,145 @@ +/* + * Copyright (C) 2020 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 static com.google.common.truth.Truth.assertThat; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import java.util.concurrent.atomic.AtomicBoolean; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Unit test for {@link ConditionVariableTest}. */ +@RunWith(AndroidJUnit4.class) +public class ConditionVariableTest { + + @Test + public void initialState_isClosed() { + ConditionVariable conditionVariable = buildTestConditionVariable(); + assertThat(conditionVariable.isOpen()).isFalse(); + } + + @Test + public void blockWithTimeout_timesOut() throws InterruptedException { + ConditionVariable conditionVariable = buildTestConditionVariable(); + assertThat(conditionVariable.block(1)).isFalse(); + assertThat(conditionVariable.isOpen()).isFalse(); + } + + @Test + public void blockWithTimeout_blocksForAtLeastTimeout() throws InterruptedException { + ConditionVariable conditionVariable = buildTestConditionVariable(); + long startTimeMs = System.currentTimeMillis(); + assertThat(conditionVariable.block(/* timeoutMs= */ 500)).isFalse(); + long endTimeMs = System.currentTimeMillis(); + assertThat(endTimeMs - startTimeMs).isAtLeast(500); + } + + @Test + public void blockWithoutTimeout_blocks() throws InterruptedException { + ConditionVariable conditionVariable = buildTestConditionVariable(); + + AtomicBoolean blockReturned = new AtomicBoolean(); + AtomicBoolean blockWasInterrupted = new AtomicBoolean(); + Thread blockingThread = + new Thread( + () -> { + try { + conditionVariable.block(); + blockReturned.set(true); + } catch (InterruptedException e) { + blockWasInterrupted.set(true); + } + }); + + blockingThread.start(); + Thread.sleep(500); + assertThat(blockReturned.get()).isFalse(); + + blockingThread.interrupt(); + blockingThread.join(); + assertThat(blockWasInterrupted.get()).isTrue(); + assertThat(conditionVariable.isOpen()).isFalse(); + } + + @Test + public void blockWithMaxTimeout_blocks() throws InterruptedException { + ConditionVariable conditionVariable = buildTestConditionVariable(); + + AtomicBoolean blockReturned = new AtomicBoolean(); + AtomicBoolean blockWasInterrupted = new AtomicBoolean(); + Thread blockingThread = + new Thread( + () -> { + try { + conditionVariable.block(/* timeoutMs= */ Long.MAX_VALUE); + blockReturned.set(true); + } catch (InterruptedException e) { + blockWasInterrupted.set(true); + } + }); + + blockingThread.start(); + Thread.sleep(500); + assertThat(blockReturned.get()).isFalse(); + + blockingThread.interrupt(); + blockingThread.join(); + assertThat(blockWasInterrupted.get()).isTrue(); + assertThat(conditionVariable.isOpen()).isFalse(); + } + + @Test + public void open_unblocksBlock() throws InterruptedException { + ConditionVariable conditionVariable = buildTestConditionVariable(); + + AtomicBoolean blockReturned = new AtomicBoolean(); + AtomicBoolean blockWasInterrupted = new AtomicBoolean(); + Thread blockingThread = + new Thread( + () -> { + try { + conditionVariable.block(); + blockReturned.set(true); + } catch (InterruptedException e) { + blockWasInterrupted.set(true); + } + }); + + blockingThread.start(); + Thread.sleep(500); + assertThat(blockReturned.get()).isFalse(); + + conditionVariable.open(); + blockingThread.join(); + assertThat(blockReturned.get()).isTrue(); + assertThat(conditionVariable.isOpen()).isTrue(); + } + + private static ConditionVariable buildTestConditionVariable() { + return new ConditionVariable( + new SystemClock() { + @Override + public long elapsedRealtime() { + // elapsedRealtime() does not advance during Robolectric test execution, so use + // currentTimeMillis() instead. This is technically unsafe because this clock is not + // guaranteed to be monotonic, but in practice it will work provided the clock of the + // host machine does not change during test execution. + return Clock.DEFAULT.currentTimeMillis(); + } + }); + } +} diff --git a/library/core/src/test/java/com/google/android/exoplayer2/util/MediaSourceEventDispatcherTest.java b/library/core/src/test/java/com/google/android/exoplayer2/util/MediaSourceEventDispatcherTest.java new file mode 100644 index 0000000000..5e5a6be7c6 --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/util/MediaSourceEventDispatcherTest.java @@ -0,0 +1,222 @@ +/* + * Copyright (C) 2020 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 static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; + +import android.os.Handler; +import android.os.Looper; +import androidx.annotation.Nullable; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.android.exoplayer2.drm.DrmSessionEventListener; +import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; +import com.google.android.exoplayer2.source.MediaSourceEventListener; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +/** Tests for {@link MediaSourceEventDispatcher}. */ +@RunWith(AndroidJUnit4.class) +public class MediaSourceEventDispatcherTest { + + private static final MediaSource.MediaPeriodId MEDIA_PERIOD_ID = + new MediaSource.MediaPeriodId("test uid"); + private static final int WINDOW_INDEX = 200; + private static final int MEDIA_TIME_OFFSET_MS = 1_000; + + @Rule public final MockitoRule mockito = MockitoJUnit.rule(); + + @Mock private MediaSourceEventListener mediaSourceEventListener; + @Mock private MediaAndDrmEventListener mediaAndDrmEventListener; + + private MediaSourceEventDispatcher eventDispatcher; + + @Before + public void setupEventDispatcher() { + eventDispatcher = new MediaSourceEventDispatcher(); + eventDispatcher = + eventDispatcher.withParameters(WINDOW_INDEX, MEDIA_PERIOD_ID, MEDIA_TIME_OFFSET_MS); + } + + @Test + public void listenerReceivesEventPopulatedWithMediaPeriodInfo() { + eventDispatcher.addEventListener( + Util.createHandler(), mediaSourceEventListener, MediaSourceEventListener.class); + + eventDispatcher.dispatch( + MediaSourceEventListener::onMediaPeriodCreated, MediaSourceEventListener.class); + + verify(mediaSourceEventListener).onMediaPeriodCreated(WINDOW_INDEX, MEDIA_PERIOD_ID); + } + + @Test + public void sameListenerObjectRegisteredTwiceOnlyReceivesEventsOnce() { + eventDispatcher.addEventListener( + Util.createHandler(), mediaSourceEventListener, MediaSourceEventListener.class); + eventDispatcher.addEventListener( + Util.createHandler(), mediaSourceEventListener, MediaSourceEventListener.class); + + eventDispatcher.dispatch( + MediaSourceEventListener::onMediaPeriodCreated, MediaSourceEventListener.class); + + verify(mediaSourceEventListener).onMediaPeriodCreated(WINDOW_INDEX, MEDIA_PERIOD_ID); + } + + @Test + public void sameListenerInstanceCanBeRegisteredWithTwoTypes() { + eventDispatcher.addEventListener( + new Handler(Looper.getMainLooper()), + mediaAndDrmEventListener, + MediaSourceEventListener.class); + eventDispatcher.addEventListener( + new Handler(Looper.getMainLooper()), + mediaAndDrmEventListener, + DrmSessionEventListener.class); + + eventDispatcher.dispatch( + MediaSourceEventListener::onMediaPeriodCreated, MediaSourceEventListener.class); + eventDispatcher.dispatch( + (listener, windowIndex, mediaPeriodId) -> listener.onDrmKeysLoaded(), + DrmSessionEventListener.class); + + verify(mediaAndDrmEventListener).onMediaPeriodCreated(WINDOW_INDEX, MEDIA_PERIOD_ID); + verify(mediaAndDrmEventListener).onDrmKeysLoaded(); + } + + // If a listener is added that implements multiple types, it should only receive events for the + // type specified at registration time. + @Test + public void listenerOnlyReceivesEventsForRegisteredType() { + eventDispatcher.addEventListener( + new Handler(Looper.getMainLooper()), + mediaAndDrmEventListener, + MediaSourceEventListener.class); + + eventDispatcher.dispatch( + MediaSourceEventListener::onMediaPeriodCreated, MediaSourceEventListener.class); + eventDispatcher.dispatch( + (listener, windowIndex, mediaPeriodId) -> listener.onDrmKeysLoaded(), + DrmSessionEventListener.class); + + verify(mediaAndDrmEventListener).onMediaPeriodCreated(WINDOW_INDEX, MEDIA_PERIOD_ID); + verify(mediaAndDrmEventListener, never()).onDrmKeysLoaded(); + } + + @Test + public void listenerDoesntReceiveEventsDispatchedToSubclass() { + SubclassListener subclassListener = mock(SubclassListener.class); + eventDispatcher.addEventListener( + new Handler(Looper.getMainLooper()), subclassListener, MediaSourceEventListener.class); + + eventDispatcher.dispatch(SubclassListener::subclassMethod, SubclassListener.class); + + // subclassListener can handle the call to subclassMethod, but it isn't called because + // it was registered 'as-a' MediaSourceEventListener, not SubclassListener. + verify(subclassListener, never()).subclassMethod(anyInt(), any()); + } + + @Test + public void listenerDoesntReceiveEventsDispatchedToSuperclass() { + SubclassListener subclassListener = mock(SubclassListener.class); + eventDispatcher.addEventListener( + new Handler(Looper.getMainLooper()), subclassListener, SubclassListener.class); + + eventDispatcher.dispatch( + MediaSourceEventListener::onMediaPeriodCreated, MediaSourceEventListener.class); + + // subclassListener 'is-a' a MediaSourceEventListener, but it isn't called because the event + // is dispatched specifically to listeners registered as MediaSourceEventListener. + verify(subclassListener, never()).onMediaPeriodCreated(anyInt(), any()); + } + + @Test + public void listenersAreCopiedToNewDispatcher() { + eventDispatcher.addEventListener( + Util.createHandler(), mediaSourceEventListener, MediaSourceEventListener.class); + + MediaSource.MediaPeriodId newPeriodId = new MediaSource.MediaPeriodId("different uid"); + MediaSourceEventDispatcher newEventDispatcher = + this.eventDispatcher.withParameters( + /* windowIndex= */ 250, newPeriodId, /* mediaTimeOffsetMs= */ 500); + + newEventDispatcher.dispatch( + MediaSourceEventListener::onMediaPeriodCreated, MediaSourceEventListener.class); + + verify(mediaSourceEventListener).onMediaPeriodCreated(250, newPeriodId); + } + + @Test + public void removingListenerStopsEventDispatch() { + eventDispatcher.addEventListener( + Util.createHandler(), mediaSourceEventListener, MediaSourceEventListener.class); + eventDispatcher.removeEventListener(mediaSourceEventListener, MediaSourceEventListener.class); + + eventDispatcher.dispatch( + MediaSourceEventListener::onMediaPeriodCreated, MediaSourceEventListener.class); + + verify(mediaSourceEventListener, never()).onMediaPeriodCreated(anyInt(), any()); + } + + @Test + public void removingListenerWithDifferentTypeToRegistrationDoesntRemove() { + eventDispatcher.addEventListener( + Util.createHandler(), mediaAndDrmEventListener, MediaSourceEventListener.class); + eventDispatcher.removeEventListener(mediaAndDrmEventListener, DrmSessionEventListener.class); + + eventDispatcher.dispatch( + MediaSourceEventListener::onMediaPeriodCreated, MediaSourceEventListener.class); + + verify(mediaAndDrmEventListener).onMediaPeriodCreated(WINDOW_INDEX, MEDIA_PERIOD_ID); + } + + @Test + public void listenersAreCountedBasedOnListenerAndType() { + // Add the listener twice and remove it once. + eventDispatcher.addEventListener( + Util.createHandler(), mediaSourceEventListener, MediaSourceEventListener.class); + eventDispatcher.addEventListener( + Util.createHandler(), mediaSourceEventListener, MediaSourceEventListener.class); + eventDispatcher.removeEventListener(mediaSourceEventListener, MediaSourceEventListener.class); + + eventDispatcher.dispatch( + MediaSourceEventListener::onMediaPeriodCreated, MediaSourceEventListener.class); + + verify(mediaSourceEventListener).onMediaPeriodCreated(WINDOW_INDEX, MEDIA_PERIOD_ID); + + // Remove it a second time and confirm the events stop being propagated. + eventDispatcher.removeEventListener(mediaSourceEventListener, MediaSourceEventListener.class); + + verifyNoMoreInteractions(mediaSourceEventListener); + } + + private interface MediaAndDrmEventListener + extends MediaSourceEventListener, DrmSessionEventListener {} + + private interface SubclassListener extends MediaSourceEventListener { + void subclassMethod(int windowIndex, @Nullable MediaPeriodId mediaPeriodId); + } +} diff --git a/library/core/src/test/java/com/google/android/exoplayer2/util/ReusableBufferedOutputStreamTest.java b/library/core/src/test/java/com/google/android/exoplayer2/util/ReusableBufferedOutputStreamTest.java index 77f59962d0..6ab273bf5b 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/util/ReusableBufferedOutputStreamTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/util/ReusableBufferedOutputStreamTest.java @@ -30,7 +30,7 @@ public final class ReusableBufferedOutputStreamTest { private static final byte[] TEST_DATA_2 = Util.getUtf8Bytes("2 test data"); @Test - public void testReset() throws Exception { + public void reset() throws Exception { ByteArrayOutputStream byteArrayOutputStream1 = new ByteArrayOutputStream(1000); ReusableBufferedOutputStream outputStream = new ReusableBufferedOutputStream( byteArrayOutputStream1, 1000); diff --git a/library/core/src/test/java/com/google/android/exoplayer2/util/TimedValueQueueTest.java b/library/core/src/test/java/com/google/android/exoplayer2/util/TimedValueQueueTest.java index f21e15a0ed..8f1949f96e 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/util/TimedValueQueueTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/util/TimedValueQueueTest.java @@ -34,7 +34,7 @@ public class TimedValueQueueTest { } @Test - public void testAddAndPollValues() { + public void addAndPollValues() { queue.add(0, "a"); queue.add(1, "b"); queue.add(2, "c"); @@ -44,7 +44,7 @@ public class TimedValueQueueTest { } @Test - public void testBufferCapacityIncreasesAutomatically() { + public void bufferCapacityIncreasesAutomatically() { queue = new TimedValueQueue<>(1); for (int i = 0; i < 20; i++) { queue.add(i, "" + i); @@ -56,7 +56,7 @@ public class TimedValueQueueTest { } @Test - public void testTimeDiscontinuityClearsValues() { + public void timeDiscontinuityClearsValues() { queue.add(1, "b"); queue.add(2, "c"); queue.add(0, "a"); @@ -65,7 +65,7 @@ public class TimedValueQueueTest { } @Test - public void testTimeDiscontinuityOnFullBufferClearsValues() { + public void timeDiscontinuityOnFullBufferClearsValues() { queue = new TimedValueQueue<>(2); queue.add(1, "b"); queue.add(3, "c"); @@ -75,7 +75,7 @@ public class TimedValueQueueTest { } @Test - public void testPollReturnsClosestValue() { + public void pollReturnsClosestValue() { queue.add(0, "a"); queue.add(3, "b"); assertThat(queue.poll(2)).isEqualTo("b"); @@ -83,7 +83,7 @@ public class TimedValueQueueTest { } @Test - public void testPollRemovesPreviousValues() { + public void pollRemovesPreviousValues() { queue.add(0, "a"); queue.add(1, "b"); queue.add(2, "c"); @@ -92,7 +92,7 @@ public class TimedValueQueueTest { } @Test - public void testPollFloorReturnsClosestPreviousValue() { + public void pollFloorReturnsClosestPreviousValue() { queue.add(0, "a"); queue.add(3, "b"); assertThat(queue.pollFloor(2)).isEqualTo("a"); @@ -102,7 +102,7 @@ public class TimedValueQueueTest { } @Test - public void testPollFloorRemovesPreviousValues() { + public void pollFloorRemovesPreviousValues() { queue.add(0, "a"); queue.add(1, "b"); queue.add(2, "c"); diff --git a/library/core/src/test/java/com/google/android/exoplayer2/util/UriUtilTest.java b/library/core/src/test/java/com/google/android/exoplayer2/util/UriUtilTest.java index 95d8a6b9a0..6d1c27c518 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/util/UriUtilTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/util/UriUtilTest.java @@ -30,11 +30,11 @@ public final class UriUtilTest { /** * Tests normal usage of {@link UriUtil#resolve(String, String)}. - *

        - * The test cases are taken from RFC-3986 5.4.1. + * + *

        The test cases are taken from RFC-3986 5.4.1. */ @Test - public void testResolveNormal() { + public void resolveNormal() { String base = "http://a/b/c/d;p?q"; assertThat(resolve(base, "g:h")).isEqualTo("g:h"); @@ -63,11 +63,11 @@ public final class UriUtilTest { /** * Tests abnormal usage of {@link UriUtil#resolve(String, String)}. - *

        - * The test cases are taken from RFC-3986 5.4.2. + * + *

        The test cases are taken from RFC-3986 5.4.2. */ @Test - public void testResolveAbnormal() { + public void resolveAbnormal() { String base = "http://a/b/c/d;p?q"; assertThat(resolve(base, "../../../g")).isEqualTo("http://a/g"); @@ -95,11 +95,9 @@ public final class UriUtilTest { assertThat(resolve(base, "http:g")).isEqualTo("http:g"); } - /** - * Tests additional abnormal usage of {@link UriUtil#resolve(String, String)}. - */ + /** Tests additional abnormal usage of {@link UriUtil#resolve(String, String)}. */ @Test - public void testResolveAbnormalAdditional() { + public void resolveAbnormalAdditional() { assertThat(resolve("http://a/b", "c:d/../e")).isEqualTo("c:e"); assertThat(resolve("a:b", "../c")).isEqualTo("a:c"); } 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 deleted file mode 100644 index 172fdf31ad..0000000000 --- a/library/core/src/test/java/com/google/android/exoplayer2/util/UtilTest.java +++ /dev/null @@ -1,385 +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.util; - -import static com.google.android.exoplayer2.util.Util.binarySearchCeil; -import static com.google.android.exoplayer2.util.Util.binarySearchFloor; -import static com.google.android.exoplayer2.util.Util.escapeFileName; -import static com.google.android.exoplayer2.util.Util.getCodecsOfType; -import static com.google.android.exoplayer2.util.Util.parseXsDateTime; -import static com.google.android.exoplayer2.util.Util.parseXsDuration; -import static com.google.android.exoplayer2.util.Util.unescapeFileName; -import static com.google.common.truth.Truth.assertThat; - -import androidx.test.ext.junit.runners.AndroidJUnit4; -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.annotation.Config; - -/** Unit tests for {@link Util}. */ -@RunWith(AndroidJUnit4.class) -public class UtilTest { - - @Test - public void testAddWithOverflowDefault() { - long res = Util.addWithOverflowDefault(5, 10, /* overflowResult= */ 0); - assertThat(res).isEqualTo(15); - - res = Util.addWithOverflowDefault(Long.MAX_VALUE - 1, 1, /* overflowResult= */ 12345); - assertThat(res).isEqualTo(Long.MAX_VALUE); - - res = Util.addWithOverflowDefault(Long.MIN_VALUE + 1, -1, /* overflowResult= */ 12345); - assertThat(res).isEqualTo(Long.MIN_VALUE); - - res = Util.addWithOverflowDefault(Long.MAX_VALUE, 1, /* overflowResult= */ 12345); - assertThat(res).isEqualTo(12345); - - res = Util.addWithOverflowDefault(Long.MIN_VALUE, -1, /* overflowResult= */ 12345); - assertThat(res).isEqualTo(12345); - } - - @Test - public void testSubtrackWithOverflowDefault() { - long res = Util.subtractWithOverflowDefault(5, 10, /* overflowResult= */ 0); - assertThat(res).isEqualTo(-5); - - res = Util.subtractWithOverflowDefault(Long.MIN_VALUE + 1, 1, /* overflowResult= */ 12345); - assertThat(res).isEqualTo(Long.MIN_VALUE); - - res = Util.subtractWithOverflowDefault(Long.MAX_VALUE - 1, -1, /* overflowResult= */ 12345); - assertThat(res).isEqualTo(Long.MAX_VALUE); - - res = Util.subtractWithOverflowDefault(Long.MIN_VALUE, 1, /* overflowResult= */ 12345); - assertThat(res).isEqualTo(12345); - - res = Util.subtractWithOverflowDefault(Long.MAX_VALUE, -1, /* overflowResult= */ 12345); - assertThat(res).isEqualTo(12345); - } - - @Test - public void testInferContentType() { - assertThat(Util.inferContentType("http://a.b/c.ism")).isEqualTo(C.TYPE_SS); - assertThat(Util.inferContentType("http://a.b/c.isml")).isEqualTo(C.TYPE_SS); - assertThat(Util.inferContentType("http://a.b/c.ism/Manifest")).isEqualTo(C.TYPE_SS); - assertThat(Util.inferContentType("http://a.b/c.isml/manifest")).isEqualTo(C.TYPE_SS); - assertThat(Util.inferContentType("http://a.b/c.isml/manifest(filter=x)")).isEqualTo(C.TYPE_SS); - - assertThat(Util.inferContentType("http://a.b/c.ism/prefix-manifest")).isEqualTo(C.TYPE_OTHER); - assertThat(Util.inferContentType("http://a.b/c.ism/manifest-suffix")).isEqualTo(C.TYPE_OTHER); - } - - @Test - public void testArrayBinarySearchFloor() { - long[] values = new long[0]; - assertThat(binarySearchFloor(values, 0, false, false)).isEqualTo(-1); - assertThat(binarySearchFloor(values, 0, false, true)).isEqualTo(0); - - values = new long[] {1, 3, 5}; - assertThat(binarySearchFloor(values, 0, false, false)).isEqualTo(-1); - assertThat(binarySearchFloor(values, 0, true, false)).isEqualTo(-1); - assertThat(binarySearchFloor(values, 0, false, true)).isEqualTo(0); - assertThat(binarySearchFloor(values, 0, true, true)).isEqualTo(0); - - assertThat(binarySearchFloor(values, 1, false, false)).isEqualTo(-1); - assertThat(binarySearchFloor(values, 1, true, false)).isEqualTo(0); - assertThat(binarySearchFloor(values, 1, false, true)).isEqualTo(0); - assertThat(binarySearchFloor(values, 1, true, true)).isEqualTo(0); - - assertThat(binarySearchFloor(values, 4, false, false)).isEqualTo(1); - assertThat(binarySearchFloor(values, 4, true, false)).isEqualTo(1); - - assertThat(binarySearchFloor(values, 5, false, false)).isEqualTo(1); - assertThat(binarySearchFloor(values, 5, true, false)).isEqualTo(2); - - assertThat(binarySearchFloor(values, 6, false, false)).isEqualTo(2); - assertThat(binarySearchFloor(values, 6, true, false)).isEqualTo(2); - } - - @Test - public void testListBinarySearchFloor() { - List values = new ArrayList<>(); - assertThat(binarySearchFloor(values, 0, false, false)).isEqualTo(-1); - assertThat(binarySearchFloor(values, 0, false, true)).isEqualTo(0); - - values.add(1); - values.add(3); - values.add(5); - assertThat(binarySearchFloor(values, 0, false, false)).isEqualTo(-1); - assertThat(binarySearchFloor(values, 0, true, false)).isEqualTo(-1); - assertThat(binarySearchFloor(values, 0, false, true)).isEqualTo(0); - assertThat(binarySearchFloor(values, 0, true, true)).isEqualTo(0); - - assertThat(binarySearchFloor(values, 1, false, false)).isEqualTo(-1); - assertThat(binarySearchFloor(values, 1, true, false)).isEqualTo(0); - assertThat(binarySearchFloor(values, 1, false, true)).isEqualTo(0); - assertThat(binarySearchFloor(values, 1, true, true)).isEqualTo(0); - - assertThat(binarySearchFloor(values, 4, false, false)).isEqualTo(1); - assertThat(binarySearchFloor(values, 4, true, false)).isEqualTo(1); - - assertThat(binarySearchFloor(values, 5, false, false)).isEqualTo(1); - assertThat(binarySearchFloor(values, 5, true, false)).isEqualTo(2); - - assertThat(binarySearchFloor(values, 6, false, false)).isEqualTo(2); - assertThat(binarySearchFloor(values, 6, true, false)).isEqualTo(2); - } - - @Test - public void testArrayBinarySearchCeil() { - long[] values = new long[0]; - assertThat(binarySearchCeil(values, 0, false, false)).isEqualTo(0); - assertThat(binarySearchCeil(values, 0, false, true)).isEqualTo(-1); - - values = new long[] {1, 3, 5}; - assertThat(binarySearchCeil(values, 0, false, false)).isEqualTo(0); - assertThat(binarySearchCeil(values, 0, true, false)).isEqualTo(0); - - assertThat(binarySearchCeil(values, 1, false, false)).isEqualTo(1); - assertThat(binarySearchCeil(values, 1, true, false)).isEqualTo(0); - - assertThat(binarySearchCeil(values, 2, false, false)).isEqualTo(1); - assertThat(binarySearchCeil(values, 2, true, false)).isEqualTo(1); - - assertThat(binarySearchCeil(values, 5, false, false)).isEqualTo(3); - assertThat(binarySearchCeil(values, 5, true, false)).isEqualTo(2); - assertThat(binarySearchCeil(values, 5, false, true)).isEqualTo(2); - assertThat(binarySearchCeil(values, 5, true, true)).isEqualTo(2); - - assertThat(binarySearchCeil(values, 6, false, false)).isEqualTo(3); - assertThat(binarySearchCeil(values, 6, true, false)).isEqualTo(3); - assertThat(binarySearchCeil(values, 6, false, true)).isEqualTo(2); - assertThat(binarySearchCeil(values, 6, true, true)).isEqualTo(2); - } - - @Test - public void testListBinarySearchCeil() { - List values = new ArrayList<>(); - assertThat(binarySearchCeil(values, 0, false, false)).isEqualTo(0); - assertThat(binarySearchCeil(values, 0, false, true)).isEqualTo(-1); - - values.add(1); - values.add(3); - values.add(5); - assertThat(binarySearchCeil(values, 0, false, false)).isEqualTo(0); - assertThat(binarySearchCeil(values, 0, true, false)).isEqualTo(0); - - assertThat(binarySearchCeil(values, 1, false, false)).isEqualTo(1); - assertThat(binarySearchCeil(values, 1, true, false)).isEqualTo(0); - - assertThat(binarySearchCeil(values, 2, false, false)).isEqualTo(1); - assertThat(binarySearchCeil(values, 2, true, false)).isEqualTo(1); - - assertThat(binarySearchCeil(values, 5, false, false)).isEqualTo(3); - assertThat(binarySearchCeil(values, 5, true, false)).isEqualTo(2); - assertThat(binarySearchCeil(values, 5, false, true)).isEqualTo(2); - assertThat(binarySearchCeil(values, 5, true, true)).isEqualTo(2); - - assertThat(binarySearchCeil(values, 6, false, false)).isEqualTo(3); - assertThat(binarySearchCeil(values, 6, true, false)).isEqualTo(3); - assertThat(binarySearchCeil(values, 6, false, true)).isEqualTo(2); - assertThat(binarySearchCeil(values, 6, true, true)).isEqualTo(2); - } - - @Test - public void testParseXsDuration() { - assertThat(parseXsDuration("PT150.279S")).isEqualTo(150279L); - assertThat(parseXsDuration("PT1.500S")).isEqualTo(1500L); - } - - @Test - public void testParseXsDateTime() throws Exception { - assertThat(parseXsDateTime("2014-06-19T23:07:42")).isEqualTo(1403219262000L); - assertThat(parseXsDateTime("2014-08-06T11:00:00Z")).isEqualTo(1407322800000L); - assertThat(parseXsDateTime("2014-08-06T11:00:00,000Z")).isEqualTo(1407322800000L); - assertThat(parseXsDateTime("2014-09-19T13:18:55-08:00")).isEqualTo(1411161535000L); - assertThat(parseXsDateTime("2014-09-19T13:18:55-0800")).isEqualTo(1411161535000L); - assertThat(parseXsDateTime("2014-09-19T13:18:55.000-0800")).isEqualTo(1411161535000L); - assertThat(parseXsDateTime("2014-09-19T13:18:55.000-800")).isEqualTo(1411161535000L); - } - - @Test - public void testToUnsignedLongPositiveValue() { - int x = 0x05D67F23; - - long result = Util.toUnsignedLong(x); - - assertThat(result).isEqualTo(0x05D67F23L); - } - - @Test - public void testToUnsignedLongNegativeValue() { - int x = 0xF5D67F23; - - long result = Util.toUnsignedLong(x); - - assertThat(result).isEqualTo(0xF5D67F23L); - } - - @Test - public void testGetCodecsOfType() { - assertThat(getCodecsOfType(null, C.TRACK_TYPE_VIDEO)).isNull(); - assertThat(getCodecsOfType("avc1.64001e,vp9.63.1", C.TRACK_TYPE_AUDIO)).isNull(); - assertThat(getCodecsOfType(" vp9.63.1, ec-3 ", C.TRACK_TYPE_AUDIO)).isEqualTo("ec-3"); - assertThat(getCodecsOfType("avc1.61e, vp9.63.1, ec-3 ", C.TRACK_TYPE_VIDEO)) - .isEqualTo("avc1.61e,vp9.63.1"); - assertThat(getCodecsOfType("avc1.61e, vp9.63.1, ec-3 ", C.TRACK_TYPE_VIDEO)) - .isEqualTo("avc1.61e,vp9.63.1"); - assertThat(getCodecsOfType("invalidCodec1, invalidCodec2 ", C.TRACK_TYPE_AUDIO)).isNull(); - } - - @Test - public void testUnescapeInvalidFileName() { - assertThat(Util.unescapeFileName("%a")).isNull(); - assertThat(Util.unescapeFileName("%xyz")).isNull(); - } - - @Test - public void testEscapeUnescapeFileName() { - assertEscapeUnescapeFileName("just+a regular+fileName", "just+a regular+fileName"); - assertEscapeUnescapeFileName("key:value", "key%3avalue"); - assertEscapeUnescapeFileName("<>:\"/\\|?*%", "%3c%3e%3a%22%2f%5c%7c%3f%2a%25"); - - Random random = new Random(0); - for (int i = 0; i < 1000; i++) { - String string = TestUtil.buildTestString(1000, random); - assertEscapeUnescapeFileName(string); - } - } - - @Test - public void testCrc32() { - byte[] bytes = {0x5F, 0x78, 0x04, 0x7B, 0x5F}; - int start = 1; - int end = 4; - int initialValue = 0xFFFFFFFF; - - int result = Util.crc32(bytes, start, end, initialValue); - - assertThat(result).isEqualTo(0x67CE9747); - } - - @Test - public void testCrc8() { - byte[] bytes = {0x5F, 0x78, 0x04, 0x7B, 0x5F}; - int start = 1; - int end = 4; - int initialValue = 0; - - int result = Util.crc8(bytes, start, end, initialValue); - - assertThat(result).isEqualTo(0x4); - } - - @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); - } - - @Test - @Config(sdk = 21) - public void testNormalizeLanguageCodeV21() { - assertThat(Util.normalizeLanguageCode(null)).isNull(); - assertThat(Util.normalizeLanguageCode("")).isEmpty(); - assertThat(Util.normalizeLanguageCode("es")).isEqualTo("es"); - assertThat(Util.normalizeLanguageCode("spa")).isEqualTo("es"); - assertThat(Util.normalizeLanguageCode("es-AR")).isEqualTo("es-ar"); - assertThat(Util.normalizeLanguageCode("SpA-ar")).isEqualTo("es-ar"); - assertThat(Util.normalizeLanguageCode("es_AR")).isEqualTo("es-ar"); - assertThat(Util.normalizeLanguageCode("spa_ar")).isEqualTo("es-ar"); - assertThat(Util.normalizeLanguageCode("es-AR-dialect")).isEqualTo("es-ar-dialect"); - assertThat(Util.normalizeLanguageCode("ES-419")).isEqualTo("es-419"); - assertThat(Util.normalizeLanguageCode("zh-hans-tw")).isEqualTo("zh-hans-tw"); - assertThat(Util.normalizeLanguageCode("zh-tw-hans")).isEqualTo("zh-tw"); - assertThat(Util.normalizeLanguageCode("zho-hans-tw")).isEqualTo("zh-hans-tw"); - assertThat(Util.normalizeLanguageCode("und")).isEqualTo("und"); - assertThat(Util.normalizeLanguageCode("DoesNotExist")).isEqualTo("doesnotexist"); - } - - @Test - @Config(sdk = 16) - public void testNormalizeLanguageCode() { - assertThat(Util.normalizeLanguageCode(null)).isNull(); - assertThat(Util.normalizeLanguageCode("")).isEmpty(); - assertThat(Util.normalizeLanguageCode("es")).isEqualTo("es"); - assertThat(Util.normalizeLanguageCode("spa")).isEqualTo("es"); - assertThat(Util.normalizeLanguageCode("es-AR")).isEqualTo("es-ar"); - assertThat(Util.normalizeLanguageCode("SpA-ar")).isEqualTo("es-ar"); - assertThat(Util.normalizeLanguageCode("es_AR")).isEqualTo("es-ar"); - assertThat(Util.normalizeLanguageCode("spa_ar")).isEqualTo("es-ar"); - assertThat(Util.normalizeLanguageCode("es-AR-dialect")).isEqualTo("es-ar-dialect"); - assertThat(Util.normalizeLanguageCode("ES-419")).isEqualTo("es-419"); - assertThat(Util.normalizeLanguageCode("zh-hans-tw")).isEqualTo("zh-hans-tw"); - // Doesn't work on API < 21 because we can't use Locale syntax verification. - // assertThat(Util.normalizeLanguageCode("zh-tw-hans")).isEqualTo("zh-tw"); - assertThat(Util.normalizeLanguageCode("zho-hans-tw")).isEqualTo("zh-hans-tw"); - assertThat(Util.normalizeLanguageCode("und")).isEqualTo("und"); - assertThat(Util.normalizeLanguageCode("DoesNotExist")).isEqualTo("doesnotexist"); - } - - @Test - public void testNormalizeIso6392BibliographicalAndTextualCodes() { - // See https://en.wikipedia.org/wiki/List_of_ISO_639-2_codes. - assertThat(Util.normalizeLanguageCode("alb")).isEqualTo(Util.normalizeLanguageCode("sqi")); - assertThat(Util.normalizeLanguageCode("arm")).isEqualTo(Util.normalizeLanguageCode("hye")); - assertThat(Util.normalizeLanguageCode("baq")).isEqualTo(Util.normalizeLanguageCode("eus")); - assertThat(Util.normalizeLanguageCode("bur")).isEqualTo(Util.normalizeLanguageCode("mya")); - assertThat(Util.normalizeLanguageCode("chi")).isEqualTo(Util.normalizeLanguageCode("zho")); - assertThat(Util.normalizeLanguageCode("cze")).isEqualTo(Util.normalizeLanguageCode("ces")); - assertThat(Util.normalizeLanguageCode("dut")).isEqualTo(Util.normalizeLanguageCode("nld")); - assertThat(Util.normalizeLanguageCode("fre")).isEqualTo(Util.normalizeLanguageCode("fra")); - assertThat(Util.normalizeLanguageCode("geo")).isEqualTo(Util.normalizeLanguageCode("kat")); - assertThat(Util.normalizeLanguageCode("ger")).isEqualTo(Util.normalizeLanguageCode("deu")); - assertThat(Util.normalizeLanguageCode("gre")).isEqualTo(Util.normalizeLanguageCode("ell")); - assertThat(Util.normalizeLanguageCode("ice")).isEqualTo(Util.normalizeLanguageCode("isl")); - assertThat(Util.normalizeLanguageCode("mac")).isEqualTo(Util.normalizeLanguageCode("mkd")); - assertThat(Util.normalizeLanguageCode("mao")).isEqualTo(Util.normalizeLanguageCode("mri")); - assertThat(Util.normalizeLanguageCode("may")).isEqualTo(Util.normalizeLanguageCode("msa")); - assertThat(Util.normalizeLanguageCode("per")).isEqualTo(Util.normalizeLanguageCode("fas")); - assertThat(Util.normalizeLanguageCode("rum")).isEqualTo(Util.normalizeLanguageCode("ron")); - assertThat(Util.normalizeLanguageCode("slo")).isEqualTo(Util.normalizeLanguageCode("slk")); - assertThat(Util.normalizeLanguageCode("tib")).isEqualTo(Util.normalizeLanguageCode("bod")); - assertThat(Util.normalizeLanguageCode("wel")).isEqualTo(Util.normalizeLanguageCode("cym")); - } - - private static void assertEscapeUnescapeFileName(String fileName, String escapedFileName) { - assertThat(escapeFileName(fileName)).isEqualTo(escapedFileName); - assertThat(unescapeFileName(escapedFileName)).isEqualTo(fileName); - } - - private static void assertEscapeUnescapeFileName(String fileName) { - String escapedFileName = Util.escapeFileName(fileName); - assertThat(unescapeFileName(escapedFileName)).isEqualTo(fileName); - } -} diff --git a/library/core/src/test/java/com/google/android/exoplayer2/video/DecoderVideoRendererTest.java b/library/core/src/test/java/com/google/android/exoplayer2/video/DecoderVideoRendererTest.java new file mode 100644 index 0000000000..f4aee42f25 --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/video/DecoderVideoRendererTest.java @@ -0,0 +1,355 @@ +/* + * Copyright (C) 2020 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.video; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import android.graphics.SurfaceTexture; +import android.os.Handler; +import android.os.SystemClock; +import android.view.Surface; +import androidx.annotation.GuardedBy; +import androidx.annotation.Nullable; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.RendererCapabilities; +import com.google.android.exoplayer2.RendererConfiguration; +import com.google.android.exoplayer2.decoder.DecoderException; +import com.google.android.exoplayer2.decoder.SimpleDecoder; +import com.google.android.exoplayer2.drm.ExoMediaCrypto; +import com.google.android.exoplayer2.testutil.FakeSampleStream; +import com.google.android.exoplayer2.testutil.FakeSampleStream.FakeSampleStreamItem; +import com.google.android.exoplayer2.util.MimeTypes; +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; +import org.robolectric.annotation.LooperMode; +import org.robolectric.shadows.ShadowLooper; + +/** Unit test for {@link DecoderVideoRenderer}. */ +@LooperMode(LooperMode.Mode.PAUSED) +@RunWith(AndroidJUnit4.class) +public final class DecoderVideoRendererTest { + @Rule public final MockitoRule mockito = MockitoJUnit.rule(); + + private static final Format H264_FORMAT = + new Format.Builder() + .setSampleMimeType(MimeTypes.VIDEO_H264) + .setWidth(1920) + .setHeight(1080) + .build(); + + private DecoderVideoRenderer renderer; + @Mock private VideoRendererEventListener eventListener; + + @Before + public void setUp() { + renderer = + new DecoderVideoRenderer( + /* allowedJoiningTimeMs= */ 0, + new Handler(), + eventListener, + /* maxDroppedFramesToNotify= */ -1) { + + private final Object pendingDecodeCallLock = new Object(); + + @GuardedBy("pendingDecodeCallLock") + private int pendingDecodeCalls; + + @C.VideoOutputMode private int outputMode; + + @Override + public String getName() { + return "TestVideoRenderer"; + } + + @Override + @Capabilities + public int supportsFormat(Format format) { + return RendererCapabilities.create(FORMAT_HANDLED); + } + + @Override + protected void setDecoderOutputMode(@C.VideoOutputMode int outputMode) { + this.outputMode = outputMode; + } + + @Override + protected void renderOutputBufferToSurface( + VideoDecoderOutputBuffer outputBuffer, Surface surface) { + // Do nothing. + } + + @Override + protected void onQueueInputBuffer(VideoDecoderInputBuffer buffer) { + // SimpleDecoder.decode() is called on a background thread we have no control about from + // the test. Ensure the background calls are predictably serialized by waiting for them + // to finish: + // 1. Mark decode calls as "pending" here. + // 2. Send a message on the test thread to wait for all pending decode calls. + // 3. Decrement the pending counter in decode calls and wake up the waiting test. + // 4. The tests need to call ShadowLooper.idleMainThread() to wait for pending calls. + synchronized (pendingDecodeCallLock) { + pendingDecodeCalls++; + } + new Handler() + .post( + () -> { + synchronized (pendingDecodeCallLock) { + while (pendingDecodeCalls > 0) { + try { + pendingDecodeCallLock.wait(); + } catch (InterruptedException e) { + // Ignore. + } + } + } + }); + super.onQueueInputBuffer(buffer); + } + + @Override + protected SimpleDecoder< + VideoDecoderInputBuffer, + ? extends VideoDecoderOutputBuffer, + ? extends DecoderException> + createDecoder(Format format, @Nullable ExoMediaCrypto mediaCrypto) { + return new SimpleDecoder< + VideoDecoderInputBuffer, VideoDecoderOutputBuffer, DecoderException>( + new VideoDecoderInputBuffer[10], new VideoDecoderOutputBuffer[10]) { + @Override + protected VideoDecoderInputBuffer createInputBuffer() { + return new VideoDecoderInputBuffer(); + } + + @Override + protected VideoDecoderOutputBuffer createOutputBuffer() { + return new VideoDecoderOutputBuffer(this::releaseOutputBuffer); + } + + @Override + protected DecoderException createUnexpectedDecodeException(Throwable error) { + return new DecoderException("error", error); + } + + @Nullable + @Override + protected DecoderException decode( + VideoDecoderInputBuffer inputBuffer, + VideoDecoderOutputBuffer outputBuffer, + boolean reset) { + outputBuffer.init(inputBuffer.timeUs, outputMode, /* supplementalData= */ null); + synchronized (pendingDecodeCallLock) { + pendingDecodeCalls--; + pendingDecodeCallLock.notify(); + } + return null; + } + + @Override + public String getName() { + return "TestDecoder"; + } + }; + } + }; + renderer.setOutputSurface(new Surface(new SurfaceTexture(/* texName= */ 0))); + } + + @Test + public void enable_withMayRenderStartOfStream_rendersFirstFrameBeforeStart() throws Exception { + FakeSampleStream fakeSampleStream = + new FakeSampleStream( + /* format= */ H264_FORMAT, + /* eventDispatcher= */ null, + /* firstSampleTimeUs= */ 0, + /* timeUsIncrement= */ 50, + new FakeSampleStreamItem(new byte[] {0}, C.BUFFER_FLAG_KEY_FRAME)); + + renderer.enable( + RendererConfiguration.DEFAULT, + new Format[] {H264_FORMAT}, + fakeSampleStream, + /* positionUs= */ 0, + /* joining= */ false, + /* mayRenderStartOfStream= */ true, + /* offsetUs */ 0); + for (int i = 0; i < 10; i++) { + renderer.render(/* positionUs= */ 0, SystemClock.elapsedRealtime() * 1000); + // Ensure pending messages are delivered. + ShadowLooper.idleMainLooper(); + } + + verify(eventListener).onRenderedFirstFrame(any()); + } + + @Test + public void enable_withoutMayRenderStartOfStream_doesNotRenderFirstFrameBeforeStart() + throws Exception { + FakeSampleStream fakeSampleStream = + new FakeSampleStream( + /* format= */ H264_FORMAT, + /* eventDispatcher= */ null, + /* firstSampleTimeUs= */ 0, + /* timeUsIncrement= */ 50, + new FakeSampleStreamItem(new byte[] {0}, C.BUFFER_FLAG_KEY_FRAME)); + + renderer.enable( + RendererConfiguration.DEFAULT, + new Format[] {H264_FORMAT}, + fakeSampleStream, + /* positionUs= */ 0, + /* joining= */ false, + /* mayRenderStartOfStream= */ false, + /* offsetUs */ 0); + for (int i = 0; i < 10; i++) { + renderer.render(/* positionUs= */ 0, SystemClock.elapsedRealtime() * 1000); + // Ensure pending messages are delivered. + ShadowLooper.idleMainLooper(); + } + + verify(eventListener, never()).onRenderedFirstFrame(any()); + } + + @Test + public void enable_withoutMayRenderStartOfStream_rendersFirstFrameAfterStart() throws Exception { + FakeSampleStream fakeSampleStream = + new FakeSampleStream( + /* format= */ H264_FORMAT, + /* eventDispatcher= */ null, + /* firstSampleTimeUs= */ 0, + /* timeUsIncrement= */ 50, + new FakeSampleStreamItem(new byte[] {0}, C.BUFFER_FLAG_KEY_FRAME)); + + renderer.enable( + RendererConfiguration.DEFAULT, + new Format[] {H264_FORMAT}, + fakeSampleStream, + /* positionUs= */ 0, + /* joining= */ false, + /* mayRenderStartOfStream= */ false, + /* offsetUs */ 0); + renderer.start(); + for (int i = 0; i < 10; i++) { + renderer.render(/* positionUs= */ 0, SystemClock.elapsedRealtime() * 1000); + // Ensure pending messages are delivered. + ShadowLooper.idleMainLooper(); + } + + verify(eventListener).onRenderedFirstFrame(any()); + } + + // TODO: Fix rendering of first frame at stream transition. + @Ignore + @Test + public void replaceStream_whenStarted_rendersFirstFrameOfNewStream() throws Exception { + FakeSampleStream fakeSampleStream1 = + new FakeSampleStream( + /* format= */ H264_FORMAT, + /* eventDispatcher= */ null, + /* firstSampleTimeUs= */ 0, + /* timeUsIncrement= */ 50, + new FakeSampleStreamItem(new byte[] {0}, C.BUFFER_FLAG_KEY_FRAME), + FakeSampleStreamItem.END_OF_STREAM_ITEM); + FakeSampleStream fakeSampleStream2 = + new FakeSampleStream( + /* format= */ H264_FORMAT, + /* eventDispatcher= */ null, + /* firstSampleTimeUs= */ 0, + /* timeUsIncrement= */ 50, + new FakeSampleStreamItem(new byte[] {0}, C.BUFFER_FLAG_KEY_FRAME), + FakeSampleStreamItem.END_OF_STREAM_ITEM); + renderer.enable( + RendererConfiguration.DEFAULT, + new Format[] {H264_FORMAT}, + fakeSampleStream1, + /* positionUs= */ 0, + /* joining= */ false, + /* mayRenderStartOfStream= */ true, + /* offsetUs */ 0); + renderer.start(); + + boolean replacedStream = false; + for (int i = 0; i <= 10; i++) { + renderer.render(/* positionUs= */ i * 10, SystemClock.elapsedRealtime() * 1000); + if (!replacedStream && renderer.hasReadStreamToEnd()) { + renderer.replaceStream(new Format[] {H264_FORMAT}, fakeSampleStream2, /* offsetUs= */ 100); + replacedStream = true; + } + // Ensure pending messages are delivered. + ShadowLooper.idleMainLooper(); + } + + verify(eventListener, times(2)).onRenderedFirstFrame(any()); + } + + // TODO: Fix rendering of first frame at stream transition. + @Ignore + @Test + public void replaceStream_whenNotStarted_doesNotRenderFirstFrameOfNewStream() throws Exception { + FakeSampleStream fakeSampleStream1 = + new FakeSampleStream( + /* format= */ H264_FORMAT, + /* eventDispatcher= */ null, + /* firstSampleTimeUs= */ 0, + /* timeUsIncrement= */ 50, + new FakeSampleStreamItem(new byte[] {0}, C.BUFFER_FLAG_KEY_FRAME), + FakeSampleStreamItem.END_OF_STREAM_ITEM); + FakeSampleStream fakeSampleStream2 = + new FakeSampleStream( + /* format= */ H264_FORMAT, + /* eventDispatcher= */ null, + /* firstSampleTimeUs= */ 0, + /* timeUsIncrement= */ 50, + new FakeSampleStreamItem(new byte[] {0}, C.BUFFER_FLAG_KEY_FRAME), + FakeSampleStreamItem.END_OF_STREAM_ITEM); + renderer.enable( + RendererConfiguration.DEFAULT, + new Format[] {H264_FORMAT}, + fakeSampleStream1, + /* positionUs= */ 0, + /* joining= */ false, + /* mayRenderStartOfStream= */ true, + /* offsetUs */ 0); + + boolean replacedStream = false; + for (int i = 0; i < 10; i++) { + renderer.render(/* positionUs= */ i * 10, SystemClock.elapsedRealtime() * 1000); + if (!replacedStream && renderer.hasReadStreamToEnd()) { + renderer.replaceStream(new Format[] {H264_FORMAT}, fakeSampleStream2, /* offsetUs= */ 100); + replacedStream = true; + } + // Ensure pending messages are delivered. + ShadowLooper.idleMainLooper(); + } + + verify(eventListener).onRenderedFirstFrame(any()); + + // Render to streamOffsetUs and verify the new first frame gets rendered. + renderer.render(/* positionUs= */ 100, SystemClock.elapsedRealtime() * 1000); + + verify(eventListener, times(2)).onRenderedFirstFrame(any()); + } +} diff --git a/library/core/src/test/java/com/google/android/exoplayer2/video/spherical/FrameRotationQueueTest.java b/library/core/src/test/java/com/google/android/exoplayer2/video/spherical/FrameRotationQueueTest.java index 887c8da09f..227a827679 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/video/spherical/FrameRotationQueueTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/video/spherical/FrameRotationQueueTest.java @@ -37,19 +37,19 @@ public class FrameRotationQueueTest { } @Test - public void testGetRotationMatrixReturnsNull_whenEmpty() throws Exception { + public void getRotationMatrixReturnsNull_whenEmpty() throws Exception { assertThat(frameRotationQueue.pollRotationMatrix(rotationMatrix, 0)).isFalse(); } @Test - public void testGetRotationMatrixReturnsNotNull_whenNotEmpty() throws Exception { + public void getRotationMatrixReturnsNotNull_whenNotEmpty() throws Exception { frameRotationQueue.setRotation(0, new float[] {1, 2, 3}); assertThat(frameRotationQueue.pollRotationMatrix(rotationMatrix, 0)).isTrue(); assertThat(rotationMatrix).hasLength(16); } @Test - public void testConvertsAngleAxisToRotationMatrix() throws Exception { + public void convertsAngleAxisToRotationMatrix() throws Exception { doTestAngleAxisToRotationMatrix(/* angleRadian= */ 0, /* x= */ 1, /* y= */ 0, /* z= */ 0); frameRotationQueue.reset(); doTestAngleAxisToRotationMatrix(/* angleRadian= */ 1, /* x= */ 1, /* y= */ 0, /* z= */ 0); @@ -61,7 +61,7 @@ public class FrameRotationQueueTest { } @Test - public void testRecentering_justYaw() throws Exception { + public void recentering_justYaw() throws Exception { float[] actualMatrix = getRotationMatrixFromAngleAxis( /* angleRadian= */ (float) Math.PI, /* x= */ 0, /* y= */ 1, /* z= */ 0); @@ -71,7 +71,7 @@ public class FrameRotationQueueTest { } @Test - public void testRecentering_yawAndPitch() throws Exception { + public void recentering_yawAndPitch() throws Exception { float[] matrix = getRotationMatrixFromAngleAxis( /* angleRadian= */ (float) Math.PI, /* x= */ 1, /* y= */ 1, /* z= */ 0); @@ -80,7 +80,7 @@ public class FrameRotationQueueTest { } @Test - public void testRecentering_yawAndPitch2() throws Exception { + public void recentering_yawAndPitch2() throws Exception { float[] matrix = getRotationMatrixFromAngleAxis( /* angleRadian= */ (float) Math.PI / 2, /* x= */ 1, /* y= */ 1, /* z= */ 0); @@ -90,7 +90,7 @@ public class FrameRotationQueueTest { } @Test - public void testRecentering_yawAndPitchAndRoll() throws Exception { + public void recentering_yawAndPitchAndRoll() throws Exception { float[] matrix = getRotationMatrixFromAngleAxis( /* angleRadian= */ (float) Math.PI * 2 / 3, /* x= */ 1, /* y= */ 1, /* z= */ 1); diff --git a/library/core/src/test/java/com/google/android/exoplayer2/video/spherical/ProjectionDecoderTest.java b/library/core/src/test/java/com/google/android/exoplayer2/video/spherical/ProjectionDecoderTest.java index 1dadfe9909..b9559816d7 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/video/spherical/ProjectionDecoderTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/video/spherical/ProjectionDecoderTest.java @@ -44,12 +44,12 @@ public final class ProjectionDecoderTest { private static final float[] LAST_UV = {1.0f, 1.0f}; @Test - public void testDecodeProj() { + public void decodeProj() { testDecoding(PROJ_DATA); } @Test - public void testDecodeMshp() { + public void decodeMshp() { testDecoding(Arrays.copyOfRange(PROJ_DATA, MSHP_OFFSET, PROJ_DATA.length)); } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/video/spherical/ProjectionTest.java b/library/core/src/test/java/com/google/android/exoplayer2/video/spherical/ProjectionTest.java index 8add0eac61..1a889bbaa7 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/video/spherical/ProjectionTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/video/spherical/ProjectionTest.java @@ -37,7 +37,7 @@ public class ProjectionTest { private static final float HORIZONTAL_FOV_DEGREES = 360; @Test - public void testSphericalMesh() throws Exception { + public void sphericalMesh() throws Exception { // Only the first param is important in this test. Projection projection = Projection.createEquirectangular( @@ -61,7 +61,7 @@ public class ProjectionTest { } @Test - public void testArgumentValidation() { + public void argumentValidation() { checkIllegalArgumentException(0, 1, 1, 1, 1); checkIllegalArgumentException(1, 0, 1, 1, 1); checkIllegalArgumentException(1, 1, 0, 1, 1); diff --git a/library/dash/build.gradle b/library/dash/build.gradle index f51fc509cc..0ffbc718f0 100644 --- a/library/dash/build.gradle +++ b/library/dash/build.gradle @@ -33,6 +33,8 @@ android { } } + sourceSets.test.assets.srcDir '../../testdata/src/test/assets/' + testOptions.unitTests.includeAndroidResources = true } @@ -40,6 +42,7 @@ dependencies { implementation project(modulePrefix + 'library-core') compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion compileOnly 'org.checkerframework:checker-compat-qual:' + checkerframeworkVersion + compileOnly 'org.jetbrains.kotlin:kotlin-annotations-jvm:' + kotlinAnnotationsVersion implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion testImplementation project(modulePrefix + 'testutils') testImplementation 'org.robolectric:robolectric:' + robolectricVersion 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 f7edf62182..e12a67a754 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 @@ -42,7 +42,8 @@ public interface DashChunkSource extends ChunkSource { * @param trackSelection The track selection. * @param elapsedRealtimeOffsetMs If known, an estimate of the instantaneous difference between * server-side unix time and {@link SystemClock#elapsedRealtime()} in milliseconds, - * specified as the server's unix time minus the local elapsed time. If unknown, set to 0. + * specified as the server's unix time minus the local elapsed time. Or {@link + * com.google.android.exoplayer2.C#TIME_UNSET} if unknown. * @param enableEventMessageTrack Whether to output an event message track. * @param closedCaptionFormats The {@link Format Formats} of closed caption tracks to be output. * @param transferListener The transfer listener which should be informed of any data transfers. 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 bb8226e172..e1a441f36f 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,6 +16,7 @@ package com.google.android.exoplayer2.source.dash; import android.util.Pair; +import android.util.SparseArray; import android.util.SparseIntArray; import androidx.annotation.IntDef; import androidx.annotation.Nullable; @@ -73,7 +74,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; /* package */ final int id; private final DashChunkSource.Factory chunkSourceFactory; @Nullable private final TransferListener transferListener; - private final DrmSessionManager drmSessionManager; + private final DrmSessionManager drmSessionManager; private final LoadErrorHandlingPolicy loadErrorHandlingPolicy; private final long elapsedRealtimeOffsetMs; private final LoaderErrorThrower manifestLoaderErrorThrower; @@ -101,7 +102,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; int periodIndex, DashChunkSource.Factory chunkSourceFactory, @Nullable TransferListener transferListener, - DrmSessionManager drmSessionManager, + DrmSessionManager drmSessionManager, LoadErrorHandlingPolicy loadErrorHandlingPolicy, EventDispatcher eventDispatcher, long elapsedRealtimeOffsetMs, @@ -480,7 +481,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; } private static Pair buildTrackGroups( - DrmSessionManager drmSessionManager, + DrmSessionManager drmSessionManager, List adaptationSets, List eventStreams) { int[][] groupedAdaptationSetIndices = getGroupedAdaptationSetIndices(adaptationSets); @@ -516,50 +517,94 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; return Pair.create(new TrackGroupArray(trackGroups), trackGroupInfos); } + /** + * Groups adaptation sets. Two adaptations sets belong to the same group if either: + * + *

          + *
        • One is a trick-play adaptation set and uses a {@code + * http://dashif.org/guidelines/trickmode} essential or supplemental property to indicate + * that the other is the main adaptation set to which it corresponds. + *
        • The two adaptation sets are marked as safe for switching using {@code + * urn:mpeg:dash:adaptation-set-switching:2016} supplemental properties. + *
        + * + * @param adaptationSets The adaptation sets to merge. + * @return An array of groups, where each group is an array of adaptation set indices. + */ private static int[][] getGroupedAdaptationSetIndices(List adaptationSets) { int adaptationSetCount = adaptationSets.size(); - SparseIntArray idToIndexMap = new SparseIntArray(adaptationSetCount); + SparseIntArray adaptationSetIdToIndex = new SparseIntArray(adaptationSetCount); + List> adaptationSetGroupedIndices = new ArrayList<>(adaptationSetCount); + SparseArray> adaptationSetIndexToGroupedIndices = + new SparseArray<>(adaptationSetCount); + + // Initially make each adaptation set belong to its own group. Also build the + // adaptationSetIdToIndex map. for (int i = 0; i < adaptationSetCount; i++) { - idToIndexMap.put(adaptationSets.get(i).id, i); + adaptationSetIdToIndex.put(adaptationSets.get(i).id, i); + List initialGroup = new ArrayList<>(); + initialGroup.add(i); + adaptationSetGroupedIndices.add(initialGroup); + adaptationSetIndexToGroupedIndices.put(i, initialGroup); } - int[][] groupedAdaptationSetIndices = new int[adaptationSetCount][]; - boolean[] adaptationSetUsedFlags = new boolean[adaptationSetCount]; - - int groupCount = 0; + // Merge adaptation set groups. for (int i = 0; i < adaptationSetCount; i++) { - if (adaptationSetUsedFlags[i]) { - // This adaptation set has already been included in a group. - continue; + int mergedGroupIndex = i; + AdaptationSet adaptationSet = adaptationSets.get(i); + + // Trick-play adaptation sets are merged with their corresponding main adaptation sets. + @Nullable + Descriptor trickPlayProperty = findTrickPlayProperty(adaptationSet.essentialProperties); + if (trickPlayProperty == null) { + // Trick-play can also be specified using a supplemental property. + trickPlayProperty = findTrickPlayProperty(adaptationSet.supplementalProperties); } - adaptationSetUsedFlags[i] = true; - Descriptor adaptationSetSwitchingProperty = findAdaptationSetSwitchingProperty( - adaptationSets.get(i).supplementalProperties); - if (adaptationSetSwitchingProperty == null) { - groupedAdaptationSetIndices[groupCount++] = new int[] {i}; - } else { - String[] extraAdaptationSetIds = Util.split(adaptationSetSwitchingProperty.value, ","); - int[] adaptationSetIndices = new int[1 + extraAdaptationSetIds.length]; - adaptationSetIndices[0] = i; - int outputIndex = 1; - for (String adaptationSetId : extraAdaptationSetIds) { - int extraIndex = - idToIndexMap.get(Integer.parseInt(adaptationSetId), /* valueIfKeyNotFound= */ -1); - if (extraIndex != -1) { - adaptationSetUsedFlags[extraIndex] = true; - adaptationSetIndices[outputIndex] = extraIndex; - outputIndex++; + if (trickPlayProperty != null) { + int mainAdaptationSetId = Integer.parseInt(trickPlayProperty.value); + int mainAdaptationSetIndex = + adaptationSetIdToIndex.get(mainAdaptationSetId, /* valueIfKeyNotFound= */ -1); + if (mainAdaptationSetIndex != -1) { + mergedGroupIndex = mainAdaptationSetIndex; + } + } + + // Adaptation sets that are safe for switching are merged, using the smallest index for the + // merged group. + if (mergedGroupIndex == i) { + @Nullable + Descriptor adaptationSetSwitchingProperty = + findAdaptationSetSwitchingProperty(adaptationSet.supplementalProperties); + if (adaptationSetSwitchingProperty != null) { + String[] otherAdaptationSetIds = Util.split(adaptationSetSwitchingProperty.value, ","); + for (String adaptationSetId : otherAdaptationSetIds) { + int otherAdaptationSetId = + adaptationSetIdToIndex.get( + Integer.parseInt(adaptationSetId), /* valueIfKeyNotFound= */ -1); + if (otherAdaptationSetId != -1) { + mergedGroupIndex = Math.min(mergedGroupIndex, otherAdaptationSetId); + } } } - if (outputIndex < adaptationSetIndices.length) { - adaptationSetIndices = Arrays.copyOf(adaptationSetIndices, outputIndex); - } - groupedAdaptationSetIndices[groupCount++] = adaptationSetIndices; + } + + // Merge the groups if necessary. + if (mergedGroupIndex != i) { + List thisGroup = adaptationSetIndexToGroupedIndices.get(i); + List mergedGroup = adaptationSetIndexToGroupedIndices.get(mergedGroupIndex); + mergedGroup.addAll(thisGroup); + adaptationSetIndexToGroupedIndices.put(i, mergedGroup); + adaptationSetGroupedIndices.remove(thisGroup); } } - return groupCount < adaptationSetCount - ? Arrays.copyOf(groupedAdaptationSetIndices, groupCount) : groupedAdaptationSetIndices; + int[][] groupedAdaptationSetIndices = new int[adaptationSetGroupedIndices.size()][]; + for (int i = 0; i < groupedAdaptationSetIndices.length; i++) { + groupedAdaptationSetIndices[i] = Util.toArray(adaptationSetGroupedIndices.get(i)); + // Restore the original adaptation set order within each group. + Arrays.sort(groupedAdaptationSetIndices[i]); + } + return groupedAdaptationSetIndices; } /** @@ -597,7 +642,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; } private static int buildPrimaryAndEmbeddedTrackGroupInfos( - DrmSessionManager drmSessionManager, + DrmSessionManager drmSessionManager, List adaptationSets, int[][] groupedAdaptationSetIndices, int primaryGroupCount, @@ -640,8 +685,11 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; eventMessageTrackGroupIndex, cea608TrackGroupIndex); if (eventMessageTrackGroupIndex != C.INDEX_UNSET) { - Format format = Format.createSampleFormat(firstAdaptationSet.id + ":emsg", - MimeTypes.APPLICATION_EMSG, null, Format.NO_VALUE, null); + Format format = + new Format.Builder() + .setId(firstAdaptationSet.id + ":emsg") + .setSampleMimeType(MimeTypes.APPLICATION_EMSG) + .build(); trackGroups[eventMessageTrackGroupIndex] = new TrackGroup(format); trackGroupInfos[eventMessageTrackGroupIndex] = TrackGroupInfo.embeddedEmsgTrack(adaptationSetIndices, primaryTrackGroupIndex); @@ -659,8 +707,11 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; TrackGroup[] trackGroups, TrackGroupInfo[] trackGroupInfos, int existingTrackGroupCount) { for (int i = 0; i < eventStreams.size(); i++) { EventStream eventStream = eventStreams.get(i); - Format format = Format.createSampleFormat(eventStream.id(), MimeTypes.APPLICATION_EMSG, null, - Format.NO_VALUE, null); + Format format = + new Format.Builder() + .setId(eventStream.id()) + .setSampleMimeType(MimeTypes.APPLICATION_EMSG) + .build(); trackGroups[existingTrackGroupCount] = new TrackGroup(format); trackGroupInfos[existingTrackGroupCount++] = TrackGroupInfo.mpdEventTrack(i); } @@ -738,10 +789,21 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; return stream; } + @Nullable private static Descriptor findAdaptationSetSwitchingProperty(List descriptors) { + return findDescriptor(descriptors, "urn:mpeg:dash:adaptation-set-switching:2016"); + } + + @Nullable + private static Descriptor findTrickPlayProperty(List descriptors) { + return findDescriptor(descriptors, "http://dashif.org/guidelines/trickmode"); + } + + @Nullable + private static Descriptor findDescriptor(List descriptors, String schemeIdUri) { for (int i = 0; i < descriptors.size(); i++) { Descriptor descriptor = descriptors.get(i); - if ("urn:mpeg:dash:adaptation-set-switching:2016".equals(descriptor.schemeIdUri)) { + if (schemeIdUri.equals(descriptor.schemeIdUri)) { return descriptor; } } @@ -770,7 +832,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; for (int j = 0; j < descriptors.size(); j++) { Descriptor descriptor = descriptors.get(j); if ("urn:scte:dash:cc:cea-608:2015".equals(descriptor.schemeIdUri)) { - String value = descriptor.value; + @Nullable String value = descriptor.value; if (value == null) { // There are embedded CEA-608 tracks, but service information is not declared. return new Format[] {buildCea608TrackFormat(adaptationSet.id)}; @@ -802,23 +864,21 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; } private static Format buildCea608TrackFormat( - int adaptationSetId, String language, int accessibilityChannel) { - return Format.createTextSampleFormat( + int adaptationSetId, @Nullable String language, int accessibilityChannel) { + String id = adaptationSetId + ":cea608" - + (accessibilityChannel != Format.NO_VALUE ? ":" + accessibilityChannel : ""), - MimeTypes.APPLICATION_CEA608, - /* codecs= */ null, - /* bitrate= */ Format.NO_VALUE, - /* selectionFlags= */ 0, - language, - accessibilityChannel, - /* drmInitData= */ null, - Format.OFFSET_SAMPLE_RELATIVE, - /* initializationData= */ null); + + (accessibilityChannel != Format.NO_VALUE ? ":" + accessibilityChannel : ""); + return new Format.Builder() + .setId(id) + .setSampleMimeType(MimeTypes.APPLICATION_CEA608) + .setLanguage(language) + .setAccessibilityChannel(accessibilityChannel) + .build(); } - @SuppressWarnings("unchecked") + // We won't assign the array to a variable that erases the generic type, and then write into it. + @SuppressWarnings({"unchecked", "rawtypes"}) private static ChunkSampleStream[] newSampleStreamArray(int length) { return new ChunkSampleStream[length]; } 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 e45b134cdb..4b74956816 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 @@ -23,6 +23,7 @@ import android.util.SparseArray; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlayerLibraryInfo; +import com.google.android.exoplayer2.MediaItem; import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.drm.DrmSession; @@ -32,6 +33,8 @@ import com.google.android.exoplayer2.offline.StreamKey; import com.google.android.exoplayer2.source.BaseMediaSource; import com.google.android.exoplayer2.source.CompositeSequenceableLoaderFactory; import com.google.android.exoplayer2.source.DefaultCompositeSequenceableLoaderFactory; +import com.google.android.exoplayer2.source.LoadEventInfo; +import com.google.android.exoplayer2.source.MediaLoadData; import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MediaSourceEventListener; @@ -47,6 +50,7 @@ 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.LoadErrorHandlingPolicy.LoadErrorInfo; import com.google.android.exoplayer2.upstream.Loader; import com.google.android.exoplayer2.upstream.Loader.LoadErrorAction; import com.google.android.exoplayer2.upstream.LoaderErrorThrower; @@ -54,6 +58,7 @@ 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.Log; +import com.google.android.exoplayer2.util.SntpClient; import com.google.android.exoplayer2.util.Util; import java.io.BufferedReader; import java.io.IOException; @@ -62,6 +67,7 @@ import java.io.InputStreamReader; import java.nio.charset.Charset; import java.text.ParseException; import java.text.SimpleDateFormat; +import java.util.Collections; import java.util.List; import java.util.Locale; import java.util.TimeZone; @@ -81,14 +87,13 @@ public final class DashMediaSource extends BaseMediaSource { private final DashChunkSource.Factory chunkSourceFactory; @Nullable private final DataSource.Factory manifestDataSourceFactory; - private DrmSessionManager drmSessionManager; - @Nullable private ParsingLoadable.Parser manifestParser; - @Nullable private List streamKeys; + private DrmSessionManager drmSessionManager; private CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory; private LoadErrorHandlingPolicy loadErrorHandlingPolicy; private long livePresentationDelayMs; private boolean livePresentationDelayOverridesManifest; - private boolean isCreateCalled; + @Nullable private ParsingLoadable.Parser manifestParser; + private List streamKeys; @Nullable private Object tag; /** @@ -119,50 +124,48 @@ public final class DashMediaSource extends BaseMediaSource { loadErrorHandlingPolicy = new DefaultLoadErrorHandlingPolicy(); livePresentationDelayMs = DEFAULT_LIVE_PRESENTATION_DELAY_MS; compositeSequenceableLoaderFactory = new DefaultCompositeSequenceableLoaderFactory(); + streamKeys = Collections.emptyList(); } /** - * Sets a tag for the media source which will be published in the {@link - * com.google.android.exoplayer2.Timeline} of the source as {@link - * com.google.android.exoplayer2.Timeline.Window#tag}. - * - * @param tag A tag for the media source. - * @return This factory, for convenience. - * @throws IllegalStateException If one of the {@code create} methods has already been called. + * @deprecated Use {@link MediaItem.Builder#setTag(Object)} and {@link + * #createMediaSource(MediaItem)} instead. */ + @Deprecated public Factory setTag(@Nullable Object tag) { - Assertions.checkState(!isCreateCalled); this.tag = tag; return this; } + /** + * @deprecated Use {@link MediaItem.Builder#setStreamKeys(List)} and {@link + * #createMediaSource(MediaItem)} instead. + */ + @SuppressWarnings("deprecation") + @Deprecated + @Override + public Factory setStreamKeys(@Nullable List streamKeys) { + this.streamKeys = streamKeys != null ? streamKeys : Collections.emptyList(); + return this; + } + /** * Sets the {@link DrmSessionManager} to use for acquiring {@link DrmSession DrmSessions}. The * default value is {@link DrmSessionManager#DUMMY}. * * @param drmSessionManager The {@link DrmSessionManager}. * @return This factory, for convenience. - * @throws IllegalStateException If one of the {@code create} methods has already been called. */ - public Factory setDrmSessionManager(DrmSessionManager drmSessionManager) { - Assertions.checkState(!isCreateCalled); - this.drmSessionManager = drmSessionManager; + @Override + public Factory setDrmSessionManager(@Nullable DrmSessionManager drmSessionManager) { + this.drmSessionManager = + drmSessionManager != null + ? drmSessionManager + : DrmSessionManager.getDummyDrmSessionManager(); return this; } - /** - * 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 Use {@link #setLoadErrorHandlingPolicy(LoadErrorHandlingPolicy)} instead. */ @Deprecated public Factory setMinLoadableRetryCount(int minLoadableRetryCount) { return setLoadErrorHandlingPolicy(new DefaultLoadErrorHandlingPolicy(minLoadableRetryCount)); @@ -176,15 +179,17 @@ public final class DashMediaSource extends BaseMediaSource { * * @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; + public Factory setLoadErrorHandlingPolicy( + @Nullable LoadErrorHandlingPolicy loadErrorHandlingPolicy) { + this.loadErrorHandlingPolicy = + loadErrorHandlingPolicy != null + ? loadErrorHandlingPolicy + : new DefaultLoadErrorHandlingPolicy(); return this; } - /** @deprecated Use {@link #setLivePresentationDelayMs(long, boolean)}. */ + /** @deprecated Use {@link #setLivePresentationDelayMs(long, boolean)} instead. */ @Deprecated @SuppressWarnings("deprecation") public Factory setLivePresentationDelayMs(long livePresentationDelayMs) { @@ -207,11 +212,9 @@ public final class DashMediaSource extends BaseMediaSource { * @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, boolean overridesManifest) { - Assertions.checkState(!isCreateCalled); this.livePresentationDelayMs = livePresentationDelayMs; this.livePresentationDelayOverridesManifest = overridesManifest; return this; @@ -222,12 +225,10 @@ public final class DashMediaSource extends BaseMediaSource { * * @param manifestParser A parser for loaded manifest data. * @return This factory, for convenience. - * @throws IllegalStateException If one of the {@code create} methods has already been called. */ public Factory setManifestParser( - ParsingLoadable.Parser manifestParser) { - Assertions.checkState(!isCreateCalled); - this.manifestParser = Assertions.checkNotNull(manifestParser); + @Nullable ParsingLoadable.Parser manifestParser) { + this.manifestParser = manifestParser; return this; } @@ -240,13 +241,13 @@ public final class DashMediaSource extends BaseMediaSource { * SequenceableLoader}s for when this media source loads data from multiple streams (video, * audio etc...). * @return This factory, for convenience. - * @throws IllegalStateException If one of the {@code create} methods has already been called. */ public Factory setCompositeSequenceableLoaderFactory( - CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory) { - Assertions.checkState(!isCreateCalled); + @Nullable CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory) { this.compositeSequenceableLoaderFactory = - Assertions.checkNotNull(compositeSequenceableLoaderFactory); + compositeSequenceableLoaderFactory != null + ? compositeSequenceableLoaderFactory + : new DefaultCompositeSequenceableLoaderFactory(); return this; } @@ -260,8 +261,7 @@ public final class DashMediaSource extends BaseMediaSource { */ public DashMediaSource createMediaSource(DashManifest manifest) { Assertions.checkArgument(!manifest.dynamic); - isCreateCalled = true; - if (streamKeys != null && !streamKeys.isEmpty()) { + if (!streamKeys.isEmpty()) { manifest = manifest.copy(streamKeys); } return new DashMediaSource( @@ -310,24 +310,38 @@ public final class DashMediaSource extends BaseMediaSource { return mediaSource; } + /** @deprecated Use {@link #createMediaSource(MediaItem)} instead. */ + @SuppressWarnings("deprecation") + @Deprecated + @Override + public DashMediaSource createMediaSource(Uri uri) { + return createMediaSource(new MediaItem.Builder().setUri(uri).build()); + } + /** * Returns a new {@link DashMediaSource} using the current parameters. * - * @param manifestUri The manifest {@link Uri}. + * @param mediaItem The media item of the dash stream. * @return The new {@link DashMediaSource}. + * @throws NullPointerException if {@link MediaItem#playbackProperties} is {@code null}. */ @Override - public DashMediaSource createMediaSource(Uri manifestUri) { - isCreateCalled = true; + public DashMediaSource createMediaSource(MediaItem mediaItem) { + Assertions.checkNotNull(mediaItem.playbackProperties); + @Nullable ParsingLoadable.Parser manifestParser = this.manifestParser; if (manifestParser == null) { manifestParser = new DashManifestParser(); } - if (streamKeys != null) { + List streamKeys = + !mediaItem.playbackProperties.streamKeys.isEmpty() + ? mediaItem.playbackProperties.streamKeys + : this.streamKeys; + if (!streamKeys.isEmpty()) { manifestParser = new FilteringManifestParser<>(manifestParser, streamKeys); } return new DashMediaSource( /* manifest= */ null, - Assertions.checkNotNull(manifestUri), + mediaItem.playbackProperties.uri, manifestDataSourceFactory, manifestParser, chunkSourceFactory, @@ -336,14 +350,7 @@ public final class DashMediaSource extends BaseMediaSource { loadErrorHandlingPolicy, livePresentationDelayMs, livePresentationDelayOverridesManifest, - tag); - } - - @Override - public Factory setStreamKeys(List streamKeys) { - Assertions.checkState(!isCreateCalled); - this.streamKeys = streamKeys; - return this; + mediaItem.playbackProperties.tag != null ? mediaItem.playbackProperties.tag : tag); } @Override @@ -381,7 +388,7 @@ public final class DashMediaSource extends BaseMediaSource { private final DataSource.Factory manifestDataSourceFactory; private final DashChunkSource.Factory chunkSourceFactory; private final CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory; - private final DrmSessionManager drmSessionManager; + private final DrmSessionManager drmSessionManager; private final LoadErrorHandlingPolicy loadErrorHandlingPolicy; private final long livePresentationDelayMs; private final boolean livePresentationDelayOverridesManifest; @@ -597,7 +604,7 @@ public final class DashMediaSource extends BaseMediaSource { @Nullable ParsingLoadable.Parser manifestParser, DashChunkSource.Factory chunkSourceFactory, CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory, - DrmSessionManager drmSessionManager, + DrmSessionManager drmSessionManager, LoadErrorHandlingPolicy loadErrorHandlingPolicy, long livePresentationDelayMs, boolean livePresentationDelayOverridesManifest, @@ -620,6 +627,7 @@ public final class DashMediaSource extends BaseMediaSource { periodsById = new SparseArray<>(); playerEmsgCallback = new DefaultPlayerEmsgCallback(); expiredManifestPublishTimeUs = C.TIME_UNSET; + elapsedRealtimeOffsetMs = C.TIME_UNSET; if (sideloadedManifest) { Assertions.checkState(!manifest.dynamic); manifestCallback = null; @@ -722,7 +730,7 @@ public final class DashMediaSource extends BaseMediaSource { handler.removeCallbacksAndMessages(null); handler = null; } - elapsedRealtimeOffsetMs = 0; + elapsedRealtimeOffsetMs = C.TIME_UNSET; staleManifestReloadAttempt = 0; expiredManifestPublishTimeUs = C.TIME_UNSET; firstPeriodId = 0; @@ -748,14 +756,17 @@ public final class DashMediaSource extends BaseMediaSource { /* package */ void onManifestLoadCompleted(ParsingLoadable loadable, long elapsedRealtimeMs, long loadDurationMs) { - manifestEventDispatcher.loadCompleted( - loadable.dataSpec, - loadable.getUri(), - loadable.getResponseHeaders(), - loadable.type, - elapsedRealtimeMs, - loadDurationMs, - loadable.bytesLoaded()); + LoadEventInfo loadEventInfo = + new LoadEventInfo( + loadable.loadTaskId, + loadable.dataSpec, + loadable.getUri(), + loadable.getResponseHeaders(), + elapsedRealtimeMs, + loadDurationMs, + loadable.bytesLoaded()); + loadErrorHandlingPolicy.onLoadTaskConcluded(loadable.loadTaskId); + manifestEventDispatcher.loadCompleted(loadEventInfo, loadable.type); DashManifest newManifest = loadable.getResult(); int oldPeriodCount = manifest == null ? 0 : manifest.getPeriodCount(); @@ -806,21 +817,28 @@ public final class DashMediaSource extends BaseMediaSource { manifestLoadPending &= manifest.dynamic; manifestLoadStartTimestampMs = elapsedRealtimeMs - loadDurationMs; manifestLoadEndTimestampMs = elapsedRealtimeMs; - if (manifest.location != null) { - 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. - @SuppressWarnings("ReferenceEquality") - boolean isSameUriInstance = loadable.dataSpec.uri == manifestUri; - if (isSameUriInstance) { - manifestUri = manifest.location; - } + + synchronized (manifestUriLock) { + // Checks whether replaceManifestUri(Uri) was called to manually replace the URI between the + // start and end of this load. If it was then isSameUriInstance evaluates to false, and we + // prefer the manual replacement to one derived from the previous request. + @SuppressWarnings("ReferenceEquality") + boolean isSameUriInstance = loadable.dataSpec.uri == manifestUri; + if (isSameUriInstance) { + // Replace the manifest URI with one specified by a manifest Location element (if present), + // or with the final (possibly redirected) URI. This follows the recommendation in + // DASH-IF-IOP 4.3, section 3.2.15.3. See: https://dashif.org/docs/DASH-IF-IOP-v4.3.pdf. + manifestUri = manifest.location != null ? manifest.location : loadable.getUri(); } } if (oldPeriodCount == 0) { - if (manifest.dynamic && manifest.utcTiming != null) { - resolveUtcTimingElement(manifest.utcTiming); + if (manifest.dynamic) { + if (manifest.utcTiming != null) { + resolveUtcTimingElement(manifest.utcTiming); + } else { + loadNtpTimeOffset(); + } } else { processManifest(true); } @@ -836,36 +854,44 @@ public final class DashMediaSource extends BaseMediaSource { long loadDurationMs, IOException error, int errorCount) { - long retryDelayMs = - loadErrorHandlingPolicy.getRetryDelayMsFor( - C.DATA_TYPE_MANIFEST, loadDurationMs, error, errorCount); + LoadEventInfo loadEventInfo = + new LoadEventInfo( + loadable.loadTaskId, + loadable.dataSpec, + loadable.getUri(), + loadable.getResponseHeaders(), + elapsedRealtimeMs, + loadDurationMs, + loadable.bytesLoaded()); + MediaLoadData mediaLoadData = new MediaLoadData(loadable.type); + LoadErrorInfo loadErrorInfo = + new LoadErrorInfo(loadEventInfo, mediaLoadData, error, errorCount); + long retryDelayMs = loadErrorHandlingPolicy.getRetryDelayMsFor(loadErrorInfo); LoadErrorAction loadErrorAction = retryDelayMs == C.TIME_UNSET ? Loader.DONT_RETRY_FATAL : Loader.createRetryAction(/* resetErrorCount= */ false, retryDelayMs); - manifestEventDispatcher.loadError( - loadable.dataSpec, - loadable.getUri(), - loadable.getResponseHeaders(), - loadable.type, - elapsedRealtimeMs, - loadDurationMs, - loadable.bytesLoaded(), - error, - !loadErrorAction.isRetry()); + boolean wasCanceled = !loadErrorAction.isRetry(); + manifestEventDispatcher.loadError(loadEventInfo, loadable.type, error, wasCanceled); + if (wasCanceled) { + loadErrorHandlingPolicy.onLoadTaskConcluded(loadable.loadTaskId); + } return loadErrorAction; } /* package */ void onUtcTimestampLoadCompleted(ParsingLoadable loadable, long elapsedRealtimeMs, long loadDurationMs) { - manifestEventDispatcher.loadCompleted( - loadable.dataSpec, - loadable.getUri(), - loadable.getResponseHeaders(), - loadable.type, - elapsedRealtimeMs, - loadDurationMs, - loadable.bytesLoaded()); + LoadEventInfo loadEventInfo = + new LoadEventInfo( + loadable.loadTaskId, + loadable.dataSpec, + loadable.getUri(), + loadable.getResponseHeaders(), + elapsedRealtimeMs, + loadDurationMs, + loadable.bytesLoaded()); + loadErrorHandlingPolicy.onLoadTaskConcluded(loadable.loadTaskId); + manifestEventDispatcher.loadCompleted(loadEventInfo, loadable.type); onUtcTimestampResolved(loadable.getResult() - elapsedRealtimeMs); } @@ -875,29 +901,35 @@ public final class DashMediaSource extends BaseMediaSource { long loadDurationMs, IOException error) { manifestEventDispatcher.loadError( - loadable.dataSpec, - loadable.getUri(), - loadable.getResponseHeaders(), + new LoadEventInfo( + loadable.loadTaskId, + loadable.dataSpec, + loadable.getUri(), + loadable.getResponseHeaders(), + elapsedRealtimeMs, + loadDurationMs, + loadable.bytesLoaded()), loadable.type, - elapsedRealtimeMs, - loadDurationMs, - loadable.bytesLoaded(), error, - true); + /* wasCanceled= */ true); + loadErrorHandlingPolicy.onLoadTaskConcluded(loadable.loadTaskId); onUtcTimestampResolutionError(error); return Loader.DONT_RETRY; } /* package */ void onLoadCanceled(ParsingLoadable loadable, long elapsedRealtimeMs, long loadDurationMs) { - manifestEventDispatcher.loadCanceled( - loadable.dataSpec, - loadable.getUri(), - loadable.getResponseHeaders(), - loadable.type, - elapsedRealtimeMs, - loadDurationMs, - loadable.bytesLoaded()); + LoadEventInfo loadEventInfo = + new LoadEventInfo( + loadable.loadTaskId, + loadable.dataSpec, + loadable.getUri(), + loadable.getResponseHeaders(), + elapsedRealtimeMs, + loadDurationMs, + loadable.bytesLoaded()); + loadErrorHandlingPolicy.onLoadTaskConcluded(loadable.loadTaskId); + manifestEventDispatcher.loadCanceled(loadEventInfo, loadable.type); } // Internal methods. @@ -913,6 +945,9 @@ public final class DashMediaSource extends BaseMediaSource { } else if (Util.areEqual(scheme, "urn:mpeg:dash:utc:http-xsdate:2014") || Util.areEqual(scheme, "urn:mpeg:dash:utc:http-xsdate:2012")) { resolveUtcTimingElementHttp(timingElement, new XsDateTimeParser()); + } else if (Util.areEqual(scheme, "urn:mpeg:dash:utc:ntp:2014") + || Util.areEqual(scheme, "urn:mpeg:dash:utc:ntp:2012")) { + loadNtpTimeOffset(); } else { // Unsupported scheme. onUtcTimestampResolutionError(new IOException("Unsupported UTC timing scheme")); @@ -934,13 +969,29 @@ public final class DashMediaSource extends BaseMediaSource { C.DATA_TYPE_TIME_SYNCHRONIZATION, parser), new UtcTimestampCallback(), 1); } + private void loadNtpTimeOffset() { + SntpClient.initialize( + loader, + new SntpClient.InitializationCallback() { + @Override + public void onInitialized() { + onUtcTimestampResolved(SntpClient.getElapsedRealtimeOffsetMs()); + } + + @Override + public void onInitializationFailed(IOException error) { + onUtcTimestampResolutionError(error); + } + }); + } + private void onUtcTimestampResolved(long elapsedRealtimeOffsetMs) { this.elapsedRealtimeOffsetMs = elapsedRealtimeOffsetMs; processManifest(true); } private void onUtcTimestampResolutionError(IOException error) { - Log.e(TAG, "Failed to resolve UtcTiming element.", error); + Log.e(TAG, "Failed to resolve time offset.", error); // Be optimistic and continue in the hope that the device clock is correct. processManifest(true); } @@ -968,7 +1019,8 @@ public final class DashMediaSource extends BaseMediaSource { if (manifest.dynamic && !lastPeriodSeekInfo.isIndexExplicit) { // The manifest describes an incomplete live stream. Update the start/end times to reflect the // live stream duration and the manifest's time shift buffer depth. - long liveStreamDurationUs = getNowUnixTimeUs() - C.msToUs(manifest.availabilityStartTimeMs); + long nowUnixTimeUs = C.msToUs(Util.getNowUnixTimeMs(elapsedRealtimeOffsetMs)); + long liveStreamDurationUs = nowUnixTimeUs - C.msToUs(manifest.availabilityStartTimeMs); long liveStreamEndPositionInLastPeriodUs = liveStreamDurationUs - C.msToUs(manifest.getPeriod(lastPeriodIndex).startMs); currentEndTimeUs = Math.min(liveStreamEndPositionInLastPeriodUs, currentEndTimeUs); @@ -1010,12 +1062,18 @@ public final class DashMediaSource extends BaseMediaSource { windowDurationUs / 2); } } - long windowStartTimeMs = manifest.availabilityStartTimeMs - + manifest.getPeriod(0).startMs + C.usToMs(currentStartTimeUs); + long windowStartTimeMs = C.TIME_UNSET; + if (manifest.availabilityStartTimeMs != C.TIME_UNSET) { + windowStartTimeMs = + manifest.availabilityStartTimeMs + + manifest.getPeriod(0).startMs + + C.usToMs(currentStartTimeUs); + } DashTimeline timeline = new DashTimeline( manifest.availabilityStartTimeMs, windowStartTimeMs, + elapsedRealtimeOffsetMs, firstPeriodId, currentStartTimeUs, windowDurationUs, @@ -1084,15 +1142,9 @@ 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); - } - - private long getNowUnixTimeUs() { - if (elapsedRealtimeOffsetMs != 0) { - return C.msToUs(SystemClock.elapsedRealtime() + elapsedRealtimeOffsetMs); - } else { - return C.msToUs(System.currentTimeMillis()); - } + manifestEventDispatcher.loadStarted( + new LoadEventInfo(loadable.loadTaskId, loadable.dataSpec, elapsedRealtimeMs), + loadable.type); } private static final class PeriodSeekInfo { @@ -1164,6 +1216,7 @@ public final class DashMediaSource extends BaseMediaSource { private final long presentationStartTimeMs; private final long windowStartTimeMs; + private final long elapsedRealtimeEpochOffsetMs; private final int firstPeriodId; private final long offsetInFirstPeriodUs; @@ -1175,6 +1228,7 @@ public final class DashMediaSource extends BaseMediaSource { public DashTimeline( long presentationStartTimeMs, long windowStartTimeMs, + long elapsedRealtimeEpochOffsetMs, int firstPeriodId, long offsetInFirstPeriodUs, long windowDurationUs, @@ -1183,6 +1237,7 @@ public final class DashMediaSource extends BaseMediaSource { @Nullable Object windowTag) { this.presentationStartTimeMs = presentationStartTimeMs; this.windowStartTimeMs = windowStartTimeMs; + this.elapsedRealtimeEpochOffsetMs = elapsedRealtimeEpochOffsetMs; this.firstPeriodId = firstPeriodId; this.offsetInFirstPeriodUs = offsetInFirstPeriodUs; this.windowDurationUs = windowDurationUs; @@ -1222,6 +1277,7 @@ public final class DashMediaSource extends BaseMediaSource { manifest, presentationStartTimeMs, windowStartTimeMs, + elapsedRealtimeEpochOffsetMs, /* isSeekable= */ true, /* isDynamic= */ isMovingLiveWindow(manifest), /* isLive= */ manifest.dynamic, @@ -1313,14 +1369,17 @@ public final class DashMediaSource extends BaseMediaSource { private final class ManifestCallback implements Loader.Callback> { @Override - public void onLoadCompleted(ParsingLoadable loadable, - long elapsedRealtimeMs, long loadDurationMs) { + public void onLoadCompleted( + ParsingLoadable loadable, long elapsedRealtimeMs, long loadDurationMs) { onManifestLoadCompleted(loadable, elapsedRealtimeMs, loadDurationMs); } @Override - public void onLoadCanceled(ParsingLoadable loadable, - long elapsedRealtimeMs, long loadDurationMs, boolean released) { + public void onLoadCanceled( + ParsingLoadable loadable, + long elapsedRealtimeMs, + long loadDurationMs, + boolean released) { DashMediaSource.this.onLoadCanceled(loadable, elapsedRealtimeMs, loadDurationMs); } @@ -1339,14 +1398,17 @@ public final class DashMediaSource extends BaseMediaSource { private final class UtcTimestampCallback implements Loader.Callback> { @Override - public void onLoadCompleted(ParsingLoadable loadable, long elapsedRealtimeMs, - long loadDurationMs) { + public void onLoadCompleted( + ParsingLoadable loadable, long elapsedRealtimeMs, long loadDurationMs) { onUtcTimestampLoadCompleted(loadable, elapsedRealtimeMs, loadDurationMs); } @Override - public void onLoadCanceled(ParsingLoadable loadable, long elapsedRealtimeMs, - long loadDurationMs, boolean released) { + public void onLoadCanceled( + ParsingLoadable loadable, + long elapsedRealtimeMs, + long loadDurationMs, + boolean released) { DashMediaSource.this.onLoadCanceled(loadable, elapsedRealtimeMs, loadDurationMs); } 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 c9433b9e41..6d440b96df 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 @@ -35,6 +35,7 @@ import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.upstream.HttpDataSource; import com.google.android.exoplayer2.upstream.ParsingLoadable; +import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.MimeTypes; import java.io.IOException; import java.util.List; @@ -44,6 +45,22 @@ import java.util.List; */ public final class DashUtil { + /** + * Builds a {@link DataSpec} for a given {@link RangedUri} belonging to {@link Representation}. + * + * @param representation The {@link Representation} to which the request belongs. + * @param requestUri The {@link RangedUri} of the data to request. + * @return The {@link DataSpec}. + */ + public static DataSpec buildDataSpec(Representation representation, RangedUri requestUri) { + return new DataSpec.Builder() + .setUri(requestUri.resolveUri(representation.baseUrl)) + .setPosition(requestUri.start) + .setLength(requestUri.length) + .setKey(representation.getCacheKey()) + .build(); + } + /** * Loads a DASH manifest. * @@ -52,8 +69,7 @@ public final class DashUtil { * @return An instance of {@link DashManifest}. * @throws IOException Thrown when there is an error while loading. */ - public static DashManifest loadManifest(DataSource dataSource, Uri uri) - throws IOException { + public static DashManifest loadManifest(DataSource dataSource, Uri uri) throws IOException { return ParsingLoadable.load(dataSource, new DashManifestParser(), uri, C.DATA_TYPE_MANIFEST); } @@ -64,11 +80,10 @@ public final class DashUtil { * @param period The {@link Period}. * @return The loaded {@link DrmInitData}, or null if none is defined. * @throws IOException Thrown when there is an error while loading. - * @throws InterruptedException Thrown if the thread was interrupted. */ @Nullable public static DrmInitData loadDrmInitData(DataSource dataSource, Period period) - throws IOException, InterruptedException { + throws IOException { int primaryTrackType = C.TRACK_TYPE_VIDEO; Representation representation = getFirstRepresentation(period, primaryTrackType); if (representation == null) { @@ -82,7 +97,7 @@ public final class DashUtil { Format sampleFormat = DashUtil.loadSampleFormat(dataSource, primaryTrackType, representation); return sampleFormat == null ? manifestFormat.drmInitData - : sampleFormat.copyWithManifestFormatInfo(manifestFormat).drmInitData; + : sampleFormat.withManifestFormatInfo(manifestFormat).drmInitData; } /** @@ -94,15 +109,15 @@ public final class DashUtil { * @param representation The representation which initialization chunk belongs to. * @return the sample {@link Format} of the given representation. * @throws IOException Thrown when there is an error while loading. - * @throws InterruptedException Thrown if the thread was interrupted. */ @Nullable public static Format loadSampleFormat( - DataSource dataSource, int trackType, Representation representation) - throws IOException, InterruptedException { + DataSource dataSource, int trackType, Representation representation) throws IOException { ChunkExtractorWrapper extractorWrapper = loadInitializationData(dataSource, trackType, representation, false); - return extractorWrapper == null ? null : extractorWrapper.getSampleFormats()[0]; + return extractorWrapper == null + ? null + : Assertions.checkStateNotNull(extractorWrapper.getSampleFormats())[0]; } /** @@ -116,12 +131,10 @@ public final class DashUtil { * @return The {@link ChunkIndex} of the given representation, or null if no initialization or * index data exists. * @throws IOException Thrown when there is an error while loading. - * @throws InterruptedException Thrown if the thread was interrupted. */ @Nullable public static ChunkIndex loadChunkIndex( - DataSource dataSource, int trackType, Representation representation) - throws IOException, InterruptedException { + DataSource dataSource, int trackType, Representation representation) throws IOException { ChunkExtractorWrapper extractorWrapper = loadInitializationData(dataSource, trackType, representation, true); return extractorWrapper == null ? null : (ChunkIndex) extractorWrapper.getSeekMap(); @@ -139,12 +152,11 @@ public final class DashUtil { * @return A {@link ChunkExtractorWrapper} for the {@code representation}, or null if no * initialization or (if requested) index data exists. * @throws IOException Thrown when there is an error while loading. - * @throws InterruptedException Thrown if the thread was interrupted. */ @Nullable private static ChunkExtractorWrapper loadInitializationData( DataSource dataSource, int trackType, Representation representation, boolean loadIndex) - throws IOException, InterruptedException { + throws IOException { RangedUri initializationUri = representation.getInitializationUri(); if (initializationUri == null) { return null; @@ -170,11 +182,13 @@ public final class DashUtil { return extractorWrapper; } - private static void loadInitializationData(DataSource dataSource, - Representation representation, ChunkExtractorWrapper extractorWrapper, RangedUri requestUri) - throws IOException, InterruptedException { - DataSpec dataSpec = new DataSpec(requestUri.resolveUri(representation.baseUrl), - requestUri.start, requestUri.length, representation.getCacheKey()); + private static void loadInitializationData( + DataSource dataSource, + Representation representation, + ChunkExtractorWrapper extractorWrapper, + RangedUri requestUri) + throws IOException { + DataSpec dataSpec = DashUtil.buildDataSpec(representation, requestUri); InitializationChunk initializationChunk = new InitializationChunk(dataSource, dataSpec, representation.format, C.SELECTION_REASON_UNKNOWN, null /* trackSelectionData */, extractorWrapper); 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 2904944493..e03ade2d48 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 @@ -136,7 +136,7 @@ public class DefaultDashChunkSource implements DashChunkSource { * @param dataSource A {@link DataSource} suitable for loading the media data. * @param elapsedRealtimeOffsetMs If known, an estimate of the instantaneous difference between * server-side unix time and {@link SystemClock#elapsedRealtime()} in milliseconds, specified - * as the server's unix time minus the local elapsed time. If unknown, set to 0. + * as the server's unix time minus the local elapsed time. Or {@link C#TIME_UNSET} if unknown. * @param maxSegmentsPerLoad The maximum number of segments to combine into a single request. Note * that segments will only be combined if their {@link Uri}s are the same and if their data * ranges are adjacent. @@ -198,7 +198,7 @@ public class DefaultDashChunkSource implements DashChunkSource { firstSyncUs < positionUs && segmentNum < representationHolder.getSegmentCount() - 1 ? representationHolder.getSegmentStartTimeUs(segmentNum + 1) : firstSyncUs; - return Util.resolveSeekPositionUs(positionUs, seekParameters, firstSyncUs, secondSyncUs); + return seekParameters.resolveSeekPositionUs(positionUs, firstSyncUs, secondSyncUs); } } // We don't have a segment index to adjust the seek position with yet. @@ -267,7 +267,7 @@ public class DefaultDashChunkSource implements DashChunkSource { return; } - long nowUnixTimeUs = getNowUnixTimeUs(); + long nowUnixTimeUs = C.msToUs(Util.getNowUnixTimeMs(elapsedRealtimeOffsetMs)); MediaChunk previous = queue.isEmpty() ? null : queue.get(queue.size() - 1); MediaChunkIterator[] chunkIterators = new MediaChunkIterator[trackSelection.length()]; for (int i = 0; i < chunkIterators.length; i++) { @@ -474,14 +474,6 @@ public class DefaultDashChunkSource implements DashChunkSource { ? representationHolder.getSegmentEndTimeUs(lastAvailableSegmentNum) : C.TIME_UNSET; } - private long getNowUnixTimeUs() { - if (elapsedRealtimeOffsetMs != 0) { - return (SystemClock.elapsedRealtime() + elapsedRealtimeOffsetMs) * 1000; - } else { - return System.currentTimeMillis() * 1000; - } - } - private long resolveTimeToLiveEdgeUs(long playbackPositionUs) { boolean resolveTimeToLiveEdgePossible = manifest.dynamic && liveEdgeTimeUs != C.TIME_UNSET; return resolveTimeToLiveEdgePossible ? liveEdgeTimeUs - playbackPositionUs : C.TIME_UNSET; @@ -495,20 +487,19 @@ public class DefaultDashChunkSource implements DashChunkSource { Object trackSelectionData, RangedUri initializationUri, RangedUri indexUri) { + Representation representation = representationHolder.representation; RangedUri requestUri; - String baseUrl = representationHolder.representation.baseUrl; if (initializationUri != null) { // It's common for initialization and index data to be stored adjacently. Attempt to merge // the two requests together to request both at once. - requestUri = initializationUri.attemptMerge(indexUri, baseUrl); + requestUri = initializationUri.attemptMerge(indexUri, representation.baseUrl); if (requestUri == null) { requestUri = initializationUri; } } else { requestUri = indexUri; } - DataSpec dataSpec = new DataSpec(requestUri.resolveUri(baseUrl), requestUri.start, - requestUri.length, representationHolder.representation.getCacheKey()); + DataSpec dataSpec = DashUtil.buildDataSpec(representation, requestUri); return new InitializationChunk(dataSource, dataSpec, trackFormat, trackSelectionReason, trackSelectionData, representationHolder.extractorWrapper); } @@ -529,15 +520,14 @@ public class DefaultDashChunkSource implements DashChunkSource { String baseUrl = representation.baseUrl; if (representationHolder.extractorWrapper == null) { long endTimeUs = representationHolder.getSegmentEndTimeUs(firstSegmentNum); - DataSpec dataSpec = new DataSpec(segmentUri.resolveUri(baseUrl), - segmentUri.start, segmentUri.length, representation.getCacheKey()); + DataSpec dataSpec = DashUtil.buildDataSpec(representation, segmentUri); return new SingleSampleMediaChunk(dataSource, dataSpec, trackFormat, trackSelectionReason, trackSelectionData, startTimeUs, endTimeUs, firstSegmentNum, trackType, trackFormat); } else { int segmentCount = 1; for (int i = 1; i < maxSegmentCount; i++) { RangedUri nextSegmentUri = representationHolder.getSegmentUrl(firstSegmentNum + i); - RangedUri mergedSegmentUri = segmentUri.attemptMerge(nextSegmentUri, baseUrl); + @Nullable RangedUri mergedSegmentUri = segmentUri.attemptMerge(nextSegmentUri, baseUrl); if (mergedSegmentUri == null) { // Unable to merge segment fetches because the URIs do not merge. break; @@ -551,8 +541,7 @@ public class DefaultDashChunkSource implements DashChunkSource { periodDurationUs != C.TIME_UNSET && periodDurationUs <= endTimeUs ? periodDurationUs : C.TIME_UNSET; - DataSpec dataSpec = new DataSpec(segmentUri.resolveUri(baseUrl), - segmentUri.start, segmentUri.length, representation.getCacheKey()); + DataSpec dataSpec = DashUtil.buildDataSpec(representation, segmentUri); long sampleOffsetUs = -representation.presentationTimeOffsetUs; return new ContainerMediaChunk( dataSource, @@ -596,11 +585,8 @@ public class DefaultDashChunkSource implements DashChunkSource { @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); + return DashUtil.buildDataSpec(representationHolder.representation, segmentUri); } @Override @@ -786,10 +772,6 @@ public class DefaultDashChunkSource implements DashChunkSource { || mimeType.startsWith(MimeTypes.APPLICATION_WEBM); } - private static boolean mimeTypeIsRawText(String mimeType) { - return MimeTypes.isText(mimeType) || MimeTypes.APPLICATION_TTML.equals(mimeType); - } - private static @Nullable ChunkExtractorWrapper createExtractorWrapper( int trackType, Representation representation, @@ -797,12 +779,15 @@ public class DefaultDashChunkSource implements DashChunkSource { List closedCaptionFormats, @Nullable 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); + if (MimeTypes.isText(containerMimeType)) { + if (MimeTypes.APPLICATION_RAWCC.equals(containerMimeType)) { + // RawCC is special because it's a text specific container format. + extractor = new RawCcExtractor(representation.format); + } else { + // All other text types are raw formats that do not need an extractor. + return null; + } } else if (mimeTypeIsWebm(containerMimeType)) { extractor = new MatroskaExtractor(MatroskaExtractor.FLAG_DISABLE_SEEK_FOR_CUES); } else { @@ -812,10 +797,12 @@ public class DefaultDashChunkSource implements DashChunkSource { } extractor = new FragmentedMp4Extractor( - flags, null, null, null, closedCaptionFormats, playerEmsgTrackOutput); + flags, + /* timestampAdjuster= */ null, + /* sideloadedTrack= */ 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/PlayerEmsgHandler.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/PlayerEmsgHandler.java index ab7eb18e0d..7888841e23 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 @@ -25,7 +25,6 @@ import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.FormatHolder; import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.drm.DrmSessionManager; -import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.MetadataInputBuffer; @@ -35,6 +34,8 @@ import com.google.android.exoplayer2.source.SampleQueue; 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.upstream.DataReader; +import com.google.android.exoplayer2.util.MediaSourceEventDispatcher; import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.Util; import java.io.IOException; @@ -196,8 +197,7 @@ public final class PlayerEmsgHandler implements Handler.Callback { /** Returns a {@link TrackOutput} that emsg messages could be written to. */ public PlayerTrackEmsgHandler newPlayerTrackEmsgHandler() { - return new PlayerTrackEmsgHandler( - new SampleQueue(allocator, DrmSessionManager.getDummyDrmSessionManager())); + return new PlayerTrackEmsgHandler(allocator); } /** Release this emsg handler. It should not be reused after this call. */ @@ -284,9 +284,13 @@ public final class PlayerEmsgHandler implements Handler.Callback { private final FormatHolder formatHolder; private final MetadataInputBuffer buffer; - /* package */ PlayerTrackEmsgHandler(SampleQueue sampleQueue) { - this.sampleQueue = sampleQueue; - + /* package */ PlayerTrackEmsgHandler(Allocator allocator) { + this.sampleQueue = + new SampleQueue( + allocator, + /* playbackLooper= */ handler.getLooper(), + DrmSessionManager.getDummyDrmSessionManager(), + new MediaSourceEventDispatcher()); formatHolder = new FormatHolder(); buffer = new MetadataInputBuffer(); } @@ -297,13 +301,14 @@ public final class PlayerEmsgHandler implements Handler.Callback { } @Override - public int sampleData(ExtractorInput input, int length, boolean allowEndOfInput) - throws IOException, InterruptedException { + public int sampleData( + DataReader input, int length, boolean allowEndOfInput, @SampleDataPart int sampleDataPart) + throws IOException { return sampleQueue.sampleData(input, length, allowEndOfInput); } @Override - public void sampleData(ParsableByteArray data, int length) { + public void sampleData(ParsableByteArray data, int length, @SampleDataPart int sampleDataPart) { sampleQueue.sampleData(data, length); } @@ -349,7 +354,7 @@ public final class PlayerEmsgHandler implements Handler.Callback { /** Release this track emsg handler. It should not be reused after this call. */ public void release() { - sampleQueue.reset(); + sampleQueue.release(); } // Internal methods. 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 d962374745..b0689eeb11 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 @@ -50,9 +50,10 @@ public class AdaptationSet { */ public final List accessibilityDescriptors; - /** - * Supplemental properties in the adaptation set. - */ + /** Essential properties in the adaptation set. */ + public final List essentialProperties; + + /** Supplemental properties in the adaptation set. */ public final List supplementalProperties; /** @@ -62,21 +63,21 @@ public class AdaptationSet { * {@code TRACK_TYPE_*} constants. * @param representations {@link Representation}s in the adaptation set. * @param accessibilityDescriptors Accessibility descriptors in the adaptation set. + * @param essentialProperties Essential properties in the adaptation set. * @param supplementalProperties Supplemental properties in the adaptation set. */ - public AdaptationSet(int id, int type, List representations, - List accessibilityDescriptors, List supplementalProperties) { + public AdaptationSet( + int id, + int type, + List representations, + List accessibilityDescriptors, + List essentialProperties, + List supplementalProperties) { 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 = Collections.unmodifiableList(accessibilityDescriptors); + this.essentialProperties = Collections.unmodifiableList(essentialProperties); + this.supplementalProperties = 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 2d8909f8b4..c21af45d15 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 @@ -224,9 +224,14 @@ public class DashManifest implements FilterableManifest { key = keys.poll(); } while (key.periodIndex == periodIndex && key.groupIndex == adaptationSetIndex); - copyAdaptationSets.add(new AdaptationSet(adaptationSet.id, adaptationSet.type, - copyRepresentations, adaptationSet.accessibilityDescriptors, - adaptationSet.supplementalProperties)); + copyAdaptationSets.add( + new AdaptationSet( + adaptationSet.id, + adaptationSet.type, + copyRepresentations, + adaptationSet.accessibilityDescriptors, + adaptationSet.essentialProperties, + adaptationSet.supplementalProperties)); } while(key.periodIndex == periodIndex); // Add back the last key which doesn't belong to the period being processed keys.addFirst(key); 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 b107be4794..23f264e64b 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 @@ -222,10 +222,11 @@ public class DashManifestParser extends DefaultHandler protected Pair parsePeriod(XmlPullParser xpp, String baseUrl, long defaultStartMs) throws XmlPullParserException, IOException { - String id = xpp.getAttributeValue(null, "id"); + @Nullable String id = xpp.getAttributeValue(null, "id"); long startMs = parseDuration(xpp, "start", defaultStartMs); long durationMs = parseDuration(xpp, "duration", C.TIME_UNSET); - SegmentBase segmentBase = null; + @Nullable SegmentBase segmentBase = null; + @Nullable Descriptor assetIdentifier = null; List adaptationSets = new ArrayList<>(); List eventStreams = new ArrayList<>(); boolean seenFirstBaseUrl = false; @@ -246,17 +247,24 @@ public class DashManifestParser extends DefaultHandler segmentBase = parseSegmentList(xpp, null, durationMs); } else if (XmlPullParserUtil.isStartTag(xpp, "SegmentTemplate")) { segmentBase = parseSegmentTemplate(xpp, null, Collections.emptyList(), durationMs); + } else if (XmlPullParserUtil.isStartTag(xpp, "AssetIdentifier")) { + assetIdentifier = parseDescriptor(xpp, "AssetIdentifier"); } else { maybeSkipTag(xpp); } } while (!XmlPullParserUtil.isEndTag(xpp, "Period")); - return Pair.create(buildPeriod(id, startMs, adaptationSets, eventStreams), durationMs); + return Pair.create( + buildPeriod(id, startMs, adaptationSets, eventStreams, assetIdentifier), durationMs); } - protected Period buildPeriod(String id, long startMs, List adaptationSets, - List eventStreams) { - return new Period(id, startMs, adaptationSets, eventStreams); + protected Period buildPeriod( + @Nullable String id, + long startMs, + List adaptationSets, + List eventStreams, + @Nullable Descriptor assetIdentifier) { + return new Period(id, startMs, adaptationSets, eventStreams, assetIdentifier); } // AdaptationSet parsing. @@ -281,6 +289,7 @@ public class DashManifestParser extends DefaultHandler ArrayList inbandEventStreams = new ArrayList<>(); ArrayList accessibilityDescriptors = new ArrayList<>(); ArrayList roleDescriptors = new ArrayList<>(); + ArrayList essentialProperties = new ArrayList<>(); ArrayList supplementalProperties = new ArrayList<>(); List representationInfos = new ArrayList<>(); @@ -309,6 +318,8 @@ public class DashManifestParser extends DefaultHandler audioChannels = parseAudioChannelConfiguration(xpp); } else if (XmlPullParserUtil.isStartTag(xpp, "Accessibility")) { accessibilityDescriptors.add(parseDescriptor(xpp, "Accessibility")); + } else if (XmlPullParserUtil.isStartTag(xpp, "EssentialProperty")) { + essentialProperties.add(parseDescriptor(xpp, "EssentialProperty")); } else if (XmlPullParserUtil.isStartTag(xpp, "SupplementalProperty")) { supplementalProperties.add(parseDescriptor(xpp, "SupplementalProperty")); } else if (XmlPullParserUtil.isStartTag(xpp, "Representation")) { @@ -326,11 +337,13 @@ public class DashManifestParser extends DefaultHandler language, roleDescriptors, accessibilityDescriptors, + essentialProperties, supplementalProperties, segmentBase, periodDurationMs); - contentType = checkContentTypeConsistency(contentType, - getContentType(representationInfo.format)); + contentType = + checkContentTypeConsistency( + contentType, MimeTypes.getTrackType(representationInfo.format.sampleMimeType)); representationInfos.add(representationInfo); } else if (XmlPullParserUtil.isStartTag(xpp, "SegmentBase")) { segmentBase = parseSegmentBase(xpp, (SingleSegmentBase) segmentBase); @@ -361,14 +374,28 @@ public class DashManifestParser extends DefaultHandler inbandEventStreams)); } - return buildAdaptationSet(id, contentType, representations, accessibilityDescriptors, + return buildAdaptationSet( + id, + contentType, + representations, + accessibilityDescriptors, + essentialProperties, supplementalProperties); } - protected AdaptationSet buildAdaptationSet(int id, int contentType, - List representations, List accessibilityDescriptors, + protected AdaptationSet buildAdaptationSet( + int id, + int contentType, + List representations, + List accessibilityDescriptors, + List essentialProperties, List supplementalProperties) { - return new AdaptationSet(id, contentType, representations, accessibilityDescriptors, + return new AdaptationSet( + id, + contentType, + representations, + accessibilityDescriptors, + essentialProperties, supplementalProperties); } @@ -381,20 +408,6 @@ public class DashManifestParser extends DefaultHandler : C.TRACK_TYPE_UNKNOWN; } - protected int getContentType(Format format) { - String sampleMimeType = format.sampleMimeType; - if (TextUtils.isEmpty(sampleMimeType)) { - return C.TRACK_TYPE_UNKNOWN; - } else if (MimeTypes.isVideo(sampleMimeType)) { - return C.TRACK_TYPE_VIDEO; - } else if (MimeTypes.isAudio(sampleMimeType)) { - return C.TRACK_TYPE_AUDIO; - } else if (mimeTypeIsRawText(sampleMimeType)) { - return C.TRACK_TYPE_TEXT; - } - return C.TRACK_TYPE_UNKNOWN; - } - /** * Parses a ContentProtection element. * @@ -497,6 +510,7 @@ public class DashManifestParser extends DefaultHandler @Nullable String adaptationSetLanguage, List adaptationSetRoleDescriptors, List adaptationSetAccessibilityDescriptors, + List adaptationSetEssentialProperties, List adaptationSetSupplementalProperties, @Nullable SegmentBase segmentBase, long periodDurationMs) @@ -514,7 +528,9 @@ public class DashManifestParser extends DefaultHandler String drmSchemeType = null; ArrayList drmSchemeDatas = new ArrayList<>(); ArrayList inbandEventStreams = new ArrayList<>(); - ArrayList supplementalProperties = new ArrayList<>(); + ArrayList essentialProperties = new ArrayList<>(adaptationSetEssentialProperties); + ArrayList supplementalProperties = + new ArrayList<>(adaptationSetSupplementalProperties); boolean seenFirstBaseUrl = false; do { @@ -547,6 +563,8 @@ public class DashManifestParser extends DefaultHandler } } else if (XmlPullParserUtil.isStartTag(xpp, "InbandEventStream")) { inbandEventStreams.add(parseDescriptor(xpp, "InbandEventStream")); + } else if (XmlPullParserUtil.isStartTag(xpp, "EssentialProperty")) { + essentialProperties.add(parseDescriptor(xpp, "EssentialProperty")); } else if (XmlPullParserUtil.isStartTag(xpp, "SupplementalProperty")) { supplementalProperties.add(parseDescriptor(xpp, "SupplementalProperty")); } else { @@ -568,6 +586,7 @@ public class DashManifestParser extends DefaultHandler adaptationSetRoleDescriptors, adaptationSetAccessibilityDescriptors, codecs, + essentialProperties, supplementalProperties); segmentBase = segmentBase != null ? segmentBase : new SingleSegmentBase(); @@ -588,77 +607,44 @@ public class DashManifestParser extends DefaultHandler List roleDescriptors, List accessibilityDescriptors, @Nullable String codecs, + List essentialProperties, List supplementalProperties) { - String sampleMimeType = getSampleMimeType(containerMimeType, codecs); + @Nullable String sampleMimeType = getSampleMimeType(containerMimeType, codecs); + if (MimeTypes.AUDIO_E_AC3.equals(sampleMimeType)) { + sampleMimeType = parseEac3SupplementalProperties(supplementalProperties); + } @C.SelectionFlags int selectionFlags = parseSelectionFlagsFromRoleDescriptors(roleDescriptors); @C.RoleFlags int roleFlags = parseRoleFlagsFromRoleDescriptors(roleDescriptors); roleFlags |= parseRoleFlagsFromAccessibilityDescriptors(accessibilityDescriptors); - if (sampleMimeType != null) { - if (MimeTypes.AUDIO_E_AC3.equals(sampleMimeType)) { - sampleMimeType = parseEac3SupplementalProperties(supplementalProperties); - } - if (MimeTypes.isVideo(sampleMimeType)) { - return Format.createVideoContainerFormat( - id, - /* label= */ null, - containerMimeType, - sampleMimeType, - codecs, - /* metadata= */ null, - bitrate, - width, - height, - frameRate, - /* initializationData= */ null, - selectionFlags, - roleFlags); - } else if (MimeTypes.isAudio(sampleMimeType)) { - return Format.createAudioContainerFormat( - id, - /* label= */ null, - containerMimeType, - sampleMimeType, - codecs, - /* metadata= */ null, - bitrate, - audioChannels, - audioSamplingRate, - /* initializationData= */ null, - selectionFlags, - roleFlags, - language); - } else if (mimeTypeIsRawText(sampleMimeType)) { - int accessibilityChannel; - if (MimeTypes.APPLICATION_CEA608.equals(sampleMimeType)) { - accessibilityChannel = parseCea608AccessibilityChannel(accessibilityDescriptors); - } else if (MimeTypes.APPLICATION_CEA708.equals(sampleMimeType)) { - accessibilityChannel = parseCea708AccessibilityChannel(accessibilityDescriptors); - } else { - accessibilityChannel = Format.NO_VALUE; - } - return Format.createTextContainerFormat( - id, - /* label= */ null, - containerMimeType, - sampleMimeType, - codecs, - bitrate, - selectionFlags, - roleFlags, - language, - accessibilityChannel); + roleFlags |= parseRoleFlagsFromProperties(essentialProperties); + roleFlags |= parseRoleFlagsFromProperties(supplementalProperties); + + Format.Builder formatBuilder = + new Format.Builder() + .setId(id) + .setContainerMimeType(containerMimeType) + .setSampleMimeType(sampleMimeType) + .setCodecs(codecs) + .setPeakBitrate(bitrate) + .setSelectionFlags(selectionFlags) + .setRoleFlags(roleFlags) + .setLanguage(language); + + if (MimeTypes.isVideo(sampleMimeType)) { + formatBuilder.setWidth(width).setHeight(height).setFrameRate(frameRate); + } else if (MimeTypes.isAudio(sampleMimeType)) { + formatBuilder.setChannelCount(audioChannels).setSampleRate(audioSamplingRate); + } else if (MimeTypes.isText(sampleMimeType)) { + int accessibilityChannel = Format.NO_VALUE; + if (MimeTypes.APPLICATION_CEA608.equals(sampleMimeType)) { + accessibilityChannel = parseCea608AccessibilityChannel(accessibilityDescriptors); + } else if (MimeTypes.APPLICATION_CEA708.equals(sampleMimeType)) { + accessibilityChannel = parseCea708AccessibilityChannel(accessibilityDescriptors); } + formatBuilder.setAccessibilityChannel(accessibilityChannel); } - return Format.createContainerFormat( - id, - /* label= */ null, - containerMimeType, - sampleMimeType, - codecs, - bitrate, - selectionFlags, - roleFlags, - language); + + return formatBuilder.build(); } protected Representation buildRepresentation( @@ -667,24 +653,25 @@ public class DashManifestParser extends DefaultHandler @Nullable String extraDrmSchemeType, ArrayList extraDrmSchemeDatas, ArrayList extraInbandEventStreams) { - Format format = representationInfo.format; + Format.Builder formatBuilder = representationInfo.format.buildUpon(); if (label != null) { - format = format.copyWithLabel(label); + formatBuilder.setLabel(label); + } + @Nullable String drmSchemeType = representationInfo.drmSchemeType; + if (drmSchemeType == null) { + drmSchemeType = extraDrmSchemeType; } - String drmSchemeType = representationInfo.drmSchemeType != null - ? representationInfo.drmSchemeType : extraDrmSchemeType; ArrayList drmSchemeDatas = representationInfo.drmSchemeDatas; drmSchemeDatas.addAll(extraDrmSchemeDatas); if (!drmSchemeDatas.isEmpty()) { filterRedundantIncompleteSchemeDatas(drmSchemeDatas); - DrmInitData drmInitData = new DrmInitData(drmSchemeType, drmSchemeDatas); - format = format.copyWithDrmInitData(drmInitData); + formatBuilder.setDrmInitData(new DrmInitData(drmSchemeType, drmSchemeDatas)); } ArrayList inbandEventStreams = representationInfo.inbandEventStreams; inbandEventStreams.addAll(extraInbandEventStreams); return Representation.newInstance( representationInfo.revisionId, - format, + formatBuilder.build(), representationInfo.baseUrl, representationInfo.segmentBase, inbandEventStreams); @@ -709,7 +696,7 @@ public class DashManifestParser extends DefaultHandler indexLength = Long.parseLong(indexRange[1]) - indexStart + 1; } - RangedUri initialization = parent != null ? parent.initialization : null; + @Nullable RangedUri initialization = parent != null ? parent.initialization : null; do { xpp.next(); if (XmlPullParserUtil.isStartTag(xpp, "Initialization")) { @@ -1225,6 +1212,18 @@ public class DashManifestParser extends DefaultHandler return result; } + @C.RoleFlags + protected int parseRoleFlagsFromProperties(List accessibilityDescriptors) { + @C.RoleFlags int result = 0; + for (int i = 0; i < accessibilityDescriptors.size(); i++) { + Descriptor descriptor = accessibilityDescriptors.get(i); + if ("http://dashif.org/guidelines/trickmode".equalsIgnoreCase(descriptor.schemeIdUri)) { + result |= C.ROLE_FLAG_TRICK_PLAY; + } + } + return result; + } + @C.RoleFlags protected int parseDashRoleSchemeValue(@Nullable String value) { if (value == null) { @@ -1337,43 +1336,19 @@ public class DashManifestParser extends DefaultHandler return MimeTypes.getAudioMediaMimeType(codecs); } else if (MimeTypes.isVideo(containerMimeType)) { return MimeTypes.getVideoMediaMimeType(codecs); - } else if (mimeTypeIsRawText(containerMimeType)) { + } else if (MimeTypes.isText(containerMimeType)) { + if (MimeTypes.APPLICATION_RAWCC.equals(containerMimeType)) { + // RawCC is special because it's a text specific container format. + return MimeTypes.getTextMediaMimeType(codecs); + } + // All other text types are raw formats. return containerMimeType; } else if (MimeTypes.APPLICATION_MP4.equals(containerMimeType)) { - 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) { - if (codecs.contains("cea708")) { - return MimeTypes.APPLICATION_CEA708; - } else if (codecs.contains("eia608") || codecs.contains("cea608")) { - return MimeTypes.APPLICATION_CEA608; - } - } - return null; + return MimeTypes.getMediaMimeType(codecs); } return null; } - /** - * Returns whether a mimeType is a text sample mimeType. - * - * @param mimeType The mimeType. - * @return Whether the mimeType is a text sample mimeType. - */ - private static boolean mimeTypeIsRawText(@Nullable String mimeType) { - return MimeTypes.isText(mimeType) - || MimeTypes.APPLICATION_TTML.equals(mimeType) - || MimeTypes.APPLICATION_MP4VTT.equals(mimeType) - || MimeTypes.APPLICATION_CEA708.equals(mimeType) - || MimeTypes.APPLICATION_CEA608.equals(mimeType); - } - /** * Checks two languages for consistency, returning the consistent language, or throwing an {@link * IllegalStateException} if the languages are inconsistent. 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 18614ca4b0..b472aed50c 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 @@ -45,13 +45,16 @@ public class Period { */ public final List eventStreams; + /** The asset identifier for this period, if one exists */ + @Nullable public final Descriptor assetIdentifier; + /** * @param id The period identifier. May be null. * @param startMs The start time of the period in milliseconds. * @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(), /* assetIdentifier= */ null); } /** @@ -62,10 +65,27 @@ public class Period { */ public Period(@Nullable String id, long startMs, List adaptationSets, List eventStreams) { + this(id, startMs, adaptationSets, eventStreams, /* assetIdentifier= */ null); + } + + /** + * @param id The period identifier. May be null. + * @param startMs The start time of the period in milliseconds. + * @param adaptationSets The adaptation sets belonging to the period. + * @param eventStreams The {@link EventStream}s belonging to the period. + * @param assetIdentifier The asset identifier for this period + */ + public Period( + @Nullable String id, + long startMs, + List adaptationSets, + List eventStreams, + @Nullable Descriptor assetIdentifier) { this.id = id; this.startMs = startMs; this.adaptationSets = Collections.unmodifiableList(adaptationSets); this.eventStreams = Collections.unmodifiableList(eventStreams); + this.assetIdentifier = assetIdentifier; } /** diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/SegmentBase.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/SegmentBase.java index db7c8d6471..b5ca31c151 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/SegmentBase.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/SegmentBase.java @@ -26,7 +26,7 @@ import java.util.List; */ public abstract class SegmentBase { - /* package */ @Nullable final RangedUri initialization; + @Nullable /* package */ final RangedUri initialization; /* package */ final long timescale; /* package */ final long presentationTimeOffset; @@ -116,7 +116,7 @@ public abstract class SegmentBase { /* package */ final long startNumber; /* package */ final long duration; - /* package */ @Nullable final List segmentTimeline; + @Nullable /* package */ final List segmentTimeline; /** * @param initialization A {@link RangedUri} corresponding to initialization data, if such data @@ -233,12 +233,10 @@ public abstract class SegmentBase { } - /** - * A {@link MultiSegmentBase} that uses a SegmentList to define its segments. - */ - public static class SegmentList extends MultiSegmentBase { + /** A {@link MultiSegmentBase} that uses a SegmentList to define its segments. */ + public static final class SegmentList extends MultiSegmentBase { - /* package */ @Nullable final List mediaSegments; + @Nullable /* package */ final List mediaSegments; /** * @param initialization A {@link RangedUri} corresponding to initialization data, if such data @@ -285,13 +283,11 @@ public abstract class SegmentBase { } - /** - * A {@link MultiSegmentBase} that uses a SegmentTemplate to define its segments. - */ - public static class SegmentTemplate extends MultiSegmentBase { + /** A {@link MultiSegmentBase} that uses a SegmentTemplate to define its segments. */ + public static final class SegmentTemplate extends MultiSegmentBase { - /* package */ @Nullable final UrlTemplate initializationTemplate; - /* package */ @Nullable final UrlTemplate mediaTemplate; + @Nullable /* package */ final UrlTemplate initializationTemplate; + @Nullable /* package */ final UrlTemplate mediaTemplate; /* package */ final long endNumber; /** @@ -378,10 +374,8 @@ public abstract class SegmentBase { } } - /** - * Represents a timeline segment from the MPD's SegmentTimeline list. - */ - public static class SegmentTimelineElement { + /** Represents a timeline segment from the MPD's SegmentTimeline list. */ + public static final class SegmentTimelineElement { /* package */ final long startTime; /* package */ final long duration; 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 2754a3341a..7b85d46f66 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 @@ -20,7 +20,6 @@ import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; 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; @@ -34,10 +33,11 @@ import com.google.android.exoplayer2.source.dash.manifest.RangedUri; import com.google.android.exoplayer2.source.dash.manifest.Representation; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSpec; -import com.google.android.exoplayer2.upstream.ParsingLoadable; +import com.google.android.exoplayer2.upstream.cache.CacheDataSource; import java.io.IOException; import java.util.ArrayList; import java.util.List; +import java.util.concurrent.Executor; /** * A downloader for DASH streams. @@ -46,19 +46,20 @@ import java.util.List; * *

        {@code
          * SimpleCache cache = new SimpleCache(downloadFolder, new NoOpCacheEvictor(), databaseProvider);
        - * DefaultHttpDataSourceFactory factory = new DefaultHttpDataSourceFactory("ExoPlayer", null);
        - * DownloaderConstructorHelper constructorHelper =
        - *     new DownloaderConstructorHelper(cache, factory);
        + * CacheDataSource.Factory cacheDataSourceFactory =
        + *     new CacheDataSource.Factory()
        + *         .setCache(cache)
        + *         .setUpstreamDataSourceFactory(new DefaultHttpDataSourceFactory(userAgent));
          * // Create a downloader for the first representation of the first adaptation set of the first
          * // period.
          * DashDownloader dashDownloader =
          *     new DashDownloader(
        - *         manifestUrl, Collections.singletonList(new StreamKey(0, 0, 0)), constructorHelper);
        + *         manifestUrl, Collections.singletonList(new StreamKey(0, 0, 0)), cacheDataSourceFactory);
          * // Perform the download.
          * dashDownloader.download(progressListener);
        - * // Access downloaded data using CacheDataSource
        - * CacheDataSource cacheDataSource =
        - *     new CacheDataSource(cache, factory.createDataSource(), CacheDataSource.FLAG_BLOCK_ON_CACHE);
        + * // Use the downloaded data for playback.
        + * DashMediaSource mediaSource =
        + *     new DashMediaSource.Factory(cacheDataSourceFactory).createMediaSource(mediaItem);
          * }
        */ public final class DashDownloader extends SegmentDownloader { @@ -67,23 +68,36 @@ public final class DashDownloader extends SegmentDownloader { * @param manifestUri The {@link Uri} of the manifest to be 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. + * @param cacheDataSourceFactory A {@link CacheDataSource.Factory} for the cache into which the + * download will be written. */ public DashDownloader( - Uri manifestUri, List streamKeys, DownloaderConstructorHelper constructorHelper) { - super(manifestUri, streamKeys, constructorHelper); + Uri manifestUri, List streamKeys, CacheDataSource.Factory cacheDataSourceFactory) { + this(manifestUri, streamKeys, cacheDataSourceFactory, Runnable::run); } - @Override - protected DashManifest getManifest(DataSource dataSource, DataSpec dataSpec) throws IOException { - return ParsingLoadable.load( - dataSource, new DashManifestParser(), dataSpec, C.DATA_TYPE_MANIFEST); + /** + * @param manifestUri The {@link Uri} of the manifest to be downloaded. + * @param streamKeys Keys defining which representations in the manifest should be selected for + * download. If empty, all representations are downloaded. + * @param cacheDataSourceFactory A {@link CacheDataSource.Factory} for the cache into which the + * download will be written. + * @param executor An {@link Executor} used to make requests for the media being downloaded. + * Providing an {@link Executor} that uses multiple threads will speed up the download by + * allowing parts of it to be executed in parallel. + */ + public DashDownloader( + Uri manifestUri, + List streamKeys, + CacheDataSource.Factory cacheDataSourceFactory, + Executor executor) { + super(manifestUri, new DashManifestParser(), streamKeys, cacheDataSourceFactory, executor); } @Override protected List getSegments( DataSource dataSource, DashManifest manifest, boolean allowIncompleteList) - throws InterruptedException, IOException { + throws IOException { ArrayList segments = new ArrayList<>(); for (int i = 0; i < manifest.getPeriodCount(); i++) { Period period = manifest.getPeriod(i); @@ -110,7 +124,7 @@ public final class DashDownloader extends SegmentDownloader { long periodDurationUs, boolean allowIncompleteList, ArrayList out) - throws IOException, InterruptedException { + throws IOException { for (int i = 0; i < adaptationSet.representations.size(); i++) { Representation representation = adaptationSet.representations.get(i); DashSegmentIndex index; @@ -153,13 +167,12 @@ public final class DashDownloader extends SegmentDownloader { private static void addSegment( long startTimeUs, String baseUrl, RangedUri rangedUri, ArrayList out) { DataSpec dataSpec = - new DataSpec(rangedUri.resolveUri(baseUrl), rangedUri.start, rangedUri.length, null); + new DataSpec(rangedUri.resolveUri(baseUrl), rangedUri.start, rangedUri.length); out.add(new Segment(startTimeUs, dataSpec)); } private static @Nullable DashSegmentIndex getSegmentIndex( - DataSource dataSource, int trackType, Representation representation) - throws IOException, InterruptedException { + DataSource dataSource, int trackType, Representation representation) throws IOException { DashSegmentIndex index = representation.getIndex(); if (index != null) { return index; diff --git a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/DashMediaPeriodTest.java b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/DashMediaPeriodTest.java index f39a493e9f..5a5318c670 100644 --- a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/DashMediaPeriodTest.java +++ b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/DashMediaPeriodTest.java @@ -26,6 +26,8 @@ import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.source.CompositeSequenceableLoaderFactory; import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; +import com.google.android.exoplayer2.source.TrackGroup; +import com.google.android.exoplayer2.source.TrackGroupArray; 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; @@ -35,7 +37,6 @@ import com.google.android.exoplayer2.source.dash.manifest.Representation; import com.google.android.exoplayer2.source.dash.manifest.SegmentBase.SingleSegmentBase; import com.google.android.exoplayer2.source.dash.manifest.UtcTimingElement; import com.google.android.exoplayer2.testutil.MediaPeriodAsserts; -import com.google.android.exoplayer2.testutil.MediaPeriodAsserts.FilterableManifestMediaPeriodFactory; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy; import com.google.android.exoplayer2.upstream.LoaderErrorThrower; @@ -43,6 +44,7 @@ import com.google.android.exoplayer2.upstream.TransferListener; import com.google.android.exoplayer2.util.MimeTypes; import java.util.Arrays; import java.util.Collections; +import java.util.List; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.annotation.LooperMode; @@ -53,7 +55,7 @@ import org.robolectric.annotation.LooperMode; public final class DashMediaPeriodTest { @Test - public void getSteamKeys_isCompatibleWithDashManifestFilter() { + public void getStreamKeys_isCompatibleWithDashManifestFilter() { // Test manifest which covers various edge cases: // - Multiple periods. // - Single and multiple representations per adaptation set. @@ -61,83 +63,220 @@ public final class DashMediaPeriodTest { // - Embedded track groups. // All cases are deliberately combined in one test to catch potential indexing problems which // only occur in combination. - DashManifest testManifest = + DashManifest manifest = createDashManifest( createPeriod( createAdaptationSet( /* id= */ 0, - /* trackType= */ C.TRACK_TYPE_VIDEO, + C.TRACK_TYPE_VIDEO, /* descriptor= */ null, createVideoRepresentation(/* bitrate= */ 1000000))), createPeriod( createAdaptationSet( /* id= */ 100, - /* trackType= */ C.TRACK_TYPE_VIDEO, - /* descriptor= */ createSwitchDescriptor(/* ids= */ 103, 104), + C.TRACK_TYPE_VIDEO, + createSwitchDescriptor(/* ids...= */ 103, 104), createVideoRepresentationWithInbandEventStream(/* bitrate= */ 200000), createVideoRepresentationWithInbandEventStream(/* bitrate= */ 400000), createVideoRepresentationWithInbandEventStream(/* bitrate= */ 600000)), createAdaptationSet( /* id= */ 101, - /* trackType= */ C.TRACK_TYPE_AUDIO, - /* descriptor= */ createSwitchDescriptor(/* ids= */ 102), + C.TRACK_TYPE_AUDIO, + createSwitchDescriptor(/* ids...= */ 102), createAudioRepresentation(/* bitrate= */ 48000), createAudioRepresentation(/* bitrate= */ 96000)), createAdaptationSet( /* id= */ 102, - /* trackType= */ C.TRACK_TYPE_AUDIO, - /* descriptor= */ createSwitchDescriptor(/* ids= */ 101), + C.TRACK_TYPE_AUDIO, + createSwitchDescriptor(/* ids...= */ 101), createAudioRepresentation(/* bitrate= */ 256000)), createAdaptationSet( /* id= */ 103, - /* trackType= */ C.TRACK_TYPE_VIDEO, - /* descriptor= */ createSwitchDescriptor(/* ids= */ 100, 104), + C.TRACK_TYPE_VIDEO, + createSwitchDescriptor(/* ids...= */ 100, 104), createVideoRepresentationWithInbandEventStream(/* bitrate= */ 800000), createVideoRepresentationWithInbandEventStream(/* bitrate= */ 1000000)), createAdaptationSet( /* id= */ 104, - /* trackType= */ C.TRACK_TYPE_VIDEO, - /* descriptor= */ createSwitchDescriptor(/* ids= */ 100, 103), + C.TRACK_TYPE_VIDEO, + createSwitchDescriptor(/* ids...= */ 100, 103), createVideoRepresentationWithInbandEventStream(/* bitrate= */ 2000000)), createAdaptationSet( /* id= */ 105, - /* trackType= */ C.TRACK_TYPE_TEXT, + C.TRACK_TYPE_TEXT, /* descriptor= */ null, createTextRepresentation(/* language= */ "eng")), createAdaptationSet( /* id= */ 105, - /* trackType= */ C.TRACK_TYPE_TEXT, + C.TRACK_TYPE_TEXT, /* descriptor= */ null, createTextRepresentation(/* language= */ "ger")))); - FilterableManifestMediaPeriodFactory mediaPeriodFactory = - (manifest, periodIndex) -> - new DashMediaPeriod( - /* id= */ periodIndex, - manifest, - periodIndex, - mock(DashChunkSource.Factory.class), - mock(TransferListener.class), - DrmSessionManager.getDummyDrmSessionManager(), - mock(LoadErrorHandlingPolicy.class), - new EventDispatcher() - .withParameters( - /* windowIndex= */ 0, - /* mediaPeriodId= */ new MediaPeriodId(/* periodUid= */ new Object()), - /* mediaTimeOffsetMs= */ 0), - /* elapsedRealtimeOffsetMs= */ 0, - mock(LoaderErrorThrower.class), - mock(Allocator.class), - mock(CompositeSequenceableLoaderFactory.class), - mock(PlayerEmsgCallback.class)); // Ignore embedded metadata as we don't want to select primary group just to get embedded track. MediaPeriodAsserts.assertGetStreamKeysAndManifestFilterIntegration( - mediaPeriodFactory, - testManifest, + DashMediaPeriodTest::createDashMediaPeriod, + manifest, /* periodIndex= */ 1, /* ignoredMimeType= */ "application/x-emsg"); } + @Test + public void adaptationSetSwitchingProperty_mergesTrackGroups() { + DashManifest manifest = + createDashManifest( + createPeriod( + createAdaptationSet( + /* id= */ 0, + C.TRACK_TYPE_VIDEO, + createSwitchDescriptor(/* ids...= */ 1, 2), + createVideoRepresentation(/* bitrate= */ 0), + createVideoRepresentation(/* bitrate= */ 1)), + createAdaptationSet( + /* id= */ 3, + C.TRACK_TYPE_VIDEO, + /* descriptor= */ null, + createVideoRepresentation(/* bitrate= */ 300)), + createAdaptationSet( + /* id= */ 2, + C.TRACK_TYPE_VIDEO, + createSwitchDescriptor(/* ids...= */ 0, 1), + createVideoRepresentation(/* bitrate= */ 200), + createVideoRepresentation(/* bitrate= */ 201)), + createAdaptationSet( + /* id= */ 1, + C.TRACK_TYPE_VIDEO, + createSwitchDescriptor(/* ids...= */ 0, 2), + createVideoRepresentation(/* bitrate= */ 100)))); + DashMediaPeriod dashMediaPeriod = createDashMediaPeriod(manifest, 0); + List adaptationSets = manifest.getPeriod(0).adaptationSets; + + // We expect the three adaptation sets with the switch descriptor to be merged, retaining the + // representations in their original order. + TrackGroupArray expectedTrackGroups = + new TrackGroupArray( + new TrackGroup( + adaptationSets.get(0).representations.get(0).format, + adaptationSets.get(0).representations.get(1).format, + adaptationSets.get(2).representations.get(0).format, + adaptationSets.get(2).representations.get(1).format, + adaptationSets.get(3).representations.get(0).format), + new TrackGroup(adaptationSets.get(1).representations.get(0).format)); + + MediaPeriodAsserts.assertTrackGroups(dashMediaPeriod, expectedTrackGroups); + } + + @Test + public void trickPlayProperty_mergesTrackGroups() { + DashManifest manifest = + createDashManifest( + createPeriod( + createAdaptationSet( + /* id= */ 0, + C.TRACK_TYPE_VIDEO, + createTrickPlayDescriptor(/* mainAdaptationSetId= */ 1), + createVideoRepresentation(/* bitrate= */ 0), + createVideoRepresentation(/* bitrate= */ 1)), + createAdaptationSet( + /* id= */ 1, + C.TRACK_TYPE_VIDEO, + /* descriptor= */ null, + createVideoRepresentation(/* bitrate= */ 100)), + createAdaptationSet( + /* id= */ 2, + C.TRACK_TYPE_VIDEO, + /* descriptor= */ null, + createVideoRepresentation(/* bitrate= */ 200), + createVideoRepresentation(/* bitrate= */ 201)), + createAdaptationSet( + /* id= */ 3, + C.TRACK_TYPE_VIDEO, + createTrickPlayDescriptor(/* mainAdaptationSetId= */ 2), + createVideoRepresentation(/* bitrate= */ 300)))); + DashMediaPeriod dashMediaPeriod = createDashMediaPeriod(manifest, 0); + List adaptationSets = manifest.getPeriod(0).adaptationSets; + + // We expect the trick play adaptation sets to be merged with the ones to which they refer, + // retaining representations in their original order. + TrackGroupArray expectedTrackGroups = + new TrackGroupArray( + new TrackGroup( + adaptationSets.get(0).representations.get(0).format, + adaptationSets.get(0).representations.get(1).format, + adaptationSets.get(1).representations.get(0).format), + new TrackGroup( + adaptationSets.get(2).representations.get(0).format, + adaptationSets.get(2).representations.get(1).format, + adaptationSets.get(3).representations.get(0).format)); + + MediaPeriodAsserts.assertTrackGroups(dashMediaPeriod, expectedTrackGroups); + } + + @Test + public void adaptationSetSwitchingProperty_andTrickPlayProperty_mergesTrackGroups() { + DashManifest manifest = + createDashManifest( + createPeriod( + createAdaptationSet( + /* id= */ 0, + C.TRACK_TYPE_VIDEO, + createTrickPlayDescriptor(/* mainAdaptationSetId= */ 1), + createVideoRepresentation(/* bitrate= */ 0), + createVideoRepresentation(/* bitrate= */ 1)), + createAdaptationSet( + /* id= */ 1, + C.TRACK_TYPE_VIDEO, + createSwitchDescriptor(/* ids...= */ 2), + createVideoRepresentation(/* bitrate= */ 100)), + createAdaptationSet( + /* id= */ 2, + C.TRACK_TYPE_VIDEO, + createSwitchDescriptor(/* ids...= */ 1), + createVideoRepresentation(/* bitrate= */ 200), + createVideoRepresentation(/* bitrate= */ 201)), + createAdaptationSet( + /* id= */ 3, + C.TRACK_TYPE_VIDEO, + createTrickPlayDescriptor(/* mainAdaptationSetId= */ 2), + createVideoRepresentation(/* bitrate= */ 300)))); + DashMediaPeriod dashMediaPeriod = createDashMediaPeriod(manifest, 0); + List adaptationSets = manifest.getPeriod(0).adaptationSets; + + // We expect all adaptation sets to be merged into one group, retaining representations in their + // original order. + TrackGroupArray expectedTrackGroups = + new TrackGroupArray( + new TrackGroup( + adaptationSets.get(0).representations.get(0).format, + adaptationSets.get(0).representations.get(1).format, + adaptationSets.get(1).representations.get(0).format, + adaptationSets.get(2).representations.get(0).format, + adaptationSets.get(2).representations.get(1).format, + adaptationSets.get(3).representations.get(0).format)); + + MediaPeriodAsserts.assertTrackGroups(dashMediaPeriod, expectedTrackGroups); + } + + private static DashMediaPeriod createDashMediaPeriod(DashManifest manifest, int periodIndex) { + return new DashMediaPeriod( + /* id= */ periodIndex, + manifest, + periodIndex, + mock(DashChunkSource.Factory.class), + mock(TransferListener.class), + DrmSessionManager.getDummyDrmSessionManager(), + mock(LoadErrorHandlingPolicy.class), + new EventDispatcher() + .withParameters( + /* windowIndex= */ 0, + /* mediaPeriodId= */ new MediaPeriodId(/* periodUid= */ new Object()), + /* mediaTimeOffsetMs= */ 0), + /* elapsedRealtimeOffsetMs= */ 0, + mock(LoaderErrorThrower.class), + mock(Allocator.class), + mock(CompositeSequenceableLoaderFactory.class), + mock(PlayerEmsgCallback.class)); + } + private static DashManifest createDashManifest(Period... periods) { return new DashManifest( /* availabilityStartTimeMs= */ 0, @@ -165,6 +304,7 @@ public final class DashMediaPeriodTest { trackType, Arrays.asList(representations), /* accessibilityDescriptors= */ Collections.emptyList(), + /* essentialProperties= */ Collections.emptyList(), descriptor == null ? Collections.emptyList() : Collections.singletonList(descriptor)); } @@ -186,50 +326,33 @@ public final class DashMediaPeriodTest { } private static Format createVideoFormat(int bitrate) { - return Format.createContainerFormat( - /* id= */ null, - /* label= */ null, - MimeTypes.VIDEO_MP4, - MimeTypes.VIDEO_H264, - /* codecs= */ null, - bitrate, - /* selectionFlags= */ 0, - /* roleFlags= */ 0, - /* language= */ null); + return new Format.Builder() + .setContainerMimeType(MimeTypes.VIDEO_MP4) + .setSampleMimeType(MimeTypes.VIDEO_H264) + .setPeakBitrate(bitrate) + .build(); } private static Representation createAudioRepresentation(int bitrate) { + Format format = + new Format.Builder() + .setContainerMimeType(MimeTypes.AUDIO_MP4) + .setSampleMimeType(MimeTypes.AUDIO_AAC) + .setPeakBitrate(bitrate) + .build(); return Representation.newInstance( - /* revisionId= */ 0, - Format.createContainerFormat( - /* id= */ null, - /* label= */ null, - MimeTypes.AUDIO_MP4, - MimeTypes.AUDIO_AAC, - /* codecs= */ null, - bitrate, - /* selectionFlags= */ 0, - /* roleFlags= */ 0, - /* language= */ null), - /* baseUrl= */ "", - new SingleSegmentBase()); + /* revisionId= */ 0, format, /* baseUrl= */ "", new SingleSegmentBase()); } private static Representation createTextRepresentation(String language) { + Format format = + new Format.Builder() + .setContainerMimeType(MimeTypes.APPLICATION_MP4) + .setSampleMimeType(MimeTypes.TEXT_VTT) + .setLanguage(language) + .build(); return Representation.newInstance( - /* revisionId= */ 0, - Format.createContainerFormat( - /* id= */ null, - /* label= */ null, - MimeTypes.APPLICATION_MP4, - MimeTypes.TEXT_VTT, - /* codecs= */ null, - /* bitrate= */ Format.NO_VALUE, - /* selectionFlags= */ 0, - /* roleFlags= */ 0, - language), - /* baseUrl= */ "", - new SingleSegmentBase()); + /* revisionId= */ 0, format, /* baseUrl= */ "", new SingleSegmentBase()); } private static Descriptor createSwitchDescriptor(int... ids) { @@ -244,6 +367,13 @@ public final class DashMediaPeriodTest { /* id= */ null); } + private static Descriptor createTrickPlayDescriptor(int mainAdaptationSetId) { + return new Descriptor( + /* schemeIdUri= */ "http://dashif.org/guidelines/trickmode", + /* value= */ Integer.toString(mainAdaptationSetId), + /* id= */ null); + } + private static Descriptor getInbandEventDescriptor() { return new Descriptor( /* schemeIdUri= */ "inBandSchemeIdUri", /* value= */ "inBandValue", /* id= */ "inBandId"); diff --git a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/DashMediaSourceTest.java b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/DashMediaSourceTest.java index 2aca8c3c05..3c8952fd62 100644 --- a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/DashMediaSourceTest.java +++ b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/DashMediaSourceTest.java @@ -32,7 +32,7 @@ import org.junit.runner.RunWith; public final class DashMediaSourceTest { @Test - public void testIso8601ParserParse() throws IOException { + public void iso8601ParserParse() throws IOException { DashMediaSource.Iso8601Parser parser = new DashMediaSource.Iso8601Parser(); // UTC. assertParseStringToLong(1512381697000L, parser, "2017-12-04T10:01:37Z"); @@ -58,7 +58,7 @@ public final class DashMediaSourceTest { } @Test - public void testIso8601ParserParseMissingTimezone() throws IOException { + public void iso8601ParserParseMissingTimezone() throws IOException { DashMediaSource.Iso8601Parser parser = new DashMediaSource.Iso8601Parser(); try { assertParseStringToLong(0, parser, "2017-12-04T10:01:37"); diff --git a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/DashUtilTest.java b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/DashUtilTest.java index 6e769b72e1..3176b06865 100644 --- a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/DashUtilTest.java +++ b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/DashUtilTest.java @@ -29,6 +29,7 @@ import com.google.android.exoplayer2.source.dash.manifest.SegmentBase.SingleSegm import com.google.android.exoplayer2.upstream.DummyDataSource; import com.google.android.exoplayer2.util.MimeTypes; import java.util.Arrays; +import java.util.Collections; import org.junit.Test; import org.junit.runner.RunWith; @@ -37,28 +38,28 @@ import org.junit.runner.RunWith; public final class DashUtilTest { @Test - public void testLoadDrmInitDataFromManifest() throws Exception { - Period period = newPeriod(newAdaptationSets(newRepresentations(newDrmInitData()))); + public void loadDrmInitDataFromManifest() throws Exception { + Period period = newPeriod(newAdaptationSet(newRepresentation(newDrmInitData()))); DrmInitData drmInitData = DashUtil.loadDrmInitData(DummyDataSource.INSTANCE, period); assertThat(drmInitData).isEqualTo(newDrmInitData()); } @Test - public void testLoadDrmInitDataMissing() throws Exception { - Period period = newPeriod(newAdaptationSets(newRepresentations(null /* no init data */))); + public void loadDrmInitDataMissing() throws Exception { + Period period = newPeriod(newAdaptationSet(newRepresentation(null /* no init data */))); DrmInitData drmInitData = DashUtil.loadDrmInitData(DummyDataSource.INSTANCE, period); assertThat(drmInitData).isNull(); } @Test - public void testLoadDrmInitDataNoRepresentations() throws Exception { - Period period = newPeriod(newAdaptationSets(/* no representation */ )); + public void loadDrmInitDataNoRepresentations() throws Exception { + Period period = newPeriod(newAdaptationSet(/* no representation */ )); DrmInitData drmInitData = DashUtil.loadDrmInitData(DummyDataSource.INSTANCE, period); assertThat(drmInitData).isNull(); } @Test - public void testLoadDrmInitDataNoAdaptationSets() throws Exception { + public void loadDrmInitDataNoAdaptationSets() throws Exception { Period period = newPeriod(/* no adaptation set */ ); DrmInitData drmInitData = DashUtil.loadDrmInitData(DummyDataSource.INSTANCE, period); assertThat(drmInitData).isNull(); @@ -68,29 +69,23 @@ public final class DashUtilTest { return new Period("", 0, Arrays.asList(adaptationSets)); } - private static AdaptationSet newAdaptationSets(Representation... representations) { - return new AdaptationSet(0, C.TRACK_TYPE_VIDEO, Arrays.asList(representations), null, null); + private static AdaptationSet newAdaptationSet(Representation... representations) { + return new AdaptationSet( + /* id= */ 0, + C.TRACK_TYPE_VIDEO, + Arrays.asList(representations), + /* accessibilityDescriptors= */ Collections.emptyList(), + /* essentialProperties= */ Collections.emptyList(), + /* supplementalProperties= */ Collections.emptyList()); } - private static Representation newRepresentations(DrmInitData drmInitData) { + private static Representation newRepresentation(DrmInitData drmInitData) { Format format = - Format.createVideoContainerFormat( - "id", - "label", - MimeTypes.VIDEO_MP4, - MimeTypes.VIDEO_H264, - /* codecs= */ "", - /* metadata= */ null, - Format.NO_VALUE, - /* width= */ 1024, - /* height= */ 768, - Format.NO_VALUE, - /* initializationData= */ null, - /* selectionFlags= */ 0, - /* roleFlags= */ 0); - if (drmInitData != null) { - format = format.copyWithDrmInitData(drmInitData); - } + new Format.Builder() + .setContainerMimeType(MimeTypes.VIDEO_MP4) + .setSampleMimeType(MimeTypes.VIDEO_H264) + .setDrmInitData(drmInitData) + .build(); return Representation.newInstance(0, format, "", new SingleSegmentBase()); } diff --git a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/DefaultMediaSourceFactoryTest.java b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/DefaultMediaSourceFactoryTest.java new file mode 100644 index 0000000000..7c3fdfc5ac --- /dev/null +++ b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/DefaultMediaSourceFactoryTest.java @@ -0,0 +1,100 @@ +/* + * Copyright 2020 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; + +import static com.google.common.truth.Truth.assertThat; + +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.MediaItem; +import com.google.android.exoplayer2.source.DefaultMediaSourceFactory; +import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.util.MimeTypes; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Unit test for creating DASH media sources with the {@link DefaultMediaSourceFactory}. */ +@RunWith(AndroidJUnit4.class) +public class DefaultMediaSourceFactoryTest { + + private static final String URI_MEDIA = "http://exoplayer.dev/video"; + + @Test + public void createMediaSource_withMimeType_dashSource() { + DefaultMediaSourceFactory defaultMediaSourceFactory = + DefaultMediaSourceFactory.newInstance(ApplicationProvider.getApplicationContext()); + MediaItem mediaItem = + new MediaItem.Builder().setUri(URI_MEDIA).setMimeType(MimeTypes.APPLICATION_MPD).build(); + + MediaSource mediaSource = defaultMediaSourceFactory.createMediaSource(mediaItem); + + assertThat(mediaSource).isInstanceOf(DashMediaSource.class); + } + + @Test + public void createMediaSource_withTag_tagInSource() { + Object tag = new Object(); + DefaultMediaSourceFactory defaultMediaSourceFactory = + DefaultMediaSourceFactory.newInstance(ApplicationProvider.getApplicationContext()); + MediaItem mediaItem = + new MediaItem.Builder() + .setUri(URI_MEDIA) + .setMimeType(MimeTypes.APPLICATION_MPD) + .setTag(tag) + .build(); + + MediaSource mediaSource = defaultMediaSourceFactory.createMediaSource(mediaItem); + + assertThat(mediaSource.getTag()).isEqualTo(tag); + } + + @Test + public void createMediaSource_withPath_dashSource() { + DefaultMediaSourceFactory defaultMediaSourceFactory = + DefaultMediaSourceFactory.newInstance(ApplicationProvider.getApplicationContext()); + MediaItem mediaItem = new MediaItem.Builder().setUri(URI_MEDIA + "/file.mpd").build(); + + MediaSource mediaSource = defaultMediaSourceFactory.createMediaSource(mediaItem); + + assertThat(mediaSource).isInstanceOf(DashMediaSource.class); + } + + @Test + public void createMediaSource_withNull_usesNonNullDefaults() { + DefaultMediaSourceFactory defaultMediaSourceFactory = + DefaultMediaSourceFactory.newInstance(ApplicationProvider.getApplicationContext()); + MediaItem mediaItem = new MediaItem.Builder().setUri(URI_MEDIA + "/file.mpd").build(); + + MediaSource mediaSource = + defaultMediaSourceFactory + .setDrmSessionManager(null) + .setDrmHttpDataSourceFactory(null) + .setLoadErrorHandlingPolicy(null) + .createMediaSource(mediaItem); + + assertThat(mediaSource).isNotNull(); + } + + @Test + public void getSupportedTypes_dashModule_containsTypeDash() { + int[] supportedTypes = + DefaultMediaSourceFactory.newInstance(ApplicationProvider.getApplicationContext()) + .getSupportedTypes(); + + assertThat(supportedTypes).asList().containsExactly(C.TYPE_OTHER, C.TYPE_DASH); + } +} diff --git a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/EventSampleStreamTest.java b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/EventSampleStreamTest.java index b946931f59..11f4b37c67 100644 --- a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/EventSampleStreamTest.java +++ b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/EventSampleStreamTest.java @@ -37,8 +37,11 @@ public final class EventSampleStreamTest { private static final String SCHEME_ID = "urn:test"; private static final String VALUE = "123"; - private static final Format FORMAT = Format.createSampleFormat("urn:test/123", - MimeTypes.APPLICATION_EMSG, null, Format.NO_VALUE, null); + private static final Format FORMAT = + new Format.Builder() + .setId("urn:test/123") + .setSampleMimeType(MimeTypes.APPLICATION_EMSG) + .build(); private static final byte[] MESSAGE_DATA = new byte[] {1, 2, 3, 4}; private static final long DURATION_MS = 3000; private static final long TIME_SCALE = 1000; @@ -59,7 +62,7 @@ public final class EventSampleStreamTest { * return format for the first call. */ @Test - public void testReadDataReturnFormatForFirstRead() { + public void readDataReturnFormatForFirstRead() { EventStream eventStream = new EventStream(SCHEME_ID, VALUE, TIME_SCALE, new long[0], new EventMessage[0]); EventSampleStream sampleStream = new EventSampleStream(eventStream, FORMAT, false); @@ -70,11 +73,11 @@ public final class EventSampleStreamTest { } /** - * Tests that a non-dynamic {@link EventSampleStream} will return a buffer with - * {@link C#BUFFER_FLAG_END_OF_STREAM} when trying to read sample out-of-bound. + * Tests that a non-dynamic {@link EventSampleStream} will return a buffer with {@link + * C#BUFFER_FLAG_END_OF_STREAM} when trying to read sample out-of-bound. */ @Test - public void testReadDataOutOfBoundReturnEndOfStreamAfterFormatForNonDynamicEventSampleStream() { + public void readDataOutOfBoundReturnEndOfStreamAfterFormatForNonDynamicEventSampleStream() { EventStream eventStream = new EventStream(SCHEME_ID, VALUE, TIME_SCALE, new long[0], new EventMessage[0]); EventSampleStream sampleStream = new EventSampleStream(eventStream, FORMAT, false); @@ -87,11 +90,11 @@ public final class EventSampleStreamTest { } /** - * Tests that a dynamic {@link EventSampleStream} will return {@link C#RESULT_NOTHING_READ} - * when trying to read sample out-of-bound. + * Tests that a dynamic {@link EventSampleStream} will return {@link C#RESULT_NOTHING_READ} when + * trying to read sample out-of-bound. */ @Test - public void testReadDataOutOfBoundReturnEndOfStreamAfterFormatForDynamicEventSampleStream() { + public void readDataOutOfBoundReturnEndOfStreamAfterFormatForDynamicEventSampleStream() { EventStream eventStream = new EventStream(SCHEME_ID, VALUE, TIME_SCALE, new long[0], new EventMessage[0]); EventSampleStream sampleStream = new EventSampleStream(eventStream, FORMAT, true); @@ -107,7 +110,7 @@ public final class EventSampleStreamTest { * return sample data after the first call. */ @Test - public void testReadDataReturnDataAfterFormat() { + public void readDataReturnDataAfterFormat() { long presentationTimeUs = 1000000; EventMessage eventMessage = newEventMessageWithId(1); EventStream eventStream = new EventStream(SCHEME_ID, VALUE, TIME_SCALE, @@ -123,12 +126,12 @@ public final class EventSampleStreamTest { } /** - * Tests that {@link EventSampleStream#skipData(long)} will skip until the given position, and - * the next {@link EventSampleStream#readData(FormatHolder, DecoderInputBuffer, boolean)} call - * will return sample data from that position. + * Tests that {@link EventSampleStream#skipData(long)} will skip until the given position, and the + * next {@link EventSampleStream#readData(FormatHolder, DecoderInputBuffer, boolean)} call will + * return sample data from that position. */ @Test - public void testSkipDataThenReadDataReturnDataFromSkippedPosition() { + public void skipDataThenReadDataReturnDataFromSkippedPosition() { long presentationTimeUs1 = 1000000; long presentationTimeUs2 = 2000000; EventMessage eventMessage1 = newEventMessageWithId(1); @@ -154,7 +157,7 @@ public final class EventSampleStreamTest { * will return sample data from that position. */ @Test - public void testSeekToUsThenReadDataReturnDataFromSeekPosition() { + public void seekToUsThenReadDataReturnDataFromSeekPosition() { long presentationTimeUs1 = 1000000; long presentationTimeUs2 = 2000000; EventMessage eventMessage1 = newEventMessageWithId(1); @@ -175,12 +178,12 @@ public final class EventSampleStreamTest { /** * Tests that {@link EventSampleStream#updateEventStream(EventStream, boolean)} will update the - * underlying event stream, but keep the read timestamp, so the next - * {@link EventSampleStream#readData(FormatHolder, DecoderInputBuffer, boolean)} call - * will return sample data from after the last read sample timestamp. + * underlying event stream, but keep the read timestamp, so the next {@link + * EventSampleStream#readData(FormatHolder, DecoderInputBuffer, boolean)} call will return sample + * data from after the last read sample timestamp. */ @Test - public void testUpdateEventStreamContinueToReadAfterLastReadSamplePresentationTime() { + public void updateEventStreamContinueToReadAfterLastReadSamplePresentationTime() { long presentationTimeUs1 = 1000000; long presentationTimeUs2 = 2000000; long presentationTimeUs3 = 3000000; @@ -209,12 +212,12 @@ public final class EventSampleStreamTest { /** * Tests that {@link EventSampleStream#updateEventStream(EventStream, boolean)} will update the - * underlying event stream, but keep the timestamp the stream has skipped to, so the next - * {@link EventSampleStream#readData(FormatHolder, DecoderInputBuffer, boolean)} call - * will return sample data from the skipped position. + * underlying event stream, but keep the timestamp the stream has skipped to, so the next {@link + * EventSampleStream#readData(FormatHolder, DecoderInputBuffer, boolean)} call will return sample + * data from the skipped position. */ @Test - public void testSkipDataThenUpdateStreamContinueToReadFromSkippedPosition() { + public void skipDataThenUpdateStreamContinueToReadFromSkippedPosition() { long presentationTimeUs1 = 1000000; long presentationTimeUs2 = 2000000; long presentationTimeUs3 = 3000000; @@ -240,14 +243,14 @@ public final class EventSampleStreamTest { } /** - * Tests that {@link EventSampleStream#skipData(long)} will only skip to the point right after - * it last event. A following {@link EventSampleStream#updateEventStream(EventStream, boolean)} - * will update the underlying event stream and keep the timestamp the stream has skipped to, so - * the next {@link EventSampleStream#readData(FormatHolder, DecoderInputBuffer, boolean)} call - * will return sample data from the skipped position. + * Tests that {@link EventSampleStream#skipData(long)} will only skip to the point right after it + * last event. A following {@link EventSampleStream#updateEventStream(EventStream, boolean)} will + * update the underlying event stream and keep the timestamp the stream has skipped to, so the + * next {@link EventSampleStream#readData(FormatHolder, DecoderInputBuffer, boolean)} call will + * return sample data from the skipped position. */ @Test - public void testSkipDataThenUpdateStreamContinueToReadDoNotSkippedMoreThanAvailable() { + public void skipDataThenUpdateStreamContinueToReadDoNotSkippedMoreThanAvailable() { long presentationTimeUs1 = 1000000; long presentationTimeUs2 = 2000000; long presentationTimeUs3 = 3000000; @@ -276,12 +279,12 @@ public final class EventSampleStreamTest { /** * Tests that {@link EventSampleStream#updateEventStream(EventStream, boolean)} will update the - * underlying event stream, but keep the timestamp the stream has seek to, so the next - * {@link EventSampleStream#readData(FormatHolder, DecoderInputBuffer, boolean)} call - * will return sample data from the seek position. + * underlying event stream, but keep the timestamp the stream has seek to, so the next {@link + * EventSampleStream#readData(FormatHolder, DecoderInputBuffer, boolean)} call will return sample + * data from the seek position. */ @Test - public void testSeekToUsThenUpdateStreamContinueToReadFromSeekPosition() { + public void seekToUsThenUpdateStreamContinueToReadFromSeekPosition() { long presentationTimeUs1 = 1000000; long presentationTimeUs2 = 2000000; long presentationTimeUs3 = 3000000; @@ -308,12 +311,12 @@ public final class EventSampleStreamTest { /** * Tests that {@link EventSampleStream#updateEventStream(EventStream, boolean)} will update the - * underlying event stream, but keep the timestamp the stream has seek to, so the next - * {@link EventSampleStream#readData(FormatHolder, DecoderInputBuffer, boolean)} call - * will return sample data from the seek position. + * underlying event stream, but keep the timestamp the stream has seek to, so the next {@link + * EventSampleStream#readData(FormatHolder, DecoderInputBuffer, boolean)} call will return sample + * data from the seek position. */ @Test - public void testSeekToThenUpdateStreamContinueToReadFromSeekPositionEvenSeekMoreThanAvailable() { + public void seekToThenUpdateStreamContinueToReadFromSeekPositionEvenSeekMoreThanAvailable() { long presentationTimeUs1 = 1000000; long presentationTimeUs2 = 2000000; long presentationTimeUs3 = 3000000; diff --git a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParserTest.java b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParserTest.java index 390a18d2cc..47087472ae 100644 --- a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParserTest.java +++ b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParserTest.java @@ -18,6 +18,7 @@ package com.google.android.exoplayer2.source.dash.manifest; import static com.google.common.truth.Truth.assertThat; import android.net.Uri; +import androidx.annotation.Nullable; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.C; @@ -25,6 +26,7 @@ import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.metadata.emsg.EventMessage; import com.google.android.exoplayer2.source.dash.manifest.SegmentBase.SegmentTimelineElement; import com.google.android.exoplayer2.testutil.TestUtil; +import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.Util; import java.io.IOException; import java.io.StringReader; @@ -40,11 +42,14 @@ import org.xmlpull.v1.XmlPullParserFactory; @RunWith(AndroidJUnit4.class) public class DashManifestParserTest { - private static final String SAMPLE_MPD = "sample_mpd"; - private static final String SAMPLE_MPD_UNKNOWN_MIME_TYPE = "sample_mpd_unknown_mime_type"; - private static final String SAMPLE_MPD_SEGMENT_TEMPLATE = "sample_mpd_segment_template"; - private static final String SAMPLE_MPD_EVENT_STREAM = "sample_mpd_event_stream"; - private static final String SAMPLE_MPD_LABELS = "sample_mpd_labels"; + private static final String SAMPLE_MPD = "mpd/sample_mpd"; + private static final String SAMPLE_MPD_UNKNOWN_MIME_TYPE = "mpd/sample_mpd_unknown_mime_type"; + private static final String SAMPLE_MPD_SEGMENT_TEMPLATE = "mpd/sample_mpd_segment_template"; + private static final String SAMPLE_MPD_EVENT_STREAM = "mpd/sample_mpd_event_stream"; + private static final String SAMPLE_MPD_LABELS = "mpd/sample_mpd_labels"; + private static final String SAMPLE_MPD_ASSET_IDENTIFIER = "mpd/sample_mpd_asset_identifier"; + private static final String SAMPLE_MPD_TEXT = "mpd/sample_mpd_text"; + private static final String SAMPLE_MPD_TRICK_PLAY = "mpd/sample_mpd_trick_play"; private static final String NEXT_TAG_NAME = "Next"; private static final String NEXT_TAG = "<" + NEXT_TAG_NAME + "/>"; @@ -65,15 +70,15 @@ public class DashManifestParserTest { @Test public void parseMediaPresentationDescription_segmentTemplate() throws IOException { DashManifestParser parser = new DashManifestParser(); - DashManifest mpd = + DashManifest manifest = parser.parse( Uri.parse("https://example.com/test.mpd"), TestUtil.getInputStream( ApplicationProvider.getApplicationContext(), SAMPLE_MPD_SEGMENT_TEMPLATE)); - assertThat(mpd.getPeriodCount()).isEqualTo(1); + assertThat(manifest.getPeriodCount()).isEqualTo(1); - Period period = mpd.getPeriod(0); + Period period = manifest.getPeriod(0); assertThat(period).isNotNull(); assertThat(period.adaptationSets).hasSize(2); @@ -97,13 +102,13 @@ public class DashManifestParserTest { @Test public void parseMediaPresentationDescription_eventStream() throws IOException { DashManifestParser parser = new DashManifestParser(); - DashManifest mpd = + DashManifest manifest = parser.parse( Uri.parse("https://example.com/test.mpd"), TestUtil.getInputStream( ApplicationProvider.getApplicationContext(), SAMPLE_MPD_EVENT_STREAM)); - Period period = mpd.getPeriod(0); + Period period = manifest.getPeriod(0); assertThat(period.eventStreams).hasSize(3); // assert text-only event stream @@ -167,14 +172,14 @@ public class DashManifestParserTest { @Test public void parseMediaPresentationDescription_programInformation() throws IOException { DashManifestParser parser = new DashManifestParser(); - DashManifest mpd = + DashManifest manifest = parser.parse( - Uri.parse("Https://example.com/test.mpd"), + Uri.parse("https://example.com/test.mpd"), TestUtil.getInputStream(ApplicationProvider.getApplicationContext(), SAMPLE_MPD)); ProgramInformation expectedProgramInformation = new ProgramInformation( "MediaTitle", "MediaSource", "MediaCopyright", "www.example.com", "enUs"); - assertThat(mpd.programInformation).isEqualTo(expectedProgramInformation); + assertThat(manifest.programInformation).isEqualTo(expectedProgramInformation); } @Test @@ -192,6 +197,75 @@ public class DashManifestParserTest { assertThat(adaptationSets.get(1).representations.get(0).format.label).isEqualTo("video label"); } + @Test + public void parseMediaPresentationDescription_text() throws IOException { + DashManifestParser parser = new DashManifestParser(); + DashManifest manifest = + parser.parse( + Uri.parse("https://example.com/test.mpd"), + TestUtil.getInputStream(ApplicationProvider.getApplicationContext(), SAMPLE_MPD_TEXT)); + + List adaptationSets = manifest.getPeriod(0).adaptationSets; + + Format format = adaptationSets.get(0).representations.get(0).format; + assertThat(format.containerMimeType).isEqualTo(MimeTypes.APPLICATION_RAWCC); + assertThat(format.sampleMimeType).isEqualTo(MimeTypes.APPLICATION_CEA608); + assertThat(format.codecs).isEqualTo("cea608"); + assertThat(adaptationSets.get(0).type).isEqualTo(C.TRACK_TYPE_TEXT); + + format = adaptationSets.get(1).representations.get(0).format; + assertThat(format.containerMimeType).isEqualTo(MimeTypes.APPLICATION_MP4); + assertThat(format.sampleMimeType).isEqualTo(MimeTypes.APPLICATION_TTML); + assertThat(format.codecs).isEqualTo("stpp.ttml.im1t"); + assertThat(adaptationSets.get(1).type).isEqualTo(C.TRACK_TYPE_TEXT); + + format = adaptationSets.get(2).representations.get(0).format; + assertThat(format.containerMimeType).isEqualTo(MimeTypes.APPLICATION_TTML); + assertThat(format.sampleMimeType).isEqualTo(MimeTypes.APPLICATION_TTML); + assertThat(format.codecs).isNull(); + assertThat(adaptationSets.get(2).type).isEqualTo(C.TRACK_TYPE_TEXT); + } + + @Test + public void parseMediaPresentationDescription_trickPlay() throws IOException { + DashManifestParser parser = new DashManifestParser(); + DashManifest manifest = + parser.parse( + Uri.parse("https://example.com/test.mpd"), + TestUtil.getInputStream( + ApplicationProvider.getApplicationContext(), SAMPLE_MPD_TRICK_PLAY)); + + List adaptationSets = manifest.getPeriod(0).adaptationSets; + + AdaptationSet adaptationSet = adaptationSets.get(0); + assertThat(adaptationSet.essentialProperties).isEmpty(); + assertThat(adaptationSet.supplementalProperties).isEmpty(); + assertThat(adaptationSet.representations.get(0).format.roleFlags).isEqualTo(0); + + adaptationSet = adaptationSets.get(1); + assertThat(adaptationSet.essentialProperties).isEmpty(); + assertThat(adaptationSet.supplementalProperties).isEmpty(); + assertThat(adaptationSet.representations.get(0).format.roleFlags).isEqualTo(0); + + adaptationSet = adaptationSets.get(2); + assertThat(adaptationSet.essentialProperties).hasSize(1); + assertThat(adaptationSet.essentialProperties.get(0).schemeIdUri) + .isEqualTo("http://dashif.org/guidelines/trickmode"); + assertThat(adaptationSet.essentialProperties.get(0).value).isEqualTo("0"); + assertThat(adaptationSet.supplementalProperties).isEmpty(); + assertThat(adaptationSet.representations.get(0).format.roleFlags) + .isEqualTo(C.ROLE_FLAG_TRICK_PLAY); + + adaptationSet = adaptationSets.get(3); + assertThat(adaptationSet.essentialProperties).isEmpty(); + assertThat(adaptationSet.supplementalProperties).hasSize(1); + assertThat(adaptationSet.supplementalProperties.get(0).schemeIdUri) + .isEqualTo("http://dashif.org/guidelines/trickmode"); + assertThat(adaptationSet.supplementalProperties.get(0).value).isEqualTo("1"); + assertThat(adaptationSet.representations.get(0).format.roleFlags) + .isEqualTo(C.ROLE_FLAG_TRICK_PLAY); + } + @Test public void parseSegmentTimeline_repeatCount() throws Exception { DashManifestParser parser = new DashManifestParser(); @@ -377,6 +451,27 @@ public class DashManifestParserTest { .isEqualTo(Format.NO_VALUE); } + @Test + public void parsePeriodAssetIdentifier() throws IOException { + DashManifestParser parser = new DashManifestParser(); + DashManifest manifest = + parser.parse( + Uri.parse("https://example.com/test.mpd"), + TestUtil.getInputStream( + ApplicationProvider.getApplicationContext(), SAMPLE_MPD_ASSET_IDENTIFIER)); + + assertThat(manifest.getPeriodCount()).isEqualTo(1); + + Period period = manifest.getPeriod(0); + assertThat(period).isNotNull(); + @Nullable Descriptor assetIdentifier = period.assetIdentifier; + assertThat(assetIdentifier).isNotNull(); + + assertThat(assetIdentifier.schemeIdUri).isEqualTo("urn:org:dashif:asset-id:2013"); + assertThat(assetIdentifier.value).isEqualTo("md:cid:EIDR:10.5240%2f0EFB-02CD-126E-8092-1E49-W"); + assertThat(assetIdentifier.id).isEqualTo("uniqueId"); + } + private static List buildCea608AccessibilityDescriptors(String value) { return Collections.singletonList(new Descriptor("urn:scte:dash:cc:cea-608:2015", value, null)); } diff --git a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestTest.java b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestTest.java index a336602965..b260bf2cee 100644 --- a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestTest.java +++ b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestTest.java @@ -35,10 +35,10 @@ public class DashManifestTest { private static final UtcTimingElement DUMMY_UTC_TIMING = new UtcTimingElement("", ""); private static final SingleSegmentBase DUMMY_SEGMENT_BASE = new SingleSegmentBase(); - private static final Format DUMMY_FORMAT = Format.createSampleFormat("", "", 0); + private static final Format DUMMY_FORMAT = new Format.Builder().build(); @Test - public void testCopy() throws Exception { + public void copy() { Representation[][][] representations = newRepresentations(3, 2, 3); DashManifest sourceManifest = newDashManifest( @@ -97,7 +97,7 @@ public class DashManifestTest { } @Test - public void testCopySameAdaptationIndexButDifferentPeriod() throws Exception { + public void copySameAdaptationIndexButDifferentPeriod() { Representation[][][] representations = newRepresentations(2, 1, 1); DashManifest sourceManifest = newDashManifest( @@ -117,7 +117,7 @@ public class DashManifestTest { } @Test - public void testCopySkipPeriod() throws Exception { + public void copySkipPeriod() { Representation[][][] representations = newRepresentations(3, 2, 3); DashManifest sourceManifest = newDashManifest( @@ -239,6 +239,12 @@ public class DashManifestTest { } private static AdaptationSet newAdaptationSet(int seed, Representation... representations) { - return new AdaptationSet(++seed, ++seed, Arrays.asList(representations), null, null); + return new AdaptationSet( + ++seed, + ++seed, + Arrays.asList(representations), + /* accessibilityDescriptors= */ Collections.emptyList(), + /* essentialProperties= */ Collections.emptyList(), + /* supplementalProperties= */ Collections.emptyList()); } } diff --git a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/manifest/RangedUriTest.java b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/manifest/RangedUriTest.java index be1866206d..44af227d96 100644 --- a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/manifest/RangedUriTest.java +++ b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/manifest/RangedUriTest.java @@ -31,7 +31,7 @@ public class RangedUriTest { private static final String FULL_URI = BASE_URI + PARTIAL_URI; @Test - public void testMerge() { + public void merge() { RangedUri rangeA = new RangedUri(FULL_URI, 0, 10); RangedUri rangeB = new RangedUri(FULL_URI, 10, 10); RangedUri expected = new RangedUri(FULL_URI, 0, 20); @@ -39,7 +39,7 @@ public class RangedUriTest { } @Test - public void testMergeUnbounded() { + public void mergeUnbounded() { RangedUri rangeA = new RangedUri(FULL_URI, 0, 10); RangedUri rangeB = new RangedUri(FULL_URI, 10, C.LENGTH_UNSET); RangedUri expected = new RangedUri(FULL_URI, 0, C.LENGTH_UNSET); @@ -47,7 +47,7 @@ public class RangedUriTest { } @Test - public void testNonMerge() { + public void nonMerge() { // A and B do not overlap, so should not merge RangedUri rangeA = new RangedUri(FULL_URI, 0, 10); RangedUri rangeB = new RangedUri(FULL_URI, 11, 10); @@ -70,7 +70,7 @@ public class RangedUriTest { } @Test - public void testMergeWithBaseUri() { + public void mergeWithBaseUri() { RangedUri rangeA = new RangedUri(PARTIAL_URI, 0, 10); RangedUri rangeB = new RangedUri(FULL_URI, 10, 10); RangedUri expected = new RangedUri(FULL_URI, 0, 20); diff --git a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/manifest/UrlTemplateTest.java b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/manifest/UrlTemplateTest.java index c65c2d0b0c..6736840d82 100644 --- a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/manifest/UrlTemplateTest.java +++ b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/manifest/UrlTemplateTest.java @@ -27,7 +27,7 @@ import org.junit.runner.RunWith; public class UrlTemplateTest { @Test - public void testRealExamples() { + public void realExamples() { String template = "QualityLevels($Bandwidth$)/Fragments(video=$Time$,format=mpd-time-csf)"; UrlTemplate urlTemplate = UrlTemplate.compile(template); String url = urlTemplate.buildUri("abc1", 10, 650000, 5000); @@ -45,7 +45,7 @@ public class UrlTemplateTest { } @Test - public void testFull() { + public void full() { String template = "$Bandwidth$_a_$RepresentationID$_b_$Time$_c_$Number$"; UrlTemplate urlTemplate = UrlTemplate.compile(template); String url = urlTemplate.buildUri("abc1", 10, 650000, 5000); @@ -53,7 +53,7 @@ public class UrlTemplateTest { } @Test - public void testFullWithDollarEscaping() { + public void fullWithDollarEscaping() { String template = "$$$Bandwidth$$$_a$$_$RepresentationID$_b_$Time$_c_$Number$$$"; UrlTemplate urlTemplate = UrlTemplate.compile(template); String url = urlTemplate.buildUri("abc1", 10, 650000, 5000); @@ -61,7 +61,7 @@ public class UrlTemplateTest { } @Test - public void testInvalidSubstitution() { + public void invalidSubstitution() { String template = "$IllegalId$"; try { UrlTemplate.compile(template); 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 94dae35ed5..49e111b7a7 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 @@ -32,17 +32,16 @@ import com.google.android.exoplayer2.offline.DefaultDownloaderFactory; import com.google.android.exoplayer2.offline.DownloadException; import com.google.android.exoplayer2.offline.DownloadRequest; import com.google.android.exoplayer2.offline.Downloader; -import com.google.android.exoplayer2.offline.DownloaderConstructorHelper; import com.google.android.exoplayer2.offline.DownloaderFactory; import com.google.android.exoplayer2.offline.StreamKey; import com.google.android.exoplayer2.testutil.CacheAsserts.RequestSet; import com.google.android.exoplayer2.testutil.FakeDataSet; import com.google.android.exoplayer2.testutil.FakeDataSource; -import com.google.android.exoplayer2.testutil.FakeDataSource.Factory; import com.google.android.exoplayer2.testutil.TestUtil; import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.upstream.DummyDataSource; import com.google.android.exoplayer2.upstream.cache.Cache; +import com.google.android.exoplayer2.upstream.cache.CacheDataSource; import com.google.android.exoplayer2.upstream.cache.NoOpCacheEvictor; import com.google.android.exoplayer2.upstream.cache.SimpleCache; import com.google.android.exoplayer2.util.Util; @@ -80,10 +79,12 @@ public class DashDownloaderTest { } @Test - public void testCreateWithDefaultDownloaderFactory() { - DownloaderConstructorHelper constructorHelper = - new DownloaderConstructorHelper(Mockito.mock(Cache.class), DummyDataSource.FACTORY); - DownloaderFactory factory = new DefaultDownloaderFactory(constructorHelper); + public void createWithDefaultDownloaderFactory() { + CacheDataSource.Factory cacheDataSourceFactory = + new CacheDataSource.Factory() + .setCache(Mockito.mock(Cache.class)) + .setUpstreamDataSourceFactory(DummyDataSource.FACTORY); + DownloaderFactory factory = new DefaultDownloaderFactory(cacheDataSourceFactory); Downloader downloader = factory.createDownloader( @@ -98,7 +99,7 @@ public class DashDownloaderTest { } @Test - public void testDownloadRepresentation() throws Exception { + public void downloadRepresentation() throws Exception { FakeDataSet fakeDataSet = new FakeDataSet() .setData(TEST_MPD_URI, TEST_MPD) @@ -113,7 +114,7 @@ public class DashDownloaderTest { } @Test - public void testDownloadRepresentationInSmallParts() throws Exception { + public void downloadRepresentationInSmallParts() throws Exception { FakeDataSet fakeDataSet = new FakeDataSet() .setData(TEST_MPD_URI, TEST_MPD) @@ -132,7 +133,7 @@ public class DashDownloaderTest { } @Test - public void testDownloadRepresentations() throws Exception { + public void downloadRepresentations() throws Exception { FakeDataSet fakeDataSet = new FakeDataSet() .setData(TEST_MPD_URI, TEST_MPD) @@ -151,7 +152,7 @@ public class DashDownloaderTest { } @Test - public void testDownloadAllRepresentations() throws Exception { + public void downloadAllRepresentations() throws Exception { FakeDataSet fakeDataSet = new FakeDataSet() .setData(TEST_MPD_URI, TEST_MPD) @@ -172,7 +173,7 @@ public class DashDownloaderTest { } @Test - public void testProgressiveDownload() throws Exception { + public void progressiveDownload() throws Exception { FakeDataSet fakeDataSet = new FakeDataSet() .setData(TEST_MPD_URI, TEST_MPD) @@ -184,7 +185,7 @@ public class DashDownloaderTest { .setRandomData("text_segment_2", 2) .setRandomData("text_segment_3", 3); FakeDataSource fakeDataSource = new FakeDataSource(fakeDataSet); - Factory factory = mock(Factory.class); + FakeDataSource.Factory factory = mock(FakeDataSource.Factory.class); when(factory.createDataSource()).thenReturn(fakeDataSource); DashDownloader dashDownloader = @@ -204,7 +205,7 @@ public class DashDownloaderTest { } @Test - public void testProgressiveDownloadSeparatePeriods() throws Exception { + public void progressiveDownloadSeparatePeriods() throws Exception { FakeDataSet fakeDataSet = new FakeDataSet() .setData(TEST_MPD_URI, TEST_MPD) @@ -216,7 +217,7 @@ public class DashDownloaderTest { .setRandomData("period_2_segment_2", 2) .setRandomData("period_2_segment_3", 3); FakeDataSource fakeDataSource = new FakeDataSource(fakeDataSet); - Factory factory = mock(Factory.class); + FakeDataSource.Factory factory = mock(FakeDataSource.Factory.class); when(factory.createDataSource()).thenReturn(fakeDataSource); DashDownloader dashDownloader = @@ -236,7 +237,7 @@ public class DashDownloaderTest { } @Test - public void testDownloadRepresentationFailure() throws Exception { + public void downloadRepresentationFailure() throws Exception { FakeDataSet fakeDataSet = new FakeDataSet() .setData(TEST_MPD_URI, TEST_MPD) @@ -261,7 +262,7 @@ public class DashDownloaderTest { } @Test - public void testCounters() throws Exception { + public void counters() throws Exception { FakeDataSet fakeDataSet = new FakeDataSet() .setData(TEST_MPD_URI, TEST_MPD) @@ -289,7 +290,7 @@ public class DashDownloaderTest { } @Test - public void testRemove() throws Exception { + public void remove() throws Exception { FakeDataSet fakeDataSet = new FakeDataSet() .setData(TEST_MPD_URI, TEST_MPD) @@ -309,7 +310,7 @@ public class DashDownloaderTest { } @Test - public void testRepresentationWithoutIndex() throws Exception { + public void representationWithoutIndex() throws Exception { FakeDataSet fakeDataSet = new FakeDataSet() .setData(TEST_MPD_URI, TEST_MPD_NO_INDEX) @@ -327,12 +328,16 @@ public class DashDownloaderTest { } private DashDownloader getDashDownloader(FakeDataSet fakeDataSet, StreamKey... keys) { - return getDashDownloader(new Factory().setFakeDataSet(fakeDataSet), keys); + return getDashDownloader(new FakeDataSource.Factory().setFakeDataSet(fakeDataSet), keys); } - private DashDownloader getDashDownloader(Factory factory, StreamKey... keys) { - return new DashDownloader( - TEST_MPD_URI, keysList(keys), new DownloaderConstructorHelper(cache, factory)); + private DashDownloader getDashDownloader( + FakeDataSource.Factory upstreamDataSourceFactory, StreamKey... keys) { + CacheDataSource.Factory cacheDataSourceFactory = + new CacheDataSource.Factory() + .setCache(cache) + .setUpstreamDataSourceFactory(upstreamDataSourceFactory); + return new DashDownloader(TEST_MPD_URI, keysList(keys), cacheDataSourceFactory); } private static ArrayList keysList(StreamKey... keys) { diff --git a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DownloadHelperTest.java b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DownloadHelperTest.java index 107bf7c790..5ecdba11eb 100644 --- a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DownloadHelperTest.java +++ b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DownloadHelperTest.java @@ -35,11 +35,11 @@ public final class DownloadHelperTest { ApplicationProvider.getApplicationContext(), Uri.parse("http://uri"), new FakeDataSource.Factory(), - (handler, videoListener, audioListener, text, metadata, drm) -> new Renderer[0]); + (handler, videoListener, audioListener, text, metadata) -> new Renderer[0]); DownloadHelper.forDash( Uri.parse("http://uri"), new FakeDataSource.Factory(), - (handler, videoListener, audioListener, text, metadata, drm) -> new Renderer[0], + (handler, videoListener, audioListener, text, metadata) -> new Renderer[0], /* drmSessionManager= */ DrmSessionManager.getDummyDrmSessionManager(), DownloadHelper.DEFAULT_TRACK_SELECTOR_PARAMETERS_WITHOUT_VIEWPORT); } 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 264b5d39e1..b16d5727b1 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 @@ -29,7 +29,6 @@ import com.google.android.exoplayer2.offline.DefaultDownloadIndex; import com.google.android.exoplayer2.offline.DefaultDownloaderFactory; import com.google.android.exoplayer2.offline.DownloadManager; import com.google.android.exoplayer2.offline.DownloadRequest; -import com.google.android.exoplayer2.offline.DownloaderConstructorHelper; import com.google.android.exoplayer2.offline.StreamKey; import com.google.android.exoplayer2.scheduler.Requirements; import com.google.android.exoplayer2.testutil.CacheAsserts.RequestSet; @@ -40,6 +39,7 @@ import com.google.android.exoplayer2.testutil.FakeDataSource; import com.google.android.exoplayer2.testutil.TestDownloadManagerListener; import com.google.android.exoplayer2.testutil.TestUtil; import com.google.android.exoplayer2.upstream.DataSource.Factory; +import com.google.android.exoplayer2.upstream.cache.CacheDataSource; import com.google.android.exoplayer2.upstream.cache.NoOpCacheEvictor; import com.google.android.exoplayer2.upstream.cache.SimpleCache; import com.google.android.exoplayer2.util.Util; @@ -111,7 +111,7 @@ public class DownloadManagerDashTest { // Disabled due to flakiness. @Ignore @Test - public void testSaveAndLoadActionFile() throws Throwable { + public void saveAndLoadActionFile() throws Throwable { // Configure fakeDataSet to block until interrupted when TEST_MPD is read. fakeDataSet .newData(TEST_MPD_URI) @@ -147,87 +147,71 @@ public class DownloadManagerDashTest { dummyMainThread.runOnMainThread(this::createDownloadManager); // Block on the test thread. - blockUntilTasksCompleteAndThrowAnyDownloadError(); + downloadManagerListener.blockUntilIdleAndThrowAnyFailure(); assertCachedData(cache, fakeDataSet); } @Test - public void testHandleDownloadRequest() throws Throwable { + public void handleDownloadRequest_downloadSuccess() throws Throwable { handleDownloadRequest(fakeStreamKey1, fakeStreamKey2); - blockUntilTasksCompleteAndThrowAnyDownloadError(); + downloadManagerListener.blockUntilIdleAndThrowAnyFailure(); assertCachedData(cache, new RequestSet(fakeDataSet).useBoundedDataSpecFor("audio_init_data")); } @Test - public void testHandleMultipleDownloadRequest() throws Throwable { + public void handleDownloadRequest_withRequest_downloadSuccess() throws Throwable { handleDownloadRequest(fakeStreamKey1); handleDownloadRequest(fakeStreamKey2); - blockUntilTasksCompleteAndThrowAnyDownloadError(); + downloadManagerListener.blockUntilIdleAndThrowAnyFailure(); assertCachedData(cache, new RequestSet(fakeDataSet).useBoundedDataSpecFor("audio_init_data")); } @Test - public void testHandleInterferingDownloadRequest() throws Throwable { + public void handleDownloadRequest_withInferringRequest_success() throws Throwable { fakeDataSet .newData("audio_segment_2") .appendReadAction(() -> handleDownloadRequest(fakeStreamKey2)) .appendReadData(TestUtil.buildTestData(5)) .endData(); - handleDownloadRequest(fakeStreamKey1); - - blockUntilTasksCompleteAndThrowAnyDownloadError(); + downloadManagerListener.blockUntilIdleAndThrowAnyFailure(); assertCachedData(cache, new RequestSet(fakeDataSet).useBoundedDataSpecFor("audio_init_data")); } @Test - public void testHandleRemoveAction() throws Throwable { + public void handleRemoveAction_blockUntilTaskCompleted_noDownloadedData() throws Throwable { handleDownloadRequest(fakeStreamKey1); - - blockUntilTasksCompleteAndThrowAnyDownloadError(); - + downloadManagerListener.blockUntilIdleAndThrowAnyFailure(); handleRemoveAction(); - - blockUntilTasksCompleteAndThrowAnyDownloadError(); - + downloadManagerListener.blockUntilIdleAndThrowAnyFailure(); assertCacheEmpty(cache); } @Test - public void testHandleRemoveActionBeforeDownloadFinish() throws Throwable { + public void handleRemoveAction_beforeDownloadFinish_noDownloadedData() throws Throwable { handleDownloadRequest(fakeStreamKey1); handleRemoveAction(); - - blockUntilTasksCompleteAndThrowAnyDownloadError(); - + downloadManagerListener.blockUntilIdleAndThrowAnyFailure(); assertCacheEmpty(cache); } @Test - public void testHandleInterferingRemoveAction() throws Throwable { + public void handleRemoveAction_withInterfering_noDownloadedData() throws Throwable { CountDownLatch downloadInProgressLatch = new CountDownLatch(1); fakeDataSet .newData("audio_segment_2") .appendReadAction(downloadInProgressLatch::countDown) .appendReadData(TestUtil.buildTestData(5)) .endData(); - handleDownloadRequest(fakeStreamKey1); - assertThat(downloadInProgressLatch.await(ASSERT_TRUE_TIMEOUT_MS, TimeUnit.MILLISECONDS)) .isTrue(); handleRemoveAction(); - - blockUntilTasksCompleteAndThrowAnyDownloadError(); - + downloadManagerListener.blockUntilIdleAndThrowAnyFailure(); assertCacheEmpty(cache); } - private void blockUntilTasksCompleteAndThrowAnyDownloadError() throws Throwable { - downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); - } - private void handleDownloadRequest(StreamKey... keys) { DownloadRequest request = getDownloadRequest(keys); runOnMainThread(() -> downloadManager.addDownload(request)); @@ -253,17 +237,16 @@ public class DownloadManagerDashTest { runOnMainThread( () -> { Factory fakeDataSourceFactory = new FakeDataSource.Factory().setFakeDataSet(fakeDataSet); + DefaultDownloaderFactory downloaderFactory = + new DefaultDownloaderFactory( + new CacheDataSource.Factory() + .setCache(cache) + .setUpstreamDataSourceFactory(fakeDataSourceFactory)); downloadManager = new DownloadManager( - ApplicationProvider.getApplicationContext(), - downloadIndex, - new DefaultDownloaderFactory( - new DownloaderConstructorHelper(cache, fakeDataSourceFactory))); + ApplicationProvider.getApplicationContext(), downloadIndex, downloaderFactory); downloadManager.setRequirements(new Requirements(0)); - - downloadManagerListener = - new TestDownloadManagerListener( - downloadManager, dummyMainThread, /* timeoutMs= */ 3000); + downloadManagerListener = new TestDownloadManagerListener(downloadManager); downloadManager.resumeDownloads(); }); } 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 fd295ea18d..0073b3bfaf 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 @@ -33,7 +33,6 @@ import com.google.android.exoplayer2.offline.Download; import com.google.android.exoplayer2.offline.DownloadManager; import com.google.android.exoplayer2.offline.DownloadRequest; import com.google.android.exoplayer2.offline.DownloadService; -import com.google.android.exoplayer2.offline.DownloaderConstructorHelper; import com.google.android.exoplayer2.offline.StreamKey; import com.google.android.exoplayer2.scheduler.Scheduler; import com.google.android.exoplayer2.testutil.DummyMainThread; @@ -42,6 +41,7 @@ import com.google.android.exoplayer2.testutil.FakeDataSource; import com.google.android.exoplayer2.testutil.TestDownloadManagerListener; import com.google.android.exoplayer2.testutil.TestUtil; import com.google.android.exoplayer2.upstream.DataSource; +import com.google.android.exoplayer2.upstream.cache.CacheDataSource; import com.google.android.exoplayer2.upstream.cache.NoOpCacheEvictor; import com.google.android.exoplayer2.upstream.cache.SimpleCache; import com.google.android.exoplayer2.util.ConditionVariable; @@ -113,14 +113,15 @@ public class DownloadServiceDashTest { () -> { DefaultDownloadIndex downloadIndex = new DefaultDownloadIndex(TestUtil.getInMemoryDatabaseProvider()); + DefaultDownloaderFactory downloaderFactory = + new DefaultDownloaderFactory( + new CacheDataSource.Factory() + .setCache(cache) + .setUpstreamDataSourceFactory(fakeDataSourceFactory)); final DownloadManager dashDownloadManager = new DownloadManager( - ApplicationProvider.getApplicationContext(), - downloadIndex, - new DefaultDownloaderFactory( - new DownloaderConstructorHelper(cache, fakeDataSourceFactory))); - downloadManagerListener = - new TestDownloadManagerListener(dashDownloadManager, dummyMainThread); + ApplicationProvider.getApplicationContext(), downloadIndex, downloaderFactory); + downloadManagerListener = new TestDownloadManagerListener(dashDownloadManager); dashDownloadManager.resumeDownloads(); dashDownloadService = @@ -130,8 +131,8 @@ public class DownloadServiceDashTest { return dashDownloadManager; } - @Nullable @Override + @Nullable protected Scheduler getScheduler() { return null; } @@ -154,38 +155,38 @@ public class DownloadServiceDashTest { @Ignore // b/78877092 @Test - public void testMultipleDownloadRequest() throws Throwable { + public void multipleDownloadRequest() throws Throwable { downloadKeys(fakeStreamKey1); downloadKeys(fakeStreamKey2); - downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); + downloadManagerListener.blockUntilIdleAndThrowAnyFailure(); assertCachedData(cache, fakeDataSet); } @Ignore // b/78877092 @Test - public void testRemoveAction() throws Throwable { + public void removeAction() throws Throwable { downloadKeys(fakeStreamKey1, fakeStreamKey2); - downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); + downloadManagerListener.blockUntilIdleAndThrowAnyFailure(); removeAll(); - downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); + downloadManagerListener.blockUntilIdleAndThrowAnyFailure(); assertCacheEmpty(cache); } @Ignore // b/78877092 @Test - public void testRemoveBeforeDownloadComplete() throws Throwable { + public void removeBeforeDownloadComplete() throws Throwable { pauseDownloadCondition = new ConditionVariable(); downloadKeys(fakeStreamKey1, fakeStreamKey2); removeAll(); - downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); + downloadManagerListener.blockUntilIdleAndThrowAnyFailure(); assertCacheEmpty(cache); } @@ -219,5 +220,4 @@ public class DownloadServiceDashTest { dashDownloadService.onStartCommand(startIntent, 0, 0); }); } - } diff --git a/library/extractor/README.md b/library/extractor/README.md new file mode 100644 index 0000000000..28e0ccdc0a --- /dev/null +++ b/library/extractor/README.md @@ -0,0 +1,11 @@ +# ExoPlayer extractor library module # + +Provides media container extractors. + +## Links ## + +* [Javadoc][]: Classes matching `com.google.android.exoplayer2.extractor.*` + belong to this module. + +[Javadoc]: https://exoplayer.dev/doc/reference/index.html + diff --git a/library/extractor/build.gradle b/library/extractor/build.gradle new file mode 100644 index 0000000000..26b38705ee --- /dev/null +++ b/library/extractor/build.gradle @@ -0,0 +1,62 @@ +// Copyright (C) 2020 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. +apply from: '../../constants.gradle' +apply plugin: 'com.android.library' + +android { + compileSdkVersion project.ext.compileSdkVersion + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + defaultConfig { + minSdkVersion project.ext.minSdkVersion + targetSdkVersion project.ext.targetSdkVersion + } + + buildTypes { + debug { + testCoverageEnabled = true + } + } + + sourceSets.test.assets.srcDir '../../testdata/src/test/assets/' + + testOptions.unitTests.includeAndroidResources = true +} + +dependencies { + implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion + compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion + compileOnly 'org.checkerframework:checker-compat-qual:' + checkerframeworkVersion + compileOnly 'org.jetbrains.kotlin:kotlin-annotations-jvm:' + kotlinAnnotationsVersion + implementation project(modulePrefix + 'library-common') + testImplementation project(modulePrefix + 'library-core') + testImplementation project(modulePrefix + 'testutils') + testImplementation project(modulePrefix + 'testdata') + testImplementation 'org.robolectric:robolectric:' + robolectricVersion +} + +ext { + javadocTitle = 'Extractor module' +} +apply from: '../../javadoc_library.gradle' + +ext { + releaseArtifact = 'exoplayer-extractor' + releaseDescription = 'The ExoPlayer library extractor module.' +} +apply from: '../../publish.gradle' diff --git a/library/extractor/proguard-rules.txt b/library/extractor/proguard-rules.txt new file mode 100644 index 0000000000..5f97a491cb --- /dev/null +++ b/library/extractor/proguard-rules.txt @@ -0,0 +1,12 @@ +# Proguard rules specific to the extractor module. + +# Constructors accessed via reflection in DefaultExtractorsFactory +-dontnote com.google.android.exoplayer2.ext.flac.FlacExtractor +-keepclassmembers class com.google.android.exoplayer2.ext.flac.FlacExtractor { + (); +} + +# Don't warn about checkerframework and Kotlin annotations +-dontwarn org.checkerframework.** +-dontwarn kotlin.annotations.jvm.** +-dontwarn javax.annotation.** diff --git a/library/extractor/src/main/AndroidManifest.xml b/library/extractor/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..8748afd2cf --- /dev/null +++ b/library/extractor/src/main/AndroidManifest.xml @@ -0,0 +1,17 @@ + + + + diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/BinarySearchSeeker.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/BinarySearchSeeker.java similarity index 97% rename from library/core/src/main/java/com/google/android/exoplayer2/extractor/BinarySearchSeeker.java rename to library/extractor/src/main/java/com/google/android/exoplayer2/extractor/BinarySearchSeeker.java index 0d823fa31d..b5eb092dfb 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/BinarySearchSeeker.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/BinarySearchSeeker.java @@ -49,10 +49,9 @@ public abstract class BinarySearchSeeker { * @param targetTimestamp The target timestamp. * @return A {@link TimestampSearchResult} that describes the result of the search. * @throws IOException If an error occurred reading from the input. - * @throws InterruptedException If the thread was interrupted. */ TimestampSearchResult searchForTimestamp(ExtractorInput input, long targetTimestamp) - throws IOException, InterruptedException; + throws IOException; /** Called when a seek operation finishes. */ default void onSeekFinished() {} @@ -91,7 +90,7 @@ public abstract class BinarySearchSeeker { protected final BinarySearchSeekMap seekMap; protected final TimestampSeeker timestampSeeker; - protected @Nullable SeekOperationParams seekOperationParams; + @Nullable protected SeekOperationParams seekOperationParams; private final int minimumSearchRange; @@ -169,13 +168,12 @@ public abstract class BinarySearchSeeker { * 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 int handlePendingSeek(ExtractorInput input, PositionHolder seekPositionHolder) - throws InterruptedException, IOException { - TimestampSeeker timestampSeeker = Assertions.checkNotNull(this.timestampSeeker); + throws IOException { while (true) { - SeekOperationParams seekOperationParams = Assertions.checkNotNull(this.seekOperationParams); + SeekOperationParams seekOperationParams = + Assertions.checkStateNotNull(this.seekOperationParams); long floorPosition = seekOperationParams.getFloorBytePosition(); long ceilingPosition = seekOperationParams.getCeilingBytePosition(); long searchPosition = seekOperationParams.getNextSearchBytePosition(); @@ -203,9 +201,9 @@ public abstract class BinarySearchSeeker { timestampSearchResult.timestampToUpdate, timestampSearchResult.bytePositionToUpdate); break; case TimestampSearchResult.TYPE_TARGET_TIMESTAMP_FOUND: + skipInputUntilPosition(input, timestampSearchResult.bytePositionToUpdate); markSeekOperationFinished( /* foundTargetFrame= */ true, timestampSearchResult.bytePositionToUpdate); - skipInputUntilPosition(input, timestampSearchResult.bytePositionToUpdate); return seekToPosition( input, timestampSearchResult.bytePositionToUpdate, seekPositionHolder); case TimestampSearchResult.TYPE_NO_TIMESTAMP: @@ -241,7 +239,7 @@ public abstract class BinarySearchSeeker { } protected final boolean skipInputUntilPosition(ExtractorInput input, long position) - throws IOException, InterruptedException { + throws IOException { long bytesToSkip = position - input.getPosition(); if (bytesToSkip >= 0 && bytesToSkip <= MAX_SKIP_BYTES) { input.skipFully((int) bytesToSkip); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/cea/CeaUtil.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/CeaUtil.java similarity index 96% rename from library/core/src/main/java/com/google/android/exoplayer2/text/cea/CeaUtil.java rename to library/extractor/src/main/java/com/google/android/exoplayer2/extractor/CeaUtil.java index cdc545e459..4c3f97975e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/cea/CeaUtil.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/CeaUtil.java @@ -13,10 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.google.android.exoplayer2.text.cea; +package com.google.android.exoplayer2.extractor; import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.ParsableByteArray; @@ -41,8 +40,8 @@ public final class CeaUtil { * @param seiBuffer The unescaped SEI NAL unit data, excluding the NAL unit start code and type. * @param outputs The outputs to which any samples should be written. */ - public static void consume(long presentationTimeUs, ParsableByteArray seiBuffer, - TrackOutput[] outputs) { + public static void consume( + long presentationTimeUs, ParsableByteArray seiBuffer, TrackOutput[] outputs) { while (seiBuffer.bytesLeft() > 1 /* last byte will be rbsp_trailing_bits */) { int payloadType = readNon255TerminatedValue(seiBuffer); int payloadSize = readNon255TerminatedValue(seiBuffer); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ChunkIndex.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ChunkIndex.java similarity index 96% rename from library/core/src/main/java/com/google/android/exoplayer2/extractor/ChunkIndex.java rename to library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ChunkIndex.java index 7ddd03bbd5..45c567235a 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ChunkIndex.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ChunkIndex.java @@ -76,7 +76,7 @@ public final class ChunkIndex implements SeekMap { * @return The index of the corresponding chunk. */ public int getChunkIndex(long timeUs) { - return Util.binarySearchFloor(timesUs, timeUs, true, true); + return Util.binarySearchFloor(timesUs, timeUs, /* inclusive= */ true, /* stayInBounds= */ true); } // SeekMap implementation. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ConstantBitrateSeekMap.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ConstantBitrateSeekMap.java similarity index 100% rename from library/core/src/main/java/com/google/android/exoplayer2/extractor/ConstantBitrateSeekMap.java rename to library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ConstantBitrateSeekMap.java diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/DefaultExtractorInput.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/DefaultExtractorInput.java similarity index 74% rename from library/core/src/main/java/com/google/android/exoplayer2/extractor/DefaultExtractorInput.java rename to library/extractor/src/main/java/com/google/android/exoplayer2/extractor/DefaultExtractorInput.java index 450cca42b0..4ab306a234 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/DefaultExtractorInput.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/DefaultExtractorInput.java @@ -16,16 +16,15 @@ package com.google.android.exoplayer2.extractor; import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.upstream.DataSource; +import com.google.android.exoplayer2.upstream.DataReader; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Util; import java.io.EOFException; import java.io.IOException; +import java.io.InterruptedIOException; import java.util.Arrays; -/** - * An {@link ExtractorInput} that wraps a {@link DataSource}. - */ +/** An {@link ExtractorInput} that wraps a {@link DataReader}. */ public final class DefaultExtractorInput implements ExtractorInput { private static final int PEEK_MIN_FREE_SPACE_AFTER_RESIZE = 64 * 1024; @@ -33,7 +32,7 @@ public final class DefaultExtractorInput implements ExtractorInput { private static final int SCRATCH_SPACE_SIZE = 4096; private final byte[] scratchSpace; - private final DataSource dataSource; + private final DataReader dataReader; private final long streamLength; private long position; @@ -42,12 +41,12 @@ public final class DefaultExtractorInput implements ExtractorInput { private int peekBufferLength; /** - * @param dataSource The wrapped {@link DataSource}. + * @param dataReader The wrapped {@link DataReader}. * @param position The initial position in the stream. * @param length The length of the stream, or {@link C#LENGTH_UNSET} if it is unknown. */ - public DefaultExtractorInput(DataSource dataSource, long position, long length) { - this.dataSource = dataSource; + public DefaultExtractorInput(DataReader dataReader, long position, long length) { + this.dataReader = dataReader; this.position = position; this.streamLength = length; peekBuffer = new byte[PEEK_MIN_FREE_SPACE_AFTER_RESIZE]; @@ -55,10 +54,12 @@ public final class DefaultExtractorInput implements ExtractorInput { } @Override - public int read(byte[] target, int offset, int length) throws IOException, InterruptedException { + public int read(byte[] target, int offset, int length) throws IOException { int bytesRead = readFromPeekBuffer(target, offset, length); if (bytesRead == 0) { - bytesRead = readFromDataSource(target, offset, length, 0, true); + bytesRead = + readFromUpstream( + target, offset, length, /* bytesAlreadyRead= */ 0, /* allowEndOfInput= */ true); } commitBytesRead(bytesRead); return bytesRead; @@ -66,53 +67,76 @@ public final class DefaultExtractorInput implements ExtractorInput { @Override public boolean readFully(byte[] target, int offset, int length, boolean allowEndOfInput) - throws IOException, InterruptedException { + throws IOException { int bytesRead = readFromPeekBuffer(target, offset, length); while (bytesRead < length && bytesRead != C.RESULT_END_OF_INPUT) { - bytesRead = readFromDataSource(target, offset, length, bytesRead, allowEndOfInput); + bytesRead = readFromUpstream(target, offset, length, bytesRead, allowEndOfInput); } commitBytesRead(bytesRead); return bytesRead != C.RESULT_END_OF_INPUT; } @Override - public void readFully(byte[] target, int offset, int length) - throws IOException, InterruptedException { + public void readFully(byte[] target, int offset, int length) throws IOException { readFully(target, offset, length, false); } @Override - public int skip(int length) throws IOException, InterruptedException { + public int skip(int length) throws IOException { int bytesSkipped = skipFromPeekBuffer(length); if (bytesSkipped == 0) { bytesSkipped = - readFromDataSource(scratchSpace, 0, Math.min(length, scratchSpace.length), 0, true); + readFromUpstream(scratchSpace, 0, Math.min(length, scratchSpace.length), 0, true); } commitBytesRead(bytesSkipped); return bytesSkipped; } @Override - public boolean skipFully(int length, boolean allowEndOfInput) - throws IOException, InterruptedException { + public boolean skipFully(int length, boolean allowEndOfInput) throws IOException { int bytesSkipped = skipFromPeekBuffer(length); while (bytesSkipped < length && bytesSkipped != C.RESULT_END_OF_INPUT) { int minLength = Math.min(length, bytesSkipped + scratchSpace.length); bytesSkipped = - readFromDataSource(scratchSpace, -bytesSkipped, minLength, bytesSkipped, allowEndOfInput); + readFromUpstream(scratchSpace, -bytesSkipped, minLength, bytesSkipped, allowEndOfInput); } commitBytesRead(bytesSkipped); return bytesSkipped != C.RESULT_END_OF_INPUT; } @Override - public void skipFully(int length) throws IOException, InterruptedException { + public void skipFully(int length) throws IOException { skipFully(length, false); } + @Override + public int peek(byte[] target, int offset, int length) throws IOException { + ensureSpaceForPeek(length); + int peekBufferRemainingBytes = peekBufferLength - peekBufferPosition; + int bytesPeeked; + if (peekBufferRemainingBytes == 0) { + bytesPeeked = + readFromUpstream( + peekBuffer, + peekBufferPosition, + length, + /* bytesAlreadyRead= */ 0, + /* allowEndOfInput= */ true); + if (bytesPeeked == C.RESULT_END_OF_INPUT) { + return C.RESULT_END_OF_INPUT; + } + peekBufferLength += bytesPeeked; + } else { + bytesPeeked = Math.min(length, peekBufferRemainingBytes); + } + System.arraycopy(peekBuffer, peekBufferPosition, target, offset, bytesPeeked); + peekBufferPosition += bytesPeeked; + return bytesPeeked; + } + @Override public boolean peekFully(byte[] target, int offset, int length, boolean allowEndOfInput) - throws IOException, InterruptedException { + throws IOException { if (!advancePeekPosition(length, allowEndOfInput)) { return false; } @@ -121,19 +145,17 @@ public final class DefaultExtractorInput implements ExtractorInput { } @Override - public void peekFully(byte[] target, int offset, int length) - throws IOException, InterruptedException { + public void peekFully(byte[] target, int offset, int length) throws IOException { peekFully(target, offset, length, false); } @Override - public boolean advancePeekPosition(int length, boolean allowEndOfInput) - throws IOException, InterruptedException { + public boolean advancePeekPosition(int length, boolean allowEndOfInput) throws IOException { ensureSpaceForPeek(length); int bytesPeeked = peekBufferLength - peekBufferPosition; while (bytesPeeked < length) { - bytesPeeked = readFromDataSource(peekBuffer, peekBufferPosition, length, bytesPeeked, - allowEndOfInput); + bytesPeeked = + readFromUpstream(peekBuffer, peekBufferPosition, length, bytesPeeked, allowEndOfInput); if (bytesPeeked == C.RESULT_END_OF_INPUT) { return false; } @@ -144,7 +166,7 @@ public final class DefaultExtractorInput implements ExtractorInput { } @Override - public void advancePeekPosition(int length) throws IOException, InterruptedException { + public void advancePeekPosition(int length) throws IOException { advancePeekPosition(length, false); } @@ -201,7 +223,7 @@ public final class DefaultExtractorInput implements ExtractorInput { } /** - * Reads from the peek buffer + * Reads from the peek buffer. * * @param target A target array into which data should be written. * @param offset The offset into the target array at which to write. @@ -235,7 +257,7 @@ public final class DefaultExtractorInput implements ExtractorInput { } /** - * Starts or continues a read from the data source. + * Starts or continues a read from the data reader. * * @param target A target array into which data should be written. * @param offset The offset into the target array at which to write. @@ -244,20 +266,20 @@ public final class DefaultExtractorInput implements ExtractorInput { * @param allowEndOfInput True if encountering the end of the input having read no data is * allowed, and should result in {@link C#RESULT_END_OF_INPUT} being returned. False if it * should be considered an error, causing an {@link EOFException} to be thrown. - * @return The total number of bytes read so far, or {@link C#RESULT_END_OF_INPUT} if - * {@code allowEndOfInput} is true and the input has ended having read no bytes. + * @return The total number of bytes read so far, or {@link C#RESULT_END_OF_INPUT} if {@code + * allowEndOfInput} is true and the input has ended having read no bytes. * @throws EOFException If the end of input was encountered having partially satisfied the read * (i.e. having read at least one byte, but fewer than {@code length}), or if no bytes were * read and {@code allowEndOfInput} is false. * @throws IOException If an error occurs reading from the input. - * @throws InterruptedException If the thread is interrupted. */ - private int readFromDataSource(byte[] target, int offset, int length, int bytesAlreadyRead, - boolean allowEndOfInput) throws InterruptedException, IOException { + private int readFromUpstream( + byte[] target, int offset, int length, int bytesAlreadyRead, boolean allowEndOfInput) + throws IOException { if (Thread.interrupted()) { - throw new InterruptedException(); + throw new InterruptedIOException(); } - int bytesRead = dataSource.read(target, offset + bytesAlreadyRead, length - bytesAlreadyRead); + int bytesRead = dataReader.read(target, offset + bytesAlreadyRead, length - bytesAlreadyRead); if (bytesRead == C.RESULT_END_OF_INPUT) { if (bytesAlreadyRead == 0 && allowEndOfInput) { return C.RESULT_END_OF_INPUT; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/DefaultExtractorsFactory.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/DefaultExtractorsFactory.java similarity index 75% rename from library/core/src/main/java/com/google/android/exoplayer2/extractor/DefaultExtractorsFactory.java rename to library/extractor/src/main/java/com/google/android/exoplayer2/extractor/DefaultExtractorsFactory.java index 26f250feea..9306a146d5 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/DefaultExtractorsFactory.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/DefaultExtractorsFactory.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.extractor; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.extractor.amr.AmrExtractor; import com.google.android.exoplayer2.extractor.flac.FlacExtractor; import com.google.android.exoplayer2.extractor.flv.FlvExtractor; @@ -51,21 +52,36 @@ import java.lang.reflect.Constructor; *
      • AC3 ({@link Ac3Extractor}) *
      • AC4 ({@link Ac4Extractor}) *
      • AMR ({@link AmrExtractor}) - *
      • FLAC (only available if the FLAC extension is built and included) + *
      • FLAC + *
          + *
        • If available, the FLAC extension extractor is used. + *
        • Otherwise, the core {@link FlacExtractor} is used. Note that Android devices do not + * generally include a FLAC decoder before API 27. This can be worked around by using + * the FLAC extension or the FFmpeg extension. + *
        *
      */ public final class DefaultExtractorsFactory implements ExtractorsFactory { + @Nullable private static final Constructor FLAC_EXTENSION_EXTRACTOR_CONSTRUCTOR; static { - Constructor flacExtensionExtractorConstructor = null; + @Nullable Constructor flacExtensionExtractorConstructor = null; try { // LINT.IfChange - flacExtensionExtractorConstructor = - Class.forName("com.google.android.exoplayer2.ext.flac.FlacExtractor") - .asSubclass(Extractor.class) - .getConstructor(); + @SuppressWarnings("nullness:argument.type.incompatible") + boolean isFlacNativeLibraryAvailable = + Boolean.TRUE.equals( + Class.forName("com.google.android.exoplayer2.ext.flac.FlacLibrary") + .getMethod("isAvailable") + .invoke(/* obj= */ null)); + if (isFlacNativeLibraryAvailable) { + flacExtensionExtractorConstructor = + Class.forName("com.google.android.exoplayer2.ext.flac.FlacExtractor") + .asSubclass(Extractor.class) + .getConstructor(); + } // LINT.ThenChange(../../../../../../../../proguard-rules.txt) } catch (ClassNotFoundException e) { // Expected if the app was built without the FLAC extension. @@ -77,14 +93,15 @@ public final class DefaultExtractorsFactory implements ExtractorsFactory { } 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; - private @Mp3Extractor.Flags int mp3Flags; - private @TsExtractor.Mode int tsMode; - private @DefaultTsPayloadReaderFactory.Flags int tsFlags; + @AdtsExtractor.Flags private int adtsFlags; + @AmrExtractor.Flags private int amrFlags; + @FlacExtractor.Flags private int coreFlacFlags; + @MatroskaExtractor.Flags private int matroskaFlags; + @Mp4Extractor.Flags private int mp4Flags; + @FragmentedMp4Extractor.Flags private int fragmentedMp4Flags; + @Mp3Extractor.Flags private int mp3Flags; + @TsExtractor.Mode private int tsMode; + @DefaultTsPayloadReaderFactory.Flags private int tsFlags; public DefaultExtractorsFactory() { tsMode = TsExtractor.MODE_SINGLE_PMT; @@ -132,6 +149,19 @@ public final class DefaultExtractorsFactory implements ExtractorsFactory { return this; } + /** + * Sets flags for {@link FlacExtractor} instances created by the factory. + * + * @see FlacExtractor#FlacExtractor(int) + * @param flags The flags to use. + * @return The factory, for convenience. + */ + public synchronized DefaultExtractorsFactory setCoreFlacExtractorFlags( + @FlacExtractor.Flags int flags) { + this.coreFlacFlags = flags; + return this; + } + /** * Sets flags for {@link MatroskaExtractor} instances created by the factory. * @@ -211,45 +241,46 @@ public final class DefaultExtractorsFactory implements ExtractorsFactory { @Override public synchronized Extractor[] createExtractors() { Extractor[] extractors = new Extractor[14]; - extractors[0] = new MatroskaExtractor(matroskaFlags); - extractors[1] = new FragmentedMp4Extractor(fragmentedMp4Flags); - extractors[2] = new Mp4Extractor(mp4Flags); - extractors[3] = - new Mp3Extractor( - mp3Flags - | (constantBitrateSeekingEnabled - ? Mp3Extractor.FLAG_ENABLE_CONSTANT_BITRATE_SEEKING - : 0)); - extractors[4] = - new AdtsExtractor( - 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( - amrFlags - | (constantBitrateSeekingEnabled - ? AmrExtractor.FLAG_ENABLE_CONSTANT_BITRATE_SEEKING - : 0)); - extractors[12] = new Ac4Extractor(); - // Prefer the FLAC extension extractor because it supports seeking. + // Extractors order is optimized according to + // https://docs.google.com/document/d/1w2mKaWMxfz2Ei8-LdxqbPs1VLe_oudB-eryXXw9OvQQ. + extractors[0] = new FlvExtractor(); if (FLAC_EXTENSION_EXTRACTOR_CONSTRUCTOR != null) { try { - extractors[13] = FLAC_EXTENSION_EXTRACTOR_CONSTRUCTOR.newInstance(); + extractors[1] = FLAC_EXTENSION_EXTRACTOR_CONSTRUCTOR.newInstance(); } catch (Exception e) { // Should never happen. throw new IllegalStateException("Unexpected error creating FLAC extractor", e); } } else { - extractors[13] = new FlacExtractor(); + extractors[1] = new FlacExtractor(coreFlacFlags); } + extractors[2] = new WavExtractor(); + extractors[3] = new FragmentedMp4Extractor(fragmentedMp4Flags); + extractors[4] = new Mp4Extractor(mp4Flags); + extractors[5] = + new AmrExtractor( + amrFlags + | (constantBitrateSeekingEnabled + ? AmrExtractor.FLAG_ENABLE_CONSTANT_BITRATE_SEEKING + : 0)); + extractors[6] = new PsExtractor(); + extractors[7] = new OggExtractor(); + extractors[8] = new TsExtractor(tsMode, tsFlags); + extractors[9] = new MatroskaExtractor(matroskaFlags); + extractors[10] = + new AdtsExtractor( + adtsFlags + | (constantBitrateSeekingEnabled + ? AdtsExtractor.FLAG_ENABLE_CONSTANT_BITRATE_SEEKING + : 0)); + extractors[11] = new Ac3Extractor(); + extractors[12] = new Ac4Extractor(); + extractors[13] = + new Mp3Extractor( + mp3Flags + | (constantBitrateSeekingEnabled + ? Mp3Extractor.FLAG_ENABLE_CONSTANT_BITRATE_SEEKING + : 0)); return extractors; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/DummyExtractorOutput.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/DummyExtractorOutput.java similarity index 100% rename from library/core/src/main/java/com/google/android/exoplayer2/extractor/DummyExtractorOutput.java rename to library/extractor/src/main/java/com/google/android/exoplayer2/extractor/DummyExtractorOutput.java diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/DummyTrackOutput.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/DummyTrackOutput.java similarity index 65% rename from library/core/src/main/java/com/google/android/exoplayer2/extractor/DummyTrackOutput.java rename to library/extractor/src/main/java/com/google/android/exoplayer2/extractor/DummyTrackOutput.java index f1aeccacb7..4700bbb480 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/DummyTrackOutput.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/DummyTrackOutput.java @@ -18,6 +18,7 @@ package com.google.android.exoplayer2.extractor; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.upstream.DataReader; import com.google.android.exoplayer2.util.ParsableByteArray; import java.io.EOFException; import java.io.IOException; @@ -27,15 +28,26 @@ import java.io.IOException; */ public final class DummyTrackOutput implements TrackOutput { + // Even though read data is discarded, data source implementations could be making use of the + // buffer contents. For example, caches. So we cannot use a static field for this which could be + // shared between different threads. + private final byte[] readBuffer; + + public DummyTrackOutput() { + readBuffer = new byte[4096]; + } + @Override public void format(Format format) { // Do nothing. } @Override - public int sampleData(ExtractorInput input, int length, boolean allowEndOfInput) - throws IOException, InterruptedException { - int bytesSkipped = input.skip(length); + public int sampleData( + DataReader input, int length, boolean allowEndOfInput, @SampleDataPart int sampleDataPart) + throws IOException { + int bytesToSkipByReading = Math.min(readBuffer.length, length); + int bytesSkipped = input.read(readBuffer, /* offset= */ 0, bytesToSkipByReading); if (bytesSkipped == C.RESULT_END_OF_INPUT) { if (allowEndOfInput) { return C.RESULT_END_OF_INPUT; @@ -46,7 +58,7 @@ public final class DummyTrackOutput implements TrackOutput { } @Override - public void sampleData(ParsableByteArray data, int length) { + public void sampleData(ParsableByteArray data, int length, @SampleDataPart int sampleDataPart) { data.skipBytes(length); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/Extractor.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/Extractor.java similarity index 87% rename from library/core/src/main/java/com/google/android/exoplayer2/extractor/Extractor.java rename to library/extractor/src/main/java/com/google/android/exoplayer2/extractor/Extractor.java index a9151a1b7c..d1371d56b6 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/Extractor.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/Extractor.java @@ -57,16 +57,15 @@ public interface Extractor { /** * Returns whether this extractor can extract samples from the {@link ExtractorInput}, which must * provide data from the start of the stream. - *

      - * If {@code true} is returned, the {@code input}'s reading position may have been modified. + * + *

      If {@code true} is returned, the {@code input}'s reading position may have been modified. * Otherwise, only its peek position may have been modified. * * @param input The {@link ExtractorInput} from which data should be peeked/read. * @return Whether this extractor can read the provided input. * @throws IOException If an error occurred reading from the input. - * @throws InterruptedException If the thread was interrupted. */ - boolean sniff(ExtractorInput input) throws IOException, InterruptedException; + boolean sniff(ExtractorInput input) throws IOException; /** * Initializes the extractor with an {@link ExtractorOutput}. Called at most once. @@ -89,20 +88,18 @@ public interface Extractor { * {@link #RESULT_SEEK} is returned. If the extractor reached the end of the data provided by the * {@link ExtractorInput}, then {@link #RESULT_END_OF_INPUT} is returned. * - *

      When this method throws an {@link IOException} or an {@link InterruptedException}, - * extraction may continue by providing an {@link ExtractorInput} with an unchanged {@link - * ExtractorInput#getPosition() read position} to a subsequent call to this method. + *

      When this method throws an {@link IOException}, extraction may continue by providing an + * {@link ExtractorInput} with an unchanged {@link ExtractorInput#getPosition() read position} to + * a subsequent call to this method. * * @param input The {@link ExtractorInput} from which data should be read. * @param seekPosition If {@link #RESULT_SEEK} is returned, this holder is updated to hold the * position of the required data. * @return One of the {@code RESULT_} values defined in this interface. * @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; + int read(ExtractorInput input, PositionHolder seekPosition) throws IOException; /** * Notifies the extractor that a seek has occurred. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ExtractorInput.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ExtractorInput.java similarity index 60% rename from library/core/src/main/java/com/google/android/exoplayer2/extractor/ExtractorInput.java rename to library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ExtractorInput.java index 45650c45fa..6799ca6de1 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ExtractorInput.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ExtractorInput.java @@ -16,18 +16,60 @@ package com.google.android.exoplayer2.extractor; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.upstream.DataReader; import java.io.EOFException; import java.io.IOException; +import java.io.InputStream; /** * Provides data to be consumed by an {@link Extractor}. + * + *

      This interface provides two modes of accessing the underlying input. See the subheadings below + * for more info about each mode. + * + *

        + *
      • The {@code read()/peek()} and {@code skip()} methods provide {@link InputStream}-like + * byte-level access operations. + *
      • The {@code read/skip/peekFully()} and {@code advancePeekPosition()} methods assume the user + * wants to read an entire block/frame/header of known length. + *
      + * + *

      {@link InputStream}-like methods

      + * + *

      The {@code read()/peek()} and {@code skip()} methods provide {@link InputStream}-like + * byte-level access operations. The {@code length} parameter is a maximum, and each method returns + * the number of bytes actually processed. This may be less than {@code length} because the end of + * the input was reached, or the method was interrupted, or the operation was aborted early for + * another reason. + * + *

      Block-based methods

      + * + *

      The {@code read/skip/peekFully()} and {@code advancePeekPosition()} methods assume the user + * wants to read an entire block/frame/header of known length. + * + *

      These methods all have a variant that takes a boolean {@code allowEndOfInput} parameter. This + * parameter is intended to be set to true when the caller believes the input might be fully + * exhausted before the call is made (i.e. they've previously read/skipped/peeked the final + * block/frame/header). It's not intended to allow a partial read (i.e. greater than 0 bytes, + * but less than {@code length}) to succeed - this will always throw an {@link EOFException} from + * these methods (a partial read is assumed to indicate a malformed block/frame/header - and + * therefore a malformed file). + * + *

      The expected behaviour of the block-based methods is therefore: + * + *

        + *
      • Already at end-of-input and {@code allowEndOfInput=false}: Throw {@link EOFException}. + *
      • Already at end-of-input and {@code allowEndOfInput=true}: Return {@code false}. + *
      • Encounter end-of-input during read/skip/peek/advance: Throw {@link EOFException} + * (regardless of {@code allowEndOfInput}). + *
      */ -public interface ExtractorInput { +public interface ExtractorInput extends DataReader { /** * Reads up to {@code length} bytes from the input and resets the peek position. - *

      - * This method blocks until at least one byte of data can be read, the end of the input is + * + *

      This method blocks until at least one byte of data can be read, the end of the input is * detected, or an exception is thrown. * * @param target A target array into which data should be written. @@ -35,48 +77,41 @@ public interface ExtractorInput { * @param length The maximum number of bytes to read from the input. * @return The number of bytes read, or {@link C#RESULT_END_OF_INPUT} if the input has ended. * @throws IOException If an error occurs reading from the input. - * @throws InterruptedException If the thread has been interrupted. */ - int read(byte[] target, int offset, int length) throws IOException, InterruptedException; + @Override + int read(byte[] target, int offset, int length) throws IOException; /** * Like {@link #read(byte[], int, int)}, but reads the requested {@code length} in full. - *

      - * If the end of the input is found having read no data, then behavior is dependent on - * {@code allowEndOfInput}. If {@code allowEndOfInput == true} then {@code false} is returned. - * Otherwise an {@link EOFException} is thrown. - *

      - * Encountering the end of input having partially satisfied the read is always considered an - * error, and will result in an {@link EOFException} being thrown. * * @param target A target array into which data should be written. * @param offset The offset into the target array at which to write. * @param length The number of bytes to read from the input. * @param allowEndOfInput True if encountering the end of the input having read no data is * allowed, and should result in {@code false} being returned. False if it should be - * considered an error, causing an {@link EOFException} to be thrown. - * @return True if the read was successful. False if the end of the input was encountered having - * read no data. + * considered an error, causing an {@link EOFException} to be thrown. See note in class + * Javadoc. + * @return True if the read was successful. False if {@code allowEndOfInput=true} and the end of + * the input was encountered having read no data. * @throws EOFException If the end of input was encountered having partially satisfied the read * (i.e. having read at least one byte, but fewer than {@code length}), or if no bytes were * read and {@code allowEndOfInput} is false. * @throws IOException If an error occurs reading from the input. - * @throws InterruptedException If the thread has been interrupted. */ boolean readFully(byte[] target, int offset, int length, boolean allowEndOfInput) - throws IOException, InterruptedException; + throws IOException; /** - * Equivalent to {@code readFully(target, offset, length, false)}. + * Equivalent to {@link #readFully(byte[], int, int, boolean) readFully(target, offset, length, + * false)}. * * @param target A target array into which data should be written. * @param offset The offset into the target array at which to write. * @param length The number of bytes to read from the input. * @throws EOFException If the end of input was encountered. * @throws IOException If an error occurs reading from the input. - * @throws InterruptedException If the thread is interrupted. */ - void readFully(byte[] target, int offset, int length) throws IOException, InterruptedException; + void readFully(byte[] target, int offset, int length) throws IOException; /** * Like {@link #read(byte[], int, int)}, except the data is skipped instead of read. @@ -84,9 +119,8 @@ public interface ExtractorInput { * @param length The maximum number of bytes to skip from the input. * @return The number of bytes skipped, or {@link C#RESULT_END_OF_INPUT} if the input has ended. * @throws IOException If an error occurs reading from the input. - * @throws InterruptedException If the thread has been interrupted. */ - int skip(int length) throws IOException, InterruptedException; + int skip(int length) throws IOException; /** * Like {@link #readFully(byte[], int, int, boolean)}, except the data is skipped instead of read. @@ -94,107 +128,107 @@ public interface ExtractorInput { * @param length The number of bytes to skip from the input. * @param allowEndOfInput True if encountering the end of the input having skipped no data is * allowed, and should result in {@code false} being returned. False if it should be - * considered an error, causing an {@link EOFException} to be thrown. - * @return True if the skip was successful. False if the end of the input was encountered having - * skipped no data. + * considered an error, causing an {@link EOFException} to be thrown. See note in class + * Javadoc. + * @return True if the skip was successful. False if {@code allowEndOfInput=true} and the end of + * the input was encountered having skipped no data. * @throws EOFException If the end of input was encountered having partially satisfied the skip * (i.e. having skipped at least one byte, but fewer than {@code length}), or if no bytes were * skipped and {@code allowEndOfInput} is false. * @throws IOException If an error occurs reading from the input. - * @throws InterruptedException If the thread has been interrupted. */ - boolean skipFully(int length, boolean allowEndOfInput) throws IOException, InterruptedException; + boolean skipFully(int length, boolean allowEndOfInput) throws IOException; /** * Like {@link #readFully(byte[], int, int)}, except the data is skipped instead of read. - *

      - * Encountering the end of input is always considered an error, and will result in an - * {@link EOFException} being thrown. + * + *

      Encountering the end of input is always considered an error, and will result in an {@link + * EOFException} being thrown. * * @param length The number of bytes to skip from the input. * @throws EOFException If the end of input was encountered. * @throws IOException If an error occurs reading from the input. - * @throws InterruptedException If the thread is interrupted. */ - void skipFully(int length) throws IOException, InterruptedException; + void skipFully(int length) throws IOException; /** - * Peeks {@code length} bytes from the peek position, writing them into {@code target} at index - * {@code offset}. The current read position is left unchanged. - *

      - * If the end of the input is found having peeked no data, then behavior is dependent on - * {@code allowEndOfInput}. If {@code allowEndOfInput == true} then {@code false} is returned. - * Otherwise an {@link EOFException} is thrown. - *

      - * Calling {@link #resetPeekPosition()} resets the peek position to equal the current read + * Peeks up to {@code length} bytes from the peek position. The current read position is left + * unchanged. + * + *

      This method blocks until at least one byte of data can be peeked, the end of the input is + * detected, or an exception is thrown. + * + *

      Calling {@link #resetPeekPosition()} resets the peek position to equal the current read * position, so the caller can peek the same data again. Reading or skipping also resets the peek * position. * * @param target A target array into which data should be written. * @param offset The offset into the target array at which to write. + * @param length The maximum number of bytes to peek from the input. + * @return The number of bytes peeked, or {@link C#RESULT_END_OF_INPUT} if the input has ended. + * @throws IOException If an error occurs peeking from the input. + */ + int peek(byte[] target, int offset, int length) throws IOException; + + /** + * Like {@link #peek(byte[], int, int)}, but peeks the requested {@code length} in full. + * + * @param target A target array into which data should be written. + * @param offset The offset into the target array at which to write. * @param length The number of bytes to peek from the input. * @param allowEndOfInput True if encountering the end of the input having peeked no data is * allowed, and should result in {@code false} being returned. False if it should be - * considered an error, causing an {@link EOFException} to be thrown. - * @return True if the peek was successful. False if the end of the input was encountered having - * peeked no data. + * considered an error, causing an {@link EOFException} to be thrown. See note in class + * Javadoc. + * @return True if the peek was successful. False if {@code allowEndOfInput=true} and the end of + * the input was encountered having peeked no data. * @throws EOFException If the end of input was encountered having partially satisfied the peek * (i.e. having peeked at least one byte, but fewer than {@code length}), or if no bytes were * peeked and {@code allowEndOfInput} is false. * @throws IOException If an error occurs peeking from the input. - * @throws InterruptedException If the thread is interrupted. */ boolean peekFully(byte[] target, int offset, int length, boolean allowEndOfInput) - throws IOException, InterruptedException; + throws IOException; /** - * Peeks {@code length} bytes from the peek position, writing them into {@code target} at index - * {@code offset}. The current read position is left unchanged. - *

      - * Calling {@link #resetPeekPosition()} resets the peek position to equal the current read - * position, so the caller can peek the same data again. Reading and skipping also reset the peek - * position. + * Equivalent to {@link #peekFully(byte[], int, int, boolean) peekFully(target, offset, length, + * false)}. * * @param target A target array into which data should be written. * @param offset The offset into the target array at which to write. * @param length The number of bytes to peek from the input. * @throws EOFException If the end of input was encountered. * @throws IOException If an error occurs peeking from the input. - * @throws InterruptedException If the thread is interrupted. */ - void peekFully(byte[] target, int offset, int length) throws IOException, InterruptedException; + void peekFully(byte[] target, int offset, int length) throws IOException; /** - * Advances the peek position by {@code length} bytes. - *

      - * If the end of the input is encountered before advancing the peek position, then behavior is - * dependent on {@code allowEndOfInput}. If {@code allowEndOfInput == true} then {@code false} is - * returned. Otherwise an {@link EOFException} is thrown. + * Advances the peek position by {@code length} bytes. Like {@link #peekFully(byte[], int, int, + * boolean)} except the data is skipped instead of read. * * @param length The number of bytes by which to advance the peek position. * @param allowEndOfInput True if encountering the end of the input before advancing is allowed, * and should result in {@code false} being returned. False if it should be considered an - * error, causing an {@link EOFException} to be thrown. - * @return True if advancing the peek position was successful. False if the end of the input was - * encountered before the peek position could be advanced. + * error, causing an {@link EOFException} to be thrown. See note in class Javadoc. + * @return True if advancing the peek position was successful. False if {@code + * allowEndOfInput=true} and the end of the input was encountered before advancing over any + * data. * @throws EOFException If the end of input was encountered having partially advanced (i.e. having * advanced by at least one byte, but fewer than {@code length}), or if the end of input was * encountered before advancing and {@code allowEndOfInput} is false. * @throws IOException If an error occurs advancing the peek position. - * @throws InterruptedException If the thread is interrupted. */ - boolean advancePeekPosition(int length, boolean allowEndOfInput) - throws IOException, InterruptedException; + boolean advancePeekPosition(int length, boolean allowEndOfInput) throws IOException; /** - * Advances the peek position by {@code length} bytes. + * Advances the peek position by {@code length} bytes. Like {@link #peekFully(byte[], int, int)} + * except the data is skipped instead of read. * * @param length The number of bytes to peek from the input. * @throws EOFException If the end of input was encountered. * @throws IOException If an error occurs peeking from the input. - * @throws InterruptedException If the thread is interrupted. */ - void advancePeekPosition(int length) throws IOException, InterruptedException; + void advancePeekPosition(int length) throws IOException; /** * Resets the peek position to equal the current read position. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ExtractorOutput.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ExtractorOutput.java similarity index 100% rename from library/core/src/main/java/com/google/android/exoplayer2/extractor/ExtractorOutput.java rename to library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ExtractorOutput.java diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ExtractorUtil.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ExtractorUtil.java new file mode 100644 index 0000000000..5ce274b11a --- /dev/null +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ExtractorUtil.java @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2019 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 java.io.IOException; + +/** Extractor related utility methods. */ +/* package */ final class ExtractorUtil { + + /** + * Peeks {@code length} bytes from the input peek position, or all the bytes to the end of the + * input if there was less than {@code length} bytes left. + * + *

      If an exception is thrown, there is no guarantee on the peek position. + * + * @param input The stream input to peek the data from. + * @param target A target array into which data should be written. + * @param offset The offset into the target array at which to write. + * @param length The maximum number of bytes to peek from the input. + * @return The number of bytes peeked. + * @throws IOException If an error occurs peeking from the input. + */ + public static int peekToLength(ExtractorInput input, byte[] target, int offset, int length) + throws IOException { + int totalBytesPeeked = 0; + while (totalBytesPeeked < length) { + int bytesPeeked = input.peek(target, offset + totalBytesPeeked, length - totalBytesPeeked); + if (bytesPeeked == C.RESULT_END_OF_INPUT) { + break; + } + totalBytesPeeked += bytesPeeked; + } + return totalBytesPeeked; + } + + private ExtractorUtil() {} +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ExtractorsFactory.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ExtractorsFactory.java similarity index 100% rename from library/core/src/main/java/com/google/android/exoplayer2/extractor/ExtractorsFactory.java rename to library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ExtractorsFactory.java diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/FlacFrameReader.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/FlacFrameReader.java new file mode 100644 index 0000000000..264c6d7b0d --- /dev/null +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/FlacFrameReader.java @@ -0,0 +1,333 @@ +/* + * Copyright (C) 2019 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.ParserException; +import com.google.android.exoplayer2.util.FlacConstants; +import com.google.android.exoplayer2.util.ParsableByteArray; +import com.google.android.exoplayer2.util.Util; +import java.io.IOException; + +/** + * Reads and peeks FLAC frame elements according to the FLAC format specification. + */ +public final class FlacFrameReader { + + /** Holds a sample number. */ + public static final class SampleNumberHolder { + /** The sample number. */ + public long sampleNumber; + } + + /** + * Checks whether the given FLAC frame header is valid and, if so, reads it and writes the frame + * first sample number in {@code sampleNumberHolder}. + * + *

      If the header is valid, the position of {@code data} is moved to the byte following it. + * Otherwise, there is no guarantee on the position. + * + * @param data The array to read the data from, whose position must correspond to the frame + * header. + * @param flacStreamMetadata The stream metadata. + * @param frameStartMarker The frame start marker of the stream. + * @param sampleNumberHolder The holder used to contain the sample number. + * @return Whether the frame header is valid. + */ + public static boolean checkAndReadFrameHeader( + ParsableByteArray data, + FlacStreamMetadata flacStreamMetadata, + int frameStartMarker, + SampleNumberHolder sampleNumberHolder) { + int frameStartPosition = data.getPosition(); + + long frameHeaderBytes = data.readUnsignedInt(); + if (frameHeaderBytes >>> 16 != frameStartMarker) { + return false; + } + + boolean isBlockSizeVariable = (frameHeaderBytes >>> 16 & 1) == 1; + int blockSizeKey = (int) (frameHeaderBytes >> 12 & 0xF); + int sampleRateKey = (int) (frameHeaderBytes >> 8 & 0xF); + int channelAssignmentKey = (int) (frameHeaderBytes >> 4 & 0xF); + int bitsPerSampleKey = (int) (frameHeaderBytes >> 1 & 0x7); + boolean reservedBit = (frameHeaderBytes & 1) == 1; + return checkChannelAssignment(channelAssignmentKey, flacStreamMetadata) + && checkBitsPerSample(bitsPerSampleKey, flacStreamMetadata) + && !reservedBit + && checkAndReadFirstSampleNumber( + data, flacStreamMetadata, isBlockSizeVariable, sampleNumberHolder) + && checkAndReadBlockSizeSamples(data, flacStreamMetadata, blockSizeKey) + && checkAndReadSampleRate(data, flacStreamMetadata, sampleRateKey) + && checkAndReadCrc(data, frameStartPosition); + } + + /** + * Checks whether the given FLAC frame header is valid and, if so, writes the frame first sample + * number in {@code sampleNumberHolder}. + * + *

      The {@code input} peek position is left unchanged. + * + * @param input The input to get the data from, whose peek position must correspond to the frame + * header. + * @param flacStreamMetadata The stream metadata. + * @param frameStartMarker The frame start marker of the stream. + * @param sampleNumberHolder The holder used to contain the sample number. + * @return Whether the frame header is valid. + */ + public static boolean checkFrameHeaderFromPeek( + ExtractorInput input, + FlacStreamMetadata flacStreamMetadata, + int frameStartMarker, + SampleNumberHolder sampleNumberHolder) + throws IOException { + long originalPeekPosition = input.getPeekPosition(); + + byte[] frameStartBytes = new byte[2]; + input.peekFully(frameStartBytes, 0, 2); + int frameStart = (frameStartBytes[0] & 0xFF) << 8 | (frameStartBytes[1] & 0xFF); + if (frameStart != frameStartMarker) { + input.resetPeekPosition(); + input.advancePeekPosition((int) (originalPeekPosition - input.getPosition())); + return false; + } + + ParsableByteArray scratch = new ParsableByteArray(FlacConstants.MAX_FRAME_HEADER_SIZE); + System.arraycopy( + frameStartBytes, /* srcPos= */ 0, scratch.data, /* destPos= */ 0, /* length= */ 2); + + int totalBytesPeeked = + ExtractorUtil.peekToLength(input, scratch.data, 2, FlacConstants.MAX_FRAME_HEADER_SIZE - 2); + scratch.setLimit(totalBytesPeeked); + + input.resetPeekPosition(); + input.advancePeekPosition((int) (originalPeekPosition - input.getPosition())); + + return checkAndReadFrameHeader( + scratch, flacStreamMetadata, frameStartMarker, sampleNumberHolder); + } + + /** + * Returns the number of the first sample in the given frame. + * + *

      The read position of {@code input} is left unchanged. + * + *

      If no exception is thrown, the peek position is aligned with the read position. Otherwise, + * there is no guarantee on the peek position. + * + * @param input Input stream to get the sample number from (starting from the read position). + * @return The frame first sample number. + * @throws ParserException If an error occurs parsing the sample number. + * @throws IOException If peeking from the input fails. + */ + public static long getFirstSampleNumber( + ExtractorInput input, FlacStreamMetadata flacStreamMetadata) throws IOException { + input.resetPeekPosition(); + input.advancePeekPosition(1); + byte[] blockingStrategyByte = new byte[1]; + input.peekFully(blockingStrategyByte, 0, 1); + boolean isBlockSizeVariable = (blockingStrategyByte[0] & 1) == 1; + input.advancePeekPosition(2); + + int maxUtf8SampleNumberSize = isBlockSizeVariable ? 7 : 6; + ParsableByteArray scratch = new ParsableByteArray(maxUtf8SampleNumberSize); + int totalBytesPeeked = + ExtractorUtil.peekToLength(input, scratch.data, 0, maxUtf8SampleNumberSize); + scratch.setLimit(totalBytesPeeked); + input.resetPeekPosition(); + + SampleNumberHolder sampleNumberHolder = new SampleNumberHolder(); + if (!checkAndReadFirstSampleNumber( + scratch, flacStreamMetadata, isBlockSizeVariable, sampleNumberHolder)) { + throw new ParserException(); + } + + return sampleNumberHolder.sampleNumber; + } + + /** + * Reads the given block size. + * + * @param data The array to read the data from, whose position must correspond to the block size + * bits. + * @param blockSizeKey The key in the block size lookup table. + * @return The block size in samples, or -1 if the {@code blockSizeKey} is invalid. + */ + public static int readFrameBlockSizeSamplesFromKey(ParsableByteArray data, int blockSizeKey) { + switch (blockSizeKey) { + case 1: + return 192; + case 2: + case 3: + case 4: + case 5: + return 576 << (blockSizeKey - 2); + case 6: + return data.readUnsignedByte() + 1; + case 7: + return data.readUnsignedShort() + 1; + case 8: + case 9: + case 10: + case 11: + case 12: + case 13: + case 14: + case 15: + return 256 << (blockSizeKey - 8); + default: + return -1; + } + } + + /** + * Checks whether the given channel assignment is valid. + * + * @param channelAssignmentKey The channel assignment lookup key. + * @param flacStreamMetadata The stream metadata. + * @return Whether the channel assignment is valid. + */ + private static boolean checkChannelAssignment( + int channelAssignmentKey, FlacStreamMetadata flacStreamMetadata) { + if (channelAssignmentKey <= 7) { + return channelAssignmentKey == flacStreamMetadata.channels - 1; + } else if (channelAssignmentKey <= 10) { + return flacStreamMetadata.channels == 2; + } else { + return false; + } + } + + /** + * Checks whether the given number of bits per sample is valid. + * + * @param bitsPerSampleKey The bits per sample lookup key. + * @param flacStreamMetadata The stream metadata. + * @return Whether the number of bits per sample is valid. + */ + private static boolean checkBitsPerSample( + int bitsPerSampleKey, FlacStreamMetadata flacStreamMetadata) { + if (bitsPerSampleKey == 0) { + return true; + } + return bitsPerSampleKey == flacStreamMetadata.bitsPerSampleLookupKey; + } + + /** + * Checks whether the given sample number is valid and, if so, reads it and writes it in {@code + * sampleNumberHolder}. + * + *

      If the sample number is valid, the position of {@code data} is moved to the byte following + * it. Otherwise, there is no guarantee on the position. + * + * @param data The array to read the data from, whose position must correspond to the sample + * number data. + * @param flacStreamMetadata The stream metadata. + * @param isBlockSizeVariable Whether the stream blocking strategy is variable block size or fixed + * block size. + * @param sampleNumberHolder The holder used to contain the sample number. + * @return Whether the sample number is valid. + */ + private static boolean checkAndReadFirstSampleNumber( + ParsableByteArray data, + FlacStreamMetadata flacStreamMetadata, + boolean isBlockSizeVariable, + SampleNumberHolder sampleNumberHolder) { + long utf8Value; + try { + utf8Value = data.readUtf8EncodedLong(); + } catch (NumberFormatException e) { + return false; + } + + sampleNumberHolder.sampleNumber = + isBlockSizeVariable ? utf8Value : utf8Value * flacStreamMetadata.maxBlockSizeSamples; + return true; + } + + /** + * Checks whether the given frame block size key and block size bits are valid and, if so, reads + * the block size bits. + * + *

      If the block size is valid, the position of {@code data} is moved to the byte following the + * block size bits. Otherwise, there is no guarantee on the position. + * + * @param data The array to read the data from, whose position must correspond to the block size + * bits. + * @param flacStreamMetadata The stream metadata. + * @param blockSizeKey The key in the block size lookup table. + * @return Whether the block size is valid. + */ + private static boolean checkAndReadBlockSizeSamples( + ParsableByteArray data, FlacStreamMetadata flacStreamMetadata, int blockSizeKey) { + int blockSizeSamples = readFrameBlockSizeSamplesFromKey(data, blockSizeKey); + return blockSizeSamples != -1 && blockSizeSamples <= flacStreamMetadata.maxBlockSizeSamples; + } + + /** + * Checks whether the given sample rate key and sample rate bits are valid and, if so, reads the + * sample rate bits. + * + *

      If the sample rate is valid, the position of {@code data} is moved to the byte following the + * sample rate bits. Otherwise, there is no guarantee on the position. + * + * @param data The array to read the data from, whose position must indicate the sample rate bits. + * @param flacStreamMetadata The stream metadata. + * @param sampleRateKey The key in the sample rate lookup table. + * @return Whether the sample rate is valid. + */ + private static boolean checkAndReadSampleRate( + ParsableByteArray data, FlacStreamMetadata flacStreamMetadata, int sampleRateKey) { + int expectedSampleRate = flacStreamMetadata.sampleRate; + if (sampleRateKey == 0) { + return true; + } else if (sampleRateKey <= 11) { + return sampleRateKey == flacStreamMetadata.sampleRateLookupKey; + } else if (sampleRateKey == 12) { + return data.readUnsignedByte() * 1000 == expectedSampleRate; + } else if (sampleRateKey <= 14) { + int sampleRate = data.readUnsignedShort(); + if (sampleRateKey == 14) { + sampleRate *= 10; + } + return sampleRate == expectedSampleRate; + } else { + return false; + } + } + + /** + * Checks whether the given CRC is valid and, if so, reads it. + * + *

      If the CRC is valid, the position of {@code data} is moved to the byte following it. + * Otherwise, there is no guarantee on the position. + * + *

      The {@code data} array must contain the whole frame header. + * + * @param data The array to read the data from, whose position must indicate the CRC. + * @param frameStartPosition The frame start offset in {@code data}. + * @return Whether the CRC is valid. + */ + private static boolean checkAndReadCrc(ParsableByteArray data, int frameStartPosition) { + int crc = data.readUnsignedByte(); + int frameEndPosition = data.getPosition(); + int expectedCrc = + Util.crc8(data.data, frameStartPosition, frameEndPosition - 1, /* initialValue= */ 0); + return crc == expectedCrc; + } + + private FlacFrameReader() {} +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/FlacMetadataReader.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/FlacMetadataReader.java similarity index 73% rename from library/core/src/main/java/com/google/android/exoplayer2/extractor/FlacMetadataReader.java rename to library/extractor/src/main/java/com/google/android/exoplayer2/extractor/FlacMetadataReader.java index e86c9b0129..65e65c401e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/FlacMetadataReader.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/FlacMetadataReader.java @@ -23,7 +23,6 @@ import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.flac.PictureFrame; import com.google.android.exoplayer2.metadata.id3.Id3Decoder; import com.google.android.exoplayer2.util.FlacConstants; -import com.google.android.exoplayer2.util.FlacStreamMetadata; import com.google.android.exoplayer2.util.ParsableBitArray; import com.google.android.exoplayer2.util.ParsableByteArray; import java.io.IOException; @@ -32,7 +31,10 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; -/** Reads and peeks FLAC stream metadata elements from an {@link ExtractorInput}. */ +/** + * Reads and peeks FLAC stream metadata elements according to the FLAC format specification. + */ public final class FlacMetadataReader { /** Holds a {@link FlacStreamMetadata}. */ @@ -45,24 +47,9 @@ public final class FlacMetadataReader { } } - /** Holds the metadata extracted from the first frame. */ - public static final class FirstFrameMetadata { - /** The frame start marker, which should correspond to the 2 first bytes of each frame. */ - public final int frameStartMarker; - /** The block size in samples. */ - public final int blockSizeSamples; - - public FirstFrameMetadata(int frameStartMarker, int blockSizeSamples) { - this.frameStartMarker = frameStartMarker; - this.blockSizeSamples = blockSizeSamples; - } - } - private static final int STREAM_MARKER = 0x664C6143; // ASCII for "fLaC" private static final int SYNC_CODE = 0x3FFE; - private static final int STREAM_INFO_TYPE = 0; - private static final int VORBIS_COMMENT_TYPE = 4; - private static final int PICTURE_TYPE = 6; + private static final int SEEK_POINT_SIZE = 18; /** * Peeks ID3 Data. @@ -73,15 +60,14 @@ public final class FlacMetadataReader { * is {@code false}. * @throws IOException If peeking from the input fails. In this case, there is no guarantee on the * peek position. - * @throws InterruptedException If interrupted while peeking from input. In this case, there is no - * guarantee on the peek position. */ @Nullable public static Metadata peekId3Metadata(ExtractorInput input, boolean parseData) - throws IOException, InterruptedException { + throws IOException { @Nullable Id3Decoder.FramePredicate id3FramePredicate = parseData ? null : Id3Decoder.NO_FRAMES_PREDICATE; - return new Id3Peeker().peekId3Data(input, id3FramePredicate); + @Nullable Metadata id3Metadata = new Id3Peeker().peekId3Data(input, id3FramePredicate); + return id3Metadata == null || id3Metadata.length() == 0 ? null : id3Metadata; } /** @@ -91,11 +77,8 @@ public final class FlacMetadataReader { * @return Whether the data peeked is the FLAC stream marker. * @throws IOException If peeking from the input fails. In this case, the peek position is left * unchanged. - * @throws InterruptedException If interrupted while peeking from input. In this case, the peek - * position is left unchanged. */ - public static boolean checkAndPeekStreamMarker(ExtractorInput input) - throws IOException, InterruptedException { + public static boolean checkAndPeekStreamMarker(ExtractorInput input) throws IOException { ParsableByteArray scratch = new ParsableByteArray(FlacConstants.STREAM_MARKER_SIZE); input.peekFully(scratch.data, 0, FlacConstants.STREAM_MARKER_SIZE); return scratch.readUnsignedInt() == STREAM_MARKER; @@ -113,12 +96,10 @@ public final class FlacMetadataReader { * is {@code false}. * @throws IOException If reading from the input fails. In this case, the read position is left * unchanged and there is no guarantee on the peek position. - * @throws InterruptedException If interrupted while reading from input. In this case, the read - * position is left unchanged and there is no guarantee on the peek position. */ @Nullable public static Metadata readId3Metadata(ExtractorInput input, boolean parseData) - throws IOException, InterruptedException { + throws IOException { input.resetPeekPosition(); long startingPeekPosition = input.getPeekPosition(); @Nullable Metadata id3Metadata = peekId3Metadata(input, parseData); @@ -135,11 +116,8 @@ public final class FlacMetadataReader { * position of {@code input} is advanced by {@link FlacConstants#STREAM_MARKER_SIZE} bytes. * @throws IOException If reading from the input fails. In this case, the position is left * unchanged. - * @throws InterruptedException If interrupted while reading from input. In this case, the - * position is left unchanged. */ - public static void readStreamMarker(ExtractorInput input) - throws IOException, InterruptedException { + public static void readStreamMarker(ExtractorInput input) throws IOException { ParsableByteArray scratch = new ParsableByteArray(FlacConstants.STREAM_MARKER_SIZE); input.readFully(scratch.data, 0, FlacConstants.STREAM_MARKER_SIZE); if (scratch.readUnsignedInt() != STREAM_MARKER) { @@ -165,13 +143,9 @@ public final class FlacMetadataReader { * start of a metadata block and there is no guarantee on the peek position. * @throws IOException If reading from the input fails. In this case, the read position will be at * the start of a metadata block and there is no guarantee on the peek position. - * @throws InterruptedException If interrupted while reading from input. In this case, the read - * position will be at the start of a metadata block and there is no guarantee on the peek - * position. */ public static boolean readMetadataBlock( - ExtractorInput input, FlacStreamMetadataHolder metadataHolder) - throws IOException, InterruptedException { + ExtractorInput input, FlacStreamMetadataHolder metadataHolder) throws IOException { input.resetPeekPosition(); ParsableBitArray scratch = new ParsableBitArray(new byte[4]); input.peekFully(scratch.data, 0, FlacConstants.METADATA_BLOCK_HEADER_SIZE); @@ -179,18 +153,21 @@ public final class FlacMetadataReader { boolean isLastMetadataBlock = scratch.readBit(); int type = scratch.readBits(7); int length = FlacConstants.METADATA_BLOCK_HEADER_SIZE + scratch.readBits(24); - if (type == STREAM_INFO_TYPE) { + if (type == FlacConstants.METADATA_TYPE_STREAM_INFO) { metadataHolder.flacStreamMetadata = readStreamInfoBlock(input); } else { - FlacStreamMetadata flacStreamMetadata = metadataHolder.flacStreamMetadata; + @Nullable FlacStreamMetadata flacStreamMetadata = metadataHolder.flacStreamMetadata; if (flacStreamMetadata == null) { throw new IllegalArgumentException(); } - if (type == VORBIS_COMMENT_TYPE) { + if (type == FlacConstants.METADATA_TYPE_SEEK_TABLE) { + FlacStreamMetadata.SeekTable seekTable = readSeekTableMetadataBlock(input, length); + metadataHolder.flacStreamMetadata = flacStreamMetadata.copyWithSeekTable(seekTable); + } else if (type == FlacConstants.METADATA_TYPE_VORBIS_COMMENT) { List vorbisComments = readVorbisCommentMetadataBlock(input, length); metadataHolder.flacStreamMetadata = flacStreamMetadata.copyWithVorbisComments(vorbisComments); - } else if (type == PICTURE_TYPE) { + } else if (type == FlacConstants.METADATA_TYPE_PICTURE) { PictureFrame pictureFrame = readPictureMetadataBlock(input, length); metadataHolder.flacStreamMetadata = flacStreamMetadata.copyWithPictureFrames(Collections.singletonList(pictureFrame)); @@ -203,23 +180,56 @@ public final class FlacMetadataReader { } /** - * Returns some metadata extracted from the first frame of a FLAC stream. + * Reads a FLAC seek table metadata block. + * + *

      The position of {@code data} is moved to the byte following the seek table metadata block + * (placeholder points included). + * + * @param data The array to read the data from, whose position must correspond to the seek table + * metadata block (header included). + * @return The seek table, without the placeholder points. + */ + public static FlacStreamMetadata.SeekTable readSeekTableMetadataBlock(ParsableByteArray data) { + data.skipBytes(1); + int length = data.readUnsignedInt24(); + + long seekTableEndPosition = data.getPosition() + length; + int seekPointCount = length / SEEK_POINT_SIZE; + long[] pointSampleNumbers = new long[seekPointCount]; + long[] pointOffsets = new long[seekPointCount]; + for (int i = 0; i < seekPointCount; i++) { + // The sample number is expected to fit in a signed long, except if it is a placeholder, in + // which case its value is -1. + long sampleNumber = data.readLong(); + if (sampleNumber == -1) { + pointSampleNumbers = Arrays.copyOf(pointSampleNumbers, i); + pointOffsets = Arrays.copyOf(pointOffsets, i); + break; + } + pointSampleNumbers[i] = sampleNumber; + pointOffsets[i] = data.readLong(); + data.skipBytes(2); + } + + data.skipBytes((int) (seekTableEndPosition - data.getPosition())); + return new FlacStreamMetadata.SeekTable(pointSampleNumbers, pointOffsets); + } + + /** + * Returns the frame start marker, consisting of the 2 first bytes of the first frame. * *

      The read position of {@code input} is left unchanged and the peek position is aligned with * the read position. * - * @param input Input stream to get the metadata from (starting from the read position). - * @return A {@link FirstFrameMetadata} containing the frame start marker (which should be the - * same for all the frames in the stream) and the block size of the frame. - * @throws ParserException If an error occurs parsing the frame metadata. + * @param input Input stream to get the start marker from (starting from the read position). + * @return The frame start marker (which must be the same for all the frames in the stream). + * @throws ParserException If an error occurs parsing the frame start marker. * @throws IOException If peeking from the input fails. - * @throws InterruptedException If interrupted while peeking from input. */ - public static FirstFrameMetadata getFirstFrameMetadata(ExtractorInput input) - throws IOException, InterruptedException { + public static int getFrameStartMarker(ExtractorInput input) throws IOException { input.resetPeekPosition(); - ParsableByteArray scratch = new ParsableByteArray(FlacConstants.MAX_FRAME_HEADER_SIZE); - input.peekFully(scratch.data, 0, FlacConstants.MAX_FRAME_HEADER_SIZE); + ParsableByteArray scratch = new ParsableByteArray(2); + input.peekFully(scratch.data, 0, 2); int frameStartMarker = scratch.readUnsignedShort(); int syncCode = frameStartMarker >> 2; @@ -228,23 +238,26 @@ public final class FlacMetadataReader { throw new ParserException("First frame does not start with sync code."); } - scratch.setPosition(0); - int firstFrameBlockSizeSamples = FlacFrameReader.getFrameBlockSizeSamples(scratch); - input.resetPeekPosition(); - return new FirstFrameMetadata(frameStartMarker, firstFrameBlockSizeSamples); + return frameStartMarker; } - private static FlacStreamMetadata readStreamInfoBlock(ExtractorInput input) - throws IOException, InterruptedException { + private static FlacStreamMetadata readStreamInfoBlock(ExtractorInput input) throws IOException { byte[] scratchData = new byte[FlacConstants.STREAM_INFO_BLOCK_SIZE]; input.readFully(scratchData, 0, FlacConstants.STREAM_INFO_BLOCK_SIZE); return new FlacStreamMetadata( scratchData, /* offset= */ FlacConstants.METADATA_BLOCK_HEADER_SIZE); } + private static FlacStreamMetadata.SeekTable readSeekTableMetadataBlock( + ExtractorInput input, int length) throws IOException { + ParsableByteArray scratch = new ParsableByteArray(length); + input.readFully(scratch.data, 0, length); + return readSeekTableMetadataBlock(scratch); + } + private static List readVorbisCommentMetadataBlock(ExtractorInput input, int length) - throws IOException, InterruptedException { + throws IOException { ParsableByteArray scratch = new ParsableByteArray(length); input.readFully(scratch.data, 0, length); scratch.skipBytes(FlacConstants.METADATA_BLOCK_HEADER_SIZE); @@ -255,7 +268,7 @@ public final class FlacMetadataReader { } private static PictureFrame readPictureMetadataBlock(ExtractorInput input, int length) - throws IOException, InterruptedException { + throws IOException { ParsableByteArray scratch = new ParsableByteArray(length); input.readFully(scratch.data, 0, length); scratch.skipBytes(FlacConstants.METADATA_BLOCK_HEADER_SIZE); diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/FlacSeekTableSeekMap.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/FlacSeekTableSeekMap.java new file mode 100644 index 0000000000..02ecc9e7b0 --- /dev/null +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/FlacSeekTableSeekMap.java @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2019 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.Assertions; +import com.google.android.exoplayer2.util.Util; + +/** + * A {@link SeekMap} implementation for FLAC streams that contain a seek table. + */ +public final class FlacSeekTableSeekMap implements SeekMap { + + private final FlacStreamMetadata flacStreamMetadata; + private final long firstFrameOffset; + + /** + * Creates a seek map from the FLAC stream seek table. + * + * @param flacStreamMetadata The stream metadata. + * @param firstFrameOffset The byte offset of the first frame in the stream. + */ + public FlacSeekTableSeekMap(FlacStreamMetadata flacStreamMetadata, long firstFrameOffset) { + this.flacStreamMetadata = flacStreamMetadata; + this.firstFrameOffset = firstFrameOffset; + } + + @Override + public boolean isSeekable() { + return true; + } + + @Override + public long getDurationUs() { + return flacStreamMetadata.getDurationUs(); + } + + @Override + public SeekPoints getSeekPoints(long timeUs) { + Assertions.checkStateNotNull(flacStreamMetadata.seekTable); + long[] pointSampleNumbers = flacStreamMetadata.seekTable.pointSampleNumbers; + long[] pointOffsets = flacStreamMetadata.seekTable.pointOffsets; + + long targetSampleNumber = flacStreamMetadata.getSampleNumber(timeUs); + int index = + Util.binarySearchFloor( + pointSampleNumbers, + targetSampleNumber, + /* inclusive= */ true, + /* stayInBounds= */ false); + + long seekPointSampleNumber = index == -1 ? 0 : pointSampleNumbers[index]; + long seekPointOffsetFromFirstFrame = index == -1 ? 0 : pointOffsets[index]; + SeekPoint seekPoint = getSeekPoint(seekPointSampleNumber, seekPointOffsetFromFirstFrame); + if (seekPoint.timeUs == timeUs || index == pointSampleNumbers.length - 1) { + return new SeekPoints(seekPoint); + } else { + SeekPoint secondSeekPoint = + getSeekPoint(pointSampleNumbers[index + 1], pointOffsets[index + 1]); + return new SeekPoints(seekPoint, secondSeekPoint); + } + } + + private SeekPoint getSeekPoint(long sampleNumber, long offsetFromFirstFrame) { + long seekTimeUs = sampleNumber * C.MICROS_PER_SECOND / flacStreamMetadata.sampleRate; + long seekPosition = firstFrameOffset + offsetFromFirstFrame; + return new SeekPoint(seekTimeUs, seekPosition); + } +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/FlacStreamMetadata.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/FlacStreamMetadata.java similarity index 77% rename from library/core/src/main/java/com/google/android/exoplayer2/util/FlacStreamMetadata.java rename to library/extractor/src/main/java/com/google/android/exoplayer2/extractor/FlacStreamMetadata.java index 2772f7e0c6..a4a2b64cb9 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/FlacStreamMetadata.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/FlacStreamMetadata.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.google.android.exoplayer2.util; +package com.google.android.exoplayer2.extractor; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; @@ -21,6 +21,10 @@ import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.flac.PictureFrame; import com.google.android.exoplayer2.metadata.flac.VorbisComment; +import com.google.android.exoplayer2.util.Log; +import com.google.android.exoplayer2.util.MimeTypes; +import com.google.android.exoplayer2.util.ParsableBitArray; +import com.google.android.exoplayer2.util.Util; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -30,6 +34,8 @@ import java.util.List; * * @see FLAC format * METADATA_BLOCK_STREAMINFO + * @see FLAC format + * METADATA_BLOCK_SEEKTABLE * @see FLAC format * METADATA_BLOCK_VORBIS_COMMENT * @see FLAC format @@ -37,6 +43,19 @@ import java.util.List; */ public final class FlacStreamMetadata { + /** A FLAC seek table. */ + public static class SeekTable { + /** Seek points sample numbers. */ + public final long[] pointSampleNumbers; + /** Seek points byte offsets from the first frame. */ + public final long[] pointOffsets; + + public SeekTable(long[] pointSampleNumbers, long[] pointOffsets) { + this.pointSampleNumbers = pointSampleNumbers; + this.pointOffsets = pointOffsets; + } + } + private static final String TAG = "FlacStreamMetadata"; /** Indicates that a value is not in the corresponding lookup table. */ @@ -79,15 +98,17 @@ public final class FlacStreamMetadata { public final int bitsPerSampleLookupKey; /** Total number of samples, or 0 if the value is unknown. */ public final long totalSamples; - - /** Content metadata. */ - private final Metadata metadata; + /** Seek table, or {@code null} if it is not provided. */ + @Nullable public final SeekTable seekTable; + /** Content metadata, or {@code null} if it is not provided. */ + @Nullable private final Metadata metadata; /** * Parses binary FLAC stream info metadata. * - * @param data An array containing binary FLAC stream info block (with or without header). - * @param offset The offset of the stream info block in {@code data} (header excluded). + * @param data An array containing binary FLAC stream info block. + * @param offset The offset of the stream info block in {@code data}, excluding the header (i.e. + * the offset points to the first byte of the minimum block size). */ public FlacStreamMetadata(byte[] data, int offset) { ParsableBitArray scratch = new ParsableBitArray(data); @@ -102,7 +123,8 @@ public final class FlacStreamMetadata { bitsPerSample = scratch.readBits(5) + 1; bitsPerSampleLookupKey = getBitsPerSampleLookupKey(bitsPerSample); totalSamples = scratch.readBitsToLong(36); - metadata = new Metadata(); + seekTable = null; + metadata = null; } // Used in native code. @@ -126,6 +148,7 @@ public final class FlacStreamMetadata { channels, bitsPerSample, totalSamples, + /* seekTable= */ null, buildMetadata(vorbisComments, pictureFrames)); } @@ -138,7 +161,8 @@ public final class FlacStreamMetadata { int channels, int bitsPerSample, long totalSamples, - Metadata metadata) { + @Nullable SeekTable seekTable, + @Nullable Metadata metadata) { this.minBlockSizeSamples = minBlockSizeSamples; this.maxBlockSizeSamples = maxBlockSizeSamples; this.minFrameSize = minFrameSize; @@ -149,6 +173,7 @@ public final class FlacStreamMetadata { this.bitsPerSample = bitsPerSample; this.bitsPerSampleLookupKey = getBitsPerSampleLookupKey(bitsPerSample); this.totalSamples = totalSamples; + this.seekTable = seekTable; this.metadata = metadata; } @@ -157,8 +182,8 @@ public final class FlacStreamMetadata { return maxBlockSizeSamples * channels * (bitsPerSample / 8); } - /** Returns the bit-rate of the FLAC stream. */ - public int getBitRate() { + /** Returns the bitrate of the stream after it's decoded into PCM. */ + public int getDecodedBitrate() { return bitsPerSample * sampleRate * channels; } @@ -171,14 +196,14 @@ public final class FlacStreamMetadata { } /** - * Returns the sample index for the sample at given position. + * Returns the sample number of the sample at a given time. * * @param timeUs Time position in microseconds in the FLAC stream. - * @return The sample index for the sample at given position. + * @return The sample number corresponding to the time position. */ - public long getSampleIndex(long timeUs) { - long sampleIndex = (timeUs * sampleRate) / C.MICROS_PER_SECOND; - return Util.constrainValue(sampleIndex, /* min= */ 0, totalSamples - 1); + public long getSampleNumber(long timeUs) { + long sampleNumber = (timeUs * sampleRate) / C.MICROS_PER_SECOND; + return Util.constrainValue(sampleNumber, /* min= */ 0, totalSamples - 1); } /** Returns the approximate number of bytes per frame for the current FLAC stream. */ @@ -213,35 +238,43 @@ public final class FlacStreamMetadata { // Set the last metadata block flag, ignore the other blocks. streamMarkerAndInfoBlock[4] = (byte) 0x80; int maxInputSize = maxFrameSize > 0 ? maxFrameSize : Format.NO_VALUE; - Metadata metadataWithId3 = metadata.copyWithAppendedEntriesFrom(id3Metadata); - - return Format.createAudioSampleFormat( - /* id= */ null, - MimeTypes.AUDIO_FLAC, - /* codecs= */ null, - getBitRate(), - maxInputSize, - channels, - sampleRate, - /* pcmEncoding= */ Format.NO_VALUE, - /* encoderDelay= */ 0, - /* encoderPadding= */ 0, - /* initializationData= */ Collections.singletonList(streamMarkerAndInfoBlock), - /* drmInitData= */ null, - /* selectionFlags= */ 0, - /* language= */ null, - metadataWithId3); + @Nullable Metadata metadataWithId3 = getMetadataCopyWithAppendedEntriesFrom(id3Metadata); + return new Format.Builder() + .setSampleMimeType(MimeTypes.AUDIO_FLAC) + .setMaxInputSize(maxInputSize) + .setChannelCount(channels) + .setSampleRate(sampleRate) + .setInitializationData(Collections.singletonList(streamMarkerAndInfoBlock)) + .setMetadata(metadataWithId3) + .build(); } /** Returns a copy of the content metadata with entries from {@code other} appended. */ + @Nullable public Metadata getMetadataCopyWithAppendedEntriesFrom(@Nullable Metadata other) { - return metadata.copyWithAppendedEntriesFrom(other); + return metadata == null ? other : metadata.copyWithAppendedEntriesFrom(other); + } + + /** Returns a copy of {@code this} with the seek table replaced by the one given. */ + public FlacStreamMetadata copyWithSeekTable(@Nullable SeekTable seekTable) { + return new FlacStreamMetadata( + minBlockSizeSamples, + maxBlockSizeSamples, + minFrameSize, + maxFrameSize, + sampleRate, + channels, + bitsPerSample, + totalSamples, + seekTable, + metadata); } /** Returns a copy of {@code this} with the given Vorbis comments added to the metadata. */ public FlacStreamMetadata copyWithVorbisComments(List vorbisComments) { + @Nullable Metadata appendedMetadata = - metadata.copyWithAppendedEntriesFrom( + getMetadataCopyWithAppendedEntriesFrom( buildMetadata(vorbisComments, Collections.emptyList())); return new FlacStreamMetadata( minBlockSizeSamples, @@ -252,13 +285,16 @@ public final class FlacStreamMetadata { channels, bitsPerSample, totalSamples, + seekTable, appendedMetadata); } /** Returns a copy of {@code this} with the given picture frames added to the metadata. */ public FlacStreamMetadata copyWithPictureFrames(List pictureFrames) { + @Nullable Metadata appendedMetadata = - metadata.copyWithAppendedEntriesFrom(buildMetadata(Collections.emptyList(), pictureFrames)); + getMetadataCopyWithAppendedEntriesFrom( + buildMetadata(Collections.emptyList(), pictureFrames)); return new FlacStreamMetadata( minBlockSizeSamples, maxBlockSizeSamples, @@ -268,6 +304,7 @@ public final class FlacStreamMetadata { channels, bitsPerSample, totalSamples, + seekTable, appendedMetadata); } @@ -317,10 +354,11 @@ public final class FlacStreamMetadata { } } + @Nullable private static Metadata buildMetadata( List vorbisComments, List pictureFrames) { if (vorbisComments.isEmpty() && pictureFrames.isEmpty()) { - return new Metadata(); + return null; } ArrayList metadataEntries = new ArrayList<>(); @@ -336,6 +374,6 @@ public final class FlacStreamMetadata { } metadataEntries.addAll(pictureFrames); - return metadataEntries.isEmpty() ? new Metadata() : new Metadata(metadataEntries); + return metadataEntries.isEmpty() ? null : new Metadata(metadataEntries); } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/GaplessInfoHolder.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/GaplessInfoHolder.java similarity index 94% rename from library/core/src/main/java/com/google/android/exoplayer2/extractor/GaplessInfoHolder.java rename to library/extractor/src/main/java/com/google/android/exoplayer2/extractor/GaplessInfoHolder.java index a0effc0df8..1ba316c64b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/GaplessInfoHolder.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/GaplessInfoHolder.java @@ -15,6 +15,8 @@ */ package com.google.android.exoplayer2.extractor; +import static com.google.android.exoplayer2.util.Util.castNonNull; + import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.id3.CommentFrame; @@ -107,8 +109,8 @@ public final class GaplessInfoHolder { Matcher matcher = GAPLESS_COMMENT_PATTERN.matcher(data); if (matcher.find()) { try { - int encoderDelay = Integer.parseInt(matcher.group(1), 16); - int encoderPadding = Integer.parseInt(matcher.group(2), 16); + int encoderDelay = Integer.parseInt(castNonNull(matcher.group(1)), 16); + int encoderPadding = Integer.parseInt(castNonNull(matcher.group(2)), 16); if (encoderDelay > 0 || encoderPadding > 0) { this.encoderDelay = encoderDelay; this.encoderPadding = encoderPadding; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/Id3Peeker.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/Id3Peeker.java similarity index 95% rename from library/core/src/main/java/com/google/android/exoplayer2/extractor/Id3Peeker.java rename to library/extractor/src/main/java/com/google/android/exoplayer2/extractor/Id3Peeker.java index 60386dcc3c..cda6a805f5 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/Id3Peeker.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/Id3Peeker.java @@ -43,14 +43,13 @@ public final class Id3Peeker { * @return The first ID3 tag decoded into a {@link Metadata} object. May be null if ID3 tag is not * present in the input. * @throws IOException If an error occurred peeking from the input. - * @throws InterruptedException If the thread was interrupted. */ @Nullable public Metadata peekId3Data( ExtractorInput input, @Nullable Id3Decoder.FramePredicate id3FramePredicate) - throws IOException, InterruptedException { + throws IOException { int peekedId3Bytes = 0; - Metadata metadata = null; + @Nullable Metadata metadata = null; while (true) { try { input.peekFully(scratch.data, /* offset= */ 0, Id3Decoder.ID3_HEADER_LENGTH); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/PositionHolder.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/PositionHolder.java similarity index 100% rename from library/core/src/main/java/com/google/android/exoplayer2/extractor/PositionHolder.java rename to library/extractor/src/main/java/com/google/android/exoplayer2/extractor/PositionHolder.java diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/SeekMap.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/SeekMap.java similarity index 100% rename from library/core/src/main/java/com/google/android/exoplayer2/extractor/SeekMap.java rename to library/extractor/src/main/java/com/google/android/exoplayer2/extractor/SeekMap.java diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/SeekPoint.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/SeekPoint.java similarity index 100% rename from library/core/src/main/java/com/google/android/exoplayer2/extractor/SeekPoint.java rename to library/extractor/src/main/java/com/google/android/exoplayer2/extractor/SeekPoint.java diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/TrackOutput.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/TrackOutput.java similarity index 60% rename from library/core/src/main/java/com/google/android/exoplayer2/extractor/TrackOutput.java rename to library/extractor/src/main/java/com/google/android/exoplayer2/extractor/TrackOutput.java index 0d5a168197..a203d164dd 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/TrackOutput.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/TrackOutput.java @@ -15,12 +15,17 @@ */ package com.google.android.exoplayer2.extractor; +import androidx.annotation.IntDef; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.upstream.DataReader; import com.google.android.exoplayer2.util.ParsableByteArray; import java.io.EOFException; import java.io.IOException; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.util.Arrays; /** @@ -93,6 +98,41 @@ public interface TrackOutput { } + /** Defines the part of the sample data to which a call to {@link #sampleData} corresponds. */ + @Documented + @Retention(RetentionPolicy.SOURCE) + @IntDef({SAMPLE_DATA_PART_MAIN, SAMPLE_DATA_PART_ENCRYPTION, SAMPLE_DATA_PART_SUPPLEMENTAL}) + @interface SampleDataPart {} + + /** Main media sample data. */ + int SAMPLE_DATA_PART_MAIN = 0; + /** + * Sample encryption data. + * + *

      The format for encryption information is: + * + *

      + */ + int SAMPLE_DATA_PART_ENCRYPTION = 1; + /** Sample supplemental data. */ + int SAMPLE_DATA_PART_SUPPLEMENTAL = 2; + /** * Called when the {@link Format} of the track has been extracted from the stream. * @@ -100,42 +140,59 @@ public interface TrackOutput { */ void format(Format format); + /** + * Equivalent to {@link #sampleData(DataReader, int, boolean, int) sampleData(input, length, + * allowEndOfInput, SAMPLE_DATA_PART_MAIN)}. + */ + default int sampleData(DataReader input, int length, boolean allowEndOfInput) throws IOException { + return sampleData(input, length, allowEndOfInput, SAMPLE_DATA_PART_MAIN); + } + + /** + * Equivalent to {@link #sampleData(ParsableByteArray, int, int)} sampleData(data, length, + * SAMPLE_DATA_PART_MAIN)}. + */ + default void sampleData(ParsableByteArray data, int length) { + sampleData(data, length, SAMPLE_DATA_PART_MAIN); + } + /** * Called to write sample data to the output. * - * @param input An {@link ExtractorInput} from which to read the sample data. + * @param input A {@link DataReader} from which to read the sample data. * @param length The maximum length to read from the input. * @param allowEndOfInput True if encountering the end of the input having read no data is * allowed, and should result in {@link C#RESULT_END_OF_INPUT} being returned. False if it * should be considered an error, causing an {@link EOFException} to be thrown. + * @param sampleDataPart The part of the sample data to which this call corresponds. * @return The number of bytes appended. * @throws IOException If an error occurred reading from the input. - * @throws InterruptedException If the thread was interrupted. */ - int sampleData(ExtractorInput input, int length, boolean allowEndOfInput) - throws IOException, InterruptedException; + int sampleData( + DataReader input, int length, boolean allowEndOfInput, @SampleDataPart int sampleDataPart) + throws IOException; /** * Called to write sample data to the output. * * @param data A {@link ParsableByteArray} from which to read the sample data. * @param length The number of bytes to read, starting from {@code data.getPosition()}. + * @param sampleDataPart The part of the sample data to which this call corresponds. */ - void sampleData(ParsableByteArray data, int length); + void sampleData(ParsableByteArray data, int length, @SampleDataPart int sampleDataPart); /** * 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)}. + * {@link #sampleData(DataReader, 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(DataReader, 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( diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/VorbisBitArray.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/VorbisBitArray.java similarity index 100% rename from library/core/src/main/java/com/google/android/exoplayer2/extractor/VorbisBitArray.java rename to library/extractor/src/main/java/com/google/android/exoplayer2/extractor/VorbisBitArray.java diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/VorbisUtil.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/VorbisUtil.java similarity index 88% rename from library/core/src/main/java/com/google/android/exoplayer2/extractor/VorbisUtil.java rename to library/extractor/src/main/java/com/google/android/exoplayer2/extractor/VorbisUtil.java index 5066c3a7bd..67d469b759 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/VorbisUtil.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/VorbisUtil.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.extractor; +import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.ParsableByteArray; @@ -37,27 +38,54 @@ public final class VorbisUtil { } } - /** Vorbis identification header. */ + /** + * Vorbis identification header. + * + * @see Vorbis + * spec/Identification header + */ public static final class VorbisIdHeader { - public final long version; + /** The {@code vorbis_version} field. */ + public final int version; + /** The {@code audio_channels} field. */ public final int channels; - public final long sampleRate; - public final int bitrateMax; + /** The {@code audio_sample_rate} field. */ + public final int sampleRate; + /** The {@code bitrate_maximum} field, or {@link Format#NO_VALUE} if not greater than zero. */ + public final int bitrateMaximum; + /** The {@code bitrate_nominal} field, or {@link Format#NO_VALUE} if not greater than zero. */ public final int bitrateNominal; - public final int bitrateMin; + /** The {@code bitrate_minimum} field, or {@link Format#NO_VALUE} if not greater than zero. */ + public final int bitrateMinimum; + /** The {@code blocksize_0} field. */ public final int blockSize0; + /** The {@code blocksize_1} field. */ public final int blockSize1; + /** The {@code framing_flag} field. */ public final boolean framingFlag; + /** The raw header data. */ public final byte[] data; + /** + * @param version See {@link #version}. + * @param channels See {@link #channels}. + * @param sampleRate See {@link #sampleRate}. + * @param bitrateMaximum See {@link #bitrateMaximum}. + * @param bitrateNominal See {@link #bitrateNominal}. + * @param bitrateMinimum See {@link #bitrateMinimum}. + * @param blockSize0 See {@link #version}. + * @param blockSize1 See {@link #blockSize1}. + * @param framingFlag See {@link #framingFlag}. + * @param data See {@link #data}. + */ public VorbisIdHeader( - long version, + int version, int channels, - long sampleRate, - int bitrateMax, + int sampleRate, + int bitrateMaximum, int bitrateNominal, - int bitrateMin, + int bitrateMinimum, int blockSize0, int blockSize1, boolean framingFlag, @@ -65,18 +93,14 @@ public final class VorbisUtil { this.version = version; this.channels = channels; this.sampleRate = sampleRate; - this.bitrateMax = bitrateMax; + this.bitrateMaximum = bitrateMaximum; this.bitrateNominal = bitrateNominal; - this.bitrateMin = bitrateMin; + this.bitrateMinimum = bitrateMinimum; this.blockSize0 = blockSize0; this.blockSize1 = blockSize1; this.framingFlag = framingFlag; this.data = data; } - - public int getApproximateBitrate() { - return bitrateNominal == 0 ? (bitrateMin + bitrateMax) / 2 : bitrateNominal; - } } /** Vorbis setup header modes. */ @@ -128,13 +152,21 @@ public final class VorbisUtil { verifyVorbisHeaderCapturePattern(0x01, headerData, false); - long version = headerData.readLittleEndianUnsignedInt(); + int version = headerData.readLittleEndianUnsignedIntToInt(); int channels = headerData.readUnsignedByte(); - long sampleRate = headerData.readLittleEndianUnsignedInt(); - int bitrateMax = headerData.readLittleEndianInt(); + int sampleRate = headerData.readLittleEndianUnsignedIntToInt(); + int bitrateMaximum = headerData.readLittleEndianInt(); + if (bitrateMaximum <= 0) { + bitrateMaximum = Format.NO_VALUE; + } int bitrateNominal = headerData.readLittleEndianInt(); - int bitrateMin = headerData.readLittleEndianInt(); - + if (bitrateNominal <= 0) { + bitrateNominal = Format.NO_VALUE; + } + int bitrateMinimum = headerData.readLittleEndianInt(); + if (bitrateMinimum <= 0) { + bitrateMinimum = Format.NO_VALUE; + } int blockSize = headerData.readUnsignedByte(); int blockSize0 = (int) Math.pow(2, blockSize & 0x0F); int blockSize1 = (int) Math.pow(2, (blockSize & 0xF0) >> 4); @@ -143,8 +175,17 @@ public final class VorbisUtil { // raw data of Vorbis setup header has to be passed to decoder as CSD buffer #1 byte[] data = Arrays.copyOf(headerData.data, headerData.limit()); - return new VorbisIdHeader(version, channels, sampleRate, bitrateMax, bitrateNominal, bitrateMin, - blockSize0, blockSize1, framingFlag, data); + return new VorbisIdHeader( + version, + channels, + sampleRate, + bitrateMaximum, + bitrateNominal, + bitrateMinimum, + blockSize0, + blockSize1, + framingFlag, + data); } /** diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/amr/AmrExtractor.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/amr/AmrExtractor.java similarity index 90% rename from library/core/src/main/java/com/google/android/exoplayer2/extractor/amr/AmrExtractor.java rename to library/extractor/src/main/java/com/google/android/exoplayer2/extractor/amr/AmrExtractor.java index f6b64245fc..4d8ef18448 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/amr/AmrExtractor.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/amr/AmrExtractor.java @@ -16,7 +16,6 @@ package com.google.android.exoplayer2.extractor.amr; import androidx.annotation.IntDef; -import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.ParserException; @@ -28,6 +27,7 @@ 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.TrackOutput; +import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.Util; import java.io.EOFException; @@ -36,6 +36,9 @@ import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.Arrays; +import org.checkerframework.checker.nullness.qual.EnsuresNonNull; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.checkerframework.checker.nullness.qual.RequiresNonNull; /** * Extracts data from the AMR containers format (either AMR or AMR-WB). This follows RFC-4867, @@ -138,9 +141,9 @@ public final class AmrExtractor implements Extractor { private int numSamplesWithSameSize; private long timeOffsetUs; - private ExtractorOutput extractorOutput; - private TrackOutput trackOutput; - @Nullable private SeekMap seekMap; + private @MonotonicNonNull ExtractorOutput extractorOutput; + private @MonotonicNonNull TrackOutput trackOutput; + private @MonotonicNonNull SeekMap seekMap; private boolean hasOutputFormat; public AmrExtractor() { @@ -157,7 +160,7 @@ public final class AmrExtractor implements Extractor { // Extractor implementation. @Override - public boolean sniff(ExtractorInput input) throws IOException, InterruptedException { + public boolean sniff(ExtractorInput input) throws IOException { return readAmrHeader(input); } @@ -169,8 +172,8 @@ public final class AmrExtractor implements Extractor { } @Override - public int read(ExtractorInput input, PositionHolder seekPosition) - throws IOException, InterruptedException { + public int read(ExtractorInput input, PositionHolder seekPosition) throws IOException { + assertInitialized(); if (input.getPosition() == 0) { if (!readAmrHeader(input)) { throw new ParserException("Could not find AMR header."); @@ -223,7 +226,7 @@ public final class AmrExtractor implements Extractor { * @param input The {@link ExtractorInput} from which data should be peeked/read. * @return Whether the AMR header has been read. */ - private boolean readAmrHeader(ExtractorInput input) throws IOException, InterruptedException { + private boolean readAmrHeader(ExtractorInput input) throws IOException { if (peekAmrSignature(input, amrSignatureNb)) { isWideBand = false; input.skipFully(amrSignatureNb.length); @@ -237,37 +240,32 @@ public final class AmrExtractor implements Extractor { } /** Peeks from the beginning of the input to see if the given AMR signature exists. */ - private boolean peekAmrSignature(ExtractorInput input, byte[] amrSignature) - throws IOException, InterruptedException { + private static boolean peekAmrSignature(ExtractorInput input, byte[] amrSignature) + throws IOException { input.resetPeekPosition(); byte[] header = new byte[amrSignature.length]; input.peekFully(header, 0, amrSignature.length); return Arrays.equals(header, amrSignature); } + @RequiresNonNull("trackOutput") private void maybeOutputFormat() { if (!hasOutputFormat) { hasOutputFormat = true; String mimeType = isWideBand ? MimeTypes.AUDIO_AMR_WB : MimeTypes.AUDIO_AMR_NB; int sampleRate = isWideBand ? SAMPLE_RATE_WB : SAMPLE_RATE_NB; trackOutput.format( - Format.createAudioSampleFormat( - /* id= */ null, - mimeType, - /* codecs= */ null, - /* bitrate= */ Format.NO_VALUE, - MAX_FRAME_SIZE_BYTES, - /* channelCount= */ 1, - sampleRate, - /* pcmEncoding= */ Format.NO_VALUE, - /* initializationData= */ null, - /* drmInitData= */ null, - /* selectionFlags= */ 0, - /* language= */ null)); + new Format.Builder() + .setSampleMimeType(mimeType) + .setMaxInputSize(MAX_FRAME_SIZE_BYTES) + .setChannelCount(1) + .setSampleRate(sampleRate) + .build()); } } - private int readSample(ExtractorInput extractorInput) throws IOException, InterruptedException { + @RequiresNonNull("trackOutput") + private int readSample(ExtractorInput extractorInput) throws IOException { if (currentSampleBytesRemaining == 0) { try { currentSampleSize = peekNextSampleSize(extractorInput); @@ -305,8 +303,7 @@ public final class AmrExtractor implements Extractor { return RESULT_CONTINUE; } - private int peekNextSampleSize(ExtractorInput extractorInput) - throws IOException, InterruptedException { + private int peekNextSampleSize(ExtractorInput extractorInput) throws IOException { extractorInput.resetPeekPosition(); extractorInput.peekFully(scratch, /* offset= */ 0, /* length= */ 1); @@ -346,6 +343,7 @@ public final class AmrExtractor implements Extractor { return !isWideBand && (frameType < 12 || frameType > 14); } + @RequiresNonNull("extractorOutput") private void maybeOutputSeekMap(long inputLength, int sampleReadResult) { if (hasOutputSeekMap) { return; @@ -370,6 +368,12 @@ public final class AmrExtractor implements Extractor { return new ConstantBitrateSeekMap(inputLength, firstSamplePosition, bitrate, firstSampleSize); } + @EnsuresNonNull({"extractorOutput", "trackOutput"}) + private void assertInitialized() { + Assertions.checkStateNotNull(trackOutput); + Util.castNonNull(extractorOutput); + } + /** * Returns the stream bitrate, given a frame size and the duration of that frame in microseconds. * diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/amr/package-info.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/amr/package-info.java new file mode 100644 index 0000000000..31d58fadc9 --- /dev/null +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/amr/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2019 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. + */ +@NonNullApi +package com.google.android.exoplayer2.extractor.amr; + +import com.google.android.exoplayer2.util.NonNullApi; diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/flac/FlacBinarySearchSeeker.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/flac/FlacBinarySearchSeeker.java new file mode 100644 index 0000000000..03fd1e792a --- /dev/null +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/flac/FlacBinarySearchSeeker.java @@ -0,0 +1,129 @@ +/* + * Copyright (C) 2019 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.flac; + +import com.google.android.exoplayer2.extractor.BinarySearchSeeker; +import com.google.android.exoplayer2.extractor.ExtractorInput; +import com.google.android.exoplayer2.extractor.FlacFrameReader; +import com.google.android.exoplayer2.extractor.FlacFrameReader.SampleNumberHolder; +import com.google.android.exoplayer2.extractor.FlacStreamMetadata; +import com.google.android.exoplayer2.extractor.SeekMap; +import com.google.android.exoplayer2.util.FlacConstants; +import java.io.IOException; + +/** + * A {@link SeekMap} implementation for FLAC stream using binary search. + * + *

      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 extends BinarySearchSeeker { + + /** + * Creates a {@link FlacBinarySearchSeeker}. + * + * @param flacStreamMetadata The stream metadata. + * @param frameStartMarker The frame start marker, consisting of the 2 bytes by which every frame + * in the stream must start. + * @param firstFramePosition The byte offset of the first frame in the stream. + * @param inputLength The length of the stream in bytes. + */ + public FlacBinarySearchSeeker( + FlacStreamMetadata flacStreamMetadata, + int frameStartMarker, + long firstFramePosition, + long inputLength) { + super( + /* seekTimestampConverter= */ flacStreamMetadata::getSampleNumber, + new FlacTimestampSeeker(flacStreamMetadata, frameStartMarker), + flacStreamMetadata.getDurationUs(), + /* floorTimePosition= */ 0, + /* ceilingTimePosition= */ flacStreamMetadata.totalSamples, + /* floorBytePosition= */ firstFramePosition, + /* ceilingBytePosition= */ inputLength, + /* approxBytesPerFrame= */ flacStreamMetadata.getApproxBytesPerFrame(), + /* minimumSearchRange= */ Math.max( + FlacConstants.MIN_FRAME_HEADER_SIZE, flacStreamMetadata.minFrameSize)); + } + + private static final class FlacTimestampSeeker implements TimestampSeeker { + + private final FlacStreamMetadata flacStreamMetadata; + private final int frameStartMarker; + private final SampleNumberHolder sampleNumberHolder; + + private FlacTimestampSeeker(FlacStreamMetadata flacStreamMetadata, int frameStartMarker) { + this.flacStreamMetadata = flacStreamMetadata; + this.frameStartMarker = frameStartMarker; + sampleNumberHolder = new SampleNumberHolder(); + } + + @Override + public TimestampSearchResult searchForTimestamp(ExtractorInput input, long targetSampleNumber) + throws IOException { + long searchPosition = input.getPosition(); + + // Find left frame. + long leftFrameFirstSampleNumber = findNextFrame(input); + long leftFramePosition = input.getPeekPosition(); + + input.advancePeekPosition( + Math.max(FlacConstants.MIN_FRAME_HEADER_SIZE, flacStreamMetadata.minFrameSize)); + + // Find right frame. + long rightFrameFirstSampleNumber = findNextFrame(input); + long rightFramePosition = input.getPeekPosition(); + + if (leftFrameFirstSampleNumber <= targetSampleNumber + && rightFrameFirstSampleNumber > targetSampleNumber) { + return TimestampSearchResult.targetFoundResult(leftFramePosition); + } else if (rightFrameFirstSampleNumber <= targetSampleNumber) { + return TimestampSearchResult.underestimatedResult( + rightFrameFirstSampleNumber, rightFramePosition); + } else { + return TimestampSearchResult.overestimatedResult( + leftFrameFirstSampleNumber, searchPosition); + } + } + + /** + * Searches for the next frame in {@code input}. + * + *

      The peek position is advanced to the start of the found frame, or at the end of the stream + * if no frame was found. + * + * @param input The input from which to search (starting from the peek position). + * @return The number of the first sample in the found frame, or the total number of samples in + * the stream if no frame was found. + * @throws IOException If peeking from the input fails. In this case, there is no guarantee on + * the peek position. + */ + private long findNextFrame(ExtractorInput input) throws IOException { + while (input.getPeekPosition() < input.getLength() - FlacConstants.MIN_FRAME_HEADER_SIZE + && !FlacFrameReader.checkFrameHeaderFromPeek( + input, flacStreamMetadata, frameStartMarker, sampleNumberHolder)) { + input.advancePeekPosition(1); + } + + if (input.getPeekPosition() >= input.getLength() - FlacConstants.MIN_FRAME_HEADER_SIZE) { + input.advancePeekPosition((int) (input.getLength() - input.getPeekPosition())); + return flacStreamMetadata.totalSamples; + } + + return sampleNumberHolder.sampleNumber; + } + } +} diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/flac/FlacExtractor.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/flac/FlacExtractor.java new file mode 100644 index 0000000000..f0da2656a1 --- /dev/null +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/flac/FlacExtractor.java @@ -0,0 +1,410 @@ +/* + * Copyright (C) 2019 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.flac; + +import static com.google.android.exoplayer2.util.Util.castNonNull; + +import androidx.annotation.IntDef; +import androidx.annotation.Nullable; +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.ExtractorOutput; +import com.google.android.exoplayer2.extractor.ExtractorsFactory; +import com.google.android.exoplayer2.extractor.FlacFrameReader; +import com.google.android.exoplayer2.extractor.FlacFrameReader.SampleNumberHolder; +import com.google.android.exoplayer2.extractor.FlacMetadataReader; +import com.google.android.exoplayer2.extractor.FlacSeekTableSeekMap; +import com.google.android.exoplayer2.extractor.FlacStreamMetadata; +import com.google.android.exoplayer2.extractor.PositionHolder; +import com.google.android.exoplayer2.extractor.SeekMap; +import com.google.android.exoplayer2.extractor.TrackOutput; +import com.google.android.exoplayer2.metadata.Metadata; +import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.FlacConstants; +import com.google.android.exoplayer2.util.ParsableByteArray; +import java.io.IOException; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; + +/** + * Extracts data from FLAC container format. + * + *

      The format specification can be found at https://xiph.org/flac/format.html. + */ +public final class FlacExtractor implements Extractor { + + /** Factory for {@link FlacExtractor} instances. */ + public static final ExtractorsFactory FACTORY = () -> new Extractor[] {new FlacExtractor()}; + + /** + * Flags controlling the behavior of the extractor. Possible flag value is {@link + * #FLAG_DISABLE_ID3_METADATA}. + */ + @Documented + @Retention(RetentionPolicy.SOURCE) + @IntDef( + flag = true, + value = {FLAG_DISABLE_ID3_METADATA}) + public @interface Flags {} + + /** + * Flag to disable parsing of ID3 metadata. Can be set to save memory if ID3 metadata is not + * required. + */ + public static final int FLAG_DISABLE_ID3_METADATA = 1; + + /** Parser state. */ + @Documented + @Retention(RetentionPolicy.SOURCE) + @IntDef({ + STATE_READ_ID3_METADATA, + STATE_GET_STREAM_MARKER_AND_INFO_BLOCK_BYTES, + STATE_READ_STREAM_MARKER, + STATE_READ_METADATA_BLOCKS, + STATE_GET_FRAME_START_MARKER, + STATE_READ_FRAMES + }) + private @interface State {} + + private static final int STATE_READ_ID3_METADATA = 0; + private static final int STATE_GET_STREAM_MARKER_AND_INFO_BLOCK_BYTES = 1; + private static final int STATE_READ_STREAM_MARKER = 2; + private static final int STATE_READ_METADATA_BLOCKS = 3; + private static final int STATE_GET_FRAME_START_MARKER = 4; + private static final int STATE_READ_FRAMES = 5; + + /** Arbitrary buffer length of 32KB, which is ~170ms of 16-bit stereo PCM audio at 48KHz. */ + private static final int BUFFER_LENGTH = 32 * 1024; + + /** Value of an unknown sample number. */ + private static final int SAMPLE_NUMBER_UNKNOWN = -1; + + private final byte[] streamMarkerAndInfoBlock; + private final ParsableByteArray buffer; + private final boolean id3MetadataDisabled; + + private final SampleNumberHolder sampleNumberHolder; + + private @MonotonicNonNull ExtractorOutput extractorOutput; + private @MonotonicNonNull TrackOutput trackOutput; + + private @State int state; + @Nullable private Metadata id3Metadata; + private @MonotonicNonNull FlacStreamMetadata flacStreamMetadata; + private int minFrameSize; + private int frameStartMarker; + private @MonotonicNonNull FlacBinarySearchSeeker binarySearchSeeker; + private int currentFrameBytesWritten; + private long currentFrameFirstSampleNumber; + + /** Constructs an instance with {@code flags = 0}. */ + public FlacExtractor() { + this(/* flags= */ 0); + } + + /** + * Constructs an instance. + * + * @param flags Flags that control the extractor's behavior. Possible flags are described by + * {@link Flags}. + */ + public FlacExtractor(int flags) { + streamMarkerAndInfoBlock = + new byte[FlacConstants.STREAM_MARKER_SIZE + FlacConstants.STREAM_INFO_BLOCK_SIZE]; + buffer = new ParsableByteArray(new byte[BUFFER_LENGTH], /* limit= */ 0); + id3MetadataDisabled = (flags & FLAG_DISABLE_ID3_METADATA) != 0; + sampleNumberHolder = new SampleNumberHolder(); + state = STATE_READ_ID3_METADATA; + } + + @Override + public boolean sniff(ExtractorInput input) throws IOException { + FlacMetadataReader.peekId3Metadata(input, /* parseData= */ false); + return FlacMetadataReader.checkAndPeekStreamMarker(input); + } + + @Override + public void init(ExtractorOutput output) { + extractorOutput = output; + trackOutput = output.track(/* id= */ 0, C.TRACK_TYPE_AUDIO); + output.endTracks(); + } + + @Override + public @ReadResult int read(ExtractorInput input, PositionHolder seekPosition) + throws IOException { + switch (state) { + case STATE_READ_ID3_METADATA: + readId3Metadata(input); + return Extractor.RESULT_CONTINUE; + case STATE_GET_STREAM_MARKER_AND_INFO_BLOCK_BYTES: + getStreamMarkerAndInfoBlockBytes(input); + return Extractor.RESULT_CONTINUE; + case STATE_READ_STREAM_MARKER: + readStreamMarker(input); + return Extractor.RESULT_CONTINUE; + case STATE_READ_METADATA_BLOCKS: + readMetadataBlocks(input); + return Extractor.RESULT_CONTINUE; + case STATE_GET_FRAME_START_MARKER: + getFrameStartMarker(input); + return Extractor.RESULT_CONTINUE; + case STATE_READ_FRAMES: + return readFrames(input, seekPosition); + default: + throw new IllegalStateException(); + } + } + + @Override + public void seek(long position, long timeUs) { + if (position == 0) { + state = STATE_READ_ID3_METADATA; + } else if (binarySearchSeeker != null) { + binarySearchSeeker.setSeekTargetUs(timeUs); + } + currentFrameFirstSampleNumber = timeUs == 0 ? 0 : SAMPLE_NUMBER_UNKNOWN; + currentFrameBytesWritten = 0; + buffer.reset(); + } + + @Override + public void release() { + // Do nothing. + } + + // Private methods. + + private void readId3Metadata(ExtractorInput input) throws IOException { + id3Metadata = FlacMetadataReader.readId3Metadata(input, /* parseData= */ !id3MetadataDisabled); + state = STATE_GET_STREAM_MARKER_AND_INFO_BLOCK_BYTES; + } + + private void getStreamMarkerAndInfoBlockBytes(ExtractorInput input) throws IOException { + input.peekFully(streamMarkerAndInfoBlock, 0, streamMarkerAndInfoBlock.length); + input.resetPeekPosition(); + state = STATE_READ_STREAM_MARKER; + } + + private void readStreamMarker(ExtractorInput input) throws IOException { + FlacMetadataReader.readStreamMarker(input); + state = STATE_READ_METADATA_BLOCKS; + } + + private void readMetadataBlocks(ExtractorInput input) throws IOException { + boolean isLastMetadataBlock = false; + FlacMetadataReader.FlacStreamMetadataHolder metadataHolder = + new FlacMetadataReader.FlacStreamMetadataHolder(flacStreamMetadata); + while (!isLastMetadataBlock) { + isLastMetadataBlock = FlacMetadataReader.readMetadataBlock(input, metadataHolder); + // Save the current metadata in case an exception occurs. + flacStreamMetadata = castNonNull(metadataHolder.flacStreamMetadata); + } + + Assertions.checkNotNull(flacStreamMetadata); + minFrameSize = Math.max(flacStreamMetadata.minFrameSize, FlacConstants.MIN_FRAME_HEADER_SIZE); + castNonNull(trackOutput) + .format(flacStreamMetadata.getFormat(streamMarkerAndInfoBlock, id3Metadata)); + + state = STATE_GET_FRAME_START_MARKER; + } + + private void getFrameStartMarker(ExtractorInput input) throws IOException { + frameStartMarker = FlacMetadataReader.getFrameStartMarker(input); + castNonNull(extractorOutput) + .seekMap( + getSeekMap( + /* firstFramePosition= */ input.getPosition(), + /* streamLength= */ input.getLength())); + + state = STATE_READ_FRAMES; + } + + private @ReadResult int readFrames(ExtractorInput input, PositionHolder seekPosition) + throws IOException { + Assertions.checkNotNull(trackOutput); + Assertions.checkNotNull(flacStreamMetadata); + + // Handle pending binary search seek if necessary. + if (binarySearchSeeker != null && binarySearchSeeker.isSeeking()) { + return binarySearchSeeker.handlePendingSeek(input, seekPosition); + } + + // Set current frame first sample number if it became unknown after seeking. + if (currentFrameFirstSampleNumber == SAMPLE_NUMBER_UNKNOWN) { + currentFrameFirstSampleNumber = + FlacFrameReader.getFirstSampleNumber(input, flacStreamMetadata); + return Extractor.RESULT_CONTINUE; + } + + // Copy more bytes into the buffer. + int currentLimit = buffer.limit(); + boolean foundEndOfInput = false; + if (currentLimit < BUFFER_LENGTH) { + int bytesRead = + input.read( + buffer.data, /* offset= */ currentLimit, /* length= */ BUFFER_LENGTH - currentLimit); + foundEndOfInput = bytesRead == C.RESULT_END_OF_INPUT; + if (!foundEndOfInput) { + buffer.setLimit(currentLimit + bytesRead); + } else if (buffer.bytesLeft() == 0) { + outputSampleMetadata(); + return Extractor.RESULT_END_OF_INPUT; + } + } + + // Search for a frame. + int positionBeforeFindingAFrame = buffer.getPosition(); + + // Skip frame search on the bytes within the minimum frame size. + if (currentFrameBytesWritten < minFrameSize) { + buffer.skipBytes(Math.min(minFrameSize - currentFrameBytesWritten, buffer.bytesLeft())); + } + + long nextFrameFirstSampleNumber = findFrame(buffer, foundEndOfInput); + int numberOfFrameBytes = buffer.getPosition() - positionBeforeFindingAFrame; + buffer.setPosition(positionBeforeFindingAFrame); + trackOutput.sampleData(buffer, numberOfFrameBytes); + currentFrameBytesWritten += numberOfFrameBytes; + + // Frame found. + if (nextFrameFirstSampleNumber != SAMPLE_NUMBER_UNKNOWN) { + outputSampleMetadata(); + currentFrameBytesWritten = 0; + currentFrameFirstSampleNumber = nextFrameFirstSampleNumber; + } + + if (buffer.bytesLeft() < FlacConstants.MAX_FRAME_HEADER_SIZE) { + // The next frame header may not fit in the rest of the buffer, so put the trailing bytes at + // the start of the buffer, and reset the position and limit. + System.arraycopy( + buffer.data, buffer.getPosition(), buffer.data, /* destPos= */ 0, buffer.bytesLeft()); + buffer.reset(buffer.bytesLeft()); + } + + return Extractor.RESULT_CONTINUE; + } + + private SeekMap getSeekMap(long firstFramePosition, long streamLength) { + Assertions.checkNotNull(flacStreamMetadata); + if (flacStreamMetadata.seekTable != null) { + return new FlacSeekTableSeekMap(flacStreamMetadata, firstFramePosition); + } else if (streamLength != C.LENGTH_UNSET && flacStreamMetadata.totalSamples > 0) { + binarySearchSeeker = + new FlacBinarySearchSeeker( + flacStreamMetadata, frameStartMarker, firstFramePosition, streamLength); + return binarySearchSeeker.getSeekMap(); + } else { + return new SeekMap.Unseekable(flacStreamMetadata.getDurationUs()); + } + } + + /** + * Searches for the start of a frame in {@code data}. + * + *

        + *
      • If the search is successful, the position is set to the start of the found frame. + *
      • Otherwise, the position is set to the first unsearched byte. + *
      + * + * @param data The array to be searched. + * @param foundEndOfInput If the end of input was met when filling in the {@code data}. + * @return The number of the first sample in the frame found, or {@code SAMPLE_NUMBER_UNKNOWN} if + * the search was not successful. + */ + private long findFrame(ParsableByteArray data, boolean foundEndOfInput) { + Assertions.checkNotNull(flacStreamMetadata); + + int frameOffset = data.getPosition(); + while (frameOffset <= data.limit() - FlacConstants.MAX_FRAME_HEADER_SIZE) { + data.setPosition(frameOffset); + if (FlacFrameReader.checkAndReadFrameHeader( + data, flacStreamMetadata, frameStartMarker, sampleNumberHolder)) { + data.setPosition(frameOffset); + return sampleNumberHolder.sampleNumber; + } + frameOffset++; + } + + if (foundEndOfInput) { + // Verify whether there is a frame of size < MAX_FRAME_HEADER_SIZE at the end of the stream by + // checking at every position at a distance between MAX_FRAME_HEADER_SIZE and minFrameSize + // from the buffer limit if it corresponds to a valid frame header. + // At every offset, the different possibilities are: + // 1. The current offset indicates the start of a valid frame header. In this case, consider + // that a frame has been found and stop searching. + // 2. A frame starting at the current offset would be invalid. In this case, keep looking for + // a valid frame header. + // 3. The current offset could be the start of a valid frame header, but there is not enough + // bytes remaining to complete the header. As the end of the file has been reached, this + // means that the current offset does not correspond to a new frame and that the last bytes + // of the last frame happen to be a valid partial frame header. This case can occur in two + // ways: + // 3.1. An attempt to read past the buffer is made when reading the potential frame header. + // 3.2. Reading the potential frame header does not exceed the buffer size, but exceeds the + // buffer limit. + // Note that the third case is very unlikely. It never happens if the end of the input has not + // been reached as it is always made sure that the buffer has at least MAX_FRAME_HEADER_SIZE + // bytes available when reading a potential frame header. + while (frameOffset <= data.limit() - minFrameSize) { + data.setPosition(frameOffset); + boolean frameFound; + try { + frameFound = + FlacFrameReader.checkAndReadFrameHeader( + data, flacStreamMetadata, frameStartMarker, sampleNumberHolder); + } catch (IndexOutOfBoundsException e) { + // Case 3.1. + frameFound = false; + } + if (data.getPosition() > data.limit()) { + // TODO: Remove (and update above comments) once [Internal ref: b/147657250] is fixed. + // Case 3.2. + frameFound = false; + } + if (frameFound) { + // Case 1. + data.setPosition(frameOffset); + return sampleNumberHolder.sampleNumber; + } + frameOffset++; + } + // The end of the frame is the end of the file. + data.setPosition(data.limit()); + } else { + data.setPosition(frameOffset); + } + + return SAMPLE_NUMBER_UNKNOWN; + } + + private void outputSampleMetadata() { + long timeUs = + currentFrameFirstSampleNumber + * C.MICROS_PER_SECOND + / castNonNull(flacStreamMetadata).sampleRate; + castNonNull(trackOutput) + .sampleMetadata( + timeUs, + C.BUFFER_FLAG_KEY_FRAME, + currentFrameBytesWritten, + /* offset= */ 0, + /* encryptionData= */ null); + } +} diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/flac/package-info.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/flac/package-info.java new file mode 100644 index 0000000000..44d3427910 --- /dev/null +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/flac/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2019 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. + */ +@NonNullApi +package com.google.android.exoplayer2.extractor.flac; + +import com.google.android.exoplayer2.util.NonNullApi; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/AudioTagPayloadReader.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/flv/AudioTagPayloadReader.java similarity index 78% rename from library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/AudioTagPayloadReader.java rename to library/extractor/src/main/java/com/google/android/exoplayer2/extractor/flv/AudioTagPayloadReader.java index b10f2bf80b..0ca65e4de5 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/AudioTagPayloadReader.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/flv/AudioTagPayloadReader.java @@ -15,12 +15,11 @@ */ package com.google.android.exoplayer2.extractor.flv; -import android.util.Pair; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.ParserException; +import com.google.android.exoplayer2.audio.AacUtil; import com.google.android.exoplayer2.extractor.TrackOutput; -import com.google.android.exoplayer2.util.CodecSpecificDataUtil; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.ParsableByteArray; import java.util.Collections; @@ -62,16 +61,23 @@ import java.util.Collections; if (audioFormat == AUDIO_FORMAT_MP3) { int sampleRateIndex = (header >> 2) & 0x03; int sampleRate = AUDIO_SAMPLING_RATE_TABLE[sampleRateIndex]; - Format format = Format.createAudioSampleFormat(null, MimeTypes.AUDIO_MPEG, null, - Format.NO_VALUE, Format.NO_VALUE, 1, sampleRate, null, null, 0, null); + Format format = + new Format.Builder() + .setSampleMimeType(MimeTypes.AUDIO_MPEG) + .setChannelCount(1) + .setSampleRate(sampleRate) + .build(); output.format(format); hasOutputFormat = true; } else if (audioFormat == AUDIO_FORMAT_ALAW || audioFormat == AUDIO_FORMAT_ULAW) { - String type = audioFormat == AUDIO_FORMAT_ALAW ? MimeTypes.AUDIO_ALAW - : MimeTypes.AUDIO_MLAW; - int pcmEncoding = (header & 0x01) == 1 ? C.ENCODING_PCM_16BIT : C.ENCODING_PCM_8BIT; - Format format = Format.createAudioSampleFormat(null, type, null, Format.NO_VALUE, - Format.NO_VALUE, 1, 8000, pcmEncoding, null, null, 0, null); + String mimeType = + audioFormat == AUDIO_FORMAT_ALAW ? MimeTypes.AUDIO_ALAW : MimeTypes.AUDIO_MLAW; + Format format = + new Format.Builder() + .setSampleMimeType(mimeType) + .setChannelCount(1) + .setSampleRate(8000) + .build(); output.format(format); hasOutputFormat = true; } else if (audioFormat != AUDIO_FORMAT_AAC) { @@ -98,11 +104,15 @@ import java.util.Collections; // Parse the sequence header. byte[] audioSpecificConfig = new byte[data.bytesLeft()]; data.readBytes(audioSpecificConfig, 0, audioSpecificConfig.length); - Pair audioParams = CodecSpecificDataUtil.parseAacAudioSpecificConfig( - audioSpecificConfig); - Format format = Format.createAudioSampleFormat(null, MimeTypes.AUDIO_AAC, null, - Format.NO_VALUE, Format.NO_VALUE, audioParams.second, audioParams.first, - Collections.singletonList(audioSpecificConfig), null, 0, null); + AacUtil.Config aacConfig = AacUtil.parseAudioSpecificConfig(audioSpecificConfig); + Format format = + new Format.Builder() + .setSampleMimeType(MimeTypes.AUDIO_AAC) + .setCodecs(aacConfig.codecs) + .setChannelCount(aacConfig.channelCount) + .setSampleRate(aacConfig.sampleRateHz) + .setInitializationData(Collections.singletonList(audioSpecificConfig)) + .build(); output.format(format); hasOutputFormat = true; return false; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/FlvExtractor.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/flv/FlvExtractor.java similarity index 93% rename from library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/FlvExtractor.java rename to library/extractor/src/main/java/com/google/android/exoplayer2/extractor/flv/FlvExtractor.java index f6835558f2..98c5fa73a4 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/FlvExtractor.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/flv/FlvExtractor.java @@ -23,11 +23,14 @@ import com.google.android.exoplayer2.extractor.ExtractorOutput; 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.util.Assertions; import com.google.android.exoplayer2.util.ParsableByteArray; import java.io.IOException; import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.checkerframework.checker.nullness.qual.RequiresNonNull; /** * Extracts data from the FLV container format. @@ -71,7 +74,7 @@ public final class FlvExtractor implements Extractor { private final ParsableByteArray tagData; private final ScriptTagPayloadReader metadataReader; - private ExtractorOutput extractorOutput; + private @MonotonicNonNull ExtractorOutput extractorOutput; private @States int state; private boolean outputFirstSample; private long mediaTagTimestampOffsetUs; @@ -80,8 +83,8 @@ public final class FlvExtractor implements Extractor { private int tagDataSize; private long tagTimestampUs; private boolean outputSeekMap; - private AudioTagPayloadReader audioReader; - private VideoTagPayloadReader videoReader; + private @MonotonicNonNull AudioTagPayloadReader audioReader; + private @MonotonicNonNull VideoTagPayloadReader videoReader; public FlvExtractor() { scratch = new ParsableByteArray(4); @@ -93,7 +96,7 @@ public final class FlvExtractor implements Extractor { } @Override - public boolean sniff(ExtractorInput input) throws IOException, InterruptedException { + public boolean sniff(ExtractorInput input) throws IOException { // Check if file starts with "FLV" tag input.peekFully(scratch.data, 0, 3); scratch.setPosition(0); @@ -141,8 +144,8 @@ public final class FlvExtractor implements Extractor { } @Override - public int read(ExtractorInput input, PositionHolder seekPosition) throws IOException, - InterruptedException { + public int read(ExtractorInput input, PositionHolder seekPosition) throws IOException { + Assertions.checkStateNotNull(extractorOutput); // Asserts that init has been called. while (true) { switch (state) { case STATE_READING_FLV_HEADER: @@ -176,9 +179,9 @@ public final class FlvExtractor implements Extractor { * @param input The {@link ExtractorInput} from which to read. * @return True if header was read successfully. False if the end of stream was reached. * @throws IOException If an error occurred reading or parsing data from the source. - * @throws InterruptedException If the thread was interrupted. */ - private boolean readFlvHeader(ExtractorInput input) throws IOException, InterruptedException { + @RequiresNonNull("extractorOutput") + private boolean readFlvHeader(ExtractorInput input) throws IOException { if (!input.readFully(headerBuffer.data, 0, FLV_HEADER_SIZE, true)) { // We've reached the end of the stream. return false; @@ -210,9 +213,8 @@ public final class FlvExtractor implements Extractor { * * @param input The {@link ExtractorInput} from which to read. * @throws IOException If an error occurred skipping data from the source. - * @throws InterruptedException If the thread was interrupted. */ - private void skipToTagHeader(ExtractorInput input) throws IOException, InterruptedException { + private void skipToTagHeader(ExtractorInput input) throws IOException { input.skipFully(bytesToNextTagHeader); bytesToNextTagHeader = 0; state = STATE_READING_TAG_HEADER; @@ -224,9 +226,8 @@ public final class FlvExtractor implements Extractor { * @param input The {@link ExtractorInput} from which to read. * @return True if tag header was read successfully. Otherwise, false. * @throws IOException If an error occurred reading or parsing data from the source. - * @throws InterruptedException If the thread was interrupted. */ - private boolean readTagHeader(ExtractorInput input) throws IOException, InterruptedException { + private boolean readTagHeader(ExtractorInput input) throws IOException { if (!input.readFully(tagHeaderBuffer.data, 0, FLV_TAG_HEADER_SIZE, true)) { // We've reached the end of the stream. return false; @@ -248,9 +249,9 @@ public final class FlvExtractor implements Extractor { * @param input The {@link ExtractorInput} from which to read. * @return True if the data was consumed by a reader. False if it was skipped. * @throws IOException If an error occurred reading or parsing data from the source. - * @throws InterruptedException If the thread was interrupted. */ - private boolean readTagData(ExtractorInput input) throws IOException, InterruptedException { + @RequiresNonNull("extractorOutput") + private boolean readTagData(ExtractorInput input) throws IOException { boolean wasConsumed = true; boolean wasSampleOutput = false; long timestampUs = getCurrentTimestampUs(); @@ -281,8 +282,7 @@ public final class FlvExtractor implements Extractor { return wasConsumed; } - private ParsableByteArray prepareTagData(ExtractorInput input) throws IOException, - InterruptedException { + private ParsableByteArray prepareTagData(ExtractorInput input) throws IOException { if (tagDataSize > tagData.capacity()) { tagData.reset(new byte[Math.max(tagData.capacity() * 2, tagDataSize)], 0); } else { @@ -293,6 +293,7 @@ public final class FlvExtractor implements Extractor { return tagData; } + @RequiresNonNull("extractorOutput") private void ensureReadyForMediaOutput() { if (!outputSeekMap) { extractorOutput.seekMap(new SeekMap.Unseekable(C.TIME_UNSET)); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/ScriptTagPayloadReader.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/flv/ScriptTagPayloadReader.java similarity index 100% rename from library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/ScriptTagPayloadReader.java rename to library/extractor/src/main/java/com/google/android/exoplayer2/extractor/flv/ScriptTagPayloadReader.java diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/TagPayloadReader.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/flv/TagPayloadReader.java similarity index 100% rename from library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/TagPayloadReader.java rename to library/extractor/src/main/java/com/google/android/exoplayer2/extractor/flv/TagPayloadReader.java diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/VideoTagPayloadReader.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/flv/VideoTagPayloadReader.java similarity index 93% rename from library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/VideoTagPayloadReader.java rename to library/extractor/src/main/java/com/google/android/exoplayer2/extractor/flv/VideoTagPayloadReader.java index 5ddaafb4a8..891b228dbb 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/VideoTagPayloadReader.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/flv/VideoTagPayloadReader.java @@ -90,9 +90,14 @@ import com.google.android.exoplayer2.video.AvcConfig; AvcConfig avcConfig = AvcConfig.parse(videoSequence); nalUnitLengthFieldLength = avcConfig.nalUnitLengthFieldLength; // Construct and output the format. - Format format = Format.createVideoSampleFormat(null, MimeTypes.VIDEO_H264, null, - Format.NO_VALUE, Format.NO_VALUE, avcConfig.width, avcConfig.height, Format.NO_VALUE, - avcConfig.initializationData, Format.NO_VALUE, avcConfig.pixelWidthAspectRatio, null); + Format format = + new Format.Builder() + .setSampleMimeType(MimeTypes.VIDEO_H264) + .setWidth(avcConfig.width) + .setHeight(avcConfig.height) + .setPixelWidthHeightRatio(avcConfig.pixelWidthAspectRatio) + .setInitializationData(avcConfig.initializationData) + .build(); output.format(format); hasOutputFormat = true; return false; diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/flv/package-info.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/flv/package-info.java new file mode 100644 index 0000000000..e726bb50e2 --- /dev/null +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/flv/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2019 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. + */ +@NonNullApi +package com.google.android.exoplayer2.extractor.flv; + +import com.google.android.exoplayer2.util.NonNullApi; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/DefaultEbmlReader.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mkv/DefaultEbmlReader.java similarity index 91% rename from library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/DefaultEbmlReader.java rename to library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mkv/DefaultEbmlReader.java index b5da6dbf2f..754cd7a4c2 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/DefaultEbmlReader.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mkv/DefaultEbmlReader.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.extractor.mkv; + import androidx.annotation.IntDef; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ParserException; @@ -26,6 +27,8 @@ import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayDeque; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.checkerframework.checker.nullness.qual.RequiresNonNull; /** * Default implementation of {@link EbmlReader}. @@ -52,7 +55,7 @@ import java.util.ArrayDeque; private final ArrayDeque masterElementsStack; private final VarintReader varintReader; - private EbmlProcessor processor; + private @MonotonicNonNull EbmlProcessor processor; private @ElementState int elementState; private int elementId; private long elementContentSize; @@ -76,11 +79,11 @@ import java.util.ArrayDeque; } @Override - public boolean read(ExtractorInput input) throws IOException, InterruptedException { - Assertions.checkNotNull(processor); + public boolean read(ExtractorInput input) throws IOException { + Assertions.checkStateNotNull(processor); while (true) { - if (!masterElementsStack.isEmpty() - && input.getPosition() >= masterElementsStack.peek().elementEndPosition) { + MasterElement head = masterElementsStack.peek(); + if (head != null && input.getPosition() >= head.elementEndPosition) { processor.endMasterElement(masterElementsStack.pop().elementId); return true; } @@ -157,10 +160,9 @@ import java.util.ArrayDeque; * @throws EOFException If the end of input was encountered when searching for the next level 1 * element. * @throws IOException If an error occurs reading from the input. - * @throws InterruptedException If the thread is interrupted. */ - private long maybeResyncToNextLevel1Element(ExtractorInput input) throws IOException, - InterruptedException { + @RequiresNonNull("processor") + private long maybeResyncToNextLevel1Element(ExtractorInput input) throws IOException { input.resetPeekPosition(); while (true) { input.peekFully(scratch, 0, MAX_ID_BYTES); @@ -183,10 +185,8 @@ import java.util.ArrayDeque; * @param byteLength The length of the integer being read. * @return The read integer value. * @throws IOException If an error occurs reading from the input. - * @throws InterruptedException If the thread is interrupted. */ - private long readInteger(ExtractorInput input, int byteLength) - throws IOException, InterruptedException { + private long readInteger(ExtractorInput input, int byteLength) throws IOException { input.readFully(scratch, 0, byteLength); long value = 0; for (int i = 0; i < byteLength; i++) { @@ -202,10 +202,8 @@ import java.util.ArrayDeque; * @param byteLength The length of the float being read. * @return The read float value. * @throws IOException If an error occurs reading from the input. - * @throws InterruptedException If the thread is interrupted. */ - private double readFloat(ExtractorInput input, int byteLength) - throws IOException, InterruptedException { + private double readFloat(ExtractorInput input, int byteLength) throws IOException { long integerValue = readInteger(input, byteLength); double floatValue; if (byteLength == VALID_FLOAT32_ELEMENT_SIZE_BYTES) { @@ -224,10 +222,8 @@ import java.util.ArrayDeque; * @param byteLength The length of the string being read, including zero padding. * @return The read string value. * @throws IOException If an error occurs reading from the input. - * @throws InterruptedException If the thread is interrupted. */ - private String readString(ExtractorInput input, int byteLength) - throws IOException, InterruptedException { + private static String readString(ExtractorInput input, int byteLength) throws IOException { if (byteLength == 0) { return ""; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/EbmlProcessor.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mkv/EbmlProcessor.java similarity index 90% rename from library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/EbmlProcessor.java rename to library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mkv/EbmlProcessor.java index 01fe5ff984..7291ae9c83 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/EbmlProcessor.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mkv/EbmlProcessor.java @@ -130,21 +130,18 @@ public interface EbmlProcessor { /** * Called when a binary element is encountered. - *

      - * The element header (containing the element ID and content size) will already have been read. - * Implementations are required to consume the whole remainder of the element, which is - * {@code contentSize} bytes in length, before returning. Implementations are permitted to fail - * (by throwing an exception) having partially consumed the data, however if they do this, they - * must consume the remainder of the content when called again. + * + *

      The element header (containing the element ID and content size) will already have been read. + * Implementations are required to consume the whole remainder of the element, which is {@code + * contentSize} bytes in length, before returning. Implementations are permitted to fail (by + * throwing an exception) having partially consumed the data, however if they do this, they must + * consume the remainder of the content when called again. * * @param id The element ID. * @param contentsSize The element's content size. * @param input The {@link ExtractorInput} from which data should be read. * @throws ParserException If a parsing error occurs. * @throws IOException If an error occurs reading from the input. - * @throws InterruptedException If the thread is interrupted. */ - void binaryElement(int id, int contentsSize, ExtractorInput input) - throws IOException, InterruptedException; - + void binaryElement(int id, int contentsSize, ExtractorInput input) throws IOException; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/EbmlReader.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mkv/EbmlReader.java similarity index 93% rename from library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/EbmlReader.java rename to library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mkv/EbmlReader.java index c3f00a222f..fc32fba4f7 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/EbmlReader.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mkv/EbmlReader.java @@ -50,8 +50,6 @@ import java.io.IOException; * @return True if data can continue to be read. False if the end of the input was encountered. * @throws ParserException If parsing fails. * @throws IOException If an error occurs reading from the input. - * @throws InterruptedException If the thread is interrupted. */ - boolean read(ExtractorInput input) throws IOException, InterruptedException; - + boolean read(ExtractorInput input) throws IOException; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java similarity index 86% rename from library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java rename to library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java index 69bdb2cd46..4d24c4fb95 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java @@ -24,6 +24,7 @@ import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.audio.Ac3Util; +import com.google.android.exoplayer2.audio.MpegAudioUtil; import com.google.android.exoplayer2.drm.DrmInitData; import com.google.android.exoplayer2.drm.DrmInitData.SchemeData; import com.google.android.exoplayer2.extractor.ChunkIndex; @@ -31,7 +32,6 @@ import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.ExtractorsFactory; -import com.google.android.exoplayer2.extractor.MpegAudioHeader; import com.google.android.exoplayer2.extractor.PositionHolder; import com.google.android.exoplayer2.extractor.SeekMap; import com.google.android.exoplayer2.extractor.TrackOutput; @@ -54,9 +54,13 @@ import java.nio.ByteOrder; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.HashMap; import java.util.List; import java.util.Locale; +import java.util.Map; import java.util.UUID; +import org.checkerframework.checker.nullness.compatqual.NullableType; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /** Extracts data from the Matroska and WebM container formats. */ public class MatroskaExtractor implements Extractor { @@ -224,7 +228,7 @@ public class MatroskaExtractor implements Extractor { * BlockAddID value for ITU T.35 metadata in a VP9 track. See also * https://www.webmproject.org/docs/container/. */ - private static final int BLOCK_ADD_ID_VP9_ITU_T_35 = 4; + private static final int BLOCK_ADDITIONAL_ID_VP9_ITU_T_35 = 4; private static final int LACING_NONE = 0; private static final int LACING_XIPH = 1; @@ -317,6 +321,18 @@ public class MatroskaExtractor implements Extractor { */ private static final UUID WAVE_SUBFORMAT_PCM = new UUID(0x0100000000001000L, 0x800000AA00389B71L); + /** Some HTC devices signal rotation in track names. */ + private static final Map TRACK_NAME_TO_ROTATION_DEGREES; + + static { + Map trackNameToRotationDegrees = new HashMap<>(); + trackNameToRotationDegrees.put("htc_video_rotA-000", 0); + trackNameToRotationDegrees.put("htc_video_rotA-090", 90); + trackNameToRotationDegrees.put("htc_video_rotA-180", 180); + trackNameToRotationDegrees.put("htc_video_rotA-270", 270); + TRACK_NAME_TO_ROTATION_DEGREES = Collections.unmodifiableMap(trackNameToRotationDegrees); + } + private final EbmlReader reader; private final VarintReader varintReader; private final SparseArray tracks; @@ -332,7 +348,7 @@ public class MatroskaExtractor implements Extractor { private final ParsableByteArray subtitleSample; private final ParsableByteArray encryptionInitializationVector; private final ParsableByteArray encryptionSubsampleData; - private final ParsableByteArray blockAddData; + private final ParsableByteArray blockAdditionalData; private ByteBuffer encryptionSubsampleDataBuffer; private long segmentContentSize; @@ -342,7 +358,7 @@ public class MatroskaExtractor implements Extractor { private long durationUs = C.TIME_UNSET; // The track corresponding to the current TrackEntry element, or null. - private Track currentTrack; + @Nullable private Track currentTrack; // Whether a seek map has been sent to the output. private boolean sentSeekMap; @@ -356,38 +372,39 @@ public class MatroskaExtractor implements Extractor { private long cuesContentPosition = C.POSITION_UNSET; private long seekPositionAfterBuildingCues = C.POSITION_UNSET; private long clusterTimecodeUs = C.TIME_UNSET; - private LongArray cueTimesUs; - private LongArray cueClusterPositions; + @Nullable private LongArray cueTimesUs; + @Nullable private LongArray cueClusterPositions; private boolean seenClusterPositionForCurrentCuePoint; + // Reading state. + private boolean haveOutputSample; + // Block reading state. private int blockState; private long blockTimeUs; private long blockDurationUs; - private int blockLacingSampleIndex; - private int blockLacingSampleCount; - private int[] blockLacingSampleSizes; + private int blockSampleIndex; + private int blockSampleCount; + private int[] blockSampleSizes; private int blockTrackNumber; private int blockTrackNumberLength; - @C.BufferFlags - private int blockFlags; - private int blockAddId; + @C.BufferFlags private int blockFlags; + private int blockAdditionalId; + private boolean blockHasReferenceBlock; - // Sample reading state. + // Sample writing state. private int sampleBytesRead; + private int sampleBytesWritten; + private int sampleCurrentNalBytesRemaining; private boolean sampleEncodingHandled; private boolean sampleSignalByteRead; - private boolean sampleInitializationVectorRead; private boolean samplePartitionCountRead; - private byte sampleSignalByte; private int samplePartitionCount; - private int sampleCurrentNalBytesRemaining; - private int sampleBytesWritten; - private boolean sampleRead; - private boolean sampleSeenReferenceBlock; + private byte sampleSignalByte; + private boolean sampleInitializationVectorRead; // Extractor outputs. - private ExtractorOutput extractorOutput; + private @MonotonicNonNull ExtractorOutput extractorOutput; public MatroskaExtractor() { this(0); @@ -412,11 +429,12 @@ public class MatroskaExtractor implements Extractor { subtitleSample = new ParsableByteArray(); encryptionInitializationVector = new ParsableByteArray(ENCRYPTION_IV_SIZE); encryptionSubsampleData = new ParsableByteArray(); - blockAddData = new ParsableByteArray(); + blockAdditionalData = new ParsableByteArray(); + blockSampleSizes = new int[1]; } @Override - public final boolean sniff(ExtractorInput input) throws IOException, InterruptedException { + public final boolean sniff(ExtractorInput input) throws IOException { return new Sniffer().sniff(input); } @@ -432,7 +450,7 @@ public class MatroskaExtractor implements Extractor { blockState = BLOCK_STATE_START; reader.reset(); varintReader.reset(); - resetSample(); + resetWriteSampleData(); for (int i = 0; i < tracks.size(); i++) { tracks.valueAt(i).reset(); } @@ -444,11 +462,10 @@ public class MatroskaExtractor implements Extractor { } @Override - public final int read(ExtractorInput input, PositionHolder seekPosition) - throws IOException, InterruptedException { - sampleRead = false; + public final int read(ExtractorInput input, PositionHolder seekPosition) throws IOException { + haveOutputSample = false; boolean continueReading = true; - while (continueReading && !sampleRead) { + while (continueReading && !haveOutputSample) { continueReading = reader.read(input); if (continueReading && maybeSeekForCues(seekPosition, input.getPosition())) { return Extractor.RESULT_SEEK; @@ -623,7 +640,7 @@ public class MatroskaExtractor implements Extractor { } break; case ID_BLOCK_GROUP: - sampleSeenReferenceBlock = false; + blockHasReferenceBlock = false; break; case ID_CONTENT_ENCODING: // TODO: check and fail if more than one content encoding is present. @@ -680,11 +697,24 @@ public class MatroskaExtractor implements Extractor { // We've skipped this block (due to incompatible track number). return; } - // If the ReferenceBlock element was not found for this sample, then it is a keyframe. - if (!sampleSeenReferenceBlock) { - blockFlags |= C.BUFFER_FLAG_KEY_FRAME; + // Commit sample metadata. + int sampleOffset = 0; + for (int i = 0; i < blockSampleCount; i++) { + sampleOffset += blockSampleSizes[i]; + } + Track track = tracks.get(blockTrackNumber); + for (int i = 0; i < blockSampleCount; i++) { + long sampleTimeUs = blockTimeUs + (i * track.defaultSampleDurationNs) / 1000; + int sampleFlags = blockFlags; + if (i == 0 && !blockHasReferenceBlock) { + // If the ReferenceBlock element was not found in this block, then the first frame is a + // keyframe. + sampleFlags |= C.BUFFER_FLAG_KEY_FRAME; + } + int sampleSize = blockSampleSizes[i]; + sampleOffset -= sampleSize; // The offset is to the end of the sample. + commitSampleToOutput(track, sampleTimeUs, sampleFlags, sampleSize, sampleOffset); } - commitSampleToOutput(tracks.get(blockTrackNumber), blockTimeUs); blockState = BLOCK_STATE_START; break; case ID_CONTENT_ENCODING: @@ -793,7 +823,7 @@ public class MatroskaExtractor implements Extractor { currentTrack.audioBitDepth = (int) value; break; case ID_REFERENCE_BLOCK: - sampleSeenReferenceBlock = true; + blockHasReferenceBlock = true; break; case ID_CONTENT_ENCODING_ORDER: // This extractor only supports one ContentEncoding element and hence the order has to be 0. @@ -935,7 +965,7 @@ public class MatroskaExtractor implements Extractor { } break; case ID_BLOCK_ADD_ID: - blockAddId = (int) value; + blockAdditionalId = (int) value; break; default: break; @@ -1034,8 +1064,7 @@ public class MatroskaExtractor implements Extractor { * @see EbmlProcessor#binaryElement(int, int, ExtractorInput) */ @CallSuper - protected void binaryElement(int id, int contentSize, ExtractorInput input) - throws IOException, InterruptedException { + protected void binaryElement(int id, int contentSize, ExtractorInput input) throws IOException { switch (id) { case ID_SEEK_ID: Arrays.fill(seekEntryIdBytes.data, (byte) 0); @@ -1091,43 +1120,38 @@ public class MatroskaExtractor implements Extractor { readScratch(input, 3); int lacing = (scratch.data[2] & 0x06) >> 1; if (lacing == LACING_NONE) { - blockLacingSampleCount = 1; - blockLacingSampleSizes = ensureArrayCapacity(blockLacingSampleSizes, 1); - blockLacingSampleSizes[0] = contentSize - blockTrackNumberLength - 3; + blockSampleCount = 1; + blockSampleSizes = ensureArrayCapacity(blockSampleSizes, 1); + blockSampleSizes[0] = contentSize - blockTrackNumberLength - 3; } else { - if (id != ID_SIMPLE_BLOCK) { - throw new ParserException("Lacing only supported in SimpleBlocks."); - } - // Read the sample count (1 byte). readScratch(input, 4); - blockLacingSampleCount = (scratch.data[3] & 0xFF) + 1; - blockLacingSampleSizes = - ensureArrayCapacity(blockLacingSampleSizes, blockLacingSampleCount); + blockSampleCount = (scratch.data[3] & 0xFF) + 1; + blockSampleSizes = ensureArrayCapacity(blockSampleSizes, blockSampleCount); if (lacing == LACING_FIXED_SIZE) { int blockLacingSampleSize = - (contentSize - blockTrackNumberLength - 4) / blockLacingSampleCount; - Arrays.fill(blockLacingSampleSizes, 0, blockLacingSampleCount, blockLacingSampleSize); + (contentSize - blockTrackNumberLength - 4) / blockSampleCount; + Arrays.fill(blockSampleSizes, 0, blockSampleCount, blockLacingSampleSize); } else if (lacing == LACING_XIPH) { int totalSamplesSize = 0; int headerSize = 4; - for (int sampleIndex = 0; sampleIndex < blockLacingSampleCount - 1; sampleIndex++) { - blockLacingSampleSizes[sampleIndex] = 0; + for (int sampleIndex = 0; sampleIndex < blockSampleCount - 1; sampleIndex++) { + blockSampleSizes[sampleIndex] = 0; int byteValue; do { readScratch(input, ++headerSize); byteValue = scratch.data[headerSize - 1] & 0xFF; - blockLacingSampleSizes[sampleIndex] += byteValue; + blockSampleSizes[sampleIndex] += byteValue; } while (byteValue == 0xFF); - totalSamplesSize += blockLacingSampleSizes[sampleIndex]; + totalSamplesSize += blockSampleSizes[sampleIndex]; } - blockLacingSampleSizes[blockLacingSampleCount - 1] = + blockSampleSizes[blockSampleCount - 1] = contentSize - blockTrackNumberLength - headerSize - totalSamplesSize; } else if (lacing == LACING_EBML) { int totalSamplesSize = 0; int headerSize = 4; - for (int sampleIndex = 0; sampleIndex < blockLacingSampleCount - 1; sampleIndex++) { - blockLacingSampleSizes[sampleIndex] = 0; + for (int sampleIndex = 0; sampleIndex < blockSampleCount - 1; sampleIndex++) { + blockSampleSizes[sampleIndex] = 0; readScratch(input, ++headerSize); if (scratch.data[headerSize - 1] == 0) { throw new ParserException("No valid varint length mask found"); @@ -1155,11 +1179,13 @@ public class MatroskaExtractor implements Extractor { throw new ParserException("EBML lacing sample size out of range."); } int intReadValue = (int) readValue; - blockLacingSampleSizes[sampleIndex] = sampleIndex == 0 - ? intReadValue : blockLacingSampleSizes[sampleIndex - 1] + intReadValue; - totalSamplesSize += blockLacingSampleSizes[sampleIndex]; + blockSampleSizes[sampleIndex] = + sampleIndex == 0 + ? intReadValue + : blockSampleSizes[sampleIndex - 1] + intReadValue; + totalSamplesSize += blockSampleSizes[sampleIndex]; } - blockLacingSampleSizes[blockLacingSampleCount - 1] = + blockSampleSizes[blockSampleCount - 1] = contentSize - blockTrackNumberLength - headerSize - totalSamplesSize; } else { // Lacing is always in the range 0--3. @@ -1175,23 +1201,31 @@ public class MatroskaExtractor implements Extractor { blockFlags = (isKeyframe ? C.BUFFER_FLAG_KEY_FRAME : 0) | (isInvisible ? C.BUFFER_FLAG_DECODE_ONLY : 0); blockState = BLOCK_STATE_DATA; - blockLacingSampleIndex = 0; + blockSampleIndex = 0; } if (id == ID_SIMPLE_BLOCK) { - // For SimpleBlock, we have metadata for each sample here. - while (blockLacingSampleIndex < blockLacingSampleCount) { - writeSampleData(input, track, blockLacingSampleSizes[blockLacingSampleIndex]); - long sampleTimeUs = blockTimeUs - + (blockLacingSampleIndex * track.defaultSampleDurationNs) / 1000; - commitSampleToOutput(track, sampleTimeUs); - blockLacingSampleIndex++; + // For SimpleBlock, we can write sample data and immediately commit the corresponding + // sample metadata. + while (blockSampleIndex < blockSampleCount) { + int sampleSize = writeSampleData(input, track, blockSampleSizes[blockSampleIndex]); + long sampleTimeUs = + blockTimeUs + (blockSampleIndex * track.defaultSampleDurationNs) / 1000; + commitSampleToOutput(track, sampleTimeUs, blockFlags, sampleSize, /* offset= */ 0); + blockSampleIndex++; } blockState = BLOCK_STATE_START; } else { - // For Block, we send the metadata at the end of the BlockGroup element since we'll know - // if the sample is a keyframe or not only at that point. - writeSampleData(input, track, blockLacingSampleSizes[0]); + // For Block, we need to wait until the end of the BlockGroup element before committing + // sample metadata. This is so that we can handle ReferenceBlock (which can be used to + // infer whether the first sample in the block is a keyframe), and BlockAdditions (which + // can contain additional sample data to append) contained in the block group. Just output + // the sample data, storing the final sample sizes for when we commit the metadata. + while (blockSampleIndex < blockSampleCount) { + blockSampleSizes[blockSampleIndex] = + writeSampleData(input, track, blockSampleSizes[blockSampleIndex]); + blockSampleIndex++; + } } break; @@ -1199,7 +1233,8 @@ public class MatroskaExtractor implements Extractor { if (blockState != BLOCK_STATE_DATA) { return; } - handleBlockAdditionalData(tracks.get(blockTrackNumber), blockAddId, input, contentSize); + handleBlockAdditionalData( + tracks.get(blockTrackNumber), blockAdditionalId, input, contentSize); break; default: throw new ParserException("Unexpected id: " + id); @@ -1207,64 +1242,60 @@ public class MatroskaExtractor implements Extractor { } protected void handleBlockAdditionalData( - Track track, int blockAddId, ExtractorInput input, int contentSize) - throws IOException, InterruptedException { - if (blockAddId == BLOCK_ADD_ID_VP9_ITU_T_35 && CODEC_ID_VP9.equals(track.codecId)) { - blockAddData.reset(contentSize); - input.readFully(blockAddData.data, 0, contentSize); + Track track, int blockAdditionalId, ExtractorInput input, int contentSize) + throws IOException { + if (blockAdditionalId == BLOCK_ADDITIONAL_ID_VP9_ITU_T_35 + && CODEC_ID_VP9.equals(track.codecId)) { + blockAdditionalData.reset(contentSize); + input.readFully(blockAdditionalData.data, 0, contentSize); } else { // Unhandled block additional data. input.skipFully(contentSize); } } - private void commitSampleToOutput(Track track, long timeUs) { + private void commitSampleToOutput( + Track track, long timeUs, @C.BufferFlags int flags, int size, int offset) { if (track.trueHdSampleRechunker != null) { - track.trueHdSampleRechunker.sampleMetadata(track, timeUs); + track.trueHdSampleRechunker.sampleMetadata(track, timeUs, flags, size, offset); } else { if (CODEC_ID_SUBRIP.equals(track.codecId) || CODEC_ID_ASS.equals(track.codecId)) { - if (durationUs == C.TIME_UNSET) { + if (blockSampleCount > 1) { + Log.w(TAG, "Skipping subtitle sample in laced block."); + } else if (blockDurationUs == C.TIME_UNSET) { Log.w(TAG, "Skipping subtitle sample with no duration."); } else { - setSubtitleEndTime(track.codecId, durationUs, subtitleSample.data); + setSubtitleEndTime(track.codecId, blockDurationUs, subtitleSample.data); // Note: If we ever want to support DRM protected subtitles then we'll need to output the // appropriate encryption data here. track.output.sampleData(subtitleSample, subtitleSample.limit()); - sampleBytesWritten += subtitleSample.limit(); + size += subtitleSample.limit(); } } - if ((blockFlags & C.BUFFER_FLAG_HAS_SUPPLEMENTAL_DATA) != 0) { - // Append supplemental data. - int size = blockAddData.limit(); - track.output.sampleData(blockAddData, size); - sampleBytesWritten += size; + if ((flags & C.BUFFER_FLAG_HAS_SUPPLEMENTAL_DATA) != 0) { + if (blockSampleCount > 1) { + // There were multiple samples in the block. Appending the additional data to the last + // sample doesn't make sense. Skip instead. + flags &= ~C.BUFFER_FLAG_HAS_SUPPLEMENTAL_DATA; + } else { + // Append supplemental data. + int blockAdditionalSize = blockAdditionalData.limit(); + track.output.sampleData( + blockAdditionalData, blockAdditionalSize, TrackOutput.SAMPLE_DATA_PART_SUPPLEMENTAL); + size += blockAdditionalSize; + } } - track.output.sampleMetadata(timeUs, blockFlags, sampleBytesWritten, 0, track.cryptoData); + track.output.sampleMetadata(timeUs, flags, size, offset, track.cryptoData); } - sampleRead = true; - resetSample(); - } - - private void resetSample() { - sampleBytesRead = 0; - sampleBytesWritten = 0; - sampleCurrentNalBytesRemaining = 0; - sampleEncodingHandled = false; - sampleSignalByteRead = false; - samplePartitionCountRead = false; - samplePartitionCount = 0; - sampleSignalByte = (byte) 0; - sampleInitializationVectorRead = false; - sampleStrippedBytes.reset(); + haveOutputSample = true; } /** * Ensures {@link #scratch} contains at least {@code requiredLength} bytes of data, reading from * the extractor input if necessary. */ - private void readScratch(ExtractorInput input, int requiredLength) - throws IOException, InterruptedException { + private void readScratch(ExtractorInput input, int requiredLength) throws IOException { if (scratch.limit() >= requiredLength) { return; } @@ -1276,14 +1307,22 @@ public class MatroskaExtractor implements Extractor { scratch.setLimit(requiredLength); } - private void writeSampleData(ExtractorInput input, Track track, int size) - throws IOException, InterruptedException { + /** + * Writes data for a single sample to the track output. + * + * @param input The input from which to read sample data. + * @param track The track to output the sample to. + * @param size The size of the sample data on the input side. + * @return The final size of the written sample. + * @throws IOException If an error occurs reading from the input. + */ + private int writeSampleData(ExtractorInput input, Track track, int size) throws IOException { if (CODEC_ID_SUBRIP.equals(track.codecId)) { writeSubtitleSampleData(input, SUBRIP_PREFIX, size); - return; + return finishWriteSampleData(); } else if (CODEC_ID_ASS.equals(track.codecId)) { writeSubtitleSampleData(input, SSA_PREFIX, size); - return; + return finishWriteSampleData(); } TrackOutput output = track.output; @@ -1312,11 +1351,14 @@ public class MatroskaExtractor implements Extractor { // Write the signal byte, containing the IV size and the subsample encryption flag. scratch.data[0] = (byte) (ENCRYPTION_IV_SIZE | (hasSubsampleEncryption ? 0x80 : 0x00)); scratch.setPosition(0); - output.sampleData(scratch, 1); + output.sampleData(scratch, 1, TrackOutput.SAMPLE_DATA_PART_ENCRYPTION); sampleBytesWritten++; // Write the IV. encryptionInitializationVector.setPosition(0); - output.sampleData(encryptionInitializationVector, ENCRYPTION_IV_SIZE); + output.sampleData( + encryptionInitializationVector, + ENCRYPTION_IV_SIZE, + TrackOutput.SAMPLE_DATA_PART_ENCRYPTION); sampleBytesWritten += ENCRYPTION_IV_SIZE; } if (hasSubsampleEncryption) { @@ -1364,7 +1406,10 @@ public class MatroskaExtractor implements Extractor { encryptionSubsampleDataBuffer.putInt(0); } encryptionSubsampleData.reset(encryptionSubsampleDataBuffer.array(), subsampleDataSize); - output.sampleData(encryptionSubsampleData, subsampleDataSize); + output.sampleData( + encryptionSubsampleData, + subsampleDataSize, + TrackOutput.SAMPLE_DATA_PART_ENCRYPTION); sampleBytesWritten += subsampleDataSize; } } @@ -1375,7 +1420,7 @@ public class MatroskaExtractor implements Extractor { if (track.maxBlockAdditionId > 0) { blockFlags |= C.BUFFER_FLAG_HAS_SUPPLEMENTAL_DATA; - blockAddData.reset(); + blockAdditionalData.reset(); // If there is supplemental data, the structure of the sample data is: // sample size (4 bytes) || sample data || supplemental data scratch.reset(/* limit= */ 4); @@ -1383,7 +1428,7 @@ public class MatroskaExtractor implements Extractor { scratch.data[1] = (byte) ((size >> 16) & 0xFF); scratch.data[2] = (byte) ((size >> 8) & 0xFF); scratch.data[3] = (byte) (size & 0xFF); - output.sampleData(scratch, 4); + output.sampleData(scratch, 4, TrackOutput.SAMPLE_DATA_PART_SUPPLEMENTAL); sampleBytesWritten += 4; } @@ -1408,8 +1453,9 @@ public class MatroskaExtractor implements Extractor { while (sampleBytesRead < size) { if (sampleCurrentNalBytesRemaining == 0) { // Read the NAL length so that we know where we find the next one. - readToTarget(input, nalLengthData, nalUnitLengthFieldLengthDiff, - nalUnitLengthFieldLength); + writeToTarget( + input, nalLengthData, nalUnitLengthFieldLengthDiff, nalUnitLengthFieldLength); + sampleBytesRead += nalUnitLengthFieldLength; nalLength.setPosition(0); sampleCurrentNalBytesRemaining = nalLength.readUnsignedIntToInt(); // Write a start code for the current NAL unit. @@ -1418,17 +1464,21 @@ public class MatroskaExtractor implements Extractor { sampleBytesWritten += 4; } else { // Write the payload of the NAL unit. - sampleCurrentNalBytesRemaining -= - readToOutput(input, output, sampleCurrentNalBytesRemaining); + int bytesWritten = writeToOutput(input, output, sampleCurrentNalBytesRemaining); + sampleBytesRead += bytesWritten; + sampleBytesWritten += bytesWritten; + sampleCurrentNalBytesRemaining -= bytesWritten; } } } else { if (track.trueHdSampleRechunker != null) { Assertions.checkState(sampleStrippedBytes.limit() == 0); - track.trueHdSampleRechunker.startSample(input, blockFlags, size); + track.trueHdSampleRechunker.startSample(input); } while (sampleBytesRead < size) { - readToOutput(input, output, size - sampleBytesRead); + int bytesWritten = writeToOutput(input, output, size - sampleBytesRead); + sampleBytesRead += bytesWritten; + sampleBytesWritten += bytesWritten; } } @@ -1443,10 +1493,36 @@ public class MatroskaExtractor implements Extractor { output.sampleData(vorbisNumPageSamples, 4); sampleBytesWritten += 4; } + + return finishWriteSampleData(); + } + + /** + * Called by {@link #writeSampleData(ExtractorInput, Track, int)} when the sample has been + * written. Returns the final sample size and resets state for the next sample. + */ + private int finishWriteSampleData() { + int sampleSize = sampleBytesWritten; + resetWriteSampleData(); + return sampleSize; + } + + /** Resets state used by {@link #writeSampleData(ExtractorInput, Track, int)}. */ + private void resetWriteSampleData() { + sampleBytesRead = 0; + sampleBytesWritten = 0; + sampleCurrentNalBytesRemaining = 0; + sampleEncodingHandled = false; + sampleSignalByteRead = false; + samplePartitionCountRead = false; + samplePartitionCount = 0; + sampleSignalByte = (byte) 0; + sampleInitializationVectorRead = false; + sampleStrippedBytes.reset(); } private void writeSubtitleSampleData(ExtractorInput input, byte[] samplePrefix, int size) - throws IOException, InterruptedException { + throws IOException { int sizeWithPrefix = samplePrefix.length + size; if (subtitleSample.capacity() < sizeWithPrefix) { // Initialize subripSample to contain the required prefix and have space to hold a subtitle @@ -1510,8 +1586,9 @@ public class MatroskaExtractor implements Extractor { int seconds = (int) (timeUs / C.MICROS_PER_SECOND); timeUs -= (seconds * C.MICROS_PER_SECOND); int lastValue = (int) (timeUs / lastTimecodeValueScalingFactor); - timeCodeData = Util.getUtf8Bytes(String.format(Locale.US, timecodeFormat, hours, minutes, - seconds, lastValue)); + timeCodeData = + Util.getUtf8Bytes( + String.format(Locale.US, timecodeFormat, hours, minutes, seconds, lastValue)); return timeCodeData; } @@ -1519,33 +1596,30 @@ public class MatroskaExtractor implements Extractor { * Writes {@code length} bytes of sample data into {@code target} at {@code offset}, consisting of * pending {@link #sampleStrippedBytes} and any remaining data read from {@code input}. */ - private void readToTarget(ExtractorInput input, byte[] target, int offset, int length) - throws IOException, InterruptedException { + private void writeToTarget(ExtractorInput input, byte[] target, int offset, int length) + throws IOException { int pendingStrippedBytes = Math.min(length, sampleStrippedBytes.bytesLeft()); input.readFully(target, offset + pendingStrippedBytes, length - pendingStrippedBytes); if (pendingStrippedBytes > 0) { sampleStrippedBytes.readBytes(target, offset, pendingStrippedBytes); } - sampleBytesRead += length; } /** * Outputs up to {@code length} bytes of sample data to {@code output}, consisting of either * {@link #sampleStrippedBytes} or data read from {@code input}. */ - private int readToOutput(ExtractorInput input, TrackOutput output, int length) - throws IOException, InterruptedException { - int bytesRead; + private int writeToOutput(ExtractorInput input, TrackOutput output, int length) + throws IOException { + int bytesWritten; int strippedBytesLeft = sampleStrippedBytes.bytesLeft(); if (strippedBytesLeft > 0) { - bytesRead = Math.min(length, strippedBytesLeft); - output.sampleData(sampleStrippedBytes, bytesRead); + bytesWritten = Math.min(length, strippedBytesLeft); + output.sampleData(sampleStrippedBytes, bytesWritten); } else { - bytesRead = output.sampleData(input, length, false); + bytesWritten = output.sampleData(input, length, false); } - sampleBytesRead += bytesRead; - sampleBytesWritten += bytesRead; - return bytesRead; + return bytesWritten; } /** @@ -1579,6 +1653,16 @@ public class MatroskaExtractor implements Extractor { sizes[cuePointsSize - 1] = (int) (segmentContentPosition + segmentContentSize - offsets[cuePointsSize - 1]); durationsUs[cuePointsSize - 1] = durationUs - timesUs[cuePointsSize - 1]; + + long lastDurationUs = durationsUs[cuePointsSize - 1]; + if (lastDurationUs <= 0) { + Log.w(TAG, "Discarding last cue point with unexpected duration: " + lastDurationUs); + sizes = Arrays.copyOf(sizes, sizes.length - 1); + offsets = Arrays.copyOf(offsets, offsets.length - 1); + durationsUs = Arrays.copyOf(durationsUs, durationsUs.length - 1); + timesUs = Arrays.copyOf(timesUs, timesUs.length - 1); + } + cueTimesUs = null; cueClusterPositions = null; return new ChunkIndex(sizes, offsets, durationsUs, timesUs); @@ -1706,8 +1790,7 @@ public class MatroskaExtractor implements Extractor { } @Override - public void binaryElement(int id, int contentsSize, ExtractorInput input) - throws IOException, InterruptedException { + public void binaryElement(int id, int contentsSize, ExtractorInput input) throws IOException { MatroskaExtractor.this.binaryElement(id, contentsSize, input); } } @@ -1720,10 +1803,11 @@ public class MatroskaExtractor implements Extractor { private final byte[] syncframePrefix; private boolean foundSyncframe; - private int sampleCount; + private int chunkSampleCount; + private long chunkTimeUs; + private @C.BufferFlags int chunkFlags; private int chunkSize; - private long timeUs; - private @C.BufferFlags int blockFlags; + private int chunkOffset; public TrueHdSampleRechunker() { syncframePrefix = new byte[Ac3Util.TRUEHD_SYNCFRAME_PREFIX_LENGTH]; @@ -1731,47 +1815,44 @@ public class MatroskaExtractor implements Extractor { public void reset() { foundSyncframe = false; + chunkSampleCount = 0; } - public void startSample(ExtractorInput input, @C.BufferFlags int blockFlags, int size) - throws IOException, InterruptedException { - if (!foundSyncframe) { - input.peekFully(syncframePrefix, 0, Ac3Util.TRUEHD_SYNCFRAME_PREFIX_LENGTH); - input.resetPeekPosition(); - if (Ac3Util.parseTrueHdSyncframeAudioSampleCount(syncframePrefix) == 0) { - return; - } - foundSyncframe = true; - sampleCount = 0; + public void startSample(ExtractorInput input) throws IOException { + if (foundSyncframe) { + return; } - if (sampleCount == 0) { - // This is the first sample in the chunk, so reset the block flags and chunk size. - this.blockFlags = blockFlags; + input.peekFully(syncframePrefix, 0, Ac3Util.TRUEHD_SYNCFRAME_PREFIX_LENGTH); + input.resetPeekPosition(); + if (Ac3Util.parseTrueHdSyncframeAudioSampleCount(syncframePrefix) == 0) { + return; + } + foundSyncframe = true; + } + + public void sampleMetadata( + Track track, long timeUs, @C.BufferFlags int flags, int size, int offset) { + if (!foundSyncframe) { + return; + } + if (chunkSampleCount++ == 0) { + // This is the first sample in the chunk. + chunkTimeUs = timeUs; + chunkFlags = flags; chunkSize = 0; } chunkSize += size; - } - - public void sampleMetadata(Track track, long timeUs) { - if (!foundSyncframe) { - return; + chunkOffset = offset; // The offset is to the end of the sample. + if (chunkSampleCount >= Ac3Util.TRUEHD_RECHUNK_SAMPLE_COUNT) { + outputPendingSampleMetadata(track); } - if (sampleCount++ == 0) { - // This is the first sample in the chunk, so update the timestamp. - this.timeUs = timeUs; - } - if (sampleCount < Ac3Util.TRUEHD_RECHUNK_SAMPLE_COUNT) { - // We haven't read enough samples to output a chunk. - return; - } - track.output.sampleMetadata(this.timeUs, blockFlags, chunkSize, 0, track.cryptoData); - sampleCount = 0; } public void outputPendingSampleMetadata(Track track) { - if (foundSyncframe && sampleCount > 0) { - track.output.sampleMetadata(this.timeUs, blockFlags, chunkSize, 0, track.cryptoData); - sampleCount = 0; + if (chunkSampleCount > 0) { + track.output.sampleMetadata( + chunkTimeUs, chunkFlags, chunkSize, chunkOffset, track.cryptoData); + chunkSampleCount = 0; } } } @@ -1858,7 +1939,7 @@ public class MatroskaExtractor implements Extractor { String mimeType; int maxInputSize = Format.NO_VALUE; @C.PcmEncoding int pcmEncoding = Format.NO_VALUE; - List initializationData = null; + @Nullable List initializationData = null; switch (codecId) { case CODEC_ID_VP8: mimeType = MimeTypes.VIDEO_VP8; @@ -1892,7 +1973,8 @@ public class MatroskaExtractor implements Extractor { nalUnitLengthFieldLength = hevcConfig.nalUnitLengthFieldLength; break; case CODEC_ID_FOURCC: - Pair> pair = parseFourCcPrivate(new ParsableByteArray(codecPrivate)); + Pair> pair = + parseFourCcPrivate(new ParsableByteArray(codecPrivate)); mimeType = pair.first; initializationData = pair.second; break; @@ -1922,11 +2004,11 @@ public class MatroskaExtractor implements Extractor { break; case CODEC_ID_MP2: mimeType = MimeTypes.AUDIO_MPEG_L2; - maxInputSize = MpegAudioHeader.MAX_FRAME_SIZE_BYTES; + maxInputSize = MpegAudioUtil.MAX_FRAME_SIZE_BYTES; break; case CODEC_ID_MP3: mimeType = MimeTypes.AUDIO_MPEG; - maxInputSize = MpegAudioHeader.MAX_FRAME_SIZE_BYTES; + maxInputSize = MpegAudioUtil.MAX_FRAME_SIZE_BYTES; break; case CODEC_ID_AC3: mimeType = MimeTypes.AUDIO_AC3; @@ -1997,18 +2079,20 @@ public class MatroskaExtractor implements Extractor { throw new ParserException("Unrecognized codec identifier."); } - int type; - Format format; @C.SelectionFlags int selectionFlags = 0; selectionFlags |= flagDefault ? C.SELECTION_FLAG_DEFAULT : 0; selectionFlags |= flagForced ? C.SELECTION_FLAG_FORCED : 0; + + int type; + Format.Builder formatBuilder = new Format.Builder(); // TODO: Consider reading the name elements of the tracks and, if present, incorporating them // into the trackId passed when creating the formats. if (MimeTypes.isAudio(mimeType)) { type = C.TRACK_TYPE_AUDIO; - format = Format.createAudioSampleFormat(Integer.toString(trackId), mimeType, null, - Format.NO_VALUE, maxInputSize, channelCount, sampleRate, pcmEncoding, - initializationData, drmInitData, selectionFlags, language); + formatBuilder + .setChannelCount(channelCount) + .setSampleRate(sampleRate) + .setPcmEncoding(pcmEncoding); } else if (MimeTypes.isVideo(mimeType)) { type = C.TRACK_TYPE_VIDEO; if (displayUnit == Track.DISPLAY_UNIT_PIXELS) { @@ -2019,21 +2103,15 @@ public class MatroskaExtractor implements Extractor { if (displayWidth != Format.NO_VALUE && displayHeight != Format.NO_VALUE) { pixelWidthHeightRatio = ((float) (height * displayWidth)) / (width * displayHeight); } - ColorInfo colorInfo = null; + @Nullable ColorInfo colorInfo = null; if (hasColorInfo) { - byte[] hdrStaticInfo = getHdrStaticInfo(); + @Nullable byte[] hdrStaticInfo = getHdrStaticInfo(); colorInfo = new ColorInfo(colorSpace, colorRange, colorTransfer, hdrStaticInfo); } int rotationDegrees = Format.NO_VALUE; - // Some HTC devices signal rotation in track names. - if ("htc_video_rotA-000".equals(name)) { - rotationDegrees = 0; - } else if ("htc_video_rotA-090".equals(name)) { - rotationDegrees = 90; - } else if ("htc_video_rotA-180".equals(name)) { - rotationDegrees = 180; - } else if ("htc_video_rotA-270".equals(name)) { - rotationDegrees = 270; + + if (TRACK_NAME_TO_ROTATION_DEGREES.containsKey(name)) { + rotationDegrees = TRACK_NAME_TO_ROTATION_DEGREES.get(name); } if (projectionType == C.PROJECTION_RECTANGULAR && Float.compare(projectionPoseYaw, 0f) == 0 @@ -2050,53 +2128,44 @@ public class MatroskaExtractor implements Extractor { rotationDegrees = 270; } } - format = - Format.createVideoSampleFormat( - Integer.toString(trackId), - mimeType, - /* codecs= */ null, - /* bitrate= */ Format.NO_VALUE, - maxInputSize, - width, - height, - /* frameRate= */ Format.NO_VALUE, - initializationData, - rotationDegrees, - pixelWidthHeightRatio, - projectionData, - stereoMode, - colorInfo, - drmInitData); + formatBuilder + .setWidth(width) + .setHeight(height) + .setPixelWidthHeightRatio(pixelWidthHeightRatio) + .setRotationDegrees(rotationDegrees) + .setProjectionData(projectionData) + .setStereoMode(stereoMode) + .setColorInfo(colorInfo); } else if (MimeTypes.APPLICATION_SUBRIP.equals(mimeType)) { type = C.TRACK_TYPE_TEXT; - format = Format.createTextSampleFormat(Integer.toString(trackId), mimeType, selectionFlags, - language, drmInitData); } else if (MimeTypes.TEXT_SSA.equals(mimeType)) { type = C.TRACK_TYPE_TEXT; initializationData = new ArrayList<>(2); initializationData.add(SSA_DIALOGUE_FORMAT); initializationData.add(codecPrivate); - format = Format.createTextSampleFormat(Integer.toString(trackId), mimeType, null, - Format.NO_VALUE, selectionFlags, language, Format.NO_VALUE, drmInitData, - Format.OFFSET_SAMPLE_RELATIVE, initializationData); } else if (MimeTypes.APPLICATION_VOBSUB.equals(mimeType) || MimeTypes.APPLICATION_PGS.equals(mimeType) || MimeTypes.APPLICATION_DVBSUBS.equals(mimeType)) { type = C.TRACK_TYPE_TEXT; - format = - Format.createImageSampleFormat( - Integer.toString(trackId), - mimeType, - null, - Format.NO_VALUE, - selectionFlags, - initializationData, - language, - drmInitData); } else { throw new ParserException("Unexpected MIME type."); } + if (!TRACK_NAME_TO_ROTATION_DEGREES.containsKey(name)) { + formatBuilder.setLabel(name); + } + + Format format = + formatBuilder + .setId(trackId) + .setSampleMimeType(mimeType) + .setMaxInputSize(maxInputSize) + .setLanguage(language) + .setSelectionFlags(selectionFlags) + .setInitializationData(initializationData) + .setDrmInitData(drmInitData) + .build(); + this.output = output.track(number, type); this.output.format(format); } @@ -2154,8 +2223,8 @@ public class MatroskaExtractor implements Extractor { * is {@code null}. * @throws ParserException If the initialization data could not be built. */ - private static Pair> parseFourCcPrivate(ParsableByteArray buffer) - throws ParserException { + private static Pair> parseFourCcPrivate( + ParsableByteArray buffer) throws ParserException { try { buffer.skipBytes(16); // size(4), width(4), height(4), planes(2), bitcount(2). long compression = buffer.readLittleEndianUnsignedInt(); @@ -2261,7 +2330,5 @@ public class MatroskaExtractor implements Extractor { throw new ParserException("Error parsing MS/ACM codec private"); } } - } - } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/Sniffer.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mkv/Sniffer.java similarity index 92% rename from library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/Sniffer.java rename to library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mkv/Sniffer.java index 62c9404916..d380fa47c7 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/Sniffer.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mkv/Sniffer.java @@ -39,10 +39,8 @@ import java.io.IOException; scratch = new ParsableByteArray(8); } - /** - * @see com.google.android.exoplayer2.extractor.Extractor#sniff(ExtractorInput) - */ - public boolean sniff(ExtractorInput input) throws IOException, InterruptedException { + /** @see com.google.android.exoplayer2.extractor.Extractor#sniff(ExtractorInput) */ + public boolean sniff(ExtractorInput input) throws IOException { long inputLength = input.getLength(); int bytesToSearch = (int) (inputLength == C.LENGTH_UNSET || inputLength > SEARCH_LENGTH ? SEARCH_LENGTH : inputLength); @@ -86,10 +84,8 @@ import java.io.IOException; return peekLength == headerStart + headerSize; } - /** - * Peeks a variable-length unsigned EBML integer from the input. - */ - private long readUint(ExtractorInput input) throws IOException, InterruptedException { + /** Peeks a variable-length unsigned EBML integer from the input. */ + private long readUint(ExtractorInput input) throws IOException { input.peekFully(scratch.data, 0, 1); int value = scratch.data[0] & 0xFF; if (value == 0) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/VarintReader.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mkv/VarintReader.java similarity index 87% rename from library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/VarintReader.java rename to library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mkv/VarintReader.java index a94a5ec216..6b244de72d 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/VarintReader.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mkv/VarintReader.java @@ -56,15 +56,15 @@ import java.io.IOException; } /** - * Reads an EBML variable-length integer (varint) from an {@link ExtractorInput} such that - * reading can be resumed later if an error occurs having read only some of it. - *

      - * If an value is successfully read, then the reader will automatically reset itself ready to + * Reads an EBML variable-length integer (varint) from an {@link ExtractorInput} such that reading + * can be resumed later if an error occurs having read only some of it. + * + *

      If an value is successfully read, then the reader will automatically reset itself ready to * read another value. - *

      - * If an {@link IOException} or {@link InterruptedException} is throw, the read can be resumed - * later by calling this method again, passing an {@link ExtractorInput} providing data starting - * where the previous one left off. + * + *

      If an {@link IOException} is thrown, the read can be resumed later by calling this method + * again, passing an {@link ExtractorInput} providing data starting where the previous one left + * off. * * @param input The {@link ExtractorInput} from which the integer should be read. * @param allowEndOfInput True if encountering the end of the input having read no data is @@ -76,10 +76,13 @@ import java.io.IOException; * and the end of the input was encountered, or {@link C#RESULT_MAX_LENGTH_EXCEEDED} if the * length of the varint exceeded maximumAllowedLength. * @throws IOException If an error occurs reading from the input. - * @throws InterruptedException If the thread is interrupted. */ - public long readUnsignedVarint(ExtractorInput input, boolean allowEndOfInput, - boolean removeLengthMask, int maximumAllowedLength) throws IOException, InterruptedException { + public long readUnsignedVarint( + ExtractorInput input, + boolean allowEndOfInput, + boolean removeLengthMask, + int maximumAllowedLength) + throws IOException { if (state == STATE_BEGIN_READING) { // Read the first byte to establish the length. if (!input.readFully(scratch, 0, 1, allowEndOfInput)) { diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mkv/package-info.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mkv/package-info.java new file mode 100644 index 0000000000..15629ba584 --- /dev/null +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mkv/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2019 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. + */ +@NonNullApi +package com.google.android.exoplayer2.extractor.mkv; + +import com.google.android.exoplayer2.util.NonNullApi; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/ConstantBitrateSeeker.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp3/ConstantBitrateSeeker.java similarity index 81% rename from library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/ConstantBitrateSeeker.java rename to library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp3/ConstantBitrateSeeker.java index 4a5feb5096..f74c3fbc33 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/ConstantBitrateSeeker.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp3/ConstantBitrateSeeker.java @@ -16,8 +16,8 @@ package com.google.android.exoplayer2.extractor.mp3; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.audio.MpegAudioUtil; import com.google.android.exoplayer2.extractor.ConstantBitrateSeekMap; -import com.google.android.exoplayer2.extractor.MpegAudioHeader; /** * MP3 seeker that doesn't rely on metadata and seeks assuming the source has a constant bitrate. @@ -30,7 +30,10 @@ import com.google.android.exoplayer2.extractor.MpegAudioHeader; * @param mpegAudioHeader The MPEG audio header associated with the first frame. */ public ConstantBitrateSeeker( - long inputLength, long firstFramePosition, MpegAudioHeader mpegAudioHeader) { + long inputLength, long firstFramePosition, MpegAudioUtil.Header mpegAudioHeader) { + // Set the seeker frame size to the size of the first frame (even though some constant bitrate + // streams have variable frame sizes) to avoid the need to re-synchronize for constant frame + // size streams. super(inputLength, firstFramePosition, mpegAudioHeader.bitrate, mpegAudioHeader.frameSize); } diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp3/IndexSeeker.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp3/IndexSeeker.java new file mode 100644 index 0000000000..f8c63ff8e2 --- /dev/null +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp3/IndexSeeker.java @@ -0,0 +1,112 @@ +/* + * Copyright (C) 2020 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.mp3; + +import androidx.annotation.VisibleForTesting; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.extractor.SeekPoint; +import com.google.android.exoplayer2.util.LongArray; +import com.google.android.exoplayer2.util.Util; + +/** MP3 seeker that builds a time-to-byte mapping as the stream is read. */ +/* package */ final class IndexSeeker implements Seeker { + + @VisibleForTesting + /* package */ static final long MIN_TIME_BETWEEN_POINTS_US = C.MICROS_PER_SECOND / 10; + + private final long dataEndPosition; + private final LongArray timesUs; + private final LongArray positions; + + private long durationUs; + + public IndexSeeker(long durationUs, long dataStartPosition, long dataEndPosition) { + this.durationUs = durationUs; + this.dataEndPosition = dataEndPosition; + timesUs = new LongArray(); + positions = new LongArray(); + timesUs.add(0L); + positions.add(dataStartPosition); + } + + @Override + public long getTimeUs(long position) { + int targetIndex = + Util.binarySearchFloor( + positions, position, /* inclusive= */ true, /* stayInBounds= */ true); + return timesUs.get(targetIndex); + } + + @Override + public long getDataEndPosition() { + return dataEndPosition; + } + + @Override + public boolean isSeekable() { + return true; + } + + @Override + public long getDurationUs() { + return durationUs; + } + + @Override + public SeekPoints getSeekPoints(long timeUs) { + int targetIndex = + Util.binarySearchFloor(timesUs, timeUs, /* inclusive= */ true, /* stayInBounds= */ true); + SeekPoint seekPoint = new SeekPoint(timesUs.get(targetIndex), positions.get(targetIndex)); + if (seekPoint.timeUs >= timeUs || targetIndex == timesUs.size() - 1) { + return new SeekPoints(seekPoint); + } else { + SeekPoint nextSeekPoint = + new SeekPoint(timesUs.get(targetIndex + 1), positions.get(targetIndex + 1)); + return new SeekPoints(seekPoint, nextSeekPoint); + } + } + + /** + * Adds a seek point to the index if it is sufficiently distant from the other points. + * + *

      Seek points must be added in order. + * + * @param timeUs The time corresponding to the seek point to add in microseconds. + * @param position The position corresponding to the seek point to add in bytes. + */ + public void maybeAddSeekPoint(long timeUs, long position) { + if (isTimeUsInIndex(timeUs)) { + return; + } + timesUs.add(timeUs); + positions.add(position); + } + + /** + * Returns whether {@code timeUs} (in microseconds) is included in the index. + * + *

      A point is included in the index if it is equal to another point, between 2 points, or + * sufficiently close to the last point. + */ + public boolean isTimeUsInIndex(long timeUs) { + long lastIndexedTimeUs = timesUs.get(timesUs.size() - 1); + return timeUs - lastIndexedTimeUs < MIN_TIME_BETWEEN_POINTS_US; + } + + /* package */ void setDurationUs(long durationUs) { + this.durationUs = durationUs; + } +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/MlltSeeker.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp3/MlltSeeker.java similarity index 100% rename from library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/MlltSeeker.java rename to library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp3/MlltSeeker.java diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java similarity index 67% rename from library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java rename to library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java index 7a25677c55..b9613f38f5 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java @@ -20,13 +20,14 @@ import androidx.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.audio.MpegAudioUtil; +import com.google.android.exoplayer2.extractor.DummyTrackOutput; import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.ExtractorsFactory; import com.google.android.exoplayer2.extractor.GaplessInfoHolder; import com.google.android.exoplayer2.extractor.Id3Peeker; -import com.google.android.exoplayer2.extractor.MpegAudioHeader; import com.google.android.exoplayer2.extractor.PositionHolder; import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.extractor.mp3.Seeker.UnseekableSeeker; @@ -34,12 +35,17 @@ import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.id3.Id3Decoder; import com.google.android.exoplayer2.metadata.id3.Id3Decoder.FramePredicate; import com.google.android.exoplayer2.metadata.id3.MlltFrame; +import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.ParsableByteArray; +import com.google.android.exoplayer2.util.Util; import java.io.EOFException; import java.io.IOException; import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import org.checkerframework.checker.nullness.qual.EnsuresNonNull; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.checkerframework.checker.nullness.qual.RequiresNonNull; /** * Extracts data from the MP3 container format. @@ -51,24 +57,44 @@ public final class Mp3Extractor implements Extractor { /** * Flags controlling the behavior of the extractor. Possible flag values are {@link - * #FLAG_ENABLE_CONSTANT_BITRATE_SEEKING} and {@link #FLAG_DISABLE_ID3_METADATA}. + * #FLAG_ENABLE_CONSTANT_BITRATE_SEEKING}, {@link #FLAG_ENABLE_INDEX_SEEKING} and {@link + * #FLAG_DISABLE_ID3_METADATA}. */ @Documented @Retention(RetentionPolicy.SOURCE) @IntDef( flag = true, - value = {FLAG_ENABLE_CONSTANT_BITRATE_SEEKING, FLAG_DISABLE_ID3_METADATA}) + value = { + FLAG_ENABLE_CONSTANT_BITRATE_SEEKING, + FLAG_ENABLE_INDEX_SEEKING, + FLAG_DISABLE_ID3_METADATA + }) public @interface Flags {} /** * Flag to force enable seeking using a constant bitrate assumption in cases where seeking would * otherwise not be possible. + * + *

      This flag is ignored if {@link #FLAG_ENABLE_INDEX_SEEKING} is set. */ public static final int FLAG_ENABLE_CONSTANT_BITRATE_SEEKING = 1; + /** + * Flag to force index seeking, in which a time-to-byte mapping is built as the file is read. + * + *

      This seeker may require to scan a significant portion of the file to compute a seek point. + * Therefore, it should only be used if one of the following is true: + * + *

        + *
      • The file is small. + *
      • The bitrate is variable (or it's unknown whether it's variable) and the file does not + * provide precise enough seeking metadata. + *
      + */ + public static final int FLAG_ENABLE_INDEX_SEEKING = 1 << 1; /** * Flag to disable parsing of ID3 metadata. Can be set to save memory if ID3 metadata is not * required. */ - public static final int FLAG_DISABLE_ID3_METADATA = 2; + public static final int FLAG_DISABLE_ID3_METADATA = 1 << 2; /** Predicate that matches ID3 frames containing only required gapless/seeking metadata. */ private static final FramePredicate REQUIRED_ID3_FRAME_PREDICATE = @@ -102,24 +128,28 @@ public final class Mp3Extractor implements Extractor { @Flags private final int flags; private final long forcedFirstSampleTimestampUs; private final ParsableByteArray scratch; - private final MpegAudioHeader synchronizedHeader; + private final MpegAudioUtil.Header synchronizedHeader; private final GaplessInfoHolder gaplessInfoHolder; private final Id3Peeker id3Peeker; + private final TrackOutput skippingTrackOutput; - // Extractor outputs. - private ExtractorOutput extractorOutput; - private TrackOutput trackOutput; + private @MonotonicNonNull ExtractorOutput extractorOutput; + private @MonotonicNonNull TrackOutput realTrackOutput; + private TrackOutput currentTrackOutput; // skippingTrackOutput or realTrackOutput. private int synchronizedHeaderData; - private Metadata metadata; - @Nullable private Seeker seeker; - private boolean disableSeeking; + @Nullable private Metadata metadata; private long basisTimeUs; private long samplesRead; private long firstSamplePosition; private int sampleBytesRemaining; + private @MonotonicNonNull Seeker seeker; + private boolean disableSeeking; + private boolean isSeekInProgress; + private long seekTimeUs; + public Mp3Extractor() { this(0); } @@ -140,23 +170,26 @@ public final class Mp3Extractor implements Extractor { this.flags = flags; this.forcedFirstSampleTimestampUs = forcedFirstSampleTimestampUs; scratch = new ParsableByteArray(SCRATCH_LENGTH); - synchronizedHeader = new MpegAudioHeader(); + synchronizedHeader = new MpegAudioUtil.Header(); gaplessInfoHolder = new GaplessInfoHolder(); basisTimeUs = C.TIME_UNSET; id3Peeker = new Id3Peeker(); + skippingTrackOutput = new DummyTrackOutput(); + currentTrackOutput = skippingTrackOutput; } // Extractor implementation. @Override - public boolean sniff(ExtractorInput input) throws IOException, InterruptedException { + public boolean sniff(ExtractorInput input) throws IOException { return synchronize(input, true); } @Override public void init(ExtractorOutput output) { extractorOutput = output; - trackOutput = extractorOutput.track(0, C.TRACK_TYPE_AUDIO); + realTrackOutput = extractorOutput.track(0, C.TRACK_TYPE_AUDIO); + currentTrackOutput = realTrackOutput; extractorOutput.endTracks(); } @@ -166,6 +199,11 @@ public final class Mp3Extractor implements Extractor { basisTimeUs = C.TIME_UNSET; samplesRead = 0; sampleBytesRemaining = 0; + seekTimeUs = timeUs; + if (seeker instanceof IndexSeeker && !((IndexSeeker) seeker).isTimeUsInIndex(timeUs)) { + isSeekInProgress = true; + currentTrackOutput = skippingTrackOutput; + } } @Override @@ -174,61 +212,18 @@ public final class Mp3Extractor implements Extractor { } @Override - public int read(ExtractorInput input, PositionHolder seekPosition) - throws IOException, InterruptedException { - if (synchronizedHeaderData == 0) { - try { - synchronize(input, false); - } catch (EOFException e) { - return RESULT_END_OF_INPUT; + public int read(ExtractorInput input, PositionHolder seekPosition) throws IOException { + assertInitialized(); + int readResult = readInternal(input); + if (readResult == RESULT_END_OF_INPUT && seeker instanceof IndexSeeker) { + // Duration is exact when index seeker is used. + long durationUs = computeTimeUs(samplesRead); + if (seeker.getDurationUs() != durationUs) { + ((IndexSeeker) seeker).setDurationUs(durationUs); + extractorOutput.seekMap(seeker); } } - if (seeker == null) { - // Read past any seek frame and set the seeker based on metadata or a seek frame. Metadata - // takes priority as it can provide greater precision. - Seeker seekFrameSeeker = maybeReadSeekFrame(input); - Seeker metadataSeeker = maybeHandleSeekMetadata(metadata, input.getPosition()); - - if (disableSeeking) { - seeker = new UnseekableSeeker(); - } else { - if (metadataSeeker != null) { - seeker = metadataSeeker; - } else if (seekFrameSeeker != null) { - seeker = seekFrameSeeker; - } - if (seeker == null - || (!seeker.isSeekable() && (flags & FLAG_ENABLE_CONSTANT_BITRATE_SEEKING) != 0)) { - seeker = getConstantBitrateSeeker(input); - } - } - extractorOutput.seekMap(seeker); - trackOutput.format( - Format.createAudioSampleFormat( - /* id= */ null, - synchronizedHeader.mimeType, - /* codecs= */ null, - /* bitrate= */ Format.NO_VALUE, - MpegAudioHeader.MAX_FRAME_SIZE_BYTES, - synchronizedHeader.channels, - synchronizedHeader.sampleRate, - /* pcmEncoding= */ Format.NO_VALUE, - gaplessInfoHolder.encoderDelay, - gaplessInfoHolder.encoderPadding, - /* initializationData= */ null, - /* drmInitData= */ null, - /* selectionFlags= */ 0, - /* language= */ null, - (flags & FLAG_DISABLE_ID3_METADATA) != 0 ? null : metadata)); - firstSamplePosition = input.getPosition(); - } else if (firstSamplePosition != 0) { - long inputPosition = input.getPosition(); - if (inputPosition < firstSamplePosition) { - // Skip past the seek frame. - input.skipFully((int) (firstSamplePosition - inputPosition)); - } - } - return readSample(input); + return readResult; } /** @@ -242,7 +237,41 @@ public final class Mp3Extractor implements Extractor { // Internal methods. - private int readSample(ExtractorInput extractorInput) throws IOException, InterruptedException { + @RequiresNonNull({"extractorOutput", "realTrackOutput"}) + private int readInternal(ExtractorInput input) throws IOException { + if (synchronizedHeaderData == 0) { + try { + synchronize(input, false); + } catch (EOFException e) { + return RESULT_END_OF_INPUT; + } + } + if (seeker == null) { + seeker = computeSeeker(input); + extractorOutput.seekMap(seeker); + currentTrackOutput.format( + new Format.Builder() + .setSampleMimeType(synchronizedHeader.mimeType) + .setMaxInputSize(MpegAudioUtil.MAX_FRAME_SIZE_BYTES) + .setChannelCount(synchronizedHeader.channels) + .setSampleRate(synchronizedHeader.sampleRate) + .setEncoderDelay(gaplessInfoHolder.encoderDelay) + .setEncoderPadding(gaplessInfoHolder.encoderPadding) + .setMetadata((flags & FLAG_DISABLE_ID3_METADATA) != 0 ? null : metadata) + .build()); + firstSamplePosition = input.getPosition(); + } else if (firstSamplePosition != 0) { + long inputPosition = input.getPosition(); + if (inputPosition < firstSamplePosition) { + // Skip past the seek frame. + input.skipFully((int) (firstSamplePosition - inputPosition)); + } + } + return readSample(input); + } + + @RequiresNonNull({"realTrackOutput", "seeker"}) + private int readSample(ExtractorInput extractorInput) throws IOException { if (sampleBytesRemaining == 0) { extractorInput.resetPeekPosition(); if (peekEndOfStreamOrHeader(extractorInput)) { @@ -251,13 +280,13 @@ public final class Mp3Extractor implements Extractor { scratch.setPosition(0); int sampleHeaderData = scratch.readInt(); if (!headersMatch(sampleHeaderData, synchronizedHeaderData) - || MpegAudioHeader.getFrameSize(sampleHeaderData) == C.LENGTH_UNSET) { + || MpegAudioUtil.getFrameSize(sampleHeaderData) == C.LENGTH_UNSET) { // We have lost synchronization, so attempt to resynchronize starting at the next byte. extractorInput.skipFully(1); synchronizedHeaderData = 0; return RESULT_CONTINUE; } - MpegAudioHeader.populateHeader(sampleHeaderData, synchronizedHeader); + synchronizedHeader.setForHeaderData(sampleHeaderData); if (basisTimeUs == C.TIME_UNSET) { basisTimeUs = seeker.getTimeUs(extractorInput.getPosition()); if (forcedFirstSampleTimestampUs != C.TIME_UNSET) { @@ -266,8 +295,20 @@ public final class Mp3Extractor implements Extractor { } } sampleBytesRemaining = synchronizedHeader.frameSize; + if (seeker instanceof IndexSeeker) { + IndexSeeker indexSeeker = (IndexSeeker) seeker; + // Add seek point corresponding to the next frame instead of the current one to be able to + // start writing to the realTrackOutput on time when a seek is in progress. + indexSeeker.maybeAddSeekPoint( + computeTimeUs(samplesRead + synchronizedHeader.samplesPerFrame), + extractorInput.getPosition() + synchronizedHeader.frameSize); + if (isSeekInProgress && indexSeeker.isTimeUsInIndex(seekTimeUs)) { + isSeekInProgress = false; + currentTrackOutput = realTrackOutput; + } + } } - int bytesAppended = trackOutput.sampleData(extractorInput, sampleBytesRemaining, true); + int bytesAppended = currentTrackOutput.sampleData(extractorInput, sampleBytesRemaining, true); if (bytesAppended == C.RESULT_END_OF_INPUT) { return RESULT_END_OF_INPUT; } @@ -275,16 +316,18 @@ public final class Mp3Extractor implements Extractor { if (sampleBytesRemaining > 0) { return RESULT_CONTINUE; } - long timeUs = basisTimeUs + (samplesRead * C.MICROS_PER_SECOND / synchronizedHeader.sampleRate); - trackOutput.sampleMetadata(timeUs, C.BUFFER_FLAG_KEY_FRAME, synchronizedHeader.frameSize, 0, - null); + currentTrackOutput.sampleMetadata( + computeTimeUs(samplesRead), C.BUFFER_FLAG_KEY_FRAME, synchronizedHeader.frameSize, 0, null); samplesRead += synchronizedHeader.samplesPerFrame; sampleBytesRemaining = 0; return RESULT_CONTINUE; } - private boolean synchronize(ExtractorInput input, boolean sniffing) - throws IOException, InterruptedException { + private long computeTimeUs(long samplesRead) { + return basisTimeUs + samplesRead * C.MICROS_PER_SECOND / synchronizedHeader.sampleRate; + } + + private boolean synchronize(ExtractorInput input, boolean sniffing) throws IOException { int validFrameCount = 0; int candidateSynchronizedHeaderData = 0; int peekedId3Bytes = 0; @@ -318,8 +361,8 @@ public final class Mp3Extractor implements Extractor { int headerData = scratch.readInt(); int frameSize; if ((candidateSynchronizedHeaderData != 0 - && !headersMatch(headerData, candidateSynchronizedHeaderData)) - || (frameSize = MpegAudioHeader.getFrameSize(headerData)) == C.LENGTH_UNSET) { + && !headersMatch(headerData, candidateSynchronizedHeaderData)) + || (frameSize = MpegAudioUtil.getFrameSize(headerData)) == C.LENGTH_UNSET) { // The header doesn't match the candidate header or is invalid. Try the next byte offset. if (searchedBytes++ == searchLimitBytes) { if (!sniffing) { @@ -339,7 +382,7 @@ public final class Mp3Extractor implements Extractor { // The header matches the candidate header and/or is valid. validFrameCount++; if (validFrameCount == 1) { - MpegAudioHeader.populateHeader(headerData, synchronizedHeader); + synchronizedHeader.setForHeaderData(headerData); candidateSynchronizedHeaderData = headerData; } else if (validFrameCount == 4) { break; @@ -361,8 +404,7 @@ public final class Mp3Extractor implements Extractor { * Returns whether the extractor input is peeking the end of the stream. If {@code false}, * populates the scratch buffer with the next four bytes. */ - private boolean peekEndOfStreamOrHeader(ExtractorInput extractorInput) - throws IOException, InterruptedException { + private boolean peekEndOfStreamOrHeader(ExtractorInput extractorInput) throws IOException { if (seeker != null) { long dataEndPosition = seeker.getDataEndPosition(); if (dataEndPosition != C.POSITION_UNSET @@ -378,6 +420,44 @@ public final class Mp3Extractor implements Extractor { } } + private Seeker computeSeeker(ExtractorInput input) throws IOException { + // Read past any seek frame and set the seeker based on metadata or a seek frame. Metadata + // takes priority as it can provide greater precision. + Seeker seekFrameSeeker = maybeReadSeekFrame(input); + Seeker metadataSeeker = maybeHandleSeekMetadata(metadata, input.getPosition()); + + if (disableSeeking) { + return new UnseekableSeeker(); + } + + @Nullable Seeker resultSeeker = null; + if ((flags & FLAG_ENABLE_INDEX_SEEKING) != 0) { + long durationUs = C.TIME_UNSET; + long dataEndPosition = C.POSITION_UNSET; + if (metadataSeeker != null) { + durationUs = metadataSeeker.getDurationUs(); + dataEndPosition = metadataSeeker.getDataEndPosition(); + } else if (seekFrameSeeker != null) { + durationUs = seekFrameSeeker.getDurationUs(); + dataEndPosition = seekFrameSeeker.getDataEndPosition(); + } + resultSeeker = + new IndexSeeker( + durationUs, /* dataStartPosition= */ input.getPosition(), dataEndPosition); + } else if (metadataSeeker != null) { + resultSeeker = metadataSeeker; + } else if (seekFrameSeeker != null) { + resultSeeker = seekFrameSeeker; + } + + if (resultSeeker == null + || (!resultSeeker.isSeekable() && (flags & FLAG_ENABLE_CONSTANT_BITRATE_SEEKING) != 0)) { + resultSeeker = getConstantBitrateSeeker(input); + } + + return resultSeeker; + } + /** * Consumes the next frame from the {@code input} if it contains VBRI or Xing seeking metadata, * returning a {@link Seeker} if the metadata was present and valid, or {@code null} otherwise. @@ -387,17 +467,16 @@ public final class Mp3Extractor implements Extractor { * @return A {@link Seeker} if seeking metadata was present and valid, or {@code null} otherwise. * @throws IOException Thrown if there was an error reading from the stream. Not expected if the * next two frames were already peeked during synchronization. - * @throws InterruptedException Thrown if reading from the stream was interrupted. Not expected if - * the next two frames were already peeked during synchronization. */ - private Seeker maybeReadSeekFrame(ExtractorInput input) throws IOException, InterruptedException { + @Nullable + private Seeker maybeReadSeekFrame(ExtractorInput input) throws IOException { ParsableByteArray frame = new ParsableByteArray(synchronizedHeader.frameSize); input.peekFully(frame.data, 0, synchronizedHeader.frameSize); int xingBase = (synchronizedHeader.version & 1) != 0 ? (synchronizedHeader.channels != 1 ? 36 : 21) // MPEG 1 : (synchronizedHeader.channels != 1 ? 21 : 13); // MPEG 2 or 2.5 int seekHeader = getSeekFrameHeader(frame, xingBase); - Seeker seeker; + @Nullable Seeker seeker; if (seekHeader == SEEK_HEADER_XING || seekHeader == SEEK_HEADER_INFO) { seeker = XingSeeker.create(input.getLength(), input.getPosition(), synchronizedHeader, frame); if (seeker != null && !gaplessInfoHolder.hasGaplessInfo()) { @@ -424,17 +503,20 @@ public final class Mp3Extractor implements Extractor { return seeker; } - /** - * Peeks the next frame and returns a {@link ConstantBitrateSeeker} based on its bitrate. - */ - private Seeker getConstantBitrateSeeker(ExtractorInput input) - throws IOException, InterruptedException { + /** Peeks the next frame and returns a {@link ConstantBitrateSeeker} based on its bitrate. */ + private Seeker getConstantBitrateSeeker(ExtractorInput input) throws IOException { input.peekFully(scratch.data, 0, 4); scratch.setPosition(0); - MpegAudioHeader.populateHeader(scratch.readInt(), synchronizedHeader); + synchronizedHeader.setForHeaderData(scratch.readInt()); return new ConstantBitrateSeeker(input.getLength(), input.getPosition(), synchronizedHeader); } + @EnsuresNonNull({"extractorOutput", "realTrackOutput"}) + private void assertInitialized() { + Assertions.checkStateNotNull(realTrackOutput); + Util.castNonNull(extractorOutput); + } + /** * Returns whether the headers match in those bits masked by {@link #MPEG_AUDIO_HEADER_MASK}. */ @@ -465,7 +547,8 @@ public final class Mp3Extractor implements Extractor { } @Nullable - private static MlltSeeker maybeHandleSeekMetadata(Metadata metadata, long firstFramePosition) { + private static MlltSeeker maybeHandleSeekMetadata( + @Nullable Metadata metadata, long firstFramePosition) { if (metadata != null) { int length = metadata.length(); for (int i = 0; i < length; i++) { @@ -477,6 +560,4 @@ public final class Mp3Extractor implements Extractor { } return null; } - - } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/Seeker.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp3/Seeker.java similarity index 100% rename from library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/Seeker.java rename to library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp3/Seeker.java diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/VbriSeeker.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp3/VbriSeeker.java similarity index 95% rename from library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/VbriSeeker.java rename to library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp3/VbriSeeker.java index 86551319e1..29584e7be7 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/VbriSeeker.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp3/VbriSeeker.java @@ -17,7 +17,7 @@ package com.google.android.exoplayer2.extractor.mp3; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.extractor.MpegAudioHeader; +import com.google.android.exoplayer2.audio.MpegAudioUtil; import com.google.android.exoplayer2.extractor.SeekPoint; import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.ParsableByteArray; @@ -41,8 +41,12 @@ import com.google.android.exoplayer2.util.Util; * @return A {@link VbriSeeker} for seeking in the stream, or {@code null} if the required * information is not present. */ - public static @Nullable VbriSeeker create( - long inputLength, long position, MpegAudioHeader mpegAudioHeader, ParsableByteArray frame) { + @Nullable + public static VbriSeeker create( + long inputLength, + long position, + MpegAudioUtil.Header mpegAudioHeader, + ParsableByteArray frame) { frame.skipBytes(10); int numFrames = frame.readInt(); if (numFrames <= 0) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/XingSeeker.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp3/XingSeeker.java similarity index 94% rename from library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/XingSeeker.java rename to library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp3/XingSeeker.java index db1a0199ac..9f31fba25e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/XingSeeker.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp3/XingSeeker.java @@ -17,7 +17,7 @@ package com.google.android.exoplayer2.extractor.mp3; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.extractor.MpegAudioHeader; +import com.google.android.exoplayer2.audio.MpegAudioUtil; import com.google.android.exoplayer2.extractor.SeekPoint; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Log; @@ -42,8 +42,12 @@ import com.google.android.exoplayer2.util.Util; * @return A {@link XingSeeker} for seeking in the stream, or {@code null} if the required * information is not present. */ - public static @Nullable XingSeeker create( - long inputLength, long position, MpegAudioHeader mpegAudioHeader, ParsableByteArray frame) { + @Nullable + public static XingSeeker create( + long inputLength, + long position, + MpegAudioUtil.Header mpegAudioHeader, + ParsableByteArray frame) { int samplesPerFrame = mpegAudioHeader.samplesPerFrame; int sampleRate = mpegAudioHeader.sampleRate; @@ -132,7 +136,7 @@ import com.google.android.exoplayer2.util.Util; scaledPosition = 256; } else { int prevTableIndex = (int) percent; - long[] tableOfContents = Assertions.checkNotNull(this.tableOfContents); + long[] tableOfContents = Assertions.checkStateNotNull(this.tableOfContents); double prevScaledPosition = tableOfContents[prevTableIndex]; double nextScaledPosition = prevTableIndex == 99 ? 256 : tableOfContents[prevTableIndex + 1]; // Linearly interpolate between the two scaled positions. @@ -152,7 +156,7 @@ import com.google.android.exoplayer2.util.Util; if (!isSeekable() || positionOffset <= xingFrameSize) { return 0L; } - long[] tableOfContents = Assertions.checkNotNull(this.tableOfContents); + long[] tableOfContents = Assertions.checkStateNotNull(this.tableOfContents); double scaledPosition = (positionOffset * 256d) / dataSize; int prevTableIndex = Util.binarySearchFloor(tableOfContents, (long) scaledPosition, true, true); long prevTimeUs = getTimeUsForTableIndex(prevTableIndex); diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp3/package-info.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp3/package-info.java new file mode 100644 index 0000000000..3483b26b47 --- /dev/null +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp3/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2019 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. + */ +@NonNullApi +package com.google.android.exoplayer2.extractor.mp3; + +import com.google.android.exoplayer2.util.NonNullApi; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Atom.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp4/Atom.java similarity index 99% rename from library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Atom.java rename to library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp4/Atom.java index 572efed1af..e86a873ed5 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Atom.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp4/Atom.java @@ -379,6 +379,9 @@ import java.util.List; @SuppressWarnings("ConstantCaseForConstants") public static final int TYPE_dfLa = 0x64664c61; + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_twos = 0x74776f73; + public final int type; public Atom(int type) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java similarity index 87% rename from library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java rename to library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java index bf05424b7f..3cf858558a 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java @@ -22,6 +22,7 @@ import androidx.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.audio.AacUtil; import com.google.android.exoplayer2.audio.Ac3Util; import com.google.android.exoplayer2.audio.Ac4Util; import com.google.android.exoplayer2.drm.DrmInitData; @@ -40,6 +41,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; +import org.checkerframework.checker.nullness.compatqual.NullableType; /** Utility methods for parsing MP4 format atom payloads according to ISO 14496-12. */ @SuppressWarnings({"ConstantField"}) @@ -85,15 +87,21 @@ import java.util.List; * * @param trak Atom to decode. * @param mvhd Movie header atom, used to get the timescale. - * @param duration The duration in units of the timescale declared in the mvhd atom, or - * {@link C#TIME_UNSET} if the duration should be parsed from the tkhd atom. - * @param drmInitData {@link DrmInitData} to be included in the format. + * @param duration The duration in units of the timescale declared in the mvhd atom, or {@link + * C#TIME_UNSET} if the duration should be parsed from the tkhd atom. + * @param drmInitData {@link DrmInitData} to be included in the format, or {@code null}. * @param ignoreEditLists Whether to ignore any edit lists in the trak box. * @param isQuickTime True for QuickTime media. False otherwise. * @return A {@link Track} instance, or {@code null} if the track's type isn't supported. */ - public static Track parseTrak(Atom.ContainerAtom trak, Atom.LeafAtom mvhd, long duration, - DrmInitData drmInitData, boolean ignoreEditLists, boolean isQuickTime) + @Nullable + public static Track parseTrak( + Atom.ContainerAtom trak, + Atom.LeafAtom mvhd, + long duration, + @Nullable DrmInitData drmInitData, + boolean ignoreEditLists, + boolean isQuickTime) throws ParserException { Atom.ContainerAtom mdia = trak.getContainerAtomOfType(Atom.TYPE_mdia); int trackType = getTrackTypeForHdlr(parseHdlr(mdia.getLeafAtomOfType(Atom.TYPE_hdlr).data)); @@ -118,12 +126,17 @@ import java.util.List; Pair mdhdData = parseMdhd(mdia.getLeafAtomOfType(Atom.TYPE_mdhd).data); StsdData stsdData = parseStsd(stbl.getLeafAtomOfType(Atom.TYPE_stsd).data, tkhdData.id, tkhdData.rotationDegrees, mdhdData.second, drmInitData, isQuickTime); - long[] editListDurations = null; - long[] editListMediaTimes = null; + @Nullable long[] editListDurations = null; + @Nullable long[] editListMediaTimes = null; if (!ignoreEditLists) { - Pair edtsData = parseEdts(trak.getContainerAtomOfType(Atom.TYPE_edts)); - editListDurations = edtsData.first; - editListMediaTimes = edtsData.second; + @Nullable Atom.ContainerAtom edtsAtom = trak.getContainerAtomOfType(Atom.TYPE_edts); + if (edtsAtom != null) { + @Nullable Pair edtsData = parseEdts(edtsAtom); + if (edtsData != null) { + editListDurations = edtsData.first; + editListMediaTimes = edtsData.second; + } + } } return stsdData.format == null ? null : new Track(tkhdData.id, trackType, mdhdData.first, movieTimescale, durationUs, @@ -144,11 +157,11 @@ import java.util.List; Track track, Atom.ContainerAtom stblAtom, GaplessInfoHolder gaplessInfoHolder) throws ParserException { SampleSizeBox sampleSizeBox; - Atom.LeafAtom stszAtom = stblAtom.getLeafAtomOfType(Atom.TYPE_stsz); + @Nullable Atom.LeafAtom stszAtom = stblAtom.getLeafAtomOfType(Atom.TYPE_stsz); if (stszAtom != null) { sampleSizeBox = new StszSampleSizeBox(stszAtom); } else { - Atom.LeafAtom stz2Atom = stblAtom.getLeafAtomOfType(Atom.TYPE_stz2); + @Nullable Atom.LeafAtom stz2Atom = stblAtom.getLeafAtomOfType(Atom.TYPE_stz2); if (stz2Atom == null) { throw new ParserException("Track has no sample table size information"); } @@ -169,7 +182,7 @@ import java.util.List; // Entries are byte offsets of chunks. boolean chunkOffsetsAreLongs = false; - Atom.LeafAtom chunkOffsetsAtom = stblAtom.getLeafAtomOfType(Atom.TYPE_stco); + @Nullable Atom.LeafAtom chunkOffsetsAtom = stblAtom.getLeafAtomOfType(Atom.TYPE_stco); if (chunkOffsetsAtom == null) { chunkOffsetsAreLongs = true; chunkOffsetsAtom = stblAtom.getLeafAtomOfType(Atom.TYPE_co64); @@ -180,11 +193,11 @@ import java.util.List; // Entries are (number of samples, timestamp delta between those samples). ParsableByteArray stts = stblAtom.getLeafAtomOfType(Atom.TYPE_stts).data; // Entries are the indices of samples that are synchronization samples. - Atom.LeafAtom stssAtom = stblAtom.getLeafAtomOfType(Atom.TYPE_stss); - ParsableByteArray stss = stssAtom != null ? stssAtom.data : null; + @Nullable Atom.LeafAtom stssAtom = stblAtom.getLeafAtomOfType(Atom.TYPE_stss); + @Nullable ParsableByteArray stss = stssAtom != null ? stssAtom.data : null; // Entries are (number of samples, timestamp offset). - Atom.LeafAtom cttsAtom = stblAtom.getLeafAtomOfType(Atom.TYPE_ctts); - ParsableByteArray ctts = cttsAtom != null ? cttsAtom.data : null; + @Nullable Atom.LeafAtom cttsAtom = stblAtom.getLeafAtomOfType(Atom.TYPE_ctts); + @Nullable ParsableByteArray ctts = cttsAtom != null ? cttsAtom.data : null; // Prepare to read chunk information. ChunkIterator chunkIterator = new ChunkIterator(stsc, chunkOffsets, chunkOffsetsAreLongs); @@ -433,10 +446,15 @@ import java.util.List; long editDuration = Util.scaleLargeTimestamp( track.editListDurations[i], track.timescale, track.movieTimescale); - startIndices[i] = Util.binarySearchCeil(timestamps, editMediaTime, true, true); + startIndices[i] = + Util.binarySearchFloor( + timestamps, editMediaTime, /* inclusive= */ true, /* stayInBounds= */ true); endIndices[i] = Util.binarySearchCeil( - timestamps, editMediaTime + editDuration, omitClippedSample, false); + timestamps, + editMediaTime + editDuration, + /* inclusive= */ omitClippedSample, + /* stayInBounds= */ false); while (startIndices[i] < endIndices[i] && (flags[startIndices[i]] & C.BUFFER_FLAG_KEY_FRAME) == 0) { // Applying the edit correctly would require prerolling from the previous sync sample. In @@ -474,7 +492,7 @@ import java.util.List; long ptsUs = Util.scaleLargeTimestamp(pts, C.MICROS_PER_SECOND, track.movieTimescale); long timeInSegmentUs = Util.scaleLargeTimestamp( - timestamps[j] - editMediaTime, C.MICROS_PER_SECOND, track.timescale); + Math.max(0, timestamps[j] - editMediaTime), C.MICROS_PER_SECOND, track.timescale); editedTimestamps[sampleIndex] = ptsUs + timeInSegmentUs; if (copyMetadata && editedSizes[sampleIndex] > editedMaximumSize) { editedMaximumSize = sizes[j]; @@ -532,9 +550,9 @@ import java.util.List; */ @Nullable public static Metadata parseMdtaFromMeta(Atom.ContainerAtom meta) { - Atom.LeafAtom hdlrAtom = meta.getLeafAtomOfType(Atom.TYPE_hdlr); - Atom.LeafAtom keysAtom = meta.getLeafAtomOfType(Atom.TYPE_keys); - Atom.LeafAtom ilstAtom = meta.getLeafAtomOfType(Atom.TYPE_ilst); + @Nullable Atom.LeafAtom hdlrAtom = meta.getLeafAtomOfType(Atom.TYPE_hdlr); + @Nullable Atom.LeafAtom keysAtom = meta.getLeafAtomOfType(Atom.TYPE_keys); + @Nullable Atom.LeafAtom ilstAtom = meta.getLeafAtomOfType(Atom.TYPE_ilst); if (hdlrAtom == null || keysAtom == null || ilstAtom == null @@ -565,6 +583,7 @@ import java.util.List; int keyIndex = ilst.readInt() - 1; if (keyIndex >= 0 && keyIndex < keyNames.length) { String key = keyNames[keyIndex]; + @Nullable Metadata.Entry entry = MetadataUtil.parseMdtaMetadataEntryFromIlst(ilst, atomPosition + atomSize, key); if (entry != null) { @@ -599,7 +618,7 @@ import java.util.List; ilst.skipBytes(Atom.HEADER_SIZE); ArrayList entries = new ArrayList<>(); while (ilst.getPosition() < limit) { - Metadata.Entry entry = MetadataUtil.parseIlstElement(ilst); + @Nullable Metadata.Entry entry = MetadataUtil.parseIlstElement(ilst); if (entry != null) { entries.add(entry); } @@ -736,19 +755,25 @@ import java.util.List; * @param trackId The track's identifier in its container. * @param rotationDegrees The rotation of the track in degrees. * @param language The language of the track. - * @param drmInitData {@link DrmInitData} to be included in the format. + * @param drmInitData {@link DrmInitData} to be included in the format, or {@code null}. * @param isQuickTime True for QuickTime media. False otherwise. * @return An object containing the parsed data. */ - private static StsdData parseStsd(ParsableByteArray stsd, int trackId, int rotationDegrees, - String language, DrmInitData drmInitData, boolean isQuickTime) throws ParserException { + private static StsdData parseStsd( + ParsableByteArray stsd, + int trackId, + int rotationDegrees, + String language, + @Nullable DrmInitData drmInitData, + boolean isQuickTime) + throws ParserException { stsd.setPosition(Atom.FULL_HEADER_SIZE); int numberOfEntries = stsd.readInt(); StsdData out = new StsdData(numberOfEntries); for (int i = 0; i < numberOfEntries; i++) { int childStartPosition = stsd.getPosition(); int childAtomSize = stsd.readInt(); - Assertions.checkArgument(childAtomSize > 0, "childAtomSize should be positive"); + Assertions.checkState(childAtomSize > 0, "childAtomSize should be positive"); int childAtomType = stsd.readInt(); if (childAtomType == Atom.TYPE_avc1 || childAtomType == Atom.TYPE_avc3 @@ -779,6 +804,7 @@ import java.util.List; || childAtomType == Atom.TYPE_sawb || childAtomType == Atom.TYPE_lpcm || childAtomType == Atom.TYPE_sowt + || childAtomType == Atom.TYPE_twos || childAtomType == Atom.TYPE__mp3 || childAtomType == Atom.TYPE_alac || childAtomType == Atom.TYPE_alaw @@ -793,20 +819,29 @@ import java.util.List; parseTextSampleEntry(stsd, childAtomType, childStartPosition, childAtomSize, trackId, language, out); } else if (childAtomType == Atom.TYPE_camm) { - out.format = Format.createSampleFormat(Integer.toString(trackId), - MimeTypes.APPLICATION_CAMERA_MOTION, null, Format.NO_VALUE, null); + out.format = + new Format.Builder() + .setId(trackId) + .setSampleMimeType(MimeTypes.APPLICATION_CAMERA_MOTION) + .build(); } stsd.setPosition(childStartPosition + childAtomSize); } return out; } - private static void parseTextSampleEntry(ParsableByteArray parent, int atomType, int position, - int atomSize, int trackId, String language, StsdData out) throws ParserException { + private static void parseTextSampleEntry( + ParsableByteArray parent, + int atomType, + int position, + int atomSize, + int trackId, + String language, + StsdData out) { parent.setPosition(position + Atom.HEADER_SIZE + StsdData.STSD_HEADER_SIZE); // Default values. - List initializationData = null; + @Nullable List initializationData = null; long subsampleOffsetUs = Format.OFFSET_SAMPLE_RELATIVE; String mimeType; @@ -833,22 +868,26 @@ import java.util.List; } 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); + new Format.Builder() + .setId(trackId) + .setSampleMimeType(mimeType) + .setLanguage(language) + .setSubsampleOffsetUs(subsampleOffsetUs) + .setInitializationData(initializationData) + .build(); } - private static void parseVideoSampleEntry(ParsableByteArray parent, int atomType, int position, - int size, int trackId, int rotationDegrees, DrmInitData drmInitData, StsdData out, - int entryIndex) throws ParserException { + private static void parseVideoSampleEntry( + ParsableByteArray parent, + int atomType, + int position, + int size, + int trackId, + int rotationDegrees, + @Nullable DrmInitData drmInitData, + StsdData out, + int entryIndex) + throws ParserException { parent.setPosition(position + Atom.HEADER_SIZE + StsdData.STSD_HEADER_SIZE); parent.skipBytes(16); @@ -860,8 +899,9 @@ import java.util.List; int childPosition = parent.getPosition(); if (atomType == Atom.TYPE_encv) { - Pair sampleEntryEncryptionData = parseSampleEntryEncryptionData( - parent, position, size); + @Nullable + Pair sampleEntryEncryptionData = + parseSampleEntryEncryptionData(parent, position, size); if (sampleEntryEncryptionData != null) { atomType = sampleEntryEncryptionData.first; drmInitData = drmInitData == null ? null @@ -875,10 +915,10 @@ import java.util.List; // drmInitData = null; // } - List initializationData = null; - String mimeType = null; - String codecs = null; - byte[] projectionData = null; + @Nullable List initializationData = null; + @Nullable String mimeType = null; + @Nullable String codecs = null; + @Nullable byte[] projectionData = null; @C.StereoMode int stereoMode = Format.NO_VALUE; while (childPosition - position < size) { @@ -889,7 +929,7 @@ import java.util.List; // Handle optional terminating four zero bytes in MOV files. break; } - Assertions.checkArgument(childAtomSize > 0, "childAtomSize should be positive"); + Assertions.checkState(childAtomSize > 0, "childAtomSize should be positive"); int childAtomType = parent.readInt(); if (childAtomType == Atom.TYPE_avcC) { Assertions.checkState(mimeType == null); @@ -909,7 +949,7 @@ import java.util.List; initializationData = hevcConfig.initializationData; out.nalUnitLengthFieldLength = hevcConfig.nalUnitLengthFieldLength; } else if (childAtomType == Atom.TYPE_dvcC || childAtomType == Atom.TYPE_dvvC) { - DolbyVisionConfig dolbyVisionConfig = DolbyVisionConfig.parse(parent); + @Nullable DolbyVisionConfig dolbyVisionConfig = DolbyVisionConfig.parse(parent); if (dolbyVisionConfig != null) { codecs = dolbyVisionConfig.codecs; mimeType = MimeTypes.VIDEO_DOLBY_VISION; @@ -925,10 +965,13 @@ import java.util.List; mimeType = MimeTypes.VIDEO_H263; } else if (childAtomType == Atom.TYPE_esds) { Assertions.checkState(mimeType == null); - Pair mimeTypeAndInitializationData = + Pair<@NullableType String, byte @NullableType []> mimeTypeAndInitializationDataBytes = parseEsdsFromParent(parent, childStartPosition); - mimeType = mimeTypeAndInitializationData.first; - initializationData = Collections.singletonList(mimeTypeAndInitializationData.second); + mimeType = mimeTypeAndInitializationDataBytes.first; + @Nullable byte[] initializationDataBytes = mimeTypeAndInitializationDataBytes.second; + if (initializationDataBytes != null) { + initializationData = Collections.singletonList(initializationDataBytes); + } } else if (childAtomType == Atom.TYPE_pasp) { pixelWidthHeightRatio = parsePaspFromParent(parent, childStartPosition); pixelWidthHeightRatioFromPasp = true; @@ -966,37 +1009,35 @@ import java.util.List; } out.format = - Format.createVideoSampleFormat( - Integer.toString(trackId), - mimeType, - codecs, - /* bitrate= */ Format.NO_VALUE, - /* maxInputSize= */ Format.NO_VALUE, - width, - height, - /* frameRate= */ Format.NO_VALUE, - initializationData, - rotationDegrees, - pixelWidthHeightRatio, - projectionData, - stereoMode, - /* colorInfo= */ null, - drmInitData); + new Format.Builder() + .setId(trackId) + .setSampleMimeType(mimeType) + .setCodecs(codecs) + .setWidth(width) + .setHeight(height) + .setPixelWidthHeightRatio(pixelWidthHeightRatio) + .setRotationDegrees(rotationDegrees) + .setProjectionData(projectionData) + .setStereoMode(stereoMode) + .setInitializationData(initializationData) + .setDrmInitData(drmInitData) + .build(); } /** * Parses the edts atom (defined in 14496-12 subsection 8.6.5). * * @param edtsAtom edts (edit box) atom to decode. - * @return Pair of edit list durations and edit list media times, or a pair of nulls if they are - * not present. + * @return Pair of edit list durations and edit list media times, or {@code null} if they are not + * present. */ + @Nullable private static Pair parseEdts(Atom.ContainerAtom edtsAtom) { - Atom.LeafAtom elst; - if (edtsAtom == null || (elst = edtsAtom.getLeafAtomOfType(Atom.TYPE_elst)) == null) { - return Pair.create(null, null); + @Nullable Atom.LeafAtom elstAtom = edtsAtom.getLeafAtomOfType(Atom.TYPE_elst); + if (elstAtom == null) { + return null; } - ParsableByteArray elstData = elst.data; + ParsableByteArray elstData = elstAtom.data; elstData.setPosition(Atom.HEADER_SIZE); int fullAtom = elstData.readInt(); int version = Atom.parseFullAtomVersion(fullAtom); @@ -1024,9 +1065,18 @@ import java.util.List; return (float) hSpacing / vSpacing; } - private static void parseAudioSampleEntry(ParsableByteArray parent, int atomType, int position, - int size, int trackId, String language, boolean isQuickTime, DrmInitData drmInitData, - StsdData out, int entryIndex) throws ParserException { + private static void parseAudioSampleEntry( + ParsableByteArray parent, + int atomType, + int position, + int size, + int trackId, + String language, + boolean isQuickTime, + @Nullable DrmInitData drmInitData, + StsdData out, + int entryIndex) + throws ParserException { parent.setPosition(position + Atom.HEADER_SIZE + StsdData.STSD_HEADER_SIZE); int quickTimeSoundDescriptionVersion = 0; @@ -1039,6 +1089,8 @@ import java.util.List; int channelCount; int sampleRate; + @C.PcmEncoding int pcmEncoding = Format.NO_VALUE; + @Nullable String codecs = null; if (quickTimeSoundDescriptionVersion == 0 || quickTimeSoundDescriptionVersion == 1) { channelCount = parent.readUnsignedShort(); @@ -1064,8 +1116,9 @@ import java.util.List; int childPosition = parent.getPosition(); if (atomType == Atom.TYPE_enca) { - Pair sampleEntryEncryptionData = parseSampleEntryEncryptionData( - parent, position, size); + @Nullable + Pair sampleEntryEncryptionData = + parseSampleEntryEncryptionData(parent, position, size); if (sampleEntryEncryptionData != null) { atomType = sampleEntryEncryptionData.first; drmInitData = drmInitData == null ? null @@ -1080,7 +1133,7 @@ import java.util.List; // } // If the atom type determines a MIME type, set it immediately. - String mimeType = null; + @Nullable String mimeType = null; if (atomType == Atom.TYPE_ac_3) { mimeType = MimeTypes.AUDIO_AC3; } else if (atomType == Atom.TYPE_ec_3) { @@ -1099,6 +1152,10 @@ import java.util.List; mimeType = MimeTypes.AUDIO_AMR_WB; } else if (atomType == Atom.TYPE_lpcm || atomType == Atom.TYPE_sowt) { mimeType = MimeTypes.AUDIO_RAW; + pcmEncoding = C.ENCODING_PCM_16BIT; + } else if (atomType == Atom.TYPE_twos) { + mimeType = MimeTypes.AUDIO_RAW; + pcmEncoding = C.ENCODING_PCM_16BIT_BIG_ENDIAN; } else if (atomType == Atom.TYPE__mp3) { mimeType = MimeTypes.AUDIO_MPEG; } else if (atomType == Atom.TYPE_alac) { @@ -1113,27 +1170,27 @@ import java.util.List; mimeType = MimeTypes.AUDIO_FLAC; } - byte[] initializationData = null; + @Nullable byte[] initializationData = null; while (childPosition - position < size) { parent.setPosition(childPosition); int childAtomSize = parent.readInt(); - Assertions.checkArgument(childAtomSize > 0, "childAtomSize should be positive"); + Assertions.checkState(childAtomSize > 0, "childAtomSize should be positive"); int childAtomType = parent.readInt(); if (childAtomType == Atom.TYPE_esds || (isQuickTime && childAtomType == Atom.TYPE_wave)) { int esdsAtomPosition = childAtomType == Atom.TYPE_esds ? childPosition : findEsdsPosition(parent, childPosition, childAtomSize); if (esdsAtomPosition != C.POSITION_UNSET) { - Pair mimeTypeAndInitializationData = + Pair<@NullableType String, byte @NullableType []> mimeTypeAndInitializationData = parseEsdsFromParent(parent, esdsAtomPosition); mimeType = mimeTypeAndInitializationData.first; initializationData = mimeTypeAndInitializationData.second; - if (MimeTypes.AUDIO_AAC.equals(mimeType)) { + if (MimeTypes.AUDIO_AAC.equals(mimeType) && initializationData != null) { // Update sampleRate and channelCount from the AudioSpecificConfig initialization data, // which is more reliable. See [Internal: b/10903778]. - Pair audioSpecificConfig = - CodecSpecificDataUtil.parseAacAudioSpecificConfig(initializationData); - sampleRate = audioSpecificConfig.first; - channelCount = audioSpecificConfig.second; + AacUtil.Config aacConfig = AacUtil.parseAudioSpecificConfig(initializationData); + sampleRate = aacConfig.sampleRateHz; + channelCount = aacConfig.channelCount; + codecs = aacConfig.codecs; } } } else if (childAtomType == Atom.TYPE_dac3) { @@ -1149,9 +1206,15 @@ import java.util.List; out.format = Ac4Util.parseAc4AnnexEFormat(parent, Integer.toString(trackId), language, drmInitData); } else if (childAtomType == Atom.TYPE_ddts) { - out.format = Format.createAudioSampleFormat(Integer.toString(trackId), mimeType, null, - Format.NO_VALUE, Format.NO_VALUE, channelCount, sampleRate, null, drmInitData, 0, - language); + out.format = + new Format.Builder() + .setId(trackId) + .setSampleMimeType(mimeType) + .setChannelCount(channelCount) + .setSampleRate(sampleRate) + .setDrmInitData(drmInitData) + .setLanguage(language) + .build(); } else if (childAtomType == Atom.TYPE_dOps) { // Build an Opus Identification Header (defined in RFC-7845) by concatenating the Opus Magic // Signature and the body of the dOps atom. @@ -1185,13 +1248,19 @@ import java.util.List; } if (out.format == null && mimeType != null) { - // TODO: Determine the correct PCM encoding. - @C.PcmEncoding int pcmEncoding = - MimeTypes.AUDIO_RAW.equals(mimeType) ? C.ENCODING_PCM_16BIT : Format.NO_VALUE; - out.format = Format.createAudioSampleFormat(Integer.toString(trackId), mimeType, null, - Format.NO_VALUE, Format.NO_VALUE, channelCount, sampleRate, pcmEncoding, - initializationData == null ? null : Collections.singletonList(initializationData), - drmInitData, 0, language); + out.format = + new Format.Builder() + .setId(trackId) + .setSampleMimeType(mimeType) + .setCodecs(codecs) + .setChannelCount(channelCount) + .setSampleRate(sampleRate) + .setPcmEncoding(pcmEncoding) + .setInitializationData( + initializationData == null ? null : Collections.singletonList(initializationData)) + .setDrmInitData(drmInitData) + .setLanguage(language) + .build(); } } @@ -1204,7 +1273,7 @@ import java.util.List; while (childAtomPosition - position < size) { parent.setPosition(childAtomPosition); int childAtomSize = parent.readInt(); - Assertions.checkArgument(childAtomSize > 0, "childAtomSize should be positive"); + Assertions.checkState(childAtomSize > 0, "childAtomSize should be positive"); int childType = parent.readInt(); if (childType == Atom.TYPE_esds) { return childAtomPosition; @@ -1214,10 +1283,9 @@ import java.util.List; return C.POSITION_UNSET; } - /** - * Returns codec-specific initialization data contained in an esds box. - */ - private static Pair parseEsdsFromParent(ParsableByteArray parent, int position) { + /** Returns codec-specific initialization data contained in an esds box. */ + private static Pair<@NullableType String, byte @NullableType []> parseEsdsFromParent( + ParsableByteArray parent, int position) { parent.setPosition(position + Atom.HEADER_SIZE + 4); // Start of the ES_Descriptor (defined in 14496-1) parent.skipBytes(1); // ES_Descriptor tag @@ -1263,13 +1331,14 @@ import java.util.List; * unencrypted atom type and a {@link TrackEncryptionBox}. Null is returned if no common * encryption sinf atom was present. */ + @Nullable private static Pair parseSampleEntryEncryptionData( ParsableByteArray parent, int position, int size) { int childPosition = parent.getPosition(); while (childPosition - position < size) { parent.setPosition(childPosition); int childAtomSize = parent.readInt(); - Assertions.checkArgument(childAtomSize > 0, "childAtomSize should be positive"); + Assertions.checkState(childAtomSize > 0, "childAtomSize should be positive"); int childAtomType = parent.readInt(); if (childAtomType == Atom.TYPE_sinf) { Pair result = parseCommonEncryptionSinfFromParent(parent, @@ -1283,13 +1352,14 @@ import java.util.List; return null; } + @Nullable /* package */ static Pair parseCommonEncryptionSinfFromParent( ParsableByteArray parent, int position, int size) { int childPosition = position + Atom.HEADER_SIZE; int schemeInformationBoxPosition = C.POSITION_UNSET; int schemeInformationBoxSize = 0; - String schemeType = null; - Integer dataFormat = null; + @Nullable String schemeType = null; + @Nullable Integer dataFormat = null; while (childPosition - position < size) { parent.setPosition(childPosition); int childAtomSize = parent.readInt(); @@ -1309,20 +1379,23 @@ import java.util.List; if (C.CENC_TYPE_cenc.equals(schemeType) || C.CENC_TYPE_cbc1.equals(schemeType) || C.CENC_TYPE_cens.equals(schemeType) || C.CENC_TYPE_cbcs.equals(schemeType)) { - Assertions.checkArgument(dataFormat != null, "frma atom is mandatory"); - Assertions.checkArgument(schemeInformationBoxPosition != C.POSITION_UNSET, - "schi atom is mandatory"); - TrackEncryptionBox encryptionBox = parseSchiFromParent(parent, schemeInformationBoxPosition, - schemeInformationBoxSize, schemeType); - Assertions.checkArgument(encryptionBox != null, "tenc atom is mandatory"); + Assertions.checkStateNotNull(dataFormat, "frma atom is mandatory"); + Assertions.checkState( + schemeInformationBoxPosition != C.POSITION_UNSET, "schi atom is mandatory"); + TrackEncryptionBox encryptionBox = + Assertions.checkStateNotNull( + parseSchiFromParent( + parent, schemeInformationBoxPosition, schemeInformationBoxSize, schemeType), + "tenc atom is mandatory"); return Pair.create(dataFormat, encryptionBox); } else { return null; } } - private static TrackEncryptionBox parseSchiFromParent(ParsableByteArray parent, int position, - int size, String schemeType) { + @Nullable + private static TrackEncryptionBox parseSchiFromParent( + ParsableByteArray parent, int position, int size, String schemeType) { int childPosition = position + Atom.HEADER_SIZE; while (childPosition - position < size) { parent.setPosition(childPosition); @@ -1359,9 +1432,8 @@ import java.util.List; return null; } - /** - * Parses the proj box from sv3d box, as specified by https://github.com/google/spatial-media. - */ + /** Parses the proj box from sv3d box, as specified by https://github.com/google/spatial-media. */ + @Nullable private static byte[] parseProjFromParent(ParsableByteArray parent, int position, int size) { int childPosition = position + Atom.HEADER_SIZE; while (childPosition - position < size) { @@ -1477,10 +1549,9 @@ import java.util.List; public final TrackEncryptionBox[] trackEncryptionBoxes; - public Format format; + @Nullable public Format format; public int nalUnitLengthFieldLength; - @Track.Transformation - public int requiredSampleTransformation; + @Track.Transformation public int requiredSampleTransformation; public StsdData(int numberOfEntries) { trackEncryptionBoxes = new TrackEncryptionBox[numberOfEntries]; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/DefaultSampleValues.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp4/DefaultSampleValues.java similarity index 100% rename from library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/DefaultSampleValues.java rename to library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp4/DefaultSampleValues.java diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/FixedSampleSizeRechunker.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp4/FixedSampleSizeRechunker.java similarity index 100% rename from library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/FixedSampleSizeRechunker.java rename to library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp4/FixedSampleSizeRechunker.java diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java similarity index 87% rename from library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java rename to library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java index f6e0fb8bc9..ae543c1642 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java @@ -25,6 +25,7 @@ import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.audio.Ac4Util; import com.google.android.exoplayer2.drm.DrmInitData; import com.google.android.exoplayer2.drm.DrmInitData.SchemeData; +import com.google.android.exoplayer2.extractor.CeaUtil; import com.google.android.exoplayer2.extractor.ChunkIndex; import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.ExtractorInput; @@ -37,7 +38,6 @@ import com.google.android.exoplayer2.extractor.mp4.Atom.ContainerAtom; import com.google.android.exoplayer2.extractor.mp4.Atom.LeafAtom; import com.google.android.exoplayer2.metadata.emsg.EventMessage; import com.google.android.exoplayer2.metadata.emsg.EventMessageEncoder; -import com.google.android.exoplayer2.text.cea.CeaUtil; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.MimeTypes; @@ -55,6 +55,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.UUID; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /** Extracts data from the FMP4 container format. */ @SuppressWarnings("ConstantField") @@ -113,7 +114,7 @@ public class FragmentedMp4Extractor implements Extractor { private static final byte[] PIFF_SAMPLE_ENCRYPTION_BOX_EXTENDED_TYPE = new byte[] {-94, 57, 79, 82, 90, -101, 79, 20, -94, 68, 108, 66, 124, 100, -115, -12}; private static final Format EMSG_FORMAT = - Format.createSampleFormat(null, MimeTypes.APPLICATION_EMSG, Format.OFFSET_SAMPLE_RELATIVE); + new Format.Builder().setSampleMimeType(MimeTypes.APPLICATION_EMSG).build(); // Parser states. private static final int STATE_READING_ATOM_HEADER = 0; @@ -128,7 +129,6 @@ public class FragmentedMp4Extractor implements Extractor { // Sideloaded data. private final List closedCaptionFormats; - @Nullable private final DrmInitData sideloadedDrmInitData; // Track-linked data bundle, accessible as a whole through trackID. private final SparseArray trackBundles; @@ -155,22 +155,21 @@ public class FragmentedMp4Extractor implements Extractor { private int atomType; private long atomSize; private int atomHeaderBytesRead; - private ParsableByteArray atomData; + @Nullable private ParsableByteArray atomData; private long endOfMdatPosition; private int pendingMetadataSampleBytes; private long pendingSeekTimeUs; private long durationUs; private long segmentIndexEarliestPresentationTimeUs; - private TrackBundle currentTrackBundle; + @Nullable private TrackBundle currentTrackBundle; private int sampleSize; private int sampleBytesWritten; private int sampleCurrentNalBytesRemaining; private boolean processSeiNalUnitPayload; - private boolean isAc4HeaderRequired; // Extractor output. - private ExtractorOutput extractorOutput; + private @MonotonicNonNull ExtractorOutput extractorOutput; private TrackOutput[] emsgTrackOutputs; private TrackOutput[] cea608TrackOutputs; @@ -185,7 +184,7 @@ public class FragmentedMp4Extractor implements Extractor { * @param flags Flags that control the extractor's behavior. */ public FragmentedMp4Extractor(@Flags int flags) { - this(flags, null); + this(flags, /* timestampAdjuster= */ null); } /** @@ -193,7 +192,7 @@ public class FragmentedMp4Extractor implements Extractor { * @param timestampAdjuster Adjusts sample timestamps. May be null if no adjustment is needed. */ public FragmentedMp4Extractor(@Flags int flags, @Nullable TimestampAdjuster timestampAdjuster) { - this(flags, timestampAdjuster, null, null); + this(flags, timestampAdjuster, /* sideloadedTrack= */ null, Collections.emptyList()); } /** @@ -201,15 +200,12 @@ public class FragmentedMp4Extractor implements Extractor { * @param timestampAdjuster Adjusts sample timestamps. May be null if no adjustment is needed. * @param sideloadedTrack Sideloaded track information, in the case that the extractor will not * receive a moov box in the input data. Null if a moov box is expected. - * @param sideloadedDrmInitData The {@link DrmInitData} to use for encrypted tracks. If null, the - * pssh boxes (if present) will be used. */ public FragmentedMp4Extractor( @Flags int flags, @Nullable TimestampAdjuster timestampAdjuster, - @Nullable Track sideloadedTrack, - @Nullable DrmInitData sideloadedDrmInitData) { - this(flags, timestampAdjuster, sideloadedTrack, sideloadedDrmInitData, Collections.emptyList()); + @Nullable Track sideloadedTrack) { + this(flags, timestampAdjuster, sideloadedTrack, Collections.emptyList()); } /** @@ -217,8 +213,6 @@ public class FragmentedMp4Extractor implements Extractor { * @param timestampAdjuster Adjusts sample timestamps. May be null if no adjustment is needed. * @param sideloadedTrack Sideloaded track information, in the case that the extractor will not * receive a moov box in the input data. Null if a moov box is expected. - * @param sideloadedDrmInitData The {@link DrmInitData} to use for encrypted tracks. If null, the - * pssh boxes (if present) will be used. * @param closedCaptionFormats For tracks that contain SEI messages, the formats of the closed * caption channels to expose. */ @@ -226,10 +220,13 @@ public class FragmentedMp4Extractor implements Extractor { @Flags int flags, @Nullable TimestampAdjuster timestampAdjuster, @Nullable Track sideloadedTrack, - @Nullable DrmInitData sideloadedDrmInitData, List closedCaptionFormats) { - this(flags, timestampAdjuster, sideloadedTrack, sideloadedDrmInitData, - closedCaptionFormats, null); + this( + flags, + timestampAdjuster, + sideloadedTrack, + closedCaptionFormats, + /* additionalEmsgTrackOutput= */ null); } /** @@ -237,8 +234,6 @@ public class FragmentedMp4Extractor implements Extractor { * @param timestampAdjuster Adjusts sample timestamps. May be null if no adjustment is needed. * @param sideloadedTrack Sideloaded track information, in the case that the extractor will not * receive a moov box in the input data. Null if a moov box is expected. - * @param sideloadedDrmInitData The {@link DrmInitData} to use for encrypted tracks. If null, the - * pssh boxes (if present) will be used. * @param closedCaptionFormats For tracks that contain SEI messages, the formats of the closed * caption channels to expose. * @param additionalEmsgTrackOutput An extra track output that will receive all emsg messages @@ -249,13 +244,11 @@ public class FragmentedMp4Extractor implements Extractor { @Flags int flags, @Nullable TimestampAdjuster timestampAdjuster, @Nullable Track sideloadedTrack, - @Nullable DrmInitData sideloadedDrmInitData, List closedCaptionFormats, @Nullable TrackOutput additionalEmsgTrackOutput) { this.flags = flags | (sideloadedTrack != null ? FLAG_SIDELOADED : 0); this.timestampAdjuster = timestampAdjuster; this.sideloadedTrack = sideloadedTrack; - this.sideloadedDrmInitData = sideloadedDrmInitData; this.closedCaptionFormats = Collections.unmodifiableList(closedCaptionFormats); this.additionalEmsgTrackOutput = additionalEmsgTrackOutput; eventMessageEncoder = new EventMessageEncoder(); @@ -275,7 +268,7 @@ public class FragmentedMp4Extractor implements Extractor { } @Override - public boolean sniff(ExtractorInput input) throws IOException, InterruptedException { + public boolean sniff(ExtractorInput input) throws IOException { return Sniffer.sniffFragmented(input); } @@ -301,7 +294,6 @@ public class FragmentedMp4Extractor implements Extractor { pendingMetadataSampleBytes = 0; pendingSeekTimeUs = timeUs; containerAtoms.clear(); - isAc4HeaderRequired = false; enterReadingAtomHeaderState(); } @@ -311,8 +303,7 @@ public class FragmentedMp4Extractor implements Extractor { } @Override - public int read(ExtractorInput input, PositionHolder seekPosition) - throws IOException, InterruptedException { + public int read(ExtractorInput input, PositionHolder seekPosition) throws IOException { while (true) { switch (parserState) { case STATE_READING_ATOM_HEADER: @@ -339,7 +330,7 @@ public class FragmentedMp4Extractor implements Extractor { atomHeaderBytesRead = 0; } - private boolean readAtomHeader(ExtractorInput input) throws IOException, InterruptedException { + private boolean readAtomHeader(ExtractorInput input) throws IOException { if (atomHeaderBytesRead == 0) { // Read the standard length atom header. if (!input.readFully(atomHeader.data, 0, Atom.HEADER_SIZE, true)) { @@ -427,7 +418,7 @@ public class FragmentedMp4Extractor implements Extractor { return true; } - private void readAtomPayload(ExtractorInput input) throws IOException, InterruptedException { + private void readAtomPayload(ExtractorInput input) throws IOException { int atomPayloadSize = (int) atomSize - atomHeaderBytesRead; if (atomData != null) { input.readFully(atomData.data, Atom.HEADER_SIZE, atomPayloadSize); @@ -471,8 +462,7 @@ public class FragmentedMp4Extractor implements Extractor { private void onMoovContainerAtomRead(ContainerAtom moov) throws ParserException { Assertions.checkState(sideloadedTrack == null, "Unexpected moov box."); - DrmInitData drmInitData = sideloadedDrmInitData != null ? sideloadedDrmInitData - : getDrmInitDataFromAtoms(moov.leafChildren); + @Nullable DrmInitData drmInitData = getDrmInitDataFromAtoms(moov.leafChildren); // Read declaration of track fragments in the Moov box. ContainerAtom mvex = moov.getContainerAtomOfType(Atom.TYPE_mvex); @@ -495,6 +485,7 @@ public class FragmentedMp4Extractor implements Extractor { for (int i = 0; i < moovContainerChildrenSize; i++) { Atom.ContainerAtom atom = moov.containerChildren.get(i); if (atom.type == Atom.TYPE_trak) { + @Nullable Track track = modifyTrack( AtomParsers.parseTrak( @@ -550,9 +541,8 @@ public class FragmentedMp4Extractor implements Extractor { private void onMoofContainerAtomRead(ContainerAtom moof) throws ParserException { parseMoof(moof, trackBundles, flags, scratchBytes); - // If drm init data is sideloaded, we ignore pssh boxes. - DrmInitData drmInitData = sideloadedDrmInitData != null ? null - : getDrmInitDataFromAtoms(moof.leafChildren); + + @Nullable DrmInitData drmInitData = getDrmInitDataFromAtoms(moof.leafChildren); if (drmInitData != null) { int trackCount = trackBundles.size(); for (int i = 0; i < trackCount; i++) { @@ -675,9 +665,9 @@ public class FragmentedMp4Extractor implements Extractor { private static Pair parseTrex(ParsableByteArray trex) { trex.setPosition(Atom.FULL_HEADER_SIZE); int trackId = trex.readInt(); - int defaultSampleDescriptionIndex = trex.readUnsignedIntToInt() - 1; - int defaultSampleDuration = trex.readUnsignedIntToInt(); - int defaultSampleSize = trex.readUnsignedIntToInt(); + int defaultSampleDescriptionIndex = trex.readInt() - 1; + int defaultSampleDuration = trex.readInt(); + int defaultSampleSize = trex.readInt(); int defaultSampleFlags = trex.readInt(); return Pair.create(trackId, new DefaultSampleValues(defaultSampleDescriptionIndex, @@ -712,7 +702,7 @@ public 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); + @Nullable TrackBundle trackBundle = parseTfhd(tfhd.data, trackBundleArray); if (trackBundle == null) { return; } @@ -721,33 +711,34 @@ public class FragmentedMp4Extractor implements Extractor { long decodeTime = fragment.nextFragmentDecodeTime; trackBundle.reset(); - LeafAtom tfdtAtom = traf.getLeafAtomOfType(Atom.TYPE_tfdt); + @Nullable LeafAtom tfdtAtom = traf.getLeafAtomOfType(Atom.TYPE_tfdt); if (tfdtAtom != null && (flags & FLAG_WORKAROUND_IGNORE_TFDT_BOX) == 0) { decodeTime = parseTfdt(traf.getLeafAtomOfType(Atom.TYPE_tfdt).data); } parseTruns(traf, trackBundle, decodeTime, flags); - TrackEncryptionBox encryptionBox = trackBundle.track - .getSampleDescriptionEncryptionBox(fragment.header.sampleDescriptionIndex); + @Nullable + TrackEncryptionBox encryptionBox = + trackBundle.track.getSampleDescriptionEncryptionBox(fragment.header.sampleDescriptionIndex); - LeafAtom saiz = traf.getLeafAtomOfType(Atom.TYPE_saiz); + @Nullable LeafAtom saiz = traf.getLeafAtomOfType(Atom.TYPE_saiz); if (saiz != null) { parseSaiz(encryptionBox, saiz.data, fragment); } - LeafAtom saio = traf.getLeafAtomOfType(Atom.TYPE_saio); + @Nullable LeafAtom saio = traf.getLeafAtomOfType(Atom.TYPE_saio); if (saio != null) { parseSaio(saio.data, fragment); } - LeafAtom senc = traf.getLeafAtomOfType(Atom.TYPE_senc); + @Nullable LeafAtom senc = traf.getLeafAtomOfType(Atom.TYPE_senc); if (senc != null) { parseSenc(senc.data, fragment); } - LeafAtom sbgp = traf.getLeafAtomOfType(Atom.TYPE_sbgp); - LeafAtom sgpd = traf.getLeafAtomOfType(Atom.TYPE_sgpd); + @Nullable LeafAtom sbgp = traf.getLeafAtomOfType(Atom.TYPE_sbgp); + @Nullable LeafAtom sgpd = traf.getLeafAtomOfType(Atom.TYPE_sgpd); if (sbgp != null && sgpd != null) { parseSgpd(sbgp.data, sgpd.data, encryptionBox != null ? encryptionBox.schemeType : null, fragment); @@ -762,8 +753,9 @@ public class FragmentedMp4Extractor implements Extractor { } } - private static void parseTruns(ContainerAtom traf, TrackBundle trackBundle, long decodeTime, - @Flags int flags) { + private static void parseTruns( + ContainerAtom traf, TrackBundle trackBundle, long decodeTime, @Flags int flags) + throws ParserException { int trunCount = 0; int totalSampleCount = 0; List leafChildren = traf.leafChildren; @@ -863,13 +855,14 @@ public 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}. */ + @Nullable 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 = getTrackBundle(trackBundles, trackId); + @Nullable TrackBundle trackBundle = getTrackBundle(trackBundles, trackId); if (trackBundle == null) { return null; } @@ -882,13 +875,20 @@ public class FragmentedMp4Extractor implements Extractor { DefaultSampleValues defaultSampleValues = trackBundle.defaultSampleValues; int defaultSampleDescriptionIndex = ((atomFlags & 0x02 /* default_sample_description_index_present */) != 0) - ? tfhd.readUnsignedIntToInt() - 1 : defaultSampleValues.sampleDescriptionIndex; - int defaultSampleDuration = ((atomFlags & 0x08 /* default_sample_duration_present */) != 0) - ? tfhd.readUnsignedIntToInt() : defaultSampleValues.duration; - int defaultSampleSize = ((atomFlags & 0x10 /* default_sample_size_present */) != 0) - ? tfhd.readUnsignedIntToInt() : defaultSampleValues.size; - int defaultSampleFlags = ((atomFlags & 0x20 /* default_sample_flags_present */) != 0) - ? tfhd.readUnsignedIntToInt() : defaultSampleValues.flags; + ? tfhd.readInt() - 1 + : defaultSampleValues.sampleDescriptionIndex; + int defaultSampleDuration = + ((atomFlags & 0x08 /* default_sample_duration_present */) != 0) + ? tfhd.readInt() + : defaultSampleValues.duration; + int defaultSampleSize = + ((atomFlags & 0x10 /* default_sample_size_present */) != 0) + ? tfhd.readInt() + : defaultSampleValues.size; + int defaultSampleFlags = + ((atomFlags & 0x20 /* default_sample_flags_present */) != 0) + ? tfhd.readInt() + : defaultSampleValues.flags; trackBundle.fragment.header = new DefaultSampleValues(defaultSampleDescriptionIndex, defaultSampleDuration, defaultSampleSize, defaultSampleFlags); return trackBundle; @@ -921,16 +921,22 @@ public class FragmentedMp4Extractor implements Extractor { /** * Parses a trun atom (defined in 14496-12). * - * @param trackBundle The {@link TrackBundle} that contains the {@link TrackFragment} into - * which parsed data should be placed. + * @param trackBundle The {@link TrackBundle} that contains the {@link TrackFragment} into which + * parsed data should be placed. * @param index Index of the track run in the fragment. * @param decodeTime The decode time of the first sample in the fragment run. * @param flags Flags to allow any required workaround to be executed. * @param trun The trun atom to decode. * @return The starting position of samples for the next run. */ - private static int parseTrun(TrackBundle trackBundle, int index, long decodeTime, - @Flags int flags, ParsableByteArray trun, int trackRunStart) { + private static int parseTrun( + TrackBundle trackBundle, + int index, + long decodeTime, + @Flags int flags, + ParsableByteArray trun, + int trackRunStart) + throws ParserException { trun.setPosition(Atom.HEADER_SIZE); int fullAtom = trun.readInt(); int atomFlags = Atom.parseFullAtomFlags(fullAtom); @@ -948,7 +954,7 @@ public class FragmentedMp4Extractor implements Extractor { boolean firstSampleFlagsPresent = (atomFlags & 0x04 /* first_sample_flags_present */) != 0; int firstSampleFlags = defaultSampleValues.flags; if (firstSampleFlagsPresent) { - firstSampleFlags = trun.readUnsignedIntToInt(); + firstSampleFlags = trun.readInt(); } boolean sampleDurationsPresent = (atomFlags & 0x100 /* sample_duration_present */) != 0; @@ -959,20 +965,20 @@ public class FragmentedMp4Extractor implements Extractor { // Offset to the entire video timeline. In the presence of B-frames this is usually used to // ensure that the first frame's presentation timestamp is zero. - long edtsOffset = 0; + long edtsOffsetUs = 0; // Currently we only support a single edit that moves the entire media timeline (indicated by // duration == 0). Other uses of edit lists are uncommon and unsupported. if (track.editListDurations != null && track.editListDurations.length == 1 && track.editListDurations[0] == 0) { - edtsOffset = + edtsOffsetUs = Util.scaleLargeTimestamp( - track.editListMediaTimes[0], C.MILLIS_PER_SECOND, track.timescale); + track.editListMediaTimes[0], C.MICROS_PER_SECOND, track.timescale); } int[] sampleSizeTable = fragment.sampleSizeTable; - int[] sampleCompositionTimeOffsetTable = fragment.sampleCompositionTimeOffsetTable; - long[] sampleDecodingTimeTable = fragment.sampleDecodingTimeTable; + int[] sampleCompositionTimeOffsetUsTable = fragment.sampleCompositionTimeOffsetUsTable; + long[] sampleDecodingTimeUsTable = fragment.sampleDecodingTimeUsTable; boolean[] sampleIsSyncFrameTable = fragment.sampleIsSyncFrameTable; boolean workaroundEveryVideoFrameIsSyncFrame = track.type == C.TRACK_TYPE_VIDEO @@ -983,9 +989,10 @@ public class FragmentedMp4Extractor implements Extractor { long cumulativeTime = index > 0 ? fragment.nextFragmentDecodeTime : decodeTime; for (int i = trackRunStart; i < trackRunEnd; i++) { // Use trun values if present, otherwise tfhd, otherwise trex. - int sampleDuration = sampleDurationsPresent ? trun.readUnsignedIntToInt() - : defaultSampleValues.duration; - int sampleSize = sampleSizesPresent ? trun.readUnsignedIntToInt() : defaultSampleValues.size; + int sampleDuration = + checkNonNegative(sampleDurationsPresent ? trun.readInt() : defaultSampleValues.duration); + int sampleSize = + checkNonNegative(sampleSizesPresent ? trun.readInt() : defaultSampleValues.size); int sampleFlags = (i == 0 && firstSampleFlagsPresent) ? firstSampleFlags : sampleFlagsPresent ? trun.readInt() : defaultSampleValues.flags; if (sampleCompositionTimeOffsetsPresent) { @@ -995,13 +1002,13 @@ public class FragmentedMp4Extractor implements Extractor { // here, because unsigned integers will still be parsed correctly (unless their top bit is // set, which is never true in practice because sample offsets are always small). int sampleOffset = trun.readInt(); - sampleCompositionTimeOffsetTable[i] = - (int) ((sampleOffset * C.MILLIS_PER_SECOND) / timescale); + sampleCompositionTimeOffsetUsTable[i] = + (int) ((sampleOffset * C.MICROS_PER_SECOND) / timescale); } else { - sampleCompositionTimeOffsetTable[i] = 0; + sampleCompositionTimeOffsetUsTable[i] = 0; } - sampleDecodingTimeTable[i] = - Util.scaleLargeTimestamp(cumulativeTime, C.MILLIS_PER_SECOND, timescale) - edtsOffset; + sampleDecodingTimeUsTable[i] = + Util.scaleLargeTimestamp(cumulativeTime, C.MICROS_PER_SECOND, timescale) - edtsOffsetUs; sampleSizeTable[i] = sampleSize; sampleIsSyncFrameTable[i] = ((sampleFlags >> 16) & 0x1) == 0 && (!workaroundEveryVideoFrameIsSyncFrame || i == 0); @@ -1011,6 +1018,13 @@ public class FragmentedMp4Extractor implements Extractor { return trackRunEnd; } + private static int checkNonNegative(int value) throws ParserException { + if (value < 0) { + throw new ParserException("Unexpected negtive value: " + value); + } + return value; + } + private static void parseUuid(ParsableByteArray uuid, TrackFragment out, byte[] extendedTypeScratch) throws ParserException { uuid.setPosition(Atom.HEADER_SIZE); @@ -1053,8 +1067,12 @@ public class FragmentedMp4Extractor implements Extractor { out.fillEncryptionData(senc); } - private static void parseSgpd(ParsableByteArray sbgp, ParsableByteArray sgpd, String schemeType, - TrackFragment out) throws ParserException { + private static void parseSgpd( + ParsableByteArray sbgp, + ParsableByteArray sgpd, + @Nullable String schemeType, + TrackFragment out) + throws ParserException { sbgp.setPosition(Atom.HEADER_SIZE); int sbgpFullAtom = sbgp.readInt(); if (sbgp.readInt() != SAMPLE_GROUP_TYPE_seig) { @@ -1173,7 +1191,7 @@ public class FragmentedMp4Extractor implements Extractor { new ChunkIndex(sizes, offsets, durationsUs, timesUs)); } - private void readEncryptionData(ExtractorInput input) throws IOException, InterruptedException { + private void readEncryptionData(ExtractorInput input) throws IOException { TrackBundle nextTrackBundle = null; long nextDataOffset = Long.MAX_VALUE; int trackBundlesSize = trackBundles.size(); @@ -1211,12 +1229,11 @@ public class FragmentedMp4Extractor implements Extractor { * @return Whether a sample was read. The read sample may have been output or skipped. False * indicates that there are no samples left to read in the current mdat. * @throws IOException If an error occurs reading from the input. - * @throws InterruptedException If the thread is interrupted. */ - private boolean readSample(ExtractorInput input) throws IOException, InterruptedException { + private boolean readSample(ExtractorInput input) throws IOException { if (parserState == STATE_READING_SAMPLE_START) { if (currentTrackBundle == null) { - TrackBundle currentTrackBundle = getNextFragmentRun(trackBundles); + @Nullable TrackBundle currentTrackBundle = getNextFragmentRun(trackBundles); if (currentTrackBundle == null) { // We've run out of samples in the current mdat. Discard any trailing data and prepare to // read the header of the next atom. @@ -1259,19 +1276,28 @@ public class FragmentedMp4Extractor implements Extractor { sampleSize -= Atom.HEADER_SIZE; input.skipFully(Atom.HEADER_SIZE); } - sampleBytesWritten = currentTrackBundle.outputSampleEncryptionData(); + + if (MimeTypes.AUDIO_AC4.equals(currentTrackBundle.track.format.sampleMimeType)) { + // AC4 samples need to be prefixed with a clear sample header. + sampleBytesWritten = + currentTrackBundle.outputSampleEncryptionData(sampleSize, Ac4Util.SAMPLE_HEADER_SIZE); + Ac4Util.getAc4SampleHeader(sampleSize, scratch); + currentTrackBundle.output.sampleData(scratch, Ac4Util.SAMPLE_HEADER_SIZE); + sampleBytesWritten += Ac4Util.SAMPLE_HEADER_SIZE; + } else { + sampleBytesWritten = + currentTrackBundle.outputSampleEncryptionData(sampleSize, /* clearHeaderSize= */ 0); + } sampleSize += sampleBytesWritten; parserState = STATE_READING_SAMPLE_CONTINUE; sampleCurrentNalBytesRemaining = 0; - isAc4HeaderRequired = - MimeTypes.AUDIO_AC4.equals(currentTrackBundle.track.format.sampleMimeType); } TrackFragment fragment = currentTrackBundle.fragment; Track track = currentTrackBundle.track; TrackOutput output = currentTrackBundle.output; int sampleIndex = currentTrackBundle.currentSampleIndex; - long sampleTimeUs = fragment.getSamplePresentationTime(sampleIndex) * 1000L; + long sampleTimeUs = fragment.getSamplePresentationTimeUs(sampleIndex); if (timestampAdjuster != null) { sampleTimeUs = timestampAdjuster.adjustSampleTimestamp(sampleTimeUs); } @@ -1329,14 +1355,6 @@ public class FragmentedMp4Extractor implements Extractor { } } } else { - if (isAc4HeaderRequired) { - Ac4Util.getAc4SampleHeader(sampleSize, scratch); - int length = scratch.limit(); - output.sampleData(scratch, length); - sampleSize += length; - sampleBytesWritten += length; - isAc4HeaderRequired = false; - } while (sampleBytesWritten < sampleSize) { int writtenBytes = output.sampleData(input, sampleSize - sampleBytesWritten, false); sampleBytesWritten += writtenBytes; @@ -1388,6 +1406,7 @@ public class FragmentedMp4Extractor implements Extractor { * Returns the {@link TrackBundle} whose fragment run has the earliest file position out of those * yet to be consumed, or null if all have been consumed. */ + @Nullable private static TrackBundle getNextFragmentRun(SparseArray trackBundles) { TrackBundle nextTrackBundle = null; long nextTrackRunOffset = Long.MAX_VALUE; @@ -1409,8 +1428,9 @@ public class FragmentedMp4Extractor implements Extractor { } /** Returns DrmInitData from leaf atoms. */ + @Nullable private static DrmInitData getDrmInitDataFromAtoms(List leafChildren) { - ArrayList schemeDatas = null; + @Nullable ArrayList schemeDatas = null; int leafChildrenSize = leafChildren.size(); for (int i = 0; i < leafChildrenSize; i++) { LeafAtom child = leafChildren.get(i); @@ -1419,7 +1439,7 @@ public class FragmentedMp4Extractor implements Extractor { schemeDatas = new ArrayList<>(); } byte[] psshData = child.data.data; - UUID uuid = PsshAtomUtil.parseUuid(psshData); + @Nullable UUID uuid = PsshAtomUtil.parseUuid(psshData); if (uuid == null) { Log.w(TAG, "Skipped pssh atom (failed to extract uuid)"); } else { @@ -1468,8 +1488,11 @@ public class FragmentedMp4Extractor implements Extractor { */ private static final class TrackBundle { + private static final int SINGLE_SUBSAMPLE_ENCRYPTION_DATA_LENGTH = 8; + public final TrackOutput output; public final TrackFragment fragment; + public final ParsableByteArray scratch; public Track track; public DefaultSampleValues defaultSampleValues; @@ -1484,6 +1507,7 @@ public class FragmentedMp4Extractor implements Extractor { public TrackBundle(TrackOutput output) { this.output = output; fragment = new TrackFragment(); + scratch = new ParsableByteArray(); encryptionSignalByte = new ParsableByteArray(1); defaultInitializationVector = new ParsableByteArray(); } @@ -1496,10 +1520,13 @@ public class FragmentedMp4Extractor implements Extractor { } public void updateDrmInitData(DrmInitData drmInitData) { + @Nullable TrackEncryptionBox encryptionBox = track.getSampleDescriptionEncryptionBox(fragment.header.sampleDescriptionIndex); - String schemeType = encryptionBox != null ? encryptionBox.schemeType : null; - output.format(track.format.copyWithDrmInitData(drmInitData.copyWithSchemeType(schemeType))); + @Nullable String schemeType = encryptionBox != null ? encryptionBox.schemeType : null; + DrmInitData updatedDrmInitData = drmInitData.copyWithSchemeType(schemeType); + Format format = track.format.buildUpon().setDrmInitData(updatedDrmInitData).build(); + output.format(format); } /** Resets the current fragment and sample indices. */ @@ -1518,10 +1545,9 @@ public class FragmentedMp4Extractor implements Extractor { * @param timeUs The seek time, in microseconds. */ public void seek(long timeUs) { - long timeMs = C.usToMs(timeUs); int searchIndex = currentSampleIndex; while (searchIndex < fragment.sampleCount - && fragment.getSamplePresentationTime(searchIndex) < timeMs) { + && fragment.getSamplePresentationTimeUs(searchIndex) < timeUs) { if (fragment.sampleIsSyncFrameTable[searchIndex]) { firstSampleToOutputIndex = searchIndex; } @@ -1551,9 +1577,13 @@ public class FragmentedMp4Extractor implements Extractor { /** * Outputs the encryption data for the current sample. * + * @param sampleSize The size of the current sample in bytes, excluding any additional clear + * header that will be prefixed to the sample by the extractor. + * @param clearHeaderSize The size of a clear header that will be prefixed to the sample by the + * extractor, or 0. * @return The number of written bytes. */ - public int outputSampleEncryptionData() { + public int outputSampleEncryptionData(int sampleSize, int clearHeaderSize) { TrackEncryptionBox encryptionBox = getEncryptionBoxIfEncrypted(); if (encryptionBox == null) { return 0; @@ -1572,30 +1602,73 @@ public class FragmentedMp4Extractor implements Extractor { vectorSize = initVectorData.length; } - boolean subsampleEncryption = fragment.sampleHasSubsampleEncryptionTable(currentSampleIndex); + boolean haveSubsampleEncryptionTable = + fragment.sampleHasSubsampleEncryptionTable(currentSampleIndex); + boolean writeSubsampleEncryptionData = haveSubsampleEncryptionTable || clearHeaderSize != 0; // Write the signal byte, containing the vector size and the subsample encryption flag. - encryptionSignalByte.data[0] = (byte) (vectorSize | (subsampleEncryption ? 0x80 : 0)); + encryptionSignalByte.data[0] = + (byte) (vectorSize | (writeSubsampleEncryptionData ? 0x80 : 0)); encryptionSignalByte.setPosition(0); - output.sampleData(encryptionSignalByte, 1); + output.sampleData(encryptionSignalByte, 1, TrackOutput.SAMPLE_DATA_PART_ENCRYPTION); // Write the vector. - output.sampleData(initializationVectorData, vectorSize); - // If we don't have subsample encryption data, we're done. - if (!subsampleEncryption) { + output.sampleData( + initializationVectorData, vectorSize, TrackOutput.SAMPLE_DATA_PART_ENCRYPTION); + + if (!writeSubsampleEncryptionData) { return 1 + vectorSize; } - // Write the subsample encryption data. + + if (!haveSubsampleEncryptionTable) { + // The sample is fully encrypted, except for the additional clear header that the extractor + // is going to prefix. We need to synthesize subsample encryption data that takes the header + // into account. + scratch.reset(SINGLE_SUBSAMPLE_ENCRYPTION_DATA_LENGTH); + // subsampleCount = 1 (unsigned short) + scratch.data[0] = (byte) 0; + scratch.data[1] = (byte) 1; + // clearDataSize = clearHeaderSize (unsigned short) + scratch.data[2] = (byte) ((clearHeaderSize >> 8) & 0xFF); + scratch.data[3] = (byte) (clearHeaderSize & 0xFF); + // encryptedDataSize = sampleSize (unsigned int) + scratch.data[4] = (byte) ((sampleSize >> 24) & 0xFF); + scratch.data[5] = (byte) ((sampleSize >> 16) & 0xFF); + scratch.data[6] = (byte) ((sampleSize >> 8) & 0xFF); + scratch.data[7] = (byte) (sampleSize & 0xFF); + output.sampleData( + scratch, + SINGLE_SUBSAMPLE_ENCRYPTION_DATA_LENGTH, + TrackOutput.SAMPLE_DATA_PART_ENCRYPTION); + return 1 + vectorSize + SINGLE_SUBSAMPLE_ENCRYPTION_DATA_LENGTH; + } + ParsableByteArray subsampleEncryptionData = fragment.sampleEncryptionData; int subsampleCount = subsampleEncryptionData.readUnsignedShort(); subsampleEncryptionData.skipBytes(-2); int subsampleDataLength = 2 + 6 * subsampleCount; - output.sampleData(subsampleEncryptionData, subsampleDataLength); + + if (clearHeaderSize != 0) { + // We need to account for the additional clear header by adding clearHeaderSize to + // clearDataSize for the first subsample specified in the subsample encryption data. + scratch.reset(subsampleDataLength); + scratch.readBytes(subsampleEncryptionData.data, /* offset= */ 0, subsampleDataLength); + subsampleEncryptionData.skipBytes(subsampleDataLength); + + int clearDataSize = (scratch.data[2] & 0xFF) << 8 | (scratch.data[3] & 0xFF); + int adjustedClearDataSize = clearDataSize + clearHeaderSize; + scratch.data[2] = (byte) ((adjustedClearDataSize >> 8) & 0xFF); + scratch.data[3] = (byte) (adjustedClearDataSize & 0xFF); + subsampleEncryptionData = scratch; + } + + output.sampleData( + subsampleEncryptionData, subsampleDataLength, TrackOutput.SAMPLE_DATA_PART_ENCRYPTION); return 1 + vectorSize + subsampleDataLength; } /** Skips the encryption data for the current sample. */ private void skipSampleEncryptionData() { - TrackEncryptionBox encryptionBox = getEncryptionBoxIfEncrypted(); + @Nullable TrackEncryptionBox encryptionBox = getEncryptionBoxIfEncrypted(); if (encryptionBox == null) { return; } @@ -1609,8 +1682,10 @@ public class FragmentedMp4Extractor implements Extractor { } } + @Nullable private TrackEncryptionBox getEncryptionBoxIfEncrypted() { int sampleDescriptionIndex = fragment.header.sampleDescriptionIndex; + @Nullable TrackEncryptionBox encryptionBox = fragment.trackEncryptionBox != null ? fragment.trackEncryptionBox diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/MdtaMetadataEntry.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp4/MdtaMetadataEntry.java similarity index 97% rename from library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/MdtaMetadataEntry.java rename to library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp4/MdtaMetadataEntry.java index e50fbd54f7..5ad2b63b4c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/MdtaMetadataEntry.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp4/MdtaMetadataEntry.java @@ -47,8 +47,7 @@ public final class MdtaMetadataEntry implements Metadata.Entry { private MdtaMetadataEntry(Parcel in) { key = Util.castNonNull(in.readString()); - value = new byte[in.readInt()]; - in.readByteArray(value); + value = Util.castNonNull(in.createByteArray()); localeIndicator = in.readInt(); typeIndicator = in.readInt(); } @@ -88,7 +87,6 @@ public final class MdtaMetadataEntry implements Metadata.Entry { @Override public void writeToParcel(Parcel dest, int flags) { dest.writeString(key); - dest.writeInt(value.length); dest.writeByteArray(value); dest.writeInt(localeIndicator); dest.writeInt(typeIndicator); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/MetadataUtil.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp4/MetadataUtil.java similarity index 74% rename from library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/MetadataUtil.java rename to library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp4/MetadataUtil.java index bec2cdbb5f..365e336e65 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/MetadataUtil.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp4/MetadataUtil.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.extractor.mp4; import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.extractor.GaplessInfoHolder; @@ -27,7 +28,6 @@ import com.google.android.exoplayer2.metadata.id3.InternalFrame; import com.google.android.exoplayer2.metadata.id3.TextInformationFrame; import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.ParsableByteArray; -import java.nio.ByteBuffer; /** Utilities for handling metadata in MP4. */ /* package */ final class MetadataUtil { @@ -74,33 +74,206 @@ import java.nio.ByteBuffer; 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. - "Blues", "Classic Rock", "Country", "Dance", "Disco", "Funk", "Grunge", "Hip-Hop", "Jazz", - "Metal", "New Age", "Oldies", "Other", "Pop", "R&B", "Rap", "Reggae", "Rock", "Techno", - "Industrial", "Alternative", "Ska", "Death Metal", "Pranks", "Soundtrack", "Euro-Techno", - "Ambient", "Trip-Hop", "Vocal", "Jazz+Funk", "Fusion", "Trance", "Classical", "Instrumental", - "Acid", "House", "Game", "Sound Clip", "Gospel", "Noise", "AlternRock", "Bass", "Soul", - "Punk", "Space", "Meditative", "Instrumental Pop", "Instrumental Rock", "Ethnic", "Gothic", - "Darkwave", "Techno-Industrial", "Electronic", "Pop-Folk", "Eurodance", "Dream", - "Southern Rock", "Comedy", "Cult", "Gangsta", "Top 40", "Christian Rap", "Pop/Funk", "Jungle", - "Native American", "Cabaret", "New Wave", "Psychadelic", "Rave", "Showtunes", "Trailer", - "Lo-Fi", "Tribal", "Acid Punk", "Acid Jazz", "Polka", "Retro", "Musical", "Rock & Roll", - "Hard Rock", - // These were made up by the authors of Winamp and later added to the ID3 spec. - "Folk", "Folk-Rock", "National Folk", "Swing", "Fast Fusion", "Bebob", "Latin", "Revival", - "Celtic", "Bluegrass", "Avantgarde", "Gothic Rock", "Progressive Rock", "Psychedelic Rock", - "Symphonic Rock", "Slow Rock", "Big Band", "Chorus", "Easy Listening", "Acoustic", "Humour", - "Speech", "Chanson", "Opera", "Chamber Music", "Sonata", "Symphony", "Booty Bass", "Primus", - "Porn Groove", "Satire", "Slow Jam", "Club", "Tango", "Samba", "Folklore", "Ballad", - "Power Ballad", "Rhythmic Soul", "Freestyle", "Duet", "Punk Rock", "Drum Solo", "A capella", - "Euro-House", "Dance Hall", - // These were med up by the authors of Winamp but have not been added to the ID3 spec. - "Goa", "Drum & Bass", "Club-House", "Hardcore", "Terror", "Indie", "BritPop", "Negerpunk", - "Polsk Punk", "Beat", "Christian Gangsta Rap", "Heavy Metal", "Black Metal", "Crossover", - "Contemporary Christian", "Christian Rock", "Merengue", "Salsa", "Thrash Metal", "Anime", - "Jpop", "Synthpop" - }; + @VisibleForTesting + /* package */ static final String[] STANDARD_GENRES = + new String[] { + // These are the official ID3v1 genres. + "Blues", + "Classic Rock", + "Country", + "Dance", + "Disco", + "Funk", + "Grunge", + "Hip-Hop", + "Jazz", + "Metal", + "New Age", + "Oldies", + "Other", + "Pop", + "R&B", + "Rap", + "Reggae", + "Rock", + "Techno", + "Industrial", + "Alternative", + "Ska", + "Death Metal", + "Pranks", + "Soundtrack", + "Euro-Techno", + "Ambient", + "Trip-Hop", + "Vocal", + "Jazz+Funk", + "Fusion", + "Trance", + "Classical", + "Instrumental", + "Acid", + "House", + "Game", + "Sound Clip", + "Gospel", + "Noise", + "AlternRock", + "Bass", + "Soul", + "Punk", + "Space", + "Meditative", + "Instrumental Pop", + "Instrumental Rock", + "Ethnic", + "Gothic", + "Darkwave", + "Techno-Industrial", + "Electronic", + "Pop-Folk", + "Eurodance", + "Dream", + "Southern Rock", + "Comedy", + "Cult", + "Gangsta", + "Top 40", + "Christian Rap", + "Pop/Funk", + "Jungle", + "Native American", + "Cabaret", + "New Wave", + "Psychadelic", + "Rave", + "Showtunes", + "Trailer", + "Lo-Fi", + "Tribal", + "Acid Punk", + "Acid Jazz", + "Polka", + "Retro", + "Musical", + "Rock & Roll", + "Hard Rock", + // Genres made up by the authors of Winamp (v1.91) and later added to the ID3 spec. + "Folk", + "Folk-Rock", + "National Folk", + "Swing", + "Fast Fusion", + "Bebob", + "Latin", + "Revival", + "Celtic", + "Bluegrass", + "Avantgarde", + "Gothic Rock", + "Progressive Rock", + "Psychedelic Rock", + "Symphonic Rock", + "Slow Rock", + "Big Band", + "Chorus", + "Easy Listening", + "Acoustic", + "Humour", + "Speech", + "Chanson", + "Opera", + "Chamber Music", + "Sonata", + "Symphony", + "Booty Bass", + "Primus", + "Porn Groove", + "Satire", + "Slow Jam", + "Club", + "Tango", + "Samba", + "Folklore", + "Ballad", + "Power Ballad", + "Rhythmic Soul", + "Freestyle", + "Duet", + "Punk Rock", + "Drum Solo", + "A capella", + "Euro-House", + "Dance Hall", + // Genres made up by the authors of Winamp (v1.91) but have not been added to the ID3 spec. + "Goa", + "Drum & Bass", + "Club-House", + "Hardcore", + "Terror", + "Indie", + "BritPop", + "Afro-Punk", + "Polsk Punk", + "Beat", + "Christian Gangsta Rap", + "Heavy Metal", + "Black Metal", + "Crossover", + "Contemporary Christian", + "Christian Rock", + "Merengue", + "Salsa", + "Thrash Metal", + "Anime", + "Jpop", + "Synthpop", + // Genres made up by the authors of Winamp (v5.6) but have not been added to the ID3 spec. + "Abstract", + "Art Rock", + "Baroque", + "Bhangra", + "Big beat", + "Breakbeat", + "Chillout", + "Downtempo", + "Dub", + "EBM", + "Eclectic", + "Electro", + "Electroclash", + "Emo", + "Experimental", + "Garage", + "Global", + "IDM", + "Illbient", + "Industro-Goth", + "Jam Band", + "Krautrock", + "Leftfield", + "Lounge", + "Math Rock", + "New Romantic", + "Nu-Breakz", + "Post-Punk", + "Post-Rock", + "Psytrance", + "Shoegaze", + "Space Rock", + "Trop Rock", + "World Music", + "Neoclassical", + "Audiobook", + "Audio theatre", + "Neue Deutsche Welle", + "Podcast", + "Indie-Rock", + "G-Funk", + "Dubstep", + "Garage Rock", + "Psybient" + }; private static final String LANGUAGE_UNDEFINED = "und"; @@ -108,29 +281,25 @@ import java.nio.ByteBuffer; private static final int TYPE_TOP_BYTE_REPLACEMENT = 0xFD; // Truncated value of \uFFFD. private static final String MDTA_KEY_ANDROID_CAPTURE_FPS = "com.android.capture.fps"; - private static final int MDTA_TYPE_INDICATOR_FLOAT = 23; private MetadataUtil() {} - /** - * Returns a {@link Format} that is the same as the input format but includes information from the - * specified sources of metadata. - */ - public static Format getFormatWithMetadata( + /** Updates a {@link Format.Builder} to include metadata from the provided sources. */ + public static void setFormatMetadata( int trackType, - Format format, @Nullable Metadata udtaMetadata, @Nullable Metadata mdtaMetadata, - GaplessInfoHolder gaplessInfoHolder) { + GaplessInfoHolder gaplessInfoHolder, + Format.Builder formatBuilder) { if (trackType == C.TRACK_TYPE_AUDIO) { if (gaplessInfoHolder.hasGaplessInfo()) { - format = - format.copyWithGaplessInfo( - gaplessInfoHolder.encoderDelay, gaplessInfoHolder.encoderPadding); + formatBuilder + .setEncoderDelay(gaplessInfoHolder.encoderDelay) + .setEncoderPadding(gaplessInfoHolder.encoderPadding); } // We assume all udta metadata is associated with the audio track. if (udtaMetadata != null) { - format = format.copyWithMetadata(udtaMetadata); + formatBuilder.setMetadata(udtaMetadata); } } else if (trackType == C.TRACK_TYPE_VIDEO && mdtaMetadata != null) { // Populate only metadata keys that are known to be specific to video. @@ -138,20 +307,12 @@ import java.nio.ByteBuffer; Metadata.Entry entry = mdtaMetadata.get(i); if (entry instanceof MdtaMetadataEntry) { MdtaMetadataEntry mdtaMetadataEntry = (MdtaMetadataEntry) entry; - if (MDTA_KEY_ANDROID_CAPTURE_FPS.equals(mdtaMetadataEntry.key) - && mdtaMetadataEntry.typeIndicator == MDTA_TYPE_INDICATOR_FLOAT) { - try { - float fps = ByteBuffer.wrap(mdtaMetadataEntry.value).asFloatBuffer().get(); - format = format.copyWithFrameRate(fps); - format = format.copyWithMetadata(new Metadata(mdtaMetadataEntry)); - } catch (NumberFormatException e) { - Log.w(TAG, "Ignoring invalid framerate"); - } + if (MDTA_KEY_ANDROID_CAPTURE_FPS.equals(mdtaMetadataEntry.key)) { + formatBuilder.setMetadata(new Metadata(mdtaMetadataEntry)); } } } } - return format; } /** @@ -334,8 +495,11 @@ import java.nio.ByteBuffer; @Nullable private static TextInformationFrame parseStandardGenreAttribute(ParsableByteArray data) { int genreCode = parseUint8AttributeValue(data); - String genreString = (0 < genreCode && genreCode <= STANDARD_GENRES.length) - ? STANDARD_GENRES[genreCode - 1] : null; + @Nullable + String genreString = + (0 < genreCode && genreCode <= STANDARD_GENRES.length) + ? STANDARD_GENRES[genreCode - 1] + : null; if (genreString != null) { return new TextInformationFrame("TCON", /* description= */ null, genreString); } @@ -350,7 +514,7 @@ import java.nio.ByteBuffer; if (atomType == Atom.TYPE_data) { int fullVersionInt = data.readInt(); int flags = Atom.parseFullAtomFlags(fullVersionInt); - String mimeType = flags == 13 ? "image/jpeg" : flags == 14 ? "image/png" : null; + @Nullable String mimeType = flags == 13 ? "image/jpeg" : flags == 14 ? "image/png" : null; if (mimeType == null) { Log.w(TAG, "Unrecognized cover art flags: " + flags); return null; @@ -370,8 +534,8 @@ import java.nio.ByteBuffer; @Nullable private static Id3Frame parseInternalAttribute(ParsableByteArray data, int endPosition) { - String domain = null; - String name = null; + @Nullable String domain = null; + @Nullable String name = null; int dataAtomPosition = -1; int dataAtomSize = -1; while (data.getPosition() < endPosition) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java similarity index 93% rename from library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java rename to library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java index 16f5b1fb29..48c7e3e122 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.extractor.mp4; import androidx.annotation.IntDef; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.ParserException; @@ -42,6 +43,7 @@ import java.lang.annotation.RetentionPolicy; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.List; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /** * Extracts data from the MP4 container format. @@ -105,15 +107,15 @@ public final class Mp4Extractor implements Extractor, SeekMap { private int atomType; private long atomSize; private int atomHeaderBytesRead; - private ParsableByteArray atomData; + @Nullable private ParsableByteArray atomData; private int sampleTrackIndex; + private int sampleBytesRead; private int sampleBytesWritten; private int sampleCurrentNalBytesRemaining; - private boolean isAc4HeaderRequired; // Extractor outputs. - private ExtractorOutput extractorOutput; + private @MonotonicNonNull ExtractorOutput extractorOutput; private Mp4Track[] tracks; private long[][] accumulatedSampleSizes; private int firstVideoTrackIndex; @@ -144,7 +146,7 @@ public final class Mp4Extractor implements Extractor, SeekMap { } @Override - public boolean sniff(ExtractorInput input) throws IOException, InterruptedException { + public boolean sniff(ExtractorInput input) throws IOException { return Sniffer.sniffUnfragmented(input); } @@ -158,9 +160,9 @@ public final class Mp4Extractor implements Extractor, SeekMap { containerAtoms.clear(); atomHeaderBytesRead = 0; sampleTrackIndex = C.INDEX_UNSET; + sampleBytesRead = 0; sampleBytesWritten = 0; sampleCurrentNalBytesRemaining = 0; - isAc4HeaderRequired = false; if (position == 0) { enterReadingAtomHeaderState(); } else if (tracks != null) { @@ -174,8 +176,7 @@ public final class Mp4Extractor implements Extractor, SeekMap { } @Override - public int read(ExtractorInput input, PositionHolder seekPosition) - throws IOException, InterruptedException { + public int read(ExtractorInput input, PositionHolder seekPosition) throws IOException { while (true) { switch (parserState) { case STATE_READING_ATOM_HEADER: @@ -268,7 +269,7 @@ public final class Mp4Extractor implements Extractor, SeekMap { atomHeaderBytesRead = 0; } - private boolean readAtomHeader(ExtractorInput input) throws IOException, InterruptedException { + private boolean readAtomHeader(ExtractorInput input) throws IOException { if (atomHeaderBytesRead == 0) { // Read the standard length atom header. if (!input.readFully(atomHeader.data, 0, Atom.HEADER_SIZE, true)) { @@ -290,8 +291,11 @@ public final class Mp4Extractor implements Extractor, SeekMap { // The atom extends to the end of the file. Note that if the atom is within a container we can // work out its size even if the input length is unknown. long endPosition = input.getLength(); - if (endPosition == C.LENGTH_UNSET && !containerAtoms.isEmpty()) { - endPosition = containerAtoms.peek().endPosition; + if (endPosition == C.LENGTH_UNSET) { + @Nullable ContainerAtom containerAtom = containerAtoms.peek(); + if (containerAtom != null) { + endPosition = containerAtom.endPosition; + } } if (endPosition != C.LENGTH_UNSET) { atomSize = endPosition - input.getPosition() + atomHeaderBytesRead; @@ -304,13 +308,13 @@ public final class Mp4Extractor implements Extractor, SeekMap { if (shouldParseContainerAtom(atomType)) { long endPosition = input.getPosition() + atomSize - atomHeaderBytesRead; + if (atomSize != atomHeaderBytesRead && atomType == Atom.TYPE_meta) { + maybeSkipRemainingMetaAtomHeaderBytes(input); + } containerAtoms.push(new ContainerAtom(atomType, endPosition)); if (atomSize == atomHeaderBytesRead) { processAtomEnded(endPosition); } else { - if (atomType == Atom.TYPE_meta) { - maybeSkipRemainingMetaAtomHeaderBytes(input); - } // Start reading the first child atom. enterReadingAtomHeaderState(); } @@ -336,7 +340,7 @@ public final class Mp4Extractor implements Extractor, SeekMap { * restart loading at the position in {@code positionHolder}. Otherwise, the atom is read/skipped. */ private boolean readAtomPayload(ExtractorInput input, PositionHolder positionHolder) - throws IOException, InterruptedException { + throws IOException { long atomPayloadSize = atomSize - atomHeaderBytesRead; long atomEndPosition = input.getPosition() + atomPayloadSize; boolean seekRequired = false; @@ -386,17 +390,17 @@ public final class Mp4Extractor implements Extractor, SeekMap { List tracks = new ArrayList<>(); // Process metadata. - Metadata udtaMetadata = null; + @Nullable Metadata udtaMetadata = null; GaplessInfoHolder gaplessInfoHolder = new GaplessInfoHolder(); - Atom.LeafAtom udta = moov.getLeafAtomOfType(Atom.TYPE_udta); + @Nullable Atom.LeafAtom udta = moov.getLeafAtomOfType(Atom.TYPE_udta); if (udta != null) { udtaMetadata = AtomParsers.parseUdta(udta, isQuickTime); if (udtaMetadata != null) { gaplessInfoHolder.setFromMetadata(udtaMetadata); } } - Metadata mdtaMetadata = null; - Atom.ContainerAtom meta = moov.getContainerAtomOfType(Atom.TYPE_meta); + @Nullable Metadata mdtaMetadata = null; + @Nullable Atom.ContainerAtom meta = moov.getContainerAtomOfType(Atom.TYPE_meta); if (meta != null) { mdtaMetadata = AtomParsers.parseMdtaFromMeta(meta); } @@ -418,17 +422,17 @@ public final class Mp4Extractor implements Extractor, SeekMap { // Each sample has up to three bytes of overhead for the start code that replaces its length. // Allow ten source samples per output sample, like the platform extractor. int maxInputSize = trackSampleTable.maximumSize + 3 * 10; - Format format = track.format.copyWithMaxInputSize(maxInputSize); + Format.Builder formatBuilder = track.format.buildUpon(); + formatBuilder.setMaxInputSize(maxInputSize); if (track.type == C.TRACK_TYPE_VIDEO && trackDurationUs > 0 && trackSampleTable.sampleCount > 1) { float frameRate = trackSampleTable.sampleCount / (trackDurationUs / 1000000f); - format = format.copyWithFrameRate(frameRate); + formatBuilder.setFrameRate(frameRate); } - format = - MetadataUtil.getFormatWithMetadata( - track.type, format, udtaMetadata, mdtaMetadata, gaplessInfoHolder); - mp4Track.trackOutput.format(format); + MetadataUtil.setFormatMetadata( + track.type, udtaMetadata, mdtaMetadata, gaplessInfoHolder, formatBuilder); + mp4Track.trackOutput.format(formatBuilder.build()); if (track.type == C.TRACK_TYPE_VIDEO && firstVideoTrackIndex == C.INDEX_UNSET) { firstVideoTrackIndex = tracks.size(); @@ -453,6 +457,7 @@ public final class Mp4Extractor implements Extractor, SeekMap { if (atom.type != Atom.TYPE_trak) { continue; } + @Nullable Track track = AtomParsers.parseTrak( atom, @@ -479,37 +484,33 @@ public final class Mp4Extractor implements Extractor, SeekMap { /** * Attempts to extract the next sample in the current mdat atom for the specified track. - *

      - * Returns {@link #RESULT_SEEK} if the source should be reloaded from the position in - * {@code positionHolder}. - *

      - * Returns {@link #RESULT_END_OF_INPUT} if no samples are left. Otherwise, returns - * {@link #RESULT_CONTINUE}. + * + *

      Returns {@link #RESULT_SEEK} if the source should be reloaded from the position in {@code + * positionHolder}. + * + *

      Returns {@link #RESULT_END_OF_INPUT} if no samples are left. Otherwise, returns {@link + * #RESULT_CONTINUE}. * * @param input The {@link ExtractorInput} from which to read data. * @param positionHolder If {@link #RESULT_SEEK} is returned, this holder is updated to hold the * position of the required data. * @return One of the {@code RESULT_*} flags in {@link Extractor}. * @throws IOException If an error occurs reading from the input. - * @throws InterruptedException If the thread is interrupted. */ - private int readSample(ExtractorInput input, PositionHolder positionHolder) - throws IOException, InterruptedException { + private int readSample(ExtractorInput input, PositionHolder positionHolder) throws IOException { long inputPosition = input.getPosition(); if (sampleTrackIndex == C.INDEX_UNSET) { sampleTrackIndex = getTrackIndexOfNextReadSample(inputPosition); if (sampleTrackIndex == C.INDEX_UNSET) { return RESULT_END_OF_INPUT; } - isAc4HeaderRequired = - MimeTypes.AUDIO_AC4.equals(tracks[sampleTrackIndex].track.format.sampleMimeType); } Mp4Track track = tracks[sampleTrackIndex]; TrackOutput trackOutput = track.trackOutput; int sampleIndex = track.sampleIndex; long position = track.sampleTable.offsets[sampleIndex]; int sampleSize = track.sampleTable.sizes[sampleIndex]; - long skipAmount = position - inputPosition + sampleBytesWritten; + long skipAmount = position - inputPosition + sampleBytesRead; if (skipAmount < 0 || skipAmount >= RELOAD_MINIMUM_SEEK_DISTANCE) { positionHolder.position = position; return RESULT_SEEK; @@ -537,6 +538,7 @@ public final class Mp4Extractor implements Extractor, SeekMap { if (sampleCurrentNalBytesRemaining == 0) { // Read the NAL length so that we know where we find the next one. input.readFully(nalLengthData, nalUnitLengthFieldLengthDiff, nalUnitLengthFieldLength); + sampleBytesRead += nalUnitLengthFieldLength; nalLength.setPosition(0); int nalLengthInt = nalLength.readInt(); if (nalLengthInt < 0) { @@ -551,21 +553,23 @@ public final class Mp4Extractor implements Extractor, SeekMap { } else { // Write the payload of the NAL unit. int writtenBytes = trackOutput.sampleData(input, sampleCurrentNalBytesRemaining, false); + sampleBytesRead += writtenBytes; sampleBytesWritten += writtenBytes; sampleCurrentNalBytesRemaining -= writtenBytes; } } } else { - if (isAc4HeaderRequired) { - Ac4Util.getAc4SampleHeader(sampleSize, scratch); - int length = scratch.limit(); - trackOutput.sampleData(scratch, length); - sampleSize += length; - sampleBytesWritten += length; - isAc4HeaderRequired = false; + if (MimeTypes.AUDIO_AC4.equals(track.track.format.sampleMimeType)) { + if (sampleBytesWritten == 0) { + Ac4Util.getAc4SampleHeader(sampleSize, scratch); + trackOutput.sampleData(scratch, Ac4Util.SAMPLE_HEADER_SIZE); + sampleBytesWritten += Ac4Util.SAMPLE_HEADER_SIZE; + } + sampleSize += Ac4Util.SAMPLE_HEADER_SIZE; } while (sampleBytesWritten < sampleSize) { int writtenBytes = trackOutput.sampleData(input, sampleSize - sampleBytesWritten, false); + sampleBytesRead += writtenBytes; sampleBytesWritten += writtenBytes; sampleCurrentNalBytesRemaining -= writtenBytes; } @@ -574,6 +578,7 @@ public final class Mp4Extractor implements Extractor, SeekMap { track.sampleTable.flags[sampleIndex], sampleSize, 0, null); track.sampleIndex++; sampleTrackIndex = C.INDEX_UNSET; + sampleBytesRead = 0; sampleBytesWritten = 0; sampleCurrentNalBytesRemaining = 0; return RESULT_CONTINUE; @@ -655,8 +660,7 @@ public final class Mp4Extractor implements Extractor, SeekMap { * we can't rely on the file type though. Instead we must check the 8 bytes after the common * header bytes ourselves. */ - private void maybeSkipRemainingMetaAtomHeaderBytes(ExtractorInput input) - throws IOException, InterruptedException { + private void maybeSkipRemainingMetaAtomHeaderBytes(ExtractorInput input) throws IOException { scratch.reset(8); // Peek the next 8 bytes which can be either // (iso) [1 byte version + 3 bytes flags][4 byte size of next atom] diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/PsshAtomUtil.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp4/PsshAtomUtil.java similarity index 93% rename from library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/PsshAtomUtil.java rename to library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp4/PsshAtomUtil.java index b9ecaf174c..b4f537f0ce 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/PsshAtomUtil.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp4/PsshAtomUtil.java @@ -49,8 +49,6 @@ public final class PsshAtomUtil { * @param data The scheme specific data. * @return The PSSH atom. */ - // dereference of possibly-null reference keyId - @SuppressWarnings({"ParameterNotNullable", "nullness:dereference.of.nullable"}) public static byte[] buildPsshAtom( UUID systemId, @Nullable UUID[] keyIds, @Nullable byte[] data) { int dataLength = data != null ? data.length : 0; @@ -97,8 +95,9 @@ public final class PsshAtomUtil { * @return The parsed UUID. Null if the input is not a valid PSSH atom, or if the PSSH atom has an * unsupported version. */ - public static @Nullable UUID parseUuid(byte[] atom) { - PsshAtom parsedAtom = parsePsshAtom(atom); + @Nullable + public static UUID parseUuid(byte[] atom) { + @Nullable PsshAtom parsedAtom = parsePsshAtom(atom); if (parsedAtom == null) { return null; } @@ -115,7 +114,7 @@ public final class PsshAtomUtil { * an unsupported version. */ public static int parseVersion(byte[] atom) { - PsshAtom parsedAtom = parsePsshAtom(atom); + @Nullable PsshAtom parsedAtom = parsePsshAtom(atom); if (parsedAtom == null) { return -1; } @@ -133,8 +132,9 @@ public final class PsshAtomUtil { * @return The parsed scheme specific data. Null if the input is not a valid PSSH atom, or if the * PSSH atom has an unsupported version, or if the PSSH atom does not match the passed UUID. */ - public static @Nullable byte[] parseSchemeSpecificData(byte[] atom, UUID uuid) { - PsshAtom parsedAtom = parsePsshAtom(atom); + @Nullable + public static byte[] parseSchemeSpecificData(byte[] atom, UUID uuid) { + @Nullable PsshAtom parsedAtom = parsePsshAtom(atom); if (parsedAtom == null) { return null; } @@ -153,7 +153,8 @@ public final class PsshAtomUtil { * has an unsupported version. */ // TODO: Support parsing of the key ids for version 1 PSSH atoms. - private static @Nullable PsshAtom parsePsshAtom(byte[] atom) { + @Nullable + private static PsshAtom parsePsshAtom(byte[] atom) { ParsableByteArray atomData = new ParsableByteArray(atom); if (atomData.limit() < Atom.FULL_HEADER_SIZE + 16 /* UUID */ + 4 /* DataSize */) { // Data too short. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Sniffer.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp4/Sniffer.java similarity index 91% rename from library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Sniffer.java rename to library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp4/Sniffer.java index 95193785c0..c661e7be07 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Sniffer.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp4/Sniffer.java @@ -66,10 +66,8 @@ import java.io.IOException; * @param input The extractor input from which to peek data. The peek position will be modified. * @return Whether the input appears to be in the fragmented MP4 format. * @throws IOException If an error occurs reading from the input. - * @throws InterruptedException If the thread has been interrupted. */ - public static boolean sniffFragmented(ExtractorInput input) - throws IOException, InterruptedException { + public static boolean sniffFragmented(ExtractorInput input) throws IOException { return sniffInternal(input, true); } @@ -80,15 +78,13 @@ import java.io.IOException; * @param input The extractor input from which to peek data. The peek position will be modified. * @return Whether the input appears to be in the unfragmented MP4 format. * @throws IOException If an error occurs reading from the input. - * @throws InterruptedException If the thread has been interrupted. */ - public static boolean sniffUnfragmented(ExtractorInput input) - throws IOException, InterruptedException { + public static boolean sniffUnfragmented(ExtractorInput input) throws IOException { return sniffInternal(input, false); } private static boolean sniffInternal(ExtractorInput input, boolean fragmented) - throws IOException, InterruptedException { + throws IOException { long inputLength = input.getLength(); int bytesToSearch = (int) (inputLength == C.LENGTH_UNSET || inputLength > SEARCH_LENGTH ? SEARCH_LENGTH : inputLength); @@ -118,10 +114,6 @@ import java.io.IOException; } } - if (inputLength != C.LENGTH_UNSET && bytesSearched + atomSize > inputLength) { - // The file is invalid because the atom extends past the end of the file. - return false; - } if (atomSize < headerSize) { // The file is invalid because the atom size is too small for its header. return false; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Track.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp4/Track.java similarity index 97% rename from library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Track.java rename to library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp4/Track.java index 0a21ddd3a3..7676926c4d 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Track.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp4/Track.java @@ -129,8 +129,6 @@ public final class Track { : sampleDescriptionEncryptionBoxes[sampleDescriptionIndex]; } - // incompatible types in argument. - @SuppressWarnings("nullness:argument.type.incompatible") public Track copyWithFormat(Format format) { return new Track( id, diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/TrackEncryptionBox.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp4/TrackEncryptionBox.java similarity index 100% rename from library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/TrackEncryptionBox.java rename to library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp4/TrackEncryptionBox.java diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/TrackFragment.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp4/TrackFragment.java similarity index 74% rename from library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/TrackFragment.java rename to library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp4/TrackFragment.java index 51ec2bf282..b214114340 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/TrackFragment.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp4/TrackFragment.java @@ -15,19 +15,19 @@ */ package com.google.android.exoplayer2.extractor.mp4; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.util.ParsableByteArray; import java.io.IOException; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /** * A holder for information corresponding to a single fragment of an mp4 file. */ /* package */ final class TrackFragment { - /** - * The default values for samples from the track fragment header. - */ - public DefaultSampleValues header; + /** The default values for samples from the track fragment header. */ + public @MonotonicNonNull DefaultSampleValues header; /** * The position (byte offset) of the start of fragment. */ @@ -60,14 +60,10 @@ import java.io.IOException; * The size of each sample in the fragment. */ public int[] sampleSizeTable; - /** - * The composition time offset of each sample in the fragment. - */ - public int[] sampleCompositionTimeOffsetTable; - /** - * The decoding time of each sample in the fragment. - */ - public long[] sampleDecodingTimeTable; + /** The composition time offset of each sample in the fragment, in microseconds. */ + public int[] sampleCompositionTimeOffsetUsTable; + /** The decoding time of each sample in the fragment, in microseconds. */ + public long[] sampleDecodingTimeUsTable; /** * Indicates which samples are sync frames. */ @@ -81,20 +77,13 @@ import java.io.IOException; * Undefined otherwise. */ public boolean[] sampleHasSubsampleEncryptionTable; - /** - * Fragment specific track encryption. May be null. - */ - public TrackEncryptionBox trackEncryptionBox; - /** - * If {@link #definesEncryptionData} is true, indicates the length of the sample encryption data. - * Undefined otherwise. - */ - public int sampleEncryptionDataLength; + /** Fragment specific track encryption. May be null. */ + @Nullable public TrackEncryptionBox trackEncryptionBox; /** * If {@link #definesEncryptionData} is true, contains binary sample encryption data. Undefined * otherwise. */ - public ParsableByteArray sampleEncryptionData; + public final ParsableByteArray sampleEncryptionData; /** * Whether {@link #sampleEncryptionData} needs populating with the actual encryption data. */ @@ -104,6 +93,17 @@ import java.io.IOException; */ public long nextFragmentDecodeTime; + public TrackFragment() { + trunDataPosition = new long[0]; + trunLength = new int[0]; + sampleSizeTable = new int[0]; + sampleCompositionTimeOffsetUsTable = new int[0]; + sampleDecodingTimeUsTable = new long[0]; + sampleIsSyncFrameTable = new boolean[0]; + sampleHasSubsampleEncryptionTable = new boolean[0]; + sampleEncryptionData = new ParsableByteArray(); + } + /** * Resets the fragment. *

      @@ -130,17 +130,17 @@ import java.io.IOException; public void initTables(int trunCount, int sampleCount) { this.trunCount = trunCount; this.sampleCount = sampleCount; - if (trunLength == null || trunLength.length < trunCount) { + if (trunLength.length < trunCount) { trunDataPosition = new long[trunCount]; trunLength = new int[trunCount]; } - if (sampleSizeTable == null || sampleSizeTable.length < sampleCount) { + if (sampleSizeTable.length < sampleCount) { // Size the tables 25% larger than needed, so as to make future resize operations less // likely. The choice of 25% is relatively arbitrary. int tableSize = (sampleCount * 125) / 100; sampleSizeTable = new int[tableSize]; - sampleCompositionTimeOffsetTable = new int[tableSize]; - sampleDecodingTimeTable = new long[tableSize]; + sampleCompositionTimeOffsetUsTable = new int[tableSize]; + sampleDecodingTimeUsTable = new long[tableSize]; sampleIsSyncFrameTable = new boolean[tableSize]; sampleHasSubsampleEncryptionTable = new boolean[tableSize]; } @@ -148,18 +148,14 @@ import java.io.IOException; /** * Configures the fragment to be one that defines encryption data of the specified length. - *

      - * {@link #definesEncryptionData} is set to true, {@link #sampleEncryptionDataLength} is set to - * the specified length, and {@link #sampleEncryptionData} is resized if necessary such that it - * is at least this length. + * + *

      {@link #definesEncryptionData} is set to true, and the {@link ParsableByteArray#limit() + * limit} of {@link #sampleEncryptionData} is set to the specified length. * * @param length The length in bytes of the encryption data. */ public void initEncryptionData(int length) { - if (sampleEncryptionData == null || sampleEncryptionData.limit() < length) { - sampleEncryptionData = new ParsableByteArray(length); - } - sampleEncryptionDataLength = length; + sampleEncryptionData.reset(length); definesEncryptionData = true; sampleEncryptionDataNeedsFill = true; } @@ -169,8 +165,8 @@ import java.io.IOException; * * @param input An {@link ExtractorInput} from which to read the encryption data. */ - public void fillEncryptionData(ExtractorInput input) throws IOException, InterruptedException { - input.readFully(sampleEncryptionData.data, 0, sampleEncryptionDataLength); + public void fillEncryptionData(ExtractorInput input) throws IOException { + input.readFully(sampleEncryptionData.data, 0, sampleEncryptionData.limit()); sampleEncryptionData.setPosition(0); sampleEncryptionDataNeedsFill = false; } @@ -181,13 +177,19 @@ import java.io.IOException; * @param source A source from which to read the encryption data. */ public void fillEncryptionData(ParsableByteArray source) { - source.readBytes(sampleEncryptionData.data, 0, sampleEncryptionDataLength); + source.readBytes(sampleEncryptionData.data, 0, sampleEncryptionData.limit()); sampleEncryptionData.setPosition(0); sampleEncryptionDataNeedsFill = false; } - public long getSamplePresentationTime(int index) { - return sampleDecodingTimeTable[index] + sampleCompositionTimeOffsetTable[index]; + /** + * Returns the sample presentation timestamp in microseconds. + * + * @param index The sample index. + * @return The presentation timestamps of this sample in microseconds. + */ + public long getSamplePresentationTimeUs(int index) { + return sampleDecodingTimeUsTable[index] + sampleCompositionTimeOffsetUsTable[index]; } /** Returns whether the sample at the given index has a subsample encryption table. */ diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/TrackSampleTable.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp4/TrackSampleTable.java similarity index 100% rename from library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/TrackSampleTable.java rename to library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp4/TrackSampleTable.java diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp4/package-info.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp4/package-info.java new file mode 100644 index 0000000000..6d0ad27361 --- /dev/null +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp4/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2019 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. + */ +@NonNullApi +package com.google.android.exoplayer2.extractor.mp4; + +import com.google.android.exoplayer2.util.NonNullApi; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeeker.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeeker.java similarity index 92% rename from library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeeker.java rename to library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeeker.java index 51ab94ba0e..1d73a1b66a 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeeker.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeeker.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.extractor.ogg; +import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ParserException; @@ -39,7 +40,7 @@ import java.io.IOException; private static final int STATE_SKIP = 3; private static final int STATE_IDLE = 4; - private final OggPageHeader pageHeader = new OggPageHeader(); + private final OggPageHeader pageHeader; private final long payloadStartPosition; private final long payloadEndPosition; private final StreamReader streamReader; @@ -83,10 +84,11 @@ import java.io.IOException; } else { state = STATE_SEEK_TO_END; } + pageHeader = new OggPageHeader(); } @Override - public long read(ExtractorInput input) throws IOException, InterruptedException { + public long read(ExtractorInput input) throws IOException { switch (state) { case STATE_IDLE: return -1; @@ -121,6 +123,7 @@ import java.io.IOException; } @Override + @Nullable public OggSeekMap createSeekMap() { return totalGranules != 0 ? new OggSeekMap() : null; } @@ -145,9 +148,8 @@ import java.io.IOException; * @return The byte position from which data should be provided for the next step, or {@link * C#POSITION_UNSET} if the search has converged. * @throws IOException If reading from the input fails. - * @throws InterruptedException If interrupted while reading from the input. */ - private long getNextSeekPosition(ExtractorInput input) throws IOException, InterruptedException { + private long getNextSeekPosition(ExtractorInput input) throws IOException { if (start == end) { return C.POSITION_UNSET; } @@ -196,10 +198,8 @@ import java.io.IOException; * @param input The {@link ExtractorInput} to read from. * @throws ParserException If populating the page header fails. * @throws IOException If reading from the input fails. - * @throws InterruptedException If interrupted while reading from the input. */ - private void skipToPageOfTargetGranule(ExtractorInput input) - throws IOException, InterruptedException { + private void skipToPageOfTargetGranule(ExtractorInput input) throws IOException { pageHeader.populate(input, /* quiet= */ false); while (pageHeader.granulePosition <= targetGranule) { input.skipFully(pageHeader.headerSize + pageHeader.bodySize); @@ -215,11 +215,10 @@ import java.io.IOException; * * @param input The {@code ExtractorInput} to skip to the next page. * @throws IOException If peeking/reading from the input fails. - * @throws InterruptedException If the thread is interrupted. * @throws EOFException If the next page can't be found before the end of the input. */ @VisibleForTesting - void skipToNextPage(ExtractorInput input) throws IOException, InterruptedException { + void skipToNextPage(ExtractorInput input) throws IOException { if (!skipToNextPage(input, payloadEndPosition)) { // Not found until eof. throw new EOFException(); @@ -233,10 +232,8 @@ import java.io.IOException; * @param limit The limit up to which the search should take place. * @return Whether the next page was found. * @throws IOException If peeking/reading from the input fails. - * @throws InterruptedException If interrupted while peeking/reading from the input. */ - private boolean skipToNextPage(ExtractorInput input, long limit) - throws IOException, InterruptedException { + private boolean skipToNextPage(ExtractorInput input, long limit) throws IOException { limit = Math.min(limit + 3, payloadEndPosition); byte[] buffer = new byte[2048]; int peekLength = buffer.length; @@ -272,10 +269,9 @@ import java.io.IOException; * @param input The {@link ExtractorInput} to read from. * @return The total number of samples of this input. * @throws IOException If reading from the input fails. - * @throws InterruptedException If the thread is interrupted. */ @VisibleForTesting - long readGranuleOfLastPage(ExtractorInput input) throws IOException, InterruptedException { + long readGranuleOfLastPage(ExtractorInput input) throws IOException { skipToNextPage(input); pageHeader.reset(); while ((pageHeader.type & 0x04) != 0x04 && input.getPosition() < payloadEndPosition) { diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ogg/FlacReader.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ogg/FlacReader.java new file mode 100644 index 0000000000..1d6f0da9a1 --- /dev/null +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ogg/FlacReader.java @@ -0,0 +1,151 @@ +/* + * 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.extractor.ogg; + +import androidx.annotation.Nullable; +import com.google.android.exoplayer2.extractor.ExtractorInput; +import com.google.android.exoplayer2.extractor.FlacFrameReader; +import com.google.android.exoplayer2.extractor.FlacMetadataReader; +import com.google.android.exoplayer2.extractor.FlacSeekTableSeekMap; +import com.google.android.exoplayer2.extractor.FlacStreamMetadata; +import com.google.android.exoplayer2.extractor.FlacStreamMetadata.SeekTable; +import com.google.android.exoplayer2.extractor.SeekMap; +import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.FlacConstants; +import com.google.android.exoplayer2.util.ParsableByteArray; +import com.google.android.exoplayer2.util.Util; +import java.util.Arrays; + +/** + * {@link StreamReader} to extract Flac data out of Ogg byte stream. + */ +/* package */ final class FlacReader extends StreamReader { + + private static final byte AUDIO_PACKET_TYPE = (byte) 0xFF; + + private static final int FRAME_HEADER_SAMPLE_NUMBER_OFFSET = 4; + + @Nullable private FlacStreamMetadata streamMetadata; + @Nullable private FlacOggSeeker flacOggSeeker; + + public static boolean verifyBitstreamType(ParsableByteArray data) { + return data.bytesLeft() >= 5 && data.readUnsignedByte() == 0x7F && // packet type + data.readUnsignedInt() == 0x464C4143; // ASCII signature "FLAC" + } + + @Override + protected void reset(boolean headerData) { + super.reset(headerData); + if (headerData) { + streamMetadata = null; + flacOggSeeker = null; + } + } + + private static boolean isAudioPacket(byte[] data) { + return data[0] == AUDIO_PACKET_TYPE; + } + + @Override + protected long preparePayload(ParsableByteArray packet) { + if (!isAudioPacket(packet.data)) { + return -1; + } + return getFlacFrameBlockSize(packet); + } + + @Override + protected boolean readHeaders(ParsableByteArray packet, long position, SetupData setupData) { + byte[] data = packet.data; + @Nullable FlacStreamMetadata streamMetadata = this.streamMetadata; + if (streamMetadata == null) { + streamMetadata = new FlacStreamMetadata(data, 17); + this.streamMetadata = streamMetadata; + byte[] metadata = Arrays.copyOfRange(data, 9, packet.limit()); + setupData.format = streamMetadata.getFormat(metadata, /* id3Metadata= */ null); + } else if ((data[0] & 0x7F) == FlacConstants.METADATA_TYPE_SEEK_TABLE) { + SeekTable seekTable = FlacMetadataReader.readSeekTableMetadataBlock(packet); + streamMetadata = streamMetadata.copyWithSeekTable(seekTable); + this.streamMetadata = streamMetadata; + flacOggSeeker = new FlacOggSeeker(streamMetadata, seekTable); + } else if (isAudioPacket(data)) { + if (flacOggSeeker != null) { + flacOggSeeker.setFirstFrameOffset(position); + setupData.oggSeeker = flacOggSeeker; + } + return false; + } + return true; + } + + private int getFlacFrameBlockSize(ParsableByteArray packet) { + int blockSizeKey = (packet.data[2] & 0xFF) >> 4; + if (blockSizeKey == 6 || blockSizeKey == 7) { + // Skip the sample number. + packet.skipBytes(FRAME_HEADER_SAMPLE_NUMBER_OFFSET); + packet.readUtf8EncodedLong(); + } + int result = FlacFrameReader.readFrameBlockSizeSamplesFromKey(packet, blockSizeKey); + packet.setPosition(0); + return result; + } + + private static final class FlacOggSeeker implements OggSeeker { + + private FlacStreamMetadata streamMetadata; + private SeekTable seekTable; + private long firstFrameOffset; + private long pendingSeekGranule; + + public FlacOggSeeker(FlacStreamMetadata streamMetadata, SeekTable seekTable) { + this.streamMetadata = streamMetadata; + this.seekTable = seekTable; + firstFrameOffset = -1; + pendingSeekGranule = -1; + } + + public void setFirstFrameOffset(long firstFrameOffset) { + this.firstFrameOffset = firstFrameOffset; + } + + @Override + public long read(ExtractorInput input) { + if (pendingSeekGranule >= 0) { + long result = -(pendingSeekGranule + 2); + pendingSeekGranule = -1; + return result; + } + return -1; + } + + @Override + public void startSeek(long targetGranule) { + long[] seekPointGranules = seekTable.pointSampleNumbers; + int index = + Util.binarySearchFloor( + seekPointGranules, targetGranule, /* inclusive= */ true, /* stayInBounds= */ true); + pendingSeekGranule = seekPointGranules[index]; + } + + @Override + public SeekMap createSeekMap() { + Assertions.checkState(firstFrameOffset != -1); + return new FlacSeekTableSeekMap(streamMetadata, firstFrameOffset); + } + + } + +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/OggExtractor.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ogg/OggExtractor.java similarity index 86% rename from library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/OggExtractor.java rename to library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ogg/OggExtractor.java index 5e74eab8d4..9aaa3332ce 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/OggExtractor.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ogg/OggExtractor.java @@ -23,8 +23,11 @@ import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.ExtractorsFactory; import com.google.android.exoplayer2.extractor.PositionHolder; import com.google.android.exoplayer2.extractor.TrackOutput; +import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.ParsableByteArray; import java.io.IOException; +import org.checkerframework.checker.nullness.qual.EnsuresNonNullIf; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /** * Extracts data from the Ogg container format. @@ -36,12 +39,12 @@ public class OggExtractor implements Extractor { private static final int MAX_VERIFICATION_BYTES = 8; - private ExtractorOutput output; - private StreamReader streamReader; + private @MonotonicNonNull ExtractorOutput output; + private @MonotonicNonNull StreamReader streamReader; private boolean streamReaderInitialized; @Override - public boolean sniff(ExtractorInput input) throws IOException, InterruptedException { + public boolean sniff(ExtractorInput input) throws IOException { try { return sniffInternal(input); } catch (ParserException e) { @@ -67,8 +70,8 @@ public class OggExtractor implements Extractor { } @Override - public int read(ExtractorInput input, PositionHolder seekPosition) - throws IOException, InterruptedException { + public int read(ExtractorInput input, PositionHolder seekPosition) throws IOException { + Assertions.checkStateNotNull(output); // Asserts that init has been called. if (streamReader == null) { if (!sniffInternal(input)) { throw new ParserException("Failed to determine bitstream type"); @@ -84,7 +87,8 @@ public class OggExtractor implements Extractor { return streamReader.read(input, seekPosition); } - private boolean sniffInternal(ExtractorInput input) throws IOException, InterruptedException { + @EnsuresNonNullIf(expression = "streamReader", result = true) + private boolean sniffInternal(ExtractorInput input) throws IOException { OggPageHeader header = new OggPageHeader(); if (!header.populate(input, true) || (header.type & 0x02) != 0x02) { return false; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/OggPacket.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ogg/OggPacket.java similarity index 98% rename from library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/OggPacket.java rename to library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ogg/OggPacket.java index 9c594ffde5..2ee65f0112 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/OggPacket.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ogg/OggPacket.java @@ -55,9 +55,8 @@ import java.util.Arrays; * @return {@code true} if the read was successful. The read fails if the end of the input is * encountered without reading data. * @throws IOException If reading from the input fails. - * @throws InterruptedException If the thread is interrupted. */ - public boolean populate(ExtractorInput input) throws IOException, InterruptedException { + public boolean populate(ExtractorInput input) throws IOException { Assertions.checkState(input != null); if (populated) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/OggPageHeader.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ogg/OggPageHeader.java similarity index 96% rename from library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/OggPageHeader.java rename to library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ogg/OggPageHeader.java index c7fb3ff6a2..d96aaa4568 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/OggPageHeader.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ogg/OggPageHeader.java @@ -82,10 +82,8 @@ import java.io.IOException; * @return Whether the read was successful. The read fails if the end of the input is encountered * without reading data. * @throws IOException If reading data fails or the stream is invalid. - * @throws InterruptedException If the thread is interrupted. */ - public boolean populate(ExtractorInput input, boolean quiet) - throws IOException, InterruptedException { + public boolean populate(ExtractorInput input, boolean quiet) throws IOException { scratch.reset(); reset(); boolean hasEnoughBytes = input.getLength() == C.LENGTH_UNSET diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/OggSeeker.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ogg/OggSeeker.java similarity index 75% rename from library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/OggSeeker.java rename to library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ogg/OggSeeker.java index e4c3a163e6..7626aa52d8 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/OggSeeker.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ogg/OggSeeker.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.extractor.ogg; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.extractor.SeekMap; import java.io.IOException; @@ -27,9 +28,10 @@ import java.io.IOException; /* package */ interface OggSeeker { /** - * Returns a {@link SeekMap} that returns an initial estimated position for progressive seeking - * or the final position for direct seeking. Returns null if {@link #read} has yet to return -1. + * Returns a {@link SeekMap} that returns an initial estimated position for progressive seeking or + * the final position for direct seeking. Returns null if {@link #read} has yet to return -1. */ + @Nullable SeekMap createSeekMap(); /** @@ -41,17 +43,15 @@ import java.io.IOException; /** * Reads data from the {@link ExtractorInput} to build the {@link SeekMap} or to continue a seek. - *

      - * If more data is required or if the position of the input needs to be modified then a position - * from which data should be provided is returned. Else a negative value is returned. If a seek - * has been completed then the value returned is -(currentGranule + 2). Else it is -1. + * + *

      If more data is required or if the position of the input needs to be modified then a + * position from which data should be provided is returned. Else a negative value is returned. If + * a seek has been completed then the value returned is -(currentGranule + 2). Else it is -1. * * @param input The {@link ExtractorInput} to read from. * @return A non-negative position to seek the {@link ExtractorInput} to, or -(currentGranule + 2) * if the progressive seek has completed, or -1 otherwise. * @throws IOException If reading from the {@link ExtractorInput} fails. - * @throws InterruptedException If the thread is interrupted. */ - long read(ExtractorInput input) throws IOException, InterruptedException; - + long read(ExtractorInput input) throws IOException; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/OpusReader.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ogg/OpusReader.java similarity index 93% rename from library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/OpusReader.java rename to library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ogg/OpusReader.java index 90ae3f0f47..018fd949b3 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/OpusReader.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ogg/OpusReader.java @@ -76,9 +76,13 @@ import java.util.List; putNativeOrderLong(initializationData, preskip); putNativeOrderLong(initializationData, DEFAULT_SEEK_PRE_ROLL_SAMPLES); - setupData.format = Format.createAudioSampleFormat(null, MimeTypes.AUDIO_OPUS, null, - Format.NO_VALUE, Format.NO_VALUE, channelCount, SAMPLE_RATE, initializationData, null, 0, - null); + setupData.format = + new Format.Builder() + .setSampleMimeType(MimeTypes.AUDIO_OPUS) + .setChannelCount(channelCount) + .setSampleRate(SAMPLE_RATE) + .setInitializationData(initializationData) + .build(); headerRead = true; } else { boolean headerPacket = packet.readInt() == OPUS_CODE; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/StreamReader.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ogg/StreamReader.java similarity index 91% rename from library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/StreamReader.java rename to library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ogg/StreamReader.java index d2671125e4..f28602d9b3 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/StreamReader.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ogg/StreamReader.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.extractor.ogg; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.extractor.Extractor; @@ -23,8 +24,10 @@ import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.PositionHolder; import com.google.android.exoplayer2.extractor.SeekMap; import com.google.android.exoplayer2.extractor.TrackOutput; +import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.ParsableByteArray; import java.io.IOException; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /** StreamReader abstract class. */ @SuppressWarnings("UngroupedOverloads") @@ -42,15 +45,15 @@ import java.io.IOException; private final OggPacket oggPacket; - private TrackOutput trackOutput; - private ExtractorOutput extractorOutput; - private OggSeeker oggSeeker; + private @MonotonicNonNull TrackOutput trackOutput; + private @MonotonicNonNull ExtractorOutput extractorOutput; + private @MonotonicNonNull OggSeeker oggSeeker; private long targetGranule; private long payloadStartPosition; private long currentGranule; private int state; private int sampleRate; - private SetupData setupData; + @Nullable private SetupData setupData; private long lengthOfReadPacket; private boolean seekMapSet; private boolean formatSet; @@ -98,11 +101,8 @@ import java.io.IOException; } } - /** - * @see Extractor#read(ExtractorInput, PositionHolder) - */ - final int read(ExtractorInput input, PositionHolder seekPosition) - throws IOException, InterruptedException { + /** @see Extractor#read(ExtractorInput, PositionHolder) */ + final int read(ExtractorInput input, PositionHolder seekPosition) throws IOException { switch (state) { case STATE_READ_HEADERS: return readHeaders(input); @@ -118,7 +118,7 @@ import java.io.IOException; } } - private int readHeaders(ExtractorInput input) throws IOException, InterruptedException { + private int readHeaders(ExtractorInput input) throws IOException { boolean readingHeaders = true; while (readingHeaders) { if (!oggPacket.populate(input)) { @@ -148,7 +148,7 @@ import java.io.IOException; boolean isLastPage = (firstPayloadPageHeader.type & 0x04) != 0; // Type 4 is end of stream. oggSeeker = new DefaultOggSeeker( - this, + /* streamReader= */ this, payloadStartPosition, input.getLength(), firstPayloadPageHeader.headerSize + firstPayloadPageHeader.bodySize, @@ -163,8 +163,7 @@ import java.io.IOException; return Extractor.RESULT_CONTINUE; } - private int readPayload(ExtractorInput input, PositionHolder seekPosition) - throws IOException, InterruptedException { + private int readPayload(ExtractorInput input, PositionHolder seekPosition) throws IOException { long position = oggSeeker.read(input); if (position >= 0) { seekPosition.position = position; @@ -172,8 +171,9 @@ import java.io.IOException; } else if (position < -1) { onSeekEnd(-(position + 2)); } + if (!seekMapSet) { - SeekMap seekMap = oggSeeker.createSeekMap(); + SeekMap seekMap = Assertions.checkStateNotNull(oggSeeker.createSeekMap()); extractorOutput.seekMap(seekMap); seekMapSet = true; } @@ -234,8 +234,8 @@ import java.io.IOException; * @param setupData Setup data to be filled. * @return Whether the packet contains header data. */ - protected abstract boolean readHeaders(ParsableByteArray packet, long position, - SetupData setupData) throws IOException, InterruptedException; + protected abstract boolean readHeaders( + ParsableByteArray packet, long position, SetupData setupData) throws IOException; /** * Called on end of seeking. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/VorbisReader.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ogg/VorbisReader.java similarity index 87% rename from library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/VorbisReader.java rename to library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ogg/VorbisReader.java index b57678266a..d6faa90927 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/VorbisReader.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ogg/VorbisReader.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.extractor.ogg; +import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.ParserException; @@ -30,16 +31,16 @@ import java.util.ArrayList; */ /* package */ final class VorbisReader extends StreamReader { - private VorbisSetup vorbisSetup; + @Nullable private VorbisSetup vorbisSetup; private int previousPacketBlockSize; private boolean seenFirstAudioPacket; - private VorbisUtil.VorbisIdHeader vorbisIdHeader; - private VorbisUtil.CommentHeader commentHeader; + @Nullable private VorbisUtil.VorbisIdHeader vorbisIdHeader; + @Nullable private VorbisUtil.CommentHeader commentHeader; public static boolean verifyBitstreamType(ParsableByteArray data) { try { - return VorbisUtil.verifyVorbisHeaderCapturePattern(0x01, data, true); + return VorbisUtil.verifyVorbisHeaderCapturePattern(/* headerType= */ 0x01, data, true); } catch (ParserException e) { return false; } @@ -88,7 +89,7 @@ import java.util.ArrayList; @Override protected boolean readHeaders(ParsableByteArray packet, long position, SetupData setupData) - throws IOException, InterruptedException { + throws IOException { if (vorbisSetup != null) { return false; } @@ -98,18 +99,26 @@ import java.util.ArrayList; return true; } - ArrayList codecInitialisationData = new ArrayList<>(); - codecInitialisationData.add(vorbisSetup.idHeader.data); - codecInitialisationData.add(vorbisSetup.setupHeaderData); + VorbisUtil.VorbisIdHeader idHeader = vorbisSetup.idHeader; - setupData.format = Format.createAudioSampleFormat(null, MimeTypes.AUDIO_VORBIS, null, - this.vorbisSetup.idHeader.bitrateNominal, Format.NO_VALUE, - this.vorbisSetup.idHeader.channels, (int) this.vorbisSetup.idHeader.sampleRate, - codecInitialisationData, null, 0, null); + ArrayList codecInitializationData = new ArrayList<>(); + codecInitializationData.add(idHeader.data); + codecInitializationData.add(vorbisSetup.setupHeaderData); + + setupData.format = + new Format.Builder() + .setSampleMimeType(MimeTypes.AUDIO_VORBIS) + .setAverageBitrate(idHeader.bitrateNominal) + .setPeakBitrate(idHeader.bitrateMaximum) + .setChannelCount(idHeader.channels) + .setSampleRate(idHeader.sampleRate) + .setInitializationData(codecInitializationData) + .build(); return true; } @VisibleForTesting + @Nullable /* package */ VorbisSetup readSetupHeaders(ParsableByteArray scratch) throws IOException { if (vorbisIdHeader == null) { diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ogg/package-info.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ogg/package-info.java new file mode 100644 index 0000000000..ef8ed054a4 --- /dev/null +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ogg/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2019 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. + */ +@NonNullApi +package com.google.android.exoplayer2.extractor.ogg; + +import com.google.android.exoplayer2.util.NonNullApi; diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/package-info.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/package-info.java new file mode 100644 index 0000000000..9920b247e6 --- /dev/null +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2019 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. + */ +@NonNullApi +package com.google.android.exoplayer2.extractor; + +import com.google.android.exoplayer2.util.NonNullApi; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/rawcc/RawCcExtractor.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/rawcc/RawCcExtractor.java similarity index 91% rename from library/core/src/main/java/com/google/android/exoplayer2/extractor/rawcc/RawCcExtractor.java rename to library/extractor/src/main/java/com/google/android/exoplayer2/extractor/rawcc/RawCcExtractor.java index 3d76276240..ae30231a50 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/rawcc/RawCcExtractor.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/rawcc/RawCcExtractor.java @@ -24,8 +24,11 @@ import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.PositionHolder; import com.google.android.exoplayer2.extractor.SeekMap; import com.google.android.exoplayer2.extractor.TrackOutput; +import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.ParsableByteArray; import java.io.IOException; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.checkerframework.checker.nullness.qual.RequiresNonNull; /** * Extracts data from the RawCC container format. @@ -44,11 +47,9 @@ public final class RawCcExtractor implements Extractor { private static final int STATE_READING_SAMPLES = 2; private final Format format; - private final ParsableByteArray dataScratch; - private TrackOutput trackOutput; - + private @MonotonicNonNull TrackOutput trackOutput; private int parserState; private int version; private long timestampUs; @@ -70,15 +71,15 @@ public final class RawCcExtractor implements Extractor { } @Override - public boolean sniff(ExtractorInput input) throws IOException, InterruptedException { + public boolean sniff(ExtractorInput input) throws IOException { dataScratch.reset(); input.peekFully(dataScratch.data, 0, HEADER_SIZE); return dataScratch.readInt() == HEADER_ID; } @Override - public int read(ExtractorInput input, PositionHolder seekPosition) - throws IOException, InterruptedException { + public int read(ExtractorInput input, PositionHolder seekPosition) throws IOException { + Assertions.checkStateNotNull(trackOutput); // Asserts that init has been called. while (true) { switch (parserState) { case STATE_READING_HEADER: @@ -116,7 +117,7 @@ public final class RawCcExtractor implements Extractor { // Do nothing } - private boolean parseHeader(ExtractorInput input) throws IOException, InterruptedException { + private boolean parseHeader(ExtractorInput input) throws IOException { dataScratch.reset(); if (input.readFully(dataScratch.data, 0, HEADER_SIZE, true)) { if (dataScratch.readInt() != HEADER_ID) { @@ -130,8 +131,7 @@ public final class RawCcExtractor implements Extractor { } } - private boolean parseTimestampAndSampleCount(ExtractorInput input) throws IOException, - InterruptedException { + private boolean parseTimestampAndSampleCount(ExtractorInput input) throws IOException { dataScratch.reset(); if (version == 0) { if (!input.readFully(dataScratch.data, 0, TIMESTAMP_SIZE_V0 + 1, true)) { @@ -153,7 +153,8 @@ public final class RawCcExtractor implements Extractor { return true; } - private void parseSamples(ExtractorInput input) throws IOException, InterruptedException { + @RequiresNonNull("trackOutput") + private void parseSamples(ExtractorInput input) throws IOException { for (; remainingSampleCount > 0; remainingSampleCount--) { dataScratch.reset(); input.readFully(dataScratch.data, 0, 3); @@ -166,5 +167,4 @@ public final class RawCcExtractor implements Extractor { trackOutput.sampleMetadata(timestampUs, C.BUFFER_FLAG_KEY_FRAME, sampleBytesWritten, 0, null); } } - } diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/rawcc/package-info.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/rawcc/package-info.java new file mode 100644 index 0000000000..b01e56b8dd --- /dev/null +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/rawcc/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2019 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. + */ +@NonNullApi +package com.google.android.exoplayer2.extractor.rawcc; + +import com.google.android.exoplayer2.util.NonNullApi; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac3Extractor.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac3Extractor.java similarity index 97% rename from library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac3Extractor.java rename to library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac3Extractor.java index b1d15b7189..f0cb8ca1f7 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac3Extractor.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac3Extractor.java @@ -61,7 +61,7 @@ public final class Ac3Extractor implements Extractor { // Extractor implementation. @Override - public boolean sniff(ExtractorInput input) throws IOException, InterruptedException { + public boolean sniff(ExtractorInput input) throws IOException { // Skip any ID3 headers. ParsableByteArray scratch = new ParsableByteArray(ID3_HEADER_LENGTH); int startPosition = 0; @@ -124,8 +124,7 @@ public final class Ac3Extractor implements Extractor { } @Override - public int read(ExtractorInput input, PositionHolder seekPosition) throws IOException, - InterruptedException { + public int read(ExtractorInput input, PositionHolder seekPosition) throws IOException { int bytesRead = input.read(sampleData.data, 0, MAX_SYNC_FRAME_SIZE); if (bytesRead == C.RESULT_END_OF_INPUT) { return RESULT_END_OF_INPUT; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac3Reader.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac3Reader.java similarity index 84% rename from library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac3Reader.java rename to library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac3Reader.java index cd07a40c6d..b025be95e3 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac3Reader.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac3Reader.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.extractor.ts; import androidx.annotation.IntDef; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.audio.Ac3Util; @@ -23,11 +24,15 @@ import com.google.android.exoplayer2.audio.Ac3Util.SyncFrameInfo; 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.Assertions; import com.google.android.exoplayer2.util.ParsableBitArray; import com.google.android.exoplayer2.util.ParsableByteArray; +import com.google.android.exoplayer2.util.Util; import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.checkerframework.checker.nullness.qual.RequiresNonNull; /** * Parses a continuous (E-)AC-3 byte stream and extracts individual samples. @@ -47,10 +52,10 @@ public final class Ac3Reader implements ElementaryStreamReader { private final ParsableBitArray headerScratchBits; private final ParsableByteArray headerScratchBytes; - private final String language; + @Nullable private final String language; - private String trackFormatId; - private TrackOutput output; + private @MonotonicNonNull String formatId; + private @MonotonicNonNull TrackOutput output; @State private int state; private int bytesRead; @@ -60,7 +65,7 @@ public final class Ac3Reader implements ElementaryStreamReader { // Used when parsing the header. private long sampleDurationUs; - private Format format; + private @MonotonicNonNull Format format; private int sampleSize; // Used when reading the samples. @@ -78,7 +83,7 @@ public final class Ac3Reader implements ElementaryStreamReader { * * @param language Track language. */ - public Ac3Reader(String language) { + public Ac3Reader(@Nullable String language) { headerScratchBits = new ParsableBitArray(new byte[HEADER_SIZE]); headerScratchBytes = new ParsableByteArray(headerScratchBits.data); state = STATE_FINDING_SYNC; @@ -95,7 +100,7 @@ public final class Ac3Reader implements ElementaryStreamReader { @Override public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator generator) { generator.generateNewId(); - trackFormatId = generator.getFormatId(); + formatId = generator.getFormatId(); output = extractorOutput.track(generator.getTrackId(), C.TRACK_TYPE_AUDIO); } @@ -106,6 +111,7 @@ public final class Ac3Reader implements ElementaryStreamReader { @Override public void consume(ParsableByteArray data) { + Assertions.checkStateNotNull(output); // Asserts that createTracks has been called. while (data.bytesLeft() > 0) { switch (state) { case STATE_FINDING_SYNC: @@ -185,19 +191,23 @@ public final class Ac3Reader implements ElementaryStreamReader { return false; } - /** - * Parses the sample header. - */ - @SuppressWarnings("ReferenceEquality") + /** Parses the sample header. */ + @RequiresNonNull("output") private void parseHeader() { headerScratchBits.setPosition(0); SyncFrameInfo frameInfo = Ac3Util.parseAc3SyncframeInfo(headerScratchBits); - if (format == null || frameInfo.channelCount != format.channelCount + if (format == null + || frameInfo.channelCount != format.channelCount || frameInfo.sampleRate != format.sampleRate - || frameInfo.mimeType != format.sampleMimeType) { - format = Format.createAudioSampleFormat(trackFormatId, frameInfo.mimeType, null, - Format.NO_VALUE, Format.NO_VALUE, frameInfo.channelCount, frameInfo.sampleRate, null, - null, 0, language); + || !Util.areEqual(frameInfo.mimeType, format.sampleMimeType)) { + format = + new Format.Builder() + .setId(formatId) + .setSampleMimeType(frameInfo.mimeType) + .setChannelCount(frameInfo.channelCount) + .setSampleRate(frameInfo.sampleRate) + .setLanguage(language) + .build(); output.format(format); } sampleSize = frameInfo.frameSize; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac4Extractor.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac4Extractor.java similarity index 97% rename from library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac4Extractor.java rename to library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac4Extractor.java index 205d71e16e..c493d1d0bd 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac4Extractor.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac4Extractor.java @@ -68,7 +68,7 @@ public final class Ac4Extractor implements Extractor { // Extractor implementation. @Override - public boolean sniff(ExtractorInput input) throws IOException, InterruptedException { + public boolean sniff(ExtractorInput input) throws IOException { // Skip any ID3 headers. ParsableByteArray scratch = new ParsableByteArray(ID3_HEADER_LENGTH); int startPosition = 0; @@ -132,8 +132,7 @@ public final class Ac4Extractor implements Extractor { } @Override - public int read(ExtractorInput input, PositionHolder seekPosition) - throws IOException, InterruptedException { + public int read(ExtractorInput input, PositionHolder seekPosition) throws IOException { int bytesRead = input.read(sampleData.data, /* offset= */ 0, /* length= */ READ_BUFFER_SIZE); if (bytesRead == C.RESULT_END_OF_INPUT) { return RESULT_END_OF_INPUT; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac4Reader.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac4Reader.java similarity index 88% rename from library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac4Reader.java rename to library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac4Reader.java index 48bd07fce4..517a233530 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac4Reader.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac4Reader.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.extractor.ts; import androidx.annotation.IntDef; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.audio.Ac4Util; @@ -23,12 +24,15 @@ import com.google.android.exoplayer2.audio.Ac4Util.SyncFrameInfo; 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.Assertions; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.ParsableBitArray; import com.google.android.exoplayer2.util.ParsableByteArray; import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.checkerframework.checker.nullness.qual.RequiresNonNull; /** Parses a continuous AC-4 byte stream and extracts individual samples. */ public final class Ac4Reader implements ElementaryStreamReader { @@ -44,10 +48,10 @@ public final class Ac4Reader implements ElementaryStreamReader { private final ParsableBitArray headerScratchBits; private final ParsableByteArray headerScratchBytes; - private final String language; + @Nullable private final String language; - private String trackFormatId; - private TrackOutput output; + private @MonotonicNonNull String formatId; + private @MonotonicNonNull TrackOutput output; @State private int state; private int bytesRead; @@ -58,7 +62,7 @@ public final class Ac4Reader implements ElementaryStreamReader { // Used when parsing the header. private long sampleDurationUs; - private Format format; + private @MonotonicNonNull Format format; private int sampleSize; // Used when reading the samples. @@ -74,7 +78,7 @@ public final class Ac4Reader implements ElementaryStreamReader { * * @param language Track language. */ - public Ac4Reader(String language) { + public Ac4Reader(@Nullable String language) { headerScratchBits = new ParsableBitArray(new byte[Ac4Util.HEADER_SIZE_FOR_PARSER]); headerScratchBytes = new ParsableByteArray(headerScratchBits.data); state = STATE_FINDING_SYNC; @@ -95,7 +99,7 @@ public final class Ac4Reader implements ElementaryStreamReader { @Override public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator generator) { generator.generateNewId(); - trackFormatId = generator.getFormatId(); + formatId = generator.getFormatId(); output = extractorOutput.track(generator.getTrackId(), C.TRACK_TYPE_AUDIO); } @@ -106,6 +110,7 @@ public final class Ac4Reader implements ElementaryStreamReader { @Override public void consume(ParsableByteArray data) { + Assertions.checkStateNotNull(output); // Asserts that createTracks has been called. while (data.bytesLeft() > 0) { switch (state) { case STATE_FINDING_SYNC: @@ -185,7 +190,7 @@ public final class Ac4Reader implements ElementaryStreamReader { } /** Parses the sample header. */ - @SuppressWarnings("ReferenceEquality") + @RequiresNonNull("output") private void parseHeader() { headerScratchBits.setPosition(0); SyncFrameInfo frameInfo = Ac4Util.parseAc4SyncframeInfo(headerScratchBits); @@ -194,18 +199,13 @@ public final class Ac4Reader implements ElementaryStreamReader { || frameInfo.sampleRate != format.sampleRate || !MimeTypes.AUDIO_AC4.equals(format.sampleMimeType)) { format = - Format.createAudioSampleFormat( - trackFormatId, - MimeTypes.AUDIO_AC4, - /* codecs= */ null, - /* bitrate= */ Format.NO_VALUE, - /* maxInputSize= */ Format.NO_VALUE, - frameInfo.channelCount, - frameInfo.sampleRate, - /* initializationData= */ null, - /* drmInitData= */ null, - /* selectionFlags= */ 0, - language); + new Format.Builder() + .setId(formatId) + .setSampleMimeType(MimeTypes.AUDIO_AC4) + .setChannelCount(frameInfo.channelCount) + .setSampleRate(frameInfo.sampleRate) + .setLanguage(language) + .build(); output.format(format); } sampleSize = frameInfo.frameSize; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractor.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractor.java similarity index 95% rename from library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractor.java rename to library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractor.java index 86dacd8c30..f870527284 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractor.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractor.java @@ -20,7 +20,6 @@ import static com.google.android.exoplayer2.metadata.id3.Id3Decoder.ID3_HEADER_L import static com.google.android.exoplayer2.metadata.id3.Id3Decoder.ID3_TAG; import androidx.annotation.IntDef; -import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.extractor.ConstantBitrateSeekMap; @@ -39,6 +38,8 @@ import java.io.IOException; import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.checkerframework.checker.nullness.qual.RequiresNonNull; /** * Extracts data from AAC bit streams with ADTS framing. @@ -86,7 +87,7 @@ public final class AdtsExtractor implements Extractor { private final ParsableByteArray scratch; private final ParsableBitArray scratchBits; - @Nullable private ExtractorOutput extractorOutput; + private @MonotonicNonNull ExtractorOutput extractorOutput; private long firstSampleTimestampUs; private long firstFramePosition; @@ -119,7 +120,7 @@ public final class AdtsExtractor implements Extractor { // Extractor implementation. @Override - public boolean sniff(ExtractorInput input) throws IOException, InterruptedException { + public boolean sniff(ExtractorInput input) throws IOException { // Skip any ID3 headers. int startPosition = peekId3Header(input); @@ -178,8 +179,9 @@ public final class AdtsExtractor implements Extractor { } @Override - public int read(ExtractorInput input, PositionHolder seekPosition) - throws IOException, InterruptedException { + public int read(ExtractorInput input, PositionHolder seekPosition) throws IOException { + Assertions.checkStateNotNull(extractorOutput); // Asserts that init has been called. + long inputLength = input.getLength(); boolean canUseConstantBitrateSeeking = (flags & FLAG_ENABLE_CONSTANT_BITRATE_SEEKING) != 0 && inputLength != C.LENGTH_UNSET; @@ -209,7 +211,7 @@ public final class AdtsExtractor implements Extractor { return RESULT_CONTINUE; } - private int peekId3Header(ExtractorInput input) throws IOException, InterruptedException { + private int peekId3Header(ExtractorInput input) throws IOException { int firstFramePosition = 0; while (true) { input.peekFully(scratch.data, /* offset= */ 0, ID3_HEADER_LENGTH); @@ -230,6 +232,7 @@ public final class AdtsExtractor implements Extractor { return firstFramePosition; } + @RequiresNonNull("extractorOutput") private void maybeOutputSeekMap( long inputLength, boolean canUseConstantBitrateSeeking, boolean readEndOfStream) { if (hasOutputSeekMap) { @@ -244,7 +247,6 @@ public final class AdtsExtractor implements Extractor { return; } - ExtractorOutput extractorOutput = Assertions.checkNotNull(this.extractorOutput); if (useConstantBitrateSeeking && reader.getSampleDurationUs() != C.TIME_UNSET) { extractorOutput.seekMap(getConstantBitrateSeekMap(inputLength)); } else { @@ -253,8 +255,7 @@ public final class AdtsExtractor implements Extractor { hasOutputSeekMap = true; } - private void calculateAverageFrameSize(ExtractorInput input) - throws IOException, InterruptedException { + private void calculateAverageFrameSize(ExtractorInput input) throws IOException { if (hasCalculatedAverageFrameSize) { return; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsReader.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsReader.java similarity index 76% rename from library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsReader.java rename to library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsReader.java index 589b543170..59ab6599b0 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsReader.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsReader.java @@ -15,21 +15,26 @@ */ package com.google.android.exoplayer2.extractor.ts; -import android.util.Pair; +import androidx.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.audio.AacUtil; import com.google.android.exoplayer2.extractor.DummyTrackOutput; 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.Assertions; import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.ParsableBitArray; import com.google.android.exoplayer2.util.ParsableByteArray; +import com.google.android.exoplayer2.util.Util; import java.util.Arrays; import java.util.Collections; +import org.checkerframework.checker.nullness.qual.EnsuresNonNull; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.checkerframework.checker.nullness.qual.RequiresNonNull; /** * Parses a continuous ADTS byte stream and extracts individual frames. @@ -62,11 +67,11 @@ public final class AdtsReader implements ElementaryStreamReader { private final boolean exposeId3; private final ParsableBitArray adtsScratch; private final ParsableByteArray id3HeaderBuffer; - private final String language; + @Nullable private final String language; - private String formatId; - private TrackOutput output; - private TrackOutput id3Output; + private @MonotonicNonNull String formatId; + private @MonotonicNonNull TrackOutput output; + private @MonotonicNonNull TrackOutput id3Output; private int state; private int bytesRead; @@ -90,7 +95,7 @@ public final class AdtsReader implements ElementaryStreamReader { // Used when reading the samples. private long timeUs; - private TrackOutput currentOutput; + private @MonotonicNonNull TrackOutput currentOutput; private long currentSampleDuration; /** @@ -104,7 +109,7 @@ public final class AdtsReader implements ElementaryStreamReader { * @param exposeId3 True if the reader should expose ID3 information. * @param language Track language. */ - public AdtsReader(boolean exposeId3, String language) { + public AdtsReader(boolean exposeId3, @Nullable String language) { adtsScratch = new ParsableBitArray(new byte[HEADER_SIZE + CRC_SIZE]); id3HeaderBuffer = new ParsableByteArray(Arrays.copyOf(ID3_IDENTIFIER, ID3_HEADER_SIZE)); setFindingSampleState(); @@ -130,11 +135,15 @@ public final class AdtsReader implements ElementaryStreamReader { idGenerator.generateNewId(); formatId = idGenerator.getFormatId(); output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_AUDIO); + currentOutput = output; if (exposeId3) { idGenerator.generateNewId(); id3Output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_METADATA); - id3Output.format(Format.createSampleFormat(idGenerator.getFormatId(), - MimeTypes.APPLICATION_ID3, null, Format.NO_VALUE, null)); + id3Output.format( + new Format.Builder() + .setId(idGenerator.getFormatId()) + .setSampleMimeType(MimeTypes.APPLICATION_ID3) + .build()); } else { id3Output = new DummyTrackOutput(); } @@ -147,6 +156,7 @@ public final class AdtsReader implements ElementaryStreamReader { @Override public void consume(ParsableByteArray data) throws ParserException { + assertTracksCreated(); while (data.bytesLeft() > 0) { switch (state) { case STATE_FINDING_SAMPLE: @@ -345,42 +355,44 @@ public final class AdtsReader implements ElementaryStreamReader { } /** - * 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: + * Checks whether a candidate SYNC word position is likely to be the position of a real SYNC word. + * The caller must check that the first byte of the SYNC word is 0xFF before calling this method. + * This method performs the following checks: * *

        - *
      • 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. + *
      • The MPEG version of this frame must match the previously detected version. + *
      • The sample rate index of this frame must match the previously detected sample rate index. + *
      • The frame size must be at least 7 bytes + *
      • The bytes following the frame must be either another SYNC word with the same MPEG + * version, or the start of an ID3 header. *
      * - * 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. + * With the exception of the first check, if there is insufficient data in the buffer then checks + * are optimistically skipped and {@code true} is returned. + * + * @param pesBuffer The buffer containing at data to check. + * @param syncPositionCandidate The candidate SYNC word position. May be -1 if the first byte of + * the candidate was the last byte of the previously consumed buffer. + * @return True if all checks were passed or skipped, indicating the position is likely to be the + * position of a real SYNC word. False otherwise. */ 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; } + // The MPEG version of this frame must match the previously detected version. adtsScratch.setPosition(4); int currentFrameVersion = adtsScratch.readBits(1); if (firstFrameVersion != VERSION_UNSET && currentFrameVersion != firstFrameVersion) { return false; } + // The sample rate index of this frame must match the previously detected sample rate index. if (firstFrameSampleRateIndex != C.INDEX_UNSET) { if (!tryRead(pesBuffer, adtsScratch.data, 1)) { + // Insufficient data for further checks. return true; } adtsScratch.setPosition(2); @@ -391,24 +403,50 @@ public final class AdtsReader implements ElementaryStreamReader { pesBuffer.setPosition(syncPositionCandidate + 2); } - // Optionally check the byte after this frame matches SYNC word. - + // The frame size must be at least 7 bytes. if (!tryRead(pesBuffer, adtsScratch.data, 4)) { + // Insufficient data for further checks. return true; } adtsScratch.setPosition(14); int frameSize = adtsScratch.readBits(13); - if (frameSize <= 6) { - // Not a frame. + if (frameSize < 7) { return false; } + + // The bytes following the frame must be either another SYNC word with the same MPEG version, or + // the start of an ID3 header. + byte[] data = pesBuffer.data; + int dataLimit = pesBuffer.limit(); int nextSyncPosition = syncPositionCandidate + frameSize; - if (nextSyncPosition + 1 >= pesBuffer.limit()) { + if (nextSyncPosition >= dataLimit) { + // Insufficient data for further checks. return true; } - return (isAdtsSyncBytes(pesBuffer.data[nextSyncPosition], pesBuffer.data[nextSyncPosition + 1]) - && (firstFrameVersion == VERSION_UNSET - || ((pesBuffer.data[nextSyncPosition + 1] & 0x8) >> 3) == currentFrameVersion)); + if (data[nextSyncPosition] == (byte) 0xFF) { + if (nextSyncPosition + 1 == dataLimit) { + // Insufficient data for further checks. + return true; + } + return isAdtsSyncBytes((byte) 0xFF, data[nextSyncPosition + 1]) + && ((data[nextSyncPosition + 1] & 0x8) >> 3) == currentFrameVersion; + } else { + if (data[nextSyncPosition] != 'I') { + return false; + } + if (nextSyncPosition + 1 == dataLimit) { + // Insufficient data for further checks. + return true; + } + if (data[nextSyncPosition + 1] != 'D') { + return false; + } + if (nextSyncPosition + 2 == dataLimit) { + // Insufficient data for further checks. + return true; + } + return data[nextSyncPosition + 2] == '3'; + } } private boolean isAdtsSyncBytes(byte firstByte, byte secondByte) { @@ -425,9 +463,8 @@ public final class AdtsReader implements ElementaryStreamReader { return true; } - /** - * Parses the Id3 header. - */ + /** Parses the Id3 header. */ + @RequiresNonNull("id3Output") private void parseId3Header() { id3Output.sampleData(id3HeaderBuffer, ID3_HEADER_SIZE); id3HeaderBuffer.setPosition(ID3_SIZE_OFFSET); @@ -435,9 +472,8 @@ public final class AdtsReader implements ElementaryStreamReader { id3HeaderBuffer.readSynchSafeInt() + ID3_HEADER_SIZE); } - /** - * Parses the sample header. - */ + /** Parses the sample header. */ + @RequiresNonNull("output") private void parseAdtsHeader() throws ParserException { adtsScratch.setPosition(0); @@ -461,14 +497,19 @@ public final class AdtsReader implements ElementaryStreamReader { int channelConfig = adtsScratch.readBits(3); byte[] audioSpecificConfig = - CodecSpecificDataUtil.buildAacAudioSpecificConfig( + AacUtil.buildAudioSpecificConfig( audioObjectType, firstFrameSampleRateIndex, channelConfig); - Pair audioParams = CodecSpecificDataUtil.parseAacAudioSpecificConfig( - audioSpecificConfig); - - Format format = Format.createAudioSampleFormat(formatId, MimeTypes.AUDIO_AAC, null, - Format.NO_VALUE, Format.NO_VALUE, audioParams.second, audioParams.first, - Collections.singletonList(audioSpecificConfig), null, 0, language); + AacUtil.Config aacConfig = AacUtil.parseAudioSpecificConfig(audioSpecificConfig); + Format format = + new Format.Builder() + .setId(formatId) + .setSampleMimeType(MimeTypes.AUDIO_AAC) + .setCodecs(aacConfig.codecs) + .setChannelCount(aacConfig.channelCount) + .setSampleRate(aacConfig.sampleRateHz) + .setInitializationData(Collections.singletonList(audioSpecificConfig)) + .setLanguage(language) + .build(); // In this class a sample is an access unit, but the MediaFormat sample rate specifies the // number of PCM audio samples per second. sampleDurationUs = (C.MICROS_PER_SECOND * 1024) / format.sampleRate; @@ -487,9 +528,8 @@ public final class AdtsReader implements ElementaryStreamReader { setReadingSampleState(output, sampleDurationUs, 0, sampleSize); } - /** - * Reads the rest of the sample - */ + /** Reads the rest of the sample */ + @RequiresNonNull("currentOutput") private void readSample(ParsableByteArray data) { int bytesToRead = Math.min(data.bytesLeft(), sampleSize - bytesRead); currentOutput.sampleData(data, bytesToRead); @@ -501,4 +541,10 @@ public final class AdtsReader implements ElementaryStreamReader { } } + @EnsuresNonNull({"output", "currentOutput", "id3Output"}) + private void assertTracksCreated() { + Assertions.checkNotNull(output); + Util.castNonNull(currentOutput); + Util.castNonNull(id3Output); + } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.java similarity index 89% rename from library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.java rename to library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.java index 24d17f4956..c48c790fbf 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.java @@ -17,9 +17,10 @@ package com.google.android.exoplayer2.extractor.ts; import android.util.SparseArray; import androidx.annotation.IntDef; +import androidx.annotation.Nullable; 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.CodecSpecificDataUtil; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.ParsableByteArray; import java.lang.annotation.Documented; @@ -79,7 +80,10 @@ public final class DefaultTsPayloadReaderFactory implements TsPayloadReader.Fact * delimiters (AUDs). */ public static final int FLAG_DETECT_ACCESS_UNITS = 1 << 3; - /** Prevents the creation of {@link SpliceInfoSectionReader} instances. */ + /** + * Prevents the creation of {@link SectionPayloadReader}s for splice information sections + * (SCTE-35). + */ public static final int FLAG_IGNORE_SPLICE_INFO_STREAM = 1 << 4; /** * Whether the list of {@code closedCaptionFormats} passed to {@link @@ -111,7 +115,7 @@ public final class DefaultTsPayloadReaderFactory implements TsPayloadReader.Fact this( flags, Collections.singletonList( - Format.createTextSampleFormat(null, MimeTypes.APPLICATION_CEA608, 0, null))); + new Format.Builder().setSampleMimeType(MimeTypes.APPLICATION_CEA608).build())); } /** @@ -135,6 +139,7 @@ public final class DefaultTsPayloadReaderFactory implements TsPayloadReader.Fact } @Override + @Nullable public TsPayloadReader createPayloadReader(int streamType, EsInfo esInfo) { switch (streamType) { case TsExtractor.TS_STREAM_TYPE_MPA: @@ -168,22 +173,25 @@ public final class DefaultTsPayloadReaderFactory implements TsPayloadReader.Fact return new PesReader(new H265Reader(buildSeiReader(esInfo))); case TsExtractor.TS_STREAM_TYPE_SPLICE_INFO: return isSet(FLAG_IGNORE_SPLICE_INFO_STREAM) - ? null : new SectionReader(new SpliceInfoSectionReader()); + ? null + : new SectionReader(new PassthroughSectionPayloadReader(MimeTypes.APPLICATION_SCTE35)); case TsExtractor.TS_STREAM_TYPE_ID3: return new PesReader(new Id3Reader()); case TsExtractor.TS_STREAM_TYPE_DVBSUBS: return new PesReader( new DvbSubtitleReader(esInfo.dvbSubtitleInfos)); + case TsExtractor.TS_STREAM_TYPE_AIT: + return new SectionReader(new PassthroughSectionPayloadReader(MimeTypes.APPLICATION_AIT)); default: return null; } } /** - * If {@link #FLAG_OVERRIDE_CAPTION_DESCRIPTORS} is set, returns a {@link SeiReader} for - * {@link #closedCaptionFormats}. If unset, parses the PMT descriptor information and returns a - * {@link SeiReader} for the declared formats, or {@link #closedCaptionFormats} if the descriptor - * is not present. + * If {@link #FLAG_OVERRIDE_CAPTION_DESCRIPTORS} is set, returns a {@link SeiReader} for {@link + * #closedCaptionFormats}. If unset, parses the PMT descriptor information and returns a {@link + * SeiReader} 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 SeiReader} for closed caption tracks. @@ -247,25 +255,21 @@ public final class DefaultTsPayloadReaderFactory implements TsPayloadReader.Fact // Skip reserved (8). scratchDescriptorData.skipBytes(1); - List initializationData = null; + @Nullable 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); + initializationData = + CodecSpecificDataUtil.buildCea708InitializationData(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)); + new Format.Builder() + .setSampleMimeType(mimeType) + .setLanguage(language) + .setAccessibilityChannel(accessibilityChannel) + .setInitializationData(initializationData) + .build()); } } else { // Unknown descriptor. Ignore. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/DtsReader.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/DtsReader.java similarity index 90% rename from library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/DtsReader.java rename to library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/DtsReader.java index 1f9b0e79d4..a201fb72d7 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/DtsReader.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/DtsReader.java @@ -15,13 +15,17 @@ */ package com.google.android.exoplayer2.extractor.ts; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.audio.DtsUtil; 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.Assertions; import com.google.android.exoplayer2.util.ParsableByteArray; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.checkerframework.checker.nullness.qual.RequiresNonNull; /** * Parses a continuous DTS byte stream and extracts individual samples. @@ -35,10 +39,10 @@ public final class DtsReader implements ElementaryStreamReader { private static final int HEADER_SIZE = 18; private final ParsableByteArray headerScratchBytes; - private final String language; + @Nullable private final String language; - private String formatId; - private TrackOutput output; + private @MonotonicNonNull String formatId; + private @MonotonicNonNull TrackOutput output; private int state; private int bytesRead; @@ -48,7 +52,7 @@ public final class DtsReader implements ElementaryStreamReader { // Used when parsing the header. private long sampleDurationUs; - private Format format; + private @MonotonicNonNull Format format; private int sampleSize; // Used when reading the samples. @@ -59,7 +63,7 @@ public final class DtsReader implements ElementaryStreamReader { * * @param language Track language. */ - public DtsReader(String language) { + public DtsReader(@Nullable String language) { headerScratchBytes = new ParsableByteArray(new byte[HEADER_SIZE]); state = STATE_FINDING_SYNC; this.language = language; @@ -86,6 +90,7 @@ public final class DtsReader implements ElementaryStreamReader { @Override public void consume(ParsableByteArray data) { + Assertions.checkStateNotNull(output); // Asserts that createTracks has been called. while (data.bytesLeft() > 0) { switch (state) { case STATE_FINDING_SYNC: @@ -162,9 +167,8 @@ public final class DtsReader implements ElementaryStreamReader { return false; } - /** - * Parses the sample header. - */ + /** Parses the sample header. */ + @RequiresNonNull("output") private void parseHeader() { byte[] frameData = headerScratchBytes.data; if (format == null) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/DvbSubtitleReader.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/DvbSubtitleReader.java similarity index 92% rename from library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/DvbSubtitleReader.java rename to library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/DvbSubtitleReader.java index 3f0a772b1c..9baaf85662 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/DvbSubtitleReader.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/DvbSubtitleReader.java @@ -61,15 +61,12 @@ public final class DvbSubtitleReader implements ElementaryStreamReader { idGenerator.generateNewId(); TrackOutput output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_TEXT); output.format( - Format.createImageSampleFormat( - idGenerator.getFormatId(), - MimeTypes.APPLICATION_DVBSUBS, - null, - Format.NO_VALUE, - 0, - Collections.singletonList(subtitleInfo.initializationData), - subtitleInfo.language, - null)); + new Format.Builder() + .setId(idGenerator.getFormatId()) + .setSampleMimeType(MimeTypes.APPLICATION_DVBSUBS) + .setInitializationData(Collections.singletonList(subtitleInfo.initializationData)) + .setLanguage(subtitleInfo.language) + .build()); outputs[i] = output; } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/ElementaryStreamReader.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/ElementaryStreamReader.java similarity index 100% rename from library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/ElementaryStreamReader.java rename to library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/ElementaryStreamReader.java diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/H262Reader.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/H262Reader.java similarity index 89% rename from library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/H262Reader.java rename to library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/H262Reader.java index e7f2c1935b..012de81297 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/H262Reader.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/H262Reader.java @@ -16,16 +16,20 @@ package com.google.android.exoplayer2.extractor.ts; import android.util.Pair; +import androidx.annotation.Nullable; 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.extractor.ts.TsPayloadReader.TrackIdGenerator; +import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.NalUnitUtil; import com.google.android.exoplayer2.util.ParsableByteArray; +import com.google.android.exoplayer2.util.Util; import java.util.Arrays; import java.util.Collections; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /** * Parses a continuous H262 byte stream and extracts individual frames. @@ -38,27 +42,27 @@ public final class H262Reader implements ElementaryStreamReader { private static final int START_GROUP = 0xB8; private static final int START_USER_DATA = 0xB2; - private String formatId; - private TrackOutput output; + private @MonotonicNonNull String formatId; + private @MonotonicNonNull TrackOutput output; // Maps (frame_rate_code - 1) indices to values, as defined in ITU-T H.262 Table 6-4. private static final double[] FRAME_RATE_VALUES = new double[] { 24000d / 1001, 24, 25, 30000d / 1001, 30, 50, 60000d / 1001, 60}; + @Nullable private final UserDataReader userDataReader; + @Nullable private final ParsableByteArray userDataParsable; + + // State that should be reset on seek. + @Nullable private final NalUnitTargetBuffer userData; + private final boolean[] prefixFlags; + private final CsdBuffer csdBuffer; + private long totalBytesWritten; + private boolean startedFirstSample; + // State that should not be reset on seek. 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; - // Per packet state that gets reset at the start of each packet. private long pesTimeUs; @@ -72,7 +76,7 @@ public final class H262Reader implements ElementaryStreamReader { this(null); } - /* package */ H262Reader(UserDataReader userDataReader) { + /* package */ H262Reader(@Nullable UserDataReader userDataReader) { this.userDataReader = userDataReader; prefixFlags = new boolean[4]; csdBuffer = new CsdBuffer(128); @@ -89,7 +93,7 @@ public final class H262Reader implements ElementaryStreamReader { public void seek() { NalUnitUtil.clearPrefixFlags(prefixFlags); csdBuffer.reset(); - if (userDataReader != null) { + if (userData != null) { userData.reset(); } totalBytesWritten = 0; @@ -114,6 +118,7 @@ public final class H262Reader implements ElementaryStreamReader { @Override public void consume(ParsableByteArray data) { + Assertions.checkStateNotNull(output); // Asserts that createTracks has been called. int offset = data.getPosition(); int limit = data.limit(); byte[] dataArray = data.data; @@ -130,7 +135,7 @@ public final class H262Reader implements ElementaryStreamReader { if (!hasOutputFormat) { csdBuffer.onData(dataArray, offset, limit); } - if (userDataReader != null) { + if (userData != null) { userData.appendToNalUnit(dataArray, offset, limit); } return; @@ -157,7 +162,7 @@ public final class H262Reader implements ElementaryStreamReader { hasOutputFormat = true; } } - if (userDataReader != null) { + if (userData != null) { int bytesAlreadyPassed = 0; if (lengthToStartCode > 0) { userData.appendToNalUnit(dataArray, offset, startCodeOffset); @@ -167,8 +172,8 @@ public final class H262Reader implements ElementaryStreamReader { if (userData.endNalUnit(bytesAlreadyPassed)) { int unescapedLength = NalUnitUtil.unescapeStream(userData.nalData, userData.nalLength); - userDataParsable.reset(userData.nalData, unescapedLength); - userDataReader.consume(sampleTimeUs, userDataParsable); + Util.castNonNull(userDataParsable).reset(userData.nalData, unescapedLength); + Util.castNonNull(userDataReader).consume(sampleTimeUs, userDataParsable); } if (startCodeValue == START_USER_DATA && data.data[startCodeOffset + 2] == 0x1) { @@ -211,10 +216,10 @@ public final class H262Reader implements ElementaryStreamReader { * * @param csdBuffer The csd buffer. * @param formatId The id for the generated format. May be null. - * @return A pair consisting of the {@link Format} and the frame duration in microseconds, or - * 0 if the duration could not be determined. + * @return A pair consisting of the {@link Format} and the frame duration in microseconds, or 0 if + * the duration could not be determined. */ - private static Pair parseCsdBuffer(CsdBuffer csdBuffer, String formatId) { + private static Pair parseCsdBuffer(CsdBuffer csdBuffer, @Nullable String formatId) { byte[] csdData = Arrays.copyOf(csdBuffer.data, csdBuffer.length); int firstByte = csdData[4] & 0xFF; @@ -240,9 +245,15 @@ public final class H262Reader implements ElementaryStreamReader { break; } - Format format = Format.createVideoSampleFormat(formatId, MimeTypes.VIDEO_MPEG2, null, - Format.NO_VALUE, Format.NO_VALUE, width, height, Format.NO_VALUE, - Collections.singletonList(csdData), Format.NO_VALUE, pixelWidthHeightRatio, null); + Format format = + new Format.Builder() + .setId(formatId) + .setSampleMimeType(MimeTypes.VIDEO_MPEG2) + .setWidth(width) + .setHeight(height) + .setPixelWidthHeightRatio(pixelWidthHeightRatio) + .setInitializationData(Collections.singletonList(csdData)) + .build(); long frameDurationUs = 0; int frameRateCodeMinusOne = (csdData[7] & 0x0F) - 1; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/H264Reader.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/H264Reader.java similarity index 87% rename from library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/H264Reader.java rename to library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/H264Reader.java index d249c1b9da..55f5fb34c6 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/H264Reader.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/H264Reader.java @@ -23,15 +23,21 @@ 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.Assertions; 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; import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.ParsableNalUnitBitArray; +import com.google.android.exoplayer2.util.Util; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import org.checkerframework.checker.nullness.qual.EnsuresNonNull; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.checker.nullness.qual.RequiresNonNull; /** * Parses a continuous H264 byte stream and extracts individual frames. @@ -51,9 +57,9 @@ public final class H264Reader implements ElementaryStreamReader { private long totalBytesWritten; private final boolean[] prefixFlags; - private String formatId; - private TrackOutput output; - private SampleReader sampleReader; + private @MonotonicNonNull String formatId; + private @MonotonicNonNull TrackOutput output; + private @MonotonicNonNull SampleReader sampleReader; // State that should not be reset on seek. private boolean hasOutputFormat; @@ -87,13 +93,15 @@ public final class H264Reader implements ElementaryStreamReader { @Override public void seek() { + totalBytesWritten = 0; + randomAccessIndicator = false; NalUnitUtil.clearPrefixFlags(prefixFlags); sps.reset(); pps.reset(); sei.reset(); - sampleReader.reset(); - totalBytesWritten = 0; - randomAccessIndicator = false; + if (sampleReader != null) { + sampleReader.reset(); + } } @Override @@ -113,6 +121,8 @@ public final class H264Reader implements ElementaryStreamReader { @Override public void consume(ParsableByteArray data) { + assertTracksCreated(); + int offset = data.getPosition(); int limit = data.limit(); byte[] dataArray = data.data; @@ -159,6 +169,7 @@ public final class H264Reader implements ElementaryStreamReader { // Do nothing. } + @RequiresNonNull("sampleReader") private void startNalUnit(long position, int nalUnitType, long pesTimeUs) { if (!hasOutputFormat || sampleReader.needsSpsPps()) { sps.startNalUnit(nalUnitType); @@ -168,6 +179,7 @@ public final class H264Reader implements ElementaryStreamReader { sampleReader.startNalUnit(position, nalUnitType, pesTimeUs); } + @RequiresNonNull("sampleReader") private void nalUnitData(byte[] dataArray, int offset, int limit) { if (!hasOutputFormat || sampleReader.needsSpsPps()) { sps.appendToNalUnit(dataArray, offset, limit); @@ -177,6 +189,7 @@ public final class H264Reader implements ElementaryStreamReader { sampleReader.appendToNalUnit(dataArray, offset, limit); } + @RequiresNonNull({"output", "sampleReader"}) private void endNalUnit(long position, int offset, int discardPadding, long pesTimeUs) { if (!hasOutputFormat || sampleReader.needsSpsPps()) { sps.endNalUnit(discardPadding); @@ -188,23 +201,21 @@ 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); + String codecs = + CodecSpecificDataUtil.buildAvcCodecString( + spsData.profileIdc, + spsData.constraintsFlagsAndReservedZero2Bits, + spsData.levelIdc); 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)); + new Format.Builder() + .setId(formatId) + .setSampleMimeType(MimeTypes.VIDEO_H264) + .setCodecs(codecs) + .setWidth(spsData.width) + .setHeight(spsData.height) + .setPixelWidthHeightRatio(spsData.pixelWidthAspectRatio) + .setInitializationData(initializationData) + .build()); hasOutputFormat = true; sampleReader.putSps(spsData); sampleReader.putPps(ppsData); @@ -237,6 +248,12 @@ public final class H264Reader implements ElementaryStreamReader { } } + @EnsuresNonNull({"output", "sampleReader"}) + private void assertTracksCreated() { + Assertions.checkStateNotNull(output); + Util.castNonNull(sampleReader); + } + /** Consumes a stream of NAL units and outputs samples. */ private static final class SampleReader { @@ -478,7 +495,7 @@ public final class H264Reader implements ElementaryStreamReader { private boolean isComplete; private boolean hasSliceType; - private SpsData spsData; + @Nullable private SpsData spsData; private int nalRefIdc; private int sliceType; private int frameNum; @@ -541,26 +558,32 @@ public final class H264Reader implements ElementaryStreamReader { } private boolean isFirstVclNalUnitOfPicture(SliceHeaderData other) { + if (!isComplete) { + return false; + } + if (!other.isComplete) { + return true; + } // See ISO 14496-10 subsection 7.4.1.2.4. - return isComplete - && (!other.isComplete - || frameNum != other.frameNum - || picParameterSetId != other.picParameterSetId - || fieldPicFlag != other.fieldPicFlag - || (bottomFieldFlagPresent - && other.bottomFieldFlagPresent - && bottomFieldFlag != other.bottomFieldFlag) - || (nalRefIdc != other.nalRefIdc && (nalRefIdc == 0 || other.nalRefIdc == 0)) - || (spsData.picOrderCountType == 0 - && other.spsData.picOrderCountType == 0 - && (picOrderCntLsb != other.picOrderCntLsb - || deltaPicOrderCntBottom != other.deltaPicOrderCntBottom)) - || (spsData.picOrderCountType == 1 - && other.spsData.picOrderCountType == 1 - && (deltaPicOrderCnt0 != other.deltaPicOrderCnt0 - || deltaPicOrderCnt1 != other.deltaPicOrderCnt1)) - || idrPicFlag != other.idrPicFlag - || (idrPicFlag && other.idrPicFlag && idrPicId != other.idrPicId)); + SpsData spsData = Assertions.checkStateNotNull(this.spsData); + SpsData otherSpsData = Assertions.checkStateNotNull(other.spsData); + return frameNum != other.frameNum + || picParameterSetId != other.picParameterSetId + || fieldPicFlag != other.fieldPicFlag + || (bottomFieldFlagPresent + && other.bottomFieldFlagPresent + && bottomFieldFlag != other.bottomFieldFlag) + || (nalRefIdc != other.nalRefIdc && (nalRefIdc == 0 || other.nalRefIdc == 0)) + || (spsData.picOrderCountType == 0 + && otherSpsData.picOrderCountType == 0 + && (picOrderCntLsb != other.picOrderCntLsb + || deltaPicOrderCntBottom != other.deltaPicOrderCntBottom)) + || (spsData.picOrderCountType == 1 + && otherSpsData.picOrderCountType == 1 + && (deltaPicOrderCnt0 != other.deltaPicOrderCnt0 + || deltaPicOrderCnt1 != other.deltaPicOrderCnt1)) + || idrPicFlag != other.idrPicFlag + || (idrPicFlag && idrPicId != other.idrPicId); } } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/H265Reader.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/H265Reader.java similarity index 82% rename from library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/H265Reader.java rename to library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/H265Reader.java index 88bde53746..c356b1c987 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/H265Reader.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/H265Reader.java @@ -20,12 +20,18 @@ 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.Assertions; import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.NalUnitUtil; import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.ParsableNalUnitBitArray; +import com.google.android.exoplayer2.util.Util; import java.util.Collections; +import org.checkerframework.checker.nullness.qual.EnsuresNonNull; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.checker.nullness.qual.RequiresNonNull; /** * Parses a continuous H.265 byte stream and extracts individual frames. @@ -41,14 +47,15 @@ public final class H265Reader implements ElementaryStreamReader { private static final int VPS_NUT = 32; private static final int SPS_NUT = 33; private static final int PPS_NUT = 34; + private static final int AUD_NUT = 35; private static final int PREFIX_SEI_NUT = 39; private static final int SUFFIX_SEI_NUT = 40; private final SeiReader seiReader; - private String formatId; - private TrackOutput output; - private SampleReader sampleReader; + private @MonotonicNonNull String formatId; + private @MonotonicNonNull TrackOutput output; + private @MonotonicNonNull SampleReader sampleReader; // State that should not be reset on seek. private boolean hasOutputFormat; @@ -59,7 +66,7 @@ public final class H265Reader implements ElementaryStreamReader { private final NalUnitTargetBuffer sps; private final NalUnitTargetBuffer pps; private final NalUnitTargetBuffer prefixSei; - private final NalUnitTargetBuffer suffixSei; // TODO: Are both needed? + private final NalUnitTargetBuffer suffixSei; private long totalBytesWritten; // Per packet state that gets reset at the start of each packet. @@ -84,14 +91,16 @@ public final class H265Reader implements ElementaryStreamReader { @Override public void seek() { + totalBytesWritten = 0; NalUnitUtil.clearPrefixFlags(prefixFlags); vps.reset(); sps.reset(); pps.reset(); prefixSei.reset(); suffixSei.reset(); - sampleReader.reset(); - totalBytesWritten = 0; + if (sampleReader != null) { + sampleReader.reset(); + } } @Override @@ -111,6 +120,8 @@ public final class H265Reader implements ElementaryStreamReader { @Override public void consume(ParsableByteArray data) { + assertTracksCreated(); + while (data.bytesLeft() > 0) { int offset = data.getPosition(); int limit = data.limit(); @@ -160,10 +171,10 @@ public final class H265Reader implements ElementaryStreamReader { // Do nothing. } + @RequiresNonNull("sampleReader") private void startNalUnit(long position, int offset, int nalUnitType, long pesTimeUs) { - if (hasOutputFormat) { - sampleReader.startNalUnit(position, offset, nalUnitType, pesTimeUs); - } else { + sampleReader.startNalUnit(position, offset, nalUnitType, pesTimeUs, hasOutputFormat); + if (!hasOutputFormat) { vps.startNalUnit(nalUnitType); sps.startNalUnit(nalUnitType); pps.startNalUnit(nalUnitType); @@ -172,10 +183,10 @@ public final class H265Reader implements ElementaryStreamReader { suffixSei.startNalUnit(nalUnitType); } + @RequiresNonNull("sampleReader") private void nalUnitData(byte[] dataArray, int offset, int limit) { - if (hasOutputFormat) { - sampleReader.readNalUnitData(dataArray, offset, limit); - } else { + sampleReader.readNalUnitData(dataArray, offset, limit); + if (!hasOutputFormat) { vps.appendToNalUnit(dataArray, offset, limit); sps.appendToNalUnit(dataArray, offset, limit); pps.appendToNalUnit(dataArray, offset, limit); @@ -184,10 +195,10 @@ public final class H265Reader implements ElementaryStreamReader { suffixSei.appendToNalUnit(dataArray, offset, limit); } + @RequiresNonNull({"output", "sampleReader"}) private void endNalUnit(long position, int offset, int discardPadding, long pesTimeUs) { - if (hasOutputFormat) { - sampleReader.endNalUnit(position, offset); - } else { + sampleReader.endNalUnit(position, offset, hasOutputFormat); + if (!hasOutputFormat) { vps.endNalUnit(discardPadding); sps.endNalUnit(discardPadding); pps.endNalUnit(discardPadding); @@ -214,13 +225,16 @@ public final class H265Reader implements ElementaryStreamReader { } } - private static Format parseMediaFormat(String formatId, NalUnitTargetBuffer vps, - NalUnitTargetBuffer sps, NalUnitTargetBuffer pps) { + private static Format parseMediaFormat( + @Nullable String formatId, + NalUnitTargetBuffer vps, + NalUnitTargetBuffer sps, + NalUnitTargetBuffer pps) { // Build codec-specific data. - byte[] csd = new byte[vps.nalLength + sps.nalLength + pps.nalLength]; - System.arraycopy(vps.nalData, 0, csd, 0, vps.nalLength); - System.arraycopy(sps.nalData, 0, csd, vps.nalLength, sps.nalLength); - System.arraycopy(pps.nalData, 0, csd, vps.nalLength + sps.nalLength, pps.nalLength); + byte[] csdData = new byte[vps.nalLength + sps.nalLength + pps.nalLength]; + System.arraycopy(vps.nalData, 0, csdData, 0, vps.nalLength); + System.arraycopy(sps.nalData, 0, csdData, vps.nalLength, sps.nalLength); + System.arraycopy(pps.nalData, 0, csdData, vps.nalLength + sps.nalLength, pps.nalLength); // Parse the SPS NAL unit, as per H.265/HEVC (2014) 7.3.2.2.1. ParsableNalUnitBitArray bitArray = new ParsableNalUnitBitArray(sps.nalData, 0, sps.nalLength); @@ -320,9 +334,14 @@ public final class H265Reader implements ElementaryStreamReader { } } - return Format.createVideoSampleFormat(formatId, MimeTypes.VIDEO_H265, null, Format.NO_VALUE, - Format.NO_VALUE, picWidthInLumaSamples, picHeightInLumaSamples, Format.NO_VALUE, - Collections.singletonList(csd), Format.NO_VALUE, pixelWidthHeightRatio, null); + return new Format.Builder() + .setId(formatId) + .setSampleMimeType(MimeTypes.VIDEO_H265) + .setWidth(picWidthInLumaSamples) + .setHeight(picHeightInLumaSamples) + .setPixelWidthHeightRatio(pixelWidthHeightRatio) + .setInitializationData(Collections.singletonList(csdData)) + .build(); } /** @@ -389,6 +408,12 @@ public final class H265Reader implements ElementaryStreamReader { } } + @EnsuresNonNull({"output", "sampleReader"}) + private void assertTracksCreated() { + Assertions.checkStateNotNull(output); + Util.castNonNull(sampleReader); + } + private static final class SampleReader { /** @@ -400,17 +425,17 @@ public final class H265Reader implements ElementaryStreamReader { private final TrackOutput output; // Per NAL unit state. A sample consists of one or more NAL units. - private long nalUnitStartPosition; + private long nalUnitPosition; private boolean nalUnitHasKeyframeData; private int nalUnitBytesRead; private long nalUnitTimeUs; private boolean lookingForFirstSliceFlag; private boolean isFirstSlice; - private boolean isFirstParameterSet; + private boolean isFirstPrefixNalUnit; // Per sample state that gets reset at the start of each sample. private boolean readingSample; - private boolean writingParameterSets; + private boolean readingPrefix; private long samplePosition; private long sampleTimeUs; private boolean sampleIsKeyframe; @@ -422,32 +447,33 @@ public final class H265Reader implements ElementaryStreamReader { public void reset() { lookingForFirstSliceFlag = false; isFirstSlice = false; - isFirstParameterSet = false; + isFirstPrefixNalUnit = false; readingSample = false; - writingParameterSets = false; + readingPrefix = false; } - public void startNalUnit(long position, int offset, int nalUnitType, long pesTimeUs) { + public void startNalUnit( + long position, int offset, int nalUnitType, long pesTimeUs, boolean hasOutputFormat) { isFirstSlice = false; - isFirstParameterSet = false; + isFirstPrefixNalUnit = false; nalUnitTimeUs = pesTimeUs; nalUnitBytesRead = 0; - nalUnitStartPosition = position; + nalUnitPosition = position; - if (nalUnitType >= VPS_NUT) { - if (!writingParameterSets && readingSample) { - // This is a non-VCL NAL unit, so flush the previous sample. - outputSample(offset); + if (!isVclBodyNalUnit(nalUnitType)) { + if (readingSample && !readingPrefix) { + if (hasOutputFormat) { + outputSample(offset); + } readingSample = false; } - if (nalUnitType <= PPS_NUT) { - // This sample will have parameter sets at the start. - isFirstParameterSet = !writingParameterSets; - writingParameterSets = true; + if (isPrefixNalUnit(nalUnitType)) { + isFirstPrefixNalUnit = !readingPrefix; + readingPrefix = true; } } - // Look for the flag if this NAL unit contains a slice_segment_layer_rbsp. + // Look for the first slice flag if this NAL unit contains a slice_segment_layer_rbsp. nalUnitHasKeyframeData = (nalUnitType >= BLA_W_LP && nalUnitType <= CRA_NUT); lookingForFirstSliceFlag = nalUnitHasKeyframeData || nalUnitType <= RASL_R; } @@ -464,31 +490,39 @@ public final class H265Reader implements ElementaryStreamReader { } } - public void endNalUnit(long position, int offset) { - if (writingParameterSets && isFirstSlice) { + public void endNalUnit(long position, int offset, boolean hasOutputFormat) { + if (readingPrefix && isFirstSlice) { // This sample has parameter sets. Reset the key-frame flag based on the first slice. sampleIsKeyframe = nalUnitHasKeyframeData; - writingParameterSets = false; - } else if (isFirstParameterSet || isFirstSlice) { + readingPrefix = false; + } else if (isFirstPrefixNalUnit || isFirstSlice) { // This NAL unit is at the start of a new sample (access unit). - if (readingSample) { + if (hasOutputFormat && readingSample) { // Output the sample ending before this NAL unit. - int nalUnitLength = (int) (position - nalUnitStartPosition); + int nalUnitLength = (int) (position - nalUnitPosition); outputSample(offset + nalUnitLength); } - samplePosition = nalUnitStartPosition; + samplePosition = nalUnitPosition; sampleTimeUs = nalUnitTimeUs; - readingSample = true; sampleIsKeyframe = nalUnitHasKeyframeData; + readingSample = true; } } private void outputSample(int offset) { @C.BufferFlags int flags = sampleIsKeyframe ? C.BUFFER_FLAG_KEY_FRAME : 0; - int size = (int) (nalUnitStartPosition - samplePosition); + int size = (int) (nalUnitPosition - samplePosition); output.sampleMetadata(sampleTimeUs, flags, size, offset, null); } - } + /** Returns whether a NAL unit type is one that occurs before any VCL NAL units in a sample. */ + private static boolean isPrefixNalUnit(int nalUnitType) { + return (VPS_NUT <= nalUnitType && nalUnitType <= AUD_NUT) || nalUnitType == PREFIX_SEI_NUT; + } + /** Returns whether a NAL unit type is one that occurs in the VLC body of a sample. */ + private static boolean isVclBodyNalUnit(int nalUnitType) { + return nalUnitType < VPS_NUT || nalUnitType == SUFFIX_SEI_NUT; + } + } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Id3Reader.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/Id3Reader.java similarity index 88% rename from library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Id3Reader.java rename to library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/Id3Reader.java index 77ec48d0a7..28c54892c4 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Id3Reader.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/Id3Reader.java @@ -23,9 +23,11 @@ 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.Assertions; import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.ParsableByteArray; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /** * Parses ID3 data and extracts individual text information frames. @@ -36,7 +38,7 @@ public final class Id3Reader implements ElementaryStreamReader { private final ParsableByteArray id3Header; - private TrackOutput output; + private @MonotonicNonNull TrackOutput output; // State that should be reset on seek. private boolean writingSample; @@ -59,8 +61,11 @@ public final class Id3Reader implements ElementaryStreamReader { public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) { idGenerator.generateNewId(); output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_METADATA); - output.format(Format.createSampleFormat(idGenerator.getFormatId(), MimeTypes.APPLICATION_ID3, - null, Format.NO_VALUE, null)); + output.format( + new Format.Builder() + .setId(idGenerator.getFormatId()) + .setSampleMimeType(MimeTypes.APPLICATION_ID3) + .build()); } @Override @@ -76,6 +81,7 @@ public final class Id3Reader implements ElementaryStreamReader { @Override public void consume(ParsableByteArray data) { + Assertions.checkStateNotNull(output); // Asserts that createTracks has been called. if (!writingSample) { return; } @@ -106,6 +112,7 @@ public final class Id3Reader implements ElementaryStreamReader { @Override public void packetFinished() { + Assertions.checkStateNotNull(output); // Asserts that createTracks has been called. if (!writingSample || sampleSize == 0 || sampleBytesRead != sampleSize) { return; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/LatmReader.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/LatmReader.java similarity index 87% rename from library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/LatmReader.java rename to library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/LatmReader.java index 4ad9adfa2a..3465d89318 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/LatmReader.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/LatmReader.java @@ -15,19 +15,21 @@ */ package com.google.android.exoplayer2.extractor.ts; -import android.util.Pair; import androidx.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.audio.AacUtil; 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.Assertions; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.ParsableBitArray; import com.google.android.exoplayer2.util.ParsableByteArray; import java.util.Collections; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.checkerframework.checker.nullness.qual.RequiresNonNull; /** * Parses and extracts samples from an AAC/LATM elementary stream. @@ -43,14 +45,14 @@ public final class LatmReader implements ElementaryStreamReader { private static final int SYNC_BYTE_FIRST = 0x56; private static final int SYNC_BYTE_SECOND = 0xE0; - private final String language; + @Nullable private final String language; private final ParsableByteArray sampleDataBuffer; private final ParsableBitArray sampleBitArray; // Track output info. - private TrackOutput output; - private Format format; - private String formatId; + private @MonotonicNonNull TrackOutput output; + private @MonotonicNonNull String formatId; + private @MonotonicNonNull Format format; // Parser state info. private int state; @@ -69,6 +71,7 @@ public final class LatmReader implements ElementaryStreamReader { private int sampleRateHz; private long sampleDurationUs; private int channelCount; + @Nullable private String codecs; /** * @param language Track language. @@ -99,6 +102,7 @@ public final class LatmReader implements ElementaryStreamReader { @Override public void consume(ParsableByteArray data) throws ParserException { + Assertions.checkStateNotNull(output); // Asserts that createTracks has been called. int bytesToRead; while (data.bytesLeft() > 0) { switch (state) { @@ -150,6 +154,7 @@ public final class LatmReader implements ElementaryStreamReader { * * @param data A {@link ParsableBitArray} containing the AudioMuxElement's bytes. */ + @RequiresNonNull("output") private void parseAudioMuxElement(ParsableBitArray data) throws ParserException { boolean useSameStreamMux = data.readBit(); if (!useSameStreamMux) { @@ -173,9 +178,8 @@ public final class LatmReader implements ElementaryStreamReader { } } - /** - * Parses a StreamMuxConfig as defined in ISO/IEC 14496-3:2009 Section 1.7.3.1, Table 1.42. - */ + /** Parses a StreamMuxConfig as defined in ISO/IEC 14496-3:2009 Section 1.7.3.1, Table 1.42. */ + @RequiresNonNull("output") private void parseStreamMuxConfig(ParsableBitArray data) throws ParserException { int audioMuxVersion = data.readBits(1); audioMuxVersionA = audioMuxVersion == 1 ? data.readBits(1) : 0; @@ -198,9 +202,16 @@ public final class LatmReader implements ElementaryStreamReader { data.setPosition(startPosition); byte[] initData = new byte[(readBits + 7) / 8]; data.readBits(initData, 0, readBits); - Format format = Format.createAudioSampleFormat(formatId, MimeTypes.AUDIO_AAC, null, - Format.NO_VALUE, Format.NO_VALUE, channelCount, sampleRateHz, - Collections.singletonList(initData), null, 0, language); + Format format = + new Format.Builder() + .setId(formatId) + .setSampleMimeType(MimeTypes.AUDIO_AAC) + .setCodecs(codecs) + .setChannelCount(channelCount) + .setSampleRate(sampleRateHz) + .setInitializationData(Collections.singletonList(initData)) + .setLanguage(language) + .build(); if (!format.equals(this.format)) { this.format = format; sampleDurationUs = (C.MICROS_PER_SECOND * 1024) / format.sampleRate; @@ -259,9 +270,10 @@ public final class LatmReader implements ElementaryStreamReader { private int parseAudioSpecificConfig(ParsableBitArray data) throws ParserException { int bitsLeft = data.bitsLeft(); - Pair config = CodecSpecificDataUtil.parseAacAudioSpecificConfig(data, true); - sampleRateHz = config.first; - channelCount = config.second; + AacUtil.Config config = AacUtil.parseAudioSpecificConfig(data, /* forceReadToEnd= */ true); + codecs = config.codecs; + sampleRateHz = config.sampleRateHz; + channelCount = config.channelCount; return bitsLeft - data.bitsLeft(); } @@ -280,6 +292,7 @@ public final class LatmReader implements ElementaryStreamReader { } } + @RequiresNonNull("output") private void parsePayloadMux(ParsableBitArray data, int muxLengthBytes) { // The start of sample data in int bitPosition = data.getPosition(); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/MpegAudioReader.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/MpegAudioReader.java similarity index 78% rename from library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/MpegAudioReader.java rename to library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/MpegAudioReader.java index 393e297818..44870c3025 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/MpegAudioReader.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/MpegAudioReader.java @@ -17,11 +17,15 @@ package com.google.android.exoplayer2.extractor.ts; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.audio.MpegAudioUtil; import com.google.android.exoplayer2.extractor.ExtractorOutput; -import com.google.android.exoplayer2.extractor.MpegAudioHeader; import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator; +import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.ParsableByteArray; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.checker.nullness.qual.RequiresNonNull; /** * Parses a continuous MPEG Audio byte stream and extracts individual frames. @@ -35,11 +39,11 @@ public final class MpegAudioReader implements ElementaryStreamReader { private static final int HEADER_SIZE = 4; private final ParsableByteArray headerScratch; - private final MpegAudioHeader header; - private final String language; + private final MpegAudioUtil.Header header; + @Nullable private final String language; - private String formatId; - private TrackOutput output; + private @MonotonicNonNull TrackOutput output; + private @MonotonicNonNull String formatId; private int state; private int frameBytesRead; @@ -59,12 +63,12 @@ public final class MpegAudioReader implements ElementaryStreamReader { this(null); } - public MpegAudioReader(String language) { + public MpegAudioReader(@Nullable String language) { state = STATE_FINDING_HEADER; // The first byte of an MPEG Audio frame header is always 0xFF. headerScratch = new ParsableByteArray(4); headerScratch.data[0] = (byte) 0xFF; - header = new MpegAudioHeader(); + header = new MpegAudioUtil.Header(); this.language = language; } @@ -89,6 +93,7 @@ public final class MpegAudioReader implements ElementaryStreamReader { @Override public void consume(ParsableByteArray data) { + Assertions.checkStateNotNull(output); // Asserts that createTracks has been called. while (data.bytesLeft() > 0) { switch (state) { case STATE_FINDING_HEADER: @@ -146,20 +151,21 @@ public final class MpegAudioReader implements ElementaryStreamReader { /** * Attempts to read the remaining two bytes of the frame header. - *

      - * If a frame header is read in full then the state is changed to {@link #STATE_READING_FRAME}, + * + *

      If a frame header is read in full then the state is changed to {@link #STATE_READING_FRAME}, * the media format is output if this has not previously occurred, the four header bytes are * output as sample data, and the position of the source is advanced to the byte that immediately * follows the header. - *

      - * If a frame header is read in full but cannot be parsed then the state is changed to - * {@link #STATE_READING_HEADER}. - *

      - * If a frame header is not read in full then the position of the source is advanced to the limit, - * and the method should be called again with the next source to continue the read. + * + *

      If a frame header is read in full but cannot be parsed then the state is changed to {@link + * #STATE_READING_HEADER}. + * + *

      If a frame header is not read in full then the position of the source is advanced to the + * limit, and the method should be called again with the next source to continue the read. * * @param source The source from which to read. */ + @RequiresNonNull("output") private void readHeaderRemainder(ParsableByteArray source) { int bytesToRead = Math.min(source.bytesLeft(), HEADER_SIZE - frameBytesRead); source.readBytes(headerScratch.data, frameBytesRead, bytesToRead); @@ -170,7 +176,7 @@ public final class MpegAudioReader implements ElementaryStreamReader { } headerScratch.setPosition(0); - boolean parsedHeader = MpegAudioHeader.populateHeader(headerScratch.readInt(), header); + boolean parsedHeader = header.setForHeaderData(headerScratch.readInt()); if (!parsedHeader) { // We thought we'd located a frame header, but we hadn't. frameBytesRead = 0; @@ -181,9 +187,15 @@ public final class MpegAudioReader implements ElementaryStreamReader { frameSize = header.frameSize; if (!hasOutputFormat) { frameDurationUs = (C.MICROS_PER_SECOND * header.samplesPerFrame) / header.sampleRate; - Format format = Format.createAudioSampleFormat(formatId, header.mimeType, null, - Format.NO_VALUE, MpegAudioHeader.MAX_FRAME_SIZE_BYTES, header.channels, header.sampleRate, - null, null, 0, language); + Format format = + new Format.Builder() + .setId(formatId) + .setSampleMimeType(header.mimeType) + .setMaxInputSize(MpegAudioUtil.MAX_FRAME_SIZE_BYTES) + .setChannelCount(header.channels) + .setSampleRate(header.sampleRate) + .setLanguage(language) + .build(); output.format(format); hasOutputFormat = true; } @@ -195,16 +207,17 @@ public final class MpegAudioReader implements ElementaryStreamReader { /** * Attempts to read the remainder of the frame. - *

      - * If a frame is read in full then true is returned. The frame will have been output, and the + * + *

      If a frame is read in full then true is returned. The frame will have been output, and the * position of the source will have been advanced to the byte that immediately follows the end of * the frame. - *

      - * If a frame is not read in full then the position of the source will have been advanced to the - * limit, and the method should be called again with the next source to continue the read. + * + *

      If a frame is not read in full then the position of the source will have been advanced to + * the limit, and the method should be called again with the next source to continue the read. * * @param source The source from which to read. */ + @RequiresNonNull("output") private void readFrameRemainder(ParsableByteArray source) { int bytesToRead = Math.min(source.bytesLeft(), frameSize - frameBytesRead); output.sampleData(source, bytesToRead); @@ -219,5 +232,4 @@ public final class MpegAudioReader implements ElementaryStreamReader { frameBytesRead = 0; state = STATE_FINDING_HEADER; } - } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/NalUnitTargetBuffer.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/NalUnitTargetBuffer.java similarity index 100% rename from library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/NalUnitTargetBuffer.java rename to library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/NalUnitTargetBuffer.java diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/PassthroughSectionPayloadReader.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/PassthroughSectionPayloadReader.java new file mode 100644 index 0000000000..72667e2a3f --- /dev/null +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/PassthroughSectionPayloadReader.java @@ -0,0 +1,89 @@ +/* + * 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.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.util.Assertions; +import com.google.android.exoplayer2.util.ParsableByteArray; +import com.google.android.exoplayer2.util.TimestampAdjuster; +import com.google.android.exoplayer2.util.Util; +import org.checkerframework.checker.nullness.qual.EnsuresNonNull; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; + +/** + * A {@link SectionPayloadReader} that directly outputs the section bytes as sample data. + * + *

      Timestamp adjustment is provided through {@link Format#subsampleOffsetUs}. + */ +public final class PassthroughSectionPayloadReader implements SectionPayloadReader { + + private Format format; + private @MonotonicNonNull TimestampAdjuster timestampAdjuster; + private @MonotonicNonNull TrackOutput output; + + /** + * Create a new PassthroughSectionPayloadReader. + * + * @param mimeType The MIME type set as {@link Format#sampleMimeType} on the created output track. + */ + public PassthroughSectionPayloadReader(String mimeType) { + this.format = new Format.Builder().setSampleMimeType(mimeType).build(); + } + + @Override + public void init( + TimestampAdjuster timestampAdjuster, + ExtractorOutput extractorOutput, + TsPayloadReader.TrackIdGenerator idGenerator) { + this.timestampAdjuster = timestampAdjuster; + idGenerator.generateNewId(); + output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_METADATA); + // Eagerly output an incomplete format (missing timestamp offset) to ensure source preparation + // is not blocked waiting for potentially sparse metadata. + output.format(format); + } + + @Override + public void consume(ParsableByteArray sectionData) { + assertInitialized(); + long subsampleOffsetUs = timestampAdjuster.getTimestampOffsetUs(); + if (subsampleOffsetUs == C.TIME_UNSET) { + // Don't output samples without a known subsample offset. + return; + } + if (subsampleOffsetUs != format.subsampleOffsetUs) { + format = format.buildUpon().setSubsampleOffsetUs(subsampleOffsetUs).build(); + output.format(format); + } + int sampleSize = sectionData.bytesLeft(); + output.sampleData(sectionData, sampleSize); + output.sampleMetadata( + timestampAdjuster.getLastAdjustedTimestampUs(), + C.BUFFER_FLAG_KEY_FRAME, + sampleSize, + 0, + null); + } + + @EnsuresNonNull({"timestampAdjuster", "output"}) + private void assertInitialized() { + Assertions.checkStateNotNull(timestampAdjuster); + Util.castNonNull(output); + } +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/PesReader.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/PesReader.java similarity index 93% rename from library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/PesReader.java rename to library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/PesReader.java index ff755f4ece..f84d323f96 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/PesReader.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/PesReader.java @@ -15,13 +15,17 @@ */ package com.google.android.exoplayer2.extractor.ts; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.extractor.ExtractorOutput; +import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.ParsableBitArray; import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.TimestampAdjuster; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.checkerframework.checker.nullness.qual.RequiresNonNull; /** * Parses PES packet data and extracts samples. @@ -45,7 +49,7 @@ public final class PesReader implements TsPayloadReader { private int state; private int bytesRead; - private TimestampAdjuster timestampAdjuster; + private @MonotonicNonNull TimestampAdjuster timestampAdjuster; private boolean ptsFlag; private boolean dtsFlag; private boolean seenFirstDts; @@ -79,6 +83,8 @@ public final class PesReader implements TsPayloadReader { @Override public final void consume(ParsableByteArray data, @Flags int flags) throws ParserException { + Assertions.checkStateNotNull(timestampAdjuster); // Asserts init has been called. + if ((flags & FLAG_PAYLOAD_UNIT_START_INDICATOR) != 0) { switch (state) { case STATE_FINDING_HEADER: @@ -119,7 +125,7 @@ public final class PesReader implements TsPayloadReader { int readLength = Math.min(MAX_HEADER_EXTENSION_SIZE, extendedHeaderLength); // Read as much of the extended header as we're interested in, and skip the rest. if (continueRead(data, pesScratch.data, readLength) - && continueRead(data, null, extendedHeaderLength)) { + && continueRead(data, /* target= */ null, extendedHeaderLength)) { parseHeaderExtension(); flags |= dataAlignmentIndicator ? FLAG_DATA_ALIGNMENT_INDICATOR : 0; reader.packetStarted(timeUs, flags); @@ -162,7 +168,8 @@ public final class PesReader implements TsPayloadReader { * @param targetLength The target length of the read. * @return Whether the target length has been reached. */ - private boolean continueRead(ParsableByteArray source, byte[] target, int targetLength) { + private boolean continueRead( + ParsableByteArray source, @Nullable byte[] target, int targetLength) { int bytesToRead = Math.min(source.bytesLeft(), targetLength - bytesRead); if (bytesToRead <= 0) { return true; @@ -207,6 +214,7 @@ public final class PesReader implements TsPayloadReader { return true; } + @RequiresNonNull("timestampAdjuster") private void parseHeaderExtension() { pesScratch.setPosition(0); timeUs = C.TIME_UNSET; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/PsBinarySearchSeeker.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/PsBinarySearchSeeker.java similarity index 99% rename from library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/PsBinarySearchSeeker.java rename to library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/PsBinarySearchSeeker.java index c4f53ba176..09cf9b3f00 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/PsBinarySearchSeeker.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/PsBinarySearchSeeker.java @@ -70,7 +70,7 @@ import java.io.IOException; @Override public TimestampSearchResult searchForTimestamp(ExtractorInput input, long targetTimestamp) - throws IOException, InterruptedException { + throws IOException { long inputPosition = input.getPosition(); int bytesToSearch = (int) Math.min(TIMESTAMP_SEARCH_BYTES, input.getLength() - inputPosition); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/PsDurationReader.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/PsDurationReader.java similarity index 97% rename from library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/PsDurationReader.java rename to library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/PsDurationReader.java index b0cdf7eb79..4748b832de 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/PsDurationReader.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/PsDurationReader.java @@ -81,11 +81,9 @@ import java.io.IOException; * 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 { + ExtractorInput input, PositionHolder seekPositionHolder) throws IOException { if (!isLastScrValueRead) { return readLastScrValue(input, seekPositionHolder); } @@ -137,7 +135,7 @@ import java.io.IOException; } private int readFirstScrValue(ExtractorInput input, PositionHolder seekPositionHolder) - throws IOException, InterruptedException { + throws IOException { int bytesToSearch = (int) Math.min(TIMESTAMP_SEARCH_BYTES, input.getLength()); int searchStartPosition = 0; if (input.getPosition() != searchStartPosition) { @@ -173,7 +171,7 @@ import java.io.IOException; } private int readLastScrValue(ExtractorInput input, PositionHolder seekPositionHolder) - throws IOException, InterruptedException { + throws IOException { long inputLength = input.getLength(); int bytesToSearch = (int) Math.min(TIMESTAMP_SEARCH_BYTES, inputLength); long searchStartPosition = inputLength - bytesToSearch; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/PsExtractor.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/PsExtractor.java similarity index 96% rename from library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/PsExtractor.java rename to library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/PsExtractor.java index fec108fd5f..96bdc22631 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/PsExtractor.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/PsExtractor.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.extractor.ts; import android.util.SparseArray; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.extractor.Extractor; @@ -25,10 +26,13 @@ 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.TimestampAdjuster; import java.io.IOException; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.checkerframework.checker.nullness.qual.RequiresNonNull; /** * Extracts data from the MPEG-2 PS container format. @@ -67,8 +71,8 @@ public final class PsExtractor implements Extractor { private long lastTrackPosition; // Accessed only by the loading thread. - private PsBinarySearchSeeker psBinarySearchSeeker; - private ExtractorOutput output; + @Nullable private PsBinarySearchSeeker psBinarySearchSeeker; + private @MonotonicNonNull ExtractorOutput output; private boolean hasOutputSeekMap; public PsExtractor() { @@ -85,7 +89,7 @@ public final class PsExtractor implements Extractor { // Extractor implementation. @Override - public boolean sniff(ExtractorInput input) throws IOException, InterruptedException { + public boolean sniff(ExtractorInput input) throws IOException { byte[] scratch = new byte[14]; input.peekFully(scratch, 0, 14); @@ -158,8 +162,8 @@ public final class PsExtractor implements Extractor { } @Override - public int read(ExtractorInput input, PositionHolder seekPosition) - throws IOException, InterruptedException { + public int read(ExtractorInput input, PositionHolder seekPosition) throws IOException { + Assertions.checkStateNotNull(output); // Asserts init has been called. long inputLength = input.getLength(); boolean canReadDuration = inputLength != C.LENGTH_UNSET; @@ -221,7 +225,7 @@ public final class PsExtractor implements Extractor { PesReader payloadReader = psPayloadReaders.get(streamId); if (!foundAllTracks) { if (payloadReader == null) { - ElementaryStreamReader elementaryStreamReader = null; + @Nullable ElementaryStreamReader elementaryStreamReader = null; 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 @@ -278,6 +282,7 @@ public final class PsExtractor implements Extractor { // Internals. + @RequiresNonNull("output") private void maybeOutputSeekMap(long inputLength) { if (!hasOutputSeekMap) { hasOutputSeekMap = true; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/SectionPayloadReader.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/SectionPayloadReader.java similarity index 100% rename from library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/SectionPayloadReader.java rename to library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/SectionPayloadReader.java diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/SectionReader.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/SectionReader.java similarity index 100% rename from library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/SectionReader.java rename to library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/SectionReader.java diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/SeiReader.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/SeiReader.java similarity index 81% rename from library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/SeiReader.java rename to library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/SeiReader.java index d032ef5883..6d8cb0da8c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/SeiReader.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/SeiReader.java @@ -15,12 +15,13 @@ */ package com.google.android.exoplayer2.extractor.ts; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.extractor.CeaUtil; 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.text.cea.CeaUtil; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.ParsableByteArray; @@ -45,23 +46,20 @@ public final class SeiReader { idGenerator.generateNewId(); TrackOutput output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_TEXT); Format channelFormat = closedCaptionFormats.get(i); - String channelMimeType = channelFormat.sampleMimeType; + @Nullable String channelMimeType = channelFormat.sampleMimeType; Assertions.checkArgument(MimeTypes.APPLICATION_CEA608.equals(channelMimeType) || 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, - /* codecs= */ null, - /* bitrate= */ Format.NO_VALUE, - channelFormat.selectionFlags, - channelFormat.language, - channelFormat.accessibilityChannel, - /* drmInitData= */ null, - Format.OFFSET_SAMPLE_RELATIVE, - channelFormat.initializationData)); + new Format.Builder() + .setId(formatId) + .setSampleMimeType(channelMimeType) + .setSelectionFlags(channelFormat.selectionFlags) + .setLanguage(channelFormat.language) + .setAccessibilityChannel(channelFormat.accessibilityChannel) + .setInitializationData(channelFormat.initializationData) + .build()); outputs[i] = output; } } @@ -69,5 +67,4 @@ public final class SeiReader { public void consume(long pesTimeUs, ParsableByteArray seiBuffer) { CeaUtil.consume(pesTimeUs, seiBuffer, outputs); } - } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsBinarySearchSeeker.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/TsBinarySearchSeeker.java similarity index 99% rename from library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsBinarySearchSeeker.java rename to library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/TsBinarySearchSeeker.java index a627c00ba2..8a1d2b2fdf 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsBinarySearchSeeker.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/TsBinarySearchSeeker.java @@ -74,7 +74,7 @@ import java.io.IOException; @Override public TimestampSearchResult searchForTimestamp(ExtractorInput input, long targetTimestamp) - throws IOException, InterruptedException { + throws IOException { long inputPosition = input.getPosition(); int bytesToSearch = (int) Math.min(TIMESTAMP_SEARCH_BYTES, input.getLength() - inputPosition); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsDurationReader.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/TsDurationReader.java similarity index 97% rename from library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsDurationReader.java rename to library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/TsDurationReader.java index 804a643414..a60d3fcb82 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsDurationReader.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/TsDurationReader.java @@ -74,11 +74,9 @@ import java.io.IOException; * @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 { + ExtractorInput input, PositionHolder seekPositionHolder, int pcrPid) throws IOException { if (pcrPid <= 0) { return finishReadDuration(input); } @@ -124,7 +122,7 @@ import java.io.IOException; } private int readFirstPcrValue(ExtractorInput input, PositionHolder seekPositionHolder, int pcrPid) - throws IOException, InterruptedException { + throws IOException { int bytesToSearch = (int) Math.min(TIMESTAMP_SEARCH_BYTES, input.getLength()); int searchStartPosition = 0; if (input.getPosition() != searchStartPosition) { @@ -159,7 +157,7 @@ import java.io.IOException; } private int readLastPcrValue(ExtractorInput input, PositionHolder seekPositionHolder, int pcrPid) - throws IOException, InterruptedException { + throws IOException { long inputLength = input.getLength(); int bytesToSearch = (int) Math.min(TIMESTAMP_SEARCH_BYTES, inputLength); long searchStartPosition = inputLength - bytesToSearch; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java similarity index 97% rename from library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java rename to library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java index 2cd7398d7c..5e85a80a5d 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java @@ -21,6 +21,7 @@ import android.util.SparseArray; import android.util.SparseBooleanArray; import android.util.SparseIntArray; import androidx.annotation.IntDef; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.extractor.Extractor; @@ -95,6 +96,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; + // Stream types that aren't defined by the MPEG-2 TS specification. + public static final int TS_STREAM_TYPE_AIT = 0x101; + public static final int TS_PACKET_SIZE = 188; public static final int TS_SYNC_BYTE = 0x47; // First byte of each TS packet. @@ -186,7 +190,7 @@ public final class TsExtractor implements Extractor { // Extractor implementation. @Override - public boolean sniff(ExtractorInput input) throws IOException, InterruptedException { + public boolean sniff(ExtractorInput input) throws IOException { byte[] buffer = tsPacketBuffer.data; input.peekFully(buffer, 0, TS_PACKET_SIZE * SNIFF_TS_PACKET_COUNT); for (int startPosCandidate = 0; startPosCandidate < TS_PACKET_SIZE; startPosCandidate++) { @@ -249,7 +253,7 @@ public final class TsExtractor implements Extractor { @Override public @ReadResult int read(ExtractorInput input, PositionHolder seekPosition) - throws IOException, InterruptedException { + throws IOException { long inputLength = input.getLength(); if (tracksEnded) { boolean canReadDuration = inputLength != C.LENGTH_UNSET && mode != MODE_HLS; @@ -368,8 +372,7 @@ public final class TsExtractor implements Extractor { } } - private boolean fillBufferWithAtLeastOnePacket(ExtractorInput input) - throws IOException, InterruptedException { + private boolean fillBufferWithAtLeastOnePacket(ExtractorInput input) throws IOException { 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) { @@ -493,6 +496,7 @@ public final class TsExtractor implements Extractor { private static final int TS_PMT_DESC_REGISTRATION = 0x05; private static final int TS_PMT_DESC_ISO639_LANG = 0x0A; private static final int TS_PMT_DESC_AC3 = 0x6A; + private static final int TS_PMT_DESC_AIT = 0x6F; private static final int TS_PMT_DESC_EAC3 = 0x7A; private static final int TS_PMT_DESC_DTS = 0x7B; private static final int TS_PMT_DESC_DVB_EXT = 0x7F; @@ -577,7 +581,7 @@ public final class TsExtractor implements Extractor { pmtScratch.skipBits(4); // reserved int esInfoLength = pmtScratch.readBits(12); // ES_info_length. EsInfo esInfo = readEsInfo(sectionData, esInfoLength); - if (streamType == 0x06) { + if (streamType == 0x06 || streamType == 0x05) { streamType = esInfo.streamType; } remainingEntriesLength -= esInfoLength + 5; @@ -587,8 +591,11 @@ public final class TsExtractor implements Extractor { continue; } - TsPayloadReader reader = mode == MODE_HLS && streamType == TS_STREAM_TYPE_ID3 ? id3Reader - : payloadReaderFactory.createPayloadReader(streamType, esInfo); + @Nullable + TsPayloadReader reader = + mode == MODE_HLS && streamType == TS_STREAM_TYPE_ID3 + ? id3Reader + : payloadReaderFactory.createPayloadReader(streamType, esInfo); if (mode != MODE_HLS || elementaryPid < trackIdToPidScratch.get(trackId, MAX_PID_PLUS_ONE)) { trackIdToPidScratch.put(trackId, elementaryPid); @@ -602,7 +609,7 @@ public final class TsExtractor implements Extractor { int trackPid = trackIdToPidScratch.valueAt(i); trackIds.put(trackId, true); trackPids.put(trackPid, true); - TsPayloadReader reader = trackIdToReaderScratch.valueAt(i); + @Nullable TsPayloadReader reader = trackIdToReaderScratch.valueAt(i); if (reader != null) { if (reader != id3Reader) { reader.init(timestampAdjuster, output, @@ -684,6 +691,8 @@ public final class TsExtractor implements Extractor { dvbSubtitleInfos.add(new DvbSubtitleInfo(dvbLanguage, dvbSubtitlingType, initializationData)); } + } else if (descriptorTag == TS_PMT_DESC_AIT) { + streamType = TS_STREAM_TYPE_AIT; } // Skip unused bytes of current descriptor. data.skipBytes(positionOfNextDescriptor - data.getPosition()); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsPayloadReader.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/TsPayloadReader.java similarity index 95% rename from library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsPayloadReader.java rename to library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/TsPayloadReader.java index af27235257..03ed10ff0d 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsPayloadReader.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/TsPayloadReader.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.extractor.ts; import android.util.SparseArray; import androidx.annotation.IntDef; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.TrackOutput; @@ -53,11 +54,11 @@ public interface TsPayloadReader { * * @param streamType Stream type value as defined in the PMT entry or associated descriptors. * @param esInfo Information associated to the elementary stream provided in the PMT. - * @return A {@link TsPayloadReader} for the packet stream carried by the provided pid. + * @return A {@link TsPayloadReader} for the packet stream carried by the provided pid, or * {@code null} if the stream is not supported. */ + @Nullable TsPayloadReader createPayloadReader(int streamType, EsInfo esInfo); - } /** @@ -66,18 +67,21 @@ public interface TsPayloadReader { final class EsInfo { public final int streamType; - public final String language; + @Nullable public final String language; public final List dvbSubtitleInfos; public final byte[] descriptorBytes; /** - * @param streamType The type of the stream as defined by the - * {@link TsExtractor}{@code .TS_STREAM_TYPE_*}. + * @param streamType The type of the stream as defined by the {@link TsExtractor}{@code + * .TS_STREAM_TYPE_*}. * @param language The language of the stream, as defined by ISO/IEC 13818-1, section 2.6.18. * @param dvbSubtitleInfos Information about DVB subtitles associated to the stream. * @param descriptorBytes The descriptor bytes associated to the stream. */ - public EsInfo(int streamType, String language, List dvbSubtitleInfos, + public EsInfo( + int streamType, + @Nullable String language, + @Nullable List dvbSubtitleInfos, byte[] descriptorBytes) { this.streamType = streamType; this.language = language; @@ -134,6 +138,7 @@ public interface TsPayloadReader { this.firstTrackId = firstTrackId; this.trackIdIncrement = trackIdIncrement; trackId = ID_UNSET; + formatId = ""; } /** diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsUtil.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/TsUtil.java similarity index 100% rename from library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsUtil.java rename to library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/TsUtil.java diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/UserDataReader.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/UserDataReader.java similarity index 82% rename from library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/UserDataReader.java rename to library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/UserDataReader.java index 724eba1d9a..a9d1e1ef1d 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/UserDataReader.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/UserDataReader.java @@ -15,11 +15,12 @@ */ package com.google.android.exoplayer2.extractor.ts; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.extractor.CeaUtil; 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; @@ -44,23 +45,20 @@ import java.util.List; idGenerator.generateNewId(); TrackOutput output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_TEXT); Format channelFormat = closedCaptionFormats.get(i); - String channelMimeType = channelFormat.sampleMimeType; + @Nullable 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)); + new Format.Builder() + .setId(idGenerator.getFormatId()) + .setSampleMimeType(channelMimeType) + .setSelectionFlags(channelFormat.selectionFlags) + .setLanguage(channelFormat.language) + .setAccessibilityChannel(channelFormat.accessibilityChannel) + .setInitializationData(channelFormat.initializationData) + .build()); outputs[i] = output; } } diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/package-info.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/package-info.java new file mode 100644 index 0000000000..78f4551db4 --- /dev/null +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2019 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. + */ +@NonNullApi +package com.google.android.exoplayer2.extractor.ts; + +import com.google.android.exoplayer2.util.NonNullApi; diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/wav/WavExtractor.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/wav/WavExtractor.java new file mode 100644 index 0000000000..1d7b6b9c6e --- /dev/null +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/wav/WavExtractor.java @@ -0,0 +1,550 @@ +/* + * 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.extractor.wav; + +import android.util.Pair; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.ParserException; +import com.google.android.exoplayer2.audio.WavUtil; +import com.google.android.exoplayer2.extractor.Extractor; +import com.google.android.exoplayer2.extractor.ExtractorInput; +import com.google.android.exoplayer2.extractor.ExtractorOutput; +import com.google.android.exoplayer2.extractor.ExtractorsFactory; +import com.google.android.exoplayer2.extractor.PositionHolder; +import com.google.android.exoplayer2.extractor.TrackOutput; +import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.MimeTypes; +import com.google.android.exoplayer2.util.ParsableByteArray; +import com.google.android.exoplayer2.util.Util; +import java.io.IOException; +import org.checkerframework.checker.nullness.qual.EnsuresNonNull; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; + +/** + * Extracts data from WAV byte streams. + */ +public final class WavExtractor implements Extractor { + + /** + * When outputting PCM data to a {@link TrackOutput}, we can choose how many frames are grouped + * into each sample, and hence each sample's duration. This is the target number of samples to + * output for each second of media, meaning that each sample will have a duration of ~100ms. + */ + private static final int TARGET_SAMPLES_PER_SECOND = 10; + + /** Factory for {@link WavExtractor} instances. */ + public static final ExtractorsFactory FACTORY = () -> new Extractor[] {new WavExtractor()}; + + private @MonotonicNonNull ExtractorOutput extractorOutput; + private @MonotonicNonNull TrackOutput trackOutput; + private @MonotonicNonNull OutputWriter outputWriter; + private int dataStartPosition; + private long dataEndPosition; + + public WavExtractor() { + dataStartPosition = C.POSITION_UNSET; + dataEndPosition = C.POSITION_UNSET; + } + + @Override + public boolean sniff(ExtractorInput input) throws IOException { + return WavHeaderReader.peek(input) != null; + } + + @Override + public void init(ExtractorOutput output) { + extractorOutput = output; + trackOutput = output.track(0, C.TRACK_TYPE_AUDIO); + output.endTracks(); + } + + @Override + public void seek(long position, long timeUs) { + if (outputWriter != null) { + outputWriter.reset(timeUs); + } + } + + @Override + public void release() { + // Do nothing + } + + @Override + public int read(ExtractorInput input, PositionHolder seekPosition) throws IOException { + assertInitialized(); + if (outputWriter == null) { + WavHeader header = WavHeaderReader.peek(input); + if (header == null) { + // Should only happen if the media wasn't sniffed. + throw new ParserException("Unsupported or unrecognized wav header."); + } + + if (header.formatType == WavUtil.TYPE_IMA_ADPCM) { + outputWriter = new ImaAdPcmOutputWriter(extractorOutput, trackOutput, header); + } else if (header.formatType == WavUtil.TYPE_ALAW) { + outputWriter = + new PassthroughOutputWriter( + extractorOutput, + trackOutput, + header, + MimeTypes.AUDIO_ALAW, + /* pcmEncoding= */ Format.NO_VALUE); + } else if (header.formatType == WavUtil.TYPE_MLAW) { + outputWriter = + new PassthroughOutputWriter( + extractorOutput, + trackOutput, + header, + MimeTypes.AUDIO_MLAW, + /* pcmEncoding= */ Format.NO_VALUE); + } else { + @C.PcmEncoding + int pcmEncoding = WavUtil.getPcmEncodingForType(header.formatType, header.bitsPerSample); + if (pcmEncoding == C.ENCODING_INVALID) { + throw new ParserException("Unsupported WAV format type: " + header.formatType); + } + outputWriter = + new PassthroughOutputWriter( + extractorOutput, trackOutput, header, MimeTypes.AUDIO_RAW, pcmEncoding); + } + } + + if (dataStartPosition == C.POSITION_UNSET) { + Pair dataBounds = WavHeaderReader.skipToData(input); + dataStartPosition = dataBounds.first.intValue(); + dataEndPosition = dataBounds.second; + outputWriter.init(dataStartPosition, dataEndPosition); + } else if (input.getPosition() == 0) { + input.skipFully(dataStartPosition); + } + + Assertions.checkState(dataEndPosition != C.POSITION_UNSET); + long bytesLeft = dataEndPosition - input.getPosition(); + return outputWriter.sampleData(input, bytesLeft) ? RESULT_END_OF_INPUT : RESULT_CONTINUE; + } + + @EnsuresNonNull({"extractorOutput", "trackOutput"}) + private void assertInitialized() { + Assertions.checkStateNotNull(trackOutput); + Util.castNonNull(extractorOutput); + } + + /** Writes to the extractor's output. */ + private interface OutputWriter { + + /** + * Resets the writer. + * + * @param timeUs The new start position in microseconds. + */ + void reset(long timeUs); + + /** + * Initializes the writer. + * + *

      Must be called once, before any calls to {@link #sampleData(ExtractorInput, long)}. + * + * @param dataStartPosition The byte position (inclusive) in the stream at which data starts. + * @param dataEndPosition The end position (exclusive) in the stream at which data ends. + * @throws ParserException If an error occurs initializing the writer. + */ + void init(int dataStartPosition, long dataEndPosition) throws ParserException; + + /** + * Consumes sample data from {@code input}, writing corresponding samples to the extractor's + * output. + * + *

      Must not be called until after {@link #init(int, long)} has been called. + * + * @param input The input from which to read. + * @param bytesLeft The number of sample data bytes left to be read from the input. + * @return Whether the end of the sample data has been reached. + * @throws IOException If an error occurs reading from the input. + */ + boolean sampleData(ExtractorInput input, long bytesLeft) throws IOException; + } + + private static final class PassthroughOutputWriter implements OutputWriter { + + private final ExtractorOutput extractorOutput; + private final TrackOutput trackOutput; + private final WavHeader header; + private final Format format; + /** The target size of each output sample, in bytes. */ + private final int targetSampleSizeBytes; + + /** The time at which the writer was last {@link #reset}. */ + private long startTimeUs; + /** + * The number of bytes that have been written to {@link #trackOutput} but have yet to be + * included as part of a sample (i.e. the corresponding call to {@link + * TrackOutput#sampleMetadata} has yet to be made). + */ + private int pendingOutputBytes; + /** + * The total number of frames in samples that have been written to the trackOutput since the + * last call to {@link #reset}. + */ + private long outputFrameCount; + + public PassthroughOutputWriter( + ExtractorOutput extractorOutput, + TrackOutput trackOutput, + WavHeader header, + String mimeType, + @C.PcmEncoding int pcmEncoding) + throws ParserException { + this.extractorOutput = extractorOutput; + this.trackOutput = trackOutput; + this.header = header; + + int bytesPerFrame = header.numChannels * header.bitsPerSample / 8; + // Validate the header. Blocks are expected to correspond to single frames. + if (header.blockSize != bytesPerFrame) { + throw new ParserException( + "Expected block size: " + bytesPerFrame + "; got: " + header.blockSize); + } + + int constantBitrate = header.frameRateHz * bytesPerFrame * 8; + targetSampleSizeBytes = + Math.max(bytesPerFrame, header.frameRateHz * bytesPerFrame / TARGET_SAMPLES_PER_SECOND); + format = + new Format.Builder() + .setSampleMimeType(mimeType) + .setAverageBitrate(constantBitrate) + .setPeakBitrate(constantBitrate) + .setMaxInputSize(targetSampleSizeBytes) + .setChannelCount(header.numChannels) + .setSampleRate(header.frameRateHz) + .setPcmEncoding(pcmEncoding) + .build(); + } + + @Override + public void reset(long timeUs) { + startTimeUs = timeUs; + pendingOutputBytes = 0; + outputFrameCount = 0; + } + + @Override + public void init(int dataStartPosition, long dataEndPosition) { + extractorOutput.seekMap( + new WavSeekMap(header, /* framesPerBlock= */ 1, dataStartPosition, dataEndPosition)); + trackOutput.format(format); + } + + @Override + public boolean sampleData(ExtractorInput input, long bytesLeft) throws IOException { + // Write sample data until we've reached the target sample size, or the end of the data. + while (bytesLeft > 0 && pendingOutputBytes < targetSampleSizeBytes) { + int bytesToRead = (int) Math.min(targetSampleSizeBytes - pendingOutputBytes, bytesLeft); + int bytesAppended = trackOutput.sampleData(input, bytesToRead, true); + if (bytesAppended == RESULT_END_OF_INPUT) { + bytesLeft = 0; + } else { + pendingOutputBytes += bytesAppended; + bytesLeft -= bytesAppended; + } + } + + // Write the corresponding sample metadata. Samples must be a whole number of frames. It's + // possible that the number of pending output bytes is not a whole number of frames if the + // stream ended unexpectedly. + int bytesPerFrame = header.blockSize; + int pendingFrames = pendingOutputBytes / bytesPerFrame; + if (pendingFrames > 0) { + long timeUs = + startTimeUs + + Util.scaleLargeTimestamp( + outputFrameCount, C.MICROS_PER_SECOND, header.frameRateHz); + int size = pendingFrames * bytesPerFrame; + int offset = pendingOutputBytes - size; + trackOutput.sampleMetadata( + timeUs, C.BUFFER_FLAG_KEY_FRAME, size, offset, /* encryptionData= */ null); + outputFrameCount += pendingFrames; + pendingOutputBytes = offset; + } + + return bytesLeft <= 0; + } + } + + private static final class ImaAdPcmOutputWriter implements OutputWriter { + + private static final int[] INDEX_TABLE = { + -1, -1, -1, -1, 2, 4, 6, 8, -1, -1, -1, -1, 2, 4, 6, 8 + }; + + private static final int[] STEP_TABLE = { + 7, 8, 9, 10, 11, 12, 13, 14, 16, 17, 19, 21, 23, 25, 28, 31, 34, 37, 41, 45, 50, 55, 60, 66, + 73, 80, 88, 97, 107, 118, 130, 143, 157, 173, 190, 209, 230, 253, 279, 307, 337, 371, 408, + 449, 494, 544, 598, 658, 724, 796, 876, 963, 1060, 1166, 1282, 1411, 1552, 1707, 1878, 2066, + 2272, 2499, 2749, 3024, 3327, 3660, 4026, 4428, 4871, 5358, 5894, 6484, 7132, 7845, 8630, + 9493, 10442, 11487, 12635, 13899, 15289, 16818, 18500, 20350, 22385, 24623, 27086, 29794, + 32767 + }; + + private final ExtractorOutput extractorOutput; + private final TrackOutput trackOutput; + private final WavHeader header; + + /** Number of frames per block of the input (yet to be decoded) data. */ + private final int framesPerBlock; + /** Target for the input (yet to be decoded) data. */ + private final byte[] inputData; + /** Target for decoded (yet to be output) data. */ + private final ParsableByteArray decodedData; + /** The target size of each output sample, in frames. */ + private final int targetSampleSizeFrames; + /** The output format. */ + private final Format format; + + /** The number of pending bytes in {@link #inputData}. */ + private int pendingInputBytes; + /** The time at which the writer was last {@link #reset}. */ + private long startTimeUs; + /** + * The number of bytes that have been written to {@link #trackOutput} but have yet to be + * included as part of a sample (i.e. the corresponding call to {@link + * TrackOutput#sampleMetadata} has yet to be made). + */ + private int pendingOutputBytes; + /** + * The total number of frames in samples that have been written to the trackOutput since the + * last call to {@link #reset}. + */ + private long outputFrameCount; + + public ImaAdPcmOutputWriter( + ExtractorOutput extractorOutput, TrackOutput trackOutput, WavHeader header) + throws ParserException { + this.extractorOutput = extractorOutput; + this.trackOutput = trackOutput; + this.header = header; + targetSampleSizeFrames = Math.max(1, header.frameRateHz / TARGET_SAMPLES_PER_SECOND); + + ParsableByteArray scratch = new ParsableByteArray(header.extraData); + scratch.readLittleEndianUnsignedShort(); + framesPerBlock = scratch.readLittleEndianUnsignedShort(); + + int numChannels = header.numChannels; + // Validate the header. This calculation is defined in "Microsoft Multimedia Standards Update + // - New Multimedia Types and Data Techniques" (1994). See the "IMA ADPCM Wave Type" and "DVI + // ADPCM Wave Type" sections, and the calculation of wSamplesPerBlock in the latter. + int expectedFramesPerBlock = + (((header.blockSize - (4 * numChannels)) * 8) / (header.bitsPerSample * numChannels)) + 1; + if (framesPerBlock != expectedFramesPerBlock) { + throw new ParserException( + "Expected frames per block: " + expectedFramesPerBlock + "; got: " + framesPerBlock); + } + + // Calculate the number of blocks we'll need to decode to obtain an output sample of the + // target sample size, and allocate suitably sized buffers for input and decoded data. + int maxBlocksToDecode = Util.ceilDivide(targetSampleSizeFrames, framesPerBlock); + inputData = new byte[maxBlocksToDecode * header.blockSize]; + decodedData = + new ParsableByteArray( + maxBlocksToDecode * numOutputFramesToBytes(framesPerBlock, numChannels)); + + // Create the format. We calculate the bitrate of the data before decoding, since this is the + // bitrate of the stream itself. + int constantBitrate = header.frameRateHz * header.blockSize * 8 / framesPerBlock; + format = + new Format.Builder() + .setSampleMimeType(MimeTypes.AUDIO_RAW) + .setAverageBitrate(constantBitrate) + .setPeakBitrate(constantBitrate) + .setMaxInputSize(numOutputFramesToBytes(targetSampleSizeFrames, numChannels)) + .setChannelCount(header.numChannels) + .setSampleRate(header.frameRateHz) + .setPcmEncoding(C.ENCODING_PCM_16BIT) + .build(); + } + + @Override + public void reset(long timeUs) { + pendingInputBytes = 0; + startTimeUs = timeUs; + pendingOutputBytes = 0; + outputFrameCount = 0; + } + + @Override + public void init(int dataStartPosition, long dataEndPosition) { + extractorOutput.seekMap( + new WavSeekMap(header, framesPerBlock, dataStartPosition, dataEndPosition)); + trackOutput.format(format); + } + + @Override + public boolean sampleData(ExtractorInput input, long bytesLeft) throws IOException { + // Calculate the number of additional frames that we need on the output side to complete a + // sample of the target size. + int targetFramesRemaining = + targetSampleSizeFrames - numOutputBytesToFrames(pendingOutputBytes); + // Calculate the whole number of blocks that we need to decode to obtain this many frames. + int blocksToDecode = Util.ceilDivide(targetFramesRemaining, framesPerBlock); + int targetReadBytes = blocksToDecode * header.blockSize; + + // Read input data until we've reached the target number of blocks, or the end of the data. + boolean endOfSampleData = bytesLeft == 0; + while (!endOfSampleData && pendingInputBytes < targetReadBytes) { + int bytesToRead = (int) Math.min(targetReadBytes - pendingInputBytes, bytesLeft); + int bytesAppended = input.read(inputData, pendingInputBytes, bytesToRead); + if (bytesAppended == RESULT_END_OF_INPUT) { + endOfSampleData = true; + } else { + pendingInputBytes += bytesAppended; + } + } + + int pendingBlockCount = pendingInputBytes / header.blockSize; + if (pendingBlockCount > 0) { + // We have at least one whole block to decode. + decode(inputData, pendingBlockCount, decodedData); + pendingInputBytes -= pendingBlockCount * header.blockSize; + + // Write all of the decoded data to the track output. + int decodedDataSize = decodedData.limit(); + trackOutput.sampleData(decodedData, decodedDataSize); + pendingOutputBytes += decodedDataSize; + + // Output the next sample at the target size. + int pendingOutputFrames = numOutputBytesToFrames(pendingOutputBytes); + if (pendingOutputFrames >= targetSampleSizeFrames) { + writeSampleMetadata(targetSampleSizeFrames); + } + } + + // If we've reached the end of the data, we might need to output a final partial sample. + if (endOfSampleData) { + int pendingOutputFrames = numOutputBytesToFrames(pendingOutputBytes); + if (pendingOutputFrames > 0) { + writeSampleMetadata(pendingOutputFrames); + } + } + + return endOfSampleData; + } + + private void writeSampleMetadata(int sampleFrames) { + long timeUs = + startTimeUs + + Util.scaleLargeTimestamp(outputFrameCount, C.MICROS_PER_SECOND, header.frameRateHz); + int size = numOutputFramesToBytes(sampleFrames); + int offset = pendingOutputBytes - size; + trackOutput.sampleMetadata( + timeUs, C.BUFFER_FLAG_KEY_FRAME, size, offset, /* encryptionData= */ null); + outputFrameCount += sampleFrames; + pendingOutputBytes -= size; + } + + /** + * Decodes IMA ADPCM data to 16 bit PCM. + * + * @param input The input data to decode. + * @param blockCount The number of blocks to decode. + * @param output The output into which the decoded data will be written. + */ + private void decode(byte[] input, int blockCount, ParsableByteArray output) { + for (int blockIndex = 0; blockIndex < blockCount; blockIndex++) { + for (int channelIndex = 0; channelIndex < header.numChannels; channelIndex++) { + decodeBlockForChannel(input, blockIndex, channelIndex, output.data); + } + } + int decodedDataSize = numOutputFramesToBytes(framesPerBlock * blockCount); + output.reset(decodedDataSize); + } + + private void decodeBlockForChannel( + byte[] input, int blockIndex, int channelIndex, byte[] output) { + int blockSize = header.blockSize; + int numChannels = header.numChannels; + + // The input data consists for a four byte header [Ci] for each of the N channels, followed + // by interleaved data segments [Ci-DATAj], each of which are four bytes long. + // + // [C1][C2]...[CN] [C1-Data0][C2-Data0]...[CN-Data0] [C1-Data1][C2-Data1]...[CN-Data1] etc + // + // Compute the start indices for the [Ci] and [Ci-Data0] for the current channel, as well as + // the number of data bytes for the channel in the block. + int blockStartIndex = blockIndex * blockSize; + int headerStartIndex = blockStartIndex + channelIndex * 4; + int dataStartIndex = headerStartIndex + numChannels * 4; + int dataSizeBytes = blockSize / numChannels - 4; + + // Decode initialization. Casting to a short is necessary for the most significant bit to be + // treated as -2^15 rather than 2^15. + int predictedSample = + (short) (((input[headerStartIndex + 1] & 0xFF) << 8) | (input[headerStartIndex] & 0xFF)); + int stepIndex = Math.min(input[headerStartIndex + 2] & 0xFF, 88); + int step = STEP_TABLE[stepIndex]; + + // Output the initial 16 bit PCM sample from the header. + int outputIndex = (blockIndex * framesPerBlock * numChannels + channelIndex) * 2; + output[outputIndex] = (byte) (predictedSample & 0xFF); + output[outputIndex + 1] = (byte) (predictedSample >> 8); + + // We examine each data byte twice during decode. + for (int i = 0; i < dataSizeBytes * 2; i++) { + int dataSegmentIndex = i / 8; + int dataSegmentOffset = (i / 2) % 4; + int dataIndex = dataStartIndex + (dataSegmentIndex * numChannels * 4) + dataSegmentOffset; + + int originalSample = input[dataIndex] & 0xFF; + if (i % 2 == 0) { + originalSample &= 0x0F; // Bottom four bits. + } else { + originalSample >>= 4; // Top four bits. + } + + int delta = originalSample & 0x07; + int difference = ((2 * delta + 1) * step) >> 3; + + if ((originalSample & 0x08) != 0) { + difference = -difference; + } + + predictedSample += difference; + predictedSample = Util.constrainValue(predictedSample, /* min= */ -32768, /* max= */ 32767); + + // Output the next 16 bit PCM sample to the correct position in the output. + outputIndex += 2 * numChannels; + output[outputIndex] = (byte) (predictedSample & 0xFF); + output[outputIndex + 1] = (byte) (predictedSample >> 8); + + stepIndex += INDEX_TABLE[originalSample]; + stepIndex = Util.constrainValue(stepIndex, /* min= */ 0, /* max= */ STEP_TABLE.length - 1); + step = STEP_TABLE[stepIndex]; + } + } + + private int numOutputBytesToFrames(int bytes) { + return bytes / (2 * header.numChannels); + } + + private int numOutputFramesToBytes(int frames) { + return numOutputFramesToBytes(frames, header.numChannels); + } + + private static int numOutputFramesToBytes(int frames, int numChannels) { + return frames * 2 * numChannels; + } + } +} diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/wav/WavHeader.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/wav/WavHeader.java new file mode 100644 index 0000000000..ca34e32cc0 --- /dev/null +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/wav/WavHeader.java @@ -0,0 +1,55 @@ +/* + * 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.extractor.wav; + +/** Header for a WAV file. */ +/* package */ final class WavHeader { + + /** + * The format type. Standard format types are the "WAVE form Registration Number" constants + * defined in RFC 2361 Appendix A. + */ + public final int formatType; + /** The number of channels. */ + public final int numChannels; + /** The sample rate in Hertz. */ + public final int frameRateHz; + /** The average bytes per second for the sample data. */ + public final int averageBytesPerSecond; + /** The block size in bytes. */ + public final int blockSize; + /** Bits per sample for a single channel. */ + public final int bitsPerSample; + /** Extra data appended to the format chunk of the header. */ + public final byte[] extraData; + + public WavHeader( + int formatType, + int numChannels, + int frameRateHz, + int averageBytesPerSecond, + int blockSize, + int bitsPerSample, + byte[] extraData) { + this.formatType = formatType; + this.numChannels = numChannels; + this.frameRateHz = frameRateHz; + this.averageBytesPerSecond = averageBytesPerSecond; + this.blockSize = blockSize; + this.bitsPerSample = bitsPerSample; + this.extraData = extraData; + } +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavHeaderReader.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/wav/WavHeaderReader.java similarity index 72% rename from library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavHeaderReader.java rename to library/extractor/src/main/java/com/google/android/exoplayer2/extractor/wav/WavHeaderReader.java index 97ce0c6a1e..bcc229f3e9 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavHeaderReader.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/wav/WavHeaderReader.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.extractor.wav; +import android.util.Pair; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ParserException; @@ -23,6 +24,7 @@ import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.ParsableByteArray; +import com.google.android.exoplayer2.util.Util; import java.io.IOException; /** Reads a {@code WavHeader} from an input stream; supports resuming from input failures. */ @@ -36,12 +38,11 @@ import java.io.IOException; * @param input Input stream to peek the WAV header from. * @throws ParserException If the input file is an incorrect RIFF WAV. * @throws IOException If peeking from the input fails. - * @throws InterruptedException If interrupted while peeking from input. * @return A new {@code WavHeader} peeked from {@code input}, or null if the input is not a * supported WAV format. */ @Nullable - public static WavHeader peek(ExtractorInput input) throws IOException, InterruptedException { + public static WavHeader peek(ExtractorInput input) throws IOException { Assertions.checkNotNull(input); // Allocate a scratch buffer large enough to store the format chunk. @@ -71,51 +72,44 @@ import java.io.IOException; Assertions.checkState(chunkHeader.size >= 16); input.peekFully(scratch.data, 0, 16); scratch.setPosition(0); - int type = scratch.readLittleEndianUnsignedShort(); + int audioFormatType = scratch.readLittleEndianUnsignedShort(); int numChannels = scratch.readLittleEndianUnsignedShort(); - int sampleRateHz = scratch.readLittleEndianUnsignedIntToInt(); + int frameRateHz = scratch.readLittleEndianUnsignedIntToInt(); int averageBytesPerSecond = scratch.readLittleEndianUnsignedIntToInt(); - int blockAlignment = scratch.readLittleEndianUnsignedShort(); + int blockSize = scratch.readLittleEndianUnsignedShort(); int bitsPerSample = scratch.readLittleEndianUnsignedShort(); - int expectedBlockAlignment = numChannels * bitsPerSample / 8; - if (blockAlignment != expectedBlockAlignment) { - throw new ParserException("Expected block alignment: " + expectedBlockAlignment + "; got: " - + blockAlignment); + int bytesLeft = (int) chunkHeader.size - 16; + byte[] extraData; + if (bytesLeft > 0) { + extraData = new byte[bytesLeft]; + input.peekFully(extraData, 0, bytesLeft); + } else { + extraData = Util.EMPTY_BYTE_ARRAY; } - @C.PcmEncoding int encoding = WavUtil.getEncodingForType(type, bitsPerSample); - if (encoding == C.ENCODING_INVALID) { - Log.e(TAG, "Unsupported WAV format: " + bitsPerSample + " bit/sample, type " + type); - return null; - } - - // If present, skip extensionSize, validBitsPerSample, channelMask, subFormatGuid, ... - input.advancePeekPosition((int) chunkHeader.size - 16); - return new WavHeader( - numChannels, sampleRateHz, averageBytesPerSecond, blockAlignment, bitsPerSample, encoding); + audioFormatType, + numChannels, + frameRateHz, + averageBytesPerSecond, + blockSize, + bitsPerSample, + extraData); } /** - * Skips to the data in the given WAV input stream. After calling, the input stream's position - * will point to the start of sample data in the WAV, and the data bounds of the provided {@link - * WavHeader} will have been set. + * Skips to the data in the given WAV input stream, and returns its bounds. After calling, the + * input stream's position will point to the start of sample data in the WAV. If an exception is + * thrown, the input position will be left pointing to a chunk header. * - *

      If an exception is thrown, the input position will be left pointing to a chunk header and - * the bounds of the provided {@link WavHeader} will not have been set. - * - * @param input Input stream to skip to the data chunk in. Its peek position must be pointing to a - * valid chunk header. - * @param wavHeader WAV header to populate with data bounds. + * @param input The input stream, whose read position must be pointing to a valid chunk header. + * @return The byte positions at which the data starts (inclusive) and ends (exclusive). * @throws ParserException If an error occurs parsing chunks. * @throws IOException If reading from the input fails. - * @throws InterruptedException If interrupted while reading from input. */ - public static void skipToData(ExtractorInput input, WavHeader wavHeader) - throws IOException, InterruptedException { + public static Pair skipToData(ExtractorInput input) throws IOException { Assertions.checkNotNull(input); - Assertions.checkNotNull(wavHeader); // Make sure the peek position is set to the read position before we peek the first header. input.resetPeekPosition(); @@ -141,14 +135,14 @@ import java.io.IOException; // Skip past the "data" header. input.skipFully(ChunkHeader.SIZE_IN_BYTES); - int dataStartPosition = (int) input.getPosition(); + long dataStartPosition = input.getPosition(); long dataEndPosition = dataStartPosition + chunkHeader.size; long inputLength = input.getLength(); if (inputLength != C.LENGTH_UNSET && dataEndPosition > inputLength) { Log.w(TAG, "Data exceeds input length: " + dataEndPosition + ", " + inputLength); dataEndPosition = inputLength; } - wavHeader.setDataBounds(dataStartPosition, dataEndPosition); + return Pair.create(dataStartPosition, dataEndPosition); } private WavHeaderReader() { @@ -177,12 +171,11 @@ import java.io.IOException; * @param input Input stream to peek the chunk header from. * @param scratch Buffer for temporary use. * @throws IOException If peeking from the input fails. - * @throws InterruptedException If interrupted while peeking from input. * @return A new {@code ChunkHeader} peeked from {@code input}. */ public static ChunkHeader peek(ExtractorInput input, ParsableByteArray scratch) - throws IOException, InterruptedException { - input.peekFully(scratch.data, 0, SIZE_IN_BYTES); + throws IOException { + input.peekFully(scratch.data, /* offset= */ 0, /* length= */ SIZE_IN_BYTES); scratch.setPosition(0); int id = scratch.readInt(); diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/wav/WavSeekMap.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/wav/WavSeekMap.java new file mode 100644 index 0000000000..2a92c38431 --- /dev/null +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/wav/WavSeekMap.java @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2019 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.wav; + +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.extractor.SeekMap; +import com.google.android.exoplayer2.extractor.SeekPoint; +import com.google.android.exoplayer2.util.Util; + +/* package */ final class WavSeekMap implements SeekMap { + + private final WavHeader wavHeader; + private final int framesPerBlock; + private final long firstBlockPosition; + private final long blockCount; + private final long durationUs; + + public WavSeekMap( + WavHeader wavHeader, int framesPerBlock, long dataStartPosition, long dataEndPosition) { + this.wavHeader = wavHeader; + this.framesPerBlock = framesPerBlock; + this.firstBlockPosition = dataStartPosition; + this.blockCount = (dataEndPosition - dataStartPosition) / wavHeader.blockSize; + durationUs = blockIndexToTimeUs(blockCount); + } + + @Override + public boolean isSeekable() { + return true; + } + + @Override + public long getDurationUs() { + return durationUs; + } + + @Override + public SeekPoints getSeekPoints(long timeUs) { + // Calculate the containing block index, constraining to valid indices. + long blockIndex = (timeUs * wavHeader.frameRateHz) / (C.MICROS_PER_SECOND * framesPerBlock); + blockIndex = Util.constrainValue(blockIndex, 0, blockCount - 1); + + long seekPosition = firstBlockPosition + (blockIndex * wavHeader.blockSize); + long seekTimeUs = blockIndexToTimeUs(blockIndex); + SeekPoint seekPoint = new SeekPoint(seekTimeUs, seekPosition); + if (seekTimeUs >= timeUs || blockIndex == blockCount - 1) { + return new SeekPoints(seekPoint); + } else { + long secondBlockIndex = blockIndex + 1; + long secondSeekPosition = firstBlockPosition + (secondBlockIndex * wavHeader.blockSize); + long secondSeekTimeUs = blockIndexToTimeUs(secondBlockIndex); + SeekPoint secondSeekPoint = new SeekPoint(secondSeekTimeUs, secondSeekPosition); + return new SeekPoints(seekPoint, secondSeekPoint); + } + } + + private long blockIndexToTimeUs(long blockIndex) { + return Util.scaleLargeTimestamp( + blockIndex * framesPerBlock, C.MICROS_PER_SECOND, wavHeader.frameRateHz); + } +} diff --git a/library/core/src/test/java/com/google/android/exoplayer2/extractor/wav/WavExtractorTest.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/wav/package-info.java similarity index 56% rename from library/core/src/test/java/com/google/android/exoplayer2/extractor/wav/WavExtractorTest.java rename to library/extractor/src/main/java/com/google/android/exoplayer2/extractor/wav/package-info.java index c617b672e2..4769ea693a 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/extractor/wav/WavExtractorTest.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/wav/package-info.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2016 The Android Open Source Project + * Copyright (C) 2019 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,19 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +@NonNullApi package com.google.android.exoplayer2.extractor.wav; -import androidx.test.ext.junit.runners.AndroidJUnit4; -import com.google.android.exoplayer2.testutil.ExtractorAsserts; -import org.junit.Test; -import org.junit.runner.RunWith; - -/** Unit test for {@link WavExtractor}. */ -@RunWith(AndroidJUnit4.class) -public final class WavExtractorTest { - - @Test - public void testSample() throws Exception { - ExtractorAsserts.assertBehavior(WavExtractor::new, "wav/sample.wav"); - } -} +import com.google.android.exoplayer2.util.NonNullApi; diff --git a/library/extractor/src/main/proguard-rules.txt b/library/extractor/src/main/proguard-rules.txt new file mode 120000 index 0000000000..499fb08b36 --- /dev/null +++ b/library/extractor/src/main/proguard-rules.txt @@ -0,0 +1 @@ +../../proguard-rules.txt \ No newline at end of file diff --git a/library/extractor/src/test/AndroidManifest.xml b/library/extractor/src/test/AndroidManifest.xml new file mode 100644 index 0000000000..79e0e48973 --- /dev/null +++ b/library/extractor/src/test/AndroidManifest.xml @@ -0,0 +1,19 @@ + + + + + + diff --git a/library/core/src/test/java/com/google/android/exoplayer2/extractor/ConstantBitrateSeekMapTest.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/ConstantBitrateSeekMapTest.java similarity index 86% rename from library/core/src/test/java/com/google/android/exoplayer2/extractor/ConstantBitrateSeekMapTest.java rename to library/extractor/src/test/java/com/google/android/exoplayer2/extractor/ConstantBitrateSeekMapTest.java index bc3ccf499f..4d70fe3b69 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/extractor/ConstantBitrateSeekMapTest.java +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/ConstantBitrateSeekMapTest.java @@ -30,7 +30,7 @@ public final class ConstantBitrateSeekMapTest { private ConstantBitrateSeekMap constantBitrateSeekMap; @Test - public void testIsSeekable_forKnownInputLength_returnSeekable() { + public void isSeekable_forKnownInputLength_returnSeekable() { constantBitrateSeekMap = new ConstantBitrateSeekMap( /* inputLength= */ 1000, @@ -41,7 +41,7 @@ public final class ConstantBitrateSeekMapTest { } @Test - public void testIsSeekable_forUnknownInputLength_returnUnseekable() { + public void isSeekable_forUnknownInputLength_returnUnseekable() { constantBitrateSeekMap = new ConstantBitrateSeekMap( /* inputLength= */ C.LENGTH_UNSET, @@ -52,7 +52,7 @@ public final class ConstantBitrateSeekMapTest { } @Test - public void testGetSeekPoints_forUnseekableInput_returnSeekPoint0() { + public void getSeekPoints_forUnseekableInput_returnSeekPoint0() { int firstBytePosition = 100; constantBitrateSeekMap = new ConstantBitrateSeekMap( @@ -67,7 +67,7 @@ public final class ConstantBitrateSeekMapTest { } @Test - public void testGetDurationUs_forKnownInputLength_returnCorrectDuration() { + public void getDurationUs_forKnownInputLength_returnCorrectDuration() { constantBitrateSeekMap = new ConstantBitrateSeekMap( /* inputLength= */ 2_300, @@ -81,7 +81,7 @@ public final class ConstantBitrateSeekMapTest { } @Test - public void testGetDurationUs_forUnnnownInputLength_returnUnknownDuration() { + public void getDurationUs_forUnnnownInputLength_returnUnknownDuration() { constantBitrateSeekMap = new ConstantBitrateSeekMap( /* inputLength= */ C.LENGTH_UNSET, @@ -92,7 +92,7 @@ public final class ConstantBitrateSeekMapTest { } @Test - public void testGetSeekPoints_forSeekableInput_forSyncPosition0_return1SeekPoint() { + public void getSeekPoints_forSeekableInput_forSyncPosition0_return1SeekPoint() { int firstBytePosition = 100; constantBitrateSeekMap = new ConstantBitrateSeekMap( @@ -107,7 +107,7 @@ public final class ConstantBitrateSeekMapTest { } @Test - public void testGetSeekPoints_forSeekableInput_forSeekPointAtSyncPosition_return1SeekPoint() { + public void getSeekPoints_forSeekableInput_forSeekPointAtSyncPosition_return1SeekPoint() { constantBitrateSeekMap = new ConstantBitrateSeekMap( /* inputLength= */ 2_300, @@ -123,7 +123,7 @@ public final class ConstantBitrateSeekMapTest { } @Test - public void testGetSeekPoints_forSeekableInput_forNonSyncSeekPosition_return2SeekPoints() { + public void getSeekPoints_forSeekableInput_forNonSyncSeekPosition_return2SeekPoints() { constantBitrateSeekMap = new ConstantBitrateSeekMap( /* inputLength= */ 2_300, @@ -140,7 +140,7 @@ public final class ConstantBitrateSeekMapTest { } @Test - public void testGetSeekPoints_forSeekableInput_forSeekPointWithinLastFrame_return1SeekPoint() { + public void getSeekPoints_forSeekableInput_forSeekPointWithinLastFrame_return1SeekPoint() { constantBitrateSeekMap = new ConstantBitrateSeekMap( /* inputLength= */ 2_300, @@ -154,7 +154,7 @@ public final class ConstantBitrateSeekMapTest { } @Test - public void testGetSeekPoints_forSeekableInput_forSeekPointAtEndOfStream_return1SeekPoint() { + public void getSeekPoints_forSeekableInput_forSeekPointAtEndOfStream_return1SeekPoint() { constantBitrateSeekMap = new ConstantBitrateSeekMap( /* inputLength= */ 2_300, @@ -168,7 +168,7 @@ public final class ConstantBitrateSeekMapTest { } @Test - public void testGetTimeUsAtPosition_forPosition0_return0() { + public void getTimeUsAtPosition_forPosition0_return0() { constantBitrateSeekMap = new ConstantBitrateSeekMap( /* inputLength= */ 2_300, @@ -180,7 +180,7 @@ public final class ConstantBitrateSeekMapTest { } @Test - public void testGetTimeUsAtPosition_forPositionWithinStream_returnCorrectTime() { + public void getTimeUsAtPosition_forPositionWithinStream_returnCorrectTime() { constantBitrateSeekMap = new ConstantBitrateSeekMap( /* inputLength= */ 2_300, @@ -192,7 +192,7 @@ public final class ConstantBitrateSeekMapTest { } @Test - public void testGetTimeUsAtPosition_forPositionAtEndOfStream_returnStreamDuration() { + public void getTimeUsAtPosition_forPositionAtEndOfStream_returnStreamDuration() { constantBitrateSeekMap = new ConstantBitrateSeekMap( /* inputLength= */ 2_300, diff --git a/library/core/src/test/java/com/google/android/exoplayer2/extractor/DefaultExtractorInputTest.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/DefaultExtractorInputTest.java similarity index 62% rename from library/core/src/test/java/com/google/android/exoplayer2/extractor/DefaultExtractorInputTest.java rename to library/extractor/src/test/java/com/google/android/exoplayer2/extractor/DefaultExtractorInputTest.java index 6dbec3ecf4..f404f24651 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/extractor/DefaultExtractorInputTest.java +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/DefaultExtractorInputTest.java @@ -41,7 +41,7 @@ public class DefaultExtractorInputTest { private static final int LARGE_TEST_DATA_LENGTH = 8192; @Test - public void testInitialPosition() throws Exception { + public void initialPosition() throws Exception { FakeDataSource testDataSource = buildDataSource(); DefaultExtractorInput input = new DefaultExtractorInput(testDataSource, 123, C.LENGTH_UNSET); @@ -49,7 +49,7 @@ public class DefaultExtractorInputTest { } @Test - public void testRead() throws Exception { + public void readMultipleTimes() throws Exception { DefaultExtractorInput input = createDefaultExtractorInput(); byte[] target = new byte[TEST_DATA.length]; // We expect to perform three reads of three bytes, as setup in buildTestDataSource. @@ -60,48 +60,79 @@ public class DefaultExtractorInputTest { assertThat(bytesRead).isEqualTo(6); bytesRead += input.read(target, 6, TEST_DATA.length); assertThat(bytesRead).isEqualTo(9); - // Check the read data is correct. - assertThat(Arrays.equals(TEST_DATA, target)).isTrue(); - // Check we're now indicated that the end of input is reached. - int expectedEndOfInput = input.read(target, 0, TEST_DATA.length); - assertThat(expectedEndOfInput).isEqualTo(RESULT_END_OF_INPUT); + assertThat(input.getPosition()).isEqualTo(9); + assertThat(TEST_DATA).isEqualTo(target); } @Test - public void testReadPeeked() throws Exception { + public void readAlreadyPeeked() throws Exception { DefaultExtractorInput input = createDefaultExtractorInput(); byte[] target = new byte[TEST_DATA.length]; input.advancePeekPosition(TEST_DATA.length); + int bytesRead = input.read(target, 0, TEST_DATA.length - 1); + assertThat(bytesRead).isEqualTo(TEST_DATA.length - 1); + assertThat(input.getPosition()).isEqualTo(TEST_DATA.length - 1); + assertThat(Arrays.copyOf(TEST_DATA, TEST_DATA.length - 1)) + .isEqualTo(Arrays.copyOf(target, TEST_DATA.length - 1)); + } + + @Test + public void readPartiallyPeeked() throws Exception { + DefaultExtractorInput input = createDefaultExtractorInput(); + byte[] target = new byte[TEST_DATA.length]; + + input.advancePeekPosition(TEST_DATA.length - 1); int bytesRead = input.read(target, 0, TEST_DATA.length); - assertThat(bytesRead).isEqualTo(TEST_DATA.length); - // Check the read data is correct. - assertThat(Arrays.equals(TEST_DATA, target)).isTrue(); + assertThat(bytesRead).isEqualTo(TEST_DATA.length - 1); + assertThat(input.getPosition()).isEqualTo(TEST_DATA.length - 1); + assertThat(Arrays.copyOf(TEST_DATA, TEST_DATA.length - 1)) + .isEqualTo(Arrays.copyOf(target, TEST_DATA.length - 1)); } @Test - public void testReadMoreDataPeeked() throws Exception { + public void readEndOfInputBeforeFirstByteRead() throws Exception { DefaultExtractorInput input = createDefaultExtractorInput(); byte[] target = new byte[TEST_DATA.length]; - input.advancePeekPosition(TEST_DATA.length); + input.skipFully(TEST_DATA.length); + int bytesRead = input.read(target, 0, TEST_DATA.length); - int bytesRead = input.read(target, 0, TEST_DATA.length + 1); - assertThat(bytesRead).isEqualTo(TEST_DATA.length); - - // Check the read data is correct. - assertThat(Arrays.equals(TEST_DATA, target)).isTrue(); + assertThat(bytesRead).isEqualTo(RESULT_END_OF_INPUT); + assertThat(input.getPosition()).isEqualTo(TEST_DATA.length); } @Test - public void testReadFullyOnce() throws Exception { + public void readEndOfInputAfterFirstByteRead() throws Exception { + DefaultExtractorInput input = createDefaultExtractorInput(); + byte[] target = new byte[TEST_DATA.length]; + + input.skipFully(TEST_DATA.length - 1); + int bytesRead = input.read(target, 0, TEST_DATA.length); + + assertThat(bytesRead).isEqualTo(1); + assertThat(input.getPosition()).isEqualTo(TEST_DATA.length); + } + + @Test + public void readZeroLength() throws Exception { + DefaultExtractorInput input = createDefaultExtractorInput(); + byte[] target = new byte[TEST_DATA.length]; + + int bytesRead = input.read(target, /* offset= */ 0, /* length= */ 0); + + assertThat(bytesRead).isEqualTo(0); + } + + @Test + public void readFullyOnce() throws Exception { DefaultExtractorInput input = createDefaultExtractorInput(); byte[] target = new byte[TEST_DATA.length]; input.readFully(target, 0, TEST_DATA.length); // Check that we read the whole of TEST_DATA. - assertThat(Arrays.equals(TEST_DATA, target)).isTrue(); + assertThat(TEST_DATA).isEqualTo(target); assertThat(input.getPosition()).isEqualTo(TEST_DATA.length); // Check that we see end of input if we read again with allowEndOfInput set. boolean result = input.readFully(target, 0, 1, true); @@ -116,21 +147,21 @@ public class DefaultExtractorInputTest { } @Test - public void testReadFullyTwice() throws Exception { + public void readFullyTwice() throws Exception { // Read TEST_DATA in two parts. DefaultExtractorInput input = createDefaultExtractorInput(); byte[] target = new byte[5]; input.readFully(target, 0, 5); - assertThat(Arrays.equals(copyOf(TEST_DATA, 5), target)).isTrue(); + assertThat(copyOf(TEST_DATA, 5)).isEqualTo(target); assertThat(input.getPosition()).isEqualTo(5); target = new byte[4]; input.readFully(target, 0, 4); - assertThat(Arrays.equals(copyOfRange(TEST_DATA, 5, 9), target)).isTrue(); + assertThat(copyOfRange(TEST_DATA, 5, 9)).isEqualTo(target); assertThat(input.getPosition()).isEqualTo(5 + 4); } @Test - public void testReadFullyTooMuch() throws Exception { + public void readFullyTooMuch() throws Exception { // Read more than TEST_DATA. Should fail with an EOFException. Position should not update. DefaultExtractorInput input = createDefaultExtractorInput(); try { @@ -156,7 +187,7 @@ public class DefaultExtractorInputTest { } @Test - public void testReadFullyWithFailingDataSource() throws Exception { + public void readFullyWithFailingDataSource() throws Exception { FakeDataSource testDataSource = buildFailingDataSource(); DefaultExtractorInput input = new DefaultExtractorInput(testDataSource, 0, C.LENGTH_UNSET); try { @@ -171,7 +202,7 @@ public class DefaultExtractorInputTest { } @Test - public void testReadFullyHalfPeeked() throws Exception { + public void readFullyHalfPeeked() throws Exception { DefaultExtractorInput input = createDefaultExtractorInput(); byte[] target = new byte[TEST_DATA.length]; @@ -180,27 +211,23 @@ public class DefaultExtractorInputTest { input.readFully(target, 0, TEST_DATA.length); // Check the read data is correct. - assertThat(Arrays.equals(TEST_DATA, target)).isTrue(); + assertThat(TEST_DATA).isEqualTo(target); assertThat(input.getPosition()).isEqualTo(TEST_DATA.length); } @Test - public void testSkip() throws Exception { - FakeDataSource testDataSource = buildDataSource(); - DefaultExtractorInput input = new DefaultExtractorInput(testDataSource, 0, C.LENGTH_UNSET); + public void skipMultipleTimes() throws Exception { + DefaultExtractorInput input = createDefaultExtractorInput(); // We expect to perform three skips of three bytes, as setup in buildTestDataSource. for (int i = 0; i < 3; i++) { assertThat(input.skip(TEST_DATA.length)).isEqualTo(3); } - // Check we're now indicated that the end of input is reached. - int expectedEndOfInput = input.skip(TEST_DATA.length); - assertThat(expectedEndOfInput).isEqualTo(RESULT_END_OF_INPUT); + assertThat(input.getPosition()).isEqualTo(TEST_DATA.length); } @Test - public void testLargeSkip() throws Exception { - FakeDataSource testDataSource = buildLargeDataSource(); - DefaultExtractorInput input = new DefaultExtractorInput(testDataSource, 0, C.LENGTH_UNSET); + public void largeSkip() throws Exception { + DefaultExtractorInput input = createDefaultExtractorInput(); // Check that skipping the entire data source succeeds. int bytesToSkip = LARGE_TEST_DATA_LENGTH; while (bytesToSkip > 0) { @@ -209,7 +236,60 @@ public class DefaultExtractorInputTest { } @Test - public void testSkipFullyOnce() throws Exception { + public void skipAlreadyPeeked() throws Exception { + DefaultExtractorInput input = createDefaultExtractorInput(); + + input.advancePeekPosition(TEST_DATA.length); + int bytesSkipped = input.skip(TEST_DATA.length - 1); + + assertThat(bytesSkipped).isEqualTo(TEST_DATA.length - 1); + assertThat(input.getPosition()).isEqualTo(TEST_DATA.length - 1); + } + + @Test + public void skipPartiallyPeeked() throws Exception { + DefaultExtractorInput input = createDefaultExtractorInput(); + + input.advancePeekPosition(TEST_DATA.length - 1); + int bytesSkipped = input.skip(TEST_DATA.length); + + assertThat(bytesSkipped).isEqualTo(TEST_DATA.length - 1); + assertThat(input.getPosition()).isEqualTo(TEST_DATA.length - 1); + } + + @Test + public void skipEndOfInputBeforeFirstByteSkipped() throws Exception { + DefaultExtractorInput input = createDefaultExtractorInput(); + + input.skipFully(TEST_DATA.length); + int bytesSkipped = input.skip(TEST_DATA.length); + + assertThat(bytesSkipped).isEqualTo(RESULT_END_OF_INPUT); + assertThat(input.getPosition()).isEqualTo(TEST_DATA.length); + } + + @Test + public void skipEndOfInputAfterFirstByteSkipped() throws Exception { + DefaultExtractorInput input = createDefaultExtractorInput(); + + input.skipFully(TEST_DATA.length - 1); + int bytesSkipped = input.skip(TEST_DATA.length); + + assertThat(bytesSkipped).isEqualTo(1); + assertThat(input.getPosition()).isEqualTo(TEST_DATA.length); + } + + @Test + public void skipZeroLength() throws Exception { + DefaultExtractorInput input = createDefaultExtractorInput(); + + int bytesRead = input.skip(0); + + assertThat(bytesRead).isEqualTo(0); + } + + @Test + public void skipFullyOnce() throws Exception { // Skip TEST_DATA. DefaultExtractorInput input = createDefaultExtractorInput(); input.skipFully(TEST_DATA.length); @@ -227,7 +307,7 @@ public class DefaultExtractorInputTest { } @Test - public void testSkipFullyTwice() throws Exception { + public void skipFullyTwice() throws Exception { // Skip TEST_DATA in two parts. DefaultExtractorInput input = createDefaultExtractorInput(); input.skipFully(5); @@ -237,7 +317,7 @@ public class DefaultExtractorInputTest { } @Test - public void testSkipFullyTwicePeeked() throws Exception { + public void skipFullyTwicePeeked() throws Exception { // Skip TEST_DATA. DefaultExtractorInput input = createDefaultExtractorInput(); @@ -252,7 +332,7 @@ public class DefaultExtractorInputTest { } @Test - public void testSkipFullyTooMuch() throws Exception { + public void skipFullyTooMuch() throws Exception { // Skip more than TEST_DATA. Should fail with an EOFException. Position should not update. DefaultExtractorInput input = createDefaultExtractorInput(); try { @@ -276,7 +356,7 @@ public class DefaultExtractorInputTest { } @Test - public void testSkipFullyWithFailingDataSource() throws Exception { + public void skipFullyWithFailingDataSource() throws Exception { FakeDataSource testDataSource = buildFailingDataSource(); DefaultExtractorInput input = new DefaultExtractorInput(testDataSource, 0, C.LENGTH_UNSET); try { @@ -290,7 +370,7 @@ public class DefaultExtractorInputTest { } @Test - public void testSkipFullyLarge() throws Exception { + public void skipFullyLarge() throws Exception { // Tests skipping an amount of data that's larger than any internal scratch space. int largeSkipSize = 1024 * 1024; FakeDataSource testDataSource = new FakeDataSource(); @@ -310,20 +390,100 @@ public class DefaultExtractorInputTest { } @Test - public void testPeekFully() throws Exception { + public void peekMultipleTimes() throws Exception { + DefaultExtractorInput input = createDefaultExtractorInput(); + byte[] target = new byte[TEST_DATA.length]; + + // We expect to perform three peeks of three bytes, as setup in buildTestDataSource. + int bytesPeeked = 0; + bytesPeeked += input.peek(target, 0, TEST_DATA.length); + assertThat(bytesPeeked).isEqualTo(3); + bytesPeeked += input.peek(target, 3, TEST_DATA.length); + assertThat(bytesPeeked).isEqualTo(6); + bytesPeeked += input.peek(target, 6, TEST_DATA.length); + assertThat(bytesPeeked).isEqualTo(9); + assertThat(input.getPeekPosition()).isEqualTo(TEST_DATA.length); + assertThat(TEST_DATA).isEqualTo(target); + } + + @Test + public void peekAlreadyPeeked() throws Exception { + DefaultExtractorInput input = createDefaultExtractorInput(); + byte[] target = new byte[TEST_DATA.length]; + + input.advancePeekPosition(TEST_DATA.length); + input.resetPeekPosition(); + int bytesPeeked = input.peek(target, 0, TEST_DATA.length - 1); + + assertThat(bytesPeeked).isEqualTo(TEST_DATA.length - 1); + assertThat(input.getPeekPosition()).isEqualTo(TEST_DATA.length - 1); + assertThat(Arrays.copyOf(TEST_DATA, TEST_DATA.length - 1)) + .isEqualTo(Arrays.copyOf(target, TEST_DATA.length - 1)); + } + + @Test + public void peekPartiallyPeeked() throws Exception { + DefaultExtractorInput input = createDefaultExtractorInput(); + byte[] target = new byte[TEST_DATA.length]; + + input.advancePeekPosition(TEST_DATA.length - 1); + input.resetPeekPosition(); + int bytesPeeked = input.peek(target, 0, TEST_DATA.length); + + assertThat(bytesPeeked).isEqualTo(TEST_DATA.length - 1); + assertThat(Arrays.copyOf(TEST_DATA, TEST_DATA.length - 1)) + .isEqualTo(Arrays.copyOf(target, TEST_DATA.length - 1)); + } + + @Test + public void peekEndOfInputBeforeFirstBytePeeked() throws Exception { + DefaultExtractorInput input = createDefaultExtractorInput(); + byte[] target = new byte[TEST_DATA.length]; + + input.advancePeekPosition(TEST_DATA.length); + int bytesPeeked = input.peek(target, 0, TEST_DATA.length); + + assertThat(bytesPeeked).isEqualTo(RESULT_END_OF_INPUT); + assertThat(input.getPeekPosition()).isEqualTo(TEST_DATA.length); + } + + @Test + public void peekEndOfInputAfterFirstBytePeeked() throws Exception { + DefaultExtractorInput input = createDefaultExtractorInput(); + byte[] target = new byte[TEST_DATA.length]; + + input.advancePeekPosition(TEST_DATA.length - 1); + int bytesPeeked = input.peek(target, 0, TEST_DATA.length); + + assertThat(bytesPeeked).isEqualTo(1); + assertThat(input.getPeekPosition()).isEqualTo(TEST_DATA.length); + } + + @Test + public void peekZeroLength() throws Exception { + DefaultExtractorInput input = createDefaultExtractorInput(); + byte[] target = new byte[TEST_DATA.length]; + + int bytesPeeked = input.peek(target, /* offset= */ 0, /* length= */ 0); + + assertThat(bytesPeeked).isEqualTo(0); + } + + @Test + public void peekFully() throws Exception { DefaultExtractorInput input = createDefaultExtractorInput(); byte[] target = new byte[TEST_DATA.length]; input.peekFully(target, 0, TEST_DATA.length); // Check that we read the whole of TEST_DATA. - assertThat(Arrays.equals(TEST_DATA, target)).isTrue(); + assertThat(TEST_DATA).isEqualTo(target); assertThat(input.getPosition()).isEqualTo(0); assertThat(input.getPeekPosition()).isEqualTo(TEST_DATA.length); // Check that we can read again from the buffer byte[] target2 = new byte[TEST_DATA.length]; input.readFully(target2, 0, TEST_DATA.length); - assertThat(Arrays.equals(TEST_DATA, target2)).isTrue(); + assertThat(TEST_DATA).isEqualTo(target2); assertThat(input.getPosition()).isEqualTo(TEST_DATA.length); assertThat(input.getPeekPosition()).isEqualTo(TEST_DATA.length); @@ -337,7 +497,7 @@ public class DefaultExtractorInputTest { } @Test - public void testPeekFullyAfterEofExceptionPeeksAsExpected() throws Exception { + public void peekFullyAfterEofExceptionPeeksAsExpected() throws Exception { DefaultExtractorInput input = createDefaultExtractorInput(); byte[] target = new byte[TEST_DATA.length + 10]; @@ -350,24 +510,24 @@ public class DefaultExtractorInputTest { input.peekFully(target, /* offset= */ 0, /* length= */ TEST_DATA.length); assertThat(input.getPeekPosition()).isEqualTo(TEST_DATA.length); - assertThat(Arrays.equals(TEST_DATA, Arrays.copyOf(target, TEST_DATA.length))).isTrue(); + assertThat(TEST_DATA).isEqualTo(Arrays.copyOf(target, TEST_DATA.length)); } @Test - public void testResetPeekPosition() throws Exception { + public void resetPeekPosition() throws Exception { DefaultExtractorInput input = createDefaultExtractorInput(); byte[] target = new byte[TEST_DATA.length]; input.peekFully(target, 0, TEST_DATA.length); // Check that we read the whole of TEST_DATA. - assertThat(Arrays.equals(TEST_DATA, target)).isTrue(); + assertThat(TEST_DATA).isEqualTo(target); assertThat(input.getPosition()).isEqualTo(0); // Check that we can peek again after resetting. input.resetPeekPosition(); byte[] target2 = new byte[TEST_DATA.length]; input.peekFully(target2, 0, TEST_DATA.length); - assertThat(Arrays.equals(TEST_DATA, target2)).isTrue(); + assertThat(TEST_DATA).isEqualTo(target2); // Check that we fail with EOFException if we peek past the end of the input. try { @@ -379,7 +539,7 @@ public class DefaultExtractorInputTest { } @Test - public void testPeekFullyAtEndOfStreamWithAllowEndOfInputSucceeds() throws Exception { + public void peekFullyAtEndOfStreamWithAllowEndOfInputSucceeds() throws Exception { DefaultExtractorInput input = createDefaultExtractorInput(); byte[] target = new byte[TEST_DATA.length]; @@ -391,7 +551,7 @@ public class DefaultExtractorInputTest { } @Test - public void testPeekFullyAtEndThenReadEndOfInput() throws Exception { + public void peekFullyAtEndThenReadEndOfInput() throws Exception { DefaultExtractorInput input = createDefaultExtractorInput(); byte[] target = new byte[TEST_DATA.length]; @@ -409,7 +569,7 @@ public class DefaultExtractorInputTest { } @Test - public void testPeekFullyAcrossEndOfInputWithAllowEndOfInputFails() throws Exception { + public void peekFullyAcrossEndOfInputWithAllowEndOfInputFails() throws Exception { DefaultExtractorInput input = createDefaultExtractorInput(); byte[] target = new byte[TEST_DATA.length]; @@ -426,7 +586,7 @@ public class DefaultExtractorInputTest { } @Test - public void testResetAndPeekFullyPastEndOfStreamWithAllowEndOfInputFails() throws Exception { + public void resetAndPeekFullyPastEndOfStreamWithAllowEndOfInputFails() throws Exception { DefaultExtractorInput input = createDefaultExtractorInput(); byte[] target = new byte[TEST_DATA.length]; @@ -462,14 +622,6 @@ public class DefaultExtractorInputTest { return testDataSource; } - private static FakeDataSource buildLargeDataSource() throws Exception { - FakeDataSource testDataSource = new FakeDataSource(); - testDataSource.getDataSet().newDefaultData() - .appendReadData(new byte[LARGE_TEST_DATA_LENGTH]); - testDataSource.open(new DataSpec(Uri.parse(TEST_URI))); - return testDataSource; - } - private static DefaultExtractorInput createDefaultExtractorInput() throws Exception { FakeDataSource testDataSource = buildDataSource(); return new DefaultExtractorInput(testDataSource, 0, C.LENGTH_UNSET); diff --git a/library/core/src/test/java/com/google/android/exoplayer2/extractor/DefaultExtractorsFactoryTest.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/DefaultExtractorsFactoryTest.java similarity index 97% rename from library/core/src/test/java/com/google/android/exoplayer2/extractor/DefaultExtractorsFactoryTest.java rename to library/extractor/src/test/java/com/google/android/exoplayer2/extractor/DefaultExtractorsFactoryTest.java index ace30dbaf5..b24c76d262 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/extractor/DefaultExtractorsFactoryTest.java +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/DefaultExtractorsFactoryTest.java @@ -42,7 +42,7 @@ import org.junit.runner.RunWith; public final class DefaultExtractorsFactoryTest { @Test - public void testCreateExtractors_returnExpectedClasses() { + public void createExtractors_returnExpectedClasses() { DefaultExtractorsFactory defaultExtractorsFactory = new DefaultExtractorsFactory(); Extractor[] extractors = defaultExtractorsFactory.createExtractors(); diff --git a/library/core/src/test/java/com/google/android/exoplayer2/extractor/ExtractorTest.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/ExtractorTest.java similarity index 97% rename from library/core/src/test/java/com/google/android/exoplayer2/extractor/ExtractorTest.java rename to library/extractor/src/test/java/com/google/android/exoplayer2/extractor/ExtractorTest.java index 0401b2be83..c95804c297 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/extractor/ExtractorTest.java +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/ExtractorTest.java @@ -27,7 +27,7 @@ import org.junit.runner.RunWith; public final class ExtractorTest { @Test - public void testConstants() { + public void constants() { // Sanity check that constant values match those defined by {@link C}. assertThat(Extractor.RESULT_END_OF_INPUT).isEqualTo(C.RESULT_END_OF_INPUT); // Sanity check that the other constant values don't overlap. diff --git a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/ExtractorUtilTest.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/ExtractorUtilTest.java new file mode 100644 index 0000000000..9604b48bee --- /dev/null +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/ExtractorUtilTest.java @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2019 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 android.net.Uri; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.testutil.FakeDataSource; +import com.google.android.exoplayer2.upstream.DataSpec; +import java.util.Arrays; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Unit test for {@link ExtractorUtil}. */ +@RunWith(AndroidJUnit4.class) +public class ExtractorUtilTest { + + private static final String TEST_URI = "http://www.google.com"; + private static final byte[] TEST_DATA = new byte[] {0, 1, 2, 3, 4, 5, 6, 7, 8}; + + @Test + public void peekToLengthEndNotReached() throws Exception { + FakeDataSource testDataSource = new FakeDataSource(); + testDataSource + .getDataSet() + .newDefaultData() + .appendReadData(Arrays.copyOfRange(TEST_DATA, 0, 3)) + .appendReadData(Arrays.copyOfRange(TEST_DATA, 3, 6)) + .appendReadData(Arrays.copyOfRange(TEST_DATA, 6, 9)); + testDataSource.open(new DataSpec(Uri.parse(TEST_URI))); + ExtractorInput input = new DefaultExtractorInput(testDataSource, 0, C.LENGTH_UNSET); + byte[] target = new byte[TEST_DATA.length]; + int offset = 2; + int length = 4; + + int bytesPeeked = ExtractorUtil.peekToLength(input, target, offset, length); + + assertThat(bytesPeeked).isEqualTo(length); + assertThat(input.getPeekPosition()).isEqualTo(length); + assertThat(Arrays.copyOfRange(target, offset, offset + length)) + .isEqualTo(Arrays.copyOf(TEST_DATA, length)); + } + + @Test + public void peekToLengthEndReached() throws Exception { + FakeDataSource testDataSource = new FakeDataSource(); + testDataSource + .getDataSet() + .newDefaultData() + .appendReadData(Arrays.copyOfRange(TEST_DATA, 0, 3)) + .appendReadData(Arrays.copyOfRange(TEST_DATA, 3, 6)) + .appendReadData(Arrays.copyOfRange(TEST_DATA, 6, 9)); + testDataSource.open(new DataSpec(Uri.parse(TEST_URI))); + ExtractorInput input = new DefaultExtractorInput(testDataSource, 0, C.LENGTH_UNSET); + byte[] target = new byte[TEST_DATA.length]; + int offset = 0; + int length = TEST_DATA.length + 1; + + int bytesPeeked = ExtractorUtil.peekToLength(input, target, offset, length); + + assertThat(bytesPeeked).isEqualTo(TEST_DATA.length); + assertThat(input.getPeekPosition()).isEqualTo(TEST_DATA.length); + assertThat(target).isEqualTo(TEST_DATA); + } +} diff --git a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/FlacFrameReaderTest.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/FlacFrameReaderTest.java new file mode 100644 index 0000000000..9150493ea3 --- /dev/null +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/FlacFrameReaderTest.java @@ -0,0 +1,322 @@ +/* + * Copyright (C) 2020 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 androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.android.exoplayer2.extractor.FlacFrameReader.SampleNumberHolder; +import com.google.android.exoplayer2.extractor.FlacMetadataReader.FlacStreamMetadataHolder; +import com.google.android.exoplayer2.testutil.FakeExtractorInput; +import com.google.android.exoplayer2.testutil.TestUtil; +import com.google.android.exoplayer2.util.FlacConstants; +import com.google.android.exoplayer2.util.ParsableByteArray; +import java.io.IOException; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** + * Unit tests for {@link FlacFrameReader}. + * + *

      Some expected results in these tests have been retrieved using the flac command. + */ +@RunWith(AndroidJUnit4.class) +public class FlacFrameReaderTest { + + @Test + public void checkAndReadFrameHeader_validData_updatesPosition() throws Exception { + FlacStreamMetadataHolder streamMetadataHolder = + new FlacStreamMetadataHolder(/* flacStreamMetadata= */ null); + ExtractorInput input = + buildExtractorInputReadingFromFirstFrame( + "flac/bear_one_metadata_block.flac", streamMetadataHolder); + int frameStartMarker = FlacMetadataReader.getFrameStartMarker(input); + ParsableByteArray scratch = new ParsableByteArray(FlacConstants.MAX_FRAME_HEADER_SIZE); + input.read(scratch.data, 0, FlacConstants.MAX_FRAME_HEADER_SIZE); + + FlacFrameReader.checkAndReadFrameHeader( + scratch, + streamMetadataHolder.flacStreamMetadata, + frameStartMarker, + new SampleNumberHolder()); + + assertThat(scratch.getPosition()).isEqualTo(FlacConstants.MIN_FRAME_HEADER_SIZE); + } + + @Test + public void checkAndReadFrameHeader_validData_isTrue() throws Exception { + FlacStreamMetadataHolder streamMetadataHolder = + new FlacStreamMetadataHolder(/* flacStreamMetadata= */ null); + ExtractorInput input = + buildExtractorInputReadingFromFirstFrame( + "flac/bear_one_metadata_block.flac", streamMetadataHolder); + int frameStartMarker = FlacMetadataReader.getFrameStartMarker(input); + ParsableByteArray scratch = new ParsableByteArray(FlacConstants.MAX_FRAME_HEADER_SIZE); + input.read(scratch.data, 0, FlacConstants.MAX_FRAME_HEADER_SIZE); + + boolean result = + FlacFrameReader.checkAndReadFrameHeader( + scratch, + streamMetadataHolder.flacStreamMetadata, + frameStartMarker, + new SampleNumberHolder()); + + assertThat(result).isTrue(); + } + + @Test + public void checkAndReadFrameHeader_validData_writesSampleNumber() throws Exception { + FlacStreamMetadataHolder streamMetadataHolder = + new FlacStreamMetadataHolder(/* flacStreamMetadata= */ null); + ExtractorInput input = + buildExtractorInputReadingFromFirstFrame( + "flac/bear_one_metadata_block.flac", streamMetadataHolder); + int frameStartMarker = FlacMetadataReader.getFrameStartMarker(input); + // Skip first frame. + input.skip(5030); + ParsableByteArray scratch = new ParsableByteArray(FlacConstants.MAX_FRAME_HEADER_SIZE); + input.read(scratch.data, 0, FlacConstants.MAX_FRAME_HEADER_SIZE); + SampleNumberHolder sampleNumberHolder = new SampleNumberHolder(); + + FlacFrameReader.checkAndReadFrameHeader( + scratch, streamMetadataHolder.flacStreamMetadata, frameStartMarker, sampleNumberHolder); + + assertThat(sampleNumberHolder.sampleNumber).isEqualTo(4096); + } + + @Test + public void checkAndReadFrameHeader_invalidData_isFalse() throws Exception { + FlacStreamMetadataHolder streamMetadataHolder = + new FlacStreamMetadataHolder(/* flacStreamMetadata= */ null); + ExtractorInput input = + buildExtractorInputReadingFromFirstFrame( + "flac/bear_one_metadata_block.flac", streamMetadataHolder); + ParsableByteArray scratch = new ParsableByteArray(FlacConstants.MAX_FRAME_HEADER_SIZE); + input.read(scratch.data, 0, FlacConstants.MAX_FRAME_HEADER_SIZE); + + // The first bytes of the frame are not equal to the frame start marker. + boolean result = + FlacFrameReader.checkAndReadFrameHeader( + scratch, + streamMetadataHolder.flacStreamMetadata, + /* frameStartMarker= */ -1, + new SampleNumberHolder()); + + assertThat(result).isFalse(); + } + + @Test + public void checkFrameHeaderFromPeek_validData_doesNotUpdatePositions() throws Exception { + String file = "flac/bear_one_metadata_block.flac"; + FlacStreamMetadataHolder streamMetadataHolder = + new FlacStreamMetadataHolder(/* flacStreamMetadata= */ null); + ExtractorInput input = buildExtractorInputReadingFromFirstFrame(file, streamMetadataHolder); + int frameStartMarker = FlacMetadataReader.getFrameStartMarker(input); + long peekPosition = input.getPosition(); + // Set read position to 0. + input = buildExtractorInput(file); + input.advancePeekPosition((int) peekPosition); + + FlacFrameReader.checkFrameHeaderFromPeek( + input, streamMetadataHolder.flacStreamMetadata, frameStartMarker, new SampleNumberHolder()); + + assertThat(input.getPosition()).isEqualTo(0); + assertThat(input.getPeekPosition()).isEqualTo(peekPosition); + } + + @Test + public void checkFrameHeaderFromPeek_validData_isTrue() throws Exception { + FlacStreamMetadataHolder streamMetadataHolder = + new FlacStreamMetadataHolder(/* flacStreamMetadata= */ null); + ExtractorInput input = + buildExtractorInputReadingFromFirstFrame( + "flac/bear_one_metadata_block.flac", streamMetadataHolder); + int frameStartMarker = FlacMetadataReader.getFrameStartMarker(input); + + boolean result = + FlacFrameReader.checkFrameHeaderFromPeek( + input, + streamMetadataHolder.flacStreamMetadata, + frameStartMarker, + new SampleNumberHolder()); + + assertThat(result).isTrue(); + } + + @Test + public void checkFrameHeaderFromPeek_validData_writesSampleNumber() throws Exception { + FlacStreamMetadataHolder streamMetadataHolder = + new FlacStreamMetadataHolder(/* flacStreamMetadata= */ null); + ExtractorInput input = + buildExtractorInputReadingFromFirstFrame( + "flac/bear_one_metadata_block.flac", streamMetadataHolder); + int frameStartMarker = FlacMetadataReader.getFrameStartMarker(input); + // Skip first frame. + input.skip(5030); + SampleNumberHolder sampleNumberHolder = new SampleNumberHolder(); + + FlacFrameReader.checkFrameHeaderFromPeek( + input, streamMetadataHolder.flacStreamMetadata, frameStartMarker, sampleNumberHolder); + + assertThat(sampleNumberHolder.sampleNumber).isEqualTo(4096); + } + + @Test + public void checkFrameHeaderFromPeek_invalidData_isFalse() throws Exception { + FlacStreamMetadataHolder streamMetadataHolder = + new FlacStreamMetadataHolder(/* flacStreamMetadata= */ null); + ExtractorInput input = + buildExtractorInputReadingFromFirstFrame( + "flac/bear_one_metadata_block.flac", streamMetadataHolder); + + // The first bytes of the frame are not equal to the frame start marker. + boolean result = + FlacFrameReader.checkFrameHeaderFromPeek( + input, + streamMetadataHolder.flacStreamMetadata, + /* frameStartMarker= */ -1, + new SampleNumberHolder()); + + assertThat(result).isFalse(); + } + + @Test + public void checkFrameHeaderFromPeek_invalidData_doesNotUpdatePositions() throws Exception { + String file = "flac/bear_one_metadata_block.flac"; + FlacStreamMetadataHolder streamMetadataHolder = + new FlacStreamMetadataHolder(/* flacStreamMetadata= */ null); + ExtractorInput input = buildExtractorInputReadingFromFirstFrame(file, streamMetadataHolder); + long peekPosition = input.getPosition(); + // Set read position to 0. + input = buildExtractorInput(file); + input.advancePeekPosition((int) peekPosition); + + // The first bytes of the frame are not equal to the frame start marker. + FlacFrameReader.checkFrameHeaderFromPeek( + input, + streamMetadataHolder.flacStreamMetadata, + /* frameStartMarker= */ -1, + new SampleNumberHolder()); + + assertThat(input.getPosition()).isEqualTo(0); + assertThat(input.getPeekPosition()).isEqualTo(peekPosition); + } + + @Test + public void getFirstSampleNumber_doesNotUpdateReadPositionAndAlignsPeekPosition() + throws Exception { + FlacStreamMetadataHolder streamMetadataHolder = + new FlacStreamMetadataHolder(/* flacStreamMetadata= */ null); + ExtractorInput input = + buildExtractorInputReadingFromFirstFrame( + "flac/bear_one_metadata_block.flac", streamMetadataHolder); + long initialReadPosition = input.getPosition(); + // Advance peek position after block size bits. + input.advancePeekPosition(FlacConstants.MAX_FRAME_HEADER_SIZE); + + FlacFrameReader.getFirstSampleNumber(input, streamMetadataHolder.flacStreamMetadata); + + assertThat(input.getPosition()).isEqualTo(initialReadPosition); + assertThat(input.getPeekPosition()).isEqualTo(input.getPosition()); + } + + @Test + public void getFirstSampleNumber_returnsSampleNumber() throws Exception { + FlacStreamMetadataHolder streamMetadataHolder = + new FlacStreamMetadataHolder(/* flacStreamMetadata= */ null); + ExtractorInput input = + buildExtractorInputReadingFromFirstFrame( + "flac/bear_one_metadata_block.flac", streamMetadataHolder); + // Skip first frame. + input.skip(5030); + + long result = + FlacFrameReader.getFirstSampleNumber(input, streamMetadataHolder.flacStreamMetadata); + + assertThat(result).isEqualTo(4096); + } + + @Test + public void readFrameBlockSizeSamplesFromKey_keyIs1_returnsCorrectBlockSize() { + int result = + FlacFrameReader.readFrameBlockSizeSamplesFromKey( + new ParsableByteArray(/* limit= */ 0), /* blockSizeKey= */ 1); + + assertThat(result).isEqualTo(192); + } + + @Test + public void readFrameBlockSizeSamplesFromKey_keyBetween2and5_returnsCorrectBlockSize() { + int result = + FlacFrameReader.readFrameBlockSizeSamplesFromKey( + new ParsableByteArray(/* limit= */ 0), /* blockSizeKey= */ 3); + + assertThat(result).isEqualTo(1152); + } + + @Test + public void readFrameBlockSizeSamplesFromKey_keyBetween6And7_returnsCorrectBlockSize() + throws Exception { + ExtractorInput input = buildExtractorInput("flac/bear_one_metadata_block.flac"); + // Skip to block size bits of last frame. + input.skipFully(164033); + ParsableByteArray scratch = new ParsableByteArray(2); + input.readFully(scratch.data, 0, 2); + + int result = FlacFrameReader.readFrameBlockSizeSamplesFromKey(scratch, /* blockSizeKey= */ 7); + + assertThat(result).isEqualTo(496); + } + + @Test + public void readFrameBlockSizeSamplesFromKey_keyBetween8and15_returnsCorrectBlockSize() { + int result = + FlacFrameReader.readFrameBlockSizeSamplesFromKey( + new ParsableByteArray(/* limit= */ 0), /* blockSizeKey= */ 11); + + assertThat(result).isEqualTo(2048); + } + + @Test + public void readFrameBlockSizeSamplesFromKey_invalidKey_returnsCorrectBlockSize() { + int result = + FlacFrameReader.readFrameBlockSizeSamplesFromKey( + new ParsableByteArray(/* limit= */ 0), /* blockSizeKey= */ 25); + + assertThat(result).isEqualTo(-1); + } + + private static ExtractorInput buildExtractorInput(String file) throws IOException { + byte[] fileData = TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), file); + return new FakeExtractorInput.Builder().setData(fileData).build(); + } + + private ExtractorInput buildExtractorInputReadingFromFirstFrame( + String file, FlacStreamMetadataHolder streamMetadataHolder) throws IOException { + ExtractorInput input = buildExtractorInput(file); + + input.skipFully(FlacConstants.STREAM_MARKER_SIZE); + + boolean lastMetadataBlock = false; + while (!lastMetadataBlock) { + lastMetadataBlock = FlacMetadataReader.readMetadataBlock(input, streamMetadataHolder); + } + + return input; + } +} diff --git a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/FlacMetadataReaderTest.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/FlacMetadataReaderTest.java new file mode 100644 index 0000000000..a6a2cd35b6 --- /dev/null +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/FlacMetadataReaderTest.java @@ -0,0 +1,411 @@ +/* + * Copyright (C) 2020 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 static org.junit.Assert.assertThrows; + +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.android.exoplayer2.ParserException; +import com.google.android.exoplayer2.extractor.FlacMetadataReader.FlacStreamMetadataHolder; +import com.google.android.exoplayer2.metadata.Metadata; +import com.google.android.exoplayer2.metadata.flac.PictureFrame; +import com.google.android.exoplayer2.metadata.flac.VorbisComment; +import com.google.android.exoplayer2.testutil.FakeExtractorInput; +import com.google.android.exoplayer2.testutil.TestUtil; +import com.google.android.exoplayer2.util.FlacConstants; +import com.google.android.exoplayer2.util.ParsableByteArray; +import java.io.IOException; +import java.util.ArrayList; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** + * Unit tests for {@link FlacMetadataReader}. + * + *

      Most expected results in these tests have been retrieved using the metaflac command. + */ +@RunWith(AndroidJUnit4.class) +public class FlacMetadataReaderTest { + + @Test + public void peekId3Metadata_updatesPeekPosition() throws Exception { + ExtractorInput input = buildExtractorInput("flac/bear_with_id3.flac"); + + FlacMetadataReader.peekId3Metadata(input, /* parseData= */ false); + + assertThat(input.getPosition()).isEqualTo(0); + assertThat(input.getPeekPosition()).isNotEqualTo(0); + } + + @Test + public void peekId3Metadata_parseData_returnsNonEmptyMetadata() throws Exception { + ExtractorInput input = buildExtractorInput("flac/bear_with_id3.flac"); + + Metadata metadata = FlacMetadataReader.peekId3Metadata(input, /* parseData= */ true); + + assertThat(metadata).isNotNull(); + assertThat(metadata.length()).isNotEqualTo(0); + } + + @Test + public void peekId3Metadata_doNotParseData_returnsNull() throws Exception { + ExtractorInput input = buildExtractorInput("flac/bear_with_id3.flac"); + + Metadata metadata = FlacMetadataReader.peekId3Metadata(input, /* parseData= */ false); + + assertThat(metadata).isNull(); + } + + @Test + public void peekId3Metadata_noId3Metadata_returnsNull() throws Exception { + String fileWithoutId3Metadata = "flac/bear.flac"; + ExtractorInput input = buildExtractorInput(fileWithoutId3Metadata); + + Metadata metadata = FlacMetadataReader.peekId3Metadata(input, /* parseData= */ true); + + assertThat(metadata).isNull(); + } + + @Test + public void checkAndPeekStreamMarker_updatesPeekPosition() throws Exception { + ExtractorInput input = buildExtractorInput("flac/bear.flac"); + + FlacMetadataReader.checkAndPeekStreamMarker(input); + + assertThat(input.getPosition()).isEqualTo(0); + assertThat(input.getPeekPosition()).isEqualTo(FlacConstants.STREAM_MARKER_SIZE); + } + + @Test + public void checkAndPeekStreamMarker_validData_isTrue() throws Exception { + ExtractorInput input = buildExtractorInput("flac/bear.flac"); + + boolean result = FlacMetadataReader.checkAndPeekStreamMarker(input); + + assertThat(result).isTrue(); + } + + @Test + public void checkAndPeekStreamMarker_invalidData_isFalse() throws Exception { + ExtractorInput input = buildExtractorInput("mp3/bear-vbr-xing-header.mp3"); + + boolean result = FlacMetadataReader.checkAndPeekStreamMarker(input); + + assertThat(result).isFalse(); + } + + @Test + public void readId3Metadata_updatesReadPositionAndAlignsPeekPosition() throws Exception { + ExtractorInput input = buildExtractorInput("flac/bear_with_id3.flac"); + // Advance peek position after ID3 metadata. + FlacMetadataReader.peekId3Metadata(input, /* parseData= */ false); + input.advancePeekPosition(1); + + FlacMetadataReader.readId3Metadata(input, /* parseData= */ false); + + assertThat(input.getPosition()).isNotEqualTo(0); + assertThat(input.getPeekPosition()).isEqualTo(input.getPosition()); + } + + @Test + public void readId3Metadata_parseData_returnsNonEmptyMetadata() throws Exception { + ExtractorInput input = buildExtractorInput("flac/bear_with_id3.flac"); + + Metadata metadata = FlacMetadataReader.readId3Metadata(input, /* parseData= */ true); + + assertThat(metadata).isNotNull(); + assertThat(metadata.length()).isNotEqualTo(0); + } + + @Test + public void readId3Metadata_doNotParseData_returnsNull() throws Exception { + ExtractorInput input = buildExtractorInput("flac/bear_with_id3.flac"); + + Metadata metadata = FlacMetadataReader.readId3Metadata(input, /* parseData= */ false); + + assertThat(metadata).isNull(); + } + + @Test + public void readId3Metadata_noId3Metadata_returnsNull() throws Exception { + ExtractorInput input = buildExtractorInput("flac/bear.flac"); + + Metadata metadata = FlacMetadataReader.readId3Metadata(input, /* parseData= */ true); + + assertThat(metadata).isNull(); + } + + @Test + public void readStreamMarker_updatesReadPosition() throws Exception { + ExtractorInput input = buildExtractorInput("flac/bear.flac"); + + FlacMetadataReader.readStreamMarker(input); + + assertThat(input.getPosition()).isEqualTo(FlacConstants.STREAM_MARKER_SIZE); + assertThat(input.getPeekPosition()).isEqualTo(input.getPosition()); + } + + @Test + public void readStreamMarker_invalidData_throwsException() throws Exception { + ExtractorInput input = buildExtractorInput("mp3/bear-vbr-xing-header.mp3"); + + assertThrows(ParserException.class, () -> FlacMetadataReader.readStreamMarker(input)); + } + + @Test + public void readMetadataBlock_updatesReadPositionAndAlignsPeekPosition() throws Exception { + ExtractorInput input = buildExtractorInput("flac/bear.flac"); + input.skipFully(FlacConstants.STREAM_MARKER_SIZE); + // Advance peek position after metadata block. + input.advancePeekPosition(FlacConstants.STREAM_INFO_BLOCK_SIZE + 1); + + FlacMetadataReader.readMetadataBlock( + input, new FlacStreamMetadataHolder(/* flacStreamMetadata= */ null)); + + assertThat(input.getPosition()).isNotEqualTo(0); + assertThat(input.getPeekPosition()).isEqualTo(input.getPosition()); + } + + @Test + public void readMetadataBlock_lastMetadataBlock_isTrue() throws Exception { + ExtractorInput input = buildExtractorInput("flac/bear_one_metadata_block.flac"); + input.skipFully(FlacConstants.STREAM_MARKER_SIZE); + + boolean result = + FlacMetadataReader.readMetadataBlock( + input, new FlacStreamMetadataHolder(/* flacStreamMetadata= */ null)); + + assertThat(result).isTrue(); + } + + @Test + public void readMetadataBlock_notLastMetadataBlock_isFalse() throws Exception { + ExtractorInput input = buildExtractorInput("flac/bear.flac"); + input.skipFully(FlacConstants.STREAM_MARKER_SIZE); + + boolean result = + FlacMetadataReader.readMetadataBlock( + input, new FlacStreamMetadataHolder(/* flacStreamMetadata= */ null)); + + assertThat(result).isFalse(); + } + + @Test + public void readMetadataBlock_streamInfoBlock_setsStreamMetadata() throws Exception { + ExtractorInput input = buildExtractorInput("flac/bear.flac"); + input.skipFully(FlacConstants.STREAM_MARKER_SIZE); + FlacStreamMetadataHolder metadataHolder = + new FlacStreamMetadataHolder(/* flacStreamMetadata= */ null); + + FlacMetadataReader.readMetadataBlock(input, metadataHolder); + + assertThat(metadataHolder.flacStreamMetadata).isNotNull(); + assertThat(metadataHolder.flacStreamMetadata.sampleRate).isEqualTo(48000); + } + + @Test + public void readMetadataBlock_seekTableBlock_updatesStreamMetadata() throws Exception { + ExtractorInput input = buildExtractorInput("flac/bear.flac"); + // Skip to seek table block. + input.skipFully(FlacConstants.STREAM_MARKER_SIZE + FlacConstants.STREAM_INFO_BLOCK_SIZE); + FlacStreamMetadataHolder metadataHolder = new FlacStreamMetadataHolder(buildStreamMetadata()); + long originalSampleRate = metadataHolder.flacStreamMetadata.sampleRate; + + FlacMetadataReader.readMetadataBlock(input, metadataHolder); + + assertThat(metadataHolder.flacStreamMetadata).isNotNull(); + // Check that metadata passed has not been erased. + assertThat(metadataHolder.flacStreamMetadata.sampleRate).isEqualTo(originalSampleRate); + assertThat(metadataHolder.flacStreamMetadata.seekTable).isNotNull(); + assertThat(metadataHolder.flacStreamMetadata.seekTable.pointSampleNumbers.length).isEqualTo(32); + } + + @Test + public void readMetadataBlock_vorbisCommentBlock_updatesStreamMetadata() throws Exception { + ExtractorInput input = buildExtractorInput("flac/bear_with_vorbis_comments.flac"); + // Skip to Vorbis comment block. + input.skipFully(640); + FlacStreamMetadataHolder metadataHolder = new FlacStreamMetadataHolder(buildStreamMetadata()); + long originalSampleRate = metadataHolder.flacStreamMetadata.sampleRate; + + FlacMetadataReader.readMetadataBlock(input, metadataHolder); + + assertThat(metadataHolder.flacStreamMetadata).isNotNull(); + // Check that metadata passed has not been erased. + assertThat(metadataHolder.flacStreamMetadata.sampleRate).isEqualTo(originalSampleRate); + Metadata metadata = + metadataHolder.flacStreamMetadata.getMetadataCopyWithAppendedEntriesFrom(null); + assertThat(metadata).isNotNull(); + VorbisComment vorbisComment = (VorbisComment) metadata.get(0); + assertThat(vorbisComment.key).isEqualTo("TITLE"); + assertThat(vorbisComment.value).isEqualTo("test title"); + } + + @Test + public void readMetadataBlock_pictureBlock_updatesStreamMetadata() throws Exception { + ExtractorInput input = buildExtractorInput("flac/bear_with_picture.flac"); + // Skip to picture block. + input.skipFully(640); + FlacStreamMetadataHolder metadataHolder = new FlacStreamMetadataHolder(buildStreamMetadata()); + long originalSampleRate = metadataHolder.flacStreamMetadata.sampleRate; + + FlacMetadataReader.readMetadataBlock(input, metadataHolder); + + assertThat(metadataHolder.flacStreamMetadata).isNotNull(); + // Check that metadata passed has not been erased. + assertThat(metadataHolder.flacStreamMetadata.sampleRate).isEqualTo(originalSampleRate); + Metadata metadata = + metadataHolder.flacStreamMetadata.getMetadataCopyWithAppendedEntriesFrom(null); + assertThat(metadata).isNotNull(); + PictureFrame pictureFrame = (PictureFrame) metadata.get(0); + assertThat(pictureFrame.pictureType).isEqualTo(3); + assertThat(pictureFrame.mimeType).isEqualTo("image/png"); + assertThat(pictureFrame.description).isEqualTo(""); + assertThat(pictureFrame.width).isEqualTo(371); + assertThat(pictureFrame.height).isEqualTo(320); + assertThat(pictureFrame.depth).isEqualTo(24); + assertThat(pictureFrame.colors).isEqualTo(0); + assertThat(pictureFrame.pictureData).hasLength(30943); + } + + @Test + public void readMetadataBlock_blockToSkip_updatesReadPosition() throws Exception { + ExtractorInput input = buildExtractorInput("flac/bear.flac"); + // Skip to padding block. + input.skipFully(640); + FlacStreamMetadataHolder metadataHolder = new FlacStreamMetadataHolder(buildStreamMetadata()); + + FlacMetadataReader.readMetadataBlock(input, metadataHolder); + + assertThat(input.getPosition()).isGreaterThan(640); + assertThat(input.getPeekPosition()).isEqualTo(input.getPosition()); + } + + @Test + public void readMetadataBlock_nonStreamInfoBlockWithNullStreamMetadata_throwsException() + throws Exception { + ExtractorInput input = buildExtractorInput("flac/bear.flac"); + // Skip to seek table block. + input.skipFully(FlacConstants.STREAM_MARKER_SIZE + FlacConstants.STREAM_INFO_BLOCK_SIZE); + + assertThrows( + IllegalArgumentException.class, + () -> + FlacMetadataReader.readMetadataBlock( + input, new FlacStreamMetadataHolder(/* flacStreamMetadata= */ null))); + } + + @Test + public void readSeekTableMetadataBlock_updatesPosition() throws Exception { + ExtractorInput input = buildExtractorInput("flac/bear.flac"); + // Skip to seek table block. + input.skipFully(FlacConstants.STREAM_MARKER_SIZE + FlacConstants.STREAM_INFO_BLOCK_SIZE); + int seekTableBlockSize = 598; + ParsableByteArray scratch = new ParsableByteArray(seekTableBlockSize); + input.read(scratch.data, 0, seekTableBlockSize); + + FlacMetadataReader.readSeekTableMetadataBlock(scratch); + + assertThat(scratch.getPosition()).isEqualTo(seekTableBlockSize); + } + + @Test + public void readSeekTableMetadataBlock_returnsCorrectSeekPoints() throws Exception { + ExtractorInput input = buildExtractorInput("flac/bear.flac"); + // Skip to seek table block. + input.skipFully(FlacConstants.STREAM_MARKER_SIZE + FlacConstants.STREAM_INFO_BLOCK_SIZE); + int seekTableBlockSize = 598; + ParsableByteArray scratch = new ParsableByteArray(seekTableBlockSize); + input.read(scratch.data, 0, seekTableBlockSize); + + FlacStreamMetadata.SeekTable seekTable = FlacMetadataReader.readSeekTableMetadataBlock(scratch); + + assertThat(seekTable.pointOffsets[0]).isEqualTo(0); + assertThat(seekTable.pointSampleNumbers[0]).isEqualTo(0); + assertThat(seekTable.pointOffsets[31]).isEqualTo(160602); + assertThat(seekTable.pointSampleNumbers[31]).isEqualTo(126976); + } + + @Test + public void readSeekTableMetadataBlock_ignoresPlaceholders() throws IOException { + byte[] fileData = + TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), "flac/bear.flac"); + ParsableByteArray scratch = new ParsableByteArray(fileData); + // Skip to seek table block. + scratch.skipBytes(FlacConstants.STREAM_MARKER_SIZE + FlacConstants.STREAM_INFO_BLOCK_SIZE); + + FlacStreamMetadata.SeekTable seekTable = FlacMetadataReader.readSeekTableMetadataBlock(scratch); + + // Seek point at index 32 is a placeholder. + assertThat(seekTable.pointSampleNumbers).hasLength(32); + } + + @Test + public void getFrameStartMarker_doesNotUpdateReadPositionAndAlignsPeekPosition() + throws Exception { + ExtractorInput input = buildExtractorInput("flac/bear.flac"); + int firstFramePosition = 8880; + input.skipFully(firstFramePosition); + // Advance the peek position after the frame start marker. + input.advancePeekPosition(3); + + FlacMetadataReader.getFrameStartMarker(input); + + assertThat(input.getPosition()).isEqualTo(firstFramePosition); + assertThat(input.getPeekPosition()).isEqualTo(input.getPosition()); + } + + @Test + public void getFrameStartMarker_returnsCorrectFrameStartMarker() throws Exception { + ExtractorInput input = buildExtractorInput("flac/bear.flac"); + // Skip to first frame. + input.skipFully(8880); + + int result = FlacMetadataReader.getFrameStartMarker(input); + + assertThat(result).isEqualTo(0xFFF8); + } + + @Test + public void getFrameStartMarker_invalidData_throwsException() throws Exception { + ExtractorInput input = buildExtractorInput("flac/bear.flac"); + + // Input position is incorrect. + assertThrows(ParserException.class, () -> FlacMetadataReader.getFrameStartMarker(input)); + } + + private static ExtractorInput buildExtractorInput(String file) throws IOException { + byte[] fileData = TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), file); + return new FakeExtractorInput.Builder().setData(fileData).build(); + } + + private static FlacStreamMetadata buildStreamMetadata() { + return new FlacStreamMetadata( + /* minBlockSizeSamples= */ 10, + /* maxBlockSizeSamples= */ 20, + /* minFrameSize= */ 5, + /* maxFrameSize= */ 10, + /* sampleRate= */ 44100, + /* channels= */ 2, + /* bitsPerSample= */ 8, + /* totalSamples= */ 1000, + /* vorbisComments= */ new ArrayList<>(), + /* pictureFrames= */ new ArrayList<>()); + } +} diff --git a/library/core/src/test/java/com/google/android/exoplayer2/util/FlacStreamMetadataTest.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/FlacStreamMetadataTest.java similarity index 78% rename from library/core/src/test/java/com/google/android/exoplayer2/util/FlacStreamMetadataTest.java rename to library/extractor/src/test/java/com/google/android/exoplayer2/extractor/FlacStreamMetadataTest.java index d3d3e53458..482781e615 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/util/FlacStreamMetadataTest.java +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/FlacStreamMetadataTest.java @@ -13,13 +13,17 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.google.android.exoplayer2.util; +package com.google.android.exoplayer2.extractor; import static com.google.common.truth.Truth.assertThat; +import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.flac.VorbisComment; +import com.google.android.exoplayer2.testutil.TestUtil; +import com.google.android.exoplayer2.util.FlacConstants; +import java.io.IOException; import java.util.ArrayList; import org.junit.Test; import org.junit.runner.RunWith; @@ -28,6 +32,27 @@ import org.junit.runner.RunWith; @RunWith(AndroidJUnit4.class) public final class FlacStreamMetadataTest { + @Test + public void constructFromByteArray_setsFieldsCorrectly() throws IOException { + byte[] fileData = + TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), "flac/bear.flac"); + + FlacStreamMetadata streamMetadata = + new FlacStreamMetadata( + fileData, FlacConstants.STREAM_MARKER_SIZE + FlacConstants.METADATA_BLOCK_HEADER_SIZE); + + assertThat(streamMetadata.minBlockSizeSamples).isEqualTo(4096); + assertThat(streamMetadata.maxBlockSizeSamples).isEqualTo(4096); + assertThat(streamMetadata.minFrameSize).isEqualTo(445); + assertThat(streamMetadata.maxFrameSize).isEqualTo(5776); + assertThat(streamMetadata.sampleRate).isEqualTo(48000); + assertThat(streamMetadata.sampleRateLookupKey).isEqualTo(10); + assertThat(streamMetadata.channels).isEqualTo(2); + assertThat(streamMetadata.bitsPerSample).isEqualTo(16); + assertThat(streamMetadata.bitsPerSampleLookupKey).isEqualTo(4); + assertThat(streamMetadata.totalSamples).isEqualTo(131568); + } + @Test public void parseVorbisComments() { ArrayList commentsList = new ArrayList<>(); @@ -75,7 +100,7 @@ public final class FlacStreamMetadataTest { /* pictureFrames= */ new ArrayList<>()) .getMetadataCopyWithAppendedEntriesFrom(/* other= */ null); - assertThat(metadata.length()).isEqualTo(0); + assertThat(metadata).isNull(); } @Test diff --git a/library/core/src/test/java/com/google/android/exoplayer2/extractor/Id3PeekerTest.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/Id3PeekerTest.java similarity index 59% rename from library/core/src/test/java/com/google/android/exoplayer2/extractor/Id3PeekerTest.java rename to library/extractor/src/test/java/com/google/android/exoplayer2/extractor/Id3PeekerTest.java index 59e904a5a4..2c7d7ad722 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/extractor/Id3PeekerTest.java +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/Id3PeekerTest.java @@ -16,13 +16,15 @@ package com.google.android.exoplayer2.extractor; +import static com.google.android.exoplayer2.testutil.TestUtil.getByteArray; import static com.google.common.truth.Truth.assertThat; +import androidx.annotation.Nullable; +import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; 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.Id3DecoderTest; import com.google.android.exoplayer2.testutil.FakeExtractorInput; import java.io.IOException; import org.junit.Test; @@ -33,33 +35,27 @@ import org.junit.runner.RunWith; public final class Id3PeekerTest { @Test - public void testPeekId3Data_returnNull_ifId3TagNotPresentAtBeginningOfInput() - throws IOException, InterruptedException { + public void peekId3Data_returnNull_ifId3TagNotPresentAtBeginningOfInput() throws IOException { Id3Peeker id3Peeker = new Id3Peeker(); FakeExtractorInput input = new FakeExtractorInput.Builder() .setData(new byte[] {1, 'I', 'D', '3', 2, 3, 4, 5, 6, 7, 8, 9, 10}) .build(); - Metadata metadata = id3Peeker.peekId3Data(input, /* id3FramePredicate= */ null); + @Nullable Metadata metadata = id3Peeker.peekId3Data(input, /* id3FramePredicate= */ null); assertThat(metadata).isNull(); } @Test - public void testPeekId3Data_returnId3Tag_ifId3TagPresent() - throws IOException, InterruptedException { + public void peekId3Data_returnId3Tag_ifId3TagPresent() throws IOException { Id3Peeker id3Peeker = new Id3Peeker(); + FakeExtractorInput input = + new FakeExtractorInput.Builder() + .setData(getByteArray(ApplicationProvider.getApplicationContext(), "id3/apic.id3")) + .build(); - byte[] rawId3 = - Id3DecoderTest.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 - }); - FakeExtractorInput input = new FakeExtractorInput.Builder().setData(rawId3).build(); - - Metadata metadata = id3Peeker.peekId3Data(input, /* id3FramePredicate= */ null); + @Nullable Metadata metadata = id3Peeker.peekId3Data(input, /* id3FramePredicate= */ null); + assertThat(metadata).isNotNull(); assertThat(metadata.length()).isEqualTo(1); ApicFrame apicFrame = (ApicFrame) metadata.get(0); @@ -71,31 +67,21 @@ public final class Id3PeekerTest { } @Test - public void testPeekId3Data_returnId3TagAccordingToGivenPredicate_ifId3TagPresent() - throws IOException, InterruptedException { + public void peekId3Data_returnId3TagAccordingToGivenPredicate_ifId3TagPresent() + throws IOException { Id3Peeker id3Peeker = new Id3Peeker(); + FakeExtractorInput input = + new FakeExtractorInput.Builder() + .setData(getByteArray(ApplicationProvider.getApplicationContext(), "id3/comm_apic.id3")) + .build(); - byte[] rawId3 = - Id3DecoderTest.buildMultiFramesTag( - new Id3DecoderTest.FrameSpec( - "COMM", - new byte[] { - 3, 101, 110, 103, 100, 101, 115, 99, 114, 105, 112, 116, 105, 111, 110, 0, 116, - 101, 120, 116, 0 - }), - new Id3DecoderTest.FrameSpec( - "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 - })); - FakeExtractorInput input = new FakeExtractorInput.Builder().setData(rawId3).build(); - + @Nullable Metadata metadata = id3Peeker.peekId3Data( input, (majorVersion, id0, id1, id2, id3) -> id0 == 'C' && id1 == 'O' && id2 == 'M' && id3 == 'M'); + assertThat(metadata).isNotNull(); assertThat(metadata.length()).isEqualTo(1); CommentFrame commentFrame = (CommentFrame) metadata.get(0); diff --git a/library/core/src/test/java/com/google/android/exoplayer2/extractor/VorbisBitArrayTest.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/VorbisBitArrayTest.java similarity index 92% rename from library/core/src/test/java/com/google/android/exoplayer2/extractor/VorbisBitArrayTest.java rename to library/extractor/src/test/java/com/google/android/exoplayer2/extractor/VorbisBitArrayTest.java index b05cdd863c..6d29af5127 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/extractor/VorbisBitArrayTest.java +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/VorbisBitArrayTest.java @@ -27,7 +27,7 @@ import org.junit.runner.RunWith; public final class VorbisBitArrayTest { @Test - public void testReadBit() { + public void readBit() { VorbisBitArray bitArray = new VorbisBitArray(TestUtil.createByteArray(0x5c, 0x50)); assertThat(bitArray.readBit()).isFalse(); assertThat(bitArray.readBit()).isFalse(); @@ -48,7 +48,7 @@ public final class VorbisBitArrayTest { } @Test - public void testSkipBits() { + public void skipBits() { VorbisBitArray bitArray = new VorbisBitArray(TestUtil.createByteArray(0xF0, 0x0F)); bitArray.skipBits(10); assertThat(bitArray.getPosition()).isEqualTo(10); @@ -62,7 +62,7 @@ public final class VorbisBitArrayTest { } @Test - public void testGetPosition() throws Exception { + public void getPosition() throws Exception { VorbisBitArray bitArray = new VorbisBitArray(TestUtil.createByteArray(0xF0, 0x0F)); assertThat(bitArray.getPosition()).isEqualTo(0); bitArray.readBit(); @@ -74,7 +74,7 @@ public final class VorbisBitArrayTest { } @Test - public void testSetPosition() throws Exception { + public void setPosition() throws Exception { VorbisBitArray bitArray = new VorbisBitArray(TestUtil.createByteArray(0xF0, 0x0F)); assertThat(bitArray.getPosition()).isEqualTo(0); bitArray.setPosition(4); @@ -84,7 +84,7 @@ public final class VorbisBitArrayTest { } @Test - public void testReadInt32() { + public void readInt32() { VorbisBitArray bitArray = new VorbisBitArray(TestUtil.createByteArray(0xF0, 0x0F, 0xF0, 0x0F)); assertThat(bitArray.readBits(32)).isEqualTo(0x0FF00FF0); bitArray = new VorbisBitArray(TestUtil.createByteArray(0x0F, 0xF0, 0x0F, 0xF0)); @@ -92,7 +92,7 @@ public final class VorbisBitArrayTest { } @Test - public void testReadBits() throws Exception { + public void readBits() throws Exception { VorbisBitArray bitArray = new VorbisBitArray(TestUtil.createByteArray(0x03, 0x22)); assertThat(bitArray.readBits(2)).isEqualTo(3); bitArray.skipBits(6); @@ -104,7 +104,7 @@ public final class VorbisBitArrayTest { } @Test - public void testRead4BitsBeyondBoundary() throws Exception { + public void read4BitsBeyondBoundary() throws Exception { VorbisBitArray bitArray = new VorbisBitArray(TestUtil.createByteArray(0x2e, 0x10)); assertThat(bitArray.readBits(7)).isEqualTo(0x2e); assertThat(bitArray.getPosition()).isEqualTo(7); @@ -112,7 +112,7 @@ public final class VorbisBitArrayTest { } @Test - public void testReadBitsBeyondByteBoundaries() throws Exception { + public void readBitsBeyondByteBoundaries() throws Exception { VorbisBitArray bitArray = new VorbisBitArray(TestUtil.createByteArray(0xFF, 0x0F, 0xFF, 0x0F)); assertThat(bitArray.readBits(32)).isEqualTo(0x0FFF0FFF); @@ -137,7 +137,7 @@ public final class VorbisBitArrayTest { } @Test - public void testReadBitsIllegalLengths() throws Exception { + public void readBitsIllegalLengths() throws Exception { VorbisBitArray bitArray = new VorbisBitArray(TestUtil.createByteArray(0x03, 0x22, 0x30)); // reading zero bits gets 0 without advancing position diff --git a/library/core/src/test/java/com/google/android/exoplayer2/extractor/VorbisUtilTest.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/VorbisUtilTest.java similarity index 86% rename from library/core/src/test/java/com/google/android/exoplayer2/extractor/VorbisUtilTest.java rename to library/extractor/src/test/java/com/google/android/exoplayer2/extractor/VorbisUtilTest.java index 15add339bd..67ac6bd1cc 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/extractor/VorbisUtilTest.java +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/VorbisUtilTest.java @@ -34,7 +34,7 @@ import org.junit.runner.RunWith; public final class VorbisUtilTest { @Test - public void testILog() throws Exception { + public void iLog_returnsHighestSetBit() { assertThat(iLog(0)).isEqualTo(0); assertThat(iLog(1)).isEqualTo(1); assertThat(iLog(2)).isEqualTo(2); @@ -47,7 +47,7 @@ public final class VorbisUtilTest { } @Test - public void testReadIdHeader() throws Exception { + public void readIdHeader() throws Exception { byte[] data = TestUtil.getByteArray( ApplicationProvider.getApplicationContext(), "binary/vorbis/id_header"); @@ -61,14 +61,13 @@ public final class VorbisUtilTest { assertThat(vorbisIdHeader.channels).isEqualTo(2); assertThat(vorbisIdHeader.blockSize0).isEqualTo(512); assertThat(vorbisIdHeader.blockSize1).isEqualTo(1024); - assertThat(vorbisIdHeader.bitrateMax).isEqualTo(-1); - assertThat(vorbisIdHeader.bitrateMin).isEqualTo(-1); + assertThat(vorbisIdHeader.bitrateMaximum).isEqualTo(-1); + assertThat(vorbisIdHeader.bitrateMinimum).isEqualTo(-1); assertThat(vorbisIdHeader.bitrateNominal).isEqualTo(66666); - assertThat(vorbisIdHeader.getApproximateBitrate()).isEqualTo(66666); } @Test - public void testReadCommentHeader() throws IOException { + public void readCommentHeader() throws IOException { byte[] data = TestUtil.getByteArray( ApplicationProvider.getApplicationContext(), "binary/vorbis/comment_header"); @@ -83,7 +82,7 @@ public final class VorbisUtilTest { } @Test - public void testReadVorbisModes() throws IOException { + public void readVorbisModes() throws IOException { byte[] data = TestUtil.getByteArray( ApplicationProvider.getApplicationContext(), "binary/vorbis/setup_header"); @@ -102,14 +101,15 @@ public final class VorbisUtilTest { } @Test - public void testVerifyVorbisHeaderCapturePattern() throws ParserException { + public void verifyVorbisHeaderCapturePattern_withValidHeader_returnsTrue() + throws ParserException { ParsableByteArray header = new ParsableByteArray( new byte[] {0x01, 'v', 'o', 'r', 'b', 'i', 's'}); assertThat(verifyVorbisHeaderCapturePattern(0x01, header, false)).isTrue(); } @Test - public void testVerifyVorbisHeaderCapturePatternInvalidHeader() { + public void verifyVorbisHeaderCapturePattern_withValidHeader_returnsFalse() { ParsableByteArray header = new ParsableByteArray( new byte[] {0x01, 'v', 'o', 'r', 'b', 'i', 's'}); try { @@ -121,14 +121,15 @@ public final class VorbisUtilTest { } @Test - public void testVerifyVorbisHeaderCapturePatternInvalidHeaderQuite() throws ParserException { + public void verifyVorbisHeaderCapturePattern_withInvalidHeaderQuite_returnsFalse() + throws ParserException { ParsableByteArray header = new ParsableByteArray( new byte[] {0x01, 'v', 'o', 'r', 'b', 'i', 's'}); assertThat(verifyVorbisHeaderCapturePattern(0x99, header, true)).isFalse(); } @Test - public void testVerifyVorbisHeaderCapturePatternInvalidPattern() { + public void verifyVorbisHeaderCapturePattern_withInvalidPattern_returnsFalse() { ParsableByteArray header = new ParsableByteArray( new byte[] {0x01, 'x', 'v', 'o', 'r', 'b', 'i', 's'}); try { @@ -140,7 +141,7 @@ public final class VorbisUtilTest { } @Test - public void testVerifyVorbisHeaderCapturePatternQuiteInvalidPatternQuite() + public void verifyVorbisHeaderCapturePatternQuite_withInvalidPatternQuite_returnsFalse() throws ParserException { ParsableByteArray header = new ParsableByteArray( new byte[] {0x01, 'x', 'v', 'o', 'r', 'b', 'i', 's'}); diff --git a/library/core/src/test/java/com/google/android/exoplayer2/extractor/amr/AmrExtractorTest.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/amr/AmrExtractorNonParameterizedTest.java similarity index 76% rename from library/core/src/test/java/com/google/android/exoplayer2/extractor/amr/AmrExtractorTest.java rename to library/extractor/src/test/java/com/google/android/exoplayer2/extractor/amr/AmrExtractorNonParameterizedTest.java index 7d6917d092..65b8122c4a 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/extractor/amr/AmrExtractorTest.java +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/amr/AmrExtractorNonParameterizedTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018 The Android Open Source Project + * Copyright (C) 2020 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. @@ -12,6 +12,7 @@ * 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; @@ -35,14 +36,19 @@ import java.util.Random; import org.junit.Test; import org.junit.runner.RunWith; -/** Unit test for {@link AmrExtractor}. */ +/** + * Tests for {@link AmrExtractor} that test specific behaviours and don't need to be parameterized. + * + *

      For parameterized tests using {@link ExtractorAsserts} see {@link + * AmrExtractorParameterizedTest}. + */ @RunWith(AndroidJUnit4.class) -public final class AmrExtractorTest { +public final class AmrExtractorNonParameterizedTest { private static final Random RANDOM = new Random(1234); @Test - public void testSniff_nonAmrSignature_returnFalse() throws IOException, InterruptedException { + public void sniff_nonAmrSignature_returnFalse() throws IOException { AmrExtractor amrExtractor = setupAmrExtractorWithOutput(); FakeExtractorInput input = fakeExtractorInputWithData(Util.getUtf8Bytes("0#!AMR\n123")); @@ -51,8 +57,7 @@ public final class AmrExtractorTest { } @Test - public void testRead_nonAmrSignature_throwParserException() - throws IOException, InterruptedException { + public void read_nonAmrSignature_throwParserException() throws IOException { AmrExtractor amrExtractor = setupAmrExtractorWithOutput(); FakeExtractorInput input = fakeExtractorInputWithData(Util.getUtf8Bytes("0#!AMR-WB\n")); @@ -65,8 +70,7 @@ public final class AmrExtractorTest { } @Test - public void testRead_amrNb_returnParserException_forInvalidFrameType() - throws IOException, InterruptedException { + public void read_amrNb_returnParserException_forInvalidFrameType() throws IOException { AmrExtractor amrExtractor = setupAmrExtractorWithOutput(); // Frame type 12-14 for narrow band is reserved for future usage. @@ -83,8 +87,7 @@ public final class AmrExtractorTest { } @Test - public void testRead_amrWb_returnParserException_forInvalidFrameType() - throws IOException, InterruptedException { + public void read_amrWb_returnParserException_forInvalidFrameType() throws IOException { AmrExtractor amrExtractor = setupAmrExtractorWithOutput(); // Frame type 10-13 for wide band is reserved for future usage. @@ -101,8 +104,7 @@ public final class AmrExtractorTest { } @Test - public void testRead_amrNb_returnEndOfInput_ifInputEncountersEoF() - throws IOException, InterruptedException { + public void read_amrNb_returnEndOfInput_ifInputEncountersEoF() throws IOException { AmrExtractor amrExtractor = setupAmrExtractorWithOutput(); byte[] amrFrame = newNarrowBandAmrFrameWithType(3); @@ -117,8 +119,7 @@ public final class AmrExtractorTest { } @Test - public void testRead_amrWb_returnEndOfInput_ifInputEncountersEoF() - throws IOException, InterruptedException { + public void read_amrWb_returnEndOfInput_ifInputEncountersEoF() throws IOException { AmrExtractor amrExtractor = setupAmrExtractorWithOutput(); byte[] amrFrame = newWideBandAmrFrameWithType(5); @@ -133,8 +134,7 @@ public final class AmrExtractorTest { } @Test - public void testRead_amrNb_returnParserException_forInvalidFrameHeader() - throws IOException, InterruptedException { + public void read_amrNb_returnParserException_forInvalidFrameHeader() throws IOException { AmrExtractor amrExtractor = setupAmrExtractorWithOutput(); byte[] invalidHeaderFrame = newNarrowBandAmrFrameWithType(4); @@ -155,8 +155,7 @@ public final class AmrExtractorTest { } @Test - public void testRead_amrWb_returnParserException_forInvalidFrameHeader() - throws IOException, InterruptedException { + public void read_amrWb_returnParserException_forInvalidFrameHeader() throws IOException { AmrExtractor amrExtractor = setupAmrExtractorWithOutput(); byte[] invalidHeaderFrame = newWideBandAmrFrameWithType(6); @@ -176,30 +175,6 @@ public final class AmrExtractorTest { } } - @Test - public void testExtractingNarrowBandSamples() throws Exception { - ExtractorAsserts.assertBehavior( - createAmrExtractorFactory(/* withSeeking= */ false), "amr/sample_nb.amr"); - } - - @Test - public void testExtractingWideBandSamples() throws Exception { - 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) { byte frameHeader = (byte) ((frameType << 3) & (0b01111100)); int frameContentInBytes = frameSizeBytesByTypeWb(frameType) - 1; @@ -244,14 +219,4 @@ public final class AmrExtractorTest { private static FakeExtractorInput fakeExtractorInputWithData(byte[] data) { return new FakeExtractorInput.Builder().setData(data).build(); } - - private static ExtractorAsserts.ExtractorFactory createAmrExtractorFactory(boolean withSeeking) { - return () -> { - if (!withSeeking) { - return new AmrExtractor(); - } else { - return new AmrExtractor(AmrExtractor.FLAG_ENABLE_CONSTANT_BITRATE_SEEKING); - } - }; - } } diff --git a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/amr/AmrExtractorParameterizedTest.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/amr/AmrExtractorParameterizedTest.java new file mode 100644 index 0000000000..e79020e5c6 --- /dev/null +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/amr/AmrExtractorParameterizedTest.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.amr; + +import com.google.android.exoplayer2.testutil.ExtractorAsserts; +import java.util.List; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.ParameterizedRobolectricTestRunner; +import org.robolectric.ParameterizedRobolectricTestRunner.Parameter; +import org.robolectric.ParameterizedRobolectricTestRunner.Parameters; + +/** + * Unit tests for {@link AmrExtractor} that use parameterization to test a range of behaviours. + * + *

      For non-parameterized tests see {@link AmrExtractorSeekTest} and {@link + * AmrExtractorNonParameterizedTest}. + */ +@RunWith(ParameterizedRobolectricTestRunner.class) +public final class AmrExtractorParameterizedTest { + + @Parameters(name = "{0}") + public static List params() { + return ExtractorAsserts.configs(); + } + + @Parameter(0) + public ExtractorAsserts.SimulationConfig simulationConfig; + + @Test + public void extractingNarrowBandSamples() throws Exception { + ExtractorAsserts.assertBehavior( + createAmrExtractorFactory(/* withSeeking= */ false), "amr/sample_nb.amr", simulationConfig); + } + + @Test + public void extractingWideBandSamples() throws Exception { + ExtractorAsserts.assertBehavior( + createAmrExtractorFactory(/* withSeeking= */ false), "amr/sample_wb.amr", simulationConfig); + } + + @Test + public void extractingNarrowBandSamples_withSeeking() throws Exception { + ExtractorAsserts.assertBehavior( + createAmrExtractorFactory(/* withSeeking= */ true), + "amr/sample_nb_cbr.amr", + simulationConfig); + } + + @Test + public void extractingWideBandSamples_withSeeking() throws Exception { + ExtractorAsserts.assertBehavior( + createAmrExtractorFactory(/* withSeeking= */ true), + "amr/sample_wb_cbr.amr", + simulationConfig); + } + + + private static ExtractorAsserts.ExtractorFactory createAmrExtractorFactory(boolean withSeeking) { + return () -> { + 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/amr/AmrExtractorSeekTest.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/amr/AmrExtractorSeekTest.java similarity index 90% rename from library/core/src/test/java/com/google/android/exoplayer2/extractor/amr/AmrExtractorSeekTest.java rename to library/extractor/src/test/java/com/google/android/exoplayer2/extractor/amr/AmrExtractorSeekTest.java index d131fce6b7..42e9f93c00 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/extractor/amr/AmrExtractorSeekTest.java +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/amr/AmrExtractorSeekTest.java @@ -33,7 +33,7 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; -/** Unit test for {@link AmrExtractor}. */ +/** Unit tests for {@link AmrExtractor} seeking behaviour. */ @RunWith(AndroidJUnit4.class) public final class AmrExtractorSeekTest { @@ -56,8 +56,7 @@ public final class AmrExtractorSeekTest { } @Test - public void testAmrExtractorReads_returnSeekableSeekMap_forNarrowBandAmr() - throws IOException, InterruptedException { + public void amrExtractorReads_returnSeekableSeekMap_forNarrowBandAmr() throws IOException { String fileName = NARROW_BAND_AMR_FILE; Uri fileUri = TestUtil.buildAssetUri(fileName); expectedTrackOutput = @@ -76,8 +75,8 @@ public final class AmrExtractorSeekTest { } @Test - public void testSeeking_handlesSeekingToPositionInFile_extractsCorrectFrame_forNarrowBandAmr() - throws IOException, InterruptedException { + public void seeking_handlesSeekingToPositionInFile_extractsCorrectFrame_forNarrowBandAmr() + throws IOException { String fileName = NARROW_BAND_AMR_FILE; Uri fileUri = TestUtil.buildAssetUri(fileName); expectedTrackOutput = @@ -103,8 +102,7 @@ public final class AmrExtractorSeekTest { } @Test - public void testSeeking_handlesSeekToEoF_extractsLastFrame_forNarrowBandAmr() - throws IOException, InterruptedException { + public void seeking_handlesSeekToEoF_extractsLastFrame_forNarrowBandAmr() throws IOException { String fileName = NARROW_BAND_AMR_FILE; Uri fileUri = TestUtil.buildAssetUri(fileName); expectedTrackOutput = @@ -130,8 +128,8 @@ public final class AmrExtractorSeekTest { } @Test - public void testSeeking_handlesSeekingBackward_extractsCorrectFrames_forNarrowBandAmr() - throws IOException, InterruptedException { + public void seeking_handlesSeekingBackward_extractsCorrectFrames_forNarrowBandAmr() + throws IOException { String fileName = NARROW_BAND_AMR_FILE; Uri fileUri = TestUtil.buildAssetUri(fileName); expectedTrackOutput = @@ -159,8 +157,8 @@ public final class AmrExtractorSeekTest { } @Test - public void testSeeking_handlesSeekingForward_extractsCorrectFrames_forNarrowBandAmr() - throws IOException, InterruptedException { + public void seeking_handlesSeekingForward_extractsCorrectFrames_forNarrowBandAmr() + throws IOException { String fileName = NARROW_BAND_AMR_FILE; Uri fileUri = TestUtil.buildAssetUri(fileName); expectedTrackOutput = @@ -188,8 +186,8 @@ public final class AmrExtractorSeekTest { } @Test - public void testSeeking_handlesRandomSeeks_extractsCorrectFrames_forNarrowBandAmr() - throws IOException, InterruptedException { + public void seeking_handlesRandomSeeks_extractsCorrectFrames_forNarrowBandAmr() + throws IOException { String fileName = NARROW_BAND_AMR_FILE; Uri fileUri = TestUtil.buildAssetUri(fileName); expectedTrackOutput = @@ -217,8 +215,7 @@ public final class AmrExtractorSeekTest { } @Test - public void testAmrExtractorReads_returnSeekableSeekMap_forWideBandAmr() - throws IOException, InterruptedException { + public void amrExtractorReads_returnSeekableSeekMap_forWideBandAmr() throws IOException { String fileName = WIDE_BAND_AMR_FILE; Uri fileUri = TestUtil.buildAssetUri(fileName); expectedTrackOutput = @@ -237,8 +234,8 @@ public final class AmrExtractorSeekTest { } @Test - public void testSeeking_handlesSeekingToPositionInFile_extractsCorrectFrame_forWideBandAmr() - throws IOException, InterruptedException { + public void seeking_handlesSeekingToPositionInFile_extractsCorrectFrame_forWideBandAmr() + throws IOException { String fileName = WIDE_BAND_AMR_FILE; Uri fileUri = TestUtil.buildAssetUri(fileName); expectedTrackOutput = @@ -264,8 +261,7 @@ public final class AmrExtractorSeekTest { } @Test - public void testSeeking_handlesSeekToEoF_extractsLastFrame_forWideBandAmr() - throws IOException, InterruptedException { + public void seeking_handlesSeekToEoF_extractsLastFrame_forWideBandAmr() throws IOException { String fileName = WIDE_BAND_AMR_FILE; Uri fileUri = TestUtil.buildAssetUri(fileName); expectedTrackOutput = @@ -291,8 +287,8 @@ public final class AmrExtractorSeekTest { } @Test - public void testSeeking_handlesSeekingBackward_extractsCorrectFrames_forWideBandAmr() - throws IOException, InterruptedException { + public void seeking_handlesSeekingBackward_extractsCorrectFrames_forWideBandAmr() + throws IOException { String fileName = WIDE_BAND_AMR_FILE; Uri fileUri = TestUtil.buildAssetUri(fileName); expectedTrackOutput = @@ -320,7 +316,7 @@ public final class AmrExtractorSeekTest { } @Test - public void testSeeking_handlesSeekingForward_extractsCorrectFrames_forWideBandAmr() + public void seeking_handlesSeekingForward_extractsCorrectFrames_forWideBandAmr() throws IOException, InterruptedException { String fileName = WIDE_BAND_AMR_FILE; Uri fileUri = TestUtil.buildAssetUri(fileName); @@ -349,7 +345,7 @@ public final class AmrExtractorSeekTest { } @Test - public void testSeeking_handlesRandomSeeks_extractsCorrectFrames_forWideBandAmr() + public void seeking_handlesRandomSeeks_extractsCorrectFrames_forWideBandAmr() throws IOException, InterruptedException { String fileName = WIDE_BAND_AMR_FILE; Uri fileUri = TestUtil.buildAssetUri(fileName); diff --git a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/flac/FlacExtractorSeekTest.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/flac/FlacExtractorSeekTest.java new file mode 100644 index 0000000000..99cf464f68 --- /dev/null +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/flac/FlacExtractorSeekTest.java @@ -0,0 +1,286 @@ +/* + * Copyright (C) 2019 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.flac; + +import static com.google.common.truth.Truth.assertThat; + +import android.net.Uri; +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.android.exoplayer2.C; +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.List; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Seeking tests for {@link FlacExtractor}. */ +@RunWith(AndroidJUnit4.class) +public class FlacExtractorSeekTest { + + private static final String TEST_FILE_SEEK_TABLE = "flac/bear.flac"; + private static final String TEST_FILE_BINARY_SEARCH = "flac/bear_one_metadata_block.flac"; + private static final String TEST_FILE_UNSEEKABLE = "flac/bear_no_seek_table_no_num_samples.flac"; + private static final int DURATION_US = 2_741_000; + + private FlacExtractor extractor = new FlacExtractor(); + private FakeExtractorOutput extractorOutput = new FakeExtractorOutput(); + private DefaultDataSource dataSource = + new DefaultDataSourceFactory(ApplicationProvider.getApplicationContext(), "UserAgent") + .createDataSource(); + + @Test + public void flacExtractorReads_seekTable_returnSeekableSeekMap() throws IOException { + Uri fileUri = TestUtil.buildAssetUri(TEST_FILE_SEEK_TABLE); + + SeekMap seekMap = TestUtil.extractSeekMap(extractor, extractorOutput, dataSource, fileUri); + + assertThat(seekMap).isNotNull(); + assertThat(seekMap.getDurationUs()).isEqualTo(DURATION_US); + assertThat(seekMap.isSeekable()).isTrue(); + } + + @Test + public void seeking_seekTable_handlesSeekToZero() throws IOException { + String fileName = TEST_FILE_SEEK_TABLE; + Uri fileUri = TestUtil.buildAssetUri(fileName); + SeekMap seekMap = TestUtil.extractSeekMap(extractor, extractorOutput, dataSource, fileUri); + FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(0); + + long targetSeekTimeUs = 0; + int extractedFrameIndex = + TestUtil.seekToTimeUs( + extractor, seekMap, targetSeekTimeUs, dataSource, trackOutput, fileUri); + + assertThat(extractedFrameIndex).isNotEqualTo(C.INDEX_UNSET); + assertFirstFrameAfterSeekPrecedesTargetSeekTime( + fileName, trackOutput, targetSeekTimeUs, extractedFrameIndex); + } + + @Test + public void seeking_seekTable_handlesSeekToEoF() throws IOException { + String fileName = TEST_FILE_SEEK_TABLE; + Uri fileUri = TestUtil.buildAssetUri(fileName); + 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(C.INDEX_UNSET); + assertFirstFrameAfterSeekPrecedesTargetSeekTime( + fileName, trackOutput, targetSeekTimeUs, extractedFrameIndex); + } + + @Test + public void seeking_seekTable_handlesSeekingBackward() throws IOException { + String fileName = TEST_FILE_SEEK_TABLE; + Uri fileUri = TestUtil.buildAssetUri(fileName); + SeekMap seekMap = TestUtil.extractSeekMap(extractor, extractorOutput, dataSource, fileUri); + FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(0); + + long firstSeekTimeUs = 1_234_000; + TestUtil.seekToTimeUs(extractor, seekMap, firstSeekTimeUs, dataSource, trackOutput, fileUri); + long targetSeekTimeUs = 987_000; + int extractedFrameIndex = + TestUtil.seekToTimeUs( + extractor, seekMap, targetSeekTimeUs, dataSource, trackOutput, fileUri); + + assertThat(extractedFrameIndex).isNotEqualTo(C.INDEX_UNSET); + assertFirstFrameAfterSeekPrecedesTargetSeekTime( + fileName, trackOutput, targetSeekTimeUs, extractedFrameIndex); + } + + @Test + public void seeking_seekTable_handlesSeekingForward() throws IOException { + String fileName = TEST_FILE_SEEK_TABLE; + Uri fileUri = TestUtil.buildAssetUri(fileName); + SeekMap seekMap = TestUtil.extractSeekMap(extractor, extractorOutput, dataSource, fileUri); + FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(0); + + 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(C.INDEX_UNSET); + assertFirstFrameAfterSeekPrecedesTargetSeekTime( + fileName, trackOutput, targetSeekTimeUs, extractedFrameIndex); + } + + @Test + public void flacExtractorReads_binarySearch_returnSeekableSeekMap() throws IOException { + Uri fileUri = TestUtil.buildAssetUri(TEST_FILE_BINARY_SEARCH); + + SeekMap seekMap = TestUtil.extractSeekMap(extractor, extractorOutput, dataSource, fileUri); + + assertThat(seekMap).isNotNull(); + assertThat(seekMap.getDurationUs()).isEqualTo(DURATION_US); + assertThat(seekMap.isSeekable()).isTrue(); + } + + @Test + public void seeking_binarySearch_handlesSeekToZero() throws IOException { + String fileName = TEST_FILE_BINARY_SEARCH; + Uri fileUri = TestUtil.buildAssetUri(fileName); + SeekMap seekMap = TestUtil.extractSeekMap(extractor, extractorOutput, dataSource, fileUri); + FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(0); + + long targetSeekTimeUs = 0; + int extractedFrameIndex = + TestUtil.seekToTimeUs( + extractor, seekMap, targetSeekTimeUs, dataSource, trackOutput, fileUri); + + assertThat(extractedFrameIndex).isNotEqualTo(C.INDEX_UNSET); + assertFirstFrameAfterSeekContainsTargetSeekTime( + fileName, trackOutput, targetSeekTimeUs, extractedFrameIndex); + } + + @Test + public void seeking_binarySearch_handlesSeekToEoF() throws IOException { + String fileName = TEST_FILE_BINARY_SEARCH; + Uri fileUri = TestUtil.buildAssetUri(fileName); + 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(C.INDEX_UNSET); + assertFirstFrameAfterSeekContainsTargetSeekTime( + fileName, trackOutput, targetSeekTimeUs, extractedFrameIndex); + } + + @Test + public void seeking_binarySearch_handlesSeekingBackward() throws IOException { + String fileName = TEST_FILE_BINARY_SEARCH; + Uri fileUri = TestUtil.buildAssetUri(fileName); + SeekMap seekMap = TestUtil.extractSeekMap(extractor, extractorOutput, dataSource, fileUri); + FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(0); + + long firstSeekTimeUs = 1_234_000; + TestUtil.seekToTimeUs(extractor, seekMap, firstSeekTimeUs, dataSource, trackOutput, fileUri); + long targetSeekTimeUs = 987_000; + int extractedFrameIndex = + TestUtil.seekToTimeUs( + extractor, seekMap, targetSeekTimeUs, dataSource, trackOutput, fileUri); + + assertThat(extractedFrameIndex).isNotEqualTo(C.INDEX_UNSET); + assertFirstFrameAfterSeekContainsTargetSeekTime( + fileName, trackOutput, targetSeekTimeUs, extractedFrameIndex); + } + + @Test + public void seeking_binarySearch_handlesSeekingForward() throws IOException { + String fileName = TEST_FILE_BINARY_SEARCH; + Uri fileUri = TestUtil.buildAssetUri(fileName); + SeekMap seekMap = TestUtil.extractSeekMap(extractor, extractorOutput, dataSource, fileUri); + FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(0); + + 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(C.INDEX_UNSET); + assertFirstFrameAfterSeekContainsTargetSeekTime( + fileName, trackOutput, targetSeekTimeUs, extractedFrameIndex); + } + + @Test + public void flacExtractorReads_unseekable_returnUnseekableSeekMap() throws IOException { + Uri fileUri = TestUtil.buildAssetUri(TEST_FILE_UNSEEKABLE); + + SeekMap seekMap = TestUtil.extractSeekMap(extractor, extractorOutput, dataSource, fileUri); + + assertThat(seekMap).isNotNull(); + assertThat(seekMap.getDurationUs()).isEqualTo(C.TIME_UNSET); + assertThat(seekMap.isSeekable()).isFalse(); + } + + private static void assertFirstFrameAfterSeekContainsTargetSeekTime( + String fileName, + FakeTrackOutput trackOutput, + long targetSeekTimeUs, + int firstFrameIndexAfterSeek) + throws IOException { + FakeTrackOutput expectedTrackOutput = getExpectedTrackOutput(fileName); + int expectedFrameIndex = getFrameIndex(expectedTrackOutput, targetSeekTimeUs); + + trackOutput.assertSample( + firstFrameIndexAfterSeek, + expectedTrackOutput.getSampleData(expectedFrameIndex), + expectedTrackOutput.getSampleTimeUs(expectedFrameIndex), + expectedTrackOutput.getSampleFlags(expectedFrameIndex), + expectedTrackOutput.getSampleCryptoData(expectedFrameIndex)); + } + + private static void assertFirstFrameAfterSeekPrecedesTargetSeekTime( + String fileName, + FakeTrackOutput trackOutput, + long targetSeekTimeUs, + int firstFrameIndexAfterSeek) + throws IOException { + FakeTrackOutput expectedTrackOutput = getExpectedTrackOutput(fileName); + int maxFrameIndex = getFrameIndex(expectedTrackOutput, targetSeekTimeUs); + + long firstFrameAfterSeekTimeUs = trackOutput.getSampleTimeUs(firstFrameIndexAfterSeek); + assertThat(firstFrameAfterSeekTimeUs).isAtMost(targetSeekTimeUs); + + boolean frameFound = false; + for (int i = maxFrameIndex; i >= 0; i--) { + if (firstFrameAfterSeekTimeUs == expectedTrackOutput.getSampleTimeUs(i)) { + trackOutput.assertSample( + firstFrameIndexAfterSeek, + expectedTrackOutput.getSampleData(i), + expectedTrackOutput.getSampleTimeUs(i), + expectedTrackOutput.getSampleFlags(i), + expectedTrackOutput.getSampleCryptoData(i)); + frameFound = true; + break; + } + } + + assertThat(frameFound).isTrue(); + } + + private static FakeTrackOutput getExpectedTrackOutput(String fileName) throws IOException { + return TestUtil.extractAllSamplesFromFile( + new FlacExtractor(), ApplicationProvider.getApplicationContext(), fileName) + .trackOutputs + .get(0); + } + + private static int getFrameIndex(FakeTrackOutput expectedTrackOutput, long targetSeekTimeUs) { + List frameTimes = expectedTrackOutput.getSampleTimesUs(); + return Util.binarySearchFloor( + frameTimes, targetSeekTimeUs, /* inclusive= */ true, /* stayInBounds= */ false); + } +} diff --git a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/flac/FlacExtractorTest.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/flac/FlacExtractorTest.java new file mode 100644 index 0000000000..94d1a5d612 --- /dev/null +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/flac/FlacExtractorTest.java @@ -0,0 +1,140 @@ +/* + * Copyright (C) 2019 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.flac; + +import com.google.android.exoplayer2.testutil.ExtractorAsserts; +import com.google.android.exoplayer2.testutil.ExtractorAsserts.AssertionConfig; +import java.util.List; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.ParameterizedRobolectricTestRunner; +import org.robolectric.ParameterizedRobolectricTestRunner.Parameter; +import org.robolectric.ParameterizedRobolectricTestRunner.Parameters; + +/** Unit tests for {@link FlacExtractor}. */ +@RunWith(ParameterizedRobolectricTestRunner.class) +public class FlacExtractorTest { + + @Parameters(name = "{0}") + public static List params() { + return ExtractorAsserts.configs(); + } + + @Parameter(0) + public ExtractorAsserts.SimulationConfig simulationConfig; + + @Test + public void sample() throws Exception { + ExtractorAsserts.assertBehavior( + FlacExtractor::new, + "flac/bear.flac", + new AssertionConfig.Builder().setDumpFilesPrefix("flac/bear_flac").build(), + simulationConfig); + } + + @Test + public void sampleWithId3HeaderAndId3Enabled() throws Exception { + ExtractorAsserts.assertBehavior( + FlacExtractor::new, + "flac/bear_with_id3.flac", + new AssertionConfig.Builder().setDumpFilesPrefix("flac/bear_with_id3_enabled_flac").build(), + simulationConfig); + } + + @Test + public void sampleWithId3HeaderAndId3Disabled() throws Exception { + ExtractorAsserts.assertBehavior( + () -> new FlacExtractor(FlacExtractor.FLAG_DISABLE_ID3_METADATA), + "flac/bear_with_id3.flac", + new AssertionConfig.Builder() + .setDumpFilesPrefix("flac/bear_with_id3_disabled_flac") + .build(), + simulationConfig); + } + + @Test + public void sampleUnseekable() throws Exception { + ExtractorAsserts.assertBehavior( + FlacExtractor::new, + "flac/bear_no_seek_table_no_num_samples.flac", + new AssertionConfig.Builder() + .setDumpFilesPrefix("flac/bear_no_seek_table_no_num_samples_flac") + .build(), + simulationConfig); + } + + @Test + public void sampleWithVorbisComments() throws Exception { + ExtractorAsserts.assertBehavior( + FlacExtractor::new, + "flac/bear_with_vorbis_comments.flac", + new AssertionConfig.Builder() + .setDumpFilesPrefix("flac/bear_with_vorbis_comments_flac") + .build(), + simulationConfig); + } + + @Test + public void sampleWithPicture() throws Exception { + ExtractorAsserts.assertBehavior( + FlacExtractor::new, + "flac/bear_with_picture.flac", + new AssertionConfig.Builder().setDumpFilesPrefix("flac/bear_with_picture_flac").build(), + simulationConfig); + } + + @Test + public void oneMetadataBlock() throws Exception { + ExtractorAsserts.assertBehavior( + FlacExtractor::new, + "flac/bear_one_metadata_block.flac", + new AssertionConfig.Builder() + .setDumpFilesPrefix("flac/bear_one_metadata_block_flac") + .build(), + simulationConfig); + } + + @Test + public void noMinMaxFrameSize() throws Exception { + ExtractorAsserts.assertBehavior( + FlacExtractor::new, + "flac/bear_no_min_max_frame_size.flac", + new AssertionConfig.Builder() + .setDumpFilesPrefix("flac/bear_no_min_max_frame_size_flac") + .build(), + simulationConfig); + } + + @Test + public void noNumSamples() throws Exception { + ExtractorAsserts.assertBehavior( + FlacExtractor::new, + "flac/bear_no_num_samples.flac", + new AssertionConfig.Builder().setDumpFilesPrefix("flac/bear_no_num_samples_flac").build(), + simulationConfig); + } + + @Test + public void uncommonSampleRate() throws Exception { + ExtractorAsserts.assertBehavior( + FlacExtractor::new, + "flac/bear_uncommon_sample_rate.flac", + new AssertionConfig.Builder() + .setDumpFilesPrefix("flac/bear_uncommon_sample_rate_flac") + .build(), + simulationConfig); + } +} diff --git a/library/core/src/test/java/com/google/android/exoplayer2/extractor/flv/FlvExtractorTest.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/flv/FlvExtractorTest.java similarity index 64% rename from library/core/src/test/java/com/google/android/exoplayer2/extractor/flv/FlvExtractorTest.java rename to library/extractor/src/test/java/com/google/android/exoplayer2/extractor/flv/FlvExtractorTest.java index e0505095fe..cde043f2d3 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/extractor/flv/FlvExtractorTest.java +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/flv/FlvExtractorTest.java @@ -15,17 +15,28 @@ */ package com.google.android.exoplayer2.extractor.flv; -import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.testutil.ExtractorAsserts; +import java.util.List; import org.junit.Test; import org.junit.runner.RunWith; +import org.robolectric.ParameterizedRobolectricTestRunner; +import org.robolectric.ParameterizedRobolectricTestRunner.Parameter; +import org.robolectric.ParameterizedRobolectricTestRunner.Parameters; /** Unit test for {@link FlvExtractor}. */ -@RunWith(AndroidJUnit4.class) +@RunWith(ParameterizedRobolectricTestRunner.class) public final class FlvExtractorTest { + @Parameters(name = "{0}") + public static List params() { + return ExtractorAsserts.configs(); + } + + @Parameter(0) + public ExtractorAsserts.SimulationConfig simulationConfig; + @Test - public void testSample() throws Exception { - ExtractorAsserts.assertBehavior(FlvExtractor::new, "flv/sample.flv"); + public void sample() throws Exception { + ExtractorAsserts.assertBehavior(FlvExtractor::new, "flv/sample.flv", simulationConfig); } } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/extractor/mkv/DefaultEbmlReaderTest.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/mkv/DefaultEbmlReaderTest.java similarity index 88% rename from library/core/src/test/java/com/google/android/exoplayer2/extractor/mkv/DefaultEbmlReaderTest.java rename to library/extractor/src/test/java/com/google/android/exoplayer2/extractor/mkv/DefaultEbmlReaderTest.java index 642b9946ed..a8275fd64c 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/extractor/mkv/DefaultEbmlReaderTest.java +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/mkv/DefaultEbmlReaderTest.java @@ -33,7 +33,7 @@ import org.junit.runner.RunWith; public class DefaultEbmlReaderTest { @Test - public void testMasterElement() throws IOException, InterruptedException { + public void masterElement() throws IOException { ExtractorInput input = createTestInput(0x1A, 0x45, 0xDF, 0xA3, 0x84, 0x42, 0x85, 0x81, 0x01); TestProcessor expected = new TestProcessor(); expected.startMasterElement(TestProcessor.ID_EBML, 5, 4); @@ -43,7 +43,7 @@ public class DefaultEbmlReaderTest { } @Test - public void testMasterElementEmpty() throws IOException, InterruptedException { + public void masterElementEmpty() throws IOException { ExtractorInput input = createTestInput(0x18, 0x53, 0x80, 0x67, 0x80); TestProcessor expected = new TestProcessor(); expected.startMasterElement(TestProcessor.ID_SEGMENT, 5, 0); @@ -52,7 +52,7 @@ public class DefaultEbmlReaderTest { } @Test - public void testUnsignedIntegerElement() throws IOException, InterruptedException { + public void unsignedIntegerElement() throws IOException { // 0xFE is chosen because for signed integers it should be interpreted as -2 ExtractorInput input = createTestInput(0x42, 0xF7, 0x81, 0xFE); TestProcessor expected = new TestProcessor(); @@ -61,7 +61,7 @@ public class DefaultEbmlReaderTest { } @Test - public void testUnsignedIntegerElementLarge() throws IOException, InterruptedException { + public void unsignedIntegerElementLarge() throws IOException { ExtractorInput input = createTestInput(0x42, 0xF7, 0x88, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF); TestProcessor expected = new TestProcessor(); @@ -70,8 +70,7 @@ public class DefaultEbmlReaderTest { } @Test - public void testUnsignedIntegerElementTooLargeBecomesNegative() - throws IOException, InterruptedException { + public void unsignedIntegerElementTooLargeBecomesNegative() throws IOException { ExtractorInput input = createTestInput(0x42, 0xF7, 0x88, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF); TestProcessor expected = new TestProcessor(); @@ -80,7 +79,7 @@ public class DefaultEbmlReaderTest { } @Test - public void testStringElement() throws IOException, InterruptedException { + public void stringElement() throws IOException { ExtractorInput input = createTestInput(0x42, 0x82, 0x86, 0x41, 0x62, 0x63, 0x31, 0x32, 0x33); TestProcessor expected = new TestProcessor(); expected.stringElement(TestProcessor.ID_DOC_TYPE, "Abc123"); @@ -88,7 +87,7 @@ public class DefaultEbmlReaderTest { } @Test - public void testStringElementWithZeroPadding() throws IOException, InterruptedException { + public void stringElementWithZeroPadding() throws IOException, InterruptedException { ExtractorInput input = createTestInput(0x42, 0x82, 0x86, 0x41, 0x62, 0x63, 0x00, 0x00, 0x00); TestProcessor expected = new TestProcessor(); expected.stringElement(TestProcessor.ID_DOC_TYPE, "Abc"); @@ -96,7 +95,7 @@ public class DefaultEbmlReaderTest { } @Test - public void testStringElementEmpty() throws IOException, InterruptedException { + public void stringElementEmpty() throws IOException { ExtractorInput input = createTestInput(0x42, 0x82, 0x80); TestProcessor expected = new TestProcessor(); expected.stringElement(TestProcessor.ID_DOC_TYPE, ""); @@ -104,7 +103,7 @@ public class DefaultEbmlReaderTest { } @Test - public void testFloatElementFourBytes() throws IOException, InterruptedException { + public void floatElementFourBytes() throws IOException { ExtractorInput input = createTestInput(0x44, 0x89, 0x84, 0x3F, 0x80, 0x00, 0x00); TestProcessor expected = new TestProcessor(); @@ -113,7 +112,7 @@ public class DefaultEbmlReaderTest { } @Test - public void testFloatElementEightBytes() throws IOException, InterruptedException { + public void floatElementEightBytes() throws IOException { ExtractorInput input = createTestInput(0x44, 0x89, 0x88, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00); TestProcessor expected = new TestProcessor(); @@ -122,7 +121,7 @@ public class DefaultEbmlReaderTest { } @Test - public void testBinaryElement() throws IOException, InterruptedException { + public void binaryElement() throws IOException { ExtractorInput input = createTestInput(0xA3, 0x88, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08); TestProcessor expected = new TestProcessor(); @@ -134,7 +133,7 @@ public class DefaultEbmlReaderTest { } private static void assertEvents(ExtractorInput input, List expectedEvents) - throws IOException, InterruptedException { + throws IOException { DefaultEbmlReader reader = new DefaultEbmlReader(); TestProcessor output = new TestProcessor(); reader.init(output); @@ -232,8 +231,7 @@ public class DefaultEbmlReaderTest { } @Override - public void binaryElement(int id, int contentSize, ExtractorInput input) - throws IOException, InterruptedException { + public void binaryElement(int id, int contentSize, ExtractorInput input) throws IOException { byte[] bytes = new byte[contentSize]; input.readFully(bytes, 0, contentSize); events.add(formatEvent(id, "bytes=" + Arrays.toString(bytes))); diff --git a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractorTest.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractorTest.java new file mode 100644 index 0000000000..f9aafbd1fb --- /dev/null +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractorTest.java @@ -0,0 +1,72 @@ +/* + * 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.extractor.mkv; + +import com.google.android.exoplayer2.testutil.ExtractorAsserts; +import java.util.List; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.ParameterizedRobolectricTestRunner; +import org.robolectric.ParameterizedRobolectricTestRunner.Parameter; +import org.robolectric.ParameterizedRobolectricTestRunner.Parameters; + +/** Tests for {@link MatroskaExtractor}. */ +@RunWith(ParameterizedRobolectricTestRunner.class) +public final class MatroskaExtractorTest { + + @Parameters(name = "{0}") + public static List params() { + return ExtractorAsserts.configs(); + } + + @Parameter(0) + public ExtractorAsserts.SimulationConfig simulationConfig; + + @Test + public void mkvSample() throws Exception { + ExtractorAsserts.assertBehavior(MatroskaExtractor::new, "mkv/sample.mkv", simulationConfig); + } + + @Test + public void mkvSample_withSubripSubtitles() throws Exception { + ExtractorAsserts.assertBehavior( + MatroskaExtractor::new, "mkv/sample_with_srt.mkv", simulationConfig); + } + + @Test + public void mkvSample_withHtcRotationInfoInTrackName() throws Exception { + ExtractorAsserts.assertBehavior( + MatroskaExtractor::new, "mkv/sample_with_htc_rotation_track_name.mkv", simulationConfig); + } + + @Test + public void mkvFullBlocksSample() throws Exception { + ExtractorAsserts.assertBehavior( + MatroskaExtractor::new, "mkv/full_blocks.mkv", simulationConfig); + } + + @Test + public void webmSubsampleEncryption() throws Exception { + ExtractorAsserts.assertBehavior( + MatroskaExtractor::new, "mkv/subsample_encrypted_noaltref.webm", simulationConfig); + } + + @Test + public void webmSubsampleEncryptionWithAltrefFrames() throws Exception { + ExtractorAsserts.assertBehavior( + MatroskaExtractor::new, "mkv/subsample_encrypted_altref.webm", simulationConfig); + } +} diff --git a/library/core/src/test/java/com/google/android/exoplayer2/extractor/mkv/VarintReaderTest.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/mkv/VarintReaderTest.java similarity index 93% rename from library/core/src/test/java/com/google/android/exoplayer2/extractor/mkv/VarintReaderTest.java rename to library/extractor/src/test/java/com/google/android/exoplayer2/extractor/mkv/VarintReaderTest.java index 86df3f50e3..7a3c73e4d5 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/extractor/mkv/VarintReaderTest.java +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/mkv/VarintReaderTest.java @@ -85,7 +85,7 @@ public final class VarintReaderTest { private static final long VALUE_8_BYTE_MAX_WITH_MASK = 0x1FFFFFFFFFFFFFFL; @Test - public void testReadVarintEndOfInputAtStart() throws IOException, InterruptedException { + public void readVarintEndOfInputAtStart() throws IOException { VarintReader reader = new VarintReader(); // Build an input with no data. ExtractorInput input = new FakeExtractorInput.Builder() @@ -104,7 +104,7 @@ public final class VarintReaderTest { } @Test - public void testReadVarintExceedsMaximumAllowedLength() throws IOException, InterruptedException { + public void readVarintExceedsMaximumAllowedLength() throws IOException { VarintReader reader = new VarintReader(); ExtractorInput input = new FakeExtractorInput.Builder() .setData(DATA_8_BYTE_0) @@ -115,7 +115,7 @@ public final class VarintReaderTest { } @Test - public void testReadVarint() throws IOException, InterruptedException { + public void readVarint() throws IOException { VarintReader reader = new VarintReader(); testReadVarint(reader, true, DATA_1_BYTE_0, 1, 0); testReadVarint(reader, true, DATA_2_BYTE_0, 2, 0); @@ -152,7 +152,7 @@ public final class VarintReaderTest { } @Test - public void testReadVarintFlaky() throws IOException, InterruptedException { + public void readVarintFlaky() throws IOException { VarintReader reader = new VarintReader(); testReadVarintFlaky(reader, true, DATA_1_BYTE_0, 1, 0); testReadVarintFlaky(reader, true, DATA_2_BYTE_0, 2, 0); @@ -188,8 +188,9 @@ public final class VarintReaderTest { testReadVarintFlaky(reader, false, DATA_8_BYTE_MAX, 8, VALUE_8_BYTE_MAX_WITH_MASK); } - private static void testReadVarint(VarintReader reader, boolean removeMask, byte[] data, - int expectedLength, long expectedValue) throws IOException, InterruptedException { + private static void testReadVarint( + VarintReader reader, boolean removeMask, byte[] data, int expectedLength, long expectedValue) + throws IOException { ExtractorInput input = new FakeExtractorInput.Builder() .setData(data) .setSimulateUnknownLength(true) @@ -199,8 +200,9 @@ public final class VarintReaderTest { assertThat(result).isEqualTo(expectedValue); } - private static void testReadVarintFlaky(VarintReader reader, boolean removeMask, byte[] data, - int expectedLength, long expectedValue) throws IOException, InterruptedException { + private static void testReadVarintFlaky( + VarintReader reader, boolean removeMask, byte[] data, int expectedLength, long expectedValue) + throws IOException { ExtractorInput input = new FakeExtractorInput.Builder() .setData(data) .setSimulateUnknownLength(true) diff --git a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/mp3/ConstantBitrateSeekerTest.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/mp3/ConstantBitrateSeekerTest.java new file mode 100644 index 0000000000..8ff5e84d69 --- /dev/null +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/mp3/ConstantBitrateSeekerTest.java @@ -0,0 +1,211 @@ +/* + * Copyright (C) 2020 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.mp3; + +import static com.google.common.truth.Truth.assertThat; + +import android.net.Uri; +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.android.exoplayer2.C; +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.List; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Tests for {@link ConstantBitrateSeeker}. */ +@RunWith(AndroidJUnit4.class) +public class ConstantBitrateSeekerTest { + private static final String CONSTANT_FRAME_SIZE_TEST_FILE = + "mp3/bear-cbr-constant-frame-size-no-seek-table.mp3"; + private static final String VARIABLE_FRAME_SIZE_TEST_FILE = + "mp3/bear-cbr-variable-frame-size-no-seek-table.mp3"; + + private Mp3Extractor extractor; + private FakeExtractorOutput extractorOutput; + private DefaultDataSource dataSource; + + @Before + public void setUp() throws Exception { + extractor = new Mp3Extractor(); + extractorOutput = new FakeExtractorOutput(); + dataSource = + new DefaultDataSourceFactory(ApplicationProvider.getApplicationContext(), "UserAgent") + .createDataSource(); + } + + @Test + public void mp3ExtractorReads_returnSeekableCbrSeeker() throws IOException { + Uri fileUri = TestUtil.buildAssetUri(CONSTANT_FRAME_SIZE_TEST_FILE); + + SeekMap seekMap = TestUtil.extractSeekMap(extractor, extractorOutput, dataSource, fileUri); + + assertThat(seekMap.getClass()).isEqualTo(ConstantBitrateSeeker.class); + assertThat(seekMap.getDurationUs()).isEqualTo(2_784_000); + assertThat(seekMap.isSeekable()).isTrue(); + } + + @Test + public void seeking_handlesSeekToZero() throws IOException { + String fileName = CONSTANT_FRAME_SIZE_TEST_FILE; + Uri fileUri = TestUtil.buildAssetUri(fileName); + SeekMap seekMap = TestUtil.extractSeekMap(extractor, extractorOutput, dataSource, fileUri); + FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(0); + + long targetSeekTimeUs = 0; + int extractedFrameIndex = + TestUtil.seekToTimeUs( + extractor, seekMap, targetSeekTimeUs, dataSource, trackOutput, fileUri); + + assertThat(extractedFrameIndex).isNotEqualTo(C.INDEX_UNSET); + assertFirstFrameAfterSeekIsExactFrame( + fileName, trackOutput, targetSeekTimeUs, extractedFrameIndex); + } + + @Test + public void seeking_handlesSeekToEoF() throws IOException { + String fileName = CONSTANT_FRAME_SIZE_TEST_FILE; + Uri fileUri = TestUtil.buildAssetUri(fileName); + 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(C.INDEX_UNSET); + assertFirstFrameAfterSeekIsExactFrame( + fileName, trackOutput, targetSeekTimeUs, extractedFrameIndex); + } + + @Test + public void seeking_handlesSeekingBackward() throws IOException { + String fileName = CONSTANT_FRAME_SIZE_TEST_FILE; + Uri fileUri = TestUtil.buildAssetUri(fileName); + SeekMap seekMap = TestUtil.extractSeekMap(extractor, extractorOutput, dataSource, fileUri); + FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(0); + + long firstSeekTimeUs = 1_234_000; + TestUtil.seekToTimeUs(extractor, seekMap, firstSeekTimeUs, dataSource, trackOutput, fileUri); + long targetSeekTimeUs = 987_000; + int extractedFrameIndex = + TestUtil.seekToTimeUs( + extractor, seekMap, targetSeekTimeUs, dataSource, trackOutput, fileUri); + + assertThat(extractedFrameIndex).isNotEqualTo(C.INDEX_UNSET); + assertFirstFrameAfterSeekIsExactFrame( + fileName, trackOutput, targetSeekTimeUs, extractedFrameIndex); + } + + @Test + public void seeking_handlesSeekingForward() throws IOException { + String fileName = CONSTANT_FRAME_SIZE_TEST_FILE; + Uri fileUri = TestUtil.buildAssetUri(fileName); + SeekMap seekMap = TestUtil.extractSeekMap(extractor, extractorOutput, dataSource, fileUri); + FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(0); + + 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(C.INDEX_UNSET); + assertFirstFrameAfterSeekIsExactFrame( + fileName, trackOutput, targetSeekTimeUs, extractedFrameIndex); + } + + @Test + public void seeking_variableFrameSize_seeksNearlyExactlyToCorrectFrame() throws IOException { + String fileName = VARIABLE_FRAME_SIZE_TEST_FILE; + Uri fileUri = TestUtil.buildAssetUri(fileName); + SeekMap seekMap = TestUtil.extractSeekMap(extractor, extractorOutput, dataSource, fileUri); + FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(0); + + long targetSeekTimeUs = 1_234_000; + int extractedFrameIndex = + TestUtil.seekToTimeUs( + extractor, seekMap, targetSeekTimeUs, dataSource, trackOutput, fileUri); + + assertThat(extractedFrameIndex).isNotEqualTo(C.INDEX_UNSET); + assertFirstFrameAfterSeekIsWithin1FrameOfExactFrame( + fileName, trackOutput, targetSeekTimeUs, extractedFrameIndex); + } + + private static void assertFirstFrameAfterSeekIsExactFrame( + String fileName, + FakeTrackOutput trackOutput, + long targetSeekTimeUs, + int firstFrameIndexAfterSeek) + throws IOException { + FakeTrackOutput expectedTrackOutput = getExpectedTrackOutput(fileName); + int exactFrameIndex = getFrameIndex(expectedTrackOutput, targetSeekTimeUs); + + assertThat(trackOutput.getSampleData(firstFrameIndexAfterSeek)) + .isEqualTo(expectedTrackOutput.getSampleData(exactFrameIndex)); + } + + private static void assertFirstFrameAfterSeekIsWithin1FrameOfExactFrame( + String fileName, + FakeTrackOutput trackOutput, + long targetSeekTimeUs, + int firstFrameIndexAfterSeek) + throws IOException { + FakeTrackOutput expectedTrackOutput = getExpectedTrackOutput(fileName); + int exactFrameIndex = getFrameIndex(expectedTrackOutput, targetSeekTimeUs); + + boolean foundPreviousFrame = + exactFrameIndex != 0 + && Arrays.equals( + trackOutput.getSampleData(firstFrameIndexAfterSeek), + expectedTrackOutput.getSampleData(exactFrameIndex - 1)); + boolean foundExactFrame = + Arrays.equals( + trackOutput.getSampleData(firstFrameIndexAfterSeek), + expectedTrackOutput.getSampleData(exactFrameIndex)); + boolean foundNextFrame = + exactFrameIndex != expectedTrackOutput.getSampleCount() - 1 + && Arrays.equals( + trackOutput.getSampleData(firstFrameIndexAfterSeek), + expectedTrackOutput.getSampleData(exactFrameIndex + 1)); + + assertThat(foundPreviousFrame || foundExactFrame || foundNextFrame).isTrue(); + } + + private static FakeTrackOutput getExpectedTrackOutput(String fileName) throws IOException { + return TestUtil.extractAllSamplesFromFile( + new Mp3Extractor(), ApplicationProvider.getApplicationContext(), fileName) + .trackOutputs + .get(0); + } + + private static int getFrameIndex(FakeTrackOutput trackOutput, long targetSeekTimeUs) { + List frameTimes = trackOutput.getSampleTimesUs(); + return Util.binarySearchFloor( + frameTimes, targetSeekTimeUs, /* inclusive= */ true, /* stayInBounds= */ false); + } +} diff --git a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/mp3/IndexSeekerTest.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/mp3/IndexSeekerTest.java new file mode 100644 index 0000000000..0e5c263644 --- /dev/null +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/mp3/IndexSeekerTest.java @@ -0,0 +1,198 @@ +/* + * Copyright (C) 2020 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.mp3; + +import static com.google.android.exoplayer2.extractor.mp3.Mp3Extractor.FLAG_ENABLE_INDEX_SEEKING; +import static com.google.android.exoplayer2.testutil.TestUtil.extractAllSamplesFromFile; +import static com.google.common.truth.Truth.assertThat; + +import android.net.Uri; +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.android.exoplayer2.C; +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.List; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Tests for {@link IndexSeeker}. */ +@RunWith(AndroidJUnit4.class) +public class IndexSeekerTest { + + private static final String TEST_FILE_NO_SEEK_TABLE = "mp3/bear-vbr-no-seek-table.mp3"; + private static final int TEST_FILE_NO_SEEK_TABLE_DURATION = 2_808_000; + + private Mp3Extractor extractor; + private FakeExtractorOutput extractorOutput; + private DefaultDataSource dataSource; + + @Before + public void setUp() throws Exception { + extractor = new Mp3Extractor(FLAG_ENABLE_INDEX_SEEKING); + extractorOutput = new FakeExtractorOutput(); + dataSource = + new DefaultDataSourceFactory(ApplicationProvider.getApplicationContext(), "UserAgent") + .createDataSource(); + } + + @Test + public void mp3ExtractorReads_returnsSeekableSeekMap() throws Exception { + Uri fileUri = TestUtil.buildAssetUri(TEST_FILE_NO_SEEK_TABLE); + + SeekMap seekMap = TestUtil.extractSeekMap(extractor, extractorOutput, dataSource, fileUri); + + assertThat(seekMap.isSeekable()).isTrue(); + } + + @Test + public void mp3ExtractorReads_correctsInexactDuration() throws Exception { + FakeExtractorOutput extractorOutput = + TestUtil.extractAllSamplesFromFile( + extractor, ApplicationProvider.getApplicationContext(), TEST_FILE_NO_SEEK_TABLE); + + SeekMap seekMap = extractorOutput.seekMap; + + assertThat(seekMap.getDurationUs()).isEqualTo(TEST_FILE_NO_SEEK_TABLE_DURATION); + } + + @Test + public void seeking_handlesSeekToZero() throws Exception { + String fileName = TEST_FILE_NO_SEEK_TABLE; + Uri fileUri = TestUtil.buildAssetUri(fileName); + SeekMap seekMap = TestUtil.extractSeekMap(extractor, extractorOutput, dataSource, fileUri); + FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(0); + + long targetSeekTimeUs = 0; + int extractedFrameIndex = + TestUtil.seekToTimeUs( + extractor, seekMap, targetSeekTimeUs, dataSource, trackOutput, fileUri); + + assertThat(extractedFrameIndex).isNotEqualTo(C.INDEX_UNSET); + assertFirstFrameAfterSeekIsWithinMinDifference( + fileName, trackOutput, targetSeekTimeUs, extractedFrameIndex); + assertFirstFrameAfterSeekHasCorrectData(fileName, trackOutput, extractedFrameIndex); + } + + @Test + public void seeking_handlesSeekToEof() throws Exception { + String fileName = TEST_FILE_NO_SEEK_TABLE; + Uri fileUri = TestUtil.buildAssetUri(fileName); + SeekMap seekMap = TestUtil.extractSeekMap(extractor, extractorOutput, dataSource, fileUri); + FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(0); + + long targetSeekTimeUs = TEST_FILE_NO_SEEK_TABLE_DURATION; + int extractedFrameIndex = + TestUtil.seekToTimeUs( + extractor, seekMap, targetSeekTimeUs, dataSource, trackOutput, fileUri); + + assertThat(extractedFrameIndex).isNotEqualTo(C.INDEX_UNSET); + assertFirstFrameAfterSeekIsWithinMinDifference( + fileName, trackOutput, targetSeekTimeUs, extractedFrameIndex); + assertFirstFrameAfterSeekHasCorrectData(fileName, trackOutput, extractedFrameIndex); + } + + @Test + public void seeking_handlesSeekingBackward() throws Exception { + String fileName = TEST_FILE_NO_SEEK_TABLE; + Uri fileUri = TestUtil.buildAssetUri(fileName); + SeekMap seekMap = TestUtil.extractSeekMap(extractor, extractorOutput, dataSource, fileUri); + FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(0); + + long firstSeekTimeUs = 1_234_000; + TestUtil.seekToTimeUs(extractor, seekMap, firstSeekTimeUs, dataSource, trackOutput, fileUri); + long targetSeekTimeUs = 987_000; + int extractedFrameIndex = + TestUtil.seekToTimeUs( + extractor, seekMap, targetSeekTimeUs, dataSource, trackOutput, fileUri); + + assertThat(extractedFrameIndex).isNotEqualTo(C.INDEX_UNSET); + assertFirstFrameAfterSeekIsWithinMinDifference( + fileName, trackOutput, targetSeekTimeUs, extractedFrameIndex); + assertFirstFrameAfterSeekHasCorrectData(fileName, trackOutput, extractedFrameIndex); + } + + @Test + public void seeking_handlesSeekingForward() throws Exception { + String fileName = TEST_FILE_NO_SEEK_TABLE; + Uri fileUri = TestUtil.buildAssetUri(fileName); + SeekMap seekMap = TestUtil.extractSeekMap(extractor, extractorOutput, dataSource, fileUri); + FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(0); + + 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(C.INDEX_UNSET); + assertFirstFrameAfterSeekIsWithinMinDifference( + fileName, trackOutput, targetSeekTimeUs, extractedFrameIndex); + assertFirstFrameAfterSeekHasCorrectData(fileName, trackOutput, extractedFrameIndex); + } + + private static void assertFirstFrameAfterSeekIsWithinMinDifference( + String fileName, + FakeTrackOutput trackOutput, + long targetSeekTimeUs, + int firstFrameIndexAfterSeek) + throws IOException { + FakeTrackOutput expectedTrackOutput = getExpectedTrackOutput(fileName); + int exactFrameIndex = getFrameIndex(expectedTrackOutput, targetSeekTimeUs); + long exactFrameTimeUs = expectedTrackOutput.getSampleTimeUs(exactFrameIndex); + long foundTimeUs = trackOutput.getSampleTimeUs(firstFrameIndexAfterSeek); + + assertThat(exactFrameTimeUs - foundTimeUs).isAtMost(IndexSeeker.MIN_TIME_BETWEEN_POINTS_US); + } + + private static void assertFirstFrameAfterSeekHasCorrectData( + String fileName, FakeTrackOutput trackOutput, int firstFrameIndexAfterSeek) + throws IOException { + FakeTrackOutput expectedTrackOutput = getExpectedTrackOutput(fileName); + long foundTimeUs = trackOutput.getSampleTimeUs(firstFrameIndexAfterSeek); + int foundFrameIndex = getFrameIndex(expectedTrackOutput, foundTimeUs); + + trackOutput.assertSample( + firstFrameIndexAfterSeek, + expectedTrackOutput.getSampleData(foundFrameIndex), + expectedTrackOutput.getSampleTimeUs(foundFrameIndex), + expectedTrackOutput.getSampleFlags(foundFrameIndex), + expectedTrackOutput.getSampleCryptoData(foundFrameIndex)); + } + + private static FakeTrackOutput getExpectedTrackOutput(String fileName) throws IOException { + return extractAllSamplesFromFile( + new Mp3Extractor(FLAG_ENABLE_INDEX_SEEKING), + ApplicationProvider.getApplicationContext(), + fileName) + .trackOutputs + .get(0); + } + + private static int getFrameIndex(FakeTrackOutput trackOutput, long targetSeekTimeUs) { + List frameTimes = trackOutput.getSampleTimesUs(); + return Util.binarySearchFloor( + frameTimes, targetSeekTimeUs, /* inclusive= */ true, /* stayInBounds= */ false); + } +} diff --git a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/mp3/Mp3ExtractorTest.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/mp3/Mp3ExtractorTest.java new file mode 100644 index 0000000000..a142ac1a4d --- /dev/null +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/mp3/Mp3ExtractorTest.java @@ -0,0 +1,81 @@ +/* + * 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.extractor.mp3; + +import com.google.android.exoplayer2.testutil.ExtractorAsserts; +import com.google.android.exoplayer2.testutil.ExtractorAsserts.AssertionConfig; +import java.util.List; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.ParameterizedRobolectricTestRunner; +import org.robolectric.ParameterizedRobolectricTestRunner.Parameter; +import org.robolectric.ParameterizedRobolectricTestRunner.Parameters; + +/** Unit test for {@link Mp3Extractor}. */ +@RunWith(ParameterizedRobolectricTestRunner.class) +public final class Mp3ExtractorTest { + + @Parameters(name = "{0}") + public static List params() { + return ExtractorAsserts.configs(); + } + + @Parameter(0) + public ExtractorAsserts.SimulationConfig simulationConfig; + + @Test + public void mp3SampleWithXingHeader() throws Exception { + ExtractorAsserts.assertBehavior( + Mp3Extractor::new, "mp3/bear-vbr-xing-header.mp3", simulationConfig); + } + + @Test + public void mp3SampleWithCbrSeeker() throws Exception { + ExtractorAsserts.assertBehavior( + Mp3Extractor::new, "mp3/bear-cbr-variable-frame-size-no-seek-table.mp3", simulationConfig); + } + + @Test + public void mp3SampleWithIndexSeeker() throws Exception { + ExtractorAsserts.assertBehavior( + () -> new Mp3Extractor(Mp3Extractor.FLAG_ENABLE_INDEX_SEEKING), + "mp3/bear-vbr-no-seek-table.mp3", + simulationConfig); + } + + @Test + public void trimmedMp3Sample() throws Exception { + ExtractorAsserts.assertBehavior(Mp3Extractor::new, "mp3/play-trimmed.mp3", simulationConfig); + } + + @Test + public void mp3SampleWithId3Enabled() throws Exception { + ExtractorAsserts.assertBehavior( + Mp3Extractor::new, + "mp3/bear-id3.mp3", + new AssertionConfig.Builder().setDumpFilesPrefix("mp3/bear-id3-enabled").build(), + simulationConfig); + } + + @Test + public void mp3SampleWithId3Disabled() throws Exception { + ExtractorAsserts.assertBehavior( + () -> new Mp3Extractor(Mp3Extractor.FLAG_DISABLE_ID3_METADATA), + "mp3/bear-id3.mp3", + new AssertionConfig.Builder().setDumpFilesPrefix("mp3/bear-id3-disabled").build(), + simulationConfig); + } +} diff --git a/library/core/src/test/java/com/google/android/exoplayer2/extractor/mp3/XingSeekerTest.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/mp3/XingSeekerTest.java similarity index 90% rename from library/core/src/test/java/com/google/android/exoplayer2/extractor/mp3/XingSeekerTest.java rename to library/extractor/src/test/java/com/google/android/exoplayer2/extractor/mp3/XingSeekerTest.java index 96fee1d07a..7c481831c2 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/extractor/mp3/XingSeekerTest.java +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/mp3/XingSeekerTest.java @@ -19,7 +19,7 @@ import static com.google.common.truth.Truth.assertThat; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.extractor.MpegAudioHeader; +import com.google.android.exoplayer2.audio.MpegAudioUtil; import com.google.android.exoplayer2.extractor.SeekMap.SeekPoints; import com.google.android.exoplayer2.extractor.SeekPoint; import com.google.android.exoplayer2.util.ParsableByteArray; @@ -59,8 +59,8 @@ public final class XingSeekerTest { @Before public void setUp() throws Exception { - MpegAudioHeader xingFrameHeader = new MpegAudioHeader(); - MpegAudioHeader.populateHeader(XING_FRAME_HEADER_DATA, xingFrameHeader); + MpegAudioUtil.Header xingFrameHeader = new MpegAudioUtil.Header(); + xingFrameHeader.setForHeaderData(XING_FRAME_HEADER_DATA); seeker = XingSeeker.create(C.LENGTH_UNSET, XING_FRAME_POSITION, xingFrameHeader, new ParsableByteArray(XING_FRAME_PAYLOAD)); seekerWithInputLength = XingSeeker.create(STREAM_LENGTH, @@ -69,19 +69,19 @@ public final class XingSeekerTest { } @Test - public void testGetTimeUsBeforeFirstAudioFrame() { + public void getTimeUsBeforeFirstAudioFrame() { assertThat(seeker.getTimeUs(-1)).isEqualTo(0); assertThat(seekerWithInputLength.getTimeUs(-1)).isEqualTo(0); } @Test - public void testGetTimeUsAtFirstAudioFrame() { + public void getTimeUsAtFirstAudioFrame() { assertThat(seeker.getTimeUs(XING_FRAME_POSITION + xingFrameSize)).isEqualTo(0); assertThat(seekerWithInputLength.getTimeUs(XING_FRAME_POSITION + xingFrameSize)).isEqualTo(0); } @Test - public void testGetTimeUsAtEndOfStream() { + public void getTimeUsAtEndOfStream() { assertThat(seeker.getTimeUs(STREAM_LENGTH)) .isEqualTo(STREAM_DURATION_US); assertThat( @@ -90,7 +90,7 @@ public final class XingSeekerTest { } @Test - public void testGetSeekPointsAtStartOfStream() { + public void getSeekPointsAtStartOfStream() { SeekPoints seekPoints = seeker.getSeekPoints(0); SeekPoint seekPoint = seekPoints.first; assertThat(seekPoint).isEqualTo(seekPoints.second); @@ -99,7 +99,7 @@ public final class XingSeekerTest { } @Test - public void testGetSeekPointsAtEndOfStream() { + public void getSeekPointsAtEndOfStream() { SeekPoints seekPoints = seeker.getSeekPoints(STREAM_DURATION_US); SeekPoint seekPoint = seekPoints.first; assertThat(seekPoint).isEqualTo(seekPoints.second); @@ -108,7 +108,7 @@ public final class XingSeekerTest { } @Test - public void testGetTimeForAllPositions() { + public void getTimeForAllPositions() { for (int offset = xingFrameSize; offset < DATA_SIZE_BYTES; offset++) { int position = XING_FRAME_POSITION + offset; // Test seeker. diff --git a/library/core/src/test/java/com/google/android/exoplayer2/extractor/mp4/AtomParsersTest.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/mp4/AtomParsersTest.java similarity index 92% rename from library/core/src/test/java/com/google/android/exoplayer2/extractor/mp4/AtomParsersTest.java rename to library/extractor/src/test/java/com/google/android/exoplayer2/extractor/mp4/AtomParsersTest.java index 712f8e43fe..9ca974afb8 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/extractor/mp4/AtomParsersTest.java +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/mp4/AtomParsersTest.java @@ -37,7 +37,7 @@ public final class AtomParsersTest { + SAMPLE_COUNT + "0001000200030004"); @Test - public void testParseCommonEncryptionSinfFromParentIgnoresUnknownSchemeType() { + public void parseCommonEncryptionSinfFromParentIgnoresUnknownSchemeType() { byte[] cencSinf = new byte[] { 0, 0, 0, 24, 115, 105, 110, 102, // size (4), 'sinf' (4) 0, 0, 0, 16, 115, 99, 104, 109, // size (4), 'schm' (4) @@ -47,17 +47,17 @@ public final class AtomParsersTest { } @Test - public void testStz2Parsing4BitFieldSize() { + public void stz2Parsing4BitFieldSize() { verifyStz2Parsing(new Atom.LeafAtom(Atom.TYPE_stsz, new ParsableByteArray(FOUR_BIT_STZ2))); } @Test - public void testStz2Parsing8BitFieldSize() { + public void stz2Parsing8BitFieldSize() { verifyStz2Parsing(new Atom.LeafAtom(Atom.TYPE_stsz, new ParsableByteArray(EIGHT_BIT_STZ2))); } @Test - public void testStz2Parsing16BitFieldSize() { + public void stz2Parsing16BitFieldSize() { verifyStz2Parsing(new Atom.LeafAtom(Atom.TYPE_stsz, new ParsableByteArray(SIXTEEN_BIT_STZ2))); } diff --git a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4ExtractorTest.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4ExtractorTest.java new file mode 100644 index 0000000000..c09b3b439f --- /dev/null +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4ExtractorTest.java @@ -0,0 +1,110 @@ +/* + * 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.extractor.mp4; + +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.testutil.ExtractorAsserts; +import com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory; +import com.google.android.exoplayer2.util.MimeTypes; +import com.google.common.collect.ImmutableList; +import java.util.Collections; +import java.util.List; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.ParameterizedRobolectricTestRunner; +import org.robolectric.ParameterizedRobolectricTestRunner.Parameter; +import org.robolectric.ParameterizedRobolectricTestRunner.Parameters; + +/** Unit test for {@link FragmentedMp4Extractor}. */ +@RunWith(ParameterizedRobolectricTestRunner.class) +public final class FragmentedMp4ExtractorTest { + + @Parameters(name = "{0}") + public static List params() { + return ExtractorAsserts.configs(); + } + + @Parameter(0) + public ExtractorAsserts.SimulationConfig simulationConfig; + + @Test + public void sample() throws Exception { + ExtractorAsserts.assertBehavior( + getExtractorFactory(ImmutableList.of()), "mp4/sample_fragmented.mp4", simulationConfig); + } + + @Test + public void sampleSeekable() throws Exception { + ExtractorAsserts.assertBehavior( + getExtractorFactory(ImmutableList.of()), + "mp4/sample_fragmented_seekable.mp4", + simulationConfig); + } + + @Test + public void sampleWithSeiPayloadParsing() throws Exception { + // Enabling the CEA-608 track enables SEI payload parsing. + ExtractorFactory extractorFactory = + getExtractorFactory( + Collections.singletonList( + new Format.Builder().setSampleMimeType(MimeTypes.APPLICATION_CEA608).build())); + ExtractorAsserts.assertBehavior( + extractorFactory, "mp4/sample_fragmented_sei.mp4", simulationConfig); + } + + @Test + public void sampleWithAc3Track() throws Exception { + ExtractorAsserts.assertBehavior( + getExtractorFactory(ImmutableList.of()), "mp4/sample_ac3_fragmented.mp4", simulationConfig); + } + + @Test + public void sampleWithAc4Track() throws Exception { + ExtractorAsserts.assertBehavior( + getExtractorFactory(ImmutableList.of()), "mp4/sample_ac4_fragmented.mp4", simulationConfig); + } + + @Test + public void sampleWithProtectedAc4Track() throws Exception { + ExtractorAsserts.assertBehavior( + getExtractorFactory(ImmutableList.of()), "mp4/sample_ac4_protected.mp4", simulationConfig); + } + + @Test + public void sampleWithEac3Track() throws Exception { + ExtractorAsserts.assertBehavior( + getExtractorFactory(ImmutableList.of()), + "mp4/sample_eac3_fragmented.mp4", + simulationConfig); + } + + @Test + public void sampleWithEac3jocTrack() throws Exception { + ExtractorAsserts.assertBehavior( + getExtractorFactory(ImmutableList.of()), + "mp4/sample_eac3joc_fragmented.mp4", + simulationConfig); + } + + private static ExtractorFactory getExtractorFactory(final List closedCaptionFormats) { + return () -> + new FragmentedMp4Extractor( + /* flags= */ 0, + /* timestampAdjuster= */ null, + /* sideloadedTrack= */ null, + closedCaptionFormats); + } +} diff --git a/library/core/src/test/java/com/google/android/exoplayer2/extractor/mp4/MdtaMetadataEntryTest.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/mp4/MdtaMetadataEntryTest.java similarity index 97% rename from library/core/src/test/java/com/google/android/exoplayer2/extractor/mp4/MdtaMetadataEntryTest.java rename to library/extractor/src/test/java/com/google/android/exoplayer2/extractor/mp4/MdtaMetadataEntryTest.java index ea1ec1d8cd..6fba801355 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/extractor/mp4/MdtaMetadataEntryTest.java +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/mp4/MdtaMetadataEntryTest.java @@ -27,7 +27,7 @@ import org.junit.runner.RunWith; public final class MdtaMetadataEntryTest { @Test - public void testParcelable() { + public void parcelable() { MdtaMetadataEntry mdtaMetadataEntryToParcel = new MdtaMetadataEntry("test", new byte[] {1, 2}, 3, 4); diff --git a/library/core/src/test/java/com/google/android/exoplayer2/extractor/mp4/Mp4ExtractorTest.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/mp4/MetadataUtilTest.java similarity index 67% rename from library/core/src/test/java/com/google/android/exoplayer2/extractor/mp4/Mp4ExtractorTest.java rename to library/extractor/src/test/java/com/google/android/exoplayer2/extractor/mp4/MetadataUtilTest.java index 981ee17e92..2466c46d8a 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/extractor/mp4/Mp4ExtractorTest.java +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/mp4/MetadataUtilTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2016 The Android Open Source Project + * Copyright (C) 2020 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. @@ -15,17 +15,19 @@ */ package com.google.android.exoplayer2.extractor.mp4; +import static com.google.common.truth.Truth.assertThat; + import androidx.test.ext.junit.runners.AndroidJUnit4; -import com.google.android.exoplayer2.testutil.ExtractorAsserts; import org.junit.Test; import org.junit.runner.RunWith; -/** Tests for {@link Mp4Extractor}. */ +/** Test for {@link MetadataUtil}. */ @RunWith(AndroidJUnit4.class) -public final class Mp4ExtractorTest { +public final class MetadataUtilTest { @Test - public void testMp4Sample() throws Exception { - ExtractorAsserts.assertBehavior(Mp4Extractor::new, "mp4/sample.mp4"); + public void standardGenre_length_matchesNumberOfId3Genres() { + // Sanity check that we haven't forgotten a genre in the list. + assertThat(MetadataUtil.STANDARD_GENRES).hasLength(192); } } diff --git a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/mp4/Mp4ExtractorTest.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/mp4/Mp4ExtractorTest.java new file mode 100644 index 0000000000..503b78624e --- /dev/null +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/mp4/Mp4ExtractorTest.java @@ -0,0 +1,78 @@ +/* + * 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.extractor.mp4; + +import com.google.android.exoplayer2.testutil.ExtractorAsserts; +import java.util.List; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.ParameterizedRobolectricTestRunner; +import org.robolectric.ParameterizedRobolectricTestRunner.Parameter; +import org.robolectric.ParameterizedRobolectricTestRunner.Parameters; + +/** Tests for {@link Mp4Extractor}. */ +@RunWith(ParameterizedRobolectricTestRunner.class) +public final class Mp4ExtractorTest { + + @Parameters(name = "{0}") + public static List params() { + return ExtractorAsserts.configs(); + } + + @Parameter(0) + public ExtractorAsserts.SimulationConfig simulationConfig; + + @Test + public void mp4Sample() throws Exception { + ExtractorAsserts.assertBehavior(Mp4Extractor::new, "mp4/sample.mp4", simulationConfig); + } + + @Test + public void mp4SampleWithSlowMotionMetadata() throws Exception { + ExtractorAsserts.assertBehavior( + Mp4Extractor::new, "mp4/sample_android_slow_motion.mp4", simulationConfig); + } + + /** + * Test case for https://github.com/google/ExoPlayer/issues/6774. The sample file contains an mdat + * atom whose size indicates that it extends 8 bytes beyond the end of the file. + */ + @Test + public void mp4SampleWithMdatTooLong() throws Exception { + ExtractorAsserts.assertBehavior( + Mp4Extractor::new, "mp4/sample_mdat_too_long.mp4", simulationConfig); + } + + @Test + public void mp4SampleWithAc3Track() throws Exception { + ExtractorAsserts.assertBehavior(Mp4Extractor::new, "mp4/sample_ac3.mp4", simulationConfig); + } + + @Test + public void mp4SampleWithAc4Track() throws Exception { + ExtractorAsserts.assertBehavior(Mp4Extractor::new, "mp4/sample_ac4.mp4", simulationConfig); + } + + @Test + public void mp4SampleWithEac3Track() throws Exception { + ExtractorAsserts.assertBehavior(Mp4Extractor::new, "mp4/sample_eac3.mp4", simulationConfig); + } + + @Test + public void mp4SampleWithEac3jocTrack() throws Exception { + ExtractorAsserts.assertBehavior(Mp4Extractor::new, "mp4/sample_eac3joc.mp4", simulationConfig); + } +} diff --git a/library/core/src/test/java/com/google/android/exoplayer2/extractor/mp4/PsshAtomUtilTest.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/mp4/PsshAtomUtilTest.java similarity index 98% rename from library/core/src/test/java/com/google/android/exoplayer2/extractor/mp4/PsshAtomUtilTest.java rename to library/extractor/src/test/java/com/google/android/exoplayer2/extractor/mp4/PsshAtomUtilTest.java index 13d4529451..cc5dd3cdd1 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/extractor/mp4/PsshAtomUtilTest.java +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/mp4/PsshAtomUtilTest.java @@ -33,7 +33,7 @@ import org.junit.runner.RunWith; public final class PsshAtomUtilTest { @Test - public void testBuildPsshAtom() { + public void buildPsshAtom() { byte[] schemeData = new byte[]{0, 1, 2, 3, 4, 5}; byte[] psshAtom = PsshAtomUtil.buildPsshAtom(C.WIDEVINE_UUID, schemeData); // Read the PSSH atom back and assert its content is as expected. diff --git a/library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeekerTest.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeekerTest.java similarity index 67% rename from library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeekerTest.java rename to library/extractor/src/test/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeekerTest.java index e97fa878f7..83aa8c6d9b 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeekerTest.java +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeekerTest.java @@ -15,9 +15,11 @@ */ package com.google.android.exoplayer2.extractor.ogg; +import static com.google.android.exoplayer2.testutil.TestUtil.getByteArray; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.fail; +import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.extractor.ExtractorInput; @@ -34,10 +36,10 @@ import org.junit.runner.RunWith; @RunWith(AndroidJUnit4.class) public final class DefaultOggSeekerTest { - private final Random random = new Random(0); + private final Random random = new Random(/* seed= */ 0); @Test - public void testSetupWithUnsetEndPositionFails() { + public void setupWithUnsetEndPositionFails() { try { new DefaultOggSeeker( /* streamReader= */ new TestStreamReader(), @@ -53,113 +55,24 @@ public final class DefaultOggSeekerTest { } @Test - public void testSeeking() throws IOException, InterruptedException { - Random random = new Random(0); - for (int i = 0; i < 100; i++) { - testSeeking(random); - } - } + public void seeking() throws Exception { + byte[] data = + getByteArray(ApplicationProvider.getApplicationContext(), "ogg/random_1000_pages"); + int granuleCount = 49269395; + int firstPayloadPageSize = 2023; + int firstPayloadPageGranuleCount = 57058; + int lastPayloadPageSize = 282; + int lastPayloadPageGranuleCount = 20806; - @Test - public void testSkipToNextPage() throws Exception { - FakeExtractorInput extractorInput = - OggTestData.createInput( - TestUtil.joinByteArrays( - TestUtil.buildTestData(4000, random), - new byte[] {'O', 'g', 'g', 'S'}, - TestUtil.buildTestData(4000, random)), - false); - skipToNextPage(extractorInput); - assertThat(extractorInput.getPosition()).isEqualTo(4000); - } - - @Test - public void testSkipToNextPageOverlap() throws Exception { - FakeExtractorInput extractorInput = - OggTestData.createInput( - TestUtil.joinByteArrays( - TestUtil.buildTestData(2046, random), - new byte[] {'O', 'g', 'g', 'S'}, - TestUtil.buildTestData(4000, random)), - false); - skipToNextPage(extractorInput); - assertThat(extractorInput.getPosition()).isEqualTo(2046); - } - - @Test - public void testSkipToNextPageInputShorterThanPeekLength() throws Exception { - FakeExtractorInput extractorInput = - OggTestData.createInput( - TestUtil.joinByteArrays(new byte[] {'x', 'O', 'g', 'g', 'S'}), false); - skipToNextPage(extractorInput); - assertThat(extractorInput.getPosition()).isEqualTo(1); - } - - @Test - public void testSkipToNextPageNoMatch() throws Exception { - FakeExtractorInput extractorInput = - OggTestData.createInput(new byte[] {'g', 'g', 'S', 'O', 'g', 'g'}, false); - try { - skipToNextPage(extractorInput); - fail(); - } catch (EOFException e) { - // expected - } - } - - @Test - public void testReadGranuleOfLastPage() throws IOException, InterruptedException { - FakeExtractorInput input = - OggTestData.createInput( - TestUtil.joinByteArrays( - TestUtil.buildTestData(100, random), - OggTestData.buildOggHeader(0x00, 20000, 66, 3), - TestUtil.createByteArray(254, 254, 254), // laces - TestUtil.buildTestData(3 * 254, random), - OggTestData.buildOggHeader(0x00, 40000, 67, 3), - TestUtil.createByteArray(254, 254, 254), // laces - TestUtil.buildTestData(3 * 254, random), - OggTestData.buildOggHeader(0x05, 60000, 68, 3), - TestUtil.createByteArray(254, 254, 254), // laces - TestUtil.buildTestData(3 * 254, random)), - false); - assertReadGranuleOfLastPage(input, 60000); - } - - @Test - public void testReadGranuleOfLastPageAfterLastHeader() throws IOException, InterruptedException { - FakeExtractorInput input = OggTestData.createInput(TestUtil.buildTestData(100, random), false); - try { - assertReadGranuleOfLastPage(input, 60000); - fail(); - } catch (EOFException e) { - // Ignored. - } - } - - @Test - public void testReadGranuleOfLastPageWithUnboundedLength() - throws IOException, InterruptedException { - FakeExtractorInput input = OggTestData.createInput(new byte[0], true); - try { - assertReadGranuleOfLastPage(input, 60000); - fail(); - } catch (IllegalArgumentException e) { - // Ignored. - } - } - - private void testSeeking(Random random) throws IOException, InterruptedException { - OggTestFile testFile = OggTestFile.generate(random, 1000); - FakeExtractorInput input = new FakeExtractorInput.Builder().setData(testFile.data).build(); + FakeExtractorInput input = new FakeExtractorInput.Builder().setData(data).build(); TestStreamReader streamReader = new TestStreamReader(); DefaultOggSeeker oggSeeker = new DefaultOggSeeker( - /* streamReader= */ streamReader, + streamReader, /* payloadStartPosition= */ 0, - /* payloadEndPosition= */ testFile.data.length, - /* firstPayloadPageSize= */ testFile.firstPayloadPageSize, - /* firstPayloadPageGranulePosition= */ testFile.firstPayloadPageGranuleCount, + /* payloadEndPosition= */ data.length, + firstPayloadPageSize, + /* firstPayloadPageGranulePosition= */ firstPayloadPageGranuleCount, /* firstPayloadPageIsLastPage= */ false); OggPageHeader pageHeader = new OggPageHeader(); @@ -177,30 +90,30 @@ public final class DefaultOggSeekerTest { assertThat(input.getPosition()).isEqualTo(0); // Test granule 0 from file end. - granule = seekTo(input, oggSeeker, 0, testFile.data.length - 1); + granule = seekTo(input, oggSeeker, 0, data.length - 1); assertThat(granule).isEqualTo(0); assertThat(input.getPosition()).isEqualTo(0); // Test last granule. - granule = seekTo(input, oggSeeker, testFile.granuleCount - 1, 0); - assertThat(granule).isEqualTo(testFile.granuleCount - testFile.lastPayloadPageGranuleCount); - assertThat(input.getPosition()).isEqualTo(testFile.data.length - testFile.lastPayloadPageSize); + granule = seekTo(input, oggSeeker, granuleCount - 1, 0); + assertThat(granule).isEqualTo(granuleCount - lastPayloadPageGranuleCount); + assertThat(input.getPosition()).isEqualTo(data.length - lastPayloadPageSize); for (int i = 0; i < 100; i += 1) { - long targetGranule = random.nextInt(testFile.granuleCount); - int initialPosition = random.nextInt(testFile.data.length); + long targetGranule = random.nextInt(granuleCount); + int initialPosition = random.nextInt(data.length); granule = seekTo(input, oggSeeker, targetGranule, initialPosition); - long currentPosition = input.getPosition(); + int currentPosition = (int) input.getPosition(); if (granule == 0) { assertThat(currentPosition).isEqualTo(0); } else { - int previousPageStart = testFile.findPreviousPageStart(currentPosition); + int previousPageStart = findPreviousPageStart(data, currentPosition); input.setPosition(previousPageStart); pageHeader.populate(input, false); assertThat(granule).isEqualTo(pageHeader.granulePosition); } - input.setPosition((int) currentPosition); + input.setPosition(currentPosition); pageHeader.populate(input, false); // The target granule should be within the current page. assertThat(granule).isAtMost(targetGranule); @@ -208,8 +121,86 @@ public final class DefaultOggSeekerTest { } } - private static void skipToNextPage(ExtractorInput extractorInput) - throws IOException, InterruptedException { + @Test + public void skipToNextPage_success() throws Exception { + FakeExtractorInput extractorInput = + createInput( + TestUtil.joinByteArrays( + TestUtil.buildTestData(4000, random), + new byte[] {'O', 'g', 'g', 'S'}, + TestUtil.buildTestData(4000, random)), + /* simulateUnknownLength= */ false); + skipToNextPage(extractorInput); + assertThat(extractorInput.getPosition()).isEqualTo(4000); + } + + @Test + public void skipToNextPage_withOverlappingInput_success() throws Exception { + FakeExtractorInput extractorInput = + createInput( + TestUtil.joinByteArrays( + TestUtil.buildTestData(2046, random), + new byte[] {'O', 'g', 'g', 'S'}, + TestUtil.buildTestData(4000, random)), + /* simulateUnknownLength= */ false); + skipToNextPage(extractorInput); + assertThat(extractorInput.getPosition()).isEqualTo(2046); + } + + @Test + public void skipToNextPage_withInputShorterThanPeekLength_success() throws Exception { + FakeExtractorInput extractorInput = + createInput( + TestUtil.joinByteArrays(new byte[] {'x', 'O', 'g', 'g', 'S'}), + /* simulateUnknownLength= */ false); + skipToNextPage(extractorInput); + assertThat(extractorInput.getPosition()).isEqualTo(1); + } + + @Test + public void skipToNextPage_withoutMatch_throwsException() throws Exception { + FakeExtractorInput extractorInput = + createInput(new byte[] {'g', 'g', 'S', 'O', 'g', 'g'}, /* simulateUnknownLength= */ false); + try { + skipToNextPage(extractorInput); + fail(); + } catch (EOFException e) { + // expected + } + } + + @Test + public void readGranuleOfLastPage() throws IOException { + // This test stream has three headers with granule numbers 20000, 40000 and 60000. + byte[] data = getByteArray(ApplicationProvider.getApplicationContext(), "ogg/three_headers"); + FakeExtractorInput input = createInput(data, /* simulateUnknownLength= */ false); + assertReadGranuleOfLastPage(input, 60000); + } + + @Test + public void readGranuleOfLastPage_afterLastHeader_throwsException() throws Exception { + FakeExtractorInput input = + createInput(TestUtil.buildTestData(100, random), /* simulateUnknownLength= */ false); + try { + assertReadGranuleOfLastPage(input, 60000); + fail(); + } catch (EOFException e) { + // Ignored. + } + } + + @Test + public void readGranuleOfLastPage_withUnboundedLength_throwsException() throws Exception { + FakeExtractorInput input = createInput(new byte[0], /* simulateUnknownLength= */ true); + try { + assertReadGranuleOfLastPage(input, 60000); + fail(); + } catch (IllegalArgumentException e) { + // Ignored. + } + } + + private static void skipToNextPage(ExtractorInput extractorInput) throws IOException { DefaultOggSeeker oggSeeker = new DefaultOggSeeker( /* streamReader= */ new FlacReader(), @@ -229,7 +220,7 @@ public final class DefaultOggSeekerTest { } private static void assertReadGranuleOfLastPage(FakeExtractorInput input, int expected) - throws IOException, InterruptedException { + throws IOException { DefaultOggSeeker oggSeeker = new DefaultOggSeeker( /* streamReader= */ new FlacReader(), @@ -248,9 +239,18 @@ public final class DefaultOggSeekerTest { } } + private static FakeExtractorInput createInput(byte[] data, boolean simulateUnknownLength) { + return new FakeExtractorInput.Builder() + .setData(data) + .setSimulateIOErrors(true) + .setSimulateUnknownLength(simulateUnknownLength) + .setSimulatePartialReads(true) + .build(); + } + private static long seekTo( FakeExtractorInput input, DefaultOggSeeker oggSeeker, long targetGranule, int initialPosition) - throws IOException, InterruptedException { + throws IOException { long nextSeekPosition = initialPosition; oggSeeker.startSeek(targetGranule); int count = 0; @@ -264,6 +264,16 @@ public final class DefaultOggSeekerTest { return -(nextSeekPosition + 2); } + private static int findPreviousPageStart(byte[] data, int position) { + for (int i = position - 4; i >= 0; i--) { + if (data[i] == 'O' && data[i + 1] == 'g' && data[i + 2] == 'g' && data[i + 3] == 'S') { + return i; + } + } + fail(); + return -1; + } + private static class TestStreamReader extends StreamReader { @Override protected long preparePayload(ParsableByteArray packet) { diff --git a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/ogg/OggExtractorNonParameterizedTest.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/ogg/OggExtractorNonParameterizedTest.java new file mode 100644 index 0000000000..bf2a350aae --- /dev/null +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/ogg/OggExtractorNonParameterizedTest.java @@ -0,0 +1,84 @@ +/* + * 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.extractor.ogg; + +import static com.google.android.exoplayer2.testutil.TestUtil.getByteArray; + +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.android.exoplayer2.testutil.ExtractorAsserts; +import com.google.android.exoplayer2.testutil.FakeExtractorInput; +import java.io.IOException; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** + * Tests for {@link OggExtractor} that test specific behaviours and don't need to be parameterized. + * + *

      For parameterized tests using {@link ExtractorAsserts} see {@link + * OggExtractorParameterizedTest}. + */ +@RunWith(AndroidJUnit4.class) +public final class OggExtractorNonParameterizedTest { + + @Test + public void sniffVorbis() throws Exception { + byte[] data = getByteArray(ApplicationProvider.getApplicationContext(), "ogg/vorbis_header"); + assertSniff(data, /* expectedResult= */ true); + } + + @Test + public void sniffFlac() throws Exception { + byte[] data = getByteArray(ApplicationProvider.getApplicationContext(), "ogg/flac_header"); + assertSniff(data, /* expectedResult= */ true); + } + + @Test + public void sniffFailsOpusFile() throws Exception { + byte[] data = getByteArray(ApplicationProvider.getApplicationContext(), "ogg/opus_header"); + assertSniff(data, /* expectedResult= */ false); + } + + @Test + public void sniffFailsInvalidOggHeader() throws Exception { + byte[] data = + getByteArray(ApplicationProvider.getApplicationContext(), "ogg/invalid_ogg_header"); + assertSniff(data, /* expectedResult= */ false); + } + + @Test + public void sniffInvalidHeader() throws Exception { + byte[] data = getByteArray(ApplicationProvider.getApplicationContext(), "ogg/invalid_header"); + assertSniff(data, /* expectedResult= */ false); + } + + @Test + public void sniffFailsEOF() throws Exception { + byte[] data = getByteArray(ApplicationProvider.getApplicationContext(), "ogg/eof_header"); + assertSniff(data, /* expectedResult= */ false); + } + + private void assertSniff(byte[] data, boolean expectedResult) throws IOException { + FakeExtractorInput input = + new FakeExtractorInput.Builder() + .setData(data) + .setSimulateIOErrors(true) + .setSimulateUnknownLength(true) + .setSimulatePartialReads(true) + .build(); + ExtractorAsserts.assertSniff(new OggExtractor(), input, expectedResult); + } +} diff --git a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/ogg/OggExtractorParameterizedTest.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/ogg/OggExtractorParameterizedTest.java new file mode 100644 index 0000000000..9b2c6caf89 --- /dev/null +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/ogg/OggExtractorParameterizedTest.java @@ -0,0 +1,62 @@ +/* + * 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.extractor.ogg; + +import com.google.android.exoplayer2.testutil.ExtractorAsserts; +import java.util.List; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.ParameterizedRobolectricTestRunner; +import org.robolectric.ParameterizedRobolectricTestRunner.Parameter; +import org.robolectric.ParameterizedRobolectricTestRunner.Parameters; + +/** + * Unit tests for {@link OggExtractor} that use parameterization to test a range of behaviours. + * + *

      For non-parameterized tests see {@link OggExtractorNonParameterizedTest}. + */ +@RunWith(ParameterizedRobolectricTestRunner.class) +public final class OggExtractorParameterizedTest { + + @Parameters(name = "{0}") + public static List params() { + return ExtractorAsserts.configs(); + } + + @Parameter(0) + public ExtractorAsserts.SimulationConfig simulationConfig; + + @Test + public void opus() throws Exception { + ExtractorAsserts.assertBehavior(OggExtractor::new, "ogg/bear.opus", simulationConfig); + } + + @Test + public void flac() throws Exception { + ExtractorAsserts.assertBehavior(OggExtractor::new, "ogg/bear_flac.ogg", simulationConfig); + } + + @Test + public void flacNoSeektable() throws Exception { + ExtractorAsserts.assertBehavior( + OggExtractor::new, "ogg/bear_flac_noseektable.ogg", simulationConfig); + } + + @Test + public void vorbis() throws Exception { + ExtractorAsserts.assertBehavior(OggExtractor::new, "ogg/bear_vorbis.ogg", simulationConfig); + } +} diff --git a/library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/OggPacketTest.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/ogg/OggPacketTest.java similarity index 57% rename from library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/OggPacketTest.java rename to library/extractor/src/test/java/com/google/android/exoplayer2/extractor/ogg/OggPacketTest.java index 18a03ddc29..492b542e95 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/OggPacketTest.java +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/ogg/OggPacketTest.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.extractor.ogg; +import static com.google.android.exoplayer2.testutil.TestUtil.getByteArray; import static com.google.common.truth.Truth.assertThat; import androidx.test.core.app.ApplicationProvider; @@ -25,7 +26,6 @@ import com.google.android.exoplayer2.util.ParsableByteArray; 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; @@ -35,41 +35,19 @@ public final class OggPacketTest { private static final String TEST_FILE = "ogg/bear.opus"; - private Random random; - private OggPacket oggPacket; - - @Before - public void setUp() throws Exception { - random = new Random(0); - oggPacket = new OggPacket(); - } + private final Random random = new Random(/* seed= */ 0); + private final OggPacket oggPacket = new OggPacket(); @Test - public void testReadPacketsWithEmptyPage() throws Exception { + public void readPacketsWithEmptyPage() throws Exception { byte[] firstPacket = TestUtil.buildTestData(8, random); byte[] secondPacket = TestUtil.buildTestData(272, random); byte[] thirdPacket = TestUtil.buildTestData(256, random); byte[] fourthPacket = TestUtil.buildTestData(271, random); - FakeExtractorInput input = - OggTestData.createInput( - TestUtil.joinByteArrays( - // First page with a single packet. - OggTestData.buildOggHeader(0x02, 0, 1000, 0x01), - TestUtil.createByteArray(0x08), // Laces - firstPacket, - // Second page with a single packet. - OggTestData.buildOggHeader(0x00, 16, 1001, 0x02), - TestUtil.createByteArray(0xFF, 0x11), // Laces - secondPacket, - // Third page with zero packets. - OggTestData.buildOggHeader(0x00, 16, 1002, 0x00), - // Fourth page with two packets. - OggTestData.buildOggHeader(0x04, 128, 1003, 0x04), - TestUtil.createByteArray(0xFF, 0x01, 0xFF, 0x10), // Laces - thirdPacket, - fourthPacket), - true); + createInput( + getByteArray( + ApplicationProvider.getApplicationContext(), "ogg/four_packets_with_empty_page")); assertReadPacket(input, firstPacket); assertThat((oggPacket.getPageHeader().type & 0x02) == 0x02).isTrue(); @@ -110,18 +88,14 @@ public final class OggPacketTest { } @Test - public void testReadPacketWithZeroSizeTerminator() throws Exception { + public void readPacketWithZeroSizeTerminator() throws Exception { byte[] firstPacket = TestUtil.buildTestData(255, random); byte[] secondPacket = TestUtil.buildTestData(8, random); - FakeExtractorInput input = - OggTestData.createInput( - TestUtil.joinByteArrays( - OggTestData.buildOggHeader(0x06, 0, 1000, 0x04), - TestUtil.createByteArray(0xFF, 0x00, 0x00, 0x08), // Laces. - firstPacket, - secondPacket), - true); + createInput( + getByteArray( + ApplicationProvider.getApplicationContext(), + "ogg/packet_with_zero_size_terminator")); assertReadPacket(input, firstPacket); assertReadPacket(input, secondPacket); @@ -129,21 +103,13 @@ public final class OggPacketTest { } @Test - public void testReadContinuedPacketOverTwoPages() throws Exception { + public void readContinuedPacketOverTwoPages() throws Exception { byte[] firstPacket = TestUtil.buildTestData(518); - FakeExtractorInput input = - OggTestData.createInput( - TestUtil.joinByteArrays( - // First page. - OggTestData.buildOggHeader(0x02, 0, 1000, 0x02), - TestUtil.createByteArray(0xFF, 0xFF), // Laces. - Arrays.copyOf(firstPacket, 510), - // Second page (continued packet). - OggTestData.buildOggHeader(0x05, 10, 1001, 0x01), - TestUtil.createByteArray(0x08), // Laces. - Arrays.copyOfRange(firstPacket, 510, 510 + 8)), - true); + createInput( + getByteArray( + ApplicationProvider.getApplicationContext(), + "ogg/continued_packet_over_two_pages")); assertReadPacket(input, firstPacket); assertThat((oggPacket.getPageHeader().type & 0x04) == 0x04).isTrue(); @@ -154,29 +120,13 @@ public final class OggPacketTest { } @Test - public void testReadContinuedPacketOverFourPages() throws Exception { + public void readContinuedPacketOverFourPages() throws Exception { byte[] firstPacket = TestUtil.buildTestData(1028); - FakeExtractorInput input = - OggTestData.createInput( - TestUtil.joinByteArrays( - // First page. - OggTestData.buildOggHeader(0x02, 0, 1000, 0x02), - TestUtil.createByteArray(0xFF, 0xFF), // Laces. - Arrays.copyOf(firstPacket, 510), - // Second page (continued packet). - OggTestData.buildOggHeader(0x01, 10, 1001, 0x01), - TestUtil.createByteArray(0xFF), // Laces. - Arrays.copyOfRange(firstPacket, 510, 510 + 255), - // Third page (continued packet). - OggTestData.buildOggHeader(0x01, 10, 1002, 0x01), - TestUtil.createByteArray(0xFF), // Laces. - Arrays.copyOfRange(firstPacket, 510 + 255, 510 + 255 + 255), - // Fourth page (continued packet). - OggTestData.buildOggHeader(0x05, 10, 1003, 0x01), - TestUtil.createByteArray(0x08), // Laces. - Arrays.copyOfRange(firstPacket, 510 + 255 + 255, 510 + 255 + 255 + 8)), - true); + createInput( + getByteArray( + ApplicationProvider.getApplicationContext(), + "ogg/continued_packet_over_four_pages")); assertReadPacket(input, firstPacket); assertThat((oggPacket.getPageHeader().type & 0x04) == 0x04).isTrue(); @@ -187,17 +137,12 @@ public final class OggPacketTest { } @Test - public void testReadDiscardContinuedPacketAtStart() throws Exception { + public void readDiscardContinuedPacketAtStart() throws Exception { byte[] pageBody = TestUtil.buildTestData(256 + 8); - FakeExtractorInput input = - OggTestData.createInput( - TestUtil.joinByteArrays( - // Page with a continued packet at start. - OggTestData.buildOggHeader(0x01, 10, 1001, 0x03), - TestUtil.createByteArray(255, 1, 8), // Laces. - pageBody), - true); + createInput( + getByteArray( + ApplicationProvider.getApplicationContext(), "ogg/continued_packet_at_start")); // Expect the first partial packet to be discarded. assertReadPacket(input, Arrays.copyOfRange(pageBody, 256, 256 + 8)); @@ -205,24 +150,15 @@ public final class OggPacketTest { } @Test - public void testReadZeroSizedPacketsAtEndOfStream() throws Exception { + public void readZeroSizedPacketsAtEndOfStream() throws Exception { byte[] firstPacket = TestUtil.buildTestData(8, random); byte[] secondPacket = TestUtil.buildTestData(8, random); byte[] thirdPacket = TestUtil.buildTestData(8, random); - FakeExtractorInput input = - OggTestData.createInput( - TestUtil.joinByteArrays( - OggTestData.buildOggHeader(0x02, 0, 1000, 0x01), - TestUtil.createByteArray(0x08), // Laces. - firstPacket, - OggTestData.buildOggHeader(0x04, 0, 1001, 0x03), - TestUtil.createByteArray(0x08, 0x00, 0x00), // Laces. - secondPacket, - OggTestData.buildOggHeader(0x04, 0, 1002, 0x03), - TestUtil.createByteArray(0x08, 0x00, 0x00), // Laces. - thirdPacket), - true); + createInput( + getByteArray( + ApplicationProvider.getApplicationContext(), + "ogg/zero_sized_packets_at_end_of_stream")); assertReadPacket(input, firstPacket); assertReadPacket(input, secondPacket); @@ -231,7 +167,7 @@ public final class OggPacketTest { } @Test - public void testParseRealFile() throws IOException, InterruptedException { + public void parseRealFile() throws IOException { byte[] data = TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), TEST_FILE); FakeExtractorInput input = new FakeExtractorInput.Builder().setData(data).build(); int packetCounter = 0; @@ -241,19 +177,27 @@ public final class OggPacketTest { assertThat(packetCounter).isEqualTo(277); } + private static FakeExtractorInput createInput(byte[] data) { + return new FakeExtractorInput.Builder() + .setData(data) + .setSimulateIOErrors(true) + .setSimulateUnknownLength(true) + .setSimulatePartialReads(true) + .build(); + } + private void assertReadPacket(FakeExtractorInput extractorInput, byte[] expected) - throws IOException, InterruptedException { + throws IOException { assertThat(readPacket(extractorInput)).isTrue(); ParsableByteArray payload = oggPacket.getPayload(); assertThat(Arrays.copyOf(payload.data, payload.limit())).isEqualTo(expected); } - private void assertReadEof(FakeExtractorInput extractorInput) - throws IOException, InterruptedException { + private void assertReadEof(FakeExtractorInput extractorInput) throws IOException { assertThat(readPacket(extractorInput)).isFalse(); } - private boolean readPacket(FakeExtractorInput input) throws InterruptedException, IOException { + private boolean readPacket(FakeExtractorInput input) throws IOException { while (true) { try { return oggPacket.populate(input); diff --git a/library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/OggPageHeaderTest.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/ogg/OggPageHeaderTest.java similarity index 50% rename from library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/OggPageHeaderTest.java rename to library/extractor/src/test/java/com/google/android/exoplayer2/extractor/ogg/OggPageHeaderTest.java index 4d9e08a12d..6b5ffe8f91 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/OggPageHeaderTest.java +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/ogg/OggPageHeaderTest.java @@ -15,13 +15,14 @@ */ package com.google.android.exoplayer2.extractor.ogg; +import static com.google.android.exoplayer2.testutil.TestUtil.getByteArray; import static com.google.common.truth.Truth.assertThat; +import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.testutil.FakeExtractorInput; import com.google.android.exoplayer2.testutil.FakeExtractorInput.SimulatedIOException; import com.google.android.exoplayer2.testutil.TestUtil; -import java.io.IOException; import org.junit.Test; import org.junit.runner.RunWith; @@ -30,13 +31,12 @@ import org.junit.runner.RunWith; public final class OggPageHeaderTest { @Test - public void testPopulatePageHeader() throws IOException, InterruptedException { - FakeExtractorInput input = OggTestData.createInput(TestUtil.joinByteArrays( - OggTestData.buildOggHeader(0x01, 123456, 4, 2), - TestUtil.createByteArray(2, 2) - ), true); + public void populatePageHeader_success() throws Exception { + byte[] data = getByteArray(ApplicationProvider.getApplicationContext(), "ogg/page_header"); + + FakeExtractorInput input = createInput(data, /* simulateUnknownLength= */ true); OggPageHeader header = new OggPageHeader(); - populatePageHeader(input, header, false); + populatePageHeader(input, header, /* quiet= */ false); assertThat(header.type).isEqualTo(0x01); assertThat(header.headerSize).isEqualTo(27 + 2); @@ -50,51 +50,52 @@ public final class OggPageHeaderTest { } @Test - public void testPopulatePageHeaderQuiteOnExceptionLessThan27Bytes() - throws IOException, InterruptedException { - FakeExtractorInput input = OggTestData.createInput(TestUtil.createByteArray(2, 2), false); + public void populatePageHeader_withLessThan27Bytes_returnFalseWithoutException() + throws Exception { + FakeExtractorInput input = + createInput(TestUtil.createByteArray(2, 2), /* simulateUnknownLength= */ false); OggPageHeader header = new OggPageHeader(); - assertThat(populatePageHeader(input, header, true)).isFalse(); + assertThat(populatePageHeader(input, header, /* quiet= */ true)).isFalse(); } @Test - public void testPopulatePageHeaderQuiteOnExceptionNotOgg() - throws IOException, InterruptedException { - byte[] headerBytes = TestUtil.joinByteArrays( - OggTestData.buildOggHeader(0x01, 123456, 4, 2), - TestUtil.createByteArray(2, 2) - ); + public void populatePageHeader_withNotOgg_returnFalseWithoutException() throws Exception { + byte[] data = getByteArray(ApplicationProvider.getApplicationContext(), "ogg/page_header"); // change from 'O' to 'o' - headerBytes[0] = 'o'; - FakeExtractorInput input = OggTestData.createInput(headerBytes, false); + data[0] = 'o'; + FakeExtractorInput input = createInput(data, /* simulateUnknownLength= */ false); OggPageHeader header = new OggPageHeader(); - assertThat(populatePageHeader(input, header, true)).isFalse(); + assertThat(populatePageHeader(input, header, /* quiet= */ true)).isFalse(); } @Test - public void testPopulatePageHeaderQuiteOnExceptionWrongRevision() - throws IOException, InterruptedException { - byte[] headerBytes = TestUtil.joinByteArrays( - OggTestData.buildOggHeader(0x01, 123456, 4, 2), - TestUtil.createByteArray(2, 2) - ); + public void populatePageHeader_withWrongRevision_returnFalseWithoutException() throws Exception { + byte[] data = getByteArray(ApplicationProvider.getApplicationContext(), "ogg/page_header"); // change revision from 0 to 1 - headerBytes[4] = 0x01; - FakeExtractorInput input = OggTestData.createInput(headerBytes, false); + data[4] = 0x01; + FakeExtractorInput input = createInput(data, /* simulateUnknownLength= */ false); OggPageHeader header = new OggPageHeader(); - assertThat(populatePageHeader(input, header, true)).isFalse(); + assertThat(populatePageHeader(input, header, /* quiet= */ true)).isFalse(); } - private boolean populatePageHeader(FakeExtractorInput input, OggPageHeader header, - boolean quite) throws IOException, InterruptedException { + private static boolean populatePageHeader( + FakeExtractorInput input, OggPageHeader header, boolean quiet) throws Exception { while (true) { try { - return header.populate(input, quite); + return header.populate(input, quiet); } catch (SimulatedIOException e) { // ignored } } } + private static FakeExtractorInput createInput(byte[] data, boolean simulateUnknownLength) { + return new FakeExtractorInput.Builder() + .setData(data) + .setSimulateIOErrors(true) + .setSimulateUnknownLength(simulateUnknownLength) + .setSimulatePartialReads(true) + .build(); + } } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/VorbisReaderTest.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/ogg/VorbisReaderTest.java similarity index 92% rename from library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/VorbisReaderTest.java rename to library/extractor/src/test/java/com/google/android/exoplayer2/extractor/ogg/VorbisReaderTest.java index 5895116e7d..c7edff700a 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/VorbisReaderTest.java +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/ogg/VorbisReaderTest.java @@ -36,7 +36,7 @@ import org.junit.runner.RunWith; public final class VorbisReaderTest { @Test - public void testReadBits() throws Exception { + public void readBits_returnsSignificantBitsFromIndex() { assertThat(readBits((byte) 0x00, 2, 2)).isEqualTo(0); assertThat(readBits((byte) 0x02, 1, 1)).isEqualTo(1); assertThat(readBits((byte) 0xF0, 4, 4)).isEqualTo(15); @@ -44,7 +44,7 @@ public final class VorbisReaderTest { } @Test - public void testAppendNumberOfSamples() throws Exception { + public void appendNumberOfSamples() { ParsableByteArray buffer = new ParsableByteArray(4); buffer.setLimit(0); VorbisReader.appendNumberOfSamples(buffer, 0x01234567); @@ -56,7 +56,7 @@ public final class VorbisReaderTest { } @Test - public void testReadSetupHeadersWithIOExceptions() throws IOException, InterruptedException { + public void readSetupHeaders_withIOExceptions_readSuccess() throws IOException { // initial two pages of bytes which by spec contain the three Vorbis header packets: // identification, comment and setup header. byte[] data = @@ -77,8 +77,8 @@ public final class VorbisReaderTest { assertThat(vorbisSetup.idHeader.data).hasLength(30); assertThat(vorbisSetup.setupHeaderData).hasLength(3597); - assertThat(vorbisSetup.idHeader.bitrateMax).isEqualTo(-1); - assertThat(vorbisSetup.idHeader.bitrateMin).isEqualTo(-1); + assertThat(vorbisSetup.idHeader.bitrateMaximum).isEqualTo(-1); + assertThat(vorbisSetup.idHeader.bitrateMinimum).isEqualTo(-1); assertThat(vorbisSetup.idHeader.bitrateNominal).isEqualTo(66666); assertThat(vorbisSetup.idHeader.blockSize0).isEqualTo(512); assertThat(vorbisSetup.idHeader.blockSize1).isEqualTo(1024); @@ -98,7 +98,7 @@ public final class VorbisReaderTest { } private static VorbisSetup readSetupHeaders(VorbisReader reader, ExtractorInput input) - throws IOException, InterruptedException { + throws IOException { OggPacket oggPacket = new OggPacket(); while (true) { try { diff --git a/library/core/src/test/java/com/google/android/exoplayer2/extractor/rawcc/RawCcExtractorTest.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/rawcc/RawCcExtractorTest.java similarity index 58% rename from library/core/src/test/java/com/google/android/exoplayer2/extractor/rawcc/RawCcExtractorTest.java rename to library/extractor/src/test/java/com/google/android/exoplayer2/extractor/rawcc/RawCcExtractorTest.java index bd7a63e43a..92e0f21451 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/extractor/rawcc/RawCcExtractorTest.java +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/rawcc/RawCcExtractorTest.java @@ -15,33 +15,35 @@ */ package com.google.android.exoplayer2.extractor.rawcc; -import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.testutil.ExtractorAsserts; import com.google.android.exoplayer2.util.MimeTypes; +import java.util.List; import org.junit.Test; import org.junit.runner.RunWith; +import org.robolectric.ParameterizedRobolectricTestRunner; /** Tests for {@link RawCcExtractor}. */ -@RunWith(AndroidJUnit4.class) +@RunWith(ParameterizedRobolectricTestRunner.class) public final class RawCcExtractorTest { + @ParameterizedRobolectricTestRunner.Parameters(name = "{0}") + public static List params() { + return ExtractorAsserts.configs(); + } + + @ParameterizedRobolectricTestRunner.Parameter(0) + public ExtractorAsserts.SimulationConfig simulationConfig; + @Test - public void testRawCcSample() throws Exception { + public void rawCcSample() throws Exception { + Format format = + new Format.Builder() + .setSampleMimeType(MimeTypes.APPLICATION_CEA608) + .setCodecs("cea608") + .setAccessibilityChannel(1) + .build(); ExtractorAsserts.assertBehavior( - () -> - new RawCcExtractor( - Format.createTextContainerFormat( - /* id= */ null, - /* label= */ null, - /* containerMimeType= */ null, - /* sampleMimeType= */ MimeTypes.APPLICATION_CEA608, - /* codecs= */ "cea608", - /* bitrate= */ Format.NO_VALUE, - /* selectionFlags= */ 0, - /* roleFlags= */ 0, - /* language= */ null, - /* accessibilityChannel= */ 1)), - "rawcc/sample.rawcc"); + () -> new RawCcExtractor(format), "rawcc/sample.rawcc", simulationConfig); } } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/extractor/ts/Ac3ExtractorTest.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/ts/Ac3ExtractorTest.java similarity index 57% rename from library/core/src/test/java/com/google/android/exoplayer2/extractor/ts/Ac3ExtractorTest.java rename to library/extractor/src/test/java/com/google/android/exoplayer2/extractor/ts/Ac3ExtractorTest.java index 0fe15ac86e..5cdaf91e74 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/extractor/ts/Ac3ExtractorTest.java +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/ts/Ac3ExtractorTest.java @@ -15,22 +15,38 @@ */ package com.google.android.exoplayer2.extractor.ts; -import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.testutil.ExtractorAsserts; +import java.util.List; import org.junit.Test; import org.junit.runner.RunWith; +import org.robolectric.ParameterizedRobolectricTestRunner; +import org.robolectric.ParameterizedRobolectricTestRunner.Parameter; +import org.robolectric.ParameterizedRobolectricTestRunner.Parameters; /** Unit test for {@link Ac3Extractor}. */ -@RunWith(AndroidJUnit4.class) +@RunWith(ParameterizedRobolectricTestRunner.class) public final class Ac3ExtractorTest { + @Parameters(name = "{0}") + public static List params() { + return ExtractorAsserts.configs(); + } + + @Parameter(0) + public ExtractorAsserts.SimulationConfig simulationConfig; + @Test - public void testAc3Sample() throws Exception { - ExtractorAsserts.assertBehavior(Ac3Extractor::new, "ts/sample.ac3"); + public void ac3Sample() throws Exception { + ExtractorAsserts.assertBehavior(Ac3Extractor::new, "ts/sample.ac3", simulationConfig); } @Test - public void testEAc3Sample() throws Exception { - ExtractorAsserts.assertBehavior(Ac3Extractor::new, "ts/sample.eac3"); + public void eAc3Sample() throws Exception { + ExtractorAsserts.assertBehavior(Ac3Extractor::new, "ts/sample.eac3", simulationConfig); + } + + @Test + public void eAc3jocSample() throws Exception { + ExtractorAsserts.assertBehavior(Ac3Extractor::new, "ts/sample_eac3joc.ec3", simulationConfig); } } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/extractor/ts/Ac4ExtractorTest.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/ts/Ac4ExtractorTest.java similarity index 64% rename from library/core/src/test/java/com/google/android/exoplayer2/extractor/ts/Ac4ExtractorTest.java rename to library/extractor/src/test/java/com/google/android/exoplayer2/extractor/ts/Ac4ExtractorTest.java index 3d1bafc7dc..c95ec27dad 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/extractor/ts/Ac4ExtractorTest.java +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/ts/Ac4ExtractorTest.java @@ -15,17 +15,28 @@ */ package com.google.android.exoplayer2.extractor.ts; -import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.testutil.ExtractorAsserts; +import java.util.List; import org.junit.Test; import org.junit.runner.RunWith; +import org.robolectric.ParameterizedRobolectricTestRunner; +import org.robolectric.ParameterizedRobolectricTestRunner.Parameter; +import org.robolectric.ParameterizedRobolectricTestRunner.Parameters; /** Unit test for {@link Ac4Extractor}. */ -@RunWith(AndroidJUnit4.class) +@RunWith(ParameterizedRobolectricTestRunner.class) public final class Ac4ExtractorTest { + @Parameters(name = "{0}") + public static List params() { + return ExtractorAsserts.configs(); + } + + @Parameter(0) + public ExtractorAsserts.SimulationConfig simulationConfig; + @Test - public void testAc4Sample() throws Exception { - ExtractorAsserts.assertBehavior(Ac4Extractor::new, "ts/sample.ac4"); + public void ac4Sample() throws Exception { + ExtractorAsserts.assertBehavior(Ac4Extractor::new, "ts/sample.ac4", simulationConfig); } } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractorSeekTest.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractorSeekTest.java similarity index 93% rename from library/core/src/test/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractorSeekTest.java rename to library/extractor/src/test/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractorSeekTest.java index 060f7fb81d..5226aa71e9 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractorSeekTest.java +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractorSeekTest.java @@ -54,8 +54,7 @@ public final class AdtsExtractorSeekTest { } @Test - public void testAdtsExtractorReads_returnSeekableSeekMap() - throws IOException, InterruptedException { + public void adtsExtractorReads_returnSeekableSeekMap() throws IOException { String fileName = TEST_FILE; Uri fileUri = TestUtil.buildAssetUri(fileName); expectedTrackOutput = @@ -74,8 +73,7 @@ public final class AdtsExtractorSeekTest { } @Test - public void testSeeking_handlesSeekingToPositionInFile_extractsCorrectSample() - throws IOException, InterruptedException { + public void seeking_handlesSeekingToPositionInFile_extractsCorrectSample() throws IOException { String fileName = TEST_FILE; Uri fileUri = TestUtil.buildAssetUri(fileName); expectedTrackOutput = @@ -101,8 +99,7 @@ public final class AdtsExtractorSeekTest { } @Test - public void testSeeking_handlesSeekToEoF_extractsLastSample() - throws IOException, InterruptedException { + public void seeking_handlesSeekToEoF_extractsLastSample() throws IOException { String fileName = TEST_FILE; Uri fileUri = TestUtil.buildAssetUri(fileName); expectedTrackOutput = @@ -128,8 +125,7 @@ public final class AdtsExtractorSeekTest { } @Test - public void testSeeking_handlesSeekingBackward_extractsCorrectSamples() - throws IOException, InterruptedException { + public void seeking_handlesSeekingBackward_extractsCorrectSamples() throws IOException { String fileName = TEST_FILE; Uri fileUri = TestUtil.buildAssetUri(fileName); expectedTrackOutput = @@ -157,8 +153,7 @@ public final class AdtsExtractorSeekTest { } @Test - public void testSeeking_handlesSeekingForward_extractsCorrectSamples() - throws IOException, InterruptedException { + public void seeking_handlesSeekingForward_extractsCorrectSamples() throws IOException { String fileName = TEST_FILE; Uri fileUri = TestUtil.buildAssetUri(fileName); expectedTrackOutput = @@ -186,8 +181,7 @@ public final class AdtsExtractorSeekTest { } @Test - public void testSeeking_handlesRandomSeeks_extractsCorrectSamples() - throws IOException, InterruptedException { + public void seeking_handlesRandomSeeks_extractsCorrectSamples() throws IOException { String fileName = TEST_FILE; Uri fileUri = TestUtil.buildAssetUri(fileName); expectedTrackOutput = diff --git a/library/core/src/test/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractorTest.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractorTest.java similarity index 57% rename from library/core/src/test/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractorTest.java rename to library/extractor/src/test/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractorTest.java index 56776aea09..593180797d 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractorTest.java +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractorTest.java @@ -15,32 +15,51 @@ */ package com.google.android.exoplayer2.extractor.ts; -import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.testutil.ExtractorAsserts; +import java.util.List; import org.junit.Test; import org.junit.runner.RunWith; +import org.robolectric.ParameterizedRobolectricTestRunner; +import org.robolectric.ParameterizedRobolectricTestRunner.Parameter; +import org.robolectric.ParameterizedRobolectricTestRunner.Parameters; /** Unit test for {@link AdtsExtractor}. */ -@RunWith(AndroidJUnit4.class) +@RunWith(ParameterizedRobolectricTestRunner.class) public final class AdtsExtractorTest { + @Parameters(name = "{0}") + public static List params() { + return ExtractorAsserts.configs(); + } + + @Parameter(0) + public ExtractorAsserts.SimulationConfig simulationConfig; + @Test - public void testSample() throws Exception { - ExtractorAsserts.assertBehavior(AdtsExtractor::new, "ts/sample.adts"); + public void sample() throws Exception { + ExtractorAsserts.assertBehavior(AdtsExtractor::new, "ts/sample.adts", simulationConfig); } @Test - public void testSample_withSeeking() throws Exception { + public void sample_with_id3() throws Exception { + ExtractorAsserts.assertBehavior( + AdtsExtractor::new, "ts/sample_with_id3.adts", simulationConfig); + } + + @Test + public void sample_withSeeking() throws Exception { ExtractorAsserts.assertBehavior( () -> new AdtsExtractor(/* flags= */ AdtsExtractor.FLAG_ENABLE_CONSTANT_BITRATE_SEEKING), - "ts/sample_cbs.adts"); + "ts/sample_cbs.adts", + simulationConfig); } // https://github.com/google/ExoPlayer/issues/6700 @Test - public void testSample_withSeekingAndTruncatedFile() throws Exception { + public void sample_withSeekingAndTruncatedFile() throws Exception { ExtractorAsserts.assertBehavior( () -> new AdtsExtractor(/* flags= */ AdtsExtractor.FLAG_ENABLE_CONSTANT_BITRATE_SEEKING), - "ts/sample_cbs_truncated.adts"); + "ts/sample_cbs_truncated.adts", + simulationConfig); } } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/extractor/ts/AdtsReaderTest.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/ts/AdtsReaderTest.java similarity index 91% rename from library/core/src/test/java/com/google/android/exoplayer2/extractor/ts/AdtsReaderTest.java rename to library/extractor/src/test/java/com/google/android/exoplayer2/extractor/ts/AdtsReaderTest.java index 1562475822..c04c7224f9 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/extractor/ts/AdtsReaderTest.java +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/ts/AdtsReaderTest.java @@ -80,7 +80,7 @@ public class AdtsReaderTest { } @Test - public void testSkipToNextSample() throws Exception { + public void skipToNextSample() throws Exception { for (int i = 1; i <= ID3_DATA_1.length + ID3_DATA_2.length; i++) { data.setPosition(i); feed(); @@ -91,7 +91,7 @@ public class AdtsReaderTest { } @Test - public void testSkipToNextSampleResetsState() throws Exception { + public void skipToNextSampleResetsState() throws Exception { data = new ParsableByteArray( TestUtil.joinByteArrays( @@ -118,45 +118,45 @@ public class AdtsReaderTest { } @Test - public void testNoData() throws Exception { + public void noData() throws Exception { feedLimited(0); assertSampleCounts(0, 0); } @Test - public void testNotEnoughDataForIdentifier() throws Exception { + public void notEnoughDataForIdentifier() throws Exception { feedLimited(3 - 1); assertSampleCounts(0, 0); } @Test - public void testNotEnoughDataForHeader() throws Exception { + public void notEnoughDataForHeader() throws Exception { feedLimited(10 - 1); assertSampleCounts(0, 0); } @Test - public void testNotEnoughDataForWholeId3Packet() throws Exception { + public void notEnoughDataForWholeId3Packet() throws Exception { feedLimited(ID3_DATA_1.length - 1); assertSampleCounts(0, 0); } @Test - public void testConsumeWholeId3Packet() throws Exception { + public void consumeWholeId3Packet() throws Exception { feedLimited(ID3_DATA_1.length); assertSampleCounts(1, 0); id3Output.assertSample(0, ID3_DATA_1, 0, C.BUFFER_FLAG_KEY_FRAME, null); } @Test - public void testMultiId3Packet() throws Exception { + public void multiId3Packet() throws Exception { feedLimited(ID3_DATA_1.length + ID3_DATA_2.length - 1); assertSampleCounts(1, 0); id3Output.assertSample(0, ID3_DATA_1, 0, C.BUFFER_FLAG_KEY_FRAME, null); } @Test - public void testMultiId3PacketConsumed() throws Exception { + public void multiId3PacketConsumed() throws Exception { feedLimited(ID3_DATA_1.length + ID3_DATA_2.length); assertSampleCounts(2, 0); id3Output.assertSample(0, ID3_DATA_1, 0, C.BUFFER_FLAG_KEY_FRAME, null); @@ -164,7 +164,7 @@ public class AdtsReaderTest { } @Test - public void testMultiPacketConsumed() throws Exception { + public void multiPacketConsumed() throws Exception { for (int i = 0; i < 10; i++) { data.setPosition(0); feed(); @@ -180,7 +180,7 @@ public class AdtsReaderTest { } @Test - public void testAdtsDataOnly() throws ParserException { + public void adtsDataOnly() throws ParserException { data.setPosition(ID3_DATA_1.length + ID3_DATA_2.length); feed(); assertSampleCounts(0, 1); diff --git a/library/core/src/test/java/com/google/android/exoplayer2/extractor/ts/PsDurationReaderTest.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/ts/PsDurationReaderTest.java similarity index 84% rename from library/core/src/test/java/com/google/android/exoplayer2/extractor/ts/PsDurationReaderTest.java rename to library/extractor/src/test/java/com/google/android/exoplayer2/extractor/ts/PsDurationReaderTest.java index d522178ceb..728a164b11 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/extractor/ts/PsDurationReaderTest.java +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/ts/PsDurationReaderTest.java @@ -42,16 +42,17 @@ public final class PsDurationReaderTest { } @Test - public void testIsDurationReadPending_returnFalseByDefault() { + public void isDurationReadPending_returnFalseByDefault() { assertThat(tsDurationReader.isDurationReadFinished()).isFalse(); } @Test - public void testReadDuration_returnsCorrectDuration() throws IOException, InterruptedException { + public void readDuration_returnsCorrectDuration() throws IOException { FakeExtractorInput input = new FakeExtractorInput.Builder() .setData( - TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), "ts/sample.ps")) + TestUtil.getByteArray( + ApplicationProvider.getApplicationContext(), "ts/sample_h262_mpeg_audio.ps")) .build(); int result = Extractor.RESULT_CONTINUE; @@ -66,12 +67,12 @@ public final class PsDurationReaderTest { } @Test - public void testReadDuration_midStream_returnsCorrectDuration() - throws IOException, InterruptedException { + public void readDuration_midStream_returnsCorrectDuration() throws IOException { FakeExtractorInput input = new FakeExtractorInput.Builder() .setData( - TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), "ts/sample.ps")) + TestUtil.getByteArray( + ApplicationProvider.getApplicationContext(), "ts/sample_h262_mpeg_audio.ps")) .build(); input.setPosition(1234); diff --git a/library/core/src/test/java/com/google/android/exoplayer2/extractor/ts/PsExtractorSeekTest.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/ts/PsExtractorSeekTest.java similarity index 91% rename from library/core/src/test/java/com/google/android/exoplayer2/extractor/ts/PsExtractorSeekTest.java rename to library/extractor/src/test/java/com/google/android/exoplayer2/extractor/ts/PsExtractorSeekTest.java index f974a86622..b5eb3a5e88 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/extractor/ts/PsExtractorSeekTest.java +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/ts/PsExtractorSeekTest.java @@ -60,7 +60,7 @@ public final class PsExtractorSeekTest { private long totalInputLength; @Before - public void setUp() throws IOException, InterruptedException { + public void setUp() throws IOException { expectedOutput = new FakeExtractorOutput(); positionHolder = new PositionHolder(); extractAllSamplesFromFileToExpectedOutput( @@ -74,8 +74,7 @@ public final class PsExtractorSeekTest { } @Test - public void testPsExtractorReads_nonSeekTableFile_returnSeekableSeekMap() - throws IOException, InterruptedException { + public void psExtractorReads_nonSeekTableFile_returnSeekableSeekMap() throws IOException { PsExtractor extractor = new PsExtractor(); SeekMap seekMap = extractSeekMapAndTracks(extractor, new FakeExtractorOutput()); @@ -86,8 +85,8 @@ public final class PsExtractorSeekTest { } @Test - public void testHandlePendingSeek_handlesSeekingToPositionInFile_extractsCorrectFrame() - throws IOException, InterruptedException { + public void handlePendingSeek_handlesSeekingToPositionInFile_extractsCorrectFrame() + throws IOException { PsExtractor extractor = new PsExtractor(); FakeExtractorOutput extractorOutput = new FakeExtractorOutput(); @@ -103,7 +102,7 @@ public final class PsExtractorSeekTest { } @Test - public void testHandlePendingSeek_handlesSeekToEoF() throws IOException, InterruptedException { + public void handlePendingSeek_handlesSeekToEoF() throws IOException, InterruptedException { PsExtractor extractor = new PsExtractor(); FakeExtractorOutput extractorOutput = new FakeExtractorOutput(); @@ -118,8 +117,7 @@ public final class PsExtractorSeekTest { } @Test - public void testHandlePendingSeek_handlesSeekingBackward_extractsCorrectFrame() - throws IOException, InterruptedException { + public void handlePendingSeek_handlesSeekingBackward_extractsCorrectFrame() throws IOException { PsExtractor extractor = new PsExtractor(); FakeExtractorOutput extractorOutput = new FakeExtractorOutput(); @@ -138,8 +136,7 @@ public final class PsExtractorSeekTest { } @Test - public void testHandlePendingSeek_handlesSeekingForward_extractsCorrectFrame() - throws IOException, InterruptedException { + public void handlePendingSeek_handlesSeekingForward_extractsCorrectFrame() throws IOException { PsExtractor extractor = new PsExtractor(); FakeExtractorOutput extractorOutput = new FakeExtractorOutput(); @@ -158,8 +155,7 @@ public final class PsExtractorSeekTest { } @Test - public void testHandlePendingSeek_handlesRandomSeeks_extractsCorrectFrame() - throws IOException, InterruptedException { + public void handlePendingSeek_handlesRandomSeeks_extractsCorrectFrame() throws IOException { PsExtractor extractor = new PsExtractor(); FakeExtractorOutput extractorOutput = new FakeExtractorOutput(); @@ -178,8 +174,8 @@ public final class PsExtractorSeekTest { } @Test - public void testHandlePendingSeek_handlesRandomSeeksAfterReadingFileOnce_extractsCorrectFrame() - throws IOException, InterruptedException { + public void handlePendingSeek_handlesRandomSeeksAfterReadingFileOnce_extractsCorrectFrame() + throws IOException { PsExtractor extractor = new PsExtractor(); FakeExtractorOutput extractorOutput = new FakeExtractorOutput(); @@ -201,8 +197,7 @@ public final class PsExtractorSeekTest { // Internal methods private long readInputLength() throws IOException { - DataSpec dataSpec = - new DataSpec(Uri.parse("asset:///" + PS_FILE_PATH), 0, C.LENGTH_UNSET, null); + DataSpec dataSpec = new DataSpec(Uri.parse("asset:///" + PS_FILE_PATH)); long totalInputLength = dataSource.open(dataSpec); Util.closeQuietly(dataSource); return totalInputLength; @@ -217,7 +212,7 @@ public final class PsExtractorSeekTest { */ private int seekToTimeUs( PsExtractor psExtractor, SeekMap seekMap, long seekTimeUs, FakeTrackOutput trackOutput) - throws IOException, InterruptedException { + throws IOException { int numSampleBeforeSeek = trackOutput.getSampleCount(); SeekMap.SeekPoints seekPoints = seekMap.getSeekPoints(seekTimeUs); @@ -251,7 +246,7 @@ public final class PsExtractorSeekTest { } private SeekMap extractSeekMapAndTracks(PsExtractor extractor, FakeExtractorOutput output) - throws IOException, InterruptedException { + throws IOException { ExtractorInput input = getExtractorInputFromPosition(0); extractor.init(output); int readResult = Extractor.RESULT_CONTINUE; @@ -279,7 +274,7 @@ public final class PsExtractorSeekTest { } private void readInputFileOnce(PsExtractor extractor, FakeExtractorOutput extractorOutput) - throws IOException, InterruptedException { + throws IOException { extractor.init(extractorOutput); int readResult = Extractor.RESULT_CONTINUE; ExtractorInput input = getExtractorInputFromPosition(0); @@ -343,14 +338,13 @@ public final class PsExtractorSeekTest { private ExtractorInput getExtractorInputFromPosition(long position) throws IOException { DataSpec dataSpec = - new DataSpec( - Uri.parse("asset:///" + PS_FILE_PATH), position, C.LENGTH_UNSET, /* key= */ null); + new DataSpec(Uri.parse("asset:///" + PS_FILE_PATH), position, C.LENGTH_UNSET); dataSource.open(dataSpec); return new DefaultExtractorInput(dataSource, position, totalInputLength); } private void extractAllSamplesFromFileToExpectedOutput(Context context, String fileName) - throws IOException, InterruptedException { + throws IOException { byte[] data = TestUtil.getByteArray(context, fileName); PsExtractor extractor = new PsExtractor(); diff --git a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/ts/PsExtractorTest.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/ts/PsExtractorTest.java new file mode 100644 index 0000000000..3425221775 --- /dev/null +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/ts/PsExtractorTest.java @@ -0,0 +1,48 @@ +/* + * 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.extractor.ts; + +import com.google.android.exoplayer2.testutil.ExtractorAsserts; +import java.util.List; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.ParameterizedRobolectricTestRunner; +import org.robolectric.ParameterizedRobolectricTestRunner.Parameter; +import org.robolectric.ParameterizedRobolectricTestRunner.Parameters; + +/** Unit test for {@link PsExtractor}. */ +@RunWith(ParameterizedRobolectricTestRunner.class) +public final class PsExtractorTest { + + @Parameters(name = "{0}") + public static List params() { + return ExtractorAsserts.configs(); + } + + @Parameter(0) + public ExtractorAsserts.SimulationConfig simulationConfig; + + @Test + public void sampleWithH262AndMpegAudio() throws Exception { + ExtractorAsserts.assertBehavior( + PsExtractor::new, "ts/sample_h262_mpeg_audio.ps", simulationConfig); + } + + @Test + public void sampleWithAc3() throws Exception { + ExtractorAsserts.assertBehavior(PsExtractor::new, "ts/sample_ac3.ps", simulationConfig); + } +} diff --git a/library/core/src/test/java/com/google/android/exoplayer2/extractor/ts/SectionReaderTest.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/ts/SectionReaderTest.java similarity index 97% rename from library/core/src/test/java/com/google/android/exoplayer2/extractor/ts/SectionReaderTest.java rename to library/extractor/src/test/java/com/google/android/exoplayer2/extractor/ts/SectionReaderTest.java index a089751464..a023a32729 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/extractor/ts/SectionReaderTest.java +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/ts/SectionReaderTest.java @@ -51,7 +51,7 @@ public final class SectionReaderTest { } @Test - public void testSingleOnePacketSection() { + public void singleOnePacketSection() { packetPayload[0] = 3; insertTableSection(4, (byte) 99, 3); reader.consume(new ParsableByteArray(packetPayload), FLAG_PAYLOAD_UNIT_START_INDICATOR); @@ -59,7 +59,7 @@ public final class SectionReaderTest { } @Test - public void testHeaderSplitAcrossPackets() { + public void headerSplitAcrossPackets() { packetPayload[0] = 3; // The first packet includes a pointer_field. insertTableSection(4, (byte) 100, 3); // This section header spreads across both packets. @@ -74,7 +74,7 @@ public final class SectionReaderTest { } @Test - public void testFiveSectionsInTwoPackets() { + public void fiveSectionsInTwoPackets() { packetPayload[0] = 0; // The first packet includes a pointer_field. insertTableSection(1, (byte) 101, 10); insertTableSection(14, (byte) 102, 10); @@ -94,7 +94,7 @@ public final class SectionReaderTest { } @Test - public void testLongSectionAcrossFourPackets() { + public void longSectionAcrossFourPackets() { packetPayload[0] = 13; // The first packet includes a pointer_field. insertTableSection(1, (byte) 106, 10); // First section. Should be skipped. // Second section spread across four packets. Should be consumed. @@ -124,7 +124,7 @@ public final class SectionReaderTest { } @Test - public void testSeek() { + public void seek() { packetPayload[0] = 13; // The first packet includes a pointer_field. insertTableSection(1, (byte) 109, 10); // First section. Should be skipped. // Second section spread across four packets. Should be consumed. @@ -156,7 +156,7 @@ public final class SectionReaderTest { } @Test - public void testCrcChecks() { + public void crcChecks() { byte[] correctCrcPat = new byte[] { (byte) 0x0, (byte) 0x0, (byte) 0xb0, (byte) 0xd, (byte) 0x0, (byte) 0x1, (byte) 0xc1, (byte) 0x0, (byte) 0x0, (byte) 0x0, (byte) 0x1, (byte) 0xe1, (byte) 0x0, (byte) 0xe8, diff --git a/library/core/src/test/java/com/google/android/exoplayer2/extractor/ts/TsDurationReaderTest.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/ts/TsDurationReaderTest.java similarity index 92% rename from library/core/src/test/java/com/google/android/exoplayer2/extractor/ts/TsDurationReaderTest.java rename to library/extractor/src/test/java/com/google/android/exoplayer2/extractor/ts/TsDurationReaderTest.java index b1531e91f7..7a1a49d712 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/extractor/ts/TsDurationReaderTest.java +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/ts/TsDurationReaderTest.java @@ -42,12 +42,12 @@ public final class TsDurationReaderTest { } @Test - public void testIsDurationReadPending_returnFalseByDefault() { + public void isDurationReadPending_returnFalseByDefault() { assertThat(tsDurationReader.isDurationReadFinished()).isFalse(); } @Test - public void testReadDuration_returnsCorrectDuration() throws IOException, InterruptedException { + public void readDuration_returnsCorrectDuration() throws IOException, InterruptedException { FakeExtractorInput input = new FakeExtractorInput.Builder() .setData( @@ -71,8 +71,7 @@ public final class TsDurationReaderTest { } @Test - public void testReadDuration_midStream_returnsCorrectDuration() - throws IOException, InterruptedException { + public void readDuration_midStream_returnsCorrectDuration() throws IOException { FakeExtractorInput input = new FakeExtractorInput.Builder() .setData( diff --git a/library/core/src/test/java/com/google/android/exoplayer2/extractor/ts/TsExtractorSeekTest.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/ts/TsExtractorSeekTest.java similarity index 91% rename from library/core/src/test/java/com/google/android/exoplayer2/extractor/ts/TsExtractorSeekTest.java rename to library/extractor/src/test/java/com/google/android/exoplayer2/extractor/ts/TsExtractorSeekTest.java index 956ccc2390..42e0acecd4 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/extractor/ts/TsExtractorSeekTest.java +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/ts/TsExtractorSeekTest.java @@ -53,7 +53,7 @@ public final class TsExtractorSeekTest { private PositionHolder positionHolder; @Before - public void setUp() throws IOException, InterruptedException { + public void setUp() throws IOException { positionHolder = new PositionHolder(); expectedTrackOutput = TestUtil.extractAllSamplesFromFile( @@ -67,8 +67,7 @@ public final class TsExtractorSeekTest { } @Test - public void testTsExtractorReads_nonSeekTableFile_returnSeekableSeekMap() - throws IOException, InterruptedException { + public void tsExtractorReads_nonSeekTableFile_returnSeekableSeekMap() throws IOException { Uri fileUri = TestUtil.buildAssetUri(TEST_FILE); TsExtractor extractor = new TsExtractor(); @@ -81,8 +80,8 @@ public final class TsExtractorSeekTest { } @Test - public void testHandlePendingSeek_handlesSeekingToPositionInFile_extractsCorrectFrame() - throws IOException, InterruptedException { + public void handlePendingSeek_handlesSeekingToPositionInFile_extractsCorrectFrame() + throws IOException { TsExtractor extractor = new TsExtractor(); Uri fileUri = TestUtil.buildAssetUri(TEST_FILE); @@ -101,8 +100,7 @@ public final class TsExtractorSeekTest { } @Test - public void testHandlePendingSeek_handlesSeekToEoF_extractsLastFrame() - throws IOException, InterruptedException { + public void handlePendingSeek_handlesSeekToEoF_extractsLastFrame() throws IOException { TsExtractor extractor = new TsExtractor(); Uri fileUri = TestUtil.buildAssetUri(TEST_FILE); @@ -122,8 +120,7 @@ public final class TsExtractorSeekTest { } @Test - public void testHandlePendingSeek_handlesSeekingBackward_extractsCorrectFrame() - throws IOException, InterruptedException { + public void handlePendingSeek_handlesSeekingBackward_extractsCorrectFrame() throws IOException { TsExtractor extractor = new TsExtractor(); Uri fileUri = TestUtil.buildAssetUri(TEST_FILE); @@ -145,8 +142,7 @@ public final class TsExtractorSeekTest { } @Test - public void testHandlePendingSeek_handlesSeekingForward_extractsCorrectFrame() - throws IOException, InterruptedException { + public void handlePendingSeek_handlesSeekingForward_extractsCorrectFrame() throws IOException { TsExtractor extractor = new TsExtractor(); Uri fileUri = TestUtil.buildAssetUri(TEST_FILE); @@ -168,8 +164,7 @@ public final class TsExtractorSeekTest { } @Test - public void testHandlePendingSeek_handlesRandomSeeks_extractsCorrectFrame() - throws IOException, InterruptedException { + public void handlePendingSeek_handlesRandomSeeks_extractsCorrectFrame() throws IOException { TsExtractor extractor = new TsExtractor(); Uri fileUri = TestUtil.buildAssetUri(TEST_FILE); @@ -191,8 +186,8 @@ public final class TsExtractorSeekTest { } @Test - public void testHandlePendingSeek_handlesRandomSeeksAfterReadingFileOnce_extractsCorrectFrame() - throws IOException, InterruptedException { + public void handlePendingSeek_handlesRandomSeeksAfterReadingFileOnce_extractsCorrectFrame() + throws IOException { TsExtractor extractor = new TsExtractor(); Uri fileUri = TestUtil.buildAssetUri(TEST_FILE); @@ -217,8 +212,7 @@ public final class TsExtractorSeekTest { // Internal methods private void readInputFileOnce( - TsExtractor extractor, FakeExtractorOutput extractorOutput, Uri fileUri) - throws IOException, InterruptedException { + TsExtractor extractor, FakeExtractorOutput extractorOutput, Uri fileUri) throws IOException { extractor.init(extractorOutput); int readResult = Extractor.RESULT_CONTINUE; ExtractorInput input = TestUtil.getExtractorInputFromPosition(dataSource, 0, fileUri); diff --git a/library/core/src/test/java/com/google/android/exoplayer2/extractor/ts/TsExtractorTest.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/ts/TsExtractorTest.java similarity index 68% rename from library/core/src/test/java/com/google/android/exoplayer2/extractor/ts/TsExtractorTest.java rename to library/extractor/src/test/java/com/google/android/exoplayer2/extractor/ts/TsExtractorTest.java index f1b962a712..18b6978967 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/extractor/ts/TsExtractorTest.java +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/ts/TsExtractorTest.java @@ -15,11 +15,12 @@ */ package com.google.android.exoplayer2.extractor.ts; +import static com.google.android.exoplayer2.extractor.ts.DefaultTsPayloadReaderFactory.FLAG_DETECT_ACCESS_UNITS; import static com.google.common.truth.Truth.assertThat; import android.util.SparseArray; +import androidx.annotation.Nullable; import androidx.test.core.app.ApplicationProvider; -import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.extractor.Extractor; @@ -35,58 +36,120 @@ import com.google.android.exoplayer2.testutil.FakeTrackOutput; import com.google.android.exoplayer2.testutil.TestUtil; import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.TimestampAdjuster; -import java.io.ByteArrayOutputStream; -import java.util.Random; +import java.util.List; import org.junit.Test; import org.junit.runner.RunWith; +import org.robolectric.ParameterizedRobolectricTestRunner; +import org.robolectric.ParameterizedRobolectricTestRunner.Parameter; +import org.robolectric.ParameterizedRobolectricTestRunner.Parameters; /** Unit test for {@link TsExtractor}. */ -@RunWith(AndroidJUnit4.class) +@RunWith(ParameterizedRobolectricTestRunner.class) public final class TsExtractorTest { - private static final int TS_PACKET_SIZE = 188; - private static final int TS_SYNC_BYTE = 0x47; // First byte of each TS packet. + @Parameters(name = "{0}") + public static List params() { + return ExtractorAsserts.configs(); + } + + @Parameter(0) + public ExtractorAsserts.SimulationConfig simulationConfig; @Test - public void testSample() throws Exception { - ExtractorAsserts.assertBehavior(TsExtractor::new, "ts/sample.ts"); + public void sampleWithH262AndMpegAudio() throws Exception { + ExtractorAsserts.assertBehavior( + TsExtractor::new, "ts/sample_h262_mpeg_audio.ts", simulationConfig); } @Test - public void testStreamWithJunkData() throws Exception { - Random random = new Random(0); - byte[] fileData = - TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), "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); - 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)); - 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); - fileData = out.toByteArray(); - - ExtractorAsserts.assertOutput( - TsExtractor::new, "ts/sample.ts", fileData, ApplicationProvider.getApplicationContext()); + public void sampleWithH264AndMpegAudio() throws Exception { + ExtractorAsserts.assertBehavior( + TsExtractor::new, "ts/sample_h264_mpeg_audio.ts", simulationConfig); } @Test - public void testCustomPesReader() throws Exception { + public void sampleWithH264NoAccessUnitDelimiters() throws Exception { + ExtractorAsserts.assertBehavior( + () -> new TsExtractor(FLAG_DETECT_ACCESS_UNITS), + "ts/sample_h264_no_access_unit_delimiters.ts", + simulationConfig); + } + + @Test + public void sampleWithH264AndDtsAudio() throws Exception { + ExtractorAsserts.assertBehavior( + () -> new TsExtractor(DefaultTsPayloadReaderFactory.FLAG_ENABLE_HDMV_DTS_AUDIO_STREAMS), + "ts/sample_h264_dts_audio.ts", + simulationConfig); + } + + @Test + public void sampleWithH265() throws Exception { + ExtractorAsserts.assertBehavior(TsExtractor::new, "ts/sample_h265.ts", simulationConfig); + } + + @Test + public void sampleWithScte35() throws Exception { + ExtractorAsserts.assertBehavior( + TsExtractor::new, + "ts/sample_scte35.ts", + new ExtractorAsserts.AssertionConfig.Builder() + .setDeduplicateConsecutiveFormats(true) + .build(), + simulationConfig); + } + + @Test + public void sampleWithAit() throws Exception { + ExtractorAsserts.assertBehavior( + TsExtractor::new, + "ts/sample_ait.ts", + new ExtractorAsserts.AssertionConfig.Builder() + .setDeduplicateConsecutiveFormats(true) + .build(), + simulationConfig); + } + + @Test + public void sampleWithAc3() throws Exception { + ExtractorAsserts.assertBehavior(TsExtractor::new, "ts/sample_ac3.ts", simulationConfig); + } + + @Test + public void sampleWithAc4() throws Exception { + ExtractorAsserts.assertBehavior(TsExtractor::new, "ts/sample_ac4.ts", simulationConfig); + } + + @Test + public void sampleWithEac3() throws Exception { + ExtractorAsserts.assertBehavior(TsExtractor::new, "ts/sample_eac3.ts", simulationConfig); + } + + @Test + public void sampleWithEac3joc() throws Exception { + ExtractorAsserts.assertBehavior(TsExtractor::new, "ts/sample_eac3joc.ts", simulationConfig); + } + + @Test + public void sampleWithLatm() throws Exception { + ExtractorAsserts.assertBehavior(TsExtractor::new, "ts/sample_latm.ts", simulationConfig); + } + + @Test + public void streamWithJunkData() throws Exception { + ExtractorAsserts.assertBehavior(TsExtractor::new, "ts/sample_with_junk", simulationConfig); + } + + @Test + public void customPesReader() throws Exception { CustomTsPayloadReaderFactory factory = new CustomTsPayloadReaderFactory(true, false); TsExtractor tsExtractor = new TsExtractor(TsExtractor.MODE_MULTI_PMT, new TimestampAdjuster(0), factory); FakeExtractorInput input = new FakeExtractorInput.Builder() .setData( - TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), "ts/sample.ts")) + TestUtil.getByteArray( + ApplicationProvider.getApplicationContext(), "ts/sample_h262_mpeg_audio.ts")) .setSimulateIOErrors(false) .setSimulateUnknownLength(false) .setSimulatePartialReads(false) @@ -105,12 +168,17 @@ public final class TsExtractorTest { assertThat(reader.packetsRead).isEqualTo(2); TrackOutput trackOutput = reader.getTrackOutput(); assertThat(trackOutput == output.trackOutputs.get(257 /* PID of audio track. */)).isTrue(); - assertThat(((FakeTrackOutput) trackOutput).format) - .isEqualTo(Format.createTextSampleFormat("1/257", "mime", null, 0, 0, "und", null, 0)); + assertThat(((FakeTrackOutput) trackOutput).lastFormat) + .isEqualTo( + new Format.Builder() + .setId("1/257") + .setSampleMimeType("mime") + .setLanguage("und") + .build()); } @Test - public void testCustomInitialSectionReader() throws Exception { + public void customInitialSectionReader() throws Exception { CustomTsPayloadReaderFactory factory = new CustomTsPayloadReaderFactory(false, true); TsExtractor tsExtractor = new TsExtractor(TsExtractor.MODE_MULTI_PMT, new TimestampAdjuster(0), factory); @@ -135,16 +203,6 @@ public final class TsExtractorTest { assertThat(factory.sdtReader.consumedSdts).isEqualTo(2); } - private static void writeJunkData(ByteArrayOutputStream out, int length) { - for (int i = 0; i < length; i++) { - if (((byte) i) == TS_SYNC_BYTE) { - out.write(0); - } else { - out.write(i); - } - } - } - private static final class CustomTsPayloadReaderFactory implements TsPayloadReader.Factory { private final boolean provideSdtReader; @@ -173,6 +231,7 @@ public final class TsExtractorTest { } @Override + @Nullable public TsPayloadReader createPayloadReader(int streamType, EsInfo esInfo) { if (provideCustomEsReader && streamType == 3) { esReader = new CustomEsReader(esInfo.language); @@ -201,8 +260,11 @@ public final class TsExtractorTest { idGenerator.generateNewId(); output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_UNKNOWN); output.format( - Format.createTextSampleFormat( - idGenerator.getFormatId(), "mime", null, 0, 0, language, null, 0)); + new Format.Builder() + .setId(idGenerator.getFormatId()) + .setSampleMimeType("mime") + .setLanguage(language) + .build()); } @Override diff --git a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/wav/WavExtractorTest.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/wav/WavExtractorTest.java new file mode 100644 index 0000000000..07586e3af8 --- /dev/null +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/wav/WavExtractorTest.java @@ -0,0 +1,56 @@ +/* + * 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.extractor.wav; + +import com.google.android.exoplayer2.testutil.ExtractorAsserts; +import com.google.android.exoplayer2.testutil.ExtractorAsserts.AssertionConfig; +import java.util.List; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.ParameterizedRobolectricTestRunner; + +/** Unit test for {@link WavExtractor}. */ +@RunWith(ParameterizedRobolectricTestRunner.class) +public final class WavExtractorTest { + + @ParameterizedRobolectricTestRunner.Parameters(name = "{0}") + public static List params() { + return ExtractorAsserts.configs(); + } + + @ParameterizedRobolectricTestRunner.Parameter(0) + public ExtractorAsserts.SimulationConfig simulationConfig; + + @Test + public void sample() throws Exception { + ExtractorAsserts.assertBehavior(WavExtractor::new, "wav/sample.wav", simulationConfig); + } + + @Test + public void sample_withTrailingBytes_extractsSameData() throws Exception { + ExtractorAsserts.assertBehavior( + WavExtractor::new, + "wav/sample_with_trailing_bytes.wav", + new AssertionConfig.Builder().setDumpFilesPrefix("wav/sample.wav").build(), + simulationConfig); + } + + @Test + public void sample_imaAdpcm() throws Exception { + ExtractorAsserts.assertBehavior( + WavExtractor::new, "wav/sample_ima_adpcm.wav", simulationConfig); + } +} diff --git a/library/hls/build.gradle b/library/hls/build.gradle index df880f7f41..4764cf9882 100644 --- a/library/hls/build.gradle +++ b/library/hls/build.gradle @@ -40,6 +40,7 @@ dependencies { implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion compileOnly 'org.checkerframework:checker-compat-qual:' + checkerframeworkVersion + compileOnly 'org.jetbrains.kotlin:kotlin-annotations-jvm:' + kotlinAnnotationsVersion implementation project(modulePrefix + 'library-core') testImplementation project(modulePrefix + 'testutils') testImplementation 'org.robolectric:robolectric:' + robolectricVersion 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 8db6166bb8..2ba2cd83af 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 @@ -19,7 +19,6 @@ import android.net.Uri; import android.text.TextUtils; import androidx.annotation.Nullable; import com.google.android.exoplayer2.Format; -import com.google.android.exoplayer2.drm.DrmInitData; import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.extractor.mp3.Mp3Extractor; @@ -30,6 +29,7 @@ import com.google.android.exoplayer2.extractor.ts.AdtsExtractor; import com.google.android.exoplayer2.extractor.ts.DefaultTsPayloadReaderFactory; import com.google.android.exoplayer2.extractor.ts.TsExtractor; import com.google.android.exoplayer2.metadata.Metadata; +import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.TimestampAdjuster; import java.io.EOFException; @@ -52,6 +52,8 @@ public final class DefaultHlsExtractorFactory implements HlsExtractorFactory { public static final String M4_FILE_EXTENSION_PREFIX = ".m4"; public static final String MP4_FILE_EXTENSION_PREFIX = ".mp4"; public static final String CMF_FILE_EXTENSION_PREFIX = ".cmf"; + public static final String TS_FILE_EXTENSION = ".ts"; + public static final String TS_FILE_EXTENSION_PREFIX = ".ts"; public static final String VTT_FILE_EXTENSION = ".vtt"; public static final String WEBVTT_FILE_EXTENSION = ".webvtt"; @@ -89,14 +91,13 @@ public final class DefaultHlsExtractorFactory implements HlsExtractorFactory { Uri uri, Format format, @Nullable List muxedCaptionFormats, - @Nullable DrmInitData drmInitData, TimestampAdjuster timestampAdjuster, Map> responseHeaders, ExtractorInput extractorInput) - throws InterruptedException, IOException { + throws IOException { if (previousExtractor != null) { - // A extractor has already been successfully used. Return one of the same type. + // An extractor has already been successfully used. Return one of the same type. if (isReusable(previousExtractor)) { return buildResult(previousExtractor); } else { @@ -110,16 +111,29 @@ public final class DefaultHlsExtractorFactory implements HlsExtractorFactory { } // Try selecting the extractor by the file extension. + @Nullable Extractor extractorByFileExtension = - createExtractorByFileExtension( - uri, format, muxedCaptionFormats, drmInitData, timestampAdjuster); + createExtractorByFileExtension(uri, format, muxedCaptionFormats, timestampAdjuster); extractorInput.resetPeekPosition(); - if (sniffQuietly(extractorByFileExtension, extractorInput)) { + if (extractorByFileExtension != null + && sniffQuietly(extractorByFileExtension, extractorInput)) { return buildResult(extractorByFileExtension); } // We need to manually sniff each known type, without retrying the one selected by file - // extension. + // extension. Extractors order is optimized according to + // https://docs.google.com/document/d/1w2mKaWMxfz2Ei8-LdxqbPs1VLe_oudB-eryXXw9OvQQ. + + // Extractor to be used if the type is not recognized. + @Nullable Extractor fallBackExtractor = extractorByFileExtension; + + if (!(extractorByFileExtension instanceof FragmentedMp4Extractor)) { + FragmentedMp4Extractor fragmentedMp4Extractor = + createFragmentedMp4Extractor(timestampAdjuster, format, muxedCaptionFormats); + if (sniffQuietly(fragmentedMp4Extractor, extractorInput)) { + return buildResult(fragmentedMp4Extractor); + } + } if (!(extractorByFileExtension instanceof WebvttExtractor)) { WebvttExtractor webvttExtractor = new WebvttExtractor(format.language, timestampAdjuster); @@ -128,6 +142,22 @@ public final class DefaultHlsExtractorFactory implements HlsExtractorFactory { } } + if (!(extractorByFileExtension instanceof TsExtractor)) { + TsExtractor tsExtractor = + createTsExtractor( + payloadReaderFactoryFlags, + exposeCea608WhenMissingDeclarations, + format, + muxedCaptionFormats, + timestampAdjuster); + if (sniffQuietly(tsExtractor, extractorInput)) { + return buildResult(tsExtractor); + } + if (fallBackExtractor == null) { + fallBackExtractor = tsExtractor; + } + } + if (!(extractorByFileExtension instanceof AdtsExtractor)) { AdtsExtractor adtsExtractor = new AdtsExtractor(); if (sniffQuietly(adtsExtractor, extractorInput)) { @@ -157,36 +187,14 @@ public final class DefaultHlsExtractorFactory implements HlsExtractorFactory { } } - if (!(extractorByFileExtension instanceof FragmentedMp4Extractor)) { - FragmentedMp4Extractor fragmentedMp4Extractor = - createFragmentedMp4Extractor(timestampAdjuster, format, drmInitData, muxedCaptionFormats); - if (sniffQuietly(fragmentedMp4Extractor, extractorInput)) { - return buildResult(fragmentedMp4Extractor); - } - } - - if (!(extractorByFileExtension instanceof TsExtractor)) { - TsExtractor tsExtractor = - createTsExtractor( - payloadReaderFactoryFlags, - exposeCea608WhenMissingDeclarations, - format, - muxedCaptionFormats, - timestampAdjuster); - if (sniffQuietly(tsExtractor, extractorInput)) { - return buildResult(tsExtractor); - } - } - - // Fall back on the extractor created by file extension. - return buildResult(extractorByFileExtension); + return buildResult(Assertions.checkNotNull(fallBackExtractor)); } + @Nullable private Extractor createExtractorByFileExtension( Uri uri, Format format, @Nullable List muxedCaptionFormats, - @Nullable DrmInitData drmInitData, TimestampAdjuster timestampAdjuster) { String lastPathSegment = uri.getLastPathSegment(); if (lastPathSegment == null) { @@ -209,16 +217,17 @@ public final class DefaultHlsExtractorFactory implements HlsExtractorFactory { || lastPathSegment.startsWith(M4_FILE_EXTENSION_PREFIX, lastPathSegment.length() - 4) || lastPathSegment.startsWith(MP4_FILE_EXTENSION_PREFIX, lastPathSegment.length() - 5) || lastPathSegment.startsWith(CMF_FILE_EXTENSION_PREFIX, lastPathSegment.length() - 5)) { - return createFragmentedMp4Extractor( - timestampAdjuster, format, drmInitData, muxedCaptionFormats); - } else { - // For any other file extension, we assume TS format. + return createFragmentedMp4Extractor(timestampAdjuster, format, muxedCaptionFormats); + } else if (lastPathSegment.endsWith(TS_FILE_EXTENSION) + || lastPathSegment.startsWith(TS_FILE_EXTENSION_PREFIX, lastPathSegment.length() - 4)) { return createTsExtractor( payloadReaderFactoryFlags, exposeCea608WhenMissingDeclarations, format, muxedCaptionFormats, timestampAdjuster); + } else { + return null; } } @@ -240,15 +249,11 @@ public final class DefaultHlsExtractorFactory implements HlsExtractorFactory { // closed caption track on channel 0. muxedCaptionFormats = Collections.singletonList( - Format.createTextSampleFormat( - /* id= */ null, - MimeTypes.APPLICATION_CEA608, - /* selectionFlags= */ 0, - /* language= */ null)); + new Format.Builder().setSampleMimeType(MimeTypes.APPLICATION_CEA608).build()); } else { muxedCaptionFormats = Collections.emptyList(); } - String codecs = format.codecs; + @Nullable String codecs = format.codecs; if (!TextUtils.isEmpty(codecs)) { // Sometimes AAC and H264 streams are declared in TS chunks even though they don't really // exist. If we know from the codec attribute that they don't exist, then we can @@ -270,7 +275,6 @@ public final class DefaultHlsExtractorFactory implements HlsExtractorFactory { private static FragmentedMp4Extractor createFragmentedMp4Extractor( TimestampAdjuster timestampAdjuster, Format format, - @Nullable DrmInitData drmInitData, @Nullable List muxedCaptionFormats) { // Only enable the EMSG TrackOutput if this is the 'variant' track (i.e. the main one) to avoid // creating a separate EMSG track for every audio track in a video stream. @@ -278,7 +282,6 @@ public final class DefaultHlsExtractorFactory implements HlsExtractorFactory { /* flags= */ isFmp4Variant(format) ? FragmentedMp4Extractor.FLAG_ENABLE_EMSG_TRACK : 0, timestampAdjuster, /* sideloadedTrack= */ null, - drmInitData, muxedCaptionFormats != null ? muxedCaptionFormats : Collections.emptyList()); } @@ -326,7 +329,7 @@ public final class DefaultHlsExtractorFactory implements HlsExtractorFactory { } private static boolean sniffQuietly(Extractor extractor, ExtractorInput input) - throws InterruptedException, IOException { + throws IOException { boolean result = false; try { result = extractor.sniff(input); 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 a7caf2d7aa..76377deaa3 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 @@ -519,7 +519,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; return null; } - byte[] encryptionKey = keyCache.remove(keyUri); + @Nullable byte[] encryptionKey = keyCache.remove(keyUri); if (encryptionKey != null) { // The key was present in the key cache. We re-insert it to prevent it from being evicted by // the following key addition. Note that removal of the key is necessary to affect the @@ -527,7 +527,8 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; keyCache.put(keyUri, encryptionKey); return null; } - DataSpec dataSpec = new DataSpec(keyUri, 0, C.LENGTH_UNSET, null, DataSpec.FLAG_ALLOW_GZIP); + DataSpec dataSpec = + new DataSpec.Builder().setUri(keyUri).setFlags(DataSpec.FLAG_ALLOW_GZIP).build(); return new EncryptionKeyChunk( encryptionDataSource, dataSpec, @@ -653,8 +654,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; checkInBounds(); 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); + return new DataSpec(chunkUri, segment.byteRangeOffset, segment.byteRangeLength); } @Override 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 927b79899d..eb3cf8bfcf 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 @@ -18,7 +18,6 @@ package com.google.android.exoplayer2.source.hls; import android.net.Uri; import androidx.annotation.Nullable; import com.google.android.exoplayer2.Format; -import com.google.android.exoplayer2.drm.DrmInitData; import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.extractor.PositionHolder; @@ -71,7 +70,6 @@ public interface HlsExtractorFactory { * @param format A {@link Format} associated with the chunk to extract. * @param muxedCaptionFormats List of muxed caption {@link Format}s. Null if no closed caption * 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. @@ -79,7 +77,6 @@ public interface HlsExtractorFactory { * extractor's {@link Extractor#read(ExtractorInput, PositionHolder)}. Must only be used to * call {@link Extractor#sniff(ExtractorInput)}. * @return A {@link Result}. - * @throws InterruptedException If the thread is interrupted while sniffing. * @throws IOException If an I/O error is encountered while sniffing. */ Result createExtractor( @@ -87,9 +84,8 @@ public interface HlsExtractorFactory { Uri uri, Format format, @Nullable List muxedCaptionFormats, - @Nullable DrmInitData drmInitData, TimestampAdjuster timestampAdjuster, Map> responseHeaders, ExtractorInput sniffingExtractorInput) - throws InterruptedException, IOException; + throws IOException; } 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 0546fa5429..ac5aae404b 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 @@ -39,6 +39,7 @@ import com.google.android.exoplayer2.util.UriUtil; import com.google.android.exoplayer2.util.Util; import java.io.EOFException; import java.io.IOException; +import java.io.InterruptedIOException; import java.math.BigInteger; import java.util.Arrays; import java.util.List; @@ -95,10 +96,10 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; DataSpec dataSpec = new DataSpec( UriUtil.resolveToUri(mediaPlaylist.baseUri, mediaSegment.url), - mediaSegment.byterangeOffset, - mediaSegment.byterangeLength, - /* key= */ null); + mediaSegment.byteRangeOffset, + mediaSegment.byteRangeLength); boolean mediaSegmentEncrypted = mediaSegmentKey != null; + @Nullable byte[] mediaSegmentIv = mediaSegmentEncrypted ? getEncryptionIvArray(Assertions.checkNotNull(mediaSegment.encryptionIV)) @@ -109,20 +110,17 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; HlsMediaPlaylist.Segment initSegment = mediaSegment.initializationSegment; DataSpec initDataSpec = null; boolean initSegmentEncrypted = false; - DataSource initDataSource = null; + @Nullable DataSource initDataSource = null; if (initSegment != null) { initSegmentEncrypted = initSegmentKey != null; + @Nullable byte[] initSegmentIv = initSegmentEncrypted ? getEncryptionIvArray(Assertions.checkNotNull(initSegment.encryptionIV)) : null; Uri initSegmentUri = UriUtil.resolveToUri(mediaPlaylist.baseUri, initSegment.url); initDataSpec = - new DataSpec( - initSegmentUri, - initSegment.byterangeOffset, - initSegment.byterangeLength, - /* key= */ null); + new DataSpec(initSegmentUri, initSegment.byteRangeOffset, initSegment.byteRangeLength); initDataSource = buildDataSource(dataSource, initSegmentKey, initSegmentIv); } @@ -131,7 +129,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; int discontinuitySequenceNumber = mediaPlaylist.discontinuitySequence + mediaSegment.relativeDiscontinuitySequence; - Extractor previousExtractor = null; + @Nullable Extractor previousExtractor = null; Id3Decoder id3Decoder; ParsableByteArray scratchId3Data; boolean shouldSpliceIn; @@ -198,6 +196,9 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; /** The url of the playlist from which this chunk was obtained. */ public final Uri playlistUrl; + /** Whether the samples parsed from this chunk should be spliced into already queued samples. */ + public final boolean shouldSpliceIn; + @Nullable private final DataSource initDataSource; @Nullable private final DataSpec initDataSpec; @Nullable private final Extractor previousExtractor; @@ -205,7 +206,6 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; private final boolean isMasterTimestampSource; private final boolean hasGapTag; private final TimestampAdjuster timestampAdjuster; - private final boolean shouldSpliceIn; private final HlsExtractorFactory extractorFactory; @Nullable private final List muxedCaptionFormats; @Nullable private final DrmInitData drmInitData; @@ -214,9 +214,9 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; private final boolean mediaSegmentEncrypted; private final boolean initSegmentEncrypted; - @MonotonicNonNull private Extractor extractor; + private @MonotonicNonNull Extractor extractor; private boolean isExtractorReusable; - @MonotonicNonNull private HlsSampleStreamWrapper output; + private @MonotonicNonNull HlsSampleStreamWrapper output; // nextLoadPosition refers to the init segment if initDataLoadRequired is true. // Otherwise, nextLoadPosition refers to the media segment. private int nextLoadPosition; @@ -328,14 +328,13 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; } @Override - public void load() throws IOException, InterruptedException { + public void load() throws IOException { // output == null means init() hasn't been called. Assertions.checkNotNull(output); if (extractor == null && previousExtractor != null) { extractor = previousExtractor; isExtractorReusable = true; initDataLoadRequired = false; - output.init(uid, shouldSpliceIn, /* reusingExtractor= */ true); } maybeLoadInitData(); if (!loadCanceled) { @@ -349,7 +348,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; // Internal methods. @RequiresNonNull("output") - private void maybeLoadInitData() throws IOException, InterruptedException { + private void maybeLoadInitData() throws IOException { if (!initDataLoadRequired) { return; } @@ -362,9 +361,13 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; } @RequiresNonNull("output") - private void loadMedia() throws IOException, InterruptedException { + private void loadMedia() throws IOException { if (!isMasterTimestampSource) { - timestampAdjuster.waitUntilInitialized(); + try { + timestampAdjuster.waitUntilInitialized(); + } catch (InterruptedException e) { + throw new InterruptedIOException(); + } } else if (timestampAdjuster.getFirstSampleTimestampUs() == TimestampAdjuster.DO_NOT_OFFSET) { // We're the master and we haven't set the desired first sample timestamp yet. timestampAdjuster.setFirstSampleTimestampUs(startTimeUs); @@ -379,8 +382,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; */ @RequiresNonNull("output") private void feedDataToExtractor( - DataSource dataSource, DataSpec dataSpec, boolean dataIsEncrypted) - throws IOException, InterruptedException { + DataSource dataSource, DataSpec dataSpec, boolean dataIsEncrypted) throws IOException { // If we previously fed part of this chunk to the extractor, we need to skip it this time. For // encrypted content we need to skip the data by reading it through the source, so as to ensure // correct decryption of the remainder of the chunk. For clear content, we can request the @@ -405,7 +407,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; result = extractor.read(input, DUMMY_POSITION_HOLDER); } } finally { - nextLoadPosition = (int) (input.getPosition() - dataSpec.absoluteStreamPosition); + nextLoadPosition = (int) (input.getPosition() - dataSpec.position); } } finally { Util.closeQuietly(dataSource); @@ -415,11 +417,10 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; @RequiresNonNull("output") @EnsuresNonNull("extractor") private DefaultExtractorInput prepareExtraction(DataSource dataSource, DataSpec dataSpec) - throws IOException, InterruptedException { + throws IOException { long bytesToRead = dataSource.open(dataSpec); - DefaultExtractorInput extractorInput = - new DefaultExtractorInput(dataSource, dataSpec.absoluteStreamPosition, bytesToRead); + new DefaultExtractorInput(dataSource, dataSpec.position, bytesToRead); if (extractor == null) { long id3Timestamp = peekId3PrivTimestamp(extractorInput); @@ -431,7 +432,6 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; dataSpec.uri, trackFormat, muxedCaptionFormats, - drmInitData, timestampAdjuster, dataSource.getResponseHeaders(), extractorInput); @@ -447,24 +447,23 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; // the timestamp offset. output.setSampleOffsetUs(/* sampleOffsetUs= */ 0L); } - output.init(uid, shouldSpliceIn, /* reusingExtractor= */ false); + output.onNewExtractor(); extractor.init(output); } - + output.setDrmInitData(drmInitData); return extractorInput; } /** - * 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 - * found. This method only modifies the peek position. + * 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 found. + * This method only modifies the peek position. * * @param input The {@link ExtractorInput} to obtain the PRIV frame from. * @return The parsed, adjusted timestamp in microseconds * @throws IOException If an error occurred peeking from the input. - * @throws InterruptedException If the thread was interrupted. */ - private long peekId3PrivTimestamp(ExtractorInput input) throws IOException, InterruptedException { + private long peekId3PrivTimestamp(ExtractorInput input) throws IOException { input.resetPeekPosition(); try { input.peekFully(scratchId3Data.data, 0, Id3Decoder.ID3_HEADER_LENGTH); @@ -546,5 +545,4 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; } return dataSource; } - } 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 f74d9b0b0c..b6985a836c 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 @@ -67,7 +67,7 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper private final HlsPlaylistTracker playlistTracker; private final HlsDataSourceFactory dataSourceFactory; @Nullable private final TransferListener mediaTransferListener; - private final DrmSessionManager drmSessionManager; + private final DrmSessionManager drmSessionManager; private final LoadErrorHandlingPolicy loadErrorHandlingPolicy; private final EventDispatcher eventDispatcher; private final Allocator allocator; @@ -75,7 +75,7 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper private final TimestampAdjusterProvider timestampAdjusterProvider; private final CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory; private final boolean allowChunklessPreparation; - private final @HlsMetadataType int metadataType; + private final @HlsMediaSource.MetadataType int metadataType; private final boolean useSessionKeys; @Nullable private Callback callback; @@ -112,13 +112,13 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper HlsPlaylistTracker playlistTracker, HlsDataSourceFactory dataSourceFactory, @Nullable TransferListener mediaTransferListener, - DrmSessionManager drmSessionManager, + DrmSessionManager drmSessionManager, LoadErrorHandlingPolicy loadErrorHandlingPolicy, EventDispatcher eventDispatcher, Allocator allocator, CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory, boolean allowChunklessPreparation, - @HlsMetadataType int metadataType, + @HlsMediaSource.MetadataType int metadataType, boolean useSessionKeys) { this.extractorFactory = extractorFactory; this.playlistTracker = playlistTracker; @@ -660,18 +660,16 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper TrackGroup id3TrackGroup = new TrackGroup( - Format.createSampleFormat( - /* id= */ "ID3", - MimeTypes.APPLICATION_ID3, - /* codecs= */ null, - /* bitrate= */ Format.NO_VALUE, - /* drmInitData= */ null)); + new Format.Builder() + .setId("ID3") + .setSampleMimeType(MimeTypes.APPLICATION_ID3) + .build()); muxedTrackGroups.add(id3TrackGroup); sampleStreamWrapper.prepareWithMasterPlaylistInfo( muxedTrackGroups.toArray(new TrackGroup[0]), /* primaryTrackGroupIndex= */ 0, - /* optionalTrackGroupsIndices= */ muxedTrackGroups.indexOf(id3TrackGroup)); + /* optionalTrackGroupsIndices...= */ muxedTrackGroups.indexOf(id3TrackGroup)); } } @@ -791,33 +789,34 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper } private static Format deriveVideoFormat(Format variantFormat) { - String codecs = Util.getCodecsOfType(variantFormat.codecs, C.TRACK_TYPE_VIDEO); - String sampleMimeType = MimeTypes.getMediaMimeType(codecs); - return Format.createVideoContainerFormat( - variantFormat.id, - variantFormat.label, - variantFormat.containerMimeType, - sampleMimeType, - codecs, - variantFormat.metadata, - variantFormat.bitrate, - variantFormat.width, - variantFormat.height, - variantFormat.frameRate, - /* initializationData= */ null, - variantFormat.selectionFlags, - variantFormat.roleFlags); + @Nullable String codecs = Util.getCodecsOfType(variantFormat.codecs, C.TRACK_TYPE_VIDEO); + @Nullable String sampleMimeType = MimeTypes.getMediaMimeType(codecs); + return new Format.Builder() + .setId(variantFormat.id) + .setLabel(variantFormat.label) + .setContainerMimeType(variantFormat.containerMimeType) + .setSampleMimeType(sampleMimeType) + .setCodecs(codecs) + .setMetadata(variantFormat.metadata) + .setAverageBitrate(variantFormat.averageBitrate) + .setPeakBitrate(variantFormat.peakBitrate) + .setWidth(variantFormat.width) + .setHeight(variantFormat.height) + .setFrameRate(variantFormat.frameRate) + .setSelectionFlags(variantFormat.selectionFlags) + .setRoleFlags(variantFormat.roleFlags) + .build(); } private static Format deriveAudioFormat( Format variantFormat, @Nullable Format mediaTagFormat, boolean isPrimaryTrackInVariant) { - String codecs; - Metadata metadata; + @Nullable String codecs; + @Nullable Metadata metadata; int channelCount = Format.NO_VALUE; int selectionFlags = 0; int roleFlags = 0; - String language = null; - String label = null; + @Nullable String language = null; + @Nullable String label = null; if (mediaTagFormat != null) { codecs = mediaTagFormat.codecs; metadata = mediaTagFormat.metadata; @@ -837,22 +836,23 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper label = variantFormat.label; } } - String sampleMimeType = MimeTypes.getMediaMimeType(codecs); - int bitrate = isPrimaryTrackInVariant ? variantFormat.bitrate : Format.NO_VALUE; - return Format.createAudioContainerFormat( - variantFormat.id, - label, - variantFormat.containerMimeType, - sampleMimeType, - codecs, - metadata, - bitrate, - channelCount, - /* sampleRate= */ Format.NO_VALUE, - /* initializationData= */ null, - selectionFlags, - roleFlags, - language); + @Nullable String sampleMimeType = MimeTypes.getMediaMimeType(codecs); + int averageBitrate = isPrimaryTrackInVariant ? variantFormat.averageBitrate : Format.NO_VALUE; + int peakBitrate = isPrimaryTrackInVariant ? variantFormat.peakBitrate : Format.NO_VALUE; + return new Format.Builder() + .setId(variantFormat.id) + .setLabel(label) + .setContainerMimeType(variantFormat.containerMimeType) + .setSampleMimeType(sampleMimeType) + .setCodecs(codecs) + .setMetadata(metadata) + .setAverageBitrate(averageBitrate) + .setPeakBitrate(peakBitrate) + .setChannelCount(channelCount) + .setSelectionFlags(selectionFlags) + .setRoleFlags(roleFlags) + .setLanguage(language) + .build(); } } 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 4f6a0405f2..39fa99c498 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 @@ -15,11 +15,15 @@ */ package com.google.android.exoplayer2.source.hls; +import static java.lang.annotation.RetentionPolicy.SOURCE; + import android.net.Uri; import android.os.Handler; +import androidx.annotation.IntDef; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlayerLibraryInfo; +import com.google.android.exoplayer2.MediaItem; import com.google.android.exoplayer2.drm.DrmSession; import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.extractor.Extractor; @@ -47,6 +51,9 @@ 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; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.util.Collections; import java.util.List; /** An HLS {@link MediaSource}. */ @@ -57,6 +64,28 @@ public final class HlsMediaSource extends BaseMediaSource ExoPlayerLibraryInfo.registerModule("goog.exo.hls"); } + /** + * The types of metadata that can be extracted from HLS streams. + * + *

      Allowed values: + * + *

        + *
      • {@link #METADATA_TYPE_ID3} + *
      • {@link #METADATA_TYPE_EMSG} + *
      + * + *

      See {@link Factory#setMetadataType(int)}. + */ + @Documented + @Retention(SOURCE) + @IntDef({METADATA_TYPE_ID3, METADATA_TYPE_EMSG}) + public @interface MetadataType {} + + /** Type for ID3 metadata in HLS streams. */ + public static final int METADATA_TYPE_ID3 = 1; + /** Type for ESMG metadata in HLS streams. */ + public static final int METADATA_TYPE_EMSG = 3; + /** Factory for {@link HlsMediaSource}s. */ public static final class Factory implements MediaSourceFactory { @@ -64,15 +93,14 @@ public final class HlsMediaSource extends BaseMediaSource private HlsExtractorFactory extractorFactory; private HlsPlaylistParserFactory playlistParserFactory; - @Nullable private List streamKeys; private HlsPlaylistTracker.Factory playlistTrackerFactory; private CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory; - private DrmSessionManager drmSessionManager; + private DrmSessionManager drmSessionManager; private LoadErrorHandlingPolicy loadErrorHandlingPolicy; private boolean allowChunklessPreparation; - @HlsMetadataType private int metadataType; + @MetadataType private int metadataType; private boolean useSessionKeys; - private boolean isCreateCalled; + private List streamKeys; @Nullable private Object tag; /** @@ -100,20 +128,16 @@ public final class HlsMediaSource extends BaseMediaSource drmSessionManager = DrmSessionManager.getDummyDrmSessionManager(); loadErrorHandlingPolicy = new DefaultLoadErrorHandlingPolicy(); compositeSequenceableLoaderFactory = new DefaultCompositeSequenceableLoaderFactory(); - metadataType = HlsMetadataType.ID3; + metadataType = METADATA_TYPE_ID3; + streamKeys = Collections.emptyList(); } /** - * Sets a tag for the media source which will be published in the {@link - * com.google.android.exoplayer2.Timeline} of the source as {@link - * com.google.android.exoplayer2.Timeline.Window#tag}. - * - * @param tag A tag for the media source. - * @return This factory, for convenience. - * @throws IllegalStateException If one of the {@code create} methods has already been called. + * @deprecated Use {@link MediaItem.Builder#setTag(Object)} and {@link + * #createMediaSource(MediaItem)} instead. */ + @Deprecated public Factory setTag(@Nullable Object tag) { - Assertions.checkState(!isCreateCalled); this.tag = tag; return this; } @@ -125,25 +149,10 @@ public final class HlsMediaSource extends BaseMediaSource * @param extractorFactory An {@link HlsExtractorFactory} for {@link Extractor}s for the * segments. * @return This factory, for convenience. - * @throws IllegalStateException If one of the {@code create} methods has already been called. */ - public Factory setExtractorFactory(HlsExtractorFactory extractorFactory) { - Assertions.checkState(!isCreateCalled); - this.extractorFactory = Assertions.checkNotNull(extractorFactory); - return this; - } - - /** - * Sets the {@link DrmSessionManager} to use for acquiring {@link DrmSession DrmSessions}. The - * default value is {@link DrmSessionManager#DUMMY}. - * - * @param drmSessionManager The {@link DrmSessionManager}. - * @return This factory, for convenience. - * @throws IllegalStateException If one of the {@code create} methods has already been called. - */ - public Factory setDrmSessionManager(DrmSessionManager drmSessionManager) { - Assertions.checkState(!isCreateCalled); - this.drmSessionManager = drmSessionManager; + public Factory setExtractorFactory(@Nullable HlsExtractorFactory extractorFactory) { + this.extractorFactory = + extractorFactory != null ? extractorFactory : HlsExtractorFactory.DEFAULT; return this; } @@ -155,30 +164,19 @@ public final class HlsMediaSource extends BaseMediaSource * * @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; + public Factory setLoadErrorHandlingPolicy( + @Nullable LoadErrorHandlingPolicy loadErrorHandlingPolicy) { + this.loadErrorHandlingPolicy = + loadErrorHandlingPolicy != null + ? loadErrorHandlingPolicy + : new DefaultLoadErrorHandlingPolicy(); return this; } - /** - * Sets the minimum number of times to retry if a loading error occurs. The default value is - * {@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 Use {@link #setLoadErrorHandlingPolicy(LoadErrorHandlingPolicy)} instead. */ @Deprecated public Factory setMinLoadableRetryCount(int minLoadableRetryCount) { - Assertions.checkState(!isCreateCalled); this.loadErrorHandlingPolicy = new DefaultLoadErrorHandlingPolicy(minLoadableRetryCount); return this; } @@ -189,11 +187,13 @@ public final class HlsMediaSource extends BaseMediaSource * * @param playlistParserFactory An {@link HlsPlaylistParserFactory}. * @return This factory, for convenience. - * @throws IllegalStateException If one of the {@code create} methods has already been called. */ - public Factory setPlaylistParserFactory(HlsPlaylistParserFactory playlistParserFactory) { - Assertions.checkState(!isCreateCalled); - this.playlistParserFactory = Assertions.checkNotNull(playlistParserFactory); + public Factory setPlaylistParserFactory( + @Nullable HlsPlaylistParserFactory playlistParserFactory) { + this.playlistParserFactory = + playlistParserFactory != null + ? playlistParserFactory + : new DefaultHlsPlaylistParserFactory(); return this; } @@ -203,11 +203,13 @@ public final class HlsMediaSource extends BaseMediaSource * * @param playlistTrackerFactory A factory for {@link HlsPlaylistTracker} instances. * @return This factory, for convenience. - * @throws IllegalStateException If one of the {@code create} methods has already been called. */ - public Factory setPlaylistTrackerFactory(HlsPlaylistTracker.Factory playlistTrackerFactory) { - Assertions.checkState(!isCreateCalled); - this.playlistTrackerFactory = Assertions.checkNotNull(playlistTrackerFactory); + public Factory setPlaylistTrackerFactory( + @Nullable HlsPlaylistTracker.Factory playlistTrackerFactory) { + this.playlistTrackerFactory = + playlistTrackerFactory != null + ? playlistTrackerFactory + : DefaultHlsPlaylistTracker.FACTORY; return this; } @@ -220,13 +222,13 @@ public final class HlsMediaSource extends BaseMediaSource * SequenceableLoader}s for when this media source loads data from multiple streams (video, * audio etc...). * @return This factory, for convenience. - * @throws IllegalStateException If one of the {@code create} methods has already been called. */ public Factory setCompositeSequenceableLoaderFactory( - CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory) { - Assertions.checkState(!isCreateCalled); + @Nullable CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory) { this.compositeSequenceableLoaderFactory = - Assertions.checkNotNull(compositeSequenceableLoaderFactory); + compositeSequenceableLoaderFactory != null + ? compositeSequenceableLoaderFactory + : new DefaultCompositeSequenceableLoaderFactory(); return this; } @@ -236,35 +238,32 @@ public final class HlsMediaSource extends BaseMediaSource * * @param allowChunklessPreparation Whether chunkless preparation is allowed. * @return This factory, for convenience. - * @throws IllegalStateException If one of the {@code create} methods has already been called. */ public Factory setAllowChunklessPreparation(boolean allowChunklessPreparation) { - Assertions.checkState(!isCreateCalled); this.allowChunklessPreparation = allowChunklessPreparation; return this; } /** * Sets the type of metadata to extract from the HLS source (defaults to {@link - * HlsMetadataType#ID3}). + * #METADATA_TYPE_ID3}). * *

      HLS supports in-band ID3 in both TS and fMP4 streams, but in the fMP4 case the data is * wrapped in an EMSG box [spec]. * - *

      If this is set to {@link HlsMetadataType#ID3} then raw ID3 metadata of will be extracted + *

      If this is set to {@link #METADATA_TYPE_ID3} then raw ID3 metadata of will be extracted * from TS sources. From fMP4 streams EMSGs containing metadata of this type (in the variant * stream only) will be unwrapped to expose the inner data. All other in-band metadata will be * dropped. * - *

      If this is set to {@link HlsMetadataType#EMSG} then all EMSG data from the fMP4 variant + *

      If this is set to {@link #METADATA_TYPE_EMSG} then all EMSG data from the fMP4 variant * stream will be extracted. No metadata will be extracted from TS streams, since they don't * support EMSG. * * @param metadataType The type of metadata to extract. * @return This factory, for convenience. */ - public Factory setMetadataType(@HlsMetadataType int metadataType) { - Assertions.checkState(!isCreateCalled); + public Factory setMetadataType(@MetadataType int metadataType) { this.metadataType = metadataType; return this; } @@ -283,10 +282,39 @@ public final class HlsMediaSource extends BaseMediaSource return this; } + /** + * Sets the {@link DrmSessionManager} to use for acquiring {@link DrmSession DrmSessions}. The + * default value is {@link DrmSessionManager#DUMMY}. + * + * @param drmSessionManager The {@link DrmSessionManager}. + * @return This factory, for convenience. + */ + @Override + public Factory setDrmSessionManager(@Nullable DrmSessionManager drmSessionManager) { + this.drmSessionManager = + drmSessionManager != null + ? drmSessionManager + : DrmSessionManager.getDummyDrmSessionManager(); + return this; + } + + /** + * @deprecated Use {@link MediaItem.Builder#setStreamKeys(List)} and {@link + * #createMediaSource(MediaItem)} instead. + */ + @SuppressWarnings("deprecation") + @Deprecated + @Override + public Factory setStreamKeys(@Nullable List streamKeys) { + this.streamKeys = streamKeys != null ? streamKeys : Collections.emptyList(); + return this; + } + /** * @deprecated Use {@link #createMediaSource(Uri)} and {@link #addEventListener(Handler, * MediaSourceEventListener)} instead. */ + @SuppressWarnings("deprecation") @Deprecated public HlsMediaSource createMediaSource( Uri playlistUri, @@ -299,20 +327,35 @@ public final class HlsMediaSource extends BaseMediaSource return mediaSource; } + /** @deprecated Use {@link #createMediaSource(MediaItem)} instead. */ + @SuppressWarnings("deprecation") + @Deprecated + @Override + public HlsMediaSource createMediaSource(Uri uri) { + return createMediaSource(new MediaItem.Builder().setUri(uri).build()); + } + /** * Returns a new {@link HlsMediaSource} using the current parameters. * + * @param mediaItem The {@link MediaItem}. * @return The new {@link HlsMediaSource}. + * @throws NullPointerException if {@link MediaItem#playbackProperties} is {@code null}. */ @Override - public HlsMediaSource createMediaSource(Uri playlistUri) { - isCreateCalled = true; - if (streamKeys != null) { + public HlsMediaSource createMediaSource(MediaItem mediaItem) { + Assertions.checkNotNull(mediaItem.playbackProperties); + HlsPlaylistParserFactory playlistParserFactory = this.playlistParserFactory; + List streamKeys = + !mediaItem.playbackProperties.streamKeys.isEmpty() + ? mediaItem.playbackProperties.streamKeys + : this.streamKeys; + if (!streamKeys.isEmpty()) { playlistParserFactory = new FilteringHlsPlaylistParserFactory(playlistParserFactory, streamKeys); } return new HlsMediaSource( - playlistUri, + mediaItem.playbackProperties.uri, hlsDataSourceFactory, extractorFactory, compositeSequenceableLoaderFactory, @@ -323,31 +366,23 @@ public final class HlsMediaSource extends BaseMediaSource allowChunklessPreparation, metadataType, useSessionKeys, - tag); - } - - @Override - public Factory setStreamKeys(List streamKeys) { - Assertions.checkState(!isCreateCalled); - this.streamKeys = streamKeys; - return this; + mediaItem.playbackProperties.tag != null ? mediaItem.playbackProperties.tag : tag); } @Override public int[] getSupportedTypes() { return new int[] {C.TYPE_HLS}; } - } private final HlsExtractorFactory extractorFactory; private final Uri manifestUri; private final HlsDataSourceFactory dataSourceFactory; private final CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory; - private final DrmSessionManager drmSessionManager; + private final DrmSessionManager drmSessionManager; private final LoadErrorHandlingPolicy loadErrorHandlingPolicy; private final boolean allowChunklessPreparation; - private final @HlsMetadataType int metadataType; + private final @MetadataType int metadataType; private final boolean useSessionKeys; private final HlsPlaylistTracker playlistTracker; @Nullable private final Object tag; @@ -359,11 +394,11 @@ public final class HlsMediaSource extends BaseMediaSource HlsDataSourceFactory dataSourceFactory, HlsExtractorFactory extractorFactory, CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory, - DrmSessionManager drmSessionManager, + DrmSessionManager drmSessionManager, LoadErrorHandlingPolicy loadErrorHandlingPolicy, HlsPlaylistTracker playlistTracker, boolean allowChunklessPreparation, - @HlsMetadataType int metadataType, + @MetadataType int metadataType, boolean useSessionKeys, @Nullable Object tag) { this.manifestUri = manifestUri; @@ -467,6 +502,7 @@ public final class HlsMediaSource extends BaseMediaSource new SinglePeriodTimeline( presentationStartTimeMs, windowStartTimeMs, + /* elapsedRealtimeEpochOffsetMs= */ C.TIME_UNSET, periodDurationUs, /* windowDurationUs= */ playlist.durationUs, /* windowPositionInPeriodUs= */ offsetFromInitialStartTimeUs, @@ -484,6 +520,7 @@ public final class HlsMediaSource extends BaseMediaSource new SinglePeriodTimeline( presentationStartTimeMs, windowStartTimeMs, + /* elapsedRealtimeEpochOffsetMs= */ C.TIME_UNSET, /* periodDurationUs= */ playlist.durationUs, /* windowDurationUs= */ playlist.durationUs, /* windowPositionInPeriodUs= */ 0, 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 8d6db4b112..a9baf53add 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 @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.source.hls; import android.net.Uri; import android.os.Handler; +import android.os.Looper; import android.util.SparseIntArray; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; @@ -28,7 +29,7 @@ import com.google.android.exoplayer2.drm.DrmInitData; import com.google.android.exoplayer2.drm.DrmSession; import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.extractor.DummyTrackOutput; -import com.google.android.exoplayer2.extractor.ExtractorInput; +import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.SeekMap; import com.google.android.exoplayer2.extractor.TrackOutput; @@ -36,6 +37,8 @@ import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.emsg.EventMessage; import com.google.android.exoplayer2.metadata.emsg.EventMessageDecoder; import com.google.android.exoplayer2.metadata.id3.PrivFrame; +import com.google.android.exoplayer2.source.LoadEventInfo; +import com.google.android.exoplayer2.source.MediaLoadData; import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; import com.google.android.exoplayer2.source.SampleQueue; import com.google.android.exoplayer2.source.SampleQueue.UpstreamFormatChangedListener; @@ -47,11 +50,14 @@ import com.google.android.exoplayer2.source.chunk.Chunk; import com.google.android.exoplayer2.source.chunk.MediaChunkIterator; import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.upstream.Allocator; +import com.google.android.exoplayer2.upstream.DataReader; import com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy; +import com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy.LoadErrorInfo; 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.Log; +import com.google.android.exoplayer2.util.MediaSourceEventDispatcher; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.Util; @@ -113,11 +119,11 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; private final HlsChunkSource chunkSource; private final Allocator allocator; @Nullable private final Format muxedAudioFormat; - private final DrmSessionManager drmSessionManager; + private final DrmSessionManager drmSessionManager; private final LoadErrorHandlingPolicy loadErrorHandlingPolicy; private final Loader loader; private final EventDispatcher eventDispatcher; - private final @HlsMetadataType int metadataType; + private final @HlsMediaSource.MetadataType int metadataType; private final HlsChunkSource.HlsChunkHolder nextChunkHolder; private final ArrayList mediaChunks; private final List readOnlyMediaChunks; @@ -128,24 +134,24 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; private final ArrayList hlsSampleStreams; private final Map overridingDrmInitData; - private SampleQueue[] sampleQueues; + private HlsSampleQueue[] sampleQueues; private int[] sampleQueueTrackIds; private Set sampleQueueMappingDoneByType; private SparseIntArray sampleQueueIndicesByType; - @MonotonicNonNull private TrackOutput emsgUnwrappingTrackOutput; + private @MonotonicNonNull TrackOutput emsgUnwrappingTrackOutput; private int primarySampleQueueType; private int primarySampleQueueIndex; private boolean sampleQueuesBuilt; private boolean prepared; private int enabledTrackGroupCount; - @MonotonicNonNull private Format upstreamTrackFormat; + private @MonotonicNonNull Format upstreamTrackFormat; @Nullable private Format downstreamTrackFormat; private boolean released; // Tracks are complicated in HLS. See documentation of buildTracksFromSampleStreams for details. // Indexed by track (as exposed by this source). - @MonotonicNonNull private TrackGroupArray trackGroups; - @MonotonicNonNull private Set optionalTrackGroups; + private @MonotonicNonNull TrackGroupArray trackGroups; + private @MonotonicNonNull Set optionalTrackGroups; // Indexed by track group. private int @MonotonicNonNull [] trackGroupToSampleQueueIndex; private int primaryTrackGroupIndex; @@ -162,7 +168,8 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; // Accessed only by the loading thread. private boolean tracksEnded; private long sampleOffsetUs; - private int chunkUid; + @Nullable private DrmInitData drmInitData; + @Nullable private HlsMediaChunk sourceChunk; /** * @param trackType The type of the track. One of the {@link C} {@code TRACK_TYPE_*} constants. @@ -188,10 +195,10 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; Allocator allocator, long positionUs, @Nullable Format muxedAudioFormat, - DrmSessionManager drmSessionManager, + DrmSessionManager drmSessionManager, LoadErrorHandlingPolicy loadErrorHandlingPolicy, EventDispatcher eventDispatcher, - @HlsMetadataType int metadataType) { + @HlsMediaSource.MetadataType int metadataType) { this.trackType = trackType; this.callback = callback; this.chunkSource = chunkSource; @@ -207,7 +214,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; sampleQueueTrackIds = new int[0]; sampleQueueMappingDoneByType = new HashSet<>(MAPPABLE_TYPES.size()); sampleQueueIndicesByType = new SparseIntArray(MAPPABLE_TYPES.size()); - sampleQueues = new SampleQueue[0]; + sampleQueues = new HlsSampleQueue[0]; sampleQueueIsAudioVideoFlags = new boolean[0]; sampleQueuesEnabledStates = new boolean[0]; mediaChunks = new ArrayList<>(); @@ -361,13 +368,12 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; // If there's still a chance of avoiding a seek, try and seek within the sample queue. if (!seekRequired) { SampleQueue sampleQueue = sampleQueues[trackGroupToSampleQueueIndex[trackGroupIndex]]; - sampleQueue.rewind(); - // A seek can be avoided if we're able to advance to the current playback position in + // A seek can be avoided if we're able to seek to the current playback position in // the sample queue, or if we haven't read anything from the queue since the previous // seek (this case is common for sparse tracks such as metadata tracks). In all other // cases a seek is required. seekRequired = - sampleQueue.advanceTo(positionUs, true, true) == SampleQueue.ADVANCE_FAILED + !sampleQueue.seekTo(positionUs, /* allowTimeBeyondBuffer= */ true) && sampleQueue.getReadIndex() != 0; } } @@ -568,7 +574,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; chunkIndex < mediaChunks.size() ? mediaChunks.get(chunkIndex).trackFormat : Assertions.checkNotNull(upstreamTrackFormat); - format = format.copyWithManifestFormatInfo(trackFormat); + format = format.withManifestFormatInfo(trackFormat); } formatHolder.format = format; } @@ -584,8 +590,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; if (loadingFinished && positionUs > sampleQueue.getLargestQueuedTimestampUs()) { return sampleQueue.advanceToEnd(); } else { - int skipCount = sampleQueue.advanceTo(positionUs, true, true); - return skipCount == SampleQueue.ADVANCE_FAILED ? 0 : skipCount; + return sampleQueue.advanceTo(positionUs); } } @@ -668,25 +673,20 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; } if (isMediaChunk(loadable)) { - pendingResetPositionUs = C.TIME_UNSET; - HlsMediaChunk mediaChunk = (HlsMediaChunk) loadable; - mediaChunk.init(this); - mediaChunks.add(mediaChunk); - upstreamTrackFormat = mediaChunk.trackFormat; + initMediaChunkLoad((HlsMediaChunk) loadable); } long elapsedRealtimeMs = loader.startLoading( loadable, this, loadErrorHandlingPolicy.getMinimumLoadableRetryCount(loadable.type)); eventDispatcher.loadStarted( - loadable.dataSpec, + new LoadEventInfo(loadable.loadTaskId, loadable.dataSpec, elapsedRealtimeMs), loadable.type, trackType, loadable.trackFormat, loadable.trackSelectionReason, loadable.trackSelectionData, loadable.startTimeUs, - loadable.endTimeUs, - elapsedRealtimeMs); + loadable.endTimeUs); return true; } @@ -789,20 +789,25 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; @Override public void onLoadCompleted(Chunk loadable, long elapsedRealtimeMs, long loadDurationMs) { chunkSource.onChunkLoadCompleted(loadable); + LoadEventInfo loadEventInfo = + new LoadEventInfo( + loadable.loadTaskId, + loadable.dataSpec, + loadable.getUri(), + loadable.getResponseHeaders(), + elapsedRealtimeMs, + loadDurationMs, + loadable.bytesLoaded()); + loadErrorHandlingPolicy.onLoadTaskConcluded(loadable.loadTaskId); eventDispatcher.loadCompleted( - loadable.dataSpec, - loadable.getUri(), - loadable.getResponseHeaders(), + loadEventInfo, loadable.type, trackType, loadable.trackFormat, loadable.trackSelectionReason, loadable.trackSelectionData, loadable.startTimeUs, - loadable.endTimeUs, - elapsedRealtimeMs, - loadDurationMs, - loadable.bytesLoaded()); + loadable.endTimeUs); if (!prepared) { continueLoading(lastSeekPositionUs); } else { @@ -811,22 +816,27 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; } @Override - public void onLoadCanceled(Chunk loadable, long elapsedRealtimeMs, long loadDurationMs, - boolean released) { + public void onLoadCanceled( + Chunk loadable, long elapsedRealtimeMs, long loadDurationMs, boolean released) { + LoadEventInfo loadEventInfo = + new LoadEventInfo( + loadable.loadTaskId, + loadable.dataSpec, + loadable.getUri(), + loadable.getResponseHeaders(), + elapsedRealtimeMs, + loadDurationMs, + loadable.bytesLoaded()); + loadErrorHandlingPolicy.onLoadTaskConcluded(loadable.loadTaskId); eventDispatcher.loadCanceled( - loadable.dataSpec, - loadable.getUri(), - loadable.getResponseHeaders(), + loadEventInfo, loadable.type, trackType, loadable.trackFormat, loadable.trackSelectionReason, loadable.trackSelectionData, loadable.startTimeUs, - loadable.endTimeUs, - elapsedRealtimeMs, - loadDurationMs, - loadable.bytesLoaded()); + loadable.endTimeUs); if (!released) { resetSampleQueues(); if (enabledTrackGroupCount > 0) { @@ -845,11 +855,28 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; long bytesLoaded = loadable.bytesLoaded(); boolean isMediaChunk = isMediaChunk(loadable); boolean blacklistSucceeded = false; + LoadEventInfo loadEventInfo = + new LoadEventInfo( + loadable.loadTaskId, + loadable.dataSpec, + loadable.getUri(), + loadable.getResponseHeaders(), + elapsedRealtimeMs, + loadDurationMs, + bytesLoaded); + MediaLoadData mediaLoadData = + new MediaLoadData( + loadable.type, + trackType, + loadable.trackFormat, + loadable.trackSelectionReason, + loadable.trackSelectionData, + C.usToMs(loadable.startTimeUs), + C.usToMs(loadable.endTimeUs)); + LoadErrorInfo loadErrorInfo = + new LoadErrorInfo(loadEventInfo, mediaLoadData, error, errorCount); LoadErrorAction loadErrorAction; - - long blacklistDurationMs = - loadErrorHandlingPolicy.getBlacklistDurationMsFor( - loadable.type, loadDurationMs, error, errorCount); + long blacklistDurationMs = loadErrorHandlingPolicy.getBlacklistDurationMsFor(loadErrorInfo); if (blacklistDurationMs != C.TIME_UNSET) { blacklistSucceeded = chunkSource.maybeBlacklistTrack(loadable, blacklistDurationMs); } @@ -864,19 +891,16 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; } loadErrorAction = Loader.DONT_RETRY; } else /* did not blacklist */ { - long retryDelayMs = - loadErrorHandlingPolicy.getRetryDelayMsFor( - loadable.type, loadDurationMs, error, errorCount); + long retryDelayMs = loadErrorHandlingPolicy.getRetryDelayMsFor(loadErrorInfo); loadErrorAction = retryDelayMs != C.TIME_UNSET ? Loader.createRetryAction(/* resetErrorCount= */ false, retryDelayMs) : Loader.DONT_RETRY_FATAL; } + boolean wasCanceled = !loadErrorAction.isRetry(); eventDispatcher.loadError( - loadable.dataSpec, - loadable.getUri(), - loadable.getResponseHeaders(), + loadEventInfo, loadable.type, trackType, loadable.trackFormat, @@ -884,11 +908,11 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; loadable.trackSelectionData, loadable.startTimeUs, loadable.endTimeUs, - elapsedRealtimeMs, - loadDurationMs, - bytesLoaded, error, - /* wasCanceled= */ !loadErrorAction.isRetry()); + wasCanceled); + if (wasCanceled) { + loadErrorHandlingPolicy.onLoadTaskConcluded(loadable.loadTaskId); + } if (blacklistSucceeded) { if (!prepared) { @@ -903,26 +927,24 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; // Called by the consuming thread, but only when there is no loading thread. /** - * Initializes the wrapper for loading a chunk. + * Performs initialization for a media chunk that's about to start loading. * - * @param chunkUid The chunk's uid. - * @param shouldSpliceIn Whether the samples parsed from the chunk should be spliced into any - * samples already queued to the wrapper. - * @param reusingExtractor Whether the extractor for the chunk has already been used for preceding - * chunks. + * @param chunk The media chunk that's about to start loading. */ - public void init(int chunkUid, boolean shouldSpliceIn, boolean reusingExtractor) { - if (!reusingExtractor) { - sampleQueueMappingDoneByType.clear(); - } - this.chunkUid = chunkUid; - HlsMediaChunk loadingChunk = findChunkMatching(chunkUid); + private void initMediaChunkLoad(HlsMediaChunk chunk) { + sourceChunk = chunk; + upstreamTrackFormat = chunk.trackFormat; + pendingResetPositionUs = C.TIME_UNSET; + mediaChunks.add(chunk); + + chunk.init(this); + HlsMediaChunk loadingChunk = findChunkMatching(chunk.uid); for (int i=0; i < sampleQueues.length; i++) { SampleQueue sampleQueue = sampleQueues[i]; - sampleQueue.sourceId(chunkUid); + sampleQueue.setSourceChunk(chunk); loadingChunk.setFirstSampleIndex(i, sampleQueue.getWriteIndex()); } - if (shouldSpliceIn) { + if (chunk.shouldSpliceIn) { for (SampleQueue sampleQueue : sampleQueues) { sampleQueue.splice(); } @@ -972,8 +994,8 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; * different ID, then return a {@link DummyTrackOutput} that does nothing. * *

      If a {@link SampleQueue} for {@code type} has been created but is not mapped, then map it to - * this {@code id} and return it. This situation can happen after a call to {@link #init} with - * {@code reusingExtractor=false}. + * this {@code id} and return it. This situation can happen after a call to {@link + * #onNewExtractor}. * * @param id The ID of the track. * @param type The type of the track, must be one of {@link #MAPPABLE_TYPES}. @@ -998,19 +1020,29 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; private SampleQueue createSampleQueue(int id, int type) { int trackCount = sampleQueues.length; - SampleQueue trackOutput = - new FormatAdjustingSampleQueue(allocator, drmSessionManager, overridingDrmInitData); - trackOutput.setSampleOffsetUs(sampleOffsetUs); - trackOutput.sourceId(chunkUid); - trackOutput.setUpstreamFormatChangeListener(this); + boolean isAudioVideo = type == C.TRACK_TYPE_AUDIO || type == C.TRACK_TYPE_VIDEO; + HlsSampleQueue sampleQueue = + new HlsSampleQueue( + allocator, + /* playbackLooper= */ handler.getLooper(), + drmSessionManager, + eventDispatcher, + overridingDrmInitData); + if (isAudioVideo) { + sampleQueue.setDrmInitData(drmInitData); + } + sampleQueue.setSampleOffsetUs(sampleOffsetUs); + if (sourceChunk != null) { + sampleQueue.setSourceChunk(sourceChunk); + } + sampleQueue.setUpstreamFormatChangeListener(this); sampleQueueTrackIds = Arrays.copyOf(sampleQueueTrackIds, trackCount + 1); sampleQueueTrackIds[trackCount] = id; - sampleQueues = Util.nullSafeArrayAppend(sampleQueues, trackOutput); - HlsMediaChunk mediaChunk = findChunkMatching(chunkUid); + sampleQueues = Util.nullSafeArrayAppend(sampleQueues, sampleQueue); + HlsMediaChunk mediaChunk = findChunkMatching(sourceChunk.uid); mediaChunk.setFirstSampleIndex(trackCount, 0); sampleQueueIsAudioVideoFlags = Arrays.copyOf(sampleQueueIsAudioVideoFlags, trackCount + 1); - sampleQueueIsAudioVideoFlags[trackCount] = - type == C.TRACK_TYPE_AUDIO || type == C.TRACK_TYPE_VIDEO; + sampleQueueIsAudioVideoFlags[trackCount] = isAudioVideo; haveAudioVideoSampleQueues |= sampleQueueIsAudioVideoFlags[trackCount]; sampleQueueMappingDoneByType.add(type); sampleQueueIndicesByType.append(type, trackCount); @@ -1019,7 +1051,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; primarySampleQueueType = type; } sampleQueuesEnabledStates = Arrays.copyOf(sampleQueuesEnabledStates, trackCount + 1); - return trackOutput; + return sampleQueue; } @Override @@ -1042,10 +1074,58 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; // Called by the loading thread. + /** Called when an {@link HlsMediaChunk} starts extracting media with a new {@link Extractor}. */ + public void onNewExtractor() { + sampleQueueMappingDoneByType.clear(); + } + + /** + * Sets an offset that will be added to the timestamps (and sub-sample timestamps) of samples that + * are subsequently loaded by this wrapper. + * + * @param sampleOffsetUs The timestamp offset in microseconds. + */ public void setSampleOffsetUs(long sampleOffsetUs) { - this.sampleOffsetUs = sampleOffsetUs; - for (SampleQueue sampleQueue : sampleQueues) { - sampleQueue.setSampleOffsetUs(sampleOffsetUs); + if (this.sampleOffsetUs != sampleOffsetUs) { + this.sampleOffsetUs = sampleOffsetUs; + for (SampleQueue sampleQueue : sampleQueues) { + sampleQueue.setSampleOffsetUs(sampleOffsetUs); + } + } + } + + /** + * Sets default {@link DrmInitData} for samples that are subsequently loaded by this wrapper. + * + *

      This method should be called prior to loading each {@link HlsMediaChunk}. The {@link + * DrmInitData} passed should be that of an EXT-X-KEY tag that applies to the chunk, or {@code + * null} otherwise. + * + *

      The final {@link DrmInitData} for subsequently queued samples is determined as followed: + * + *

        + *
      1. It is initially set to {@code drmInitData}, unless {@code drmInitData} is null in which + * case it's set to {@link Format#drmInitData} of the upstream {@link Format}. + *
      2. If the initial {@link DrmInitData} is non-null and {@link #overridingDrmInitData} + * contains an entry whose key matches the {@link DrmInitData#schemeType}, then the sample's + * {@link DrmInitData} is overridden to be this entry's value. + *
      + * + *

      + * + * @param drmInitData The default {@link DrmInitData} for samples that are subsequently queued. If + * non-null then it takes precedence over {@link Format#drmInitData} of the upstream {@link + * Format}, but will still be overridden by a matching override in {@link + * #overridingDrmInitData}. + */ + public void setDrmInitData(@Nullable DrmInitData drmInitData) { + if (!Util.areEqual(this.drmInitData, drmInitData)) { + this.drmInitData = drmInitData; + for (int i = 0; i < sampleQueues.length; i++) { + if (sampleQueueIsAudioVideoFlags[i]) { + sampleQueues[i].setDrmInitData(drmInitData); + } + } } } @@ -1064,7 +1144,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; private void updateSampleStreams(@NullableType SampleStream[] streams) { hlsSampleStreams.clear(); - for (SampleStream stream : streams) { + for (@Nullable SampleStream stream : streams) { if (stream != null) { hlsSampleStreams.add((HlsSampleStream) stream); } @@ -1139,7 +1219,8 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; for (int i = 0; i < trackGroupCount; i++) { for (int queueIndex = 0; queueIndex < sampleQueues.length; queueIndex++) { SampleQueue sampleQueue = sampleQueues[queueIndex]; - if (formatsMatch(sampleQueue.getUpstreamFormat(), trackGroups.get(i).getFormat(0))) { + Format upstreamFormat = Assertions.checkStateNotNull(sampleQueue.getUpstreamFormat()); + if (formatsMatch(upstreamFormat, trackGroups.get(i).getFormat(0))) { trackGroupToSampleQueueIndex[i] = queueIndex; break; } @@ -1188,7 +1269,9 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; int primaryExtractorTrackIndex = C.INDEX_UNSET; int extractorTrackCount = sampleQueues.length; for (int i = 0; i < extractorTrackCount; i++) { - String sampleMimeType = sampleQueues[i].getUpstreamFormat().sampleMimeType; + @Nullable + String sampleMimeType = + Assertions.checkStateNotNull(sampleQueues[i].getUpstreamFormat()).sampleMimeType; int trackType; if (MimeTypes.isVideo(sampleMimeType)) { trackType = C.TRACK_TYPE_VIDEO; @@ -1223,11 +1306,11 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; // Construct the set of exposed track groups. TrackGroup[] trackGroups = new TrackGroup[extractorTrackCount]; for (int i = 0; i < extractorTrackCount; i++) { - Format sampleFormat = sampleQueues[i].getUpstreamFormat(); + Format sampleFormat = Assertions.checkStateNotNull(sampleQueues[i].getUpstreamFormat()); if (i == primaryExtractorTrackIndex) { Format[] formats = new Format[chunkSourceTrackCount]; if (chunkSourceTrackCount == 1) { - formats[0] = sampleFormat.copyWithManifestFormatInfo(chunkSourceTrackGroup.getFormat(0)); + formats[0] = sampleFormat.withManifestFormatInfo(chunkSourceTrackGroup.getFormat(0)); } else { for (int j = 0; j < chunkSourceTrackCount; j++) { formats[j] = deriveFormat(chunkSourceTrackGroup.getFormat(j), sampleFormat, true); @@ -1236,6 +1319,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; trackGroups[i] = new TrackGroup(formats); primaryTrackGroupIndex = i; } else { + @Nullable Format trackFormat = primaryExtractorTrackType == C.TRACK_TYPE_VIDEO && MimeTypes.isAudio(sampleFormat.sampleMimeType) @@ -1285,9 +1369,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; int sampleQueueCount = sampleQueues.length; for (int i = 0; i < sampleQueueCount; i++) { SampleQueue sampleQueue = sampleQueues[i]; - sampleQueue.rewind(); - boolean seekInsideQueue = sampleQueue.advanceTo(positionUs, true, false) - != SampleQueue.ADVANCE_FAILED; + boolean seekInsideQueue = sampleQueue.seekTo(positionUs, /* allowTimeBeyondBuffer= */ false); // If we have AV tracks then an in-queue seek is successful if the seek into every AV queue // is successful. We ignore whether seeks within non-AV queues are successful in this case, as // they may be sparse or poorly interleaved. If we only have non-AV tracks then a seek is @@ -1337,38 +1419,51 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; * * @param playlistFormat The format information obtained from the master playlist. * @param sampleFormat The format information obtained from the samples. - * @param propagateBitrate Whether the bitrate from the playlist format should be included in the - * derived format. + * @param propagateBitrates Whether the bitrates from the playlist format should be included in + * the derived format. * @return The derived track format. */ private static Format deriveFormat( - @Nullable Format playlistFormat, Format sampleFormat, boolean propagateBitrate) { + @Nullable Format playlistFormat, Format sampleFormat, boolean propagateBitrates) { if (playlistFormat == null) { return sampleFormat; } - int bitrate = propagateBitrate ? playlistFormat.bitrate : Format.NO_VALUE; - int channelCount = - playlistFormat.channelCount != Format.NO_VALUE - ? playlistFormat.channelCount - : sampleFormat.channelCount; + int sampleTrackType = MimeTypes.getTrackType(sampleFormat.sampleMimeType); - String codecs = Util.getCodecsOfType(playlistFormat.codecs, sampleTrackType); - String mimeType = MimeTypes.getMediaMimeType(codecs); - if (mimeType == null) { - mimeType = sampleFormat.sampleMimeType; + @Nullable String codecs = Util.getCodecsOfType(playlistFormat.codecs, sampleTrackType); + @Nullable String sampleMimeType = MimeTypes.getMediaMimeType(codecs); + + Format.Builder formatBuilder = + sampleFormat + .buildUpon() + .setId(playlistFormat.id) + .setLabel(playlistFormat.label) + .setLanguage(playlistFormat.language) + .setSelectionFlags(playlistFormat.selectionFlags) + .setRoleFlags(playlistFormat.roleFlags) + .setAverageBitrate(propagateBitrates ? playlistFormat.averageBitrate : Format.NO_VALUE) + .setPeakBitrate(propagateBitrates ? playlistFormat.peakBitrate : Format.NO_VALUE) + .setCodecs(codecs) + .setWidth(playlistFormat.width) + .setHeight(playlistFormat.height); + + if (sampleMimeType != null) { + formatBuilder.setSampleMimeType(sampleMimeType); } - return sampleFormat.copyWithContainerInfo( - playlistFormat.id, - playlistFormat.label, - mimeType, - codecs, - playlistFormat.metadata, - bitrate, - playlistFormat.width, - playlistFormat.height, - channelCount, - playlistFormat.selectionFlags, - playlistFormat.language); + + if (playlistFormat.channelCount != Format.NO_VALUE) { + formatBuilder.setChannelCount(playlistFormat.channelCount); + } + + if (playlistFormat.metadata != null) { + Metadata metadata = playlistFormat.metadata; + if (sampleFormat.metadata != null) { + metadata = sampleFormat.metadata.copyWithAppendedEntriesFrom(metadata); + } + formatBuilder.setMetadata(metadata); + } + + return formatBuilder.build(); } private static boolean isMediaChunk(Chunk chunk) { @@ -1376,8 +1471,8 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; } private static boolean formatsMatch(Format manifestFormat, Format sampleFormat) { - String manifestFormatMimeType = manifestFormat.sampleMimeType; - String sampleFormatMimeType = sampleFormat.sampleMimeType; + @Nullable String manifestFormatMimeType = manifestFormat.sampleMimeType; + @Nullable String sampleFormatMimeType = sampleFormat.sampleMimeType; int manifestFormatTrackType = MimeTypes.getTrackType(manifestFormatMimeType); if (manifestFormatTrackType != C.TRACK_TYPE_TEXT) { return manifestFormatTrackType == MimeTypes.getTrackType(sampleFormatMimeType); @@ -1396,28 +1491,84 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; return new DummyTrackOutput(); } - private static final class FormatAdjustingSampleQueue extends SampleQueue { + /** + * A {@link SampleQueue} that adds HLS specific functionality: + * + *

        + *
      • Detection of spurious discontinuities, by checking sample timestamps against the range + * expected for the currently loading chunk. + *
      • Stripping private timestamp metadata from {@link Format Formats} to avoid an excessive + * number of format switches in the queue. + *
      • Overriding of {@link Format#drmInitData}. + *
      + */ + private static final class HlsSampleQueue extends SampleQueue { + + /** + * The fraction of the chunk duration from which timestamps of samples loaded from within a + * chunk are allowed to deviate from the expected range. + */ + private static final double MAX_TIMESTAMP_DEVIATION_FRACTION = 0.5; + + /** + * A minimum tolerance for sample timestamps in microseconds. Timestamps of samples loaded from + * within a chunk are always allowed to deviate up to this amount from the expected range. + */ + private static final long MIN_TIMESTAMP_DEVIATION_TOLERANCE_US = 4_000_000; + + @Nullable private HlsMediaChunk sourceChunk; + private long sourceChunkLastSampleTimeUs; + private long minAllowedSampleTimeUs; + private long maxAllowedSampleTimeUs; private final Map overridingDrmInitData; + @Nullable private DrmInitData drmInitData; - public FormatAdjustingSampleQueue( + private HlsSampleQueue( Allocator allocator, - DrmSessionManager drmSessionManager, + Looper playbackLooper, + DrmSessionManager drmSessionManager, + MediaSourceEventDispatcher eventDispatcher, Map overridingDrmInitData) { - super(allocator, drmSessionManager); + super(allocator, playbackLooper, drmSessionManager, eventDispatcher); this.overridingDrmInitData = overridingDrmInitData; } + public void setSourceChunk(HlsMediaChunk chunk) { + sourceChunk = chunk; + sourceChunkLastSampleTimeUs = C.TIME_UNSET; + sourceId(chunk.uid); + + long allowedDeviationUs = + Math.max( + (long) ((chunk.endTimeUs - chunk.startTimeUs) * MAX_TIMESTAMP_DEVIATION_FRACTION), + MIN_TIMESTAMP_DEVIATION_TOLERANCE_US); + minAllowedSampleTimeUs = chunk.startTimeUs - allowedDeviationUs; + maxAllowedSampleTimeUs = chunk.endTimeUs + allowedDeviationUs; + } + + public void setDrmInitData(@Nullable DrmInitData drmInitData) { + this.drmInitData = drmInitData; + invalidateUpstreamFormatAdjustment(); + } + + @SuppressWarnings("ReferenceEquality") @Override - public void format(Format format) { - DrmInitData drmInitData = format.drmInitData; + public Format getAdjustedUpstreamFormat(Format format) { + @Nullable + DrmInitData drmInitData = this.drmInitData != null ? this.drmInitData : format.drmInitData; if (drmInitData != null) { + @Nullable DrmInitData overridingDrmInitData = this.overridingDrmInitData.get(drmInitData.schemeType); if (overridingDrmInitData != null) { drmInitData = overridingDrmInitData; } } - super.format(format.copyWithAdjustments(drmInitData, getAdjustedMetadata(format.metadata))); + @Nullable Metadata metadata = getAdjustedMetadata(format.metadata); + if (drmInitData != format.drmInitData || metadata != format.metadata) { + format = format.buildUpon().setDrmInitData(drmInitData).setMetadata(metadata).build(); + } + return super.getAdjustedUpstreamFormat(format); } /** @@ -1456,36 +1607,53 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; } return new Metadata(newMetadataEntries); } + + @Override + public void sampleMetadata( + long timeUs, + @C.BufferFlags int flags, + int size, + int offset, + @Nullable CryptoData cryptoData) { + // TODO: Uncomment this to reject samples with unexpected timestamps. See + // https://github.com/google/ExoPlayer/issues/7030. + // if (timeUs < minAllowedSampleTimeUs || timeUs > maxAllowedSampleTimeUs) { + // Util.sneakyThrow( + // new UnexpectedSampleTimestampException( + // sourceChunk, sourceChunkLastSampleTimeUs, timeUs)); + // } + sourceChunkLastSampleTimeUs = timeUs; + super.sampleMetadata(timeUs, flags, size, offset, cryptoData); + } } private static class EmsgUnwrappingTrackOutput implements TrackOutput { private static final String TAG = "EmsgUnwrappingTrackOutput"; - // TODO(ibaker): Create a Formats util class with common constants like this. + // TODO: Create a Formats util class with common constants like this. private static final Format ID3_FORMAT = - Format.createSampleFormat( - /* id= */ null, MimeTypes.APPLICATION_ID3, Format.OFFSET_SAMPLE_RELATIVE); + new Format.Builder().setSampleMimeType(MimeTypes.APPLICATION_ID3).build(); private static final Format EMSG_FORMAT = - Format.createSampleFormat( - /* id= */ null, MimeTypes.APPLICATION_EMSG, Format.OFFSET_SAMPLE_RELATIVE); + new Format.Builder().setSampleMimeType(MimeTypes.APPLICATION_EMSG).build(); private final EventMessageDecoder emsgDecoder; private final TrackOutput delegate; private final Format delegateFormat; - @MonotonicNonNull private Format format; + private @MonotonicNonNull Format format; private byte[] buffer; private int bufferPosition; - public EmsgUnwrappingTrackOutput(TrackOutput delegate, @HlsMetadataType int metadataType) { + public EmsgUnwrappingTrackOutput( + TrackOutput delegate, @HlsMediaSource.MetadataType int metadataType) { this.emsgDecoder = new EventMessageDecoder(); this.delegate = delegate; switch (metadataType) { - case HlsMetadataType.ID3: + case HlsMediaSource.METADATA_TYPE_ID3: delegateFormat = ID3_FORMAT; break; - case HlsMetadataType.EMSG: + case HlsMediaSource.METADATA_TYPE_EMSG: delegateFormat = EMSG_FORMAT; break; default: @@ -1503,8 +1671,9 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; } @Override - public int sampleData(ExtractorInput input, int length, boolean allowEndOfInput) - throws IOException, InterruptedException { + public int sampleData( + DataReader input, int length, boolean allowEndOfInput, @SampleDataPart int sampleDataPart) + throws IOException { ensureBufferCapacity(bufferPosition + length); int numBytesRead = input.read(buffer, bufferPosition, length); if (numBytesRead == C.RESULT_END_OF_INPUT) { @@ -1519,7 +1688,8 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; } @Override - public void sampleData(ParsableByteArray buffer, int length) { + public void sampleData( + ParsableByteArray buffer, int length, @SampleDataPart int sampleDataPart) { ensureBufferCapacity(bufferPosition + length); buffer.readBytes(this.buffer, bufferPosition, length); bufferPosition += length; diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsTrackMetadataEntry.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsTrackMetadataEntry.java index f26a9b8e9a..9a9566b63e 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsTrackMetadataEntry.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsTrackMetadataEntry.java @@ -19,6 +19,7 @@ import android.os.Parcel; import android.os.Parcelable; import android.text.TextUtils; import androidx.annotation.Nullable; +import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.metadata.Metadata; import java.util.ArrayList; import java.util.Collections; @@ -30,8 +31,14 @@ public final class HlsTrackMetadataEntry implements Metadata.Entry { /** Holds attributes defined in an EXT-X-STREAM-INF tag. */ public static final class VariantInfo implements Parcelable { - /** The bitrate as declared by the EXT-X-STREAM-INF tag. */ - public final long bitrate; + /** + * The average bitrate as declared by the AVERAGE-BANDWIDTH attribute of the EXT-X-STREAM-INF + * tag, or {@link Format#NO_VALUE} if the attribute is not declared. + */ + public final int averageBitrate; + + /** The peak bitrate as declared by the BANDWIDTH attribute of the EXT-X-STREAM-INF tag. */ + public final int peakBitrate; /** * The VIDEO value as defined in the EXT-X-STREAM-INF tag, or null if the VIDEO attribute is not @@ -60,19 +67,22 @@ public final class HlsTrackMetadataEntry implements Metadata.Entry { /** * Creates an instance. * - * @param bitrate See {@link #bitrate}. + * @param averageBitrate See {@link #averageBitrate}. + * @param peakBitrate See {@link #peakBitrate}. * @param videoGroupId See {@link #videoGroupId}. * @param audioGroupId See {@link #audioGroupId}. * @param subtitleGroupId See {@link #subtitleGroupId}. * @param captionGroupId See {@link #captionGroupId}. */ public VariantInfo( - long bitrate, + int averageBitrate, + int peakBitrate, @Nullable String videoGroupId, @Nullable String audioGroupId, @Nullable String subtitleGroupId, @Nullable String captionGroupId) { - this.bitrate = bitrate; + this.averageBitrate = averageBitrate; + this.peakBitrate = peakBitrate; this.videoGroupId = videoGroupId; this.audioGroupId = audioGroupId; this.subtitleGroupId = subtitleGroupId; @@ -80,7 +90,8 @@ public final class HlsTrackMetadataEntry implements Metadata.Entry { } /* package */ VariantInfo(Parcel in) { - bitrate = in.readLong(); + averageBitrate = in.readInt(); + peakBitrate = in.readInt(); videoGroupId = in.readString(); audioGroupId = in.readString(); subtitleGroupId = in.readString(); @@ -96,7 +107,8 @@ public final class HlsTrackMetadataEntry implements Metadata.Entry { return false; } VariantInfo that = (VariantInfo) other; - return bitrate == that.bitrate + return averageBitrate == that.averageBitrate + && peakBitrate == that.peakBitrate && TextUtils.equals(videoGroupId, that.videoGroupId) && TextUtils.equals(audioGroupId, that.audioGroupId) && TextUtils.equals(subtitleGroupId, that.subtitleGroupId) @@ -105,7 +117,8 @@ public final class HlsTrackMetadataEntry implements Metadata.Entry { @Override public int hashCode() { - int result = (int) (bitrate ^ (bitrate >>> 32)); + int result = averageBitrate; + result = 31 * result + peakBitrate; result = 31 * result + (videoGroupId != null ? videoGroupId.hashCode() : 0); result = 31 * result + (audioGroupId != null ? audioGroupId.hashCode() : 0); result = 31 * result + (subtitleGroupId != null ? subtitleGroupId.hashCode() : 0); @@ -122,7 +135,8 @@ public final class HlsTrackMetadataEntry implements Metadata.Entry { @Override public void writeToParcel(Parcel dest, int flags) { - dest.writeLong(bitrate); + dest.writeInt(averageBitrate); + dest.writeInt(peakBitrate); dest.writeString(videoGroupId); dest.writeString(audioGroupId); dest.writeString(subtitleGroupId); diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/UnexpectedSampleTimestampException.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/UnexpectedSampleTimestampException.java new file mode 100644 index 0000000000..50a11170a3 --- /dev/null +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/UnexpectedSampleTimestampException.java @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2020 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; + +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.source.SampleQueue; +import com.google.android.exoplayer2.source.chunk.MediaChunk; +import java.io.IOException; + +/** + * Thrown when an attempt is made to write a sample to a {@link SampleQueue} whose timestamp is + * inconsistent with the chunk from which it originates. + */ +/* package */ final class UnexpectedSampleTimestampException extends IOException { + + /** The {@link MediaChunk} that contained the rejected sample. */ + public final MediaChunk mediaChunk; + + /** + * The timestamp of the last sample that was loaded from {@link #mediaChunk} and successfully + * written to the {@link SampleQueue}, in microseconds. {@link C#TIME_UNSET} if the first sample + * in the chunk was rejected. + */ + public final long lastAcceptedSampleTimeUs; + + /** The timestamp of the rejected sample, in microseconds. */ + public final long rejectedSampleTimeUs; + + /** + * Constructs an instance. + * + * @param mediaChunk The {@link MediaChunk} with the unexpected sample timestamp. + * @param lastAcceptedSampleTimeUs The timestamp of the last sample that was loaded from the chunk + * and successfully written to the {@link SampleQueue}, in microseconds. {@link C#TIME_UNSET} + * if the first sample in the chunk was rejected. + * @param rejectedSampleTimeUs The timestamp of the rejected sample, in microseconds. + */ + public UnexpectedSampleTimestampException( + MediaChunk mediaChunk, long lastAcceptedSampleTimeUs, long rejectedSampleTimeUs) { + super( + "Unexpected sample timestamp: " + + C.usToMs(rejectedSampleTimeUs) + + " in chunk [" + + mediaChunk.startTimeUs + + ", " + + mediaChunk.endTimeUs + + "]"); + this.mediaChunk = mediaChunk; + this.lastAcceptedSampleTimeUs = lastAcceptedSampleTimeUs; + this.rejectedSampleTimeUs = rejectedSampleTimeUs; + } +} 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 a62e135b77..6a390001d2 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 @@ -49,7 +49,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; 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+)"); + private static final Pattern MEDIA_TIMESTAMP = Pattern.compile("MPEGTS:(-?\\d+)"); private static final int HEADER_MIN_LENGTH = 6 /* "WEBVTT" */; private static final int HEADER_MAX_LENGTH = 3 /* optional Byte Order Mark */ + HEADER_MIN_LENGTH; @@ -72,7 +72,7 @@ public final class WebvttExtractor implements Extractor { // Extractor implementation. @Override - public boolean sniff(ExtractorInput input) throws IOException, InterruptedException { + public boolean sniff(ExtractorInput input) throws IOException { // Check whether there is a header without BOM. input.peekFully( sampleData, /* offset= */ 0, /* length= */ HEADER_MIN_LENGTH, /* allowEndOfInput= */ false); @@ -108,8 +108,7 @@ public final class WebvttExtractor implements Extractor { } @Override - public int read(ExtractorInput input, PositionHolder seekPosition) - throws IOException, InterruptedException { + public int read(ExtractorInput input, PositionHolder seekPosition) throws IOException { // output == null suggests init() hasn't been called Assertions.checkNotNull(output); int currentFileSize = (int) input.getLength(); @@ -158,8 +157,12 @@ public final class WebvttExtractor implements Extractor { if (!mediaTimestampMatcher.find()) { throw new ParserException("X-TIMESTAMP-MAP doesn't contain media timestamp: " + line); } - vttTimestampUs = WebvttParserUtil.parseTimestampUs(localTimestampMatcher.group(1)); - tsTimestampUs = TimestampAdjuster.ptsToUs(Long.parseLong(mediaTimestampMatcher.group(1))); + vttTimestampUs = + WebvttParserUtil.parseTimestampUs( + Assertions.checkNotNull(localTimestampMatcher.group(1))); + tsTimestampUs = + TimestampAdjuster.ptsToUs( + Long.parseLong(Assertions.checkNotNull(mediaTimestampMatcher.group(1)))); } } @@ -171,7 +174,8 @@ public final class WebvttExtractor implements Extractor { return; } - long firstCueTimeUs = WebvttParserUtil.parseTimestampUs(cueHeaderMatcher.group(1)); + long firstCueTimeUs = + WebvttParserUtil.parseTimestampUs(Assertions.checkNotNull(cueHeaderMatcher.group(1))); long sampleTimeUs = timestampAdjuster.adjustTsTimestamp( TimestampAdjuster.usToPts(firstCueTimeUs + tsTimestampUs - vttTimestampUs)); long subsampleOffsetUs = sampleTimeUs - firstCueTimeUs; @@ -186,8 +190,12 @@ public final class WebvttExtractor implements Extractor { @RequiresNonNull("output") private TrackOutput buildTrackOutput(long subsampleOffsetUs) { TrackOutput trackOutput = output.track(0, C.TRACK_TYPE_TEXT); - trackOutput.format(Format.createTextSampleFormat(null, MimeTypes.TEXT_VTT, null, - Format.NO_VALUE, 0, language, null, subsampleOffsetUs)); + trackOutput.format( + new Format.Builder() + .setSampleMimeType(MimeTypes.TEXT_VTT) + .setLanguage(language) + .setSubsampleOffsetUs(subsampleOffsetUs) + .build()); output.endTracks(); return trackOutput; } 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 6e6d0afd49..2e97c4bc58 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 @@ -16,8 +16,7 @@ package com.google.android.exoplayer2.source.hls.offline; import android.net.Uri; -import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.offline.DownloaderConstructorHelper; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.offline.SegmentDownloader; import com.google.android.exoplayer2.offline.StreamKey; import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist; @@ -26,12 +25,13 @@ import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylist; import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylistParser; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSpec; -import com.google.android.exoplayer2.upstream.ParsingLoadable; +import com.google.android.exoplayer2.upstream.cache.CacheDataSource; import com.google.android.exoplayer2.util.UriUtil; import java.io.IOException; import java.util.ArrayList; import java.util.HashSet; import java.util.List; +import java.util.concurrent.Executor; /** * A downloader for HLS streams. @@ -40,20 +40,20 @@ import java.util.List; * *
      {@code
        * SimpleCache cache = new SimpleCache(downloadFolder, new NoOpCacheEvictor(), databaseProvider);
      - * DefaultHttpDataSourceFactory factory = new DefaultHttpDataSourceFactory("ExoPlayer", null);
      - * DownloaderConstructorHelper constructorHelper =
      - *     new DownloaderConstructorHelper(cache, factory);
      + * CacheDataSource.Factory cacheDataSourceFactory =
      + *     new CacheDataSource.Factory()
      + *         .setCache(cache)
      + *         .setUpstreamDataSourceFactory(new DefaultHttpDataSourceFactory(userAgent));
        * // Create a downloader for the first variant in a master playlist.
        * HlsDownloader hlsDownloader =
        *     new HlsDownloader(
        *         playlistUri,
      - *         Collections.singletonList(new StreamKey(HlsMasterPlaylist.GROUP_INDEX_VARIANT, 0)),
      - *         constructorHelper);
      + *         Collections.singletonList(new StreamKey(HlsMasterPlaylist.GROUP_INDEX_VARIANT, 0));
        * // Perform the download.
        * hlsDownloader.download(progressListener);
      - * // Access downloaded data using CacheDataSource
      - * CacheDataSource cacheDataSource =
      - *     new CacheDataSource(cache, factory.createDataSource(), CacheDataSource.FLAG_BLOCK_ON_CACHE);
      + * // Use the downloaded data for playback.
      + * HlsMediaSource mediaSource =
      + *     new HlsMediaSource.Factory(cacheDataSourceFactory).createMediaSource(mediaItem);
        * }
      */ public final class HlsDownloader extends SegmentDownloader { @@ -62,16 +62,30 @@ public final class HlsDownloader extends SegmentDownloader { * @param playlistUri The {@link Uri} of the playlist to be downloaded. * @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. + * @param cacheDataSourceFactory A {@link CacheDataSource.Factory} for the cache into which the + * download will be written. */ public HlsDownloader( - Uri playlistUri, List streamKeys, DownloaderConstructorHelper constructorHelper) { - super(playlistUri, streamKeys, constructorHelper); + Uri playlistUri, List streamKeys, CacheDataSource.Factory cacheDataSourceFactory) { + this(playlistUri, streamKeys, cacheDataSourceFactory, Runnable::run); } - @Override - protected HlsPlaylist getManifest(DataSource dataSource, DataSpec dataSpec) throws IOException { - return loadManifest(dataSource, dataSpec); + /** + * @param playlistUri The {@link Uri} of the playlist to be downloaded. + * @param streamKeys Keys defining which renditions in the playlist should be selected for + * download. If empty, all renditions are downloaded. + * @param cacheDataSourceFactory A {@link CacheDataSource.Factory} for the cache into which the + * download will be written. + * @param executor An {@link Executor} used to make requests for the media being downloaded. + * Providing an {@link Executor} that uses multiple threads will speed up the download by + * allowing parts of it to be executed in parallel. + */ + public HlsDownloader( + Uri playlistUri, + List streamKeys, + CacheDataSource.Factory cacheDataSourceFactory, + Executor executor) { + super(playlistUri, new HlsPlaylistParser(), streamKeys, cacheDataSourceFactory, executor); } @Override @@ -92,7 +106,7 @@ public final class HlsDownloader extends SegmentDownloader { segments.add(new Segment(/* startTimeUs= */ 0, mediaPlaylistDataSpec)); HlsMediaPlaylist mediaPlaylist; try { - mediaPlaylist = (HlsMediaPlaylist) loadManifest(dataSource, mediaPlaylistDataSpec); + mediaPlaylist = (HlsMediaPlaylist) getManifest(dataSource, mediaPlaylistDataSpec); } catch (IOException e) { if (!allowIncompleteList) { throw e; @@ -100,7 +114,7 @@ public final class HlsDownloader extends SegmentDownloader { // Generating an incomplete segment list is allowed. Advance to the next media playlist. continue; } - HlsMediaPlaylist.Segment lastInitSegment = null; + @Nullable HlsMediaPlaylist.Segment lastInitSegment = null; List hlsSegments = mediaPlaylist.segments; for (int i = 0; i < hlsSegments.size(); i++) { HlsMediaPlaylist.Segment segment = hlsSegments.get(i); @@ -121,12 +135,6 @@ public final class HlsDownloader extends SegmentDownloader { } } - private static HlsPlaylist loadManifest(DataSource dataSource, DataSpec dataSpec) - throws IOException { - return ParsingLoadable.load( - dataSource, new HlsPlaylistParser(), dataSpec, C.DATA_TYPE_MANIFEST); - } - private void addSegment( HlsMediaPlaylist mediaPlaylist, HlsMediaPlaylist.Segment segment, @@ -141,8 +149,7 @@ public final class HlsDownloader extends SegmentDownloader { } } Uri segmentUri = UriUtil.resolveToUri(baseUri, segment.url); - DataSpec dataSpec = - new DataSpec(segmentUri, segment.byterangeOffset, segment.byterangeLength, /* key= */ null); + DataSpec dataSpec = new DataSpec(segmentUri, segment.byteRangeOffset, segment.byteRangeLength); out.add(new Segment(startTimeUs, dataSpec)); } } 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 index e624027d75..f179447785 100644 --- 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 @@ -21,12 +21,15 @@ import android.os.SystemClock; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ParserException; +import com.google.android.exoplayer2.source.LoadEventInfo; +import com.google.android.exoplayer2.source.MediaLoadData; 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.Variant; 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.LoadErrorHandlingPolicy.LoadErrorInfo; import com.google.android.exoplayer2.upstream.Loader; import com.google.android.exoplayer2.upstream.Loader.LoadErrorAction; import com.google.android.exoplayer2.upstream.ParsingLoadable; @@ -135,9 +138,9 @@ public final class DefaultHlsPlaylistTracker this, loadErrorHandlingPolicy.getMinimumLoadableRetryCount(masterPlaylistLoadable.type)); eventDispatcher.loadStarted( - masterPlaylistLoadable.dataSpec, - masterPlaylistLoadable.type, - elapsedRealtime); + new LoadEventInfo( + masterPlaylistLoadable.loadTaskId, masterPlaylistLoadable.dataSpec, elapsedRealtime), + masterPlaylistLoadable.type); } @Override @@ -241,14 +244,17 @@ public final class DefaultHlsPlaylistTracker } else { primaryBundle.loadPlaylist(); } - eventDispatcher.loadCompleted( - loadable.dataSpec, - loadable.getUri(), - loadable.getResponseHeaders(), - C.DATA_TYPE_MANIFEST, - elapsedRealtimeMs, - loadDurationMs, - loadable.bytesLoaded()); + LoadEventInfo loadEventInfo = + new LoadEventInfo( + loadable.loadTaskId, + loadable.dataSpec, + loadable.getUri(), + loadable.getResponseHeaders(), + elapsedRealtimeMs, + loadDurationMs, + loadable.bytesLoaded()); + loadErrorHandlingPolicy.onLoadTaskConcluded(loadable.loadTaskId); + eventDispatcher.loadCompleted(loadEventInfo, C.DATA_TYPE_MANIFEST); } @Override @@ -257,14 +263,17 @@ public final class DefaultHlsPlaylistTracker long elapsedRealtimeMs, long loadDurationMs, boolean released) { - eventDispatcher.loadCanceled( - loadable.dataSpec, - loadable.getUri(), - loadable.getResponseHeaders(), - C.DATA_TYPE_MANIFEST, - elapsedRealtimeMs, - loadDurationMs, - loadable.bytesLoaded()); + LoadEventInfo loadEventInfo = + new LoadEventInfo( + loadable.loadTaskId, + loadable.dataSpec, + loadable.getUri(), + loadable.getResponseHeaders(), + elapsedRealtimeMs, + loadDurationMs, + loadable.bytesLoaded()); + loadErrorHandlingPolicy.onLoadTaskConcluded(loadable.loadTaskId); + eventDispatcher.loadCanceled(loadEventInfo, C.DATA_TYPE_MANIFEST); } @Override @@ -274,20 +283,24 @@ public final class DefaultHlsPlaylistTracker long loadDurationMs, IOException error, int errorCount) { + LoadEventInfo loadEventInfo = + new LoadEventInfo( + loadable.loadTaskId, + loadable.dataSpec, + loadable.getUri(), + loadable.getResponseHeaders(), + elapsedRealtimeMs, + loadDurationMs, + loadable.bytesLoaded()); + MediaLoadData mediaLoadData = new MediaLoadData(loadable.type); long retryDelayMs = loadErrorHandlingPolicy.getRetryDelayMsFor( - loadable.type, loadDurationMs, error, errorCount); + new LoadErrorInfo(loadEventInfo, mediaLoadData, error, errorCount)); boolean isFatal = retryDelayMs == C.TIME_UNSET; - eventDispatcher.loadError( - loadable.dataSpec, - loadable.getUri(), - loadable.getResponseHeaders(), - C.DATA_TYPE_MANIFEST, - elapsedRealtimeMs, - loadDurationMs, - loadable.bytesLoaded(), - error, - isFatal); + eventDispatcher.loadError(loadEventInfo, loadable.type, error, isFatal); + if (isFatal) { + loadErrorHandlingPolicy.onLoadTaskConcluded(loadable.loadTaskId); + } return isFatal ? Loader.DONT_RETRY_FATAL : Loader.createRetryAction(/* resetErrorCount= */ false, retryDelayMs); @@ -518,18 +531,23 @@ public final class DefaultHlsPlaylistTracker public void onLoadCompleted( ParsingLoadable loadable, long elapsedRealtimeMs, long loadDurationMs) { HlsPlaylist result = loadable.getResult(); + LoadEventInfo loadEventInfo = + new LoadEventInfo( + loadable.loadTaskId, + loadable.dataSpec, + loadable.getUri(), + loadable.getResponseHeaders(), + elapsedRealtimeMs, + loadDurationMs, + loadable.bytesLoaded()); + loadErrorHandlingPolicy.onLoadTaskConcluded(loadable.loadTaskId); if (result instanceof HlsMediaPlaylist) { processLoadedPlaylist((HlsMediaPlaylist) result, loadDurationMs); - eventDispatcher.loadCompleted( - loadable.dataSpec, - loadable.getUri(), - loadable.getResponseHeaders(), - C.DATA_TYPE_MANIFEST, - elapsedRealtimeMs, - loadDurationMs, - loadable.bytesLoaded()); + eventDispatcher.loadCompleted(loadEventInfo, C.DATA_TYPE_MANIFEST); } else { playlistError = new ParserException("Loaded playlist has unexpected type."); + eventDispatcher.loadError( + loadEventInfo, C.DATA_TYPE_MANIFEST, playlistError, /* wasCanceled= */ true); } } @@ -539,14 +557,17 @@ public final class DefaultHlsPlaylistTracker long elapsedRealtimeMs, long loadDurationMs, boolean released) { - eventDispatcher.loadCanceled( - loadable.dataSpec, - loadable.getUri(), - loadable.getResponseHeaders(), - C.DATA_TYPE_MANIFEST, - elapsedRealtimeMs, - loadDurationMs, - loadable.bytesLoaded()); + LoadEventInfo loadEventInfo = + new LoadEventInfo( + loadable.loadTaskId, + loadable.dataSpec, + loadable.getUri(), + loadable.getResponseHeaders(), + elapsedRealtimeMs, + loadDurationMs, + loadable.bytesLoaded()); + loadErrorHandlingPolicy.onLoadTaskConcluded(loadable.loadTaskId); + eventDispatcher.loadCanceled(loadEventInfo, C.DATA_TYPE_MANIFEST); } @Override @@ -556,11 +577,20 @@ public final class DefaultHlsPlaylistTracker long loadDurationMs, IOException error, int errorCount) { + LoadEventInfo loadEventInfo = + new LoadEventInfo( + loadable.loadTaskId, + loadable.dataSpec, + loadable.getUri(), + loadable.getResponseHeaders(), + elapsedRealtimeMs, + loadDurationMs, + loadable.bytesLoaded()); + MediaLoadData mediaLoadData = new MediaLoadData(loadable.type); + LoadErrorInfo loadErrorInfo = + new LoadErrorInfo(loadEventInfo, mediaLoadData, error, errorCount); LoadErrorAction loadErrorAction; - - long blacklistDurationMs = - loadErrorHandlingPolicy.getBlacklistDurationMsFor( - loadable.type, loadDurationMs, error, errorCount); + long blacklistDurationMs = loadErrorHandlingPolicy.getBlacklistDurationMsFor(loadErrorInfo); boolean shouldBlacklist = blacklistDurationMs != C.TIME_UNSET; boolean blacklistingFailed = @@ -570,9 +600,7 @@ public final class DefaultHlsPlaylistTracker } if (blacklistingFailed) { - long retryDelay = - loadErrorHandlingPolicy.getRetryDelayMsFor( - loadable.type, loadDurationMs, error, errorCount); + long retryDelay = loadErrorHandlingPolicy.getRetryDelayMsFor(loadErrorInfo); loadErrorAction = retryDelay != C.TIME_UNSET ? Loader.createRetryAction(false, retryDelay) @@ -581,17 +609,11 @@ public final class DefaultHlsPlaylistTracker loadErrorAction = Loader.DONT_RETRY; } - eventDispatcher.loadError( - loadable.dataSpec, - loadable.getUri(), - loadable.getResponseHeaders(), - C.DATA_TYPE_MANIFEST, - elapsedRealtimeMs, - loadDurationMs, - loadable.bytesLoaded(), - error, - /* wasCanceled= */ !loadErrorAction.isRetry()); - + boolean wasCanceled = !loadErrorAction.isRetry(); + eventDispatcher.loadError(loadEventInfo, loadable.type, error, wasCanceled); + if (wasCanceled) { + loadErrorHandlingPolicy.onLoadTaskConcluded(loadable.loadTaskId); + } return loadErrorAction; } @@ -612,9 +634,9 @@ public final class DefaultHlsPlaylistTracker this, loadErrorHandlingPolicy.getMinimumLoadableRetryCount(mediaPlaylistLoadable.type)); eventDispatcher.loadStarted( - mediaPlaylistLoadable.dataSpec, - mediaPlaylistLoadable.type, - elapsedRealtime); + new LoadEventInfo( + mediaPlaylistLoadable.loadTaskId, mediaPlaylistLoadable.dataSpec, elapsedRealtime), + mediaPlaylistLoadable.type); } private void processLoadedPlaylist(HlsMediaPlaylist loadedPlaylist, long loadDurationMs) { 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 f96c7dfa92..72f6a361d7 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 @@ -102,16 +102,7 @@ public final class HlsMasterPlaylist extends HlsPlaylist { */ public static Variant createMediaPlaylistVariantUrl(Uri url) { Format format = - Format.createContainerFormat( - "0", - /* label= */ null, - MimeTypes.APPLICATION_M3U8, - /* sampleMimeType= */ null, - /* codecs= */ null, - /* bitrate= */ Format.NO_VALUE, - /* selectionFlags= */ 0, - /* roleFlags= */ 0, - /* language= */ null); + new Format.Builder().setId("0").setContainerMimeType(MimeTypes.APPLICATION_M3U8).build(); return new Variant( url, format, 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 58f500cf94..be771b92fc 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 @@ -70,30 +70,28 @@ public final class HlsMediaPlaylist extends HlsPlaylist { * encrypted. */ @Nullable public final String encryptionIV; - /** - * The segment's byte range offset, as defined by #EXT-X-BYTERANGE. - */ - public final long byterangeOffset; + /** The segment's byte range offset, as defined by #EXT-X-BYTERANGE. */ + public final long byteRangeOffset; /** * The segment's byte range length, as defined by #EXT-X-BYTERANGE, or {@link C#LENGTH_UNSET} if * no byte range is specified. */ - public final long byterangeLength; + public final long byteRangeLength; /** Whether the segment is tagged with #EXT-X-GAP. */ public final boolean hasGapTag; /** * @param uri See {@link #url}. - * @param byterangeOffset See {@link #byterangeOffset}. - * @param byterangeLength See {@link #byterangeLength}. + * @param byteRangeOffset See {@link #byteRangeOffset}. + * @param byteRangeLength See {@link #byteRangeLength}. * @param fullSegmentEncryptionKeyUri See {@link #fullSegmentEncryptionKeyUri}. * @param encryptionIV See {@link #encryptionIV}. */ public Segment( String uri, - long byterangeOffset, - long byterangeLength, + long byteRangeOffset, + long byteRangeLength, @Nullable String fullSegmentEncryptionKeyUri, @Nullable String encryptionIV) { this( @@ -106,8 +104,8 @@ public final class HlsMediaPlaylist extends HlsPlaylist { /* drmInitData= */ null, fullSegmentEncryptionKeyUri, encryptionIV, - byterangeOffset, - byterangeLength, + byteRangeOffset, + byteRangeLength, /* hasGapTag= */ false); } @@ -121,8 +119,8 @@ public final class HlsMediaPlaylist extends HlsPlaylist { * @param drmInitData See {@link #drmInitData}. * @param fullSegmentEncryptionKeyUri See {@link #fullSegmentEncryptionKeyUri}. * @param encryptionIV See {@link #encryptionIV}. - * @param byterangeOffset See {@link #byterangeOffset}. - * @param byterangeLength See {@link #byterangeLength}. + * @param byteRangeOffset See {@link #byteRangeOffset}. + * @param byteRangeLength See {@link #byteRangeLength}. * @param hasGapTag See {@link #hasGapTag}. */ public Segment( @@ -135,8 +133,8 @@ public final class HlsMediaPlaylist extends HlsPlaylist { @Nullable DrmInitData drmInitData, @Nullable String fullSegmentEncryptionKeyUri, @Nullable String encryptionIV, - long byterangeOffset, - long byterangeLength, + long byteRangeOffset, + long byteRangeLength, boolean hasGapTag) { this.url = url; this.initializationSegment = initializationSegment; @@ -147,8 +145,8 @@ public final class HlsMediaPlaylist extends HlsPlaylist { this.drmInitData = drmInitData; this.fullSegmentEncryptionKeyUri = fullSegmentEncryptionKeyUri; this.encryptionIV = encryptionIV; - this.byterangeOffset = byterangeOffset; - this.byterangeLength = byterangeLength; + this.byteRangeOffset = byteRangeOffset; + this.byteRangeLength = byteRangeLength; this.hasGapTag = hasGapTag; } 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 993ce8e5c1..77e541fb57 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 @@ -69,6 +69,8 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser variantInfosForUrl = urlToVariantInfos.get(uri); + @Nullable ArrayList variantInfosForUrl = urlToVariantInfos.get(uri); if (variantInfosForUrl == null) { variantInfosForUrl = new ArrayList<>(); urlToVariantInfos.put(uri, variantInfosForUrl); } variantInfosForUrl.add( new VariantInfo( - bitrate, videoGroupId, audioGroupId, subtitlesGroupId, closedCaptionsGroupId)); + averageBitrate, + peakBitrate, + videoGroupId, + audioGroupId, + subtitlesGroupId, + closedCaptionsGroupId)); } } @@ -385,9 +395,9 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser(); } - muxedCaptionFormats.add( - Format.createTextContainerFormat( - /* id= */ formatId, - /* label= */ name, - /* containerMimeType= */ null, - /* sampleMimeType= */ mimeType, - /* codecs= */ null, - /* bitrate= */ Format.NO_VALUE, - selectionFlags, - roleFlags, - language, - accessibilityChannel)); + formatBuilder + .setSampleMimeType(sampleMimeType) + .setAccessibilityChannel(accessibilityChannel); + muxedCaptionFormats.add(formatBuilder.build()); // TODO: Remove muxedCaptionFormats and add a Rendition with a null uri to closedCaptions. break; default: @@ -569,6 +548,17 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser variants, String groupId) { + for (int i = 0; i < variants.size(); i++) { + Variant variant = variants.get(i); + if (groupId.equals(variant.subtitleGroupId)) { + return variant; + } + } + return null; + } + private static HlsMediaPlaylist parseMediaPlaylist( HlsMasterPlaylist masterPlaylist, LineIterator iterator, String baseUri) throws IOException { @HlsMediaPlaylist.PlaylistType int playlistType = HlsMediaPlaylist.PLAYLIST_TYPE_UNKNOWN; @@ -578,8 +568,9 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser variableDefinitions = new HashMap<>(); + HashMap urlToInferredInitSegment = new HashMap<>(); List segments = new ArrayList<>(); List tags = new ArrayList<>(); @@ -592,6 +583,7 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser variableDefinitions) { Matcher matcher = pattern.matcher(line); - String value = matcher.find() ? matcher.group(1) : defaultValue; + @PolyNull + String value = matcher.find() ? Assertions.checkNotNull(matcher.group(1)) : defaultValue; return variableDefinitions.isEmpty() || value == null ? value : replaceVariableReferences(value, variableDefinitions); @@ -931,7 +943,7 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser new Renderer[0]); + (handler, videoListener, audioListener, text, metadata) -> new Renderer[0]); DownloadHelper.forHls( Uri.parse("http://uri"), new FakeDataSource.Factory(), - (handler, videoListener, audioListener, text, metadata, drm) -> new Renderer[0], + (handler, videoListener, audioListener, text, metadata) -> new Renderer[0], /* drmSessionManager= */ null, DownloadHelper.DEFAULT_TRACK_SELECTOR_PARAMETERS_WITHOUT_VIEWPORT); } 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 d06d047f66..0dcae17f74 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 @@ -40,15 +40,15 @@ import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.offline.DefaultDownloaderFactory; import com.google.android.exoplayer2.offline.DownloadRequest; import com.google.android.exoplayer2.offline.Downloader; -import com.google.android.exoplayer2.offline.DownloaderConstructorHelper; import com.google.android.exoplayer2.offline.DownloaderFactory; import com.google.android.exoplayer2.offline.StreamKey; import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist; import com.google.android.exoplayer2.testutil.CacheAsserts.RequestSet; import com.google.android.exoplayer2.testutil.FakeDataSet; -import com.google.android.exoplayer2.testutil.FakeDataSource.Factory; +import com.google.android.exoplayer2.testutil.FakeDataSource; import com.google.android.exoplayer2.upstream.DummyDataSource; import com.google.android.exoplayer2.upstream.cache.Cache; +import com.google.android.exoplayer2.upstream.cache.CacheDataSource; import com.google.android.exoplayer2.upstream.cache.NoOpCacheEvictor; import com.google.android.exoplayer2.upstream.cache.SimpleCache; import com.google.android.exoplayer2.util.Util; @@ -96,10 +96,12 @@ public class HlsDownloaderTest { } @Test - public void testCreateWithDefaultDownloaderFactory() { - DownloaderConstructorHelper constructorHelper = - new DownloaderConstructorHelper(Mockito.mock(Cache.class), DummyDataSource.FACTORY); - DownloaderFactory factory = new DefaultDownloaderFactory(constructorHelper); + public void createWithDefaultDownloaderFactory() { + CacheDataSource.Factory cacheDataSourceFactory = + new CacheDataSource.Factory() + .setCache(Mockito.mock(Cache.class)) + .setUpstreamDataSourceFactory(DummyDataSource.FACTORY); + DownloaderFactory factory = new DefaultDownloaderFactory(cacheDataSourceFactory); Downloader downloader = factory.createDownloader( @@ -114,7 +116,7 @@ public class HlsDownloaderTest { } @Test - public void testCounterMethods() throws Exception { + public void counterMethods() throws Exception { HlsDownloader downloader = getHlsDownloader(MASTER_PLAYLIST_URI, getKeys(MASTER_MEDIA_PLAYLIST_1_INDEX)); downloader.download(progressListener); @@ -123,7 +125,7 @@ public class HlsDownloaderTest { } @Test - public void testDownloadRepresentation() throws Exception { + public void downloadRepresentation() throws Exception { HlsDownloader downloader = getHlsDownloader(MASTER_PLAYLIST_URI, getKeys(MASTER_MEDIA_PLAYLIST_1_INDEX)); downloader.download(progressListener); @@ -140,7 +142,7 @@ public class HlsDownloaderTest { } @Test - public void testDownloadMultipleRepresentations() throws Exception { + public void downloadMultipleRepresentations() throws Exception { HlsDownloader downloader = getHlsDownloader( MASTER_PLAYLIST_URI, @@ -151,7 +153,7 @@ public class HlsDownloaderTest { } @Test - public void testDownloadAllRepresentations() throws Exception { + public void downloadAllRepresentations() throws Exception { // Add data for the rest of the playlists fakeDataSet .setData(MEDIA_PLAYLIST_0_URI, MEDIA_PLAYLIST_DATA) @@ -170,7 +172,7 @@ public class HlsDownloaderTest { } @Test - public void testRemove() throws Exception { + public void remove() throws Exception { HlsDownloader downloader = getHlsDownloader( MASTER_PLAYLIST_URI, @@ -182,7 +184,7 @@ public class HlsDownloaderTest { } @Test - public void testDownloadMediaPlaylist() throws Exception { + public void downloadMediaPlaylist() throws Exception { HlsDownloader downloader = getHlsDownloader(MEDIA_PLAYLIST_1_URI, getKeys()); downloader.download(progressListener); @@ -197,7 +199,7 @@ public class HlsDownloaderTest { } @Test - public void testDownloadEncMediaPlaylist() throws Exception { + public void downloadEncMediaPlaylist() throws Exception { fakeDataSet = new FakeDataSet() .setData(ENC_MEDIA_PLAYLIST_URI, ENC_MEDIA_PLAYLIST_DATA) @@ -213,9 +215,11 @@ public class HlsDownloaderTest { } private HlsDownloader getHlsDownloader(String mediaPlaylistUri, List keys) { - Factory factory = new Factory().setFakeDataSet(fakeDataSet); - return new HlsDownloader( - Uri.parse(mediaPlaylistUri), keys, new DownloaderConstructorHelper(cache, factory)); + CacheDataSource.Factory cacheDataSourceFactory = + new CacheDataSource.Factory() + .setCache(cache) + .setUpstreamDataSourceFactory(new FakeDataSource.Factory().setFakeDataSet(fakeDataSet)); + return new HlsDownloader(Uri.parse(mediaPlaylistUri), keys, cacheDataSourceFactory); } private static ArrayList getKeys(int... variantIndices) { 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 0aa78d9f02..05bc3ba985 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 @@ -25,6 +25,7 @@ import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.source.hls.HlsTrackMetadataEntry; +import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist.Variant; import com.google.android.exoplayer2.util.MimeTypes; import java.io.ByteArrayInputStream; import java.io.IOException; @@ -35,7 +36,7 @@ import java.util.List; import org.junit.Test; import org.junit.runner.RunWith; -/** Test for {@link HlsMasterPlaylistParserTest}. */ +/** Test for {@link HlsMasterPlaylist}. */ @RunWith(AndroidJUnit4.class) public class HlsMasterPlaylistParserTest { @@ -194,8 +195,37 @@ public class HlsMasterPlaylistParserTest { + "#EXT-X-MEDIA:TYPE=SUBTITLES," + "GROUP-ID=\"sub1\",NAME=\"English\",URI=\"s1/en/prog_index.m3u8\"\n"; + private static final String PLAYLIST_WITH_TTML_SUBTITLE = + " #EXTM3U\n" + + "\n" + + "#EXT-X-VERSION:6\n" + + "\n" + + "#EXT-X-INDEPENDENT-SEGMENTS\n" + + "\n" + + "#EXT-X-STREAM-INF:BANDWIDTH=1280000,CODECS=\"stpp.ttml.im1t,mp4a.40.2,avc1.66.30\",RESOLUTION=304x128,AUDIO=\"aud1\",SUBTITLES=\"sub1\"\n" + + "http://example.com/low.m3u8\n" + + "\n" + + "#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"aud1\",NAME=\"English\",URI=\"a1/index.m3u8\"\n" + + "#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID=\"sub1\",NAME=\"English\",AUTOSELECT=YES,DEFAULT=YES,URI=\"s1/en/prog_index.m3u8\"\n"; + + private static final String PLAYLIST_WITH_IFRAME_VARIANTS = + "#EXTM3U\n" + + "#EXT-X-VERSION:5\n" + + "#EXT-X-MEDIA:URI=\"AUDIO_English/index.m3u8\",TYPE=AUDIO,GROUP-ID=\"audio-aac\",LANGUAGE=\"en\",NAME=\"English\",AUTOSELECT=YES\n" + + "#EXT-X-MEDIA:URI=\"AUDIO_Spanish/index.m3u8\",TYPE=AUDIO,GROUP-ID=\"audio-aac\",LANGUAGE=\"es\",NAME=\"Spanish\",AUTOSELECT=YES\n" + + "#EXT-X-MEDIA:TYPE=CLOSED-CAPTIONS,GROUP-ID=\"cc1\",LANGUAGE=\"en\",NAME=\"English\",AUTOSELECT=YES,DEFAULT=YES,INSTREAM-ID=\"CC1\"\n" + + "#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=400000,RESOLUTION=480x320,CODECS=\"mp4a.40.2,avc1.640015\",AUDIO=\"audio-aac\",CLOSED-CAPTIONS=\"cc1\"\n" + + "400000/index.m3u8\n" + + "#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=1000000,RESOLUTION=848x480,CODECS=\"mp4a.40.2,avc1.64001f\",AUDIO=\"audio-aac\",CLOSED-CAPTIONS=\"cc1\"\n" + + "1000000/index.m3u8\n" + + "#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=3220000,RESOLUTION=1280x720,CODECS=\"mp4a.40.2,avc1.64001f\",AUDIO=\"audio-aac\",CLOSED-CAPTIONS=\"cc1\"\n" + + "3220000/index.m3u8\n" + + "#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=8940000,RESOLUTION=1920x1080,CODECS=\"mp4a.40.2,avc1.640028\",AUDIO=\"audio-aac\",CLOSED-CAPTIONS=\"cc1\"\n" + + "8940000/index.m3u8\n" + + "#EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=1313400,RESOLUTION=1920x1080,CODECS=\"avc1.640028\",URI=\"iframe_1313400/index.m3u8\"\n"; + @Test - public void testParseMasterPlaylist() throws IOException { + public void parseMasterPlaylist_withSimple_success() throws IOException { HlsMasterPlaylist masterPlaylist = parseMasterPlaylist(PLAYLIST_URI, PLAYLIST_SIMPLE); List variants = masterPlaylist.variants; @@ -236,7 +266,7 @@ public class HlsMasterPlaylistParserTest { } @Test - public void testMasterPlaylistWithBandwdithAverage() throws IOException { + public void parseMasterPlaylist_withAverageBandwidth_success() throws IOException { HlsMasterPlaylist masterPlaylist = parseMasterPlaylist(PLAYLIST_URI, PLAYLIST_WITH_AVG_BANDWIDTH); @@ -247,7 +277,7 @@ public class HlsMasterPlaylistParserTest { } @Test - public void testPlaylistWithInvalidHeader() throws IOException { + public void parseMasterPlaylist_withInvalidHeader_throwsException() throws IOException { try { parseMasterPlaylist(PLAYLIST_URI, PLAYLIST_WITH_INVALID_HEADER); fail("Expected exception not thrown."); @@ -257,7 +287,7 @@ public class HlsMasterPlaylistParserTest { } @Test - public void testPlaylistWithClosedCaption() throws IOException { + public void parseMasterPlaylist_withClosedCaption_success() throws IOException { HlsMasterPlaylist playlist = parseMasterPlaylist(PLAYLIST_URI, PLAYLIST_WITH_CC); assertThat(playlist.muxedCaptionFormats).hasSize(1); Format closedCaptionFormat = playlist.muxedCaptionFormats.get(0); @@ -267,7 +297,7 @@ public class HlsMasterPlaylistParserTest { } @Test - public void testPlaylistWithChannelsAttribute() throws IOException { + public void parseMasterPlaylist_withChannelsAttribute_success() throws IOException { HlsMasterPlaylist playlist = parseMasterPlaylist(PLAYLIST_URI, PLAYLIST_WITH_CHANNELS_ATTRIBUTE); List audios = playlist.audios; @@ -278,13 +308,13 @@ public class HlsMasterPlaylistParserTest { } @Test - public void testPlaylistWithoutClosedCaptions() throws IOException { + public void parseMasterPlaylist_withoutClosedCaption_success() throws IOException { HlsMasterPlaylist playlist = parseMasterPlaylist(PLAYLIST_URI, PLAYLIST_WITHOUT_CC); assertThat(playlist.muxedCaptionFormats).isEmpty(); } @Test - public void testCodecPropagation() throws IOException { + public void parseMasterPlaylist_withAudio_codecPropagated() throws IOException { HlsMasterPlaylist playlist = parseMasterPlaylist(PLAYLIST_URI, PLAYLIST_WITH_AUDIO_MEDIA_TAG); Format firstAudioFormat = playlist.audios.get(0).format; @@ -297,7 +327,7 @@ public class HlsMasterPlaylistParserTest { } @Test - public void testAudioIdPropagation() throws IOException { + public void parseMasterPlaylist_withAudio_audioIdPropagated() throws IOException { HlsMasterPlaylist playlist = parseMasterPlaylist(PLAYLIST_URI, PLAYLIST_WITH_AUDIO_MEDIA_TAG); Format firstAudioFormat = playlist.audios.get(0).format; @@ -308,7 +338,7 @@ public class HlsMasterPlaylistParserTest { } @Test - public void testCCIdPropagation() throws IOException { + public void parseMasterPlaylist_withCc_cCIdPropagated() throws IOException { HlsMasterPlaylist playlist = parseMasterPlaylist(PLAYLIST_URI, PLAYLIST_WITH_CC); Format firstTextFormat = playlist.muxedCaptionFormats.get(0); @@ -316,15 +346,17 @@ public class HlsMasterPlaylistParserTest { } @Test - public void testSubtitleIdPropagation() throws IOException { + public void parseMasterPlaylist_withSubtitles_subtitlesIdPropagated() throws IOException { HlsMasterPlaylist playlist = parseMasterPlaylist(PLAYLIST_URI, PLAYLIST_WITH_SUBTITLES); Format firstTextFormat = playlist.subtitles.get(0).format; assertThat(firstTextFormat.id).isEqualTo("sub1:Eng"); + assertThat(firstTextFormat.sampleMimeType).isEqualTo(MimeTypes.TEXT_VTT); } @Test - public void testIndependentSegments() throws IOException { + public void parseMasterPlaylist_withIndependentSegments_hasNoIndenpendentSegments() + throws IOException { HlsMasterPlaylist playlistWithIndependentSegments = parseMasterPlaylist(PLAYLIST_URI, PLAYLIST_WITH_INDEPENDENT_SEGMENTS); assertThat(playlistWithIndependentSegments.hasIndependentSegments).isTrue(); @@ -335,7 +367,7 @@ public class HlsMasterPlaylistParserTest { } @Test - public void testVariableSubstitution() throws IOException { + public void parseMasterPlaylist_withVariableSubstitution_success() throws IOException { HlsMasterPlaylist playlistWithSubstitutions = parseMasterPlaylist(PLAYLIST_URI, PLAYLIST_WITH_VARIABLE_SUBSTITUTION); HlsMasterPlaylist.Variant variant = playlistWithSubstitutions.variants.get(0); @@ -345,31 +377,43 @@ public class HlsMasterPlaylistParserTest { } @Test - public void testHlsMetadata() throws IOException { + public void parseMasterPlaylist_withTtmlSubtitle() throws IOException { + HlsMasterPlaylist playlistWithTtmlSubtitle = + parseMasterPlaylist(PLAYLIST_URI, PLAYLIST_WITH_TTML_SUBTITLE); + HlsMasterPlaylist.Variant variant = playlistWithTtmlSubtitle.variants.get(0); + Format firstTextFormat = playlistWithTtmlSubtitle.subtitles.get(0).format; + assertThat(firstTextFormat.id).isEqualTo("sub1:English"); + assertThat(firstTextFormat.containerMimeType).isEqualTo(MimeTypes.APPLICATION_M3U8); + assertThat(firstTextFormat.sampleMimeType).isEqualTo(MimeTypes.APPLICATION_TTML); + assertThat(variant.format.codecs).isEqualTo("stpp.ttml.im1t,mp4a.40.2,avc1.66.30"); + } + + @Test + public void parseMasterPlaylist_withMatchingStreamInfUrls_success() throws IOException { HlsMasterPlaylist playlist = parseMasterPlaylist(PLAYLIST_URI, PLAYLIST_WITH_MATCHING_STREAM_INF_URLS); assertThat(playlist.variants).hasSize(4); assertThat(playlist.variants.get(0).format.metadata) .isEqualTo( createExtXStreamInfMetadata( - createVariantInfo(/* bitrate= */ 2227464, /* audioGroupId= */ "aud1"), - createVariantInfo(/* bitrate= */ 2448841, /* audioGroupId= */ "aud2"), - createVariantInfo(/* bitrate= */ 2256841, /* audioGroupId= */ "aud3"))); + createVariantInfo(/* peakBitrate= */ 2227464, /* audioGroupId= */ "aud1"), + createVariantInfo(/* peakBitrate= */ 2448841, /* audioGroupId= */ "aud2"), + createVariantInfo(/* peakBitrate= */ 2256841, /* audioGroupId= */ "aud3"))); assertThat(playlist.variants.get(1).format.metadata) .isEqualTo( createExtXStreamInfMetadata( - createVariantInfo(/* bitrate= */ 6453202, /* audioGroupId= */ "aud1"), - createVariantInfo(/* bitrate= */ 6482579, /* audioGroupId= */ "aud3"))); + createVariantInfo(/* peakBitrate= */ 6453202, /* audioGroupId= */ "aud1"), + createVariantInfo(/* peakBitrate= */ 6482579, /* audioGroupId= */ "aud3"))); assertThat(playlist.variants.get(2).format.metadata) .isEqualTo( createExtXStreamInfMetadata( - createVariantInfo(/* bitrate= */ 5054232, /* audioGroupId= */ "aud1"), - createVariantInfo(/* bitrate= */ 5275609, /* audioGroupId= */ "aud2"))); + createVariantInfo(/* peakBitrate= */ 5054232, /* audioGroupId= */ "aud1"), + createVariantInfo(/* peakBitrate= */ 5275609, /* audioGroupId= */ "aud2"))); assertThat(playlist.variants.get(3).format.metadata) .isEqualTo( createExtXStreamInfMetadata( - createVariantInfo(/* bitrate= */ 8399417, /* audioGroupId= */ "aud2"), - createVariantInfo(/* bitrate= */ 8207417, /* audioGroupId= */ "aud3"))); + createVariantInfo(/* peakBitrate= */ 8399417, /* audioGroupId= */ "aud2"), + createVariantInfo(/* peakBitrate= */ 8207417, /* audioGroupId= */ "aud3"))); assertThat(playlist.audios).hasSize(3); assertThat(playlist.audios.get(0).format.metadata) @@ -380,6 +424,19 @@ public class HlsMasterPlaylistParserTest { .isEqualTo(createExtXMediaMetadata(/* groupId= */ "aud3", /* name= */ "English")); } + @Test + public void testIFrameVariant() throws IOException { + HlsMasterPlaylist playlist = parseMasterPlaylist(PLAYLIST_URI, PLAYLIST_WITH_IFRAME_VARIANTS); + assertThat(playlist.variants).hasSize(5); + for (int i = 0; i < 4; i++) { + assertThat(playlist.variants.get(i).format.roleFlags).isEqualTo(0); + } + Variant iFramesOnlyVariant = playlist.variants.get(4); + assertThat(iFramesOnlyVariant.format.bitrate).isEqualTo(1313400); + assertThat(iFramesOnlyVariant.format.roleFlags & C.ROLE_FLAG_TRICK_PLAY) + .isEqualTo(C.ROLE_FLAG_TRICK_PLAY); + } + private static Metadata createExtXStreamInfMetadata(HlsTrackMetadataEntry.VariantInfo... infos) { return new Metadata( new HlsTrackMetadataEntry(/* groupId= */ null, /* name= */ null, Arrays.asList(infos))); @@ -390,9 +447,10 @@ public class HlsMasterPlaylistParserTest { } private static HlsTrackMetadataEntry.VariantInfo createVariantInfo( - long bitrate, String audioGroupId) { + int peakBitrate, String audioGroupId) { return new HlsTrackMetadataEntry.VariantInfo( - bitrate, + /* averageBitrate= */ Format.NO_VALUE, + /* peakBitrate= */ peakBitrate, /* videoGroupId= */ null, audioGroupId, /* subtitleGroupId= */ "sub1", 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 3fd67b294a..dd8a32b7f0 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 @@ -19,6 +19,7 @@ import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.fail; import android.net.Uri; +import androidx.annotation.Nullable; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ParserException; @@ -38,7 +39,7 @@ import org.junit.runner.RunWith; public class HlsMediaPlaylistParserTest { @Test - public void testParseMediaPlaylist() throws Exception { + public void parseMediaPlaylist() throws Exception { Uri playlistUri = Uri.parse("https://example.com/test.m3u8"); String playlistString = "#EXTM3U\n" @@ -96,8 +97,8 @@ public class HlsMediaPlaylistParserTest { assertThat(segment.title).isEqualTo(""); assertThat(segment.fullSegmentEncryptionKeyUri).isNull(); assertThat(segment.encryptionIV).isNull(); - assertThat(segment.byterangeLength).isEqualTo(51370); - assertThat(segment.byterangeOffset).isEqualTo(0); + assertThat(segment.byteRangeLength).isEqualTo(51370); + assertThat(segment.byteRangeOffset).isEqualTo(0); assertThat(segment.url).isEqualTo("https://priv.example.com/fileSequence2679.ts"); segment = segments.get(1); @@ -107,8 +108,8 @@ public class HlsMediaPlaylistParserTest { assertThat(segment.fullSegmentEncryptionKeyUri) .isEqualTo("https://priv.example.com/key.php?r=2680"); assertThat(segment.encryptionIV).isEqualTo("0x1566B"); - assertThat(segment.byterangeLength).isEqualTo(51501); - assertThat(segment.byterangeOffset).isEqualTo(2147483648L); + assertThat(segment.byteRangeLength).isEqualTo(51501); + assertThat(segment.byteRangeOffset).isEqualTo(2147483648L); assertThat(segment.url).isEqualTo("https://priv.example.com/fileSequence2680.ts"); segment = segments.get(2); @@ -117,8 +118,8 @@ public class HlsMediaPlaylistParserTest { assertThat(segment.title).isEqualTo("segment title .,:/# with interesting chars"); assertThat(segment.fullSegmentEncryptionKeyUri).isNull(); assertThat(segment.encryptionIV).isEqualTo(null); - assertThat(segment.byterangeLength).isEqualTo(51501); - assertThat(segment.byterangeOffset).isEqualTo(2147535149L); + assertThat(segment.byteRangeLength).isEqualTo(51501); + assertThat(segment.byteRangeOffset).isEqualTo(2147535149L); assertThat(segment.url).isEqualTo("https://priv.example.com/fileSequence2681.ts"); segment = segments.get(3); @@ -130,8 +131,8 @@ public class HlsMediaPlaylistParserTest { // 0xA7A == 2682. assertThat(segment.encryptionIV).isNotNull(); assertThat(Util.toUpperInvariant(segment.encryptionIV)).isEqualTo("A7A"); - assertThat(segment.byterangeLength).isEqualTo(51740); - assertThat(segment.byterangeOffset).isEqualTo(2147586650L); + assertThat(segment.byteRangeLength).isEqualTo(51740); + assertThat(segment.byteRangeOffset).isEqualTo(2147586650L); assertThat(segment.url).isEqualTo("https://priv.example.com/fileSequence2682.ts"); segment = segments.get(4); @@ -143,13 +144,13 @@ public class HlsMediaPlaylistParserTest { // 0xA7B == 2683. assertThat(segment.encryptionIV).isNotNull(); assertThat(Util.toUpperInvariant(segment.encryptionIV)).isEqualTo("A7B"); - assertThat(segment.byterangeLength).isEqualTo(C.LENGTH_UNSET); - assertThat(segment.byterangeOffset).isEqualTo(0); + assertThat(segment.byteRangeLength).isEqualTo(C.LENGTH_UNSET); + assertThat(segment.byteRangeOffset).isEqualTo(0); assertThat(segment.url).isEqualTo("https://priv.example.com/fileSequence2683.ts"); } @Test - public void testParseSampleAesMethod() throws Exception { + public void parseSampleAesMethod() throws Exception { Uri playlistUri = Uri.parse("https://example.com/test.m3u8"); String playlistString = "#EXTM3U\n" @@ -178,7 +179,7 @@ public class HlsMediaPlaylistParserTest { } @Test - public void testParseSampleAesCencMethod() throws Exception { + public void parseSampleAesCencMethod() throws Exception { Uri playlistUri = Uri.parse("https://example.com/test.m3u8"); String playlistString = "#EXTM3U\n" @@ -202,7 +203,7 @@ public class HlsMediaPlaylistParserTest { } @Test - public void testParseSampleAesCtrMethod() throws Exception { + public void parseSampleAesCtrMethod() throws Exception { Uri playlistUri = Uri.parse("https://example.com/test.m3u8"); String playlistString = "#EXTM3U\n" @@ -226,7 +227,7 @@ public class HlsMediaPlaylistParserTest { } @Test - public void testMultipleExtXKeysForSingleSegment() throws Exception { + public void multipleExtXKeysForSingleSegment() throws Exception { Uri playlistUri = Uri.parse("https://example.com/test.m3u8"); String playlistString = "#EXTM3U\n" @@ -303,7 +304,7 @@ public class HlsMediaPlaylistParserTest { } @Test - public void testGapTag() throws IOException { + public void gapTag() throws IOException { Uri playlistUri = Uri.parse("https://example.com/test2.m3u8"); String playlistString = "#EXTM3U\n" @@ -338,7 +339,7 @@ public class HlsMediaPlaylistParserTest { } @Test - public void testMapTag() throws IOException { + public void mapTag() throws IOException { Uri playlistUri = Uri.parse("https://example.com/test3.m3u8"); String playlistString = "#EXTM3U\n" @@ -368,7 +369,41 @@ public class HlsMediaPlaylistParserTest { } @Test - public void testEncryptedMapTag() throws IOException { + public void noExplicitInitSegmentInIFrameOnly_infersInitSegment() throws IOException { + Uri playlistUri = Uri.parse("https://example.com/test3.m3u8"); + String playlistString = + "#EXTM3U\n" + + "#EXT-X-TARGETDURATION:5\n" + + "#EXT-X-I-FRAMES-ONLY\n" + + "#EXTINF:5.005,\n" + + "#EXT-X-BYTERANGE:100@300\n" + + "segment1.ts\n" + + "#EXTINF:5.005,\n" + + "#EXT-X-BYTERANGE:100@400\n" + + "segment2.ts\n" + + "#EXTINF:5.005,\n" + + "#EXT-X-BYTERANGE:100@400\n" + + "segment1.ts\n"; + InputStream inputStream = new ByteArrayInputStream(Util.getUtf8Bytes(playlistString)); + HlsMediaPlaylist playlist = + (HlsMediaPlaylist) new HlsPlaylistParser().parse(playlistUri, inputStream); + List segments = playlist.segments; + @Nullable Segment initializationSegment = segments.get(0).initializationSegment; + assertThat(initializationSegment.url).isEqualTo("segment1.ts"); + assertThat(initializationSegment.byteRangeOffset).isEqualTo(0); + assertThat(initializationSegment.byteRangeLength).isEqualTo(300); + initializationSegment = segments.get(1).initializationSegment; + assertThat(initializationSegment.url).isEqualTo("segment2.ts"); + assertThat(initializationSegment.byteRangeOffset).isEqualTo(0); + assertThat(initializationSegment.byteRangeLength).isEqualTo(400); + initializationSegment = segments.get(2).initializationSegment; + assertThat(initializationSegment.url).isEqualTo("segment1.ts"); + assertThat(initializationSegment.byteRangeOffset).isEqualTo(0); + assertThat(initializationSegment.byteRangeLength).isEqualTo(300); + } + + @Test + public void encryptedMapTag() throws IOException { Uri playlistUri = Uri.parse("https://example.com/test3.m3u8"); String playlistString = "#EXTM3U\n" @@ -399,7 +434,7 @@ public class HlsMediaPlaylistParserTest { } @Test - public void testEncryptedMapTagWithNoIvFails() throws IOException { + public void encryptedMapTagWithNoIvFails() throws IOException { Uri playlistUri = Uri.parse("https://example.com/test3.m3u8"); String playlistString = "#EXTM3U\n" @@ -423,7 +458,7 @@ public class HlsMediaPlaylistParserTest { } @Test - public void testMasterPlaylistAttributeInheritance() throws IOException { + public void masterPlaylistAttributeInheritance() throws IOException { Uri playlistUri = Uri.parse("https://example.com/test3.m3u8"); String playlistString = "#EXTM3U\n" @@ -466,7 +501,7 @@ public class HlsMediaPlaylistParserTest { } @Test - public void testVariableSubstitution() throws IOException { + public void variableSubstitution() throws IOException { Uri playlistUri = Uri.parse("https://example.com/substitution.m3u8"); String playlistString = "#EXTM3U\n" @@ -489,7 +524,7 @@ public class HlsMediaPlaylistParserTest { } @Test - public void testInheritedVariableSubstitution() throws IOException { + public void inheritedVariableSubstitution() throws IOException { Uri playlistUri = Uri.parse("https://example.com/test3.m3u8"); String playlistString = "#EXTM3U\n" diff --git a/library/smoothstreaming/build.gradle b/library/smoothstreaming/build.gradle index 6ced528631..404f1d6541 100644 --- a/library/smoothstreaming/build.gradle +++ b/library/smoothstreaming/build.gradle @@ -33,6 +33,8 @@ android { } } + sourceSets.test.assets.srcDir '../../testdata/src/test/assets/' + testOptions.unitTests.includeAndroidResources = true } @@ -40,6 +42,7 @@ dependencies { implementation project(modulePrefix + 'library-core') compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion compileOnly 'org.checkerframework:checker-compat-qual:' + checkerframeworkVersion + compileOnly 'org.jetbrains.kotlin:kotlin-annotations-jvm:' + kotlinAnnotationsVersion implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion testImplementation project(modulePrefix + 'testutils') testImplementation 'org.robolectric:robolectric:' + robolectricVersion 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 22dfb04f13..5ce2e6a1c5 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 @@ -38,7 +38,7 @@ 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 com.google.android.exoplayer2.util.Assertions; import java.io.IOException; import java.util.List; @@ -81,7 +81,7 @@ public class DefaultSsChunkSource implements SsChunkSource { private SsManifest manifest; private int currentManifestChunkOffset; - private IOException fatalError; + @Nullable private IOException fatalError; /** * @param manifestLoaderErrorThrower Throws errors affecting loading of manifests. @@ -107,15 +107,21 @@ public class DefaultSsChunkSource implements SsChunkSource { for (int i = 0; i < extractorWrappers.length; i++) { int manifestTrackIndex = trackSelection.getIndexInTrackGroup(i); Format format = streamElement.formats[manifestTrackIndex]; + @Nullable TrackEncryptionBox[] trackEncryptionBoxes = - format.drmInitData != null ? manifest.protectionElement.trackEncryptionBoxes : null; + format.drmInitData != null + ? Assertions.checkNotNull(manifest.protectionElement).trackEncryptionBoxes + : null; int nalUnitLengthFieldLength = streamElement.type == C.TRACK_TYPE_VIDEO ? 4 : 0; Track track = new Track(manifestTrackIndex, streamElement.type, streamElement.timescale, C.TIME_UNSET, manifest.durationUs, format, Track.TRANSFORMATION_NONE, trackEncryptionBoxes, nalUnitLengthFieldLength, null, null); - FragmentedMp4Extractor extractor = new FragmentedMp4Extractor( - FragmentedMp4Extractor.FLAG_WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME - | FragmentedMp4Extractor.FLAG_WORKAROUND_IGNORE_TFDT_BOX, null, track, null); + FragmentedMp4Extractor extractor = + new FragmentedMp4Extractor( + FragmentedMp4Extractor.FLAG_WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME + | FragmentedMp4Extractor.FLAG_WORKAROUND_IGNORE_TFDT_BOX, + /* timestampAdjuster= */ null, + track); extractorWrappers[i] = new ChunkExtractorWrapper(extractor, streamElement.type, format); } } @@ -129,7 +135,7 @@ public class DefaultSsChunkSource implements SsChunkSource { firstSyncUs < positionUs && chunkIndex < streamElement.chunkCount - 1 ? streamElement.getStartTimeUs(chunkIndex + 1) : firstSyncUs; - return Util.resolveSeekPositionUs(positionUs, seekParameters, firstSyncUs, secondSyncUs); + return seekParameters.resolveSeekPositionUs(positionUs, firstSyncUs, secondSyncUs); } @Override @@ -242,7 +248,6 @@ public class DefaultSsChunkSource implements SsChunkSource { trackSelection.getSelectedFormat(), dataSource, uri, - null, currentAbsoluteChunkIndex, chunkStartTimeUs, chunkEndTimeUs, @@ -271,15 +276,14 @@ public class DefaultSsChunkSource implements SsChunkSource { Format format, DataSource dataSource, Uri uri, - String cacheKey, int chunkIndex, long chunkStartTimeUs, long chunkEndTimeUs, long chunkSeekTimeUs, int trackSelectionReason, - Object trackSelectionData, + @Nullable Object trackSelectionData, ChunkExtractorWrapper extractorWrapper) { - DataSpec dataSpec = new DataSpec(uri, 0, C.LENGTH_UNSET, cacheKey); + DataSpec dataSpec = new DataSpec(uri); // In SmoothStreaming each chunk contains sample timestamps relative to the start of the chunk. // To convert them the absolute timestamps, we need to set sampleOffsetUs to chunkStartTimeUs. long sampleOffsetUs = chunkStartTimeUs; 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 42ac82e553..8efff23f43 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 @@ -47,7 +47,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; private final SsChunkSource.Factory chunkSourceFactory; @Nullable private final TransferListener transferListener; private final LoaderErrorThrower manifestLoaderErrorThrower; - private final DrmSessionManager drmSessionManager; + private final DrmSessionManager drmSessionManager; private final LoadErrorHandlingPolicy loadErrorHandlingPolicy; private final EventDispatcher eventDispatcher; private final Allocator allocator; @@ -65,7 +65,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; SsChunkSource.Factory chunkSourceFactory, @Nullable TransferListener transferListener, CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory, - DrmSessionManager drmSessionManager, + DrmSessionManager drmSessionManager, LoadErrorHandlingPolicy loadErrorHandlingPolicy, EventDispatcher eventDispatcher, LoaderErrorThrower manifestLoaderErrorThrower, @@ -259,7 +259,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; } private static TrackGroupArray buildTrackGroups( - SsManifest manifest, DrmSessionManager drmSessionManager) { + SsManifest manifest, DrmSessionManager drmSessionManager) { TrackGroup[] trackGroups = new TrackGroup[manifest.streamElements.length]; for (int i = 0; i < manifest.streamElements.length; i++) { Format[] manifestFormats = manifest.streamElements[i].formats; @@ -277,7 +277,8 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; return new TrackGroupArray(trackGroups); } - @SuppressWarnings("unchecked") + // We won't assign the array to a variable that erases the generic type, and then write into it. + @SuppressWarnings({"unchecked", "rawtypes"}) private static ChunkSampleStream[] newSampleStreamArray(int length) { return new ChunkSampleStream[length]; } 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 882e82753e..03506284ec 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 @@ -21,6 +21,7 @@ import android.os.SystemClock; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlayerLibraryInfo; +import com.google.android.exoplayer2.MediaItem; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.drm.DrmSession; import com.google.android.exoplayer2.drm.DrmSessionManager; @@ -29,6 +30,8 @@ import com.google.android.exoplayer2.offline.StreamKey; import com.google.android.exoplayer2.source.BaseMediaSource; import com.google.android.exoplayer2.source.CompositeSequenceableLoaderFactory; import com.google.android.exoplayer2.source.DefaultCompositeSequenceableLoaderFactory; +import com.google.android.exoplayer2.source.LoadEventInfo; +import com.google.android.exoplayer2.source.MediaLoadData; import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MediaSourceEventListener; @@ -44,6 +47,7 @@ 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.LoadErrorHandlingPolicy.LoadErrorInfo; import com.google.android.exoplayer2.upstream.Loader; import com.google.android.exoplayer2.upstream.Loader.LoadErrorAction; import com.google.android.exoplayer2.upstream.LoaderErrorThrower; @@ -53,6 +57,7 @@ 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.Collections; import java.util.List; /** A SmoothStreaming {@link MediaSource}. */ @@ -69,13 +74,12 @@ public final class SsMediaSource extends BaseMediaSource private final SsChunkSource.Factory chunkSourceFactory; @Nullable private final DataSource.Factory manifestDataSourceFactory; - @Nullable private ParsingLoadable.Parser manifestParser; - @Nullable private List streamKeys; private CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory; - private DrmSessionManager drmSessionManager; + private DrmSessionManager drmSessionManager; private LoadErrorHandlingPolicy loadErrorHandlingPolicy; private long livePresentationDelayMs; - private boolean isCreateCalled; + @Nullable private ParsingLoadable.Parser manifestParser; + private List streamKeys; @Nullable private Object tag; /** @@ -106,49 +110,20 @@ public final class SsMediaSource extends BaseMediaSource loadErrorHandlingPolicy = new DefaultLoadErrorHandlingPolicy(); livePresentationDelayMs = DEFAULT_LIVE_PRESENTATION_DELAY_MS; compositeSequenceableLoaderFactory = new DefaultCompositeSequenceableLoaderFactory(); + streamKeys = Collections.emptyList(); } /** - * Sets a tag for the media source which will be published in the {@link Timeline} of the source - * as {@link Timeline.Window#tag}. - * - * @param tag A tag for the media source. - * @return This factory, for convenience. - * @throws IllegalStateException If one of the {@code create} methods has already been called. + * @deprecated Use {@link MediaItem.Builder#setTag(Object)} and {@link + * #createMediaSource(MediaItem)} instead. */ + @Deprecated public Factory setTag(@Nullable Object tag) { - Assertions.checkState(!isCreateCalled); this.tag = tag; return this; } - /** - * Sets the {@link DrmSessionManager} to use for acquiring {@link DrmSession DrmSessions}. The - * default value is {@link DrmSessionManager#DUMMY}. - * - * @param drmSessionManager The {@link DrmSessionManager}. - * @return This factory, for convenience. - * @throws IllegalStateException If one of the {@code create} methods has already been called. - */ - public Factory setDrmSessionManager(DrmSessionManager drmSessionManager) { - Assertions.checkState(!isCreateCalled); - this.drmSessionManager = drmSessionManager; - return this; - } - - /** - * 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 Use {@link #setLoadErrorHandlingPolicy(LoadErrorHandlingPolicy)} instead. */ @Deprecated public Factory setMinLoadableRetryCount(int minLoadableRetryCount) { return setLoadErrorHandlingPolicy(new DefaultLoadErrorHandlingPolicy(minLoadableRetryCount)); @@ -162,11 +137,13 @@ public final class SsMediaSource extends BaseMediaSource * * @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; + public Factory setLoadErrorHandlingPolicy( + @Nullable LoadErrorHandlingPolicy loadErrorHandlingPolicy) { + this.loadErrorHandlingPolicy = + loadErrorHandlingPolicy != null + ? loadErrorHandlingPolicy + : new DefaultLoadErrorHandlingPolicy(); return this; } @@ -178,10 +155,8 @@ public final class SsMediaSource extends BaseMediaSource * @param livePresentationDelayMs For live playbacks, the duration in milliseconds by which the * default start position should precede the end of the live window. * @return This factory, for convenience. - * @throws IllegalStateException If one of the {@code create} methods has already been called. */ public Factory setLivePresentationDelayMs(long livePresentationDelayMs) { - Assertions.checkState(!isCreateCalled); this.livePresentationDelayMs = livePresentationDelayMs; return this; } @@ -191,11 +166,10 @@ public final class SsMediaSource extends BaseMediaSource * * @param manifestParser A parser for loaded manifest data. * @return This factory, for convenience. - * @throws IllegalStateException If one of the {@code create} methods has already been called. */ - public Factory setManifestParser(ParsingLoadable.Parser manifestParser) { - Assertions.checkState(!isCreateCalled); - this.manifestParser = Assertions.checkNotNull(manifestParser); + public Factory setManifestParser( + @Nullable ParsingLoadable.Parser manifestParser) { + this.manifestParser = manifestParser; return this; } @@ -208,16 +182,52 @@ public final class SsMediaSource extends BaseMediaSource * SequenceableLoader}s for when this media source loads data from multiple streams (video, * audio etc.). * @return This factory, for convenience. - * @throws IllegalStateException If one of the {@code create} methods has already been called. */ public Factory setCompositeSequenceableLoaderFactory( - CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory) { - Assertions.checkState(!isCreateCalled); + @Nullable CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory) { this.compositeSequenceableLoaderFactory = - Assertions.checkNotNull(compositeSequenceableLoaderFactory); + compositeSequenceableLoaderFactory != null + ? compositeSequenceableLoaderFactory + : new DefaultCompositeSequenceableLoaderFactory(); return this; } + /** + * Sets the {@link DrmSessionManager} to use for acquiring {@link DrmSession DrmSessions}. The + * default value is {@link DrmSessionManager#DUMMY}. + * + * @param drmSessionManager The {@link DrmSessionManager}. + * @return This factory, for convenience. + */ + @Override + public Factory setDrmSessionManager(@Nullable DrmSessionManager drmSessionManager) { + this.drmSessionManager = + drmSessionManager != null + ? drmSessionManager + : DrmSessionManager.getDummyDrmSessionManager(); + return this; + } + + /** + * @deprecated Use {@link MediaItem.Builder#setStreamKeys(List)} and {@link + * #createMediaSource(MediaItem)} instead. + */ + @SuppressWarnings("deprecation") + @Deprecated + @Override + public Factory setStreamKeys(@Nullable List streamKeys) { + this.streamKeys = streamKeys != null ? streamKeys : Collections.emptyList(); + return this; + } + + /** @deprecated Use {@link #createMediaSource(MediaItem)} instead. */ + @SuppressWarnings("deprecation") + @Deprecated + @Override + public SsMediaSource createMediaSource(Uri uri) { + return createMediaSource(new MediaItem.Builder().setUri(uri).build()); + } + /** * Returns a new {@link SsMediaSource} using the current parameters and the specified sideloaded * manifest. @@ -228,8 +238,7 @@ public final class SsMediaSource extends BaseMediaSource */ public SsMediaSource createMediaSource(SsManifest manifest) { Assertions.checkArgument(!manifest.isLive); - isCreateCalled = true; - if (streamKeys != null && !streamKeys.isEmpty()) { + if (!streamKeys.isEmpty()) { manifest = manifest.copy(streamKeys); } return new SsMediaSource( @@ -280,21 +289,27 @@ public final class SsMediaSource extends BaseMediaSource /** * Returns a new {@link SsMediaSource} using the current parameters. * - * @param manifestUri The manifest {@link Uri}. + * @param mediaItem The {@link MediaItem}. * @return The new {@link SsMediaSource}. + * @throws NullPointerException if {@link MediaItem#playbackProperties} is {@code null}. */ @Override - public SsMediaSource createMediaSource(Uri manifestUri) { - isCreateCalled = true; + public SsMediaSource createMediaSource(MediaItem mediaItem) { + Assertions.checkNotNull(mediaItem.playbackProperties); + @Nullable ParsingLoadable.Parser manifestParser = this.manifestParser; if (manifestParser == null) { manifestParser = new SsManifestParser(); } - if (streamKeys != null) { + List streamKeys = + !mediaItem.playbackProperties.streamKeys.isEmpty() + ? mediaItem.playbackProperties.streamKeys + : this.streamKeys; + if (!streamKeys.isEmpty()) { manifestParser = new FilteringManifestParser<>(manifestParser, streamKeys); } return new SsMediaSource( /* manifest= */ null, - Assertions.checkNotNull(manifestUri), + mediaItem.playbackProperties.uri, manifestDataSourceFactory, manifestParser, chunkSourceFactory, @@ -302,21 +317,13 @@ public final class SsMediaSource extends BaseMediaSource drmSessionManager, loadErrorHandlingPolicy, livePresentationDelayMs, - tag); - } - - @Override - public Factory setStreamKeys(List streamKeys) { - Assertions.checkState(!isCreateCalled); - this.streamKeys = streamKeys; - return this; + mediaItem.playbackProperties.tag != null ? mediaItem.playbackProperties.tag : tag); } @Override public int[] getSupportedTypes() { return new int[] {C.TYPE_SS}; } - } /** @@ -339,7 +346,7 @@ public final class SsMediaSource extends BaseMediaSource private final DataSource.Factory manifestDataSourceFactory; private final SsChunkSource.Factory chunkSourceFactory; private final CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory; - private final DrmSessionManager drmSessionManager; + private final DrmSessionManager drmSessionManager; private final LoadErrorHandlingPolicy loadErrorHandlingPolicy; private final long livePresentationDelayMs; private final EventDispatcher manifestEventDispatcher; @@ -522,7 +529,7 @@ public final class SsMediaSource extends BaseMediaSource @Nullable ParsingLoadable.Parser manifestParser, SsChunkSource.Factory chunkSourceFactory, CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory, - DrmSessionManager drmSessionManager, + DrmSessionManager drmSessionManager, LoadErrorHandlingPolicy loadErrorHandlingPolicy, long livePresentationDelayMs, @Nullable Object tag) { @@ -614,16 +621,19 @@ public final class SsMediaSource extends BaseMediaSource // Loader.Callback implementation @Override - public void onLoadCompleted(ParsingLoadable loadable, long elapsedRealtimeMs, - long loadDurationMs) { - manifestEventDispatcher.loadCompleted( - loadable.dataSpec, - loadable.getUri(), - loadable.getResponseHeaders(), - loadable.type, - elapsedRealtimeMs, - loadDurationMs, - loadable.bytesLoaded()); + public void onLoadCompleted( + ParsingLoadable loadable, long elapsedRealtimeMs, long loadDurationMs) { + LoadEventInfo loadEventInfo = + new LoadEventInfo( + loadable.loadTaskId, + loadable.dataSpec, + loadable.getUri(), + loadable.getResponseHeaders(), + elapsedRealtimeMs, + loadDurationMs, + loadable.bytesLoaded()); + loadErrorHandlingPolicy.onLoadTaskConcluded(loadable.loadTaskId); + manifestEventDispatcher.loadCompleted(loadEventInfo, loadable.type); manifest = loadable.getResult(); manifestLoadStartTimestamp = elapsedRealtimeMs - loadDurationMs; processManifest(); @@ -631,16 +641,22 @@ public final class SsMediaSource extends BaseMediaSource } @Override - public void onLoadCanceled(ParsingLoadable loadable, long elapsedRealtimeMs, - long loadDurationMs, boolean released) { - manifestEventDispatcher.loadCanceled( - loadable.dataSpec, - loadable.getUri(), - loadable.getResponseHeaders(), - loadable.type, - elapsedRealtimeMs, - loadDurationMs, - loadable.bytesLoaded()); + public void onLoadCanceled( + ParsingLoadable loadable, + long elapsedRealtimeMs, + long loadDurationMs, + boolean released) { + LoadEventInfo loadEventInfo = + new LoadEventInfo( + loadable.loadTaskId, + loadable.dataSpec, + loadable.getUri(), + loadable.getResponseHeaders(), + elapsedRealtimeMs, + loadDurationMs, + loadable.bytesLoaded()); + loadErrorHandlingPolicy.onLoadTaskConcluded(loadable.loadTaskId); + manifestEventDispatcher.loadCanceled(loadEventInfo, loadable.type); } @Override @@ -650,23 +666,28 @@ public final class SsMediaSource extends BaseMediaSource long loadDurationMs, IOException error, int errorCount) { + LoadEventInfo loadEventInfo = + new LoadEventInfo( + loadable.loadTaskId, + loadable.dataSpec, + loadable.getUri(), + loadable.getResponseHeaders(), + elapsedRealtimeMs, + loadDurationMs, + loadable.bytesLoaded()); + MediaLoadData mediaLoadData = new MediaLoadData(loadable.type); long retryDelayMs = loadErrorHandlingPolicy.getRetryDelayMsFor( - C.DATA_TYPE_MANIFEST, loadDurationMs, error, errorCount); + new LoadErrorInfo(loadEventInfo, mediaLoadData, error, errorCount)); LoadErrorAction loadErrorAction = retryDelayMs == C.TIME_UNSET ? Loader.DONT_RETRY_FATAL : Loader.createRetryAction(/* resetErrorCount= */ false, retryDelayMs); - manifestEventDispatcher.loadError( - loadable.dataSpec, - loadable.getUri(), - loadable.getResponseHeaders(), - loadable.type, - elapsedRealtimeMs, - loadDurationMs, - loadable.bytesLoaded(), - error, - !loadErrorAction.isRetry()); + boolean wasCanceled = !loadErrorAction.isRetry(); + manifestEventDispatcher.loadError(loadEventInfo, loadable.type, error, wasCanceled); + if (wasCanceled) { + loadErrorHandlingPolicy.onLoadTaskConcluded(loadable.loadTaskId); + } return loadErrorAction; } @@ -760,7 +781,9 @@ public final class SsMediaSource extends BaseMediaSource long elapsedRealtimeMs = manifestLoader.startLoading( loadable, this, loadErrorHandlingPolicy.getMinimumLoadableRetryCount(loadable.type)); - manifestEventDispatcher.loadStarted(loadable.dataSpec, loadable.type, elapsedRealtimeMs); + manifestEventDispatcher.loadStarted( + new LoadEventInfo(loadable.loadTaskId, loadable.dataSpec, elapsedRealtimeMs), + loadable.type); } } 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 d395e95fd9..d848542bde 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 @@ -23,6 +23,7 @@ import androidx.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.audio.AacUtil; import com.google.android.exoplayer2.drm.DrmInitData; import com.google.android.exoplayer2.drm.DrmInitData.SchemeData; import com.google.android.exoplayer2.extractor.mp4.PsshAtomUtil; @@ -386,7 +387,7 @@ public class SsManifestParser implements ParsingLoadable.Parser { if (type == C.TRACK_TYPE_VIDEO || type == C.TRACK_TYPE_AUDIO) { Format[] formats = streamElement.formats; for (int i = 0; i < formats.length; i++) { - formats[i] = formats[i].copyWithDrmInitData(drmInitData); + formats[i] = formats[i].buildUpon().setDrmInitData(drmInitData).build(); } } } @@ -663,96 +664,65 @@ public class SsManifestParser implements ParsingLoadable.Parser { @Override 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)); + Format.Builder formatBuilder = new Format.Builder(); + @Nullable String sampleMimeType = fourCCToMimeType(parseRequiredString(parser, KEY_FOUR_CC)); + int type = (Integer) getNormalizedAttribute(KEY_TYPE); if (type == C.TRACK_TYPE_VIDEO) { - int width = parseRequiredInt(parser, KEY_MAX_WIDTH); - int height = parseRequiredInt(parser, KEY_MAX_HEIGHT); List codecSpecificData = buildCodecSpecificData( parser.getAttributeValue(null, KEY_CODEC_PRIVATE_DATA)); - format = - Format.createVideoContainerFormat( - id, - name, - MimeTypes.VIDEO_MP4, - sampleMimeType, - /* codecs= */ null, - /* metadata= */ null, - bitrate, - width, - height, - /* frameRate= */ Format.NO_VALUE, - codecSpecificData, - /* selectionFlags= */ 0, - /* roleFlags= */ 0); + formatBuilder + .setContainerMimeType(MimeTypes.VIDEO_MP4) + .setWidth(parseRequiredInt(parser, KEY_MAX_WIDTH)) + .setHeight(parseRequiredInt(parser, KEY_MAX_HEIGHT)) + .setInitializationData(codecSpecificData); } else if (type == C.TRACK_TYPE_AUDIO) { - sampleMimeType = sampleMimeType == null ? MimeTypes.AUDIO_AAC : sampleMimeType; - int channels = parseRequiredInt(parser, KEY_CHANNELS); - int samplingRate = parseRequiredInt(parser, KEY_SAMPLING_RATE); + if (sampleMimeType == null) { + // If we don't know the MIME type, assume AAC. + sampleMimeType = MimeTypes.AUDIO_AAC; + } + int channelCount = parseRequiredInt(parser, KEY_CHANNELS); + int sampleRate = parseRequiredInt(parser, KEY_SAMPLING_RATE); List codecSpecificData = buildCodecSpecificData( parser.getAttributeValue(null, KEY_CODEC_PRIVATE_DATA)); if (codecSpecificData.isEmpty() && MimeTypes.AUDIO_AAC.equals(sampleMimeType)) { - codecSpecificData = Collections.singletonList( - CodecSpecificDataUtil.buildAacLcAudioSpecificConfig(samplingRate, channels)); + codecSpecificData = + Collections.singletonList( + AacUtil.buildAacLcAudioSpecificConfig(sampleRate, channelCount)); } - String language = (String) getNormalizedAttribute(KEY_LANGUAGE); - format = - Format.createAudioContainerFormat( - id, - name, - MimeTypes.AUDIO_MP4, - sampleMimeType, - /* codecs= */ null, - /* metadata= */ null, - bitrate, - channels, - samplingRate, - codecSpecificData, - /* selectionFlags= */ 0, - /* roleFlags= */ 0, - language); + formatBuilder + .setContainerMimeType(MimeTypes.AUDIO_MP4) + .setChannelCount(channelCount) + .setSampleRate(sampleRate) + .setInitializationData(codecSpecificData); } else if (type == C.TRACK_TYPE_TEXT) { - String subType = (String) getNormalizedAttribute(KEY_SUB_TYPE); @C.RoleFlags int roleFlags = 0; - switch (subType) { - case "CAPT": - roleFlags = C.ROLE_FLAG_CAPTION; - break; - case "DESC": - roleFlags = C.ROLE_FLAG_DESCRIBES_MUSIC_AND_SOUND; - break; - default: - break; + @Nullable String subType = (String) getNormalizedAttribute(KEY_SUB_TYPE); + if (subType != null) { + switch (subType) { + case "CAPT": + roleFlags = C.ROLE_FLAG_CAPTION; + break; + case "DESC": + roleFlags = C.ROLE_FLAG_DESCRIBES_MUSIC_AND_SOUND; + break; + default: + break; + } } - String language = (String) getNormalizedAttribute(KEY_LANGUAGE); - format = - Format.createTextContainerFormat( - id, - name, - MimeTypes.APPLICATION_MP4, - sampleMimeType, - /* codecs= */ null, - bitrate, - /* selectionFlags= */ 0, - roleFlags, - language); + formatBuilder.setContainerMimeType(MimeTypes.APPLICATION_MP4).setRoleFlags(roleFlags); } else { - format = - Format.createContainerFormat( - id, - name, - MimeTypes.APPLICATION_MP4, - sampleMimeType, - /* codecs= */ null, - bitrate, - /* selectionFlags= */ 0, - /* roleFlags= */ 0, - /* language= */ null); + formatBuilder.setContainerMimeType(MimeTypes.APPLICATION_MP4); } + + format = + formatBuilder + .setId(parser.getAttributeValue(null, KEY_INDEX)) + .setLabel((String) getNormalizedAttribute(KEY_NAME)) + .setSampleMimeType(sampleMimeType) + .setAverageBitrate(parseRequiredInt(parser, KEY_BITRATE)) + .setLanguage((String) getNormalizedAttribute(KEY_LANGUAGE)) + .build(); } @Override @@ -764,7 +734,7 @@ public class SsManifestParser implements ParsingLoadable.Parser { ArrayList csd = new ArrayList<>(); if (!TextUtils.isEmpty(codecSpecificDataString)) { byte[] codecPrivateData = Util.getBytesFromHexString(codecSpecificDataString); - byte[][] split = CodecSpecificDataUtil.splitNalUnits(codecPrivateData); + @Nullable byte[][] split = CodecSpecificDataUtil.splitNalUnits(codecPrivateData); if (split == null) { csd.add(codecPrivateData); } else { @@ -774,6 +744,7 @@ public class SsManifestParser implements ParsingLoadable.Parser { return csd; } + @Nullable private static String fourCCToMimeType(String fourCC) { if (fourCC.equalsIgnoreCase("H264") || fourCC.equalsIgnoreCase("X264") || fourCC.equalsIgnoreCase("AVC1") || fourCC.equalsIgnoreCase("DAVC")) { 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 1331fe4617..3a2cf10439 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 @@ -16,8 +16,6 @@ package com.google.android.exoplayer2.source.smoothstreaming.offline; 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; @@ -26,10 +24,10 @@ import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifestP import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsUtil; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSpec; -import com.google.android.exoplayer2.upstream.ParsingLoadable; -import java.io.IOException; +import com.google.android.exoplayer2.upstream.cache.CacheDataSource; import java.util.ArrayList; import java.util.List; +import java.util.concurrent.Executor; /** * A downloader for SmoothStreaming streams. @@ -38,20 +36,21 @@ import java.util.List; * *

      {@code
        * SimpleCache cache = new SimpleCache(downloadFolder, new NoOpCacheEvictor(), databaseProvider);
      - * DefaultHttpDataSourceFactory factory = new DefaultHttpDataSourceFactory("ExoPlayer", null);
      - * DownloaderConstructorHelper constructorHelper =
      - *     new DownloaderConstructorHelper(cache, factory);
      + * CacheDataSource.Factory cacheDataSourceFactory =
      + *     new CacheDataSource.Factory()
      + *         .setCache(cache)
      + *         .setUpstreamDataSourceFactory(new DefaultHttpDataSourceFactory(userAgent));
        * // Create a downloader for the first track of the first stream element.
        * SsDownloader ssDownloader =
        *     new SsDownloader(
        *         manifestUrl,
        *         Collections.singletonList(new StreamKey(0, 0)),
      - *         constructorHelper);
      + *         cacheDataSourceFactory);
        * // Perform the download.
        * ssDownloader.download(progressListener);
      - * // Access downloaded data using CacheDataSource
      - * CacheDataSource cacheDataSource =
      - *     new CacheDataSource(cache, factory.createDataSource(), CacheDataSource.FLAG_BLOCK_ON_CACHE);
      + * // Use the downloaded data for playback.
      + * SsMediaSource mediaSource =
      + *     new SsMediaSource.Factory(cacheDataSourceFactory).createMediaSource(mediaItem);
        * }
      */ public final class SsDownloader extends SegmentDownloader { @@ -60,16 +59,35 @@ public final class SsDownloader extends SegmentDownloader { * @param manifestUri The {@link Uri} of the manifest to be downloaded. * @param streamKeys Keys defining which streams in the manifest should be selected for download. * If empty, all streams are downloaded. - * @param constructorHelper A {@link DownloaderConstructorHelper} instance. + * @param cacheDataSourceFactory A {@link CacheDataSource.Factory} for the cache into which the + * download will be written. */ public SsDownloader( - Uri manifestUri, List streamKeys, DownloaderConstructorHelper constructorHelper) { - super(SsUtil.fixManifestUri(manifestUri), streamKeys, constructorHelper); + Uri manifestUri, List streamKeys, CacheDataSource.Factory cacheDataSourceFactory) { + this(manifestUri, streamKeys, cacheDataSourceFactory, Runnable::run); } - @Override - protected SsManifest getManifest(DataSource dataSource, DataSpec dataSpec) throws IOException { - return ParsingLoadable.load(dataSource, new SsManifestParser(), dataSpec, C.DATA_TYPE_MANIFEST); + /** + * @param manifestUri The {@link Uri} of the manifest to be downloaded. + * @param streamKeys Keys defining which streams in the manifest should be selected for download. + * If empty, all streams are downloaded. + * @param cacheDataSourceFactory A {@link CacheDataSource.Factory} for the cache into which the + * download will be written. + * @param executor An {@link Executor} used to make requests for the media being downloaded. + * Providing an {@link Executor} that uses multiple threads will speed up the download by + * allowing parts of it to be executed in parallel. + */ + public SsDownloader( + Uri manifestUri, + List streamKeys, + CacheDataSource.Factory cacheDataSourceFactory, + Executor executor) { + super( + SsUtil.fixManifestUri(manifestUri), + new SsManifestParser(), + streamKeys, + cacheDataSourceFactory, + executor); } @Override diff --git a/library/smoothstreaming/src/test/java/com/google/android/exoplayer2/source/smoothstreaming/DefaultMediaSourceFactoryTest.java b/library/smoothstreaming/src/test/java/com/google/android/exoplayer2/source/smoothstreaming/DefaultMediaSourceFactoryTest.java new file mode 100644 index 0000000000..37b686183f --- /dev/null +++ b/library/smoothstreaming/src/test/java/com/google/android/exoplayer2/source/smoothstreaming/DefaultMediaSourceFactoryTest.java @@ -0,0 +1,111 @@ +/* + * Copyright 2020 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; + +import static com.google.common.truth.Truth.assertThat; + +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.MediaItem; +import com.google.android.exoplayer2.source.DefaultMediaSourceFactory; +import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.util.MimeTypes; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** + * Unit test for creating SmoothStreaming media sources with the {@link DefaultMediaSourceFactory}. + */ +@RunWith(AndroidJUnit4.class) +public class DefaultMediaSourceFactoryTest { + + private static final String URI_MEDIA = "http://exoplayer.dev/video"; + + @Test + public void createMediaSource_withMimeType_smoothstreamingSource() { + DefaultMediaSourceFactory defaultMediaSourceFactory = + DefaultMediaSourceFactory.newInstance(ApplicationProvider.getApplicationContext()); + MediaItem mediaItem = + new MediaItem.Builder().setUri(URI_MEDIA).setMimeType(MimeTypes.APPLICATION_SS).build(); + MediaSource mediaSource = defaultMediaSourceFactory.createMediaSource(mediaItem); + assertThat(mediaSource).isInstanceOf(SsMediaSource.class); + } + + @Test + public void createMediaSource_withTag_tagInSource() { + Object tag = new Object(); + DefaultMediaSourceFactory defaultMediaSourceFactory = + DefaultMediaSourceFactory.newInstance(ApplicationProvider.getApplicationContext()); + MediaItem mediaItem = + new MediaItem.Builder() + .setUri(URI_MEDIA) + .setMimeType(MimeTypes.APPLICATION_SS) + .setTag(tag) + .build(); + + MediaSource mediaSource = defaultMediaSourceFactory.createMediaSource(mediaItem); + + assertThat(mediaSource.getTag()).isEqualTo(tag); + } + + @Test + public void createMediaSource_withIsmPath_smoothstreamingSource() { + DefaultMediaSourceFactory defaultMediaSourceFactory = + DefaultMediaSourceFactory.newInstance(ApplicationProvider.getApplicationContext()); + MediaItem mediaItem = new MediaItem.Builder().setUri(URI_MEDIA + "/file.ism").build(); + + MediaSource mediaSource = defaultMediaSourceFactory.createMediaSource(mediaItem); + + assertThat(mediaSource).isInstanceOf(SsMediaSource.class); + } + + @Test + public void createMediaSource_withManifestPath_smoothstreamingSource() { + DefaultMediaSourceFactory defaultMediaSourceFactory = + DefaultMediaSourceFactory.newInstance(ApplicationProvider.getApplicationContext()); + MediaItem mediaItem = new MediaItem.Builder().setUri(URI_MEDIA + ".ism/Manifest").build(); + + MediaSource mediaSource = defaultMediaSourceFactory.createMediaSource(mediaItem); + + assertThat(mediaSource).isInstanceOf(SsMediaSource.class); + } + + @Test + public void createMediaSource_withNull_usesNonNullDefaults() { + DefaultMediaSourceFactory defaultMediaSourceFactory = + DefaultMediaSourceFactory.newInstance(ApplicationProvider.getApplicationContext()); + MediaItem mediaItem = new MediaItem.Builder().setUri(URI_MEDIA + "/file.ism").build(); + + MediaSource mediaSource = + defaultMediaSourceFactory + .setDrmSessionManager(null) + .setDrmHttpDataSourceFactory(null) + .setLoadErrorHandlingPolicy(null) + .createMediaSource(mediaItem); + + assertThat(mediaSource).isNotNull(); + } + + @Test + public void getSupportedTypes_smoothstreamingModule_containsTypeSS() { + int[] supportedTypes = + DefaultMediaSourceFactory.newInstance(ApplicationProvider.getApplicationContext()) + .getSupportedTypes(); + + assertThat(supportedTypes).asList().containsExactly(C.TYPE_OTHER, C.TYPE_SS); + } +} diff --git a/library/smoothstreaming/src/test/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaPeriodTest.java b/library/smoothstreaming/src/test/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaPeriodTest.java index b9c63f843d..a20e5790a7 100644 --- a/library/smoothstreaming/src/test/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaPeriodTest.java +++ b/library/smoothstreaming/src/test/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaPeriodTest.java @@ -82,41 +82,26 @@ public class SsMediaPeriodTest { } private static Format createVideoFormat(int bitrate) { - return Format.createContainerFormat( - /* id= */ null, - /* label= */ null, - MimeTypes.VIDEO_MP4, - MimeTypes.VIDEO_H264, - /* codecs= */ null, - bitrate, - /* selectionFlags= */ 0, - /* roleFlags= */ 0, - /* language= */ null); + return new Format.Builder() + .setContainerMimeType(MimeTypes.VIDEO_MP4) + .setSampleMimeType(MimeTypes.VIDEO_H264) + .setAverageBitrate(bitrate) + .build(); } private static Format createAudioFormat(int bitrate) { - return Format.createContainerFormat( - /* id= */ null, - /* label= */ null, - MimeTypes.AUDIO_MP4, - MimeTypes.AUDIO_AAC, - /* codecs= */ null, - bitrate, - /* selectionFlags= */ 0, - /* roleFlags= */ 0, - /* language= */ null); + return new Format.Builder() + .setContainerMimeType(MimeTypes.AUDIO_MP4) + .setSampleMimeType(MimeTypes.AUDIO_AAC) + .setAverageBitrate(bitrate) + .build(); } private static Format createTextFormat(String language) { - return Format.createContainerFormat( - /* id= */ null, - /* label= */ null, - MimeTypes.APPLICATION_MP4, - MimeTypes.TEXT_VTT, - /* codecs= */ null, - /* bitrate= */ Format.NO_VALUE, - /* selectionFlags= */ 0, - /* roleFlags= */ 0, - language); + return new Format.Builder() + .setContainerMimeType(MimeTypes.APPLICATION_MP4) + .setSampleMimeType(MimeTypes.TEXT_VTT) + .setLanguage(language) + .build(); } } diff --git a/library/smoothstreaming/src/test/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsManifestParserTest.java b/library/smoothstreaming/src/test/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsManifestParserTest.java index 94be71e84e..60d9c40dc3 100644 --- a/library/smoothstreaming/src/test/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsManifestParserTest.java +++ b/library/smoothstreaming/src/test/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsManifestParserTest.java @@ -27,12 +27,12 @@ import org.junit.runner.RunWith; @RunWith(AndroidJUnit4.class) public final class SsManifestParserTest { - private static final String SAMPLE_ISMC_1 = "sample_ismc_1"; - private static final String SAMPLE_ISMC_2 = "sample_ismc_2"; + private static final String SAMPLE_ISMC_1 = "smooth-streaming/sample_ismc_1"; + private static final String SAMPLE_ISMC_2 = "smooth-streaming/sample_ismc_2"; /** Simple test to ensure the sample manifests parse without any exceptions being thrown. */ @Test - public void testParseSmoothStreamingManifest() throws IOException { + public void parseSmoothStreamingManifest() throws IOException { SsManifestParser parser = new SsManifestParser(); parser.parse( Uri.parse("https://example.com/test.ismc"), 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 427cd9b5da..5715a787bd 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 @@ -37,7 +37,7 @@ import org.junit.runner.RunWith; public class SsManifestTest { @Test - public void testCopy() throws Exception { + public void copy() throws Exception { Format[][] formats = newFormats(2, 3); SsManifest sourceManifest = createSsManifest( @@ -58,7 +58,7 @@ public class SsManifestTest { } @Test - public void testCopyRemoveStreamElement() throws Exception { + public void copyRemoveStreamElement() throws Exception { Format[][] formats = newFormats(2, 3); SsManifest sourceManifest = createSsManifest( @@ -114,15 +114,10 @@ public class SsManifestTest { } private static Format newFormat(String id) { - return Format.createContainerFormat( - id, - /* label= */ null, - MimeTypes.VIDEO_MP4, - MimeTypes.VIDEO_H264, - /* codecs= */ null, - /* bitrate= */ Format.NO_VALUE, - /* selectionFlags= */ 0, - /* roleFlags= */ 0, - /* language= */ null); + return new Format.Builder() + .setId(id) + .setContainerMimeType(MimeTypes.VIDEO_MP4) + .setSampleMimeType(MimeTypes.VIDEO_H264) + .build(); } } diff --git a/library/smoothstreaming/src/test/java/com/google/android/exoplayer2/source/smoothstreaming/offline/DownloadHelperTest.java b/library/smoothstreaming/src/test/java/com/google/android/exoplayer2/source/smoothstreaming/offline/DownloadHelperTest.java index a103f89cec..b6d29d8b72 100644 --- a/library/smoothstreaming/src/test/java/com/google/android/exoplayer2/source/smoothstreaming/offline/DownloadHelperTest.java +++ b/library/smoothstreaming/src/test/java/com/google/android/exoplayer2/source/smoothstreaming/offline/DownloadHelperTest.java @@ -34,11 +34,11 @@ public final class DownloadHelperTest { ApplicationProvider.getApplicationContext(), Uri.parse("http://uri"), new FakeDataSource.Factory(), - (handler, videoListener, audioListener, text, metadata, drm) -> new Renderer[0]); + (handler, videoListener, audioListener, text, metadata) -> new Renderer[0]); DownloadHelper.forSmoothStreaming( Uri.parse("http://uri"), new FakeDataSource.Factory(), - (handler, videoListener, audioListener, text, metadata, drm) -> new Renderer[0], + (handler, videoListener, audioListener, text, metadata) -> new Renderer[0], /* drmSessionManager= */ null, DownloadHelper.DEFAULT_TRACK_SELECTOR_PARAMETERS_WITHOUT_VIEWPORT); } diff --git a/library/smoothstreaming/src/test/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloaderTest.java b/library/smoothstreaming/src/test/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloaderTest.java index 5560a724c8..1bbe0b191d 100644 --- a/library/smoothstreaming/src/test/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloaderTest.java +++ b/library/smoothstreaming/src/test/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloaderTest.java @@ -22,11 +22,11 @@ import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.offline.DefaultDownloaderFactory; import com.google.android.exoplayer2.offline.DownloadRequest; import com.google.android.exoplayer2.offline.Downloader; -import com.google.android.exoplayer2.offline.DownloaderConstructorHelper; import com.google.android.exoplayer2.offline.DownloaderFactory; import com.google.android.exoplayer2.offline.StreamKey; import com.google.android.exoplayer2.upstream.DummyDataSource; import com.google.android.exoplayer2.upstream.cache.Cache; +import com.google.android.exoplayer2.upstream.cache.CacheDataSource; import java.util.Collections; import org.junit.Test; import org.junit.runner.RunWith; @@ -38,9 +38,11 @@ public final class SsDownloaderTest { @Test public void createWithDefaultDownloaderFactory() throws Exception { - DownloaderConstructorHelper constructorHelper = - new DownloaderConstructorHelper(Mockito.mock(Cache.class), DummyDataSource.FACTORY); - DownloaderFactory factory = new DefaultDownloaderFactory(constructorHelper); + CacheDataSource.Factory cacheDataSourceFactory = + new CacheDataSource.Factory() + .setCache(Mockito.mock(Cache.class)) + .setUpstreamDataSourceFactory(DummyDataSource.FACTORY); + DownloaderFactory factory = new DefaultDownloaderFactory(cacheDataSourceFactory); Downloader downloader = factory.createDownloader( diff --git a/library/ui/build.gradle b/library/ui/build.gradle index b6bf139963..f404ee38a5 100644 --- a/library/ui/build.gradle +++ b/library/ui/build.gradle @@ -41,6 +41,7 @@ dependencies { api 'androidx.media:media:' + androidxMediaVersion implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion + compileOnly 'org.jetbrains.kotlin:kotlin-annotations-jvm:' + kotlinAnnotationsVersion testImplementation project(modulePrefix + 'testutils') testImplementation 'org.robolectric:robolectric:' + robolectricVersion } 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 0841296193..39ea45aec8 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 @@ -79,7 +79,13 @@ public class DebugTextViewHelper implements Player.EventListener, Runnable { // Player.EventListener implementation. @Override - public final void onPlayerStateChanged(boolean playWhenReady, @Player.State int playbackState) { + public final void onPlaybackStateChanged(@Player.State int playbackState) { + updateAndPost(); + } + + @Override + public final void onPlayWhenReadyChanged( + boolean playWhenReady, @Player.PlayWhenReadyChangeReason int playbackState) { updateAndPost(); } @@ -151,6 +157,10 @@ public class DebugTextViewHelper implements Player.EventListener, Runnable { + format.height + getPixelAspectRatioString(format.pixelWidthHeightRatio) + getDecoderCountersBufferCountString(decoderCounters) + + " vfpo: " + + getVideoFrameProcessingOffsetAverageString( + decoderCounters.totalVideoFrameProcessingOffsetUs, + decoderCounters.videoFrameProcessingOffsetCount) + ")"; } @@ -191,4 +201,13 @@ public class DebugTextViewHelper implements Player.EventListener, Runnable { : (" par:" + String.format(Locale.US, "%.02f", pixelAspectRatio)); } + private static String getVideoFrameProcessingOffsetAverageString( + long totalOffsetUs, int frameCount) { + if (frameCount == 0) { + return "N/A"; + } else { + long averageUs = (long) ((double) totalOffsetUs / frameCount); + return String.valueOf(averageUs); + } + } } 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 8b737bc006..44c0035278 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 @@ -15,7 +15,7 @@ */ package com.google.android.exoplayer2.ui; -import android.annotation.TargetApi; +import android.animation.ValueAnimator; import android.content.Context; import android.content.res.Resources; import android.content.res.TypedArray; @@ -36,12 +36,15 @@ import android.view.accessibility.AccessibilityNodeInfo; import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction; import androidx.annotation.ColorInt; import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Util; +import java.util.Collections; import java.util.Formatter; import java.util.Locale; import java.util.concurrent.CopyOnWriteArraySet; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /** * A time bar that shows a current position, buffered position, duration and ad markers. @@ -52,8 +55,6 @@ import java.util.concurrent.CopyOnWriteArraySet; * * The following attributes can be set on a DefaultTimeBar when used in a layout XML file: * - *

      - * *

        *
      • {@code bar_height} - Dimension for the height of the time bar. *
          @@ -163,6 +164,9 @@ public class DefaultTimeBar extends View implements TimeBar { private static final int DEFAULT_INCREMENT_COUNT = 20; + private static final float SHOWN_SCRUBBER_SCALE = 1.0f; + private static final float HIDDEN_SCRUBBER_SCALE = 0.0f; + /** * The name of the Android SDK view that most closely resembles this custom view. Used as the * class name for accessibility. @@ -192,14 +196,16 @@ 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 final float density; private int keyCountIncrement; private long keyTimeIncrement; private int lastCoarseScrubXPosition; + private @MonotonicNonNull Rect lastExclusionRectangle; + private ValueAnimator scrubberScalingAnimator; + private float scrubberScale; private boolean scrubbing; private long scrubPosition; private long duration; @@ -222,11 +228,7 @@ public class DefaultTimeBar extends View implements TimeBar { } // Suppress warnings due to usage of View methods in the constructor. - // the constructor does not initialize fields: adGroupTimesMs, playedAdGroups - @SuppressWarnings({ - "nullness:method.invocation.invalid", - "nullness:initialization.fields.uninitialized" - }) + @SuppressWarnings("nullness:method.invocation.invalid") public DefaultTimeBar( Context context, @Nullable AttributeSet attrs, @@ -245,7 +247,6 @@ 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. @@ -327,6 +328,13 @@ public class DefaultTimeBar extends View implements TimeBar { (Math.max(scrubberDisabledSize, Math.max(scrubberEnabledSize, scrubberDraggedSize)) + 1) / 2; } + scrubberScale = 1.0f; + scrubberScalingAnimator = new ValueAnimator(); + scrubberScalingAnimator.addUpdateListener( + animation -> { + scrubberScale = (float) animation.getAnimatedValue(); + invalidate(seekBounds); + }); duration = C.TIME_UNSET; keyTimeIncrement = C.TIME_UNSET; keyCountIncrement = DEFAULT_INCREMENT_COUNT; @@ -336,6 +344,44 @@ public class DefaultTimeBar extends View implements TimeBar { } } + /** Shows the scrubber handle. */ + public void showScrubber() { + showScrubber(/* showAnimationDurationMs= */ 0); + } + + /** + * Shows the scrubber handle with animation. + * + * @param showAnimationDurationMs The duration for scrubber showing animation. + */ + public void showScrubber(long showAnimationDurationMs) { + if (scrubberScalingAnimator.isStarted()) { + scrubberScalingAnimator.cancel(); + } + scrubberScalingAnimator.setFloatValues(scrubberScale, SHOWN_SCRUBBER_SCALE); + scrubberScalingAnimator.setDuration(showAnimationDurationMs); + scrubberScalingAnimator.start(); + } + + /** Hides the scrubber handle. */ + public void hideScrubber() { + hideScrubber(/* hideAnimationDurationMs= */ 0); + } + + /** + * Hides the scrubber handle with animation. + * + * @param hideAnimationDurationMs The duration for scrubber hiding animation. + */ + public void hideScrubber(long hideAnimationDurationMs) { + if (scrubberScalingAnimator.isStarted()) { + scrubberScalingAnimator.cancel(); + } + scrubberScalingAnimator.setFloatValues(scrubberScale, HIDDEN_SCRUBBER_SCALE); + scrubberScalingAnimator.setDuration(hideAnimationDurationMs); + scrubberScalingAnimator.start(); + } + /** * Sets the color for the portion of the time bar representing media before the playback position. * @@ -604,6 +650,9 @@ public class DefaultTimeBar extends View implements TimeBar { seekBounds.set(seekLeft, barY, seekRight, barY + touchTargetHeight); progressBar.set(seekBounds.left + scrubberPadding, progressY, seekBounds.right - scrubberPadding, progressY + barHeight); + if (Util.SDK_INT >= 29) { + setSystemGestureExclusionRectsV29(width, height); + } update(); } @@ -623,7 +672,6 @@ public class DefaultTimeBar extends View implements TimeBar { event.setClassName(ACCESSIBILITY_CLASS_NAME); } - @TargetApi(21) @Override public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { super.onInitializeAccessibilityNodeInfo(info); @@ -748,10 +796,7 @@ public class DefaultTimeBar extends View implements TimeBar { } private Point resolveRelativeTouchPosition(MotionEvent motionEvent) { - getLocationOnScreen(locationOnScreen); - touchPosition.set( - ((int) motionEvent.getRawX()) - locationOnScreen[0], - ((int) motionEvent.getRawY()) - locationOnScreen[1]); + touchPosition.set((int) motionEvent.getX(), (int) motionEvent.getY()); return touchPosition; } @@ -813,11 +858,11 @@ public class DefaultTimeBar extends View implements TimeBar { if (scrubberDrawable == null) { int scrubberSize = (scrubbing || isFocused()) ? scrubberDraggedSize : (isEnabled() ? scrubberEnabledSize : scrubberDisabledSize); - int playheadRadius = scrubberSize / 2; + int playheadRadius = (int) ((scrubberSize * scrubberScale) / 2); canvas.drawCircle(playheadX, playheadY, playheadRadius, scrubberPaint); } else { - int scrubberDrawableWidth = scrubberDrawable.getIntrinsicWidth(); - int scrubberDrawableHeight = scrubberDrawable.getIntrinsicHeight(); + int scrubberDrawableWidth = (int) (scrubberDrawable.getIntrinsicWidth() * scrubberScale); + int scrubberDrawableHeight = (int) (scrubberDrawable.getIntrinsicHeight() * scrubberScale); scrubberDrawable.setBounds( playheadX - scrubberDrawableWidth / 2, playheadY - scrubberDrawableHeight / 2, @@ -834,6 +879,18 @@ public class DefaultTimeBar extends View implements TimeBar { } } + @RequiresApi(29) + private void setSystemGestureExclusionRectsV29(int width, int height) { + if (lastExclusionRectangle != null + && lastExclusionRectangle.width() == width + && lastExclusionRectangle.height() == height) { + // Allocating inside onLayout is considered a DrawAllocation lint error, so avoid if possible. + return; + } + lastExclusionRectangle = new Rect(/* left= */ 0, /* top= */ 0, width, height); + setSystemGestureExclusionRects(Collections.singletonList(lastExclusionRectangle)); + } + private String getProgressText() { return Util.getStringForTime(formatBuilder, formatter, position); } diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/HtmlUtils.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/HtmlUtils.java new file mode 100644 index 0000000000..0edee287a9 --- /dev/null +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/HtmlUtils.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2020 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; + +import android.graphics.Color; +import androidx.annotation.ColorInt; +import com.google.android.exoplayer2.util.Util; + +/** + * Utility methods for generating HTML and CSS for use with {@link SubtitleWebView} and {@link + * SpannedToHtmlConverter}. + */ +/* package */ final class HtmlUtils { + + private HtmlUtils() {} + + public static String toCssRgba(@ColorInt int color) { + return Util.formatInvariant( + "rgba(%d,%d,%d,%.3f)", + Color.red(color), Color.green(color), Color.blue(color), Color.alpha(color) / 255.0); + } +} 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 a6636d71be..778f033f0c 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 @@ -33,6 +33,8 @@ import android.widget.ImageView; import android.widget.TextView; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.ControlDispatcher; +import com.google.android.exoplayer2.DefaultControlDispatcher; import com.google.android.exoplayer2.ExoPlayerLibraryInfo; import com.google.android.exoplayer2.PlaybackPreparer; import com.google.android.exoplayer2.Player; @@ -49,8 +51,8 @@ import java.util.concurrent.CopyOnWriteArrayList; * A view for controlling {@link Player} instances. * *

          A PlayerControlView can be customized by setting attributes (or calling corresponding - * methods), overriding the view's layout file or by specifying a custom view layout file, as - * outlined below. + * methods), overriding drawables, overriding the view's layout file, or by specifying a custom view + * layout file. * *

          Attributes

          * @@ -67,13 +69,13 @@ import java.util.concurrent.CopyOnWriteArrayList; *
        • {@code rewind_increment} - The duration of the rewind applied when the user taps the * rewind button, in milliseconds. Use zero to disable the rewind button. *
            - *
          • Corresponding method: {@link #setRewindIncrementMs(int)} - *
          • Default: {@link #DEFAULT_REWIND_MS} + *
          • Corresponding method: {@link #setControlDispatcher(ControlDispatcher)} + *
          • Default: {@link DefaultControlDispatcher#DEFAULT_REWIND_MS} *
          *
        • {@code fastforward_increment} - Like {@code rewind_increment}, but for fast forward. *
            - *
          • Corresponding method: {@link #setFastForwardIncrementMs(int)} - *
          • Default: {@link #DEFAULT_FAST_FORWARD_MS} + *
          • Corresponding method: {@link #setControlDispatcher(ControlDispatcher)} + *
          • Default: {@link DefaultControlDispatcher#DEFAULT_FAST_FORWARD_MS} *
          *
        • {@code repeat_toggle_modes} - A flagged enumeration value specifying which repeat * mode toggle options are enabled. Valid values are: {@code none}, {@code one}, {@code all}, @@ -104,6 +106,30 @@ import java.util.concurrent.CopyOnWriteArrayList; * layout is overridden to specify a custom {@code exo_progress} (see below). *
        * + *

        Overriding drawables

        + * + * The drawables used by PlayerControlView (with its default layout file) can be overridden by + * drawables with the same names defined in your application. The drawables that can be overridden + * are: + * + *
          + *
        • {@code exo_controls_play} - The play icon. + *
        • {@code exo_controls_pause} - The pause icon. + *
        • {@code exo_controls_rewind} - The rewind icon. + *
        • {@code exo_controls_fastforward} - The fast forward icon. + *
        • {@code exo_controls_previous} - The previous icon. + *
        • {@code exo_controls_next} - The next icon. + *
        • {@code exo_controls_repeat_off} - The repeat icon for {@link + * Player#REPEAT_MODE_OFF}. + *
        • {@code exo_controls_repeat_one} - The repeat icon for {@link + * Player#REPEAT_MODE_ONE}. + *
        • {@code exo_controls_repeat_all} - The repeat icon for {@link + * Player#REPEAT_MODE_ALL}. + *
        • {@code exo_controls_shuffle_off} - The shuffle icon when shuffling is disabled. + *
        • {@code exo_controls_shuffle_on} - The shuffle icon when shuffling is enabled. + *
        • {@code exo_controls_vr} - The VR icon. + *
        + * *

        Overriding the layout file

        * * To customize the layout of PlayerControlView throughout your app, or just for certain @@ -112,8 +138,6 @@ import java.util.concurrent.CopyOnWriteArrayList; * ExoPlayer library, and will be inflated for use by PlayerControlView. The view identifies and * binds its children by looking for the following ids: * - *

        - * *

          *
        • {@code exo_play} - The play button. *
            @@ -123,29 +147,38 @@ import java.util.concurrent.CopyOnWriteArrayList; *
              *
            • Type: {@link View} *
            - *
          • {@code exo_ffwd} - The fast forward button. - *
              - *
            • Type: {@link View} - *
            *
          • {@code exo_rew} - The rewind button. *
              *
            • Type: {@link View} *
            - *
          • {@code exo_prev} - The previous track button. + *
          • {@code exo_ffwd} - The fast forward button. *
              *
            • Type: {@link View} *
            - *
          • {@code exo_next} - The next track button. + *
          • {@code exo_prev} - The previous button. + *
              + *
            • Type: {@link View} + *
            + *
          • {@code exo_next} - The next button. *
              *
            • Type: {@link View} *
            *
          • {@code exo_repeat_toggle} - The repeat toggle button. *
              - *
            • Type: {@link View} + *
            • Type: {@link ImageView} + *
            • Note: PlayerControlView will programmatically set the drawable on the repeat toggle + * button according to the player's current repeat mode. The drawables used are {@code + * exo_controls_repeat_off}, {@code exo_controls_repeat_one} and {@code + * exo_controls_repeat_all}. See the section above for information on overriding these + * drawables. *
            *
          • {@code exo_shuffle} - The shuffle button. *
              - *
            • Type: {@link View} + *
            • Type: {@link ImageView} + *
            • Note: PlayerControlView will programmatically set the drawable on the shuffle button + * according to the player's current repeat mode. The drawables used are {@code + * exo_controls_shuffle_off} and {@code exo_controls_shuffle_on}. See the section above + * for information on overriding these drawables. *
            *
          • {@code exo_vr} - The VR mode button. *
              @@ -213,10 +246,6 @@ public class PlayerControlView extends FrameLayout { void onProgressUpdate(long position, long bufferedPosition); } - /** The default fast forward increment, in milliseconds. */ - public static final int DEFAULT_FAST_FORWARD_MS = 15000; - /** The default rewind increment, in milliseconds. */ - public static final int DEFAULT_REWIND_MS = 5000; /** The default show timeout, in milliseconds. */ public static final int DEFAULT_SHOW_TIMEOUT_MS = 5000; /** The default repeat toggle modes. */ @@ -227,7 +256,6 @@ public class PlayerControlView extends FrameLayout { /** The maximum number of windows that can be shown in a multi-window time bar. */ public static final int MAX_WINDOWS_FOR_MULTI_WINDOW_TIME_BAR = 100; - private static final long MAX_POSITION_FOR_SEEK_TO_PREVIOUS = 3000; /** The maximum interval between time bar position updates. */ private static final int MAX_UPDATE_INTERVAL_MS = 1000; @@ -274,8 +302,6 @@ public class PlayerControlView extends FrameLayout { private boolean showMultiWindowTimeBar; private boolean multiWindowTimeBar; private boolean scrubbing; - private int rewindMs; - private int fastForwardMs; private int showTimeoutMs; private int timeBarMinUpdateIntervalMs; private @RepeatModeUtil.RepeatToggleModes int repeatToggleModes; @@ -311,13 +337,13 @@ public class PlayerControlView extends FrameLayout { @Nullable AttributeSet playbackAttrs) { super(context, attrs, defStyleAttr); int controllerLayoutId = R.layout.exo_player_control_view; - rewindMs = DEFAULT_REWIND_MS; - fastForwardMs = DEFAULT_FAST_FORWARD_MS; showTimeoutMs = DEFAULT_SHOW_TIMEOUT_MS; repeatToggleModes = DEFAULT_REPEAT_TOGGLE_MODES; timeBarMinUpdateIntervalMs = DEFAULT_TIME_BAR_MIN_UPDATE_INTERVAL_MS; hideAtMs = C.TIME_UNSET; showShuffleButton = false; + int rewindMs = DefaultControlDispatcher.DEFAULT_REWIND_MS; + int fastForwardMs = DefaultControlDispatcher.DEFAULT_FAST_FORWARD_MS; if (playbackAttrs != null) { TypedArray a = context @@ -351,7 +377,8 @@ public class PlayerControlView extends FrameLayout { extraAdGroupTimesMs = new long[0]; extraPlayedAdGroups = new boolean[0]; componentListener = new ComponentListener(); - controlDispatcher = new com.google.android.exoplayer2.DefaultControlDispatcher(); + controlDispatcher = + new com.google.android.exoplayer2.DefaultControlDispatcher(fastForwardMs, rewindMs); updateProgressAction = this::updateProgress; hideAction = this::hide; @@ -556,37 +583,39 @@ public class PlayerControlView extends FrameLayout { /** * Sets the {@link com.google.android.exoplayer2.ControlDispatcher}. * - * @param controlDispatcher The {@link com.google.android.exoplayer2.ControlDispatcher}, or null - * to use {@link com.google.android.exoplayer2.DefaultControlDispatcher}. + * @param controlDispatcher The {@link com.google.android.exoplayer2.ControlDispatcher}. */ - public void setControlDispatcher( - @Nullable com.google.android.exoplayer2.ControlDispatcher controlDispatcher) { - this.controlDispatcher = - controlDispatcher == null - ? new com.google.android.exoplayer2.DefaultControlDispatcher() - : controlDispatcher; + public void setControlDispatcher(ControlDispatcher controlDispatcher) { + if (this.controlDispatcher != controlDispatcher) { + this.controlDispatcher = controlDispatcher; + updateNavigation(); + } } /** - * Sets the rewind increment in milliseconds. - * - * @param rewindMs The rewind increment in milliseconds. A non-positive value will cause the - * rewind button to be disabled. + * @deprecated Use {@link #setControlDispatcher(ControlDispatcher)} with {@link + * DefaultControlDispatcher#DefaultControlDispatcher(long, long)}. */ + @SuppressWarnings("deprecation") + @Deprecated public void setRewindIncrementMs(int rewindMs) { - this.rewindMs = rewindMs; - updateNavigation(); + if (controlDispatcher instanceof DefaultControlDispatcher) { + ((DefaultControlDispatcher) controlDispatcher).setRewindIncrementMs(rewindMs); + updateNavigation(); + } } /** - * Sets the fast forward increment in milliseconds. - * - * @param fastForwardMs The fast forward increment in milliseconds. A non-positive value will - * cause the fast forward button to be disabled. + * @deprecated Use {@link #setControlDispatcher(ControlDispatcher)} with {@link + * DefaultControlDispatcher#DefaultControlDispatcher(long, long)}. */ + @SuppressWarnings("deprecation") + @Deprecated public void setFastForwardIncrementMs(int fastForwardMs) { - this.fastForwardMs = fastForwardMs; - updateNavigation(); + if (controlDispatcher instanceof DefaultControlDispatcher) { + ((DefaultControlDispatcher) controlDispatcher).setFastForwardIncrementMs(fastForwardMs); + updateNavigation(); + } } /** @@ -797,8 +826,8 @@ public class PlayerControlView extends FrameLayout { boolean isSeekable = window.isSeekable; enableSeeking = isSeekable; enablePrevious = isSeekable || !window.isDynamic || player.hasPrevious(); - enableRewind = isSeekable && rewindMs > 0; - enableFastForward = isSeekable && fastForwardMs > 0; + enableRewind = isSeekable && controlDispatcher.isRewindEnabled(); + enableFastForward = isSeekable && controlDispatcher.isFastForwardEnabled(); enableNext = window.isDynamic || player.hasNext(); } } @@ -910,7 +939,7 @@ public class PlayerControlView extends FrameLayout { adGroupTimeInPeriodUs = period.durationUs; } long adGroupTimeInWindowUs = adGroupTimeInPeriodUs + period.getPositionInWindowUs(); - if (adGroupTimeInWindowUs >= 0 && adGroupTimeInWindowUs <= window.durationUs) { + if (adGroupTimeInWindowUs >= 0) { if (adGroupCount == adGroupTimesMs.length) { int newLength = adGroupTimesMs.length == 0 ? 1 : adGroupTimesMs.length * 2; adGroupTimesMs = Arrays.copyOf(adGroupTimesMs, newLength); @@ -979,7 +1008,7 @@ public class PlayerControlView extends FrameLayout { mediaTimeDelayMs = Math.min(mediaTimeDelayMs, mediaTimeUntilNextFullSecondMs); // Calculate the delay until the next update in real time, taking playbackSpeed into account. - float playbackSpeed = player.getPlaybackParameters().speed; + float playbackSpeed = player.getPlaybackSpeed(); long delayMs = playbackSpeed > 0 ? (long) (mediaTimeDelayMs / playbackSpeed) : MAX_UPDATE_INTERVAL_MS; @@ -1009,59 +1038,6 @@ public class PlayerControlView extends FrameLayout { view.setVisibility(VISIBLE); } - private void previous(Player player) { - Timeline timeline = player.getCurrentTimeline(); - if (timeline.isEmpty() || player.isPlayingAd()) { - return; - } - int windowIndex = player.getCurrentWindowIndex(); - timeline.getWindow(windowIndex, window); - int previousWindowIndex = player.getPreviousWindowIndex(); - if (previousWindowIndex != C.INDEX_UNSET - && (player.getCurrentPosition() <= MAX_POSITION_FOR_SEEK_TO_PREVIOUS - || (window.isDynamic && !window.isSeekable))) { - seekTo(player, previousWindowIndex, C.TIME_UNSET); - } else { - seekTo(player, windowIndex, /* positionMs= */ 0); - } - } - - private void next(Player player) { - Timeline timeline = player.getCurrentTimeline(); - if (timeline.isEmpty() || player.isPlayingAd()) { - return; - } - int windowIndex = player.getCurrentWindowIndex(); - int nextWindowIndex = player.getNextWindowIndex(); - if (nextWindowIndex != C.INDEX_UNSET) { - seekTo(player, nextWindowIndex, C.TIME_UNSET); - } else if (timeline.getWindow(windowIndex, window).isDynamic) { - seekTo(player, windowIndex, C.TIME_UNSET); - } - } - - private void rewind(Player player) { - if (player.isCurrentWindowSeekable() && rewindMs > 0) { - seekToOffset(player, -rewindMs); - } - } - - private void fastForward(Player player) { - if (player.isCurrentWindowSeekable() && fastForwardMs > 0) { - seekToOffset(player, fastForwardMs); - } - } - - private void seekToOffset(Player player, long offsetMs) { - long positionMs = player.getCurrentPosition() + offsetMs; - long durationMs = player.getDuration(); - if (durationMs != C.TIME_UNSET) { - positionMs = Math.min(positionMs, durationMs); - } - positionMs = Math.max(positionMs, 0); - seekTo(player, player.getCurrentWindowIndex(), positionMs); - } - private void seekToTimeBarPosition(Player player, long positionMs) { int windowIndex; Timeline timeline = player.getCurrentTimeline(); @@ -1150,9 +1126,9 @@ public class PlayerControlView extends FrameLayout { } if (event.getAction() == KeyEvent.ACTION_DOWN) { if (keyCode == KeyEvent.KEYCODE_MEDIA_FAST_FORWARD) { - fastForward(player); + controlDispatcher.dispatchFastForward(player); } else if (keyCode == KeyEvent.KEYCODE_MEDIA_REWIND) { - rewind(player); + controlDispatcher.dispatchRewind(player); } else if (event.getRepeatCount() == 0) { switch (keyCode) { case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE: @@ -1165,10 +1141,10 @@ public class PlayerControlView extends FrameLayout { controlDispatcher.dispatchSetPlayWhenReady(player, false); break; case KeyEvent.KEYCODE_MEDIA_NEXT: - next(player); + controlDispatcher.dispatchNext(player); break; case KeyEvent.KEYCODE_MEDIA_PREVIOUS: - previous(player); + controlDispatcher.dispatchPrevious(player); break; default: break; @@ -1243,7 +1219,14 @@ public class PlayerControlView extends FrameLayout { } @Override - public void onPlayerStateChanged(boolean playWhenReady, @Player.State int playbackState) { + public void onPlaybackStateChanged(@Player.State int playbackState) { + updatePlayPauseButton(); + updateProgress(); + } + + @Override + public void onPlayWhenReadyChanged( + boolean playWhenReady, @Player.PlayWhenReadyChangeReason int reason) { updatePlayPauseButton(); updateProgress(); } @@ -1284,13 +1267,13 @@ public class PlayerControlView extends FrameLayout { return; } if (nextButton == view) { - next(player); + controlDispatcher.dispatchNext(player); } else if (previousButton == view) { - previous(player); + controlDispatcher.dispatchPrevious(player); } else if (fastForwardButton == view) { - fastForward(player); + controlDispatcher.dispatchFastForward(player); } else if (rewindButton == view) { - rewind(player); + controlDispatcher.dispatchRewind(player); } else if (playButton == view) { if (player.getPlaybackState() == Player.STATE_IDLE) { if (playbackPreparer != null) { 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 6c77284e46..b3d646c99d 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 @@ -37,7 +37,6 @@ import androidx.media.app.NotificationCompat.MediaStyle; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ControlDispatcher; import com.google.android.exoplayer2.DefaultControlDispatcher; -import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.PlaybackPreparer; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Timeline; @@ -55,28 +54,28 @@ import java.util.List; import java.util.Map; /** - * A notification manager to start, update and cancel a media style notification reflecting the - * player state. + * Starts, updates and cancels a media style notification reflecting the player state. The actions + * displayed and the drawables used can both be customized, as described below. * *

              The notification is cancelled when {@code null} is passed to {@link #setPlayer(Player)} or * when the notification is dismissed by the user. * *

              If the player is released it must be removed from the manager by calling {@code - * setPlayer(null)} which will cancel the notification. + * setPlayer(null)}. * *

              Action customization

              * - * Standard playback actions can be shown or omitted as follows: + * Playback actions can be displayed or omitted as follows: * *
                - *
              • {@code useNavigationActions} - Sets whether the navigation previous and next actions - * are displayed. + *
              • {@code useNavigationActions} - Sets whether the previous and next actions are + * displayed. *
                  *
                • Corresponding setter: {@link #setUseNavigationActions(boolean)} *
                • Default: {@code true} *
                - *
              • {@code useNavigationActionsInCompactView} - Sets whether the navigation previous and - * next actions should are displayed in compact view (including the lock screen notification). + *
              • {@code useNavigationActionsInCompactView} - Sets whether the previous and next + * actions are displayed in compact view (including the lock screen notification). *
                  *
                • Corresponding setter: {@link #setUseNavigationActionsInCompactView(boolean)} *
                • Default: {@code false} @@ -94,16 +93,39 @@ import java.util.Map; *
                • {@code rewindIncrementMs} - Sets the rewind increment. If set to zero the rewind * action is not displayed. *
                    - *
                  • Corresponding setter: {@link #setRewindIncrementMs(long)} - *
                  • Default: {@link #DEFAULT_REWIND_MS} (5000) + *
                  • Corresponding setter: {@link #setControlDispatcher(ControlDispatcher)} + *
                  • Default: {@link DefaultControlDispatcher#DEFAULT_REWIND_MS} (5000) *
                  *
                • {@code fastForwardIncrementMs} - Sets the fast forward increment. If set to zero the - * fast forward action is not included in the notification. + * fast forward action is not displayed. *
                    - *
                  • Corresponding setter: {@link #setFastForwardIncrementMs(long)} - *
                  • Default: {@link #DEFAULT_FAST_FORWARD_MS} (5000) + *
                  • Corresponding setter: {@link #setControlDispatcher(ControlDispatcher)} + *
                  • Default: {@link DefaultControlDispatcher#DEFAULT_FAST_FORWARD_MS} (15000) *
                  *
                + * + *

                Overriding drawables

                + * + * The drawables used by PlayerNotificationManager can be overridden by drawables with the same + * names defined in your application. The drawables that can be overridden are: + * + *
                  + *
                • {@code exo_notification_small_icon} - The icon passed by default to {@link + * NotificationCompat.Builder#setSmallIcon(int)}. A different icon can also be specified + * programmatically by calling {@link #setSmallIcon(int)}. + *
                • {@code exo_notification_play} - The play icon. + *
                • {@code exo_notification_pause} - The pause icon. + *
                • {@code exo_notification_rewind} - The rewind icon. + *
                • {@code exo_notification_fastforward} - The fast forward icon. + *
                • {@code exo_notification_previous} - The previous icon. + *
                • {@code exo_notification_next} - The next icon. + *
                • {@code exo_notification_stop} - The stop icon. + *
                + * + * Unlike the drawables above, the large icon (i.e. the icon passed to {@link + * NotificationCompat.Builder#setLargeIcon(Bitmap)} cannot be overridden in this way. Instead, the + * large icon is obtained from the {@link MediaDescriptionAdapter} injected when creating the + * PlayerNotificationManager. */ public class PlayerNotificationManager { @@ -117,7 +139,7 @@ public class PlayerNotificationManager { * * @param player The {@link Player} for which a notification is being built. */ - String getCurrentContentTitle(Player player); + CharSequence getCurrentContentTitle(Player player); /** * Creates a content intent for the current media item. @@ -137,7 +159,7 @@ public class PlayerNotificationManager { * @param player The {@link Player} for which a notification is being built. */ @Nullable - String getCurrentContentText(Player player); + CharSequence getCurrentContentText(Player player); /** * Gets the content sub text for the current media item. @@ -147,18 +169,17 @@ public class PlayerNotificationManager { * @param player The {@link Player} for which a notification is being built. */ @Nullable - default String getCurrentSubText(Player player) { + default CharSequence getCurrentSubText(Player player) { return null; } /** * Gets the large icon for the current media item. * - *

                When a bitmap initially needs to be asynchronously loaded, a placeholder (or null) can be - * returned and the bitmap asynchronously passed to the {@link BitmapCallback} once it is - * loaded. Because the adapter may be called multiple times for the same media item, the bitmap - * should be cached by the app and whenever possible be returned synchronously at subsequent - * calls for the same media item. + *

                When a bitmap needs to be loaded asynchronously, a placeholder bitmap (or null) should be + * returned. The actual bitmap should be passed to the {@link BitmapCallback} once it has been + * loaded. Because the adapter may be called multiple times for the same media item, bitmaps + * should be cached by the app and returned synchronously when possible. * *

                See {@link NotificationCompat.Builder#setLargeIcon(Bitmap)}. * @@ -332,13 +353,6 @@ public class PlayerNotificationManager { }) public @interface Priority {} - /** The default fast forward increment, in milliseconds. */ - public static final int DEFAULT_FAST_FORWARD_MS = 15000; - /** The default rewind increment, in milliseconds. */ - public static final int DEFAULT_REWIND_MS = 5000; - - private static final long MAX_POSITION_FOR_SEEK_TO_PREVIOUS = 3000; - private static int instanceIdCounter; private final Context context; @@ -358,7 +372,7 @@ public class PlayerNotificationManager { private final Timeline.Window window; @Nullable private NotificationCompat.Builder builder; - @Nullable private ArrayList builderActions; + @Nullable private List builderActions; @Nullable private Player player; @Nullable private PlaybackPreparer playbackPreparer; private ControlDispatcher controlDispatcher; @@ -370,8 +384,6 @@ public class PlayerNotificationManager { private boolean useNavigationActionsInCompactView; private boolean usePlayPauseActions; private boolean useStopAction; - private long fastForwardMs; - private long rewindMs; private int badgeIconType; private boolean colorized; private int defaults; @@ -612,8 +624,6 @@ public class PlayerNotificationManager { smallIconResourceId = R.drawable.exo_notification_small_icon; defaults = 0; priority = NotificationCompat.PRIORITY_LOW; - fastForwardMs = DEFAULT_FAST_FORWARD_MS; - rewindMs = DEFAULT_REWIND_MS; badgeIconType = NotificationCompat.BADGE_ICON_SMALL; visibility = NotificationCompat.VISIBILITY_PUBLIC; @@ -679,12 +689,13 @@ public class PlayerNotificationManager { /** * Sets the {@link ControlDispatcher}. * - * @param controlDispatcher The {@link ControlDispatcher}, or null to use {@link - * DefaultControlDispatcher}. + * @param controlDispatcher The {@link ControlDispatcher}. */ public final void setControlDispatcher(ControlDispatcher controlDispatcher) { - this.controlDispatcher = - controlDispatcher != null ? controlDispatcher : new DefaultControlDispatcher(); + if (this.controlDispatcher != controlDispatcher) { + this.controlDispatcher = controlDispatcher; + invalidate(); + } } /** @@ -703,31 +714,29 @@ public class PlayerNotificationManager { } /** - * Sets the fast forward increment in milliseconds. - * - * @param fastForwardMs The fast forward increment in milliseconds. A value of zero will cause the - * fast forward action to be disabled. + * @deprecated Use {@link #setControlDispatcher(ControlDispatcher)} with {@link + * DefaultControlDispatcher#DefaultControlDispatcher(long, long)}. */ + @SuppressWarnings("deprecation") + @Deprecated public final void setFastForwardIncrementMs(long fastForwardMs) { - if (this.fastForwardMs == fastForwardMs) { - return; + if (controlDispatcher instanceof DefaultControlDispatcher) { + ((DefaultControlDispatcher) controlDispatcher).setFastForwardIncrementMs(fastForwardMs); + invalidate(); } - this.fastForwardMs = fastForwardMs; - invalidate(); } /** - * Sets the rewind increment in milliseconds. - * - * @param rewindMs The rewind increment in milliseconds. A value of zero will cause the rewind - * action to be disabled. + * @deprecated Use {@link #setControlDispatcher(ControlDispatcher)} with {@link + * DefaultControlDispatcher#DefaultControlDispatcher(long, long)}. */ + @SuppressWarnings("deprecation") + @Deprecated public final void setRewindIncrementMs(long rewindMs) { - if (this.rewindMs == rewindMs) { - return; + if (controlDispatcher instanceof DefaultControlDispatcher) { + ((DefaultControlDispatcher) controlDispatcher).setRewindIncrementMs(rewindMs); + invalidate(); } - this.rewindMs = rewindMs; - invalidate(); } /** @@ -905,7 +914,18 @@ public class PlayerNotificationManager { } /** - * Sets whether the elapsed time of the media playback should be displayed + * Sets whether the elapsed time of the media playback should be displayed. + * + *

                Note that this setting only works if all of the following are true: + * + *

                  + *
                • The media is {@link Player#isPlaying() actively playing}. + *
                • The media is not {@link Player#isCurrentWindowDynamic() dynamically changing its + * duration} (like for example a live stream). + *
                • The media is not {@link Player#isPlayingAd() interrupted by an ad}. + *
                • The media is played at {@link Player#getPlaybackParameters() regular speed}. + *
                • The device is running at least API 21 (Lollipop). + *
                * *

                See {@link NotificationCompat.Builder#setUsesChronometer(boolean)}. * @@ -998,8 +1018,6 @@ public class PlayerNotificationManager { * NotificationCompat.Builder#build()} to obtain the notification, or {@code null} if no * notification should be displayed. */ - // incompatible types in argument. - @SuppressWarnings("nullness:argument.type.incompatible") @Nullable protected NotificationCompat.Builder createNotification( Player player, @@ -1013,9 +1031,10 @@ public class PlayerNotificationManager { } List actionNames = getActions(player); - ArrayList actions = new ArrayList<>(actionNames.size()); + List actions = new ArrayList<>(actionNames.size()); for (int i = 0; i < actionNames.size(); i++) { String actionName = actionNames.get(i); + @Nullable NotificationCompat.Action action = playbackActions.containsKey(actionName) ? playbackActions.get(actionName) @@ -1062,7 +1081,8 @@ public class PlayerNotificationManager { && useChronometer && player.isPlaying() && !player.isPlayingAd() - && !player.isCurrentWindowDynamic()) { + && !player.isCurrentWindowDynamic() + && player.getPlaybackParameters().speed == 1f) { builder .setWhen(System.currentTimeMillis() - player.getContentPosition()) .setShowWhen(true) @@ -1114,8 +1134,8 @@ public class PlayerNotificationManager { if (!timeline.isEmpty() && !player.isPlayingAd()) { timeline.getWindow(player.getCurrentWindowIndex(), window); enablePrevious = window.isSeekable || !window.isDynamic || player.hasPrevious(); - enableRewind = rewindMs > 0; - enableFastForward = fastForwardMs > 0; + enableRewind = controlDispatcher.isRewindEnabled(); + enableFastForward = controlDispatcher.isFastForwardEnabled(); enableNext = window.isDynamic || player.hasNext(); } @@ -1190,63 +1210,6 @@ public class PlayerNotificationManager { && player.getPlayWhenReady(); } - private void previous(Player player) { - Timeline timeline = player.getCurrentTimeline(); - if (timeline.isEmpty() || player.isPlayingAd()) { - return; - } - int windowIndex = player.getCurrentWindowIndex(); - timeline.getWindow(windowIndex, window); - int previousWindowIndex = player.getPreviousWindowIndex(); - if (previousWindowIndex != C.INDEX_UNSET - && (player.getCurrentPosition() <= MAX_POSITION_FOR_SEEK_TO_PREVIOUS - || (window.isDynamic && !window.isSeekable))) { - seekTo(player, previousWindowIndex, C.TIME_UNSET); - } else { - seekTo(player, windowIndex, /* positionMs= */ 0); - } - } - - private void next(Player player) { - Timeline timeline = player.getCurrentTimeline(); - if (timeline.isEmpty() || player.isPlayingAd()) { - return; - } - int windowIndex = player.getCurrentWindowIndex(); - int nextWindowIndex = player.getNextWindowIndex(); - if (nextWindowIndex != C.INDEX_UNSET) { - seekTo(player, nextWindowIndex, C.TIME_UNSET); - } else if (timeline.getWindow(windowIndex, window).isDynamic) { - seekTo(player, windowIndex, C.TIME_UNSET); - } - } - - private void rewind(Player player) { - if (player.isCurrentWindowSeekable() && rewindMs > 0) { - seekToOffset(player, /* offsetMs= */ -rewindMs); - } - } - - private void fastForward(Player player) { - if (player.isCurrentWindowSeekable() && fastForwardMs > 0) { - seekToOffset(player, /* offsetMs= */ fastForwardMs); - } - } - - private void seekToOffset(Player player, long offsetMs) { - long positionMs = player.getCurrentPosition() + offsetMs; - long durationMs = player.getDuration(); - if (durationMs != C.TIME_UNSET) { - positionMs = Math.min(positionMs, durationMs); - } - positionMs = Math.max(positionMs, 0); - seekTo(player, player.getCurrentWindowIndex(), positionMs); - } - - private void seekTo(Player player, int windowIndex, long positionMs) { - controlDispatcher.dispatchSeekTo(player, windowIndex, positionMs); - } - private boolean shouldShowPauseButton(Player player) { return player.getPlaybackState() != Player.STATE_ENDED && player.getPlaybackState() != Player.STATE_IDLE @@ -1348,7 +1311,13 @@ public class PlayerNotificationManager { private class PlayerListener implements Player.EventListener { @Override - public void onPlayerStateChanged(boolean playWhenReady, @Player.State int playbackState) { + public void onPlaybackStateChanged(@Player.State int playbackState) { + postStartOrUpdateNotification(); + } + + @Override + public void onPlayWhenReadyChanged( + boolean playWhenReady, @Player.PlayWhenReadyChangeReason int reason) { postStartOrUpdateNotification(); } @@ -1363,7 +1332,7 @@ public class PlayerNotificationManager { } @Override - public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) { + public void onPlaybackSpeedChanged(float playbackSpeed) { postStartOrUpdateNotification(); } @@ -1400,19 +1369,19 @@ public class PlayerNotificationManager { playbackPreparer.preparePlayback(); } } else if (player.getPlaybackState() == Player.STATE_ENDED) { - seekTo(player, player.getCurrentWindowIndex(), C.TIME_UNSET); + controlDispatcher.dispatchSeekTo(player, player.getCurrentWindowIndex(), C.TIME_UNSET); } controlDispatcher.dispatchSetPlayWhenReady(player, /* playWhenReady= */ true); } else if (ACTION_PAUSE.equals(action)) { controlDispatcher.dispatchSetPlayWhenReady(player, /* playWhenReady= */ false); } else if (ACTION_PREVIOUS.equals(action)) { - previous(player); + controlDispatcher.dispatchPrevious(player); } else if (ACTION_REWIND.equals(action)) { - rewind(player); + controlDispatcher.dispatchRewind(player); } else if (ACTION_FAST_FORWARD.equals(action)) { - fastForward(player); + controlDispatcher.dispatchFastForward(player); } else if (ACTION_NEXT.equals(action)) { - next(player); + controlDispatcher.dispatchNext(player); } else if (ACTION_STOP.equals(action)) { controlDispatcher.dispatchStop(player, /* reset= */ true); } else if (ACTION_DISMISS.equals(action)) { 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 c55fe09f76..60b72783e8 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 @@ -16,7 +16,6 @@ package com.google.android.exoplayer2.ui; import android.annotation.SuppressLint; -import android.annotation.TargetApi; import android.content.Context; import android.content.res.Resources; import android.content.res.TypedArray; @@ -40,6 +39,7 @@ import android.widget.ImageView; import android.widget.TextView; import androidx.annotation.IntDef; import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; import androidx.core.content.ContextCompat; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ControlDispatcher; @@ -79,7 +79,8 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; * during playback, and displays playback controls using a {@link PlayerControlView}. * *

                A PlayerView can be customized by setting attributes (or calling corresponding methods), - * overriding the view's layout file or by specifying a custom view layout file, as outlined below. + * overriding drawables, overriding the view's layout file, or by specifying a custom view layout + * file. * *

                Attributes

                * @@ -142,6 +143,12 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; *
              • Corresponding method: None *
              • Default: {@code surface_view} *
              + *
            • {@code use_sensor_rotation} - Whether to use the orientation sensor for rotation + * during spherical playbacks (if available). + *
                + *
              • Corresponding method: {@link #setUseSensorRotation(boolean)} + *
              • Default: {@code true} + *
              *
            • {@code shutter_background_color} - The background color of the {@code exo_shutter} * view. *
                @@ -172,6 +179,12 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; * exo_controller} (see below). *
              * + *

              Overriding drawables

              + * + * The drawables used by {@link PlayerControlView} (with its default layout file) can be overridden + * by drawables with the same names defined in your application. See the {@link PlayerControlView} + * documentation for a list of drawables that can be overridden. + * *

              Overriding the layout file

              * * To customize the layout of PlayerView throughout your app, or just for certain configurations, @@ -180,8 +193,6 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; * inflated for use by PlayerView. The view identifies and binds its children by looking for the * following ids: * - *

              - * *

                *
              • {@code exo_content_frame} - A frame whose aspect ratio is resized based on the video * or album art of the media being played, and the configured {@code resize_mode}. The video @@ -301,6 +312,7 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider @Nullable private Drawable defaultArtwork; private @ShowBuffering int showBuffering; private boolean keepContentOnPlayerReset; + private boolean useSensorRotation; @Nullable private ErrorMessageProvider errorMessageProvider; @Nullable private CharSequence customErrorMessage; private int controllerShowTimeoutMs; @@ -360,6 +372,7 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider boolean controllerAutoShow = true; boolean controllerHideDuringAds = true; int showBuffering = SHOW_BUFFERING_NEVER; + useSensorRotation = true; if (attrs != null) { TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.PlayerView, 0, 0); try { @@ -383,6 +396,8 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider R.styleable.PlayerView_keep_content_on_player_reset, keepContentOnPlayerReset); controllerHideDuringAds = a.getBoolean(R.styleable.PlayerView_hide_during_ads, controllerHideDuringAds); + useSensorRotation = + a.getBoolean(R.styleable.PlayerView_use_sensor_rotation, useSensorRotation); } finally { a.recycle(); } @@ -415,6 +430,7 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider case SURFACE_TYPE_SPHERICAL_GL_SURFACE_VIEW: SphericalGLSurfaceView sphericalGLSurfaceView = new SphericalGLSurfaceView(context); sphericalGLSurfaceView.setSingleTapListener(componentListener); + sphericalGLSurfaceView.setUseSensorRotation(useSensorRotation); surfaceView = sphericalGLSurfaceView; break; case SURFACE_TYPE_VIDEO_DECODER_GL_SURFACE_VIEW: @@ -739,6 +755,22 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider } } + /** + * Sets whether to use the orientation sensor for rotation during spherical playbacks (if + * available) + * + * @param useSensorRotation Whether to use the orientation sensor for rotation during spherical + * playbacks. + */ + public void setUseSensorRotation(boolean useSensorRotation) { + if (this.useSensorRotation != useSensorRotation) { + this.useSensorRotation = useSensorRotation; + if (surfaceView instanceof SphericalGLSurfaceView) { + ((SphericalGLSurfaceView) surfaceView).setUseSensorRotation(useSensorRotation); + } + } + } + /** * Sets whether a buffering spinner is displayed when the player is in the buffering state. The * buffering spinner is not displayed by default. @@ -958,31 +990,30 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider /** * Sets the {@link ControlDispatcher}. * - * @param controlDispatcher The {@link ControlDispatcher}, or null to use {@link - * DefaultControlDispatcher}. + * @param controlDispatcher The {@link ControlDispatcher}. */ - public void setControlDispatcher(@Nullable ControlDispatcher controlDispatcher) { + public void setControlDispatcher(ControlDispatcher controlDispatcher) { Assertions.checkStateNotNull(controller); controller.setControlDispatcher(controlDispatcher); } /** - * Sets the rewind increment in milliseconds. - * - * @param rewindMs The rewind increment in milliseconds. A non-positive value will cause the - * rewind button to be disabled. + * @deprecated Use {@link #setControlDispatcher(ControlDispatcher)} with {@link + * DefaultControlDispatcher#DefaultControlDispatcher(long, long)}. */ + @SuppressWarnings("deprecation") + @Deprecated public void setRewindIncrementMs(int rewindMs) { Assertions.checkStateNotNull(controller); controller.setRewindIncrementMs(rewindMs); } /** - * Sets the fast forward increment in milliseconds. - * - * @param fastForwardMs The fast forward increment in milliseconds. A non-positive value will - * cause the fast forward button to be disabled. + * @deprecated Use {@link #setControlDispatcher(ControlDispatcher)} with {@link + * DefaultControlDispatcher#DefaultControlDispatcher(long, long)}. */ + @SuppressWarnings("deprecation") + @Deprecated public void setFastForwardIncrementMs(int fastForwardMs) { Assertions.checkStateNotNull(controller); controller.setFastForwardIncrementMs(fastForwardMs); @@ -1387,7 +1418,7 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider errorMessageView.setVisibility(View.VISIBLE); return; } - @Nullable ExoPlaybackException error = player != null ? player.getPlaybackError() : null; + @Nullable ExoPlaybackException error = player != null ? player.getPlayerError() : null; if (error != null && errorMessageProvider != null) { CharSequence errorMessage = errorMessageProvider.getErrorMessage(error).second; errorMessageView.setText(errorMessage); @@ -1412,7 +1443,15 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider } } - @TargetApi(23) + private void updateControllerVisibility() { + if (isPlayingAd() && controllerHideDuringAds) { + hideController(); + } else { + maybeShowController(false); + } + } + + @RequiresApi(23) private static void configureEditModeLogoV23(Resources resources, ImageView logo) { logo.setImageDrawable(resources.getDrawable(R.drawable.exo_edit_mode_logo, null)); logo.setBackgroundColor(resources.getColor(R.color.exo_edit_mode_background_color, null)); @@ -1526,14 +1565,17 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider // Player.EventListener implementation @Override - public void onPlayerStateChanged(boolean playWhenReady, @Player.State int playbackState) { + public void onPlaybackStateChanged(@Player.State int playbackState) { updateBuffering(); updateErrorMessage(); - if (isPlayingAd() && controllerHideDuringAds) { - hideController(); - } else { - maybeShowController(false); - } + updateControllerVisibility(); + } + + @Override + public void onPlayWhenReadyChanged( + boolean playWhenReady, @Player.PlayWhenReadyChangeReason int reason) { + updateBuffering(); + updateControllerVisibility(); } @Override diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SpannedToHtmlConverter.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SpannedToHtmlConverter.java new file mode 100644 index 0000000000..8f61902205 --- /dev/null +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SpannedToHtmlConverter.java @@ -0,0 +1,287 @@ +/* + * Copyright (C) 2020 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; + +import android.graphics.Typeface; +import android.text.Html; +import android.text.Spanned; +import android.text.style.AbsoluteSizeSpan; +import android.text.style.BackgroundColorSpan; +import android.text.style.ForegroundColorSpan; +import android.text.style.RelativeSizeSpan; +import android.text.style.StyleSpan; +import android.text.style.TypefaceSpan; +import android.text.style.UnderlineSpan; +import android.util.SparseArray; +import androidx.annotation.Nullable; +import com.google.android.exoplayer2.text.span.HorizontalTextInVerticalContextSpan; +import com.google.android.exoplayer2.text.span.RubySpan; +import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.Util; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.regex.Pattern; + +/** + * Utility class to convert from span-styled text to HTML. + * + *

                Supports all of the spans used by ExoPlayer's subtitle decoders, including custom ones found + * in {@link com.google.android.exoplayer2.text.span}. + */ +// TODO: Add support for more span types - only a small selection are currently implemented. +/* package */ final class SpannedToHtmlConverter { + + // Matches /n and /r/n in ampersand-encoding (returned from Html.escapeHtml). + private static final Pattern NEWLINE_PATTERN = Pattern.compile("( )? "); + + private SpannedToHtmlConverter() {} + + /** + * Convert {@code text} into HTML, adding tags and styling to match any styling spans present. + * + *

                All textual content is HTML-escaped during the conversion. + * + *

                NOTE: The current implementation does not handle overlapping spans correctly, it will + * generate overlapping HTML tags that are invalid. In most cases this won't be a problem because: + * + *

                  + *
                • Most subtitle formats use a tagged structure to carry formatting information (e.g. WebVTT + * and TTML), so the {@link Spanned} objects created by these decoders likely won't have + * overlapping spans. + *
                • WebView/Chromium (the intended destination of this HTML) gracefully handles overlapping + * tags and usually renders the same result as spanned text in a TextView. + *
                + * + * @param text The (possibly span-styled) text to convert to HTML. + * @param displayDensity The screen density of the device. WebView treats 1 CSS px as one Android + * dp, so to convert size values from Android px to CSS px we need to know the screen density. + */ + public static String convert(@Nullable CharSequence text, float displayDensity) { + if (text == null) { + return ""; + } + if (!(text instanceof Spanned)) { + return escapeHtml(text); + } + Spanned spanned = (Spanned) text; + SparseArray spanTransitions = findSpanTransitions(spanned, displayDensity); + + StringBuilder html = new StringBuilder(spanned.length()); + int previousTransition = 0; + for (int i = 0; i < spanTransitions.size(); i++) { + int index = spanTransitions.keyAt(i); + html.append(escapeHtml(spanned.subSequence(previousTransition, index))); + + Transition transition = spanTransitions.get(index); + Collections.sort(transition.spansRemoved, SpanInfo.FOR_CLOSING_TAGS); + for (SpanInfo spanInfo : transition.spansRemoved) { + html.append(spanInfo.closingTag); + } + Collections.sort(transition.spansAdded, SpanInfo.FOR_OPENING_TAGS); + for (SpanInfo spanInfo : transition.spansAdded) { + html.append(spanInfo.openingTag); + } + previousTransition = index; + } + + html.append(escapeHtml(spanned.subSequence(previousTransition, spanned.length()))); + + return html.toString(); + } + + private static SparseArray findSpanTransitions( + Spanned spanned, float displayDensity) { + SparseArray spanTransitions = new SparseArray<>(); + + for (Object span : spanned.getSpans(0, spanned.length(), Object.class)) { + @Nullable String openingTag = getOpeningTag(span, displayDensity); + @Nullable String closingTag = getClosingTag(span); + int spanStart = spanned.getSpanStart(span); + int spanEnd = spanned.getSpanEnd(span); + if (openingTag != null) { + Assertions.checkNotNull(closingTag); + SpanInfo spanInfo = new SpanInfo(spanStart, spanEnd, openingTag, closingTag); + getOrCreate(spanTransitions, spanStart).spansAdded.add(spanInfo); + getOrCreate(spanTransitions, spanEnd).spansRemoved.add(spanInfo); + } + } + + return spanTransitions; + } + + @Nullable + private static String getOpeningTag(Object span, float displayDensity) { + if (span instanceof ForegroundColorSpan) { + ForegroundColorSpan colorSpan = (ForegroundColorSpan) span; + return Util.formatInvariant( + "", HtmlUtils.toCssRgba(colorSpan.getForegroundColor())); + } else if (span instanceof BackgroundColorSpan) { + BackgroundColorSpan colorSpan = (BackgroundColorSpan) span; + return Util.formatInvariant( + "", + HtmlUtils.toCssRgba(colorSpan.getBackgroundColor())); + } else if (span instanceof HorizontalTextInVerticalContextSpan) { + return ""; + } else if (span instanceof AbsoluteSizeSpan) { + AbsoluteSizeSpan absoluteSizeSpan = (AbsoluteSizeSpan) span; + float sizeCssPx = + absoluteSizeSpan.getDip() + ? absoluteSizeSpan.getSize() + : absoluteSizeSpan.getSize() / displayDensity; + return Util.formatInvariant("", sizeCssPx); + } else if (span instanceof RelativeSizeSpan) { + return Util.formatInvariant( + "", ((RelativeSizeSpan) span).getSizeChange() * 100); + } else if (span instanceof TypefaceSpan) { + @Nullable String fontFamily = ((TypefaceSpan) span).getFamily(); + return fontFamily != null + ? Util.formatInvariant("", fontFamily) + : null; + } else if (span instanceof StyleSpan) { + switch (((StyleSpan) span).getStyle()) { + case Typeface.BOLD: + return ""; + case Typeface.ITALIC: + return ""; + case Typeface.BOLD_ITALIC: + return ""; + default: + return null; + } + } else if (span instanceof RubySpan) { + RubySpan rubySpan = (RubySpan) span; + switch (rubySpan.position) { + case RubySpan.POSITION_OVER: + return ""; + case RubySpan.POSITION_UNDER: + return ""; + case RubySpan.POSITION_UNKNOWN: + return ""; + default: + return null; + } + } else if (span instanceof UnderlineSpan) { + return ""; + } else { + return null; + } + } + + @Nullable + private static String getClosingTag(Object span) { + if (span instanceof ForegroundColorSpan + || span instanceof BackgroundColorSpan + || span instanceof HorizontalTextInVerticalContextSpan + || span instanceof AbsoluteSizeSpan + || span instanceof RelativeSizeSpan) { + return ""; + } else if (span instanceof TypefaceSpan) { + @Nullable String fontFamily = ((TypefaceSpan) span).getFamily(); + return fontFamily != null ? "" : null; + } else if (span instanceof StyleSpan) { + switch (((StyleSpan) span).getStyle()) { + case Typeface.BOLD: + return ""; + case Typeface.ITALIC: + return ""; + case Typeface.BOLD_ITALIC: + return ""; + } + } else if (span instanceof RubySpan) { + RubySpan rubySpan = (RubySpan) span; + return "" + escapeHtml(rubySpan.rubyText) + ""; + } else if (span instanceof UnderlineSpan) { + return ""; + } + return null; + } + + private static Transition getOrCreate(SparseArray transitions, int key) { + @Nullable Transition transition = transitions.get(key); + if (transition == null) { + transition = new Transition(); + transitions.put(key, transition); + } + return transition; + } + + private static String escapeHtml(CharSequence text) { + String escaped = Html.escapeHtml(text); + return NEWLINE_PATTERN.matcher(escaped).replaceAll("
                "); + } + + private static final class SpanInfo { + /** + * Sort by end index (descending), then by opening tag and then closing tag (both ascending, for + * determinism). + */ + private static final Comparator FOR_OPENING_TAGS = + (info1, info2) -> { + int result = Integer.compare(info2.end, info1.end); + if (result != 0) { + return result; + } + result = info1.openingTag.compareTo(info2.openingTag); + if (result != 0) { + return result; + } + return info1.closingTag.compareTo(info2.closingTag); + }; + + /** + * Sort by start index (descending), then by opening tag and then closing tag (both descending, + * for determinism). + */ + private static final Comparator FOR_CLOSING_TAGS = + (info1, info2) -> { + int result = Integer.compare(info2.start, info1.start); + if (result != 0) { + return result; + } + result = info2.openingTag.compareTo(info1.openingTag); + if (result != 0) { + return result; + } + return info2.closingTag.compareTo(info1.closingTag); + }; + + public final int start; + public final int end; + public final String openingTag; + public final String closingTag; + + private SpanInfo(int start, int end, String openingTag, String closingTag) { + this.start = start; + this.end = end; + this.openingTag = openingTag; + this.closingTag = closingTag; + } + } + + private static final class Transition { + private final List spansAdded; + private final List spansRemoved; + + public Transition() { + this.spansAdded = new ArrayList<>(); + this.spansRemoved = new ArrayList<>(); + } + } +} 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 76768804df..f0093a282c 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 @@ -33,6 +33,7 @@ import android.text.TextPaint; import android.text.TextUtils; import android.text.style.AbsoluteSizeSpan; import android.text.style.BackgroundColorSpan; +import android.text.style.ForegroundColorSpan; import android.text.style.RelativeSizeSpan; import android.util.DisplayMetrics; import androidx.annotation.Nullable; @@ -64,7 +65,8 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; private final float spacingAdd; private final TextPaint textPaint; - private final Paint paint; + private final Paint windowPaint; + private final Paint bitmapPaint; // Previous input variables. @Nullable private CharSequence cueText; @@ -98,6 +100,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; // Derived drawing variables. private @MonotonicNonNull StaticLayout textLayout; + private @MonotonicNonNull StaticLayout edgeLayout; private int textLeft; private int textTop; private int textPaddingX; @@ -122,9 +125,13 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; textPaint.setAntiAlias(true); textPaint.setSubpixelText(true); - paint = new Paint(); - paint.setAntiAlias(true); - paint.setStyle(Style.FILL); + windowPaint = new Paint(); + windowPaint.setAntiAlias(true); + windowPaint.setStyle(Style.FILL); + + bitmapPaint = new Paint(); + bitmapPaint.setAntiAlias(true); + bitmapPaint.setFilterBitmap(true); } /** @@ -240,7 +247,10 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; @RequiresNonNull("cueText") private void setupTextLayout() { - CharSequence cueText = this.cueText; + SpannableStringBuilder cueText = + this.cueText instanceof SpannableStringBuilder + ? (SpannableStringBuilder) this.cueText + : new SpannableStringBuilder(this.cueText); int parentWidth = parentRight - parentLeft; int parentHeight = parentBottom - parentTop; @@ -258,39 +268,57 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; // Remove embedded styling or font size if requested. if (!applyEmbeddedStyles) { - cueText = cueText.toString(); // Equivalent to erasing all spans. + // Remove all spans, regardless of type. + for (Object span : cueText.getSpans(0, cueText.length(), Object.class)) { + cueText.removeSpan(span); + } } 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); + AbsoluteSizeSpan[] absSpans = cueText.getSpans(0, cueText.length(), AbsoluteSizeSpan.class); for (AbsoluteSizeSpan absSpan : absSpans) { - newCueText.removeSpan(absSpan); + cueText.removeSpan(absSpan); } + RelativeSizeSpan[] relSpans = cueText.getSpans(0, cueText.length(), RelativeSizeSpan.class); for (RelativeSizeSpan relSpan : relSpans) { - newCueText.removeSpan(relSpan); + cueText.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( + // Use an AbsoluteSizeSpan encompassing the whole text to apply the default cueTextSizePx. + cueText.setSpan( new AbsoluteSizeSpan((int) cueTextSizePx), /* start= */ 0, - /* end= */ newCueText.length(), + /* end= */ cueText.length(), Spanned.SPAN_PRIORITY); - cueText = newCueText; } } + // Remove embedded font color to not destroy edges, otherwise it overrides edge color. + SpannableStringBuilder cueTextEdge = new SpannableStringBuilder(cueText); + if (edgeType == CaptionStyleCompat.EDGE_TYPE_OUTLINE) { + ForegroundColorSpan[] foregroundColorSpans = + cueTextEdge.getSpans(0, cueTextEdge.length(), ForegroundColorSpan.class); + for (ForegroundColorSpan foregroundColorSpan : foregroundColorSpans) { + cueTextEdge.removeSpan(foregroundColorSpan); + } + } + + // EDGE_TYPE_NONE & EDGE_TYPE_DROP_SHADOW both paint in one pass, they ignore cueTextEdge. + // In other cases we use two painters and we need to apply the background in the first one only, + // otherwise the background color gets drawn in front of the edge color + // (https://github.com/google/ExoPlayer/pull/6724#issuecomment-564650572). if (Color.alpha(backgroundColor) > 0) { - SpannableStringBuilder newCueText = new SpannableStringBuilder(cueText); - newCueText.setSpan( - new BackgroundColorSpan(backgroundColor), 0, newCueText.length(), Spanned.SPAN_PRIORITY); - cueText = newCueText; + if (edgeType == CaptionStyleCompat.EDGE_TYPE_NONE + || edgeType == CaptionStyleCompat.EDGE_TYPE_DROP_SHADOW) { + cueText.setSpan( + new BackgroundColorSpan(backgroundColor), 0, cueText.length(), Spanned.SPAN_PRIORITY); + } else { + cueTextEdge.setSpan( + new BackgroundColorSpan(backgroundColor), + 0, + cueTextEdge.length(), + Spanned.SPAN_PRIORITY); + } } Alignment textAlignment = cueTextAlignment == null ? Alignment.ALIGN_CENTER : cueTextAlignment; @@ -366,6 +394,9 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; // Update the derived drawing variables. this.textLayout = new StaticLayout(cueText, textPaint, textWidth, textAlignment, spacingMult, spacingAdd, true); + this.edgeLayout = + new StaticLayout( + cueTextEdge, textPaint, textWidth, textAlignment, spacingMult, spacingAdd, true); this.textLeft = textLeft; this.textTop = textTop; this.textPaddingX = textPaddingX; @@ -405,8 +436,9 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; } private void drawTextLayout(Canvas canvas) { - StaticLayout layout = textLayout; - if (layout == null) { + StaticLayout textLayout = this.textLayout; + StaticLayout edgeLayout = this.edgeLayout; + if (textLayout == null || edgeLayout == null) { // Nothing to draw. return; } @@ -415,9 +447,13 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; canvas.translate(textLeft, textTop); if (Color.alpha(windowColor) > 0) { - paint.setColor(windowColor); - canvas.drawRect(-textPaddingX, 0, layout.getWidth() + textPaddingX, layout.getHeight(), - paint); + windowPaint.setColor(windowColor); + canvas.drawRect( + -textPaddingX, + 0, + textLayout.getWidth() + textPaddingX, + textLayout.getHeight(), + windowPaint); } if (edgeType == CaptionStyleCompat.EDGE_TYPE_OUTLINE) { @@ -425,7 +461,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; textPaint.setStrokeWidth(outlineWidth); textPaint.setColor(edgeColor); textPaint.setStyle(Style.FILL_AND_STROKE); - layout.draw(canvas); + edgeLayout.draw(canvas); } else if (edgeType == CaptionStyleCompat.EDGE_TYPE_DROP_SHADOW) { textPaint.setShadowLayer(shadowRadius, shadowOffset, shadowOffset, edgeColor); } else if (edgeType == CaptionStyleCompat.EDGE_TYPE_RAISED @@ -437,13 +473,13 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; textPaint.setColor(foregroundColor); textPaint.setStyle(Style.FILL); textPaint.setShadowLayer(shadowRadius, -offset, -offset, colorUp); - layout.draw(canvas); + edgeLayout.draw(canvas); textPaint.setShadowLayer(shadowRadius, offset, offset, colorDown); } textPaint.setColor(foregroundColor); textPaint.setStyle(Style.FILL); - layout.draw(canvas); + textLayout.draw(canvas); textPaint.setShadowLayer(0, 0, 0, 0); canvas.restoreToCount(saveCount); @@ -451,7 +487,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; @RequiresNonNull({"cueBitmap", "bitmapRect"}) private void drawBitmapLayout(Canvas canvas) { - canvas.drawBitmap(cueBitmap, /* src= */ null, bitmapRect, /* paint= */ null); + canvas.drawBitmap(cueBitmap, /* src= */ null, bitmapRect, bitmapPaint); } /** @@ -466,4 +502,5 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; // equals methods, so we perform one explicitly here. return first == second || (first != null && first.equals(second)); } + } diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitleTextView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitleTextView.java new file mode 100644 index 0000000000..9d0dfb78a2 --- /dev/null +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitleTextView.java @@ -0,0 +1,221 @@ +/* + * 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.ui; + +import static com.google.android.exoplayer2.ui.SubtitleView.DEFAULT_BOTTOM_PADDING_FRACTION; +import static com.google.android.exoplayer2.ui.SubtitleView.DEFAULT_TEXT_SIZE_FRACTION; + +import android.content.Context; +import android.graphics.Canvas; +import android.util.AttributeSet; +import android.view.View; +import androidx.annotation.Nullable; +import com.google.android.exoplayer2.text.CaptionStyleCompat; +import com.google.android.exoplayer2.text.Cue; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * A {@link SubtitleView.Output} that uses Android's native text tooling via {@link + * SubtitlePainter}. + */ +/* package */ final class SubtitleTextView extends View implements SubtitleView.Output { + + private final List painters; + + private List cues; + @Cue.TextSizeType private int textSizeType; + private float textSize; + private boolean applyEmbeddedStyles; + private boolean applyEmbeddedFontSizes; + private CaptionStyleCompat style; + private float bottomPaddingFraction; + + public SubtitleTextView(Context context) { + this(context, /* attrs= */ null); + } + + public SubtitleTextView(Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + painters = new ArrayList<>(); + cues = Collections.emptyList(); + textSizeType = Cue.TEXT_SIZE_TYPE_FRACTIONAL; + textSize = DEFAULT_TEXT_SIZE_FRACTION; + applyEmbeddedStyles = true; + applyEmbeddedFontSizes = true; + style = CaptionStyleCompat.DEFAULT; + bottomPaddingFraction = DEFAULT_BOTTOM_PADDING_FRACTION; + } + + @Override + public void onCues(List cues) { + if (this.cues == cues || this.cues.isEmpty() && cues.isEmpty()) { + return; + } + this.cues = cues; + // Ensure we have sufficient painters. + while (painters.size() < cues.size()) { + painters.add(new SubtitlePainter(getContext())); + } + // Invalidate to trigger drawing. + invalidate(); + } + + @Override + public void setTextSize(@Cue.TextSizeType int textSizeType, float textSize) { + if (this.textSizeType == textSizeType && this.textSize == textSize) { + return; + } + this.textSizeType = textSizeType; + this.textSize = textSize; + invalidate(); + } + + @Override + public void setApplyEmbeddedStyles(boolean applyEmbeddedStyles) { + if (this.applyEmbeddedStyles == applyEmbeddedStyles + && this.applyEmbeddedFontSizes == applyEmbeddedStyles) { + return; + } + this.applyEmbeddedStyles = applyEmbeddedStyles; + this.applyEmbeddedFontSizes = applyEmbeddedStyles; + invalidate(); + } + + @Override + public void setApplyEmbeddedFontSizes(boolean applyEmbeddedFontSizes) { + if (this.applyEmbeddedFontSizes == applyEmbeddedFontSizes) { + return; + } + this.applyEmbeddedFontSizes = applyEmbeddedFontSizes; + invalidate(); + } + + @Override + public void setStyle(CaptionStyleCompat style) { + if (this.style == style) { + return; + } + this.style = style; + invalidate(); + } + + @Override + public void setBottomPaddingFraction(float bottomPaddingFraction) { + if (this.bottomPaddingFraction == bottomPaddingFraction) { + return; + } + this.bottomPaddingFraction = bottomPaddingFraction; + invalidate(); + } + + @Override + public void dispatchDraw(Canvas canvas) { + @Nullable List cues = this.cues; + if (cues.isEmpty()) { + return; + } + + int rawViewHeight = getHeight(); + + // Calculate the cue box bounds relative to the canvas after padding is taken into account. + int left = getPaddingLeft(); + int top = getPaddingTop(); + int right = getWidth() - getPaddingRight(); + int bottom = rawViewHeight - getPaddingBottom(); + if (bottom <= top || right <= left) { + // No space to draw subtitles. + return; + } + int viewHeightMinusPadding = bottom - top; + + float defaultViewTextSizePx = + SubtitleViewUtils.resolveTextSize( + textSizeType, textSize, rawViewHeight, viewHeightMinusPadding); + if (defaultViewTextSizePx <= 0) { + // Text has no height. + return; + } + + int cueCount = cues.size(); + for (int i = 0; i < cueCount; i++) { + Cue cue = cues.get(i); + if (cue.verticalType != Cue.TYPE_UNSET) { + cue = repositionVerticalCue(cue); + } + float cueTextSizePx = + SubtitleViewUtils.resolveTextSize( + cue.textSizeType, cue.textSize, rawViewHeight, viewHeightMinusPadding); + SubtitlePainter painter = painters.get(i); + painter.draw( + cue, + applyEmbeddedStyles, + applyEmbeddedFontSizes, + style, + defaultViewTextSizePx, + cueTextSizePx, + bottomPaddingFraction, + canvas, + left, + top, + right, + bottom); + } + } + + /** + * Reposition a vertical cue for horizontal display. + * + *

                This class doesn't support rendering vertical text, but if we naively interpret vertical + * {@link Cue#position} and{@link Cue#line} values for horizontal display then the cues will often + * be displayed in unexpected positions. For example, the 'default' position for vertical-rl + * subtitles is the right-hand edge of the viewport, so cues that would appear vertically in this + * position should appear horizontally at the bottom of the viewport (generally the default + * position). Similarly left-edge vertical-rl cues should be shown at the top of a horizontal + * viewport. + * + *

                There isn't a meaningful way to transform {@link Cue#position} and related values (e.g. + * alignment), so we clear these and allow {@link SubtitlePainter} to do the default behaviour of + * centering the cue. + */ + private static Cue repositionVerticalCue(Cue cue) { + Cue.Builder cueBuilder = + cue.buildUpon() + .setPosition(Cue.DIMEN_UNSET) + .setPositionAnchor(Cue.TYPE_UNSET) + .setTextAlignment(null); + + if (cue.lineType == Cue.LINE_TYPE_FRACTION) { + cueBuilder.setLine(1.0f - cue.line, Cue.LINE_TYPE_FRACTION); + } else { + cueBuilder.setLine(-cue.line - 1f, Cue.LINE_TYPE_NUMBER); + } + switch (cue.lineAnchor) { + case Cue.ANCHOR_TYPE_END: + cueBuilder.setLineAnchor(Cue.ANCHOR_TYPE_START); + break; + case Cue.ANCHOR_TYPE_START: + cueBuilder.setLineAnchor(Cue.ANCHOR_TYPE_END); + break; + case Cue.ANCHOR_TYPE_MIDDLE: + case Cue.TYPE_UNSET: + default: + // Fall through + } + return cueBuilder.build(); + } +} 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 5fa2e4816b..23a1add0fc 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 @@ -12,34 +12,39 @@ * 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; -import android.annotation.TargetApi; +import static java.lang.annotation.RetentionPolicy.SOURCE; + import android.content.Context; import android.content.res.Resources; -import android.graphics.Canvas; import android.util.AttributeSet; import android.util.TypedValue; import android.view.View; import android.view.accessibility.CaptioningManager; +import android.widget.FrameLayout; +import androidx.annotation.Dimension; +import androidx.annotation.IntDef; import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; import com.google.android.exoplayer2.text.CaptionStyleCompat; import com.google.android.exoplayer2.text.Cue; import com.google.android.exoplayer2.text.TextOutput; import com.google.android.exoplayer2.util.Util; -import java.util.ArrayList; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.util.Collections; import java.util.List; -/** - * A view for displaying subtitle {@link Cue}s. - */ -public final class SubtitleView extends View implements TextOutput { +/** A view for displaying subtitle {@link Cue}s. */ +public final class SubtitleView extends FrameLayout implements TextOutput { /** * The default fractional text size. * - * @see #setFractionalTextSize(float, boolean) + * @see SubtitleView#setFractionalTextSize(float, boolean) */ public static final float DEFAULT_TEXT_SIZE_FRACTION = 0.0533f; @@ -51,29 +56,50 @@ public final class SubtitleView extends View implements TextOutput { */ public static final float DEFAULT_BOTTOM_PADDING_FRACTION = 0.08f; - private final List painters; + /** + * Indicates a {@link SubtitleTextView} should be used to display subtitles. This is the default. + */ + public static final int VIEW_TYPE_TEXT = 1; - @Nullable private List cues; - @Cue.TextSizeType private int textSizeType; - private float textSize; - private boolean applyEmbeddedStyles; - private boolean applyEmbeddedFontSizes; - private CaptionStyleCompat style; - private float bottomPaddingFraction; + /** + * Indicates a {@link SubtitleWebView} should be used to display subtitles. + * + *

                This will instantiate a {@link android.webkit.WebView} and use CSS and HTML styling to + * render the subtitles. This supports some additional styling features beyond those supported by + * {@link SubtitleTextView} such as vertical text. + */ + public static final int VIEW_TYPE_WEB = 2; + + /** + * The type of {@link View} to use to display subtitles. + * + *

                One of: + * + *

                  + *
                • {@link #VIEW_TYPE_TEXT} + *
                • {@link #VIEW_TYPE_WEB} + *
                + */ + @Documented + @Retention(SOURCE) + @IntDef({VIEW_TYPE_TEXT, VIEW_TYPE_WEB}) + public @interface ViewType {} + + private @ViewType int viewType; + private Output output; + private View innerSubtitleView; public SubtitleView(Context context) { - this(context, /* attrs= */ null); + this(context, null); } public SubtitleView(Context context, @Nullable AttributeSet attrs) { super(context, attrs); - painters = new ArrayList<>(); - textSizeType = Cue.TEXT_SIZE_TYPE_FRACTIONAL; - textSize = DEFAULT_TEXT_SIZE_FRACTION; - applyEmbeddedStyles = true; - applyEmbeddedFontSizes = true; - style = CaptionStyleCompat.DEFAULT; - bottomPaddingFraction = DEFAULT_BOTTOM_PADDING_FRACTION; + SubtitleTextView subtitleTextView = new SubtitleTextView(context, attrs); + output = subtitleTextView; + innerSubtitleView = subtitleTextView; + addView(innerSubtitleView); + viewType = VIEW_TYPE_TEXT; } @Override @@ -87,28 +113,53 @@ public final class SubtitleView extends View implements TextOutput { * @param cues The cues to display, or null to clear the cues. */ public void setCues(@Nullable List cues) { - if (this.cues == cues) { + output.onCues(cues != null ? cues : Collections.emptyList()); + } + + /** + * Set the type of {@link View} used to display subtitles. + * + *

                NOTE: {@link #VIEW_TYPE_WEB} is currently very experimental, and doesn't support most + * styling and layout properties of {@link Cue}. + * + * @param viewType The {@link ViewType} to use. + */ + public void setViewType(@ViewType int viewType) { + if (this.viewType == viewType) { return; } - this.cues = cues; - // Ensure we have sufficient painters. - int cueCount = (cues == null) ? 0 : cues.size(); - while (painters.size() < cueCount) { - painters.add(new SubtitlePainter(getContext())); + switch (viewType) { + case VIEW_TYPE_TEXT: + setView(new SubtitleTextView(getContext())); + break; + case VIEW_TYPE_WEB: + setView(new SubtitleWebView(getContext())); + break; + default: + throw new IllegalArgumentException(); } - // Invalidate to trigger drawing. - invalidate(); + this.viewType = viewType; + } + + private void setView(T view) { + removeView(innerSubtitleView); + if (innerSubtitleView instanceof SubtitleWebView) { + ((SubtitleWebView) innerSubtitleView).destroy(); + } + innerSubtitleView = view; + output = view; + addView(view); } /** * Set the text size to a given unit and value. - *

                - * See {@link TypedValue} for the possible dimension units. + * + *

                See {@link TypedValue} for the possible dimension units. * * @param unit The desired dimension unit. * @param size The desired size in the given units. */ - public void setFixedTextSize(int unit, float size) { + public void setFixedTextSize(@Dimension int unit, float size) { Context context = getContext(); Resources resources; if (context == null) { @@ -160,13 +211,7 @@ public final class SubtitleView extends View implements TextOutput { } private void setTextSize(@Cue.TextSizeType int textSizeType, float textSize) { - if (this.textSizeType == textSizeType && this.textSize == textSize) { - return; - } - this.textSizeType = textSizeType; - this.textSize = textSize; - // Invalidate to trigger drawing. - invalidate(); + output.setTextSize(textSizeType, textSize); } /** @@ -176,14 +221,7 @@ public final class SubtitleView extends View implements TextOutput { * @param applyEmbeddedStyles Whether styling embedded within the cues should be applied. */ public void setApplyEmbeddedStyles(boolean applyEmbeddedStyles) { - if (this.applyEmbeddedStyles == applyEmbeddedStyles - && this.applyEmbeddedFontSizes == applyEmbeddedStyles) { - return; - } - this.applyEmbeddedStyles = applyEmbeddedStyles; - this.applyEmbeddedFontSizes = applyEmbeddedStyles; - // Invalidate to trigger drawing. - invalidate(); + output.setApplyEmbeddedStyles(applyEmbeddedStyles); } /** @@ -193,12 +231,7 @@ public final class SubtitleView extends View implements TextOutput { * @param applyEmbeddedFontSizes Whether font sizes embedded within the cues should be applied. */ public void setApplyEmbeddedFontSizes(boolean applyEmbeddedFontSizes) { - if (this.applyEmbeddedFontSizes == applyEmbeddedFontSizes) { - return; - } - this.applyEmbeddedFontSizes = applyEmbeddedFontSizes; - // Invalidate to trigger drawing. - invalidate(); + output.setApplyEmbeddedFontSizes(applyEmbeddedFontSizes); } /** @@ -218,12 +251,7 @@ public final class SubtitleView extends View implements TextOutput { * @param style A style for the view. */ public void setStyle(CaptionStyleCompat style) { - if (this.style == style) { - return; - } - this.style = style; - // Invalidate to trigger drawing. - invalidate(); + output.setStyle(style); } /** @@ -236,108 +264,36 @@ public final class SubtitleView extends View implements TextOutput { * @param bottomPaddingFraction The bottom padding fraction. */ public void setBottomPaddingFraction(float bottomPaddingFraction) { - if (this.bottomPaddingFraction == bottomPaddingFraction) { - return; - } - this.bottomPaddingFraction = bottomPaddingFraction; - // Invalidate to trigger drawing. - invalidate(); + output.setBottomPaddingFraction(bottomPaddingFraction); } - @Override - public void dispatchDraw(Canvas canvas) { - List cues = this.cues; - if (cues == null || cues.isEmpty()) { - return; - } - - int rawViewHeight = getHeight(); - - // Calculate the cue box bounds relative to the canvas after padding is taken into account. - int left = getPaddingLeft(); - int top = getPaddingTop(); - int right = getWidth() - getPaddingRight(); - int bottom = rawViewHeight - getPaddingBottom(); - if (bottom <= top || right <= left) { - // No space to draw subtitles. - return; - } - int viewHeightMinusPadding = bottom - top; - - float defaultViewTextSizePx = - resolveTextSize(textSizeType, textSize, rawViewHeight, viewHeightMinusPadding); - if (defaultViewTextSizePx <= 0) { - // Text has no height. - return; - } - - int cueCount = cues.size(); - for (int i = 0; i < cueCount; i++) { - Cue cue = cues.get(i); - float cueTextSizePx = resolveCueTextSize(cue, rawViewHeight, viewHeightMinusPadding); - SubtitlePainter painter = painters.get(i); - painter.draw( - cue, - applyEmbeddedStyles, - applyEmbeddedFontSizes, - style, - defaultViewTextSizePx, - cueTextSizePx, - bottomPaddingFraction, - canvas, - left, - top, - right, - bottom); - } - } - - private float resolveCueTextSize(Cue cue, int rawViewHeight, int viewHeightMinusPadding) { - if (cue.textSizeType == Cue.TYPE_UNSET || cue.textSize == Cue.DIMEN_UNSET) { - return 0; - } - float defaultCueTextSizePx = - resolveTextSize(cue.textSizeType, cue.textSize, rawViewHeight, viewHeightMinusPadding); - return Math.max(defaultCueTextSizePx, 0); - } - - private float resolveTextSize( - @Cue.TextSizeType int textSizeType, - float textSize, - int rawViewHeight, - int viewHeightMinusPadding) { - switch (textSizeType) { - case Cue.TEXT_SIZE_TYPE_ABSOLUTE: - return textSize; - case Cue.TEXT_SIZE_TYPE_FRACTIONAL: - return textSize * viewHeightMinusPadding; - case Cue.TEXT_SIZE_TYPE_FRACTIONAL_IGNORE_PADDING: - return textSize * rawViewHeight; - case Cue.TYPE_UNSET: - default: - return Cue.DIMEN_UNSET; - } - } - - @TargetApi(19) + @RequiresApi(19) private boolean isCaptionManagerEnabled() { CaptioningManager captioningManager = (CaptioningManager) getContext().getSystemService(Context.CAPTIONING_SERVICE); return captioningManager.isEnabled(); } - @TargetApi(19) + @RequiresApi(19) private float getUserCaptionFontScaleV19() { CaptioningManager captioningManager = (CaptioningManager) getContext().getSystemService(Context.CAPTIONING_SERVICE); return captioningManager.getFontScale(); } - @TargetApi(19) + @RequiresApi(19) private CaptionStyleCompat getUserCaptionStyleV19() { CaptioningManager captioningManager = (CaptioningManager) getContext().getSystemService(Context.CAPTIONING_SERVICE); return CaptionStyleCompat.createFromCaptionStyle(captioningManager.getUserStyle()); } + /* package */ interface Output { + void onCues(List cues); + void setTextSize(@Cue.TextSizeType int textSizeType, float textSize); + void setApplyEmbeddedStyles(boolean applyEmbeddedStyles); + void setApplyEmbeddedFontSizes(boolean applyEmbeddedFontSizes); + void setStyle(CaptionStyleCompat style); + void setBottomPaddingFraction(float bottomPaddingFraction); + } } diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitleViewUtils.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitleViewUtils.java new file mode 100644 index 0000000000..24b5e30b2e --- /dev/null +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitleViewUtils.java @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2020 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; + +import com.google.android.exoplayer2.text.Cue; + +/** Utility class for subtitle layout logic. */ +/* package */ final class SubtitleViewUtils { + + /** + * Returns the text size in px, derived from {@code textSize} and {@code textSizeType}. + * + *

                Returns {@link Cue#DIMEN_UNSET} if {@code textSize == Cue.DIMEN_UNSET} or {@code + * textSizeType == Cue.TYPE_UNSET}. + */ + public static float resolveTextSize( + @Cue.TextSizeType int textSizeType, + float textSize, + int rawViewHeight, + int viewHeightMinusPadding) { + if (textSize == Cue.DIMEN_UNSET) { + return Cue.DIMEN_UNSET; + } + switch (textSizeType) { + case Cue.TEXT_SIZE_TYPE_ABSOLUTE: + return textSize; + case Cue.TEXT_SIZE_TYPE_FRACTIONAL: + return textSize * viewHeightMinusPadding; + case Cue.TEXT_SIZE_TYPE_FRACTIONAL_IGNORE_PADDING: + return textSize * rawViewHeight; + case Cue.TYPE_UNSET: + default: + return Cue.DIMEN_UNSET; + } + } + + private SubtitleViewUtils() {} +} diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitleWebView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitleWebView.java new file mode 100644 index 0000000000..ee081f384e --- /dev/null +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitleWebView.java @@ -0,0 +1,395 @@ +/* + * Copyright (C) 2020 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; + +import static com.google.android.exoplayer2.ui.SubtitleView.DEFAULT_BOTTOM_PADDING_FRACTION; +import static com.google.android.exoplayer2.ui.SubtitleView.DEFAULT_TEXT_SIZE_FRACTION; + +import android.content.Context; +import android.graphics.Color; +import android.text.Layout; +import android.util.AttributeSet; +import android.util.Base64; +import android.util.DisplayMetrics; +import android.view.MotionEvent; +import android.webkit.WebView; +import android.widget.FrameLayout; +import androidx.annotation.Nullable; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.text.CaptionStyleCompat; +import com.google.android.exoplayer2.text.Cue; +import com.google.android.exoplayer2.util.Util; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.List; + +/** + * A {@link SubtitleView.Output} that uses a {@link WebView} to render subtitles. + * + *

                This is useful for subtitle styling not supported by Android's native text libraries such as + * vertical text. + * + *

                NOTE: This is currently extremely experimental and doesn't support most {@link Cue} styling + * properties. + */ +/* package */ final class SubtitleWebView extends FrameLayout implements SubtitleView.Output { + + /** + * A {@link SubtitleTextView} used for displaying bitmap cues. + * + *

                There's no advantage to displaying bitmap cues in a {@link WebView}, so we re-use the + * existing logic. + */ + private final SubtitleTextView subtitleTextView; + + private final WebView webView; + private final List cues; + + @Cue.TextSizeType private int defaultTextSizeType; + private float defaultTextSize; + private boolean applyEmbeddedStyles; + private boolean applyEmbeddedFontSizes; + private CaptionStyleCompat style; + private float bottomPaddingFraction; + + public SubtitleWebView(Context context) { + this(context, null); + } + + public SubtitleWebView(Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + cues = new ArrayList<>(); + defaultTextSizeType = Cue.TEXT_SIZE_TYPE_FRACTIONAL; + defaultTextSize = DEFAULT_TEXT_SIZE_FRACTION; + applyEmbeddedStyles = true; + applyEmbeddedFontSizes = true; + style = CaptionStyleCompat.DEFAULT; + bottomPaddingFraction = DEFAULT_BOTTOM_PADDING_FRACTION; + + subtitleTextView = new SubtitleTextView(context, attrs); + webView = + new WebView(context, attrs) { + @Override + public boolean onTouchEvent(MotionEvent event) { + super.onTouchEvent(event); + // Return false so that touch events are allowed down into @id/exo_content_frame below. + return false; + } + + @Override + public boolean performClick() { + super.performClick(); + // Return false so that clicks are allowed down into @id/exo_content_frame below. + return false; + } + }; + webView.setBackgroundColor(Color.TRANSPARENT); + + addView(subtitleTextView); + addView(webView); + } + + @Override + public void onCues(List cues) { + List bitmapCues = new ArrayList<>(); + this.cues.clear(); + for (int i = 0; i < cues.size(); i++) { + Cue cue = cues.get(i); + if (cue.bitmap != null) { + bitmapCues.add(cue); + } else { + this.cues.add(cue); + } + } + subtitleTextView.onCues(bitmapCues); + // Invalidate to trigger subtitleTextView to draw. + invalidate(); + updateWebView(); + } + + @Override + public void setTextSize(@Cue.TextSizeType int textSizeType, float textSize) { + if (this.defaultTextSizeType == textSizeType && this.defaultTextSize == textSize) { + return; + } + this.defaultTextSizeType = textSizeType; + this.defaultTextSize = textSize; + invalidate(); + updateWebView(); + } + + @Override + public void setApplyEmbeddedStyles(boolean applyEmbeddedStyles) { + if (this.applyEmbeddedStyles == applyEmbeddedStyles + && this.applyEmbeddedFontSizes == applyEmbeddedStyles) { + return; + } + this.applyEmbeddedStyles = applyEmbeddedStyles; + this.applyEmbeddedFontSizes = applyEmbeddedStyles; + invalidate(); + updateWebView(); + } + + @Override + public void setApplyEmbeddedFontSizes(boolean applyEmbeddedFontSizes) { + if (this.applyEmbeddedFontSizes == applyEmbeddedFontSizes) { + return; + } + this.applyEmbeddedFontSizes = applyEmbeddedFontSizes; + invalidate(); + updateWebView(); + } + + @Override + public void setStyle(CaptionStyleCompat style) { + if (this.style == style) { + return; + } + this.style = style; + invalidate(); + updateWebView(); + } + + @Override + public void setBottomPaddingFraction(float bottomPaddingFraction) { + if (this.bottomPaddingFraction == bottomPaddingFraction) { + return; + } + this.bottomPaddingFraction = bottomPaddingFraction; + invalidate(); + updateWebView(); + } + + /** + * Cleans up internal state, including calling {@link WebView#destroy()} on the delegate view. + * + *

                This method may only be called after this view has been removed from the view system. No + * other methods may be called on this view after destroy. + */ + public void destroy() { + cues.clear(); + webView.destroy(); + } + + private void updateWebView() { + StringBuilder html = new StringBuilder(); + html.append( + Util.formatInvariant( + "

                ", + HtmlUtils.toCssRgba(style.foregroundColor), + convertTextSizeToCss(defaultTextSizeType, defaultTextSize))); + + String backgroundColorCss = HtmlUtils.toCssRgba(style.backgroundColor); + + for (int i = 0; i < cues.size(); i++) { + Cue cue = cues.get(i); + float positionPercent = (cue.position != Cue.DIMEN_UNSET) ? (cue.position * 100) : 50; + int positionAnchorTranslatePercent = anchorTypeToTranslatePercent(cue.positionAnchor); + + float linePercent; + int lineTranslatePercent; + @Cue.AnchorType int lineAnchor; + if (cue.line != Cue.DIMEN_UNSET) { + switch (cue.lineType) { + case Cue.LINE_TYPE_NUMBER: + if (cue.line >= 0) { + linePercent = 0; + lineTranslatePercent = Math.round(cue.line) * 100; + } else { + linePercent = 100; + lineTranslatePercent = Math.round(cue.line + 1) * 100; + } + break; + case Cue.LINE_TYPE_FRACTION: + case Cue.TYPE_UNSET: + default: + linePercent = cue.line * 100; + lineTranslatePercent = 0; + } + lineAnchor = cue.lineAnchor; + } else { + linePercent = (1.0f - bottomPaddingFraction) * 100; + lineTranslatePercent = 0; + // If Cue.line == DIMEN_UNSET then ignore Cue.lineAnchor and assume ANCHOR_TYPE_END. + lineAnchor = Cue.ANCHOR_TYPE_END; + } + int lineAnchorTranslatePercent = + cue.verticalType == Cue.VERTICAL_TYPE_RL + ? -anchorTypeToTranslatePercent(lineAnchor) + : anchorTypeToTranslatePercent(lineAnchor); + + String size = + cue.size != Cue.DIMEN_UNSET + ? Util.formatInvariant("%.2f%%", cue.size * 100) + : "fit-content"; + + String textAlign = convertAlignmentToCss(cue.textAlignment); + String writingMode = convertVerticalTypeToCss(cue.verticalType); + String cueTextSizeCssPx = convertTextSizeToCss(cue.textSizeType, cue.textSize); + String windowCssColor = + HtmlUtils.toCssRgba( + cue.windowColorSet && applyEmbeddedStyles ? cue.windowColor : style.windowColor); + + String positionProperty; + String lineProperty; + switch (cue.verticalType) { + case Cue.VERTICAL_TYPE_LR: + lineProperty = "left"; + positionProperty = "top"; + break; + case Cue.VERTICAL_TYPE_RL: + lineProperty = "right"; + positionProperty = "top"; + break; + case Cue.TYPE_UNSET: + default: + lineProperty = "top"; + positionProperty = "left"; + } + + String sizeProperty; + int horizontalTranslatePercent; + int verticalTranslatePercent; + if (cue.verticalType == Cue.VERTICAL_TYPE_LR || cue.verticalType == Cue.VERTICAL_TYPE_RL) { + sizeProperty = "height"; + horizontalTranslatePercent = lineTranslatePercent + lineAnchorTranslatePercent; + verticalTranslatePercent = positionAnchorTranslatePercent; + } else { + sizeProperty = "width"; + horizontalTranslatePercent = positionAnchorTranslatePercent; + verticalTranslatePercent = lineTranslatePercent + lineAnchorTranslatePercent; + } + + html.append( + Util.formatInvariant( + "
                ", + positionProperty, + positionPercent, + lineProperty, + linePercent, + sizeProperty, + size, + textAlign, + writingMode, + cueTextSizeCssPx, + windowCssColor, + horizontalTranslatePercent, + verticalTranslatePercent)) + .append(Util.formatInvariant("", backgroundColorCss)) + .append( + SpannedToHtmlConverter.convert( + cue.text, getContext().getResources().getDisplayMetrics().density)) + .append("") + .append("
                "); + } + + html.append("
                "); + + webView.loadData( + Base64.encodeToString( + html.toString().getBytes(Charset.forName(C.UTF8_NAME)), Base64.NO_PADDING), + "text/html", + "base64"); + } + + /** + * Converts a text size to a CSS px value. + * + *

                First converts to Android px using {@link SubtitleViewUtils#resolveTextSize(int, float, int, + * int)}. + * + *

                Then divides by {@link DisplayMetrics#density} to convert from Android px to dp because + * WebView treats one CSS px as one Android dp. + */ + private String convertTextSizeToCss(@Cue.TextSizeType int type, float size) { + float sizePx = + SubtitleViewUtils.resolveTextSize( + type, size, getHeight(), getHeight() - getPaddingTop() - getPaddingBottom()); + if (sizePx == Cue.DIMEN_UNSET) { + return "unset"; + } + float sizeDp = sizePx / getContext().getResources().getDisplayMetrics().density; + return Util.formatInvariant("%.2fpx", sizeDp); + } + + private static String convertVerticalTypeToCss(@Cue.VerticalType int verticalType) { + switch (verticalType) { + case Cue.VERTICAL_TYPE_LR: + return "vertical-lr"; + case Cue.VERTICAL_TYPE_RL: + return "vertical-rl"; + case Cue.TYPE_UNSET: + default: + return "horizontal-tb"; + } + } + + private static String convertAlignmentToCss(@Nullable Layout.Alignment alignment) { + if (alignment == null) { + return "center"; + } + switch (alignment) { + case ALIGN_NORMAL: + return "start"; + case ALIGN_OPPOSITE: + return "end"; + case ALIGN_CENTER: + default: + return "center"; + } + } + + /** + * Converts a {@link Cue.AnchorType} to a percentage for use in a CSS {@code transform: + * translate(x,y)} function. + * + *

                We use {@code position: absolute} and always use the same CSS positioning property (top, + * bottom, left, right) regardless of the anchor type. The anchor is effectively 'moved' by using + * a CSS {@code translate(x,y)} operation on the value returned from this function. + */ + private static int anchorTypeToTranslatePercent(@Cue.AnchorType int anchorType) { + switch (anchorType) { + case Cue.ANCHOR_TYPE_END: + return -100; + case Cue.ANCHOR_TYPE_MIDDLE: + return -50; + case Cue.ANCHOR_TYPE_START: + case Cue.TYPE_UNSET: + default: + return 0; + } + } +} 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 79990e53a6..b47feb2a71 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 @@ -326,7 +326,7 @@ public class TrackSelectionView extends LinearLayout { private void onTrackViewClicked(View view) { isDisabled = false; @SuppressWarnings("unchecked") - Pair tag = (Pair) view.getTag(); + Pair tag = (Pair) Assertions.checkNotNull(view.getTag()); int groupIndex = tag.first; int trackIndex = tag.second; SelectionOverride override = overrides.get(groupIndex); @@ -371,7 +371,8 @@ public class TrackSelectionView extends LinearLayout { private boolean shouldEnableAdaptiveSelection(int groupIndex) { return allowAdaptiveSelections && trackGroups.get(groupIndex).length > 1 - && mappedTrackInfo.getAdaptiveSupport(rendererIndex, groupIndex, false) + && mappedTrackInfo.getAdaptiveSupport( + rendererIndex, groupIndex, /* includeCapabilitiesExceededTracks= */ false) != RendererCapabilities.ADAPTIVE_NOT_SUPPORTED; } 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 index 01fa6837ea..5080e86345 100644 --- 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 @@ -60,8 +60,6 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; // Methods called on any thread. - // the constructor does not initialize fields: lastProjectionData - @SuppressWarnings("nullness:initialization.fields.uninitialized") public SceneRenderer() { frameAvailable = new AtomicBoolean(); resetRotationAtNextFrame = new AtomicBoolean(true); diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/spherical/SphericalGLSurfaceView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/spherical/SphericalGLSurfaceView.java index c01fccf54b..1c96f41df5 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/spherical/SphericalGLSurfaceView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/spherical/SphericalGLSurfaceView.java @@ -72,6 +72,9 @@ public final class SphericalGLSurfaceView extends GLSurfaceView { @Nullable private SurfaceTexture surfaceTexture; @Nullable private Surface surface; @Nullable private Player.VideoComponent videoComponent; + private boolean useSensorRotation; + private boolean isStarted; + private boolean isOrientationListenerRegistered; public SphericalGLSurfaceView(Context context) { this(context, null); @@ -104,6 +107,7 @@ public final class SphericalGLSurfaceView extends GLSurfaceView { WindowManager windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); Display display = Assertions.checkNotNull(windowManager).getDefaultDisplay(); orientationListener = new OrientationListener(display, touchTracker, renderer); + useSensorRotation = true; setEGLContextClientVersion(2); setRenderer(renderer); @@ -145,20 +149,23 @@ public final class SphericalGLSurfaceView extends GLSurfaceView { touchTracker.setSingleTapListener(listener); } + /** Sets whether to use the orientation sensor for rotation (if available). */ + public void setUseSensorRotation(boolean useSensorRotation) { + this.useSensorRotation = useSensorRotation; + updateOrientationListenerRegistration(); + } + @Override public void onResume() { super.onResume(); - if (orientationSensor != null) { - sensorManager.registerListener( - orientationListener, orientationSensor, SensorManager.SENSOR_DELAY_FASTEST); - } + isStarted = true; + updateOrientationListenerRegistration(); } @Override public void onPause() { - if (orientationSensor != null) { - sensorManager.unregisterListener(orientationListener); - } + isStarted = false; + updateOrientationListenerRegistration(); super.onPause(); } @@ -181,6 +188,20 @@ public final class SphericalGLSurfaceView extends GLSurfaceView { }); } + private void updateOrientationListenerRegistration() { + boolean enabled = useSensorRotation && isStarted; + if (orientationSensor == null || enabled == isOrientationListenerRegistered) { + return; + } + if (enabled) { + sensorManager.registerListener( + orientationListener, orientationSensor, SensorManager.SENSOR_DELAY_FASTEST); + } else { + sensorManager.unregisterListener(orientationListener); + } + isOrientationListenerRegistered = enabled; + } + // Called on GL thread. private void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture) { mainHandler.post( diff --git a/library/ui/src/main/res/layout/exo_playback_control_view.xml b/library/ui/src/main/res/layout/exo_playback_control_view.xml deleted file mode 100644 index acfddf1146..0000000000 --- a/library/ui/src/main/res/layout/exo_playback_control_view.xml +++ /dev/null @@ -1,96 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/library/ui/src/main/res/layout/exo_player_control_view.xml b/library/ui/src/main/res/layout/exo_player_control_view.xml index fd221e5d84..acfddf1146 100644 --- a/library/ui/src/main/res/layout/exo_player_control_view.xml +++ b/library/ui/src/main/res/layout/exo_player_control_view.xml @@ -1,5 +1,5 @@ - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/library/ui/src/main/res/layout/exo_player_view.xml b/library/ui/src/main/res/layout/exo_player_view.xml index dc6dda1667..65dea9271e 100644 --- a/library/ui/src/main/res/layout/exo_player_view.xml +++ b/library/ui/src/main/res/layout/exo_player_view.xml @@ -1,5 +1,5 @@ - - - + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/library/ui/src/main/res/layout/exo_simple_player_view.xml b/library/ui/src/main/res/layout/exo_simple_player_view.xml deleted file mode 100644 index 65dea9271e..0000000000 --- a/library/ui/src/main/res/layout/exo_simple_player_view.xml +++ /dev/null @@ -1,67 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/library/ui/src/main/res/values/attrs.xml b/library/ui/src/main/res/values/attrs.xml index 535bf320fb..e0a6b7faf4 100644 --- a/library/ui/src/main/res/values/attrs.xml +++ b/library/ui/src/main/res/values/attrs.xml @@ -77,8 +77,8 @@ + - diff --git a/library/core/src/test/java/com/google/android/exoplayer2/extractor/mp3/Mp3ExtractorTest.java b/library/ui/src/test/java/com/google/android/exoplayer2/ui/HtmlUtilsTest.java similarity index 55% rename from library/core/src/test/java/com/google/android/exoplayer2/extractor/mp3/Mp3ExtractorTest.java rename to library/ui/src/test/java/com/google/android/exoplayer2/ui/HtmlUtilsTest.java index 670296cc13..82ddfb4202 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/extractor/mp3/Mp3ExtractorTest.java +++ b/library/ui/src/test/java/com/google/android/exoplayer2/ui/HtmlUtilsTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2016 The Android Open Source Project + * Copyright (C) 2020 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,24 +13,28 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.google.android.exoplayer2.extractor.mp3; +package com.google.android.exoplayer2.ui; +import static com.google.common.truth.Truth.assertThat; + +import android.graphics.Color; import androidx.test.ext.junit.runners.AndroidJUnit4; -import com.google.android.exoplayer2.testutil.ExtractorAsserts; import org.junit.Test; import org.junit.runner.RunWith; -/** Unit test for {@link Mp3Extractor}. */ +/** Tests for {@link HtmlUtils}. */ @RunWith(AndroidJUnit4.class) -public final class Mp3ExtractorTest { +public class HtmlUtilsTest { @Test - public void testMp3Sample() throws Exception { - ExtractorAsserts.assertBehavior(Mp3Extractor::new, "mp3/bear.mp3"); + public void toCssRgba_exactAlpha() { + String cssRgba = HtmlUtils.toCssRgba(Color.argb(51, 13, 23, 37)); + assertThat(cssRgba).isEqualTo("rgba(13,23,37,0.200)"); } @Test - public void testTrimmedMp3Sample() throws Exception { - ExtractorAsserts.assertBehavior(Mp3Extractor::new, "mp3/play-trimmed.mp3"); + public void toCssRgba_truncatedAlpha() { + String cssRgba = HtmlUtils.toCssRgba(Color.argb(100, 13, 23, 37)); + assertThat(cssRgba).isEqualTo("rgba(13,23,37,0.392)"); } } diff --git a/library/ui/src/test/java/com/google/android/exoplayer2/ui/SpannedToHtmlConverterTest.java b/library/ui/src/test/java/com/google/android/exoplayer2/ui/SpannedToHtmlConverterTest.java new file mode 100644 index 0000000000..ad62506324 --- /dev/null +++ b/library/ui/src/test/java/com/google/android/exoplayer2/ui/SpannedToHtmlConverterTest.java @@ -0,0 +1,377 @@ +/* + * Copyright (C) 2020 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; + +import static com.google.common.truth.Truth.assertThat; + +import android.graphics.Color; +import android.graphics.Typeface; +import android.text.SpannableString; +import android.text.Spanned; +import android.text.style.AbsoluteSizeSpan; +import android.text.style.BackgroundColorSpan; +import android.text.style.ForegroundColorSpan; +import android.text.style.RelativeSizeSpan; +import android.text.style.StyleSpan; +import android.text.style.TypefaceSpan; +import android.text.style.UnderlineSpan; +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.android.exoplayer2.text.span.HorizontalTextInVerticalContextSpan; +import com.google.android.exoplayer2.text.span.RubySpan; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.annotation.Config; + +/** Tests for {@link SpannedToHtmlConverter}. */ +@RunWith(AndroidJUnit4.class) +public class SpannedToHtmlConverterTest { + + private final float displayDensity; + + public SpannedToHtmlConverterTest() { + displayDensity = + ApplicationProvider.getApplicationContext().getResources().getDisplayMetrics().density; + } + + @Test + public void convert_supportsForegroundColorSpan() { + SpannableString spanned = new SpannableString("String with colored section"); + spanned.setSpan( + new ForegroundColorSpan(Color.argb(51, 64, 32, 16)), + "String with ".length(), + "String with colored".length(), + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + + String html = SpannedToHtmlConverter.convert(spanned, displayDensity); + + assertThat(html) + .isEqualTo("String with colored section"); + } + + @Test + public void convert_supportsBackgroundColorSpan() { + SpannableString spanned = new SpannableString("String with highlighted section"); + spanned.setSpan( + new BackgroundColorSpan(Color.argb(51, 64, 32, 16)), + "String with ".length(), + "String with highlighted".length(), + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + + String html = SpannedToHtmlConverter.convert(spanned, displayDensity); + + assertThat(html) + .isEqualTo( + "String with highlighted" + + " section"); + } + + @Test + public void convert_supportsHorizontalTextInVerticalContextSpan() { + SpannableString spanned = new SpannableString("Vertical text with 123 horizontal numbers"); + spanned.setSpan( + new HorizontalTextInVerticalContextSpan(), + "Vertical text with ".length(), + "Vertical text with 123".length(), + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + + String html = SpannedToHtmlConverter.convert(spanned, displayDensity); + + assertThat(html) + .isEqualTo( + "Vertical text with 123 " + + "horizontal numbers"); + } + + // Set the screen density so we see that px are handled differently to dp. + @Config(qualifiers = "xhdpi") + @Test + public void convert_supportsAbsoluteSizeSpan_px() { + SpannableString spanned = new SpannableString("String with 10px section"); + spanned.setSpan( + new AbsoluteSizeSpan(/* size= */ 10, /* dip= */ false), + "String with ".length(), + "String with 10px".length(), + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + + String html = SpannedToHtmlConverter.convert(spanned, displayDensity); + + // 10 Android px are converted to 5 CSS px because WebView treats 1 CSS px as 1 Android dp + // and we're using screen density xhdpi i.e. density=2. + assertThat(html).isEqualTo("String with 10px section"); + } + + // Set the screen density so we see that px are handled differently to dp. + @Config(qualifiers = "xhdpi") + @Test + public void convert_supportsAbsoluteSizeSpan_dp() { + SpannableString spanned = new SpannableString("String with 10dp section"); + spanned.setSpan( + new AbsoluteSizeSpan(/* size= */ 10, /* dip= */ true), + "String with ".length(), + "String with 10dp".length(), + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + + String html = SpannedToHtmlConverter.convert(spanned, displayDensity); + + assertThat(html).isEqualTo("String with 10dp section"); + } + + @Test + public void convert_supportsRelativeSizeSpan() { + SpannableString spanned = new SpannableString("String with 10% section"); + spanned.setSpan( + new RelativeSizeSpan(/* proportion= */ 0.1f), + "String with ".length(), + "String with 10%".length(), + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + + String html = SpannedToHtmlConverter.convert(spanned, displayDensity); + + assertThat(html).isEqualTo("String with 10% section"); + } + + @Test + public void convert_supportsTypefaceSpan() { + SpannableString spanned = new SpannableString("String with Times New Roman section"); + spanned.setSpan( + new TypefaceSpan(/* family= */ "Times New Roman"), + "String with ".length(), + "String with Times New Roman".length(), + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + + String html = SpannedToHtmlConverter.convert(spanned, displayDensity); + + assertThat(html) + .isEqualTo( + "String with Times New Roman" + + " section"); + } + + @Test + public void convert_supportsTypefaceSpan_nullFamily() { + SpannableString spanned = new SpannableString("String with unstyled section"); + spanned.setSpan( + new TypefaceSpan(/* family= */ (String) null), + "String with ".length(), + "String with unstyled".length(), + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + + String html = SpannedToHtmlConverter.convert(spanned, displayDensity); + + assertThat(html).isEqualTo("String with unstyled section"); + } + + @Test + public void convert_supportsStyleSpan() { + SpannableString spanned = + new SpannableString("String with bold, italic and bold-italic sections."); + spanned.setSpan( + new StyleSpan(Typeface.BOLD), + "String with ".length(), + "String with bold".length(), + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + spanned.setSpan( + new StyleSpan(Typeface.ITALIC), + "String with bold, ".length(), + "String with bold, italic".length(), + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + spanned.setSpan( + new StyleSpan(Typeface.BOLD_ITALIC), + "String with bold, italic and ".length(), + "String with bold, italic and bold-italic".length(), + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + + String html = SpannedToHtmlConverter.convert(spanned, displayDensity); + + assertThat(html) + .isEqualTo( + "String with bold, italic and bold-italic sections."); + } + + @Test + public void convert_supportsRubySpan() { + SpannableString spanned = + new SpannableString("String with over-annotated and under-annotated section"); + spanned.setSpan( + new RubySpan("ruby-text", RubySpan.POSITION_OVER), + "String with ".length(), + "String with over-annotated".length(), + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + spanned.setSpan( + new RubySpan("non-àscìì-text", RubySpan.POSITION_UNDER), + "String with over-annotated and ".length(), + "String with over-annotated and under-annotated".length(), + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + + String html = SpannedToHtmlConverter.convert(spanned, displayDensity); + + assertThat(html) + .isEqualTo( + "String with " + + "" + + "over-annotated" + + "ruby-text" + + " " + + "and " + + "" + + "under-annotated" + + "non-àscìì-text" + + " " + + "section"); + } + + @Test + public void convert_supportsUnderlineSpan() { + SpannableString spanned = new SpannableString("String with underlined section."); + spanned.setSpan( + new UnderlineSpan(), + "String with ".length(), + "String with underlined".length(), + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + + String html = SpannedToHtmlConverter.convert(spanned, displayDensity); + + assertThat(html).isEqualTo("String with underlined section."); + } + + @Test + public void convert_escapesHtmlInUnspannedString() { + String html = SpannedToHtmlConverter.convert("String with bold tags", displayDensity); + + assertThat(html).isEqualTo("String with <b>bold</b> tags"); + } + + @Test + public void convert_handlesLinebreakInUnspannedString() { + String html = + SpannedToHtmlConverter.convert( + "String with\nnew line and\r\ncrlf style too", displayDensity); + + assertThat(html).isEqualTo("String with
                new line and
                crlf style too"); + } + + @Test + public void convert_doesntConvertAmpersandLineFeedToBrTag() { + String html = + SpannedToHtmlConverter.convert("String with new line ampersand code", displayDensity); + + assertThat(html).isEqualTo("String with&#10;new line ampersand code"); + } + + @Test + public void convert_escapesUnrecognisedTagInSpannedString() { + SpannableString spanned = new SpannableString("String with unrecognised tags"); + spanned.setSpan( + new StyleSpan(Typeface.ITALIC), + "String with ".length(), + "String with unrecognised".length(), + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + + String html = SpannedToHtmlConverter.convert(spanned, displayDensity); + + assertThat(html).isEqualTo("String with <foo>unrecognised</foo> tags"); + } + + @Test + public void convert_handlesLinebreakInSpannedString() { + String html = + SpannedToHtmlConverter.convert( + "String with\nnew line and\r\ncrlf style too", displayDensity); + + assertThat(html).isEqualTo("String with
                new line and
                crlf style too"); + } + + @Test + public void convert_convertsNonAsciiCharactersToAmpersandCodes() { + String html = + SpannedToHtmlConverter.convert( + new SpannableString("Strìng with 優しいの non-ASCII characters"), displayDensity); + + assertThat(html) + .isEqualTo("Strìng with 優しいの non-ASCII characters"); + } + + @Test + public void convert_ignoresUnrecognisedSpan() { + SpannableString spanned = new SpannableString("String with unrecognised span"); + spanned.setSpan( + new Object() { + @Override + public String toString() { + return "Force an anonymous class to be created"; + } + }, + "String with ".length(), + "String with unrecognised".length(), + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + + String html = SpannedToHtmlConverter.convert(spanned, displayDensity); + + assertThat(html).isEqualTo("String with unrecognised span"); + } + + @Test + public void convert_sortsTagsConsistently() { + SpannableString spanned = new SpannableString("String with italic-bold-underlined section"); + int start = "String with ".length(); + int end = "String with italic-bold-underlined".length(); + spanned.setSpan(new StyleSpan(Typeface.BOLD), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + spanned.setSpan(new StyleSpan(Typeface.ITALIC), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + spanned.setSpan(new UnderlineSpan(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + + String html = SpannedToHtmlConverter.convert(spanned, displayDensity); + + assertThat(html).isEqualTo("String with italic-bold-underlined section"); + } + + @Test + public void convert_supportsNestedTags() { + SpannableString spanned = new SpannableString("String with italic and bold section"); + int start = "String with ".length(); + int end = "String with italic and bold".length(); + spanned.setSpan(new StyleSpan(Typeface.ITALIC), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + spanned.setSpan( + new StyleSpan(Typeface.BOLD), + "String with italic and ".length(), + "String with italic and bold".length(), + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + + String html = SpannedToHtmlConverter.convert(spanned, displayDensity); + + assertThat(html).isEqualTo("String with italic and bold section"); + } + + @Test + public void convert_overlappingSpans_producesInvalidHtml() { + SpannableString spanned = new SpannableString("String with italic and bold section"); + spanned.setSpan( + new StyleSpan(Typeface.ITALIC), + 0, + "String with italic and bold".length(), + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + spanned.setSpan( + new StyleSpan(Typeface.BOLD), + "String with italic ".length(), + "String with italic and bold section".length(), + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + + String html = SpannedToHtmlConverter.convert(spanned, displayDensity); + + assertThat(html).isEqualTo("String with italic and bold section"); + } +} diff --git a/library/ui/src/test/java/com/google/android/exoplayer2/ui/spherical/TouchTrackerTest.java b/library/ui/src/test/java/com/google/android/exoplayer2/ui/spherical/TouchTrackerTest.java index 6212a74f30..8147ae89a0 100644 --- a/library/ui/src/test/java/com/google/android/exoplayer2/ui/spherical/TouchTrackerTest.java +++ b/library/ui/src/test/java/com/google/android/exoplayer2/ui/spherical/TouchTrackerTest.java @@ -59,7 +59,7 @@ public class TouchTrackerTest { } @Test - public void testTap() { + public void tap() { // Tap is a noop. swipe(tracker, 0, 0, 0, 0); assertThat(yaw).isWithin(EPSILON).of(0); @@ -67,21 +67,21 @@ public class TouchTrackerTest { } @Test - public void testBasicYaw() { + public void basicYaw() { swipe(tracker, 0, 0, SWIPE_PX, 0); assertThat(yaw).isWithin(EPSILON).of(-SWIPE_PX / PX_PER_DEGREES); assertThat(pitch).isWithin(EPSILON).of(0); } @Test - public void testBigYaw() { + public void bigYaw() { swipe(tracker, 0, 0, -10 * SWIPE_PX, 0); assertThat(yaw).isEqualTo(10 * SWIPE_PX / PX_PER_DEGREES); assertThat(pitch).isWithin(EPSILON).of(0); } @Test - public void testYawUnaffectedByPitch() { + public void yawUnaffectedByPitch() { swipe(tracker, 0, 0, 0, SWIPE_PX); assertThat(yaw).isWithin(EPSILON).of(0); @@ -90,14 +90,14 @@ public class TouchTrackerTest { } @Test - public void testBasicPitch() { + public void basicPitch() { swipe(tracker, 0, 0, 0, SWIPE_PX); assertThat(yaw).isWithin(EPSILON).of(0); assertThat(pitch).isWithin(EPSILON).of(SWIPE_PX / PX_PER_DEGREES); } @Test - public void testPitchClipped() { + public void pitchClipped() { // Big reverse pitch should be clipped. swipe(tracker, 0, 0, 0, -20 * SWIPE_PX); assertThat(yaw).isWithin(EPSILON).of(0); @@ -110,7 +110,7 @@ public class TouchTrackerTest { } @Test - public void testWithRoll90() { + public void withRoll90() { tracker.onOrientationChange(dummyMatrix, (float) Math.toRadians(90)); // Y-axis should now control yaw. @@ -123,7 +123,7 @@ public class TouchTrackerTest { } @Test - public void testWithRoll180() { + public void withRoll180() { tracker.onOrientationChange(dummyMatrix, (float) Math.toRadians(180)); // X-axis should now control reverse yaw. @@ -136,7 +136,7 @@ public class TouchTrackerTest { } @Test - public void testWithRoll270() { + public void withRoll270() { tracker.onOrientationChange(dummyMatrix, (float) Math.toRadians(270)); // Y-axis should now control reverse yaw. diff --git a/playbacktests/src/androidTest/AndroidManifest.xml b/playbacktests/src/androidTest/AndroidManifest.xml index b6c6064227..2c2099496a 100644 --- a/playbacktests/src/androidTest/AndroidManifest.xml +++ b/playbacktests/src/androidTest/AndroidManifest.xml @@ -26,12 +26,9 @@ - - - buildDrmSessionManager( - final String userAgent) { + protected DrmSessionManager buildDrmSessionManager(final String userAgent) { if (widevineLicenseUrl == null) { return DrmSessionManager.getDummyDrmSessionManager(); } @@ -265,12 +265,12 @@ import java.util.List; MediaDrmCallback drmCallback = new HttpMediaDrmCallback(widevineLicenseUrl, new DefaultHttpDataSourceFactory(userAgent)); FrameworkMediaDrm frameworkMediaDrm = FrameworkMediaDrm.newInstance(WIDEVINE_UUID); - DefaultDrmSessionManager drmSessionManager = - new DefaultDrmSessionManager<>( + DefaultDrmSessionManager drmSessionManager = + new DefaultDrmSessionManager( C.WIDEVINE_UUID, frameworkMediaDrm, drmCallback, - /* optionalKeyRequestParameters= */ null, + /* keyRequestParameters= */ null, /* multiSession= */ false, DefaultDrmSessionManager.INITIAL_DRM_REQUEST_RETRY_COUNT); if (!useL1Widevine) { @@ -299,7 +299,10 @@ import java.util.List; @Override protected MediaSource buildSource( - HostActivity host, String userAgent, DrmSessionManager drmSessionManager) { + HostActivity host, + String userAgent, + DrmSessionManager drmSessionManager, + FrameLayout overlayFrameLayout) { DataSource.Factory dataSourceFactory = this.dataSourceFactory != null ? this.dataSourceFactory @@ -312,7 +315,7 @@ import java.util.List; } @Override - protected void onTestFinished(DecoderCounters audioCounters, DecoderCounters videoCounters) { + protected void logMetrics(DecoderCounters audioCounters, DecoderCounters videoCounters) { metricsLogger.logMetric(MetricsLogger.KEY_TEST_NAME, streamName); metricsLogger.logMetric(MetricsLogger.KEY_IS_CDD_LIMITED_RETRY, isCddLimitedRetry); metricsLogger.logMetric(MetricsLogger.KEY_FRAMES_DROPPED_COUNT, @@ -324,7 +327,10 @@ import java.util.List; metricsLogger.logMetric(MetricsLogger.KEY_FRAMES_RENDERED_COUNT, videoCounters.renderedOutputBufferCount); metricsLogger.close(); + } + @Override + protected void assertPassed(DecoderCounters audioCounters, DecoderCounters videoCounters) { if (fullPlaybackNoSeeking) { // We shouldn't have skipped any output buffers. DecoderCountersUtil @@ -371,7 +377,9 @@ import java.util.List; private DashTestTrackSelector(String tag, String audioFormatId, String[] videoFormatIds, boolean canIncludeAdditionalVideoFormats) { - super(new RandomTrackSelection.Factory(/* seed= */ 0)); + super( + ApplicationProvider.getApplicationContext(), + new RandomTrackSelection.Factory(/* seed= */ 0)); this.tag = tag; this.audioFormatId = audioFormatId; this.videoFormatIds = videoFormatIds; @@ -451,17 +459,17 @@ import java.util.List; } private static boolean isFormatHandled(int formatSupport) { - return (formatSupport & RendererCapabilities.FORMAT_SUPPORT_MASK) + return RendererCapabilities.getFormatSupport(formatSupport) == RendererCapabilities.FORMAT_HANDLED; } } /** - * Creates a new {@code MediaDrm} object. The encapsulation ensures that the tests can be - * executed for API level < 18. + * Creates a new {@code MediaDrm} object. The encapsulation ensures that the tests can be executed + * for API level < 18. */ - @TargetApi(18) + @RequiresApi(18) private static final class MediaDrmBuilder { public static MediaDrm build () { diff --git a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashWidevineOfflineTest.java b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashWidevineOfflineTest.java index efc6c011cc..40ec1ed9bb 100644 --- a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashWidevineOfflineTest.java +++ b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashWidevineOfflineTest.java @@ -17,7 +17,7 @@ package com.google.android.exoplayer2.playbacktests.gts; import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertWithMessage; -import static org.junit.Assert.fail; +import static org.junit.Assert.assertThrows; import android.media.MediaDrm.MediaDrmStateException; import android.net.Uri; @@ -27,7 +27,6 @@ import androidx.test.rule.ActivityTestRule; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.drm.DrmInitData; import com.google.android.exoplayer2.drm.DrmSession.DrmSessionException; -import com.google.android.exoplayer2.drm.FrameworkMediaCrypto; import com.google.android.exoplayer2.drm.OfflineLicenseHelper; import com.google.android.exoplayer2.source.dash.DashUtil; import com.google.android.exoplayer2.source.dash.manifest.DashManifest; @@ -35,6 +34,7 @@ import com.google.android.exoplayer2.testutil.ActionSchedule; import com.google.android.exoplayer2.testutil.HostActivity; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory; +import com.google.android.exoplayer2.util.MediaSourceEventDispatcher; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.Util; import java.io.IOException; @@ -53,7 +53,7 @@ public final class DashWidevineOfflineTest { private DashTestRunner testRunner; private DefaultHttpDataSourceFactory httpDataSourceFactory; - private OfflineLicenseHelper offlineLicenseHelper; + private OfflineLicenseHelper offlineLicenseHelper; private byte[] offlineLicenseKeySetId; @Rule public ActivityTestRule testRule = new ActivityTestRule<>(HostActivity.class); @@ -75,8 +75,9 @@ public final class DashWidevineOfflineTest { String widevineLicenseUrl = DashTestData.getWidevineLicenseUrl(true, useL1Widevine); httpDataSourceFactory = new DefaultHttpDataSourceFactory(USER_AGENT); if (Util.SDK_INT >= 18) { - offlineLicenseHelper = OfflineLicenseHelper.newWidevineInstance(widevineLicenseUrl, - httpDataSourceFactory); + offlineLicenseHelper = + OfflineLicenseHelper.newWidevineInstance( + widevineLicenseUrl, httpDataSourceFactory, new MediaSourceEventDispatcher()); } } @@ -96,7 +97,7 @@ public final class DashWidevineOfflineTest { // Offline license tests @Test - public void testWidevineOfflineLicenseV22() throws Exception { + public void widevineOfflineLicenseV22() throws Exception { if (Util.SDK_INT < 22) { return; // Pass. } @@ -109,34 +110,52 @@ public final class DashWidevineOfflineTest { } @Test - public void testWidevineOfflineReleasedLicenseV22() throws Throwable { - if (Util.SDK_INT < 22) { + public void widevineOfflineReleasedLicenseV22() throws Throwable { + if (Util.SDK_INT < 22 || Util.SDK_INT > 28) { return; // Pass. } downloadLicense(); releaseLicense(); // keySetId no longer valid. - try { - testRunner.run(); - fail("Playback should fail because the license has been released."); - } catch (Throwable e) { - // Get the root cause - while (true) { - Throwable cause = e.getCause(); - if (cause == null || cause == e) { - break; - } - e = cause; - } - // It should be a MediaDrmStateException instance - if (!(e instanceof MediaDrmStateException)) { - throw e; - } + Throwable error = + assertThrows( + "Playback should fail because the license has been released.", + Throwable.class, + () -> testRunner.run()); + + // Get the root cause + Throwable cause = error.getCause(); + while (cause != null && cause != error) { + error = cause; + cause = error.getCause(); } + assertThat(error).isInstanceOf(MediaDrmStateException.class); } @Test - public void testWidevineOfflineExpiredLicenseV22() throws Exception { + public void widevineOfflineReleasedLicenseV29() throws Throwable { + if (Util.SDK_INT < 29) { + return; // Pass. + } + downloadLicense(); + releaseLicense(); // keySetId no longer valid. + + Throwable error = + assertThrows( + "Playback should fail because the license has been released.", + Throwable.class, + () -> testRunner.run()); + // Get the root cause + Throwable cause = error.getCause(); + while (cause != null && cause != error) { + error = cause; + cause = error.getCause(); + } + assertThat(error).isInstanceOf(IllegalArgumentException.class); + } + + @Test + public void widevineOfflineExpiredLicenseV22() throws Exception { if (Util.SDK_INT < 22) { return; // Pass. } @@ -166,7 +185,7 @@ public final class DashWidevineOfflineTest { } @Test - public void testWidevineOfflineLicenseExpiresOnPauseV22() throws Exception { + public void widevineOfflineLicenseExpiresOnPauseV22() throws Exception { if (Util.SDK_INT < 22) { return; // Pass. } @@ -188,7 +207,7 @@ public final class DashWidevineOfflineTest { testRunner.setActionSchedule(schedule).run(); } - private void downloadLicense() throws InterruptedException, DrmSessionException, IOException { + private void downloadLicense() throws IOException { DataSource dataSource = httpDataSourceFactory.createDataSource(); DashManifest dashManifest = DashUtil.loadManifest(dataSource, Uri.parse(DashTestData.WIDEVINE_H264_MANIFEST)); diff --git a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DebugRenderersFactory.java b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DebugRenderersFactory.java index affd762f61..04b15f5240 100644 --- a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DebugRenderersFactory.java +++ b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DebugRenderersFactory.java @@ -15,20 +15,18 @@ */ package com.google.android.exoplayer2.playbacktests.gts; -import android.annotation.TargetApi; import android.content.Context; import android.media.MediaCodec; import android.media.MediaCrypto; import android.os.Handler; import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; import com.google.android.exoplayer2.DefaultRenderersFactory; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.FormatHolder; import com.google.android.exoplayer2.Renderer; import com.google.android.exoplayer2.decoder.DecoderInputBuffer; -import com.google.android.exoplayer2.drm.DrmSessionManager; -import com.google.android.exoplayer2.drm.FrameworkMediaCrypto; import com.google.android.exoplayer2.mediacodec.MediaCodecInfo; import com.google.android.exoplayer2.mediacodec.MediaCodecSelector; import com.google.android.exoplayer2.video.MediaCodecVideoRenderer; @@ -53,8 +51,6 @@ import java.util.ArrayList; Context context, @ExtensionRendererMode int extensionRendererMode, MediaCodecSelector mediaCodecSelector, - @Nullable DrmSessionManager drmSessionManager, - boolean playClearSamplesWithoutKeys, boolean enableDecoderFallback, Handler eventHandler, VideoRendererEventListener eventListener, @@ -65,8 +61,6 @@ import java.util.ArrayList; context, mediaCodecSelector, allowedVideoJoiningTimeMs, - drmSessionManager, - playClearSamplesWithoutKeys, eventHandler, eventListener, MAX_DROPPED_VIDEO_FRAME_COUNT_TO_NOTIFY)); @@ -78,6 +72,7 @@ import java.util.ArrayList; */ private static class DebugMediaCodecVideoRenderer extends MediaCodecVideoRenderer { + private static final String TAG = "DebugMediaCodecVideoRenderer"; private static final int ARRAY_SIZE = 1000; private final long[] timestampsList = new long[ARRAY_SIZE]; @@ -92,8 +87,6 @@ import java.util.ArrayList; Context context, MediaCodecSelector mediaCodecSelector, long allowedJoiningTimeMs, - DrmSessionManager drmSessionManager, - boolean playClearSamplesWithoutKeys, Handler eventHandler, VideoRendererEventListener eventListener, int maxDroppedFrameCountToNotify) { @@ -101,13 +94,16 @@ import java.util.ArrayList; context, mediaCodecSelector, allowedJoiningTimeMs, - drmSessionManager, - playClearSamplesWithoutKeys, eventHandler, eventListener, maxDroppedFrameCountToNotify); } + @Override + public String getName() { + return TAG; + } + @Override protected void configureCodec( MediaCodecInfo codecInfo, @@ -125,19 +121,15 @@ import java.util.ArrayList; } @Override - protected void releaseCodec() { - super.releaseCodec(); + protected void resetCodecStateForFlush() { + super.resetCodecStateForFlush(); clearTimestamps(); - skipToPositionBeforeRenderingFirstFrame = false; } @Override - protected boolean flushOrReleaseCodec() { - try { - return super.flushOrReleaseCodec(); - } finally { - clearTimestamps(); - } + protected void resetCodecStateForRelease() { + super.resetCodecStateForRelease(); + skipToPositionBeforeRenderingFirstFrame = false; } @Override @@ -159,10 +151,11 @@ import java.util.ArrayList; protected boolean processOutputBuffer( long positionUs, long elapsedRealtimeUs, - MediaCodec codec, + @Nullable MediaCodec codec, ByteBuffer buffer, int bufferIndex, int bufferFlags, + int sampleCount, long bufferPresentationTimeUs, boolean isDecodeOnlyBuffer, boolean isLastBuffer, @@ -181,6 +174,7 @@ import java.util.ArrayList; buffer, bufferIndex, bufferFlags, + sampleCount, bufferPresentationTimeUs, isDecodeOnlyBuffer, isLastBuffer, @@ -193,10 +187,10 @@ import java.util.ArrayList; super.renderOutputBuffer(codec, index, presentationTimeUs); } - @TargetApi(21) + @RequiresApi(21) @Override - protected void renderOutputBufferV21(MediaCodec codec, int index, long presentationTimeUs, - long releaseTimeNs) { + protected void renderOutputBufferV21( + MediaCodec codec, int index, long presentationTimeUs, long releaseTimeNs) { skipToPositionBeforeRenderingFirstFrame = false; super.renderOutputBufferV21(codec, index, presentationTimeUs, releaseTimeNs); } 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 index f7b376d7ad..a29b056edb 100644 --- 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 @@ -47,7 +47,7 @@ public class EnumerateDecodersTest { } @Test - public void testEnumerateDecoders() throws Exception { + public void enumerateDecoders() throws Exception { enumerateDecoders(MimeTypes.VIDEO_H263); enumerateDecoders(MimeTypes.VIDEO_H264); enumerateDecoders(MimeTypes.VIDEO_H265); diff --git a/settings.gradle b/settings.gradle index 39e4791bb5..946b5b78de 100644 --- a/settings.gradle +++ b/settings.gradle @@ -20,10 +20,12 @@ if (gradle.ext.has('exoplayerModulePrefix')) { include modulePrefix + 'demo' include modulePrefix + 'demo-cast' +include modulePrefix + 'demo-gl' include modulePrefix + 'demo-surface' include modulePrefix + 'playbacktests' project(modulePrefix + 'demo').projectDir = new File(rootDir, 'demos/main') project(modulePrefix + 'demo-cast').projectDir = new File(rootDir, 'demos/cast') +project(modulePrefix + 'demo-gl').projectDir = new File(rootDir, 'demos/gl') project(modulePrefix + 'demo-surface').projectDir = new File(rootDir, 'demos/surface') project(modulePrefix + 'playbacktests').projectDir = new File(rootDir, 'playbacktests') diff --git a/testdata/README.md b/testdata/README.md new file mode 100644 index 0000000000..911505926b --- /dev/null +++ b/testdata/README.md @@ -0,0 +1,4 @@ +# ExoPlayer test data # + +Provides sample data for ExoPlayer unit and instrumentation tests. + diff --git a/testdata/build.gradle b/testdata/build.gradle new file mode 100644 index 0000000000..372a01132e --- /dev/null +++ b/testdata/build.gradle @@ -0,0 +1,18 @@ +// Copyright 2020 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. +apply from: '../constants.gradle' +apply plugin: 'com.android.library' + +android.compileSdkVersion project.ext.compileSdkVersion + diff --git a/testdata/src/main/AndroidManifest.xml b/testdata/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..280235f11a --- /dev/null +++ b/testdata/src/main/AndroidManifest.xml @@ -0,0 +1,17 @@ + + + + diff --git a/testdata/src/test/assets/ad-responses/midroll10s_midroll20s.xml b/testdata/src/test/assets/ad-responses/midroll10s_midroll20s.xml new file mode 100644 index 0000000000..1543e11d26 --- /dev/null +++ b/testdata/src/test/assets/ad-responses/midroll10s_midroll20s.xml @@ -0,0 +1,64 @@ + + + + + + + + GDFP + Midroll + + + + + + + 00:00:05 + + + + + + + + + + + + + + + + + + + + + GDFP + Midroll + + + + + + + 00:00:05 + + + + + + + + + + + + + + + diff --git a/testdata/src/test/assets/ad-responses/midroll1s_midroll7s.xml b/testdata/src/test/assets/ad-responses/midroll1s_midroll7s.xml new file mode 100644 index 0000000000..7b693747fc --- /dev/null +++ b/testdata/src/test/assets/ad-responses/midroll1s_midroll7s.xml @@ -0,0 +1,64 @@ + + + + + + + + GDFP + Midroll + + + + + + + 00:00:05 + + + + + + + + + + + + + + + + + + + + + GDFP + Midroll + + + + + + + 00:00:05 + + + + + + + + + + + + + + + diff --git a/testdata/src/test/assets/ad-responses/preroll.xml b/testdata/src/test/assets/ad-responses/preroll.xml new file mode 100644 index 0000000000..3456649b29 --- /dev/null +++ b/testdata/src/test/assets/ad-responses/preroll.xml @@ -0,0 +1,26 @@ + + + + GDFP + Preroll + + + + + + + 00:00:05 + + + + + + + + + + + diff --git a/testdata/src/test/assets/ad-responses/preroll_midroll6s_postroll.xml b/testdata/src/test/assets/ad-responses/preroll_midroll6s_postroll.xml new file mode 100644 index 0000000000..bbf216bf12 --- /dev/null +++ b/testdata/src/test/assets/ad-responses/preroll_midroll6s_postroll.xml @@ -0,0 +1,95 @@ + + + + + + + + GDFP + Preroll + + + + + + + 00:00:05 + + + + + + + + + + + + + + + + + + + + + GDFP + Midroll + + + + + + + 00:00:05 + + + + + + + + + + + + + + + + + + + + + GDFP + Postroll + + + + + + + 00:00:05 + + + + + + + + + + + + + + + diff --git a/library/core/src/test/assets/amr/sample_nb.amr b/testdata/src/test/assets/amr/sample_nb.amr similarity index 100% rename from library/core/src/test/assets/amr/sample_nb.amr rename to testdata/src/test/assets/amr/sample_nb.amr diff --git a/library/core/src/test/assets/amr/sample_nb.amr.0.dump b/testdata/src/test/assets/amr/sample_nb.amr.0.dump similarity index 97% rename from library/core/src/test/assets/amr/sample_nb.amr.0.dump rename to testdata/src/test/assets/amr/sample_nb.amr.0.dump index e0dec9c62c..8430964566 100644 --- a/library/core/src/test/assets/amr/sample_nb.amr.0.dump +++ b/testdata/src/test/assets/amr/sample_nb.amr.0.dump @@ -4,29 +4,13 @@ seekMap: 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 + format 0: + sampleMimeType = audio/3gpp + maxInputSize = 61 + channelCount = 1 + sampleRate = 8000 sample 0: time = 0 flags = 1 diff --git a/library/core/src/test/assets/amr/sample_nb_cbr.amr.unklen.dump b/testdata/src/test/assets/amr/sample_nb.amr.unknown_length.dump similarity index 97% rename from library/core/src/test/assets/amr/sample_nb_cbr.amr.unklen.dump rename to testdata/src/test/assets/amr/sample_nb.amr.unknown_length.dump index e0dec9c62c..8430964566 100644 --- a/library/core/src/test/assets/amr/sample_nb_cbr.amr.unklen.dump +++ b/testdata/src/test/assets/amr/sample_nb.amr.unknown_length.dump @@ -4,29 +4,13 @@ seekMap: 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 + format 0: + sampleMimeType = audio/3gpp + maxInputSize = 61 + channelCount = 1 + sampleRate = 8000 sample 0: time = 0 flags = 1 diff --git a/library/core/src/test/assets/amr/sample_nb_cbr.amr b/testdata/src/test/assets/amr/sample_nb_cbr.amr similarity index 100% rename from library/core/src/test/assets/amr/sample_nb_cbr.amr rename to testdata/src/test/assets/amr/sample_nb_cbr.amr diff --git a/library/core/src/test/assets/amr/sample_nb_cbr.amr.0.dump b/testdata/src/test/assets/amr/sample_nb_cbr.amr.0.dump similarity index 97% rename from library/core/src/test/assets/amr/sample_nb_cbr.amr.0.dump rename to testdata/src/test/assets/amr/sample_nb_cbr.amr.0.dump index e8ba3c3588..ceef911773 100644 --- a/library/core/src/test/assets/amr/sample_nb_cbr.amr.0.dump +++ b/testdata/src/test/assets/amr/sample_nb_cbr.amr.0.dump @@ -2,31 +2,18 @@ seekMap: isSeekable = true duration = 4360000 getPosition(0) = [[timeUs=0, position=6]] + getPosition(1) = [[timeUs=0, position=6], [timeUs=20000, position=19]] + getPosition(2180000) = [[timeUs=2180000, position=1423]] + getPosition(4360000) = [[timeUs=4340000, position=2827]] 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 + format 0: + sampleMimeType = audio/3gpp + maxInputSize = 61 + channelCount = 1 + sampleRate = 8000 sample 0: time = 0 flags = 1 diff --git a/library/core/src/test/assets/amr/sample_nb_cbr.amr.1.dump b/testdata/src/test/assets/amr/sample_nb_cbr.amr.1.dump similarity index 97% rename from library/core/src/test/assets/amr/sample_nb_cbr.amr.1.dump rename to testdata/src/test/assets/amr/sample_nb_cbr.amr.1.dump index d00ae65c7e..a4cb365b4b 100644 --- a/library/core/src/test/assets/amr/sample_nb_cbr.amr.1.dump +++ b/testdata/src/test/assets/amr/sample_nb_cbr.amr.1.dump @@ -2,31 +2,18 @@ seekMap: isSeekable = true duration = 4360000 getPosition(0) = [[timeUs=0, position=6]] + getPosition(1) = [[timeUs=0, position=6], [timeUs=20000, position=19]] + getPosition(2180000) = [[timeUs=2180000, position=1423]] + getPosition(4360000) = [[timeUs=4340000, position=2827]] 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 + format 0: + sampleMimeType = audio/3gpp + maxInputSize = 61 + channelCount = 1 + sampleRate = 8000 sample 0: time = 1440000 flags = 1 diff --git a/library/core/src/test/assets/amr/sample_nb_cbr.amr.2.dump b/testdata/src/test/assets/amr/sample_nb_cbr.amr.2.dump similarity index 94% rename from library/core/src/test/assets/amr/sample_nb_cbr.amr.2.dump rename to testdata/src/test/assets/amr/sample_nb_cbr.amr.2.dump index f68b6df3a3..57fb75cc10 100644 --- a/library/core/src/test/assets/amr/sample_nb_cbr.amr.2.dump +++ b/testdata/src/test/assets/amr/sample_nb_cbr.amr.2.dump @@ -2,31 +2,18 @@ seekMap: isSeekable = true duration = 4360000 getPosition(0) = [[timeUs=0, position=6]] + getPosition(1) = [[timeUs=0, position=6], [timeUs=20000, position=19]] + getPosition(2180000) = [[timeUs=2180000, position=1423]] + getPosition(4360000) = [[timeUs=4340000, position=2827]] 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 + format 0: + sampleMimeType = audio/3gpp + maxInputSize = 61 + channelCount = 1 + sampleRate = 8000 sample 0: time = 2900000 flags = 1 diff --git a/testdata/src/test/assets/amr/sample_nb_cbr.amr.3.dump b/testdata/src/test/assets/amr/sample_nb_cbr.amr.3.dump new file mode 100644 index 0000000000..a77bd61f7e --- /dev/null +++ b/testdata/src/test/assets/amr/sample_nb_cbr.amr.3.dump @@ -0,0 +1,21 @@ +seekMap: + isSeekable = true + duration = 4360000 + getPosition(0) = [[timeUs=0, position=6]] + getPosition(1) = [[timeUs=0, position=6], [timeUs=20000, position=19]] + getPosition(2180000) = [[timeUs=2180000, position=1423]] + getPosition(4360000) = [[timeUs=4340000, position=2827]] +numberOfTracks = 1 +track 0: + total output bytes = 13 + sample count = 1 + format 0: + sampleMimeType = audio/3gpp + maxInputSize = 61 + channelCount = 1 + sampleRate = 8000 + sample 0: + time = 4340000 + flags = 1 + data = length 13, hash AC59BA7C +tracksEnded = true diff --git a/testdata/src/test/assets/amr/sample_nb_cbr.amr.unknown_length.dump b/testdata/src/test/assets/amr/sample_nb_cbr.amr.unknown_length.dump new file mode 100644 index 0000000000..8430964566 --- /dev/null +++ b/testdata/src/test/assets/amr/sample_nb_cbr.amr.unknown_length.dump @@ -0,0 +1,886 @@ +seekMap: + isSeekable = false + duration = UNSET TIME + getPosition(0) = [[timeUs=0, position=0]] +numberOfTracks = 1 +track 0: + total output bytes = 2834 + sample count = 218 + format 0: + sampleMimeType = audio/3gpp + maxInputSize = 61 + channelCount = 1 + sampleRate = 8000 + 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.amr b/testdata/src/test/assets/amr/sample_wb.amr similarity index 100% rename from library/core/src/test/assets/amr/sample_wb.amr rename to testdata/src/test/assets/amr/sample_wb.amr diff --git a/library/core/src/test/assets/amr/sample_wb.amr.0.dump b/testdata/src/test/assets/amr/sample_wb.amr.0.dump similarity index 97% rename from library/core/src/test/assets/amr/sample_wb.amr.0.dump rename to testdata/src/test/assets/amr/sample_wb.amr.0.dump index 1b3b8bd0dd..c4fb207a3a 100644 --- a/library/core/src/test/assets/amr/sample_wb.amr.0.dump +++ b/testdata/src/test/assets/amr/sample_wb.amr.0.dump @@ -4,29 +4,13 @@ seekMap: 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 + format 0: + sampleMimeType = audio/amr-wb + maxInputSize = 61 + channelCount = 1 + sampleRate = 16000 sample 0: time = 0 flags = 1 diff --git a/library/core/src/test/assets/amr/sample_wb_cbr.amr.unklen.dump b/testdata/src/test/assets/amr/sample_wb.amr.unknown_length.dump similarity index 97% rename from library/core/src/test/assets/amr/sample_wb_cbr.amr.unklen.dump rename to testdata/src/test/assets/amr/sample_wb.amr.unknown_length.dump index 1b3b8bd0dd..c4fb207a3a 100644 --- a/library/core/src/test/assets/amr/sample_wb_cbr.amr.unklen.dump +++ b/testdata/src/test/assets/amr/sample_wb.amr.unknown_length.dump @@ -4,29 +4,13 @@ seekMap: 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 + format 0: + sampleMimeType = audio/amr-wb + maxInputSize = 61 + channelCount = 1 + sampleRate = 16000 sample 0: time = 0 flags = 1 diff --git a/library/core/src/test/assets/amr/sample_wb_cbr.amr b/testdata/src/test/assets/amr/sample_wb_cbr.amr similarity index 100% rename from library/core/src/test/assets/amr/sample_wb_cbr.amr rename to testdata/src/test/assets/amr/sample_wb_cbr.amr diff --git a/library/core/src/test/assets/amr/sample_wb_cbr.amr.0.dump b/testdata/src/test/assets/amr/sample_wb_cbr.amr.0.dump similarity index 97% rename from library/core/src/test/assets/amr/sample_wb_cbr.amr.0.dump rename to testdata/src/test/assets/amr/sample_wb_cbr.amr.0.dump index c987c6e357..de452ce2a3 100644 --- a/library/core/src/test/assets/amr/sample_wb_cbr.amr.0.dump +++ b/testdata/src/test/assets/amr/sample_wb_cbr.amr.0.dump @@ -2,31 +2,18 @@ seekMap: isSeekable = true duration = 3380000 getPosition(0) = [[timeUs=0, position=9]] + getPosition(1) = [[timeUs=0, position=9], [timeUs=20000, position=33]] + getPosition(1690000) = [[timeUs=1680000, position=2025], [timeUs=1700000, position=2049]] + getPosition(3380000) = [[timeUs=3360000, position=4041]] 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 + format 0: + sampleMimeType = audio/amr-wb + maxInputSize = 61 + channelCount = 1 + sampleRate = 16000 sample 0: time = 0 flags = 1 diff --git a/library/core/src/test/assets/amr/sample_wb_cbr.amr.1.dump b/testdata/src/test/assets/amr/sample_wb_cbr.amr.1.dump similarity index 96% rename from library/core/src/test/assets/amr/sample_wb_cbr.amr.1.dump rename to testdata/src/test/assets/amr/sample_wb_cbr.amr.1.dump index fad4565195..1fc2a1c806 100644 --- a/library/core/src/test/assets/amr/sample_wb_cbr.amr.1.dump +++ b/testdata/src/test/assets/amr/sample_wb_cbr.amr.1.dump @@ -2,31 +2,18 @@ seekMap: isSeekable = true duration = 3380000 getPosition(0) = [[timeUs=0, position=9]] + getPosition(1) = [[timeUs=0, position=9], [timeUs=20000, position=33]] + getPosition(1690000) = [[timeUs=1680000, position=2025], [timeUs=1700000, position=2049]] + getPosition(3380000) = [[timeUs=3360000, position=4041]] 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 + format 0: + sampleMimeType = audio/amr-wb + maxInputSize = 61 + channelCount = 1 + sampleRate = 16000 sample 0: time = 1120000 flags = 1 diff --git a/library/core/src/test/assets/amr/sample_wb_cbr.amr.2.dump b/testdata/src/test/assets/amr/sample_wb_cbr.amr.2.dump similarity index 92% rename from library/core/src/test/assets/amr/sample_wb_cbr.amr.2.dump rename to testdata/src/test/assets/amr/sample_wb_cbr.amr.2.dump index 1f00a90739..4f41388bbd 100644 --- a/library/core/src/test/assets/amr/sample_wb_cbr.amr.2.dump +++ b/testdata/src/test/assets/amr/sample_wb_cbr.amr.2.dump @@ -2,31 +2,18 @@ seekMap: isSeekable = true duration = 3380000 getPosition(0) = [[timeUs=0, position=9]] + getPosition(1) = [[timeUs=0, position=9], [timeUs=20000, position=33]] + getPosition(1690000) = [[timeUs=1680000, position=2025], [timeUs=1700000, position=2049]] + getPosition(3380000) = [[timeUs=3360000, position=4041]] 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 + format 0: + sampleMimeType = audio/amr-wb + maxInputSize = 61 + channelCount = 1 + sampleRate = 16000 sample 0: time = 2240000 flags = 1 diff --git a/testdata/src/test/assets/amr/sample_wb_cbr.amr.3.dump b/testdata/src/test/assets/amr/sample_wb_cbr.amr.3.dump new file mode 100644 index 0000000000..6dfb7623a8 --- /dev/null +++ b/testdata/src/test/assets/amr/sample_wb_cbr.amr.3.dump @@ -0,0 +1,21 @@ +seekMap: + isSeekable = true + duration = 3380000 + getPosition(0) = [[timeUs=0, position=9]] + getPosition(1) = [[timeUs=0, position=9], [timeUs=20000, position=33]] + getPosition(1690000) = [[timeUs=1680000, position=2025], [timeUs=1700000, position=2049]] + getPosition(3380000) = [[timeUs=3360000, position=4041]] +numberOfTracks = 1 +track 0: + total output bytes = 24 + sample count = 1 + format 0: + sampleMimeType = audio/amr-wb + maxInputSize = 61 + channelCount = 1 + sampleRate = 16000 + sample 0: + time = 3360000 + flags = 1 + data = length 24, hash 772665A0 +tracksEnded = true diff --git a/testdata/src/test/assets/amr/sample_wb_cbr.amr.unknown_length.dump b/testdata/src/test/assets/amr/sample_wb_cbr.amr.unknown_length.dump new file mode 100644 index 0000000000..c4fb207a3a --- /dev/null +++ b/testdata/src/test/assets/amr/sample_wb_cbr.amr.unknown_length.dump @@ -0,0 +1,690 @@ +seekMap: + isSeekable = false + duration = UNSET TIME + getPosition(0) = [[timeUs=0, position=0]] +numberOfTracks = 1 +track 0: + total output bytes = 4056 + sample count = 169 + format 0: + sampleMimeType = audio/amr-wb + maxInputSize = 61 + channelCount = 1 + sampleRate = 16000 + 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/binary/ogg/vorbis_header_pages b/testdata/src/test/assets/binary/ogg/vorbis_header_pages similarity index 100% rename from library/core/src/test/assets/binary/ogg/vorbis_header_pages rename to testdata/src/test/assets/binary/ogg/vorbis_header_pages diff --git a/library/core/src/test/assets/binary/vorbis/comment_header b/testdata/src/test/assets/binary/vorbis/comment_header similarity index 100% rename from library/core/src/test/assets/binary/vorbis/comment_header rename to testdata/src/test/assets/binary/vorbis/comment_header diff --git a/library/core/src/test/assets/binary/vorbis/id_header b/testdata/src/test/assets/binary/vorbis/id_header similarity index 100% rename from library/core/src/test/assets/binary/vorbis/id_header rename to testdata/src/test/assets/binary/vorbis/id_header diff --git a/library/core/src/test/assets/binary/vorbis/setup_header b/testdata/src/test/assets/binary/vorbis/setup_header similarity index 100% rename from library/core/src/test/assets/binary/vorbis/setup_header rename to testdata/src/test/assets/binary/vorbis/setup_header diff --git a/library/core/src/androidTest/assets/bitmap/image_256_256.png b/testdata/src/test/assets/bitmap/image_256_256.png similarity index 100% rename from library/core/src/androidTest/assets/bitmap/image_256_256.png rename to testdata/src/test/assets/bitmap/image_256_256.png diff --git a/library/core/src/androidTest/assets/bitmap/image_80_60.bmp b/testdata/src/test/assets/bitmap/image_80_60.bmp similarity index 100% rename from library/core/src/androidTest/assets/bitmap/image_80_60.bmp rename to testdata/src/test/assets/bitmap/image_80_60.bmp diff --git a/library/core/src/test/assets/download-actions/dash-download-v0 b/testdata/src/test/assets/download-actions/dash-download-v0 similarity index 100% rename from library/core/src/test/assets/download-actions/dash-download-v0 rename to testdata/src/test/assets/download-actions/dash-download-v0 diff --git a/library/core/src/test/assets/download-actions/dash-remove-v0 b/testdata/src/test/assets/download-actions/dash-remove-v0 similarity index 100% rename from library/core/src/test/assets/download-actions/dash-remove-v0 rename to testdata/src/test/assets/download-actions/dash-remove-v0 diff --git a/library/core/src/test/assets/download-actions/hls-download-v0 b/testdata/src/test/assets/download-actions/hls-download-v0 similarity index 100% rename from library/core/src/test/assets/download-actions/hls-download-v0 rename to testdata/src/test/assets/download-actions/hls-download-v0 diff --git a/library/core/src/test/assets/download-actions/hls-download-v1 b/testdata/src/test/assets/download-actions/hls-download-v1 similarity index 100% rename from library/core/src/test/assets/download-actions/hls-download-v1 rename to testdata/src/test/assets/download-actions/hls-download-v1 diff --git a/library/core/src/test/assets/download-actions/hls-remove-v0 b/testdata/src/test/assets/download-actions/hls-remove-v0 similarity index 100% rename from library/core/src/test/assets/download-actions/hls-remove-v0 rename to testdata/src/test/assets/download-actions/hls-remove-v0 diff --git a/library/core/src/test/assets/download-actions/hls-remove-v1 b/testdata/src/test/assets/download-actions/hls-remove-v1 similarity index 100% rename from library/core/src/test/assets/download-actions/hls-remove-v1 rename to testdata/src/test/assets/download-actions/hls-remove-v1 diff --git a/library/core/src/test/assets/download-actions/progressive-download-v0 b/testdata/src/test/assets/download-actions/progressive-download-v0 similarity index 100% rename from library/core/src/test/assets/download-actions/progressive-download-v0 rename to testdata/src/test/assets/download-actions/progressive-download-v0 diff --git a/library/core/src/test/assets/download-actions/progressive-remove-v0 b/testdata/src/test/assets/download-actions/progressive-remove-v0 similarity index 100% rename from library/core/src/test/assets/download-actions/progressive-remove-v0 rename to testdata/src/test/assets/download-actions/progressive-remove-v0 diff --git a/library/core/src/test/assets/download-actions/ss-download-v0 b/testdata/src/test/assets/download-actions/ss-download-v0 similarity index 100% rename from library/core/src/test/assets/download-actions/ss-download-v0 rename to testdata/src/test/assets/download-actions/ss-download-v0 diff --git a/library/core/src/test/assets/download-actions/ss-download-v1 b/testdata/src/test/assets/download-actions/ss-download-v1 similarity index 100% rename from library/core/src/test/assets/download-actions/ss-download-v1 rename to testdata/src/test/assets/download-actions/ss-download-v1 diff --git a/library/core/src/test/assets/download-actions/ss-remove-v0 b/testdata/src/test/assets/download-actions/ss-remove-v0 similarity index 100% rename from library/core/src/test/assets/download-actions/ss-remove-v0 rename to testdata/src/test/assets/download-actions/ss-remove-v0 diff --git a/library/core/src/test/assets/download-actions/ss-remove-v1 b/testdata/src/test/assets/download-actions/ss-remove-v1 similarity index 100% rename from library/core/src/test/assets/download-actions/ss-remove-v1 rename to testdata/src/test/assets/download-actions/ss-remove-v1 diff --git a/testdata/src/test/assets/dvbsi/README.md b/testdata/src/test/assets/dvbsi/README.md new file mode 100644 index 0000000000..b17512aaa9 --- /dev/null +++ b/testdata/src/test/assets/dvbsi/README.md @@ -0,0 +1,15 @@ +# DVB Test Data + +The `.bin` files in this directory are generated from the `.xml` files using +`tstabcomp` from [TSDuck](https://tsduck.io/). + +The XML files are kept to make it clear where the values in the test assertions +are coming from, and to make it easier to change or add data in future. When +adding new files, or making changes to existing ones, you should regenerate the +`.bin` files using the command above before committing. + +To regenerate all the `.bin` files: + +```shell +$ tstabcomp -c testdata/src/test/assets/dvbsi/*.xml +``` diff --git a/testdata/src/test/assets/dvbsi/ait_no_url_base.bin b/testdata/src/test/assets/dvbsi/ait_no_url_base.bin new file mode 100644 index 0000000000..b1215d5d94 Binary files /dev/null and b/testdata/src/test/assets/dvbsi/ait_no_url_base.bin differ diff --git a/testdata/src/test/assets/dvbsi/ait_no_url_base.xml b/testdata/src/test/assets/dvbsi/ait_no_url_base.xml new file mode 100644 index 0000000000..0b54643345 --- /dev/null +++ b/testdata/src/test/assets/dvbsi/ait_no_url_base.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/testdata/src/test/assets/dvbsi/ait_no_url_path.bin b/testdata/src/test/assets/dvbsi/ait_no_url_path.bin new file mode 100644 index 0000000000..4fedaf6c76 Binary files /dev/null and b/testdata/src/test/assets/dvbsi/ait_no_url_path.bin differ diff --git a/testdata/src/test/assets/dvbsi/ait_no_url_path.xml b/testdata/src/test/assets/dvbsi/ait_no_url_path.xml new file mode 100644 index 0000000000..0bc17fc616 --- /dev/null +++ b/testdata/src/test/assets/dvbsi/ait_no_url_path.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + diff --git a/testdata/src/test/assets/dvbsi/ait_typical.bin b/testdata/src/test/assets/dvbsi/ait_typical.bin new file mode 100644 index 0000000000..0ab2d3f1fe Binary files /dev/null and b/testdata/src/test/assets/dvbsi/ait_typical.bin differ diff --git a/testdata/src/test/assets/dvbsi/ait_typical.xml b/testdata/src/test/assets/dvbsi/ait_typical.xml new file mode 100644 index 0000000000..1da4178bdd --- /dev/null +++ b/testdata/src/test/assets/dvbsi/ait_typical.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/library/core/src/test/assets/flac/bear.flac b/testdata/src/test/assets/flac/bear.flac similarity index 100% rename from library/core/src/test/assets/flac/bear.flac rename to testdata/src/test/assets/flac/bear.flac diff --git a/library/core/src/test/assets/flac/bear_with_picture.flac.0.dump b/testdata/src/test/assets/flac/bear_flac.0.dump similarity index 87% rename from library/core/src/test/assets/flac/bear_with_picture.flac.0.dump rename to testdata/src/test/assets/flac/bear_flac.0.dump index e35dcc2081..6b9ba356f4 100644 --- a/library/core/src/test/assets/flac/bear_with_picture.flac.0.dump +++ b/testdata/src/test/assets/flac/bear_flac.0.dump @@ -1,33 +1,21 @@ seekMap: - isSeekable = false + isSeekable = true duration = 2741000 - getPosition(0) = [[timeUs=0, position=0]] + getPosition(0) = [[timeUs=0, position=8880]] + getPosition(1) = [[timeUs=0, position=8880], [timeUs=85333, position=13910]] + getPosition(1370500) = [[timeUs=1365333, position=90825], [timeUs=1450666, position=96601]] + getPosition(2741000) = [[timeUs=2645333, position=169482]] numberOfTracks = 1 track 0: - format: - bitrate = 1536000 - id = null - containerMimeType = null - sampleMimeType = audio/flac - maxInputSize = 5776 - width = -1 - height = -1 - frameRate = -1.0 - rotationDegrees = 0 - pixelWidthHeightRatio = 1.0 - channelCount = 2 - sampleRate = 48000 - pcmEncoding = -1 - encoderDelay = 0 - encoderPadding = 0 - subsampleOffsetUs = 9223372036854775807 - selectionFlags = 0 - language = null - drmInitData = - - initializationData: - data = length 42, hash 83F6895 total output bytes = 164431 sample count = 33 + format 0: + sampleMimeType = audio/flac + maxInputSize = 5776 + channelCount = 2 + sampleRate = 48000 + initializationData: + data = length 42, hash 83F6895 sample 0: time = 0 flags = 1 diff --git a/testdata/src/test/assets/flac/bear_flac.1.dump b/testdata/src/test/assets/flac/bear_flac.1.dump new file mode 100644 index 0000000000..dc636b9837 --- /dev/null +++ b/testdata/src/test/assets/flac/bear_flac.1.dump @@ -0,0 +1,111 @@ +seekMap: + isSeekable = true + duration = 2741000 + getPosition(0) = [[timeUs=0, position=8880]] + getPosition(1) = [[timeUs=0, position=8880], [timeUs=85333, position=13910]] + getPosition(1370500) = [[timeUs=1365333, position=90825], [timeUs=1450666, position=96601]] + getPosition(2741000) = [[timeUs=2645333, position=169482]] +numberOfTracks = 1 +track 0: + total output bytes = 113666 + sample count = 23 + format 0: + sampleMimeType = audio/flac + maxInputSize = 5776 + channelCount = 2 + sampleRate = 48000 + initializationData: + data = length 42, hash 83F6895 + sample 0: + time = 853333 + flags = 1 + data = length 5031, hash 9D55EA53 + sample 1: + time = 938666 + flags = 1 + data = length 5119, hash E1CB9BA6 + sample 2: + time = 1024000 + flags = 1 + data = length 5360, hash 17265C5D + sample 3: + time = 1109333 + flags = 1 + data = length 5340, hash A90FDDF1 + sample 4: + time = 1194666 + flags = 1 + data = length 5162, hash 31F65AD5 + sample 5: + time = 1280000 + flags = 1 + data = length 5168, hash F2394F2D + sample 6: + time = 1365333 + flags = 1 + data = length 5776, hash 58437AB3 + sample 7: + time = 1450666 + flags = 1 + data = length 5394, hash EBAB20A8 + sample 8: + time = 1536000 + flags = 1 + data = length 5168, hash BF37C7A5 + sample 9: + time = 1621333 + flags = 1 + data = length 5324, hash 59546B7B + sample 10: + time = 1706666 + flags = 1 + data = length 5172, hash 6036EF0B + sample 11: + time = 1792000 + flags = 1 + data = length 5102, hash 5A131071 + sample 12: + time = 1877333 + flags = 1 + data = length 5111, hash 3D9EBB3B + sample 13: + time = 1962666 + flags = 1 + data = length 5113, hash 61101D4F + sample 14: + time = 2048000 + flags = 1 + data = length 5229, hash D2E55742 + sample 15: + time = 2133333 + flags = 1 + data = length 5162, hash 7F2E97FA + sample 16: + time = 2218666 + flags = 1 + data = length 5255, hash D92A782 + sample 17: + time = 2304000 + flags = 1 + data = length 5196, hash 98FE5138 + sample 18: + time = 2389333 + flags = 1 + data = length 5214, hash 3D35C38C + sample 19: + time = 2474666 + flags = 1 + data = length 5211, hash 7E25420F + sample 20: + time = 2560000 + flags = 1 + data = length 5230, hash 2AD96FBC + sample 21: + time = 2645333 + flags = 1 + data = length 3384, hash 938BCDD9 + sample 22: + time = 2730666 + flags = 1 + data = length 445, hash A388E3D6 +tracksEnded = true diff --git a/testdata/src/test/assets/flac/bear_flac.2.dump b/testdata/src/test/assets/flac/bear_flac.2.dump new file mode 100644 index 0000000000..6562202e9d --- /dev/null +++ b/testdata/src/test/assets/flac/bear_flac.2.dump @@ -0,0 +1,67 @@ +seekMap: + isSeekable = true + duration = 2741000 + getPosition(0) = [[timeUs=0, position=8880]] + getPosition(1) = [[timeUs=0, position=8880], [timeUs=85333, position=13910]] + getPosition(1370500) = [[timeUs=1365333, position=90825], [timeUs=1450666, position=96601]] + getPosition(2741000) = [[timeUs=2645333, position=169482]] +numberOfTracks = 1 +track 0: + total output bytes = 55652 + sample count = 12 + format 0: + sampleMimeType = audio/flac + maxInputSize = 5776 + channelCount = 2 + sampleRate = 48000 + initializationData: + data = length 42, hash 83F6895 + sample 0: + time = 1792000 + flags = 1 + data = length 5102, hash 5A131071 + sample 1: + time = 1877333 + flags = 1 + data = length 5111, hash 3D9EBB3B + sample 2: + time = 1962666 + flags = 1 + data = length 5113, hash 61101D4F + sample 3: + time = 2048000 + flags = 1 + data = length 5229, hash D2E55742 + sample 4: + time = 2133333 + flags = 1 + data = length 5162, hash 7F2E97FA + sample 5: + time = 2218666 + flags = 1 + data = length 5255, hash D92A782 + sample 6: + time = 2304000 + flags = 1 + data = length 5196, hash 98FE5138 + sample 7: + time = 2389333 + flags = 1 + data = length 5214, hash 3D35C38C + sample 8: + time = 2474666 + flags = 1 + data = length 5211, hash 7E25420F + sample 9: + time = 2560000 + flags = 1 + data = length 5230, hash 2AD96FBC + sample 10: + time = 2645333 + flags = 1 + data = length 3384, hash 938BCDD9 + sample 11: + time = 2730666 + flags = 1 + data = length 445, hash A388E3D6 +tracksEnded = true diff --git a/testdata/src/test/assets/flac/bear_flac.3.dump b/testdata/src/test/assets/flac/bear_flac.3.dump new file mode 100644 index 0000000000..a12d386453 --- /dev/null +++ b/testdata/src/test/assets/flac/bear_flac.3.dump @@ -0,0 +1,27 @@ +seekMap: + isSeekable = true + duration = 2741000 + getPosition(0) = [[timeUs=0, position=8880]] + getPosition(1) = [[timeUs=0, position=8880], [timeUs=85333, position=13910]] + getPosition(1370500) = [[timeUs=1365333, position=90825], [timeUs=1450666, position=96601]] + getPosition(2741000) = [[timeUs=2645333, position=169482]] +numberOfTracks = 1 +track 0: + total output bytes = 3829 + sample count = 2 + format 0: + sampleMimeType = audio/flac + maxInputSize = 5776 + channelCount = 2 + sampleRate = 48000 + initializationData: + data = length 42, hash 83F6895 + sample 0: + time = 2645333 + flags = 1 + data = length 3384, hash 938BCDD9 + sample 1: + time = 2730666 + flags = 1 + data = length 445, hash A388E3D6 +tracksEnded = true diff --git a/library/core/src/test/assets/flac/bear_with_id3.flac.0.dump b/testdata/src/test/assets/flac/bear_flac.unknown_length.dump similarity index 87% rename from library/core/src/test/assets/flac/bear_with_id3.flac.0.dump rename to testdata/src/test/assets/flac/bear_flac.unknown_length.dump index e35dcc2081..6b9ba356f4 100644 --- a/library/core/src/test/assets/flac/bear_with_id3.flac.0.dump +++ b/testdata/src/test/assets/flac/bear_flac.unknown_length.dump @@ -1,33 +1,21 @@ seekMap: - isSeekable = false + isSeekable = true duration = 2741000 - getPosition(0) = [[timeUs=0, position=0]] + getPosition(0) = [[timeUs=0, position=8880]] + getPosition(1) = [[timeUs=0, position=8880], [timeUs=85333, position=13910]] + getPosition(1370500) = [[timeUs=1365333, position=90825], [timeUs=1450666, position=96601]] + getPosition(2741000) = [[timeUs=2645333, position=169482]] numberOfTracks = 1 track 0: - format: - bitrate = 1536000 - id = null - containerMimeType = null - sampleMimeType = audio/flac - maxInputSize = 5776 - width = -1 - height = -1 - frameRate = -1.0 - rotationDegrees = 0 - pixelWidthHeightRatio = 1.0 - channelCount = 2 - sampleRate = 48000 - pcmEncoding = -1 - encoderDelay = 0 - encoderPadding = 0 - subsampleOffsetUs = 9223372036854775807 - selectionFlags = 0 - language = null - drmInitData = - - initializationData: - data = length 42, hash 83F6895 total output bytes = 164431 sample count = 33 + format 0: + sampleMimeType = audio/flac + maxInputSize = 5776 + channelCount = 2 + sampleRate = 48000 + initializationData: + data = length 42, hash 83F6895 sample 0: time = 0 flags = 1 diff --git a/library/core/src/test/assets/flac/bear_no_min_max_frame_size.flac b/testdata/src/test/assets/flac/bear_no_min_max_frame_size.flac similarity index 100% rename from library/core/src/test/assets/flac/bear_no_min_max_frame_size.flac rename to testdata/src/test/assets/flac/bear_no_min_max_frame_size.flac diff --git a/library/core/src/test/assets/flac/bear_no_min_max_frame_size.flac.0.dump b/testdata/src/test/assets/flac/bear_no_min_max_frame_size_flac.0.dump similarity index 87% rename from library/core/src/test/assets/flac/bear_no_min_max_frame_size.flac.0.dump rename to testdata/src/test/assets/flac/bear_no_min_max_frame_size_flac.0.dump index 2c394e71b7..3a94f57d11 100644 --- a/library/core/src/test/assets/flac/bear_no_min_max_frame_size.flac.0.dump +++ b/testdata/src/test/assets/flac/bear_no_min_max_frame_size_flac.0.dump @@ -1,33 +1,20 @@ seekMap: - isSeekable = false + isSeekable = true duration = 2741000 - getPosition(0) = [[timeUs=0, position=0]] + getPosition(0) = [[timeUs=0, position=8880]] + getPosition(1) = [[timeUs=0, position=8880], [timeUs=85333, position=13910]] + getPosition(1370500) = [[timeUs=1365333, position=90825], [timeUs=1450666, position=96601]] + getPosition(2741000) = [[timeUs=2645333, position=169482]] numberOfTracks = 1 track 0: - format: - bitrate = 1536000 - id = null - containerMimeType = null - sampleMimeType = audio/flac - maxInputSize = -1 - width = -1 - height = -1 - frameRate = -1.0 - rotationDegrees = 0 - pixelWidthHeightRatio = 1.0 - channelCount = 2 - sampleRate = 48000 - pcmEncoding = -1 - encoderDelay = 0 - encoderPadding = 0 - subsampleOffsetUs = 9223372036854775807 - selectionFlags = 0 - language = null - drmInitData = - - initializationData: - data = length 42, hash 9218FDB7 total output bytes = 164431 sample count = 33 + format 0: + sampleMimeType = audio/flac + channelCount = 2 + sampleRate = 48000 + initializationData: + data = length 42, hash 9218FDB7 sample 0: time = 0 flags = 1 diff --git a/testdata/src/test/assets/flac/bear_no_min_max_frame_size_flac.1.dump b/testdata/src/test/assets/flac/bear_no_min_max_frame_size_flac.1.dump new file mode 100644 index 0000000000..a101b03b09 --- /dev/null +++ b/testdata/src/test/assets/flac/bear_no_min_max_frame_size_flac.1.dump @@ -0,0 +1,110 @@ +seekMap: + isSeekable = true + duration = 2741000 + getPosition(0) = [[timeUs=0, position=8880]] + getPosition(1) = [[timeUs=0, position=8880], [timeUs=85333, position=13910]] + getPosition(1370500) = [[timeUs=1365333, position=90825], [timeUs=1450666, position=96601]] + getPosition(2741000) = [[timeUs=2645333, position=169482]] +numberOfTracks = 1 +track 0: + total output bytes = 113666 + sample count = 23 + format 0: + sampleMimeType = audio/flac + channelCount = 2 + sampleRate = 48000 + initializationData: + data = length 42, hash 9218FDB7 + sample 0: + time = 853333 + flags = 1 + data = length 5031, hash 9D55EA53 + sample 1: + time = 938666 + flags = 1 + data = length 5119, hash E1CB9BA6 + sample 2: + time = 1024000 + flags = 1 + data = length 5360, hash 17265C5D + sample 3: + time = 1109333 + flags = 1 + data = length 5340, hash A90FDDF1 + sample 4: + time = 1194666 + flags = 1 + data = length 5162, hash 31F65AD5 + sample 5: + time = 1280000 + flags = 1 + data = length 5168, hash F2394F2D + sample 6: + time = 1365333 + flags = 1 + data = length 5776, hash 58437AB3 + sample 7: + time = 1450666 + flags = 1 + data = length 5394, hash EBAB20A8 + sample 8: + time = 1536000 + flags = 1 + data = length 5168, hash BF37C7A5 + sample 9: + time = 1621333 + flags = 1 + data = length 5324, hash 59546B7B + sample 10: + time = 1706666 + flags = 1 + data = length 5172, hash 6036EF0B + sample 11: + time = 1792000 + flags = 1 + data = length 5102, hash 5A131071 + sample 12: + time = 1877333 + flags = 1 + data = length 5111, hash 3D9EBB3B + sample 13: + time = 1962666 + flags = 1 + data = length 5113, hash 61101D4F + sample 14: + time = 2048000 + flags = 1 + data = length 5229, hash D2E55742 + sample 15: + time = 2133333 + flags = 1 + data = length 5162, hash 7F2E97FA + sample 16: + time = 2218666 + flags = 1 + data = length 5255, hash D92A782 + sample 17: + time = 2304000 + flags = 1 + data = length 5196, hash 98FE5138 + sample 18: + time = 2389333 + flags = 1 + data = length 5214, hash 3D35C38C + sample 19: + time = 2474666 + flags = 1 + data = length 5211, hash 7E25420F + sample 20: + time = 2560000 + flags = 1 + data = length 5230, hash 2AD96FBC + sample 21: + time = 2645333 + flags = 1 + data = length 3384, hash 938BCDD9 + sample 22: + time = 2730666 + flags = 1 + data = length 445, hash A388E3D6 +tracksEnded = true diff --git a/testdata/src/test/assets/flac/bear_no_min_max_frame_size_flac.2.dump b/testdata/src/test/assets/flac/bear_no_min_max_frame_size_flac.2.dump new file mode 100644 index 0000000000..6e72772a14 --- /dev/null +++ b/testdata/src/test/assets/flac/bear_no_min_max_frame_size_flac.2.dump @@ -0,0 +1,66 @@ +seekMap: + isSeekable = true + duration = 2741000 + getPosition(0) = [[timeUs=0, position=8880]] + getPosition(1) = [[timeUs=0, position=8880], [timeUs=85333, position=13910]] + getPosition(1370500) = [[timeUs=1365333, position=90825], [timeUs=1450666, position=96601]] + getPosition(2741000) = [[timeUs=2645333, position=169482]] +numberOfTracks = 1 +track 0: + total output bytes = 55652 + sample count = 12 + format 0: + sampleMimeType = audio/flac + channelCount = 2 + sampleRate = 48000 + initializationData: + data = length 42, hash 9218FDB7 + sample 0: + time = 1792000 + flags = 1 + data = length 5102, hash 5A131071 + sample 1: + time = 1877333 + flags = 1 + data = length 5111, hash 3D9EBB3B + sample 2: + time = 1962666 + flags = 1 + data = length 5113, hash 61101D4F + sample 3: + time = 2048000 + flags = 1 + data = length 5229, hash D2E55742 + sample 4: + time = 2133333 + flags = 1 + data = length 5162, hash 7F2E97FA + sample 5: + time = 2218666 + flags = 1 + data = length 5255, hash D92A782 + sample 6: + time = 2304000 + flags = 1 + data = length 5196, hash 98FE5138 + sample 7: + time = 2389333 + flags = 1 + data = length 5214, hash 3D35C38C + sample 8: + time = 2474666 + flags = 1 + data = length 5211, hash 7E25420F + sample 9: + time = 2560000 + flags = 1 + data = length 5230, hash 2AD96FBC + sample 10: + time = 2645333 + flags = 1 + data = length 3384, hash 938BCDD9 + sample 11: + time = 2730666 + flags = 1 + data = length 445, hash A388E3D6 +tracksEnded = true diff --git a/testdata/src/test/assets/flac/bear_no_min_max_frame_size_flac.3.dump b/testdata/src/test/assets/flac/bear_no_min_max_frame_size_flac.3.dump new file mode 100644 index 0000000000..566c93cc5b --- /dev/null +++ b/testdata/src/test/assets/flac/bear_no_min_max_frame_size_flac.3.dump @@ -0,0 +1,26 @@ +seekMap: + isSeekable = true + duration = 2741000 + getPosition(0) = [[timeUs=0, position=8880]] + getPosition(1) = [[timeUs=0, position=8880], [timeUs=85333, position=13910]] + getPosition(1370500) = [[timeUs=1365333, position=90825], [timeUs=1450666, position=96601]] + getPosition(2741000) = [[timeUs=2645333, position=169482]] +numberOfTracks = 1 +track 0: + total output bytes = 3829 + sample count = 2 + format 0: + sampleMimeType = audio/flac + channelCount = 2 + sampleRate = 48000 + initializationData: + data = length 42, hash 9218FDB7 + sample 0: + time = 2645333 + flags = 1 + data = length 3384, hash 938BCDD9 + sample 1: + time = 2730666 + flags = 1 + data = length 445, hash A388E3D6 +tracksEnded = true diff --git a/testdata/src/test/assets/flac/bear_no_min_max_frame_size_flac.unknown_length.dump b/testdata/src/test/assets/flac/bear_no_min_max_frame_size_flac.unknown_length.dump new file mode 100644 index 0000000000..3a94f57d11 --- /dev/null +++ b/testdata/src/test/assets/flac/bear_no_min_max_frame_size_flac.unknown_length.dump @@ -0,0 +1,150 @@ +seekMap: + isSeekable = true + duration = 2741000 + getPosition(0) = [[timeUs=0, position=8880]] + getPosition(1) = [[timeUs=0, position=8880], [timeUs=85333, position=13910]] + getPosition(1370500) = [[timeUs=1365333, position=90825], [timeUs=1450666, position=96601]] + getPosition(2741000) = [[timeUs=2645333, position=169482]] +numberOfTracks = 1 +track 0: + total output bytes = 164431 + sample count = 33 + format 0: + sampleMimeType = audio/flac + channelCount = 2 + sampleRate = 48000 + initializationData: + data = length 42, hash 9218FDB7 + sample 0: + time = 0 + flags = 1 + data = length 5030, hash D2B60530 + sample 1: + time = 85333 + flags = 1 + data = length 5066, hash 4C932A54 + sample 2: + time = 170666 + flags = 1 + data = length 5112, hash 7E5A7B61 + sample 3: + time = 256000 + flags = 1 + data = length 5044, hash 7EF93F13 + sample 4: + time = 341333 + flags = 1 + data = length 4943, hash DE7E27F8 + sample 5: + time = 426666 + flags = 1 + data = length 5121, hash 6D0D0B40 + sample 6: + time = 512000 + flags = 1 + data = length 5068, hash 9924644F + sample 7: + time = 597333 + flags = 1 + data = length 5143, hash 6C34F0CE + sample 8: + time = 682666 + flags = 1 + data = length 5109, hash E3B7BEFB + sample 9: + time = 768000 + flags = 1 + data = length 5129, hash 44111D9B + sample 10: + time = 853333 + flags = 1 + data = length 5031, hash 9D55EA53 + sample 11: + time = 938666 + flags = 1 + data = length 5119, hash E1CB9BA6 + sample 12: + time = 1024000 + flags = 1 + data = length 5360, hash 17265C5D + sample 13: + time = 1109333 + flags = 1 + data = length 5340, hash A90FDDF1 + sample 14: + time = 1194666 + flags = 1 + data = length 5162, hash 31F65AD5 + sample 15: + time = 1280000 + flags = 1 + data = length 5168, hash F2394F2D + sample 16: + time = 1365333 + flags = 1 + data = length 5776, hash 58437AB3 + sample 17: + time = 1450666 + flags = 1 + data = length 5394, hash EBAB20A8 + sample 18: + time = 1536000 + flags = 1 + data = length 5168, hash BF37C7A5 + sample 19: + time = 1621333 + flags = 1 + data = length 5324, hash 59546B7B + sample 20: + time = 1706666 + flags = 1 + data = length 5172, hash 6036EF0B + sample 21: + time = 1792000 + flags = 1 + data = length 5102, hash 5A131071 + sample 22: + time = 1877333 + flags = 1 + data = length 5111, hash 3D9EBB3B + sample 23: + time = 1962666 + flags = 1 + data = length 5113, hash 61101D4F + sample 24: + time = 2048000 + flags = 1 + data = length 5229, hash D2E55742 + sample 25: + time = 2133333 + flags = 1 + data = length 5162, hash 7F2E97FA + sample 26: + time = 2218666 + flags = 1 + data = length 5255, hash D92A782 + sample 27: + time = 2304000 + flags = 1 + data = length 5196, hash 98FE5138 + sample 28: + time = 2389333 + flags = 1 + data = length 5214, hash 3D35C38C + sample 29: + time = 2474666 + flags = 1 + data = length 5211, hash 7E25420F + sample 30: + time = 2560000 + flags = 1 + data = length 5230, hash 2AD96FBC + sample 31: + time = 2645333 + flags = 1 + data = length 3384, hash 938BCDD9 + sample 32: + time = 2730666 + flags = 1 + data = length 445, hash A388E3D6 +tracksEnded = true diff --git a/extensions/flac/src/androidTest/assets/bear.flac.0.dump b/testdata/src/test/assets/flac/bear_no_min_max_frame_size_raw.0.dump similarity index 89% rename from extensions/flac/src/androidTest/assets/bear.flac.0.dump rename to testdata/src/test/assets/flac/bear_no_min_max_frame_size_raw.0.dump index 87060e8d61..37eee44b2e 100644 --- a/extensions/flac/src/androidTest/assets/bear.flac.0.dump +++ b/testdata/src/test/assets/flac/bear_no_min_max_frame_size_raw.0.dump @@ -2,31 +2,21 @@ seekMap: isSeekable = true duration = 2741000 getPosition(0) = [[timeUs=0, position=8880]] + getPosition(1) = [[timeUs=0, position=8880]] + getPosition(1370500) = [[timeUs=1365333, position=90825], [timeUs=1450666, position=96601]] + getPosition(2741000) = [[timeUs=2645333, position=169482]] numberOfTracks = 1 track 0: - format: - bitrate = 1536000 - id = null - containerMimeType = null + total output bytes = 526272 + sample count = 33 + format 0: + averageBitrate = 1536000 + peakBitrate = 1536000 sampleMimeType = audio/raw maxInputSize = 16384 - width = -1 - height = -1 - frameRate = -1.0 - rotationDegrees = 0 - pixelWidthHeightRatio = 1.0 channelCount = 2 sampleRate = 48000 pcmEncoding = 2 - encoderDelay = 0 - encoderPadding = 0 - subsampleOffsetUs = 9223372036854775807 - selectionFlags = 0 - language = null - drmInitData = - - initializationData: - total output bytes = 526272 - sample count = 33 sample 0: time = 0 flags = 1 diff --git a/extensions/flac/src/androidTest/assets/bear.flac.1.dump b/testdata/src/test/assets/flac/bear_no_min_max_frame_size_raw.1.dump similarity index 86% rename from extensions/flac/src/androidTest/assets/bear.flac.1.dump rename to testdata/src/test/assets/flac/bear_no_min_max_frame_size_raw.1.dump index b12f4dbf9d..2c299253e3 100644 --- a/extensions/flac/src/androidTest/assets/bear.flac.1.dump +++ b/testdata/src/test/assets/flac/bear_no_min_max_frame_size_raw.1.dump @@ -2,31 +2,21 @@ seekMap: isSeekable = true duration = 2741000 getPosition(0) = [[timeUs=0, position=8880]] + getPosition(1) = [[timeUs=0, position=8880]] + getPosition(1370500) = [[timeUs=1365333, position=90825], [timeUs=1450666, position=96601]] + getPosition(2741000) = [[timeUs=2645333, position=169482]] numberOfTracks = 1 track 0: - format: - bitrate = 1536000 - id = null - containerMimeType = null + total output bytes = 362432 + sample count = 23 + format 0: + averageBitrate = 1536000 + peakBitrate = 1536000 sampleMimeType = audio/raw maxInputSize = 16384 - width = -1 - height = -1 - frameRate = -1.0 - rotationDegrees = 0 - pixelWidthHeightRatio = 1.0 channelCount = 2 sampleRate = 48000 pcmEncoding = 2 - encoderDelay = 0 - encoderPadding = 0 - subsampleOffsetUs = 9223372036854775807 - selectionFlags = 0 - language = null - drmInitData = - - initializationData: - total output bytes = 362432 - sample count = 23 sample 0: time = 853333 flags = 1 diff --git a/extensions/flac/src/androidTest/assets/bear.flac.2.dump b/testdata/src/test/assets/flac/bear_no_min_max_frame_size_raw.2.dump similarity index 78% rename from extensions/flac/src/androidTest/assets/bear.flac.2.dump rename to testdata/src/test/assets/flac/bear_no_min_max_frame_size_raw.2.dump index 613023e86c..86d36c9e24 100644 --- a/extensions/flac/src/androidTest/assets/bear.flac.2.dump +++ b/testdata/src/test/assets/flac/bear_no_min_max_frame_size_raw.2.dump @@ -2,31 +2,21 @@ seekMap: isSeekable = true duration = 2741000 getPosition(0) = [[timeUs=0, position=8880]] + getPosition(1) = [[timeUs=0, position=8880]] + getPosition(1370500) = [[timeUs=1365333, position=90825], [timeUs=1450666, position=96601]] + getPosition(2741000) = [[timeUs=2645333, position=169482]] numberOfTracks = 1 track 0: - format: - bitrate = 1536000 - id = null - containerMimeType = null + total output bytes = 182208 + sample count = 12 + format 0: + averageBitrate = 1536000 + peakBitrate = 1536000 sampleMimeType = audio/raw maxInputSize = 16384 - width = -1 - height = -1 - frameRate = -1.0 - rotationDegrees = 0 - pixelWidthHeightRatio = 1.0 channelCount = 2 sampleRate = 48000 pcmEncoding = 2 - encoderDelay = 0 - encoderPadding = 0 - subsampleOffsetUs = 9223372036854775807 - selectionFlags = 0 - language = null - drmInitData = - - initializationData: - total output bytes = 182208 - sample count = 12 sample 0: time = 1792000 flags = 1 diff --git a/extensions/flac/src/androidTest/assets/bear.flac.3.dump b/testdata/src/test/assets/flac/bear_no_min_max_frame_size_raw.3.dump similarity index 57% rename from extensions/flac/src/androidTest/assets/bear.flac.3.dump rename to testdata/src/test/assets/flac/bear_no_min_max_frame_size_raw.3.dump index 79f369751c..2ad86792ff 100644 --- a/extensions/flac/src/androidTest/assets/bear.flac.3.dump +++ b/testdata/src/test/assets/flac/bear_no_min_max_frame_size_raw.3.dump @@ -2,31 +2,21 @@ seekMap: isSeekable = true duration = 2741000 getPosition(0) = [[timeUs=0, position=8880]] + getPosition(1) = [[timeUs=0, position=8880]] + getPosition(1370500) = [[timeUs=1365333, position=90825], [timeUs=1450666, position=96601]] + getPosition(2741000) = [[timeUs=2645333, position=169482]] numberOfTracks = 1 track 0: - format: - bitrate = 1536000 - id = null - containerMimeType = null + total output bytes = 18368 + sample count = 2 + format 0: + averageBitrate = 1536000 + peakBitrate = 1536000 sampleMimeType = audio/raw maxInputSize = 16384 - width = -1 - height = -1 - frameRate = -1.0 - rotationDegrees = 0 - pixelWidthHeightRatio = 1.0 channelCount = 2 sampleRate = 48000 pcmEncoding = 2 - encoderDelay = 0 - encoderPadding = 0 - subsampleOffsetUs = 9223372036854775807 - selectionFlags = 0 - language = null - drmInitData = - - initializationData: - total output bytes = 18368 - sample count = 2 sample 0: time = 2645333 flags = 1 diff --git a/testdata/src/test/assets/flac/bear_no_min_max_frame_size_raw.unknown_length.dump b/testdata/src/test/assets/flac/bear_no_min_max_frame_size_raw.unknown_length.dump new file mode 100644 index 0000000000..37eee44b2e --- /dev/null +++ b/testdata/src/test/assets/flac/bear_no_min_max_frame_size_raw.unknown_length.dump @@ -0,0 +1,152 @@ +seekMap: + isSeekable = true + duration = 2741000 + getPosition(0) = [[timeUs=0, position=8880]] + getPosition(1) = [[timeUs=0, position=8880]] + getPosition(1370500) = [[timeUs=1365333, position=90825], [timeUs=1450666, position=96601]] + getPosition(2741000) = [[timeUs=2645333, position=169482]] +numberOfTracks = 1 +track 0: + total output bytes = 526272 + sample count = 33 + format 0: + averageBitrate = 1536000 + peakBitrate = 1536000 + sampleMimeType = audio/raw + maxInputSize = 16384 + channelCount = 2 + sampleRate = 48000 + pcmEncoding = 2 + sample 0: + time = 0 + flags = 1 + data = length 16384, hash 61D2C5C2 + sample 1: + time = 85333 + flags = 1 + data = length 16384, hash E6D7F214 + sample 2: + time = 170666 + flags = 1 + data = length 16384, hash 59BF0D5D + sample 3: + time = 256000 + flags = 1 + data = length 16384, hash 3625F468 + sample 4: + time = 341333 + flags = 1 + data = length 16384, hash F66A323 + sample 5: + time = 426666 + flags = 1 + data = length 16384, hash CDBAE629 + sample 6: + time = 512000 + flags = 1 + data = length 16384, hash 536F3A91 + sample 7: + time = 597333 + flags = 1 + data = length 16384, hash D4F35C9C + sample 8: + time = 682666 + flags = 1 + data = length 16384, hash EE04CEBF + sample 9: + time = 768000 + flags = 1 + data = length 16384, hash 647E2A67 + sample 10: + time = 853333 + flags = 1 + data = length 16384, hash 31583F2C + sample 11: + time = 938666 + flags = 1 + data = length 16384, hash E433A93D + sample 12: + time = 1024000 + flags = 1 + data = length 16384, hash 5E1C7051 + sample 13: + time = 1109333 + flags = 1 + data = length 16384, hash 43E6E358 + sample 14: + time = 1194666 + flags = 1 + data = length 16384, hash 5DC1B256 + sample 15: + time = 1280000 + flags = 1 + data = length 16384, hash 3D9D95CF + sample 16: + time = 1365333 + flags = 1 + data = length 16384, hash 2A5BD2C0 + sample 17: + time = 1450666 + flags = 1 + data = length 16384, hash 93E25061 + sample 18: + time = 1536000 + flags = 1 + data = length 16384, hash B81793D8 + sample 19: + time = 1621333 + flags = 1 + data = length 16384, hash 1A3BD49F + sample 20: + time = 1706666 + flags = 1 + data = length 16384, hash FB672FF1 + sample 21: + time = 1792000 + flags = 1 + data = length 16384, hash 48AB8B45 + sample 22: + time = 1877333 + flags = 1 + data = length 16384, hash 13C9640A + sample 23: + time = 1962666 + flags = 1 + data = length 16384, hash 499E4A0B + sample 24: + time = 2048000 + flags = 1 + data = length 16384, hash F9A783E6 + sample 25: + time = 2133333 + flags = 1 + data = length 16384, hash D2B77598 + sample 26: + time = 2218666 + flags = 1 + data = length 16384, hash CE5B826C + sample 27: + time = 2304000 + flags = 1 + data = length 16384, hash E99EE956 + sample 28: + time = 2389333 + flags = 1 + data = length 16384, hash F2DB1486 + sample 29: + time = 2474666 + flags = 1 + data = length 16384, hash 1636EAB + sample 30: + time = 2560000 + flags = 1 + data = length 16384, hash 23457C08 + sample 31: + time = 2645333 + flags = 1 + data = length 16384, hash 30EB8381 + sample 32: + time = 2730666 + flags = 1 + data = length 1984, hash 59CFDE1B +tracksEnded = true diff --git a/library/core/src/test/assets/flac/bear_no_num_samples.flac b/testdata/src/test/assets/flac/bear_no_num_samples.flac similarity index 100% rename from library/core/src/test/assets/flac/bear_no_num_samples.flac rename to testdata/src/test/assets/flac/bear_no_num_samples.flac diff --git a/testdata/src/test/assets/flac/bear_no_num_samples_flac.0.dump b/testdata/src/test/assets/flac/bear_no_num_samples_flac.0.dump new file mode 100644 index 0000000000..072524eb5e --- /dev/null +++ b/testdata/src/test/assets/flac/bear_no_num_samples_flac.0.dump @@ -0,0 +1,149 @@ +seekMap: + isSeekable = true + duration = UNSET TIME + getPosition(0) = [[timeUs=0, position=8880]] + getPosition(1) = [[timeUs=0, position=8880], [timeUs=85333, position=13910]] +numberOfTracks = 1 +track 0: + total output bytes = 164431 + sample count = 33 + format 0: + sampleMimeType = audio/flac + maxInputSize = 5776 + channelCount = 2 + sampleRate = 48000 + initializationData: + data = length 42, hash 49FA2C21 + sample 0: + time = 0 + flags = 1 + data = length 5030, hash D2B60530 + sample 1: + time = 85333 + flags = 1 + data = length 5066, hash 4C932A54 + sample 2: + time = 170666 + flags = 1 + data = length 5112, hash 7E5A7B61 + sample 3: + time = 256000 + flags = 1 + data = length 5044, hash 7EF93F13 + sample 4: + time = 341333 + flags = 1 + data = length 4943, hash DE7E27F8 + sample 5: + time = 426666 + flags = 1 + data = length 5121, hash 6D0D0B40 + sample 6: + time = 512000 + flags = 1 + data = length 5068, hash 9924644F + sample 7: + time = 597333 + flags = 1 + data = length 5143, hash 6C34F0CE + sample 8: + time = 682666 + flags = 1 + data = length 5109, hash E3B7BEFB + sample 9: + time = 768000 + flags = 1 + data = length 5129, hash 44111D9B + sample 10: + time = 853333 + flags = 1 + data = length 5031, hash 9D55EA53 + sample 11: + time = 938666 + flags = 1 + data = length 5119, hash E1CB9BA6 + sample 12: + time = 1024000 + flags = 1 + data = length 5360, hash 17265C5D + sample 13: + time = 1109333 + flags = 1 + data = length 5340, hash A90FDDF1 + sample 14: + time = 1194666 + flags = 1 + data = length 5162, hash 31F65AD5 + sample 15: + time = 1280000 + flags = 1 + data = length 5168, hash F2394F2D + sample 16: + time = 1365333 + flags = 1 + data = length 5776, hash 58437AB3 + sample 17: + time = 1450666 + flags = 1 + data = length 5394, hash EBAB20A8 + sample 18: + time = 1536000 + flags = 1 + data = length 5168, hash BF37C7A5 + sample 19: + time = 1621333 + flags = 1 + data = length 5324, hash 59546B7B + sample 20: + time = 1706666 + flags = 1 + data = length 5172, hash 6036EF0B + sample 21: + time = 1792000 + flags = 1 + data = length 5102, hash 5A131071 + sample 22: + time = 1877333 + flags = 1 + data = length 5111, hash 3D9EBB3B + sample 23: + time = 1962666 + flags = 1 + data = length 5113, hash 61101D4F + sample 24: + time = 2048000 + flags = 1 + data = length 5229, hash D2E55742 + sample 25: + time = 2133333 + flags = 1 + data = length 5162, hash 7F2E97FA + sample 26: + time = 2218666 + flags = 1 + data = length 5255, hash D92A782 + sample 27: + time = 2304000 + flags = 1 + data = length 5196, hash 98FE5138 + sample 28: + time = 2389333 + flags = 1 + data = length 5214, hash 3D35C38C + sample 29: + time = 2474666 + flags = 1 + data = length 5211, hash 7E25420F + sample 30: + time = 2560000 + flags = 1 + data = length 5230, hash 2AD96FBC + sample 31: + time = 2645333 + flags = 1 + data = length 3384, hash 938BCDD9 + sample 32: + time = 2730666 + flags = 1 + data = length 445, hash A388E3D6 +tracksEnded = true diff --git a/testdata/src/test/assets/flac/bear_no_num_samples_flac.unknown_length.dump b/testdata/src/test/assets/flac/bear_no_num_samples_flac.unknown_length.dump new file mode 100644 index 0000000000..072524eb5e --- /dev/null +++ b/testdata/src/test/assets/flac/bear_no_num_samples_flac.unknown_length.dump @@ -0,0 +1,149 @@ +seekMap: + isSeekable = true + duration = UNSET TIME + getPosition(0) = [[timeUs=0, position=8880]] + getPosition(1) = [[timeUs=0, position=8880], [timeUs=85333, position=13910]] +numberOfTracks = 1 +track 0: + total output bytes = 164431 + sample count = 33 + format 0: + sampleMimeType = audio/flac + maxInputSize = 5776 + channelCount = 2 + sampleRate = 48000 + initializationData: + data = length 42, hash 49FA2C21 + sample 0: + time = 0 + flags = 1 + data = length 5030, hash D2B60530 + sample 1: + time = 85333 + flags = 1 + data = length 5066, hash 4C932A54 + sample 2: + time = 170666 + flags = 1 + data = length 5112, hash 7E5A7B61 + sample 3: + time = 256000 + flags = 1 + data = length 5044, hash 7EF93F13 + sample 4: + time = 341333 + flags = 1 + data = length 4943, hash DE7E27F8 + sample 5: + time = 426666 + flags = 1 + data = length 5121, hash 6D0D0B40 + sample 6: + time = 512000 + flags = 1 + data = length 5068, hash 9924644F + sample 7: + time = 597333 + flags = 1 + data = length 5143, hash 6C34F0CE + sample 8: + time = 682666 + flags = 1 + data = length 5109, hash E3B7BEFB + sample 9: + time = 768000 + flags = 1 + data = length 5129, hash 44111D9B + sample 10: + time = 853333 + flags = 1 + data = length 5031, hash 9D55EA53 + sample 11: + time = 938666 + flags = 1 + data = length 5119, hash E1CB9BA6 + sample 12: + time = 1024000 + flags = 1 + data = length 5360, hash 17265C5D + sample 13: + time = 1109333 + flags = 1 + data = length 5340, hash A90FDDF1 + sample 14: + time = 1194666 + flags = 1 + data = length 5162, hash 31F65AD5 + sample 15: + time = 1280000 + flags = 1 + data = length 5168, hash F2394F2D + sample 16: + time = 1365333 + flags = 1 + data = length 5776, hash 58437AB3 + sample 17: + time = 1450666 + flags = 1 + data = length 5394, hash EBAB20A8 + sample 18: + time = 1536000 + flags = 1 + data = length 5168, hash BF37C7A5 + sample 19: + time = 1621333 + flags = 1 + data = length 5324, hash 59546B7B + sample 20: + time = 1706666 + flags = 1 + data = length 5172, hash 6036EF0B + sample 21: + time = 1792000 + flags = 1 + data = length 5102, hash 5A131071 + sample 22: + time = 1877333 + flags = 1 + data = length 5111, hash 3D9EBB3B + sample 23: + time = 1962666 + flags = 1 + data = length 5113, hash 61101D4F + sample 24: + time = 2048000 + flags = 1 + data = length 5229, hash D2E55742 + sample 25: + time = 2133333 + flags = 1 + data = length 5162, hash 7F2E97FA + sample 26: + time = 2218666 + flags = 1 + data = length 5255, hash D92A782 + sample 27: + time = 2304000 + flags = 1 + data = length 5196, hash 98FE5138 + sample 28: + time = 2389333 + flags = 1 + data = length 5214, hash 3D35C38C + sample 29: + time = 2474666 + flags = 1 + data = length 5211, hash 7E25420F + sample 30: + time = 2560000 + flags = 1 + data = length 5230, hash 2AD96FBC + sample 31: + time = 2645333 + flags = 1 + data = length 3384, hash 938BCDD9 + sample 32: + time = 2730666 + flags = 1 + data = length 445, hash A388E3D6 +tracksEnded = true diff --git a/testdata/src/test/assets/flac/bear_no_num_samples_raw.0.dump b/testdata/src/test/assets/flac/bear_no_num_samples_raw.0.dump new file mode 100644 index 0000000000..9615182d68 --- /dev/null +++ b/testdata/src/test/assets/flac/bear_no_num_samples_raw.0.dump @@ -0,0 +1,150 @@ +seekMap: + isSeekable = true + duration = UNSET TIME + getPosition(0) = [[timeUs=0, position=8880]] + getPosition(1) = [[timeUs=0, position=8880]] +numberOfTracks = 1 +track 0: + total output bytes = 526272 + sample count = 33 + format 0: + averageBitrate = 1536000 + peakBitrate = 1536000 + sampleMimeType = audio/raw + maxInputSize = 16384 + channelCount = 2 + sampleRate = 48000 + pcmEncoding = 2 + sample 0: + time = 0 + flags = 1 + data = length 16384, hash 61D2C5C2 + sample 1: + time = 85333 + flags = 1 + data = length 16384, hash E6D7F214 + sample 2: + time = 170666 + flags = 1 + data = length 16384, hash 59BF0D5D + sample 3: + time = 256000 + flags = 1 + data = length 16384, hash 3625F468 + sample 4: + time = 341333 + flags = 1 + data = length 16384, hash F66A323 + sample 5: + time = 426666 + flags = 1 + data = length 16384, hash CDBAE629 + sample 6: + time = 512000 + flags = 1 + data = length 16384, hash 536F3A91 + sample 7: + time = 597333 + flags = 1 + data = length 16384, hash D4F35C9C + sample 8: + time = 682666 + flags = 1 + data = length 16384, hash EE04CEBF + sample 9: + time = 768000 + flags = 1 + data = length 16384, hash 647E2A67 + sample 10: + time = 853333 + flags = 1 + data = length 16384, hash 31583F2C + sample 11: + time = 938666 + flags = 1 + data = length 16384, hash E433A93D + sample 12: + time = 1024000 + flags = 1 + data = length 16384, hash 5E1C7051 + sample 13: + time = 1109333 + flags = 1 + data = length 16384, hash 43E6E358 + sample 14: + time = 1194666 + flags = 1 + data = length 16384, hash 5DC1B256 + sample 15: + time = 1280000 + flags = 1 + data = length 16384, hash 3D9D95CF + sample 16: + time = 1365333 + flags = 1 + data = length 16384, hash 2A5BD2C0 + sample 17: + time = 1450666 + flags = 1 + data = length 16384, hash 93E25061 + sample 18: + time = 1536000 + flags = 1 + data = length 16384, hash B81793D8 + sample 19: + time = 1621333 + flags = 1 + data = length 16384, hash 1A3BD49F + sample 20: + time = 1706666 + flags = 1 + data = length 16384, hash FB672FF1 + sample 21: + time = 1792000 + flags = 1 + data = length 16384, hash 48AB8B45 + sample 22: + time = 1877333 + flags = 1 + data = length 16384, hash 13C9640A + sample 23: + time = 1962666 + flags = 1 + data = length 16384, hash 499E4A0B + sample 24: + time = 2048000 + flags = 1 + data = length 16384, hash F9A783E6 + sample 25: + time = 2133333 + flags = 1 + data = length 16384, hash D2B77598 + sample 26: + time = 2218666 + flags = 1 + data = length 16384, hash CE5B826C + sample 27: + time = 2304000 + flags = 1 + data = length 16384, hash E99EE956 + sample 28: + time = 2389333 + flags = 1 + data = length 16384, hash F2DB1486 + sample 29: + time = 2474666 + flags = 1 + data = length 16384, hash 1636EAB + sample 30: + time = 2560000 + flags = 1 + data = length 16384, hash 23457C08 + sample 31: + time = 2645333 + flags = 1 + data = length 16384, hash 30EB8381 + sample 32: + time = 2730666 + flags = 1 + data = length 1984, hash 59CFDE1B +tracksEnded = true diff --git a/testdata/src/test/assets/flac/bear_no_num_samples_raw.unknown_length.dump b/testdata/src/test/assets/flac/bear_no_num_samples_raw.unknown_length.dump new file mode 100644 index 0000000000..9615182d68 --- /dev/null +++ b/testdata/src/test/assets/flac/bear_no_num_samples_raw.unknown_length.dump @@ -0,0 +1,150 @@ +seekMap: + isSeekable = true + duration = UNSET TIME + getPosition(0) = [[timeUs=0, position=8880]] + getPosition(1) = [[timeUs=0, position=8880]] +numberOfTracks = 1 +track 0: + total output bytes = 526272 + sample count = 33 + format 0: + averageBitrate = 1536000 + peakBitrate = 1536000 + sampleMimeType = audio/raw + maxInputSize = 16384 + channelCount = 2 + sampleRate = 48000 + pcmEncoding = 2 + sample 0: + time = 0 + flags = 1 + data = length 16384, hash 61D2C5C2 + sample 1: + time = 85333 + flags = 1 + data = length 16384, hash E6D7F214 + sample 2: + time = 170666 + flags = 1 + data = length 16384, hash 59BF0D5D + sample 3: + time = 256000 + flags = 1 + data = length 16384, hash 3625F468 + sample 4: + time = 341333 + flags = 1 + data = length 16384, hash F66A323 + sample 5: + time = 426666 + flags = 1 + data = length 16384, hash CDBAE629 + sample 6: + time = 512000 + flags = 1 + data = length 16384, hash 536F3A91 + sample 7: + time = 597333 + flags = 1 + data = length 16384, hash D4F35C9C + sample 8: + time = 682666 + flags = 1 + data = length 16384, hash EE04CEBF + sample 9: + time = 768000 + flags = 1 + data = length 16384, hash 647E2A67 + sample 10: + time = 853333 + flags = 1 + data = length 16384, hash 31583F2C + sample 11: + time = 938666 + flags = 1 + data = length 16384, hash E433A93D + sample 12: + time = 1024000 + flags = 1 + data = length 16384, hash 5E1C7051 + sample 13: + time = 1109333 + flags = 1 + data = length 16384, hash 43E6E358 + sample 14: + time = 1194666 + flags = 1 + data = length 16384, hash 5DC1B256 + sample 15: + time = 1280000 + flags = 1 + data = length 16384, hash 3D9D95CF + sample 16: + time = 1365333 + flags = 1 + data = length 16384, hash 2A5BD2C0 + sample 17: + time = 1450666 + flags = 1 + data = length 16384, hash 93E25061 + sample 18: + time = 1536000 + flags = 1 + data = length 16384, hash B81793D8 + sample 19: + time = 1621333 + flags = 1 + data = length 16384, hash 1A3BD49F + sample 20: + time = 1706666 + flags = 1 + data = length 16384, hash FB672FF1 + sample 21: + time = 1792000 + flags = 1 + data = length 16384, hash 48AB8B45 + sample 22: + time = 1877333 + flags = 1 + data = length 16384, hash 13C9640A + sample 23: + time = 1962666 + flags = 1 + data = length 16384, hash 499E4A0B + sample 24: + time = 2048000 + flags = 1 + data = length 16384, hash F9A783E6 + sample 25: + time = 2133333 + flags = 1 + data = length 16384, hash D2B77598 + sample 26: + time = 2218666 + flags = 1 + data = length 16384, hash CE5B826C + sample 27: + time = 2304000 + flags = 1 + data = length 16384, hash E99EE956 + sample 28: + time = 2389333 + flags = 1 + data = length 16384, hash F2DB1486 + sample 29: + time = 2474666 + flags = 1 + data = length 16384, hash 1636EAB + sample 30: + time = 2560000 + flags = 1 + data = length 16384, hash 23457C08 + sample 31: + time = 2645333 + flags = 1 + data = length 16384, hash 30EB8381 + sample 32: + time = 2730666 + flags = 1 + data = length 1984, hash 59CFDE1B +tracksEnded = true diff --git a/extensions/flac/src/androidTest/assets/bear_no_seek.flac b/testdata/src/test/assets/flac/bear_no_seek_table_no_num_samples.flac similarity index 99% rename from extensions/flac/src/androidTest/assets/bear_no_seek.flac rename to testdata/src/test/assets/flac/bear_no_seek_table_no_num_samples.flac index cd3271178b..0f1dff1e84 100644 Binary files a/extensions/flac/src/androidTest/assets/bear_no_seek.flac and b/testdata/src/test/assets/flac/bear_no_seek_table_no_num_samples.flac differ diff --git a/library/core/src/test/assets/flac/bear_no_num_samples.flac.0.dump b/testdata/src/test/assets/flac/bear_no_seek_table_no_num_samples_flac.0.dump similarity index 89% rename from library/core/src/test/assets/flac/bear_no_num_samples.flac.0.dump rename to testdata/src/test/assets/flac/bear_no_seek_table_no_num_samples_flac.0.dump index c913738be5..28aa56e94d 100644 --- a/library/core/src/test/assets/flac/bear_no_num_samples.flac.0.dump +++ b/testdata/src/test/assets/flac/bear_no_seek_table_no_num_samples_flac.0.dump @@ -4,30 +4,15 @@ seekMap: getPosition(0) = [[timeUs=0, position=0]] numberOfTracks = 1 track 0: - format: - bitrate = 1536000 - id = null - containerMimeType = null - sampleMimeType = audio/flac - maxInputSize = 5776 - width = -1 - height = -1 - frameRate = -1.0 - rotationDegrees = 0 - pixelWidthHeightRatio = 1.0 - channelCount = 2 - sampleRate = 48000 - pcmEncoding = -1 - encoderDelay = 0 - encoderPadding = 0 - subsampleOffsetUs = 9223372036854775807 - selectionFlags = 0 - language = null - drmInitData = - - initializationData: - data = length 42, hash 49FA2C21 total output bytes = 164431 sample count = 33 + format 0: + sampleMimeType = audio/flac + maxInputSize = 5776 + channelCount = 2 + sampleRate = 48000 + initializationData: + data = length 42, hash 49FA2C21 sample 0: time = 0 flags = 1 diff --git a/library/core/src/test/assets/flac/bear.flac.0.dump b/testdata/src/test/assets/flac/bear_no_seek_table_no_num_samples_flac.unknown_length.dump similarity index 88% rename from library/core/src/test/assets/flac/bear.flac.0.dump rename to testdata/src/test/assets/flac/bear_no_seek_table_no_num_samples_flac.unknown_length.dump index e35dcc2081..28aa56e94d 100644 --- a/library/core/src/test/assets/flac/bear.flac.0.dump +++ b/testdata/src/test/assets/flac/bear_no_seek_table_no_num_samples_flac.unknown_length.dump @@ -1,33 +1,18 @@ seekMap: isSeekable = false - duration = 2741000 + duration = UNSET TIME getPosition(0) = [[timeUs=0, position=0]] numberOfTracks = 1 track 0: - format: - bitrate = 1536000 - id = null - containerMimeType = null - sampleMimeType = audio/flac - maxInputSize = 5776 - width = -1 - height = -1 - frameRate = -1.0 - rotationDegrees = 0 - pixelWidthHeightRatio = 1.0 - channelCount = 2 - sampleRate = 48000 - pcmEncoding = -1 - encoderDelay = 0 - encoderPadding = 0 - subsampleOffsetUs = 9223372036854775807 - selectionFlags = 0 - language = null - drmInitData = - - initializationData: - data = length 42, hash 83F6895 total output bytes = 164431 sample count = 33 + format 0: + sampleMimeType = audio/flac + maxInputSize = 5776 + channelCount = 2 + sampleRate = 48000 + initializationData: + data = length 42, hash 49FA2C21 sample 0: time = 0 flags = 1 diff --git a/testdata/src/test/assets/flac/bear_no_seek_table_no_num_samples_raw.0.dump b/testdata/src/test/assets/flac/bear_no_seek_table_no_num_samples_raw.0.dump new file mode 100644 index 0000000000..34a2535c7a --- /dev/null +++ b/testdata/src/test/assets/flac/bear_no_seek_table_no_num_samples_raw.0.dump @@ -0,0 +1,149 @@ +seekMap: + isSeekable = false + duration = UNSET TIME + getPosition(0) = [[timeUs=0, position=0]] +numberOfTracks = 1 +track 0: + total output bytes = 526272 + sample count = 33 + format 0: + averageBitrate = 1536000 + peakBitrate = 1536000 + sampleMimeType = audio/raw + maxInputSize = 16384 + channelCount = 2 + sampleRate = 48000 + pcmEncoding = 2 + sample 0: + time = 0 + flags = 1 + data = length 16384, hash 61D2C5C2 + sample 1: + time = 85333 + flags = 1 + data = length 16384, hash E6D7F214 + sample 2: + time = 170666 + flags = 1 + data = length 16384, hash 59BF0D5D + sample 3: + time = 256000 + flags = 1 + data = length 16384, hash 3625F468 + sample 4: + time = 341333 + flags = 1 + data = length 16384, hash F66A323 + sample 5: + time = 426666 + flags = 1 + data = length 16384, hash CDBAE629 + sample 6: + time = 512000 + flags = 1 + data = length 16384, hash 536F3A91 + sample 7: + time = 597333 + flags = 1 + data = length 16384, hash D4F35C9C + sample 8: + time = 682666 + flags = 1 + data = length 16384, hash EE04CEBF + sample 9: + time = 768000 + flags = 1 + data = length 16384, hash 647E2A67 + sample 10: + time = 853333 + flags = 1 + data = length 16384, hash 31583F2C + sample 11: + time = 938666 + flags = 1 + data = length 16384, hash E433A93D + sample 12: + time = 1024000 + flags = 1 + data = length 16384, hash 5E1C7051 + sample 13: + time = 1109333 + flags = 1 + data = length 16384, hash 43E6E358 + sample 14: + time = 1194666 + flags = 1 + data = length 16384, hash 5DC1B256 + sample 15: + time = 1280000 + flags = 1 + data = length 16384, hash 3D9D95CF + sample 16: + time = 1365333 + flags = 1 + data = length 16384, hash 2A5BD2C0 + sample 17: + time = 1450666 + flags = 1 + data = length 16384, hash 93E25061 + sample 18: + time = 1536000 + flags = 1 + data = length 16384, hash B81793D8 + sample 19: + time = 1621333 + flags = 1 + data = length 16384, hash 1A3BD49F + sample 20: + time = 1706666 + flags = 1 + data = length 16384, hash FB672FF1 + sample 21: + time = 1792000 + flags = 1 + data = length 16384, hash 48AB8B45 + sample 22: + time = 1877333 + flags = 1 + data = length 16384, hash 13C9640A + sample 23: + time = 1962666 + flags = 1 + data = length 16384, hash 499E4A0B + sample 24: + time = 2048000 + flags = 1 + data = length 16384, hash F9A783E6 + sample 25: + time = 2133333 + flags = 1 + data = length 16384, hash D2B77598 + sample 26: + time = 2218666 + flags = 1 + data = length 16384, hash CE5B826C + sample 27: + time = 2304000 + flags = 1 + data = length 16384, hash E99EE956 + sample 28: + time = 2389333 + flags = 1 + data = length 16384, hash F2DB1486 + sample 29: + time = 2474666 + flags = 1 + data = length 16384, hash 1636EAB + sample 30: + time = 2560000 + flags = 1 + data = length 16384, hash 23457C08 + sample 31: + time = 2645333 + flags = 1 + data = length 16384, hash 30EB8381 + sample 32: + time = 2730666 + flags = 1 + data = length 1984, hash 59CFDE1B +tracksEnded = true diff --git a/testdata/src/test/assets/flac/bear_no_seek_table_no_num_samples_raw.unknown_length.dump b/testdata/src/test/assets/flac/bear_no_seek_table_no_num_samples_raw.unknown_length.dump new file mode 100644 index 0000000000..34a2535c7a --- /dev/null +++ b/testdata/src/test/assets/flac/bear_no_seek_table_no_num_samples_raw.unknown_length.dump @@ -0,0 +1,149 @@ +seekMap: + isSeekable = false + duration = UNSET TIME + getPosition(0) = [[timeUs=0, position=0]] +numberOfTracks = 1 +track 0: + total output bytes = 526272 + sample count = 33 + format 0: + averageBitrate = 1536000 + peakBitrate = 1536000 + sampleMimeType = audio/raw + maxInputSize = 16384 + channelCount = 2 + sampleRate = 48000 + pcmEncoding = 2 + sample 0: + time = 0 + flags = 1 + data = length 16384, hash 61D2C5C2 + sample 1: + time = 85333 + flags = 1 + data = length 16384, hash E6D7F214 + sample 2: + time = 170666 + flags = 1 + data = length 16384, hash 59BF0D5D + sample 3: + time = 256000 + flags = 1 + data = length 16384, hash 3625F468 + sample 4: + time = 341333 + flags = 1 + data = length 16384, hash F66A323 + sample 5: + time = 426666 + flags = 1 + data = length 16384, hash CDBAE629 + sample 6: + time = 512000 + flags = 1 + data = length 16384, hash 536F3A91 + sample 7: + time = 597333 + flags = 1 + data = length 16384, hash D4F35C9C + sample 8: + time = 682666 + flags = 1 + data = length 16384, hash EE04CEBF + sample 9: + time = 768000 + flags = 1 + data = length 16384, hash 647E2A67 + sample 10: + time = 853333 + flags = 1 + data = length 16384, hash 31583F2C + sample 11: + time = 938666 + flags = 1 + data = length 16384, hash E433A93D + sample 12: + time = 1024000 + flags = 1 + data = length 16384, hash 5E1C7051 + sample 13: + time = 1109333 + flags = 1 + data = length 16384, hash 43E6E358 + sample 14: + time = 1194666 + flags = 1 + data = length 16384, hash 5DC1B256 + sample 15: + time = 1280000 + flags = 1 + data = length 16384, hash 3D9D95CF + sample 16: + time = 1365333 + flags = 1 + data = length 16384, hash 2A5BD2C0 + sample 17: + time = 1450666 + flags = 1 + data = length 16384, hash 93E25061 + sample 18: + time = 1536000 + flags = 1 + data = length 16384, hash B81793D8 + sample 19: + time = 1621333 + flags = 1 + data = length 16384, hash 1A3BD49F + sample 20: + time = 1706666 + flags = 1 + data = length 16384, hash FB672FF1 + sample 21: + time = 1792000 + flags = 1 + data = length 16384, hash 48AB8B45 + sample 22: + time = 1877333 + flags = 1 + data = length 16384, hash 13C9640A + sample 23: + time = 1962666 + flags = 1 + data = length 16384, hash 499E4A0B + sample 24: + time = 2048000 + flags = 1 + data = length 16384, hash F9A783E6 + sample 25: + time = 2133333 + flags = 1 + data = length 16384, hash D2B77598 + sample 26: + time = 2218666 + flags = 1 + data = length 16384, hash CE5B826C + sample 27: + time = 2304000 + flags = 1 + data = length 16384, hash E99EE956 + sample 28: + time = 2389333 + flags = 1 + data = length 16384, hash F2DB1486 + sample 29: + time = 2474666 + flags = 1 + data = length 16384, hash 1636EAB + sample 30: + time = 2560000 + flags = 1 + data = length 16384, hash 23457C08 + sample 31: + time = 2645333 + flags = 1 + data = length 16384, hash 30EB8381 + sample 32: + time = 2730666 + flags = 1 + data = length 1984, hash 59CFDE1B +tracksEnded = true diff --git a/library/core/src/test/assets/flac/bear_one_metadata_block.flac b/testdata/src/test/assets/flac/bear_one_metadata_block.flac similarity index 100% rename from library/core/src/test/assets/flac/bear_one_metadata_block.flac rename to testdata/src/test/assets/flac/bear_one_metadata_block.flac diff --git a/testdata/src/test/assets/flac/bear_one_metadata_block_flac.0.dump b/testdata/src/test/assets/flac/bear_one_metadata_block_flac.0.dump new file mode 100644 index 0000000000..2c97070225 --- /dev/null +++ b/testdata/src/test/assets/flac/bear_one_metadata_block_flac.0.dump @@ -0,0 +1,151 @@ +seekMap: + isSeekable = true + duration = 2741000 + getPosition(0) = [[timeUs=0, position=42]] + getPosition(1) = [[timeUs=1, position=42]] + getPosition(1370500) = [[timeUs=1370500, position=75036]] + getPosition(2741000) = [[timeUs=2741000, position=153139]] +numberOfTracks = 1 +track 0: + total output bytes = 164431 + sample count = 33 + format 0: + sampleMimeType = audio/flac + maxInputSize = 5776 + channelCount = 2 + sampleRate = 48000 + initializationData: + data = length 42, hash 83F6895 + sample 0: + time = 0 + flags = 1 + data = length 5030, hash D2B60530 + sample 1: + time = 85333 + flags = 1 + data = length 5066, hash 4C932A54 + sample 2: + time = 170666 + flags = 1 + data = length 5112, hash 7E5A7B61 + sample 3: + time = 256000 + flags = 1 + data = length 5044, hash 7EF93F13 + sample 4: + time = 341333 + flags = 1 + data = length 4943, hash DE7E27F8 + sample 5: + time = 426666 + flags = 1 + data = length 5121, hash 6D0D0B40 + sample 6: + time = 512000 + flags = 1 + data = length 5068, hash 9924644F + sample 7: + time = 597333 + flags = 1 + data = length 5143, hash 6C34F0CE + sample 8: + time = 682666 + flags = 1 + data = length 5109, hash E3B7BEFB + sample 9: + time = 768000 + flags = 1 + data = length 5129, hash 44111D9B + sample 10: + time = 853333 + flags = 1 + data = length 5031, hash 9D55EA53 + sample 11: + time = 938666 + flags = 1 + data = length 5119, hash E1CB9BA6 + sample 12: + time = 1024000 + flags = 1 + data = length 5360, hash 17265C5D + sample 13: + time = 1109333 + flags = 1 + data = length 5340, hash A90FDDF1 + sample 14: + time = 1194666 + flags = 1 + data = length 5162, hash 31F65AD5 + sample 15: + time = 1280000 + flags = 1 + data = length 5168, hash F2394F2D + sample 16: + time = 1365333 + flags = 1 + data = length 5776, hash 58437AB3 + sample 17: + time = 1450666 + flags = 1 + data = length 5394, hash EBAB20A8 + sample 18: + time = 1536000 + flags = 1 + data = length 5168, hash BF37C7A5 + sample 19: + time = 1621333 + flags = 1 + data = length 5324, hash 59546B7B + sample 20: + time = 1706666 + flags = 1 + data = length 5172, hash 6036EF0B + sample 21: + time = 1792000 + flags = 1 + data = length 5102, hash 5A131071 + sample 22: + time = 1877333 + flags = 1 + data = length 5111, hash 3D9EBB3B + sample 23: + time = 1962666 + flags = 1 + data = length 5113, hash 61101D4F + sample 24: + time = 2048000 + flags = 1 + data = length 5229, hash D2E55742 + sample 25: + time = 2133333 + flags = 1 + data = length 5162, hash 7F2E97FA + sample 26: + time = 2218666 + flags = 1 + data = length 5255, hash D92A782 + sample 27: + time = 2304000 + flags = 1 + data = length 5196, hash 98FE5138 + sample 28: + time = 2389333 + flags = 1 + data = length 5214, hash 3D35C38C + sample 29: + time = 2474666 + flags = 1 + data = length 5211, hash 7E25420F + sample 30: + time = 2560000 + flags = 1 + data = length 5230, hash 2AD96FBC + sample 31: + time = 2645333 + flags = 1 + data = length 3384, hash 938BCDD9 + sample 32: + time = 2730666 + flags = 1 + data = length 445, hash A388E3D6 +tracksEnded = true diff --git a/testdata/src/test/assets/flac/bear_one_metadata_block_flac.1.dump b/testdata/src/test/assets/flac/bear_one_metadata_block_flac.1.dump new file mode 100644 index 0000000000..efb7caa6fb --- /dev/null +++ b/testdata/src/test/assets/flac/bear_one_metadata_block_flac.1.dump @@ -0,0 +1,111 @@ +seekMap: + isSeekable = true + duration = 2741000 + getPosition(0) = [[timeUs=0, position=42]] + getPosition(1) = [[timeUs=1, position=42]] + getPosition(1370500) = [[timeUs=1370500, position=75036]] + getPosition(2741000) = [[timeUs=2741000, position=153139]] +numberOfTracks = 1 +track 0: + total output bytes = 113666 + sample count = 23 + format 0: + sampleMimeType = audio/flac + maxInputSize = 5776 + channelCount = 2 + sampleRate = 48000 + initializationData: + data = length 42, hash 83F6895 + sample 0: + time = 853333 + flags = 1 + data = length 5031, hash 9D55EA53 + sample 1: + time = 938666 + flags = 1 + data = length 5119, hash E1CB9BA6 + sample 2: + time = 1024000 + flags = 1 + data = length 5360, hash 17265C5D + sample 3: + time = 1109333 + flags = 1 + data = length 5340, hash A90FDDF1 + sample 4: + time = 1194666 + flags = 1 + data = length 5162, hash 31F65AD5 + sample 5: + time = 1280000 + flags = 1 + data = length 5168, hash F2394F2D + sample 6: + time = 1365333 + flags = 1 + data = length 5776, hash 58437AB3 + sample 7: + time = 1450666 + flags = 1 + data = length 5394, hash EBAB20A8 + sample 8: + time = 1536000 + flags = 1 + data = length 5168, hash BF37C7A5 + sample 9: + time = 1621333 + flags = 1 + data = length 5324, hash 59546B7B + sample 10: + time = 1706666 + flags = 1 + data = length 5172, hash 6036EF0B + sample 11: + time = 1792000 + flags = 1 + data = length 5102, hash 5A131071 + sample 12: + time = 1877333 + flags = 1 + data = length 5111, hash 3D9EBB3B + sample 13: + time = 1962666 + flags = 1 + data = length 5113, hash 61101D4F + sample 14: + time = 2048000 + flags = 1 + data = length 5229, hash D2E55742 + sample 15: + time = 2133333 + flags = 1 + data = length 5162, hash 7F2E97FA + sample 16: + time = 2218666 + flags = 1 + data = length 5255, hash D92A782 + sample 17: + time = 2304000 + flags = 1 + data = length 5196, hash 98FE5138 + sample 18: + time = 2389333 + flags = 1 + data = length 5214, hash 3D35C38C + sample 19: + time = 2474666 + flags = 1 + data = length 5211, hash 7E25420F + sample 20: + time = 2560000 + flags = 1 + data = length 5230, hash 2AD96FBC + sample 21: + time = 2645333 + flags = 1 + data = length 3384, hash 938BCDD9 + sample 22: + time = 2730666 + flags = 1 + data = length 445, hash A388E3D6 +tracksEnded = true diff --git a/testdata/src/test/assets/flac/bear_one_metadata_block_flac.2.dump b/testdata/src/test/assets/flac/bear_one_metadata_block_flac.2.dump new file mode 100644 index 0000000000..851efbcd8d --- /dev/null +++ b/testdata/src/test/assets/flac/bear_one_metadata_block_flac.2.dump @@ -0,0 +1,67 @@ +seekMap: + isSeekable = true + duration = 2741000 + getPosition(0) = [[timeUs=0, position=42]] + getPosition(1) = [[timeUs=1, position=42]] + getPosition(1370500) = [[timeUs=1370500, position=75036]] + getPosition(2741000) = [[timeUs=2741000, position=153139]] +numberOfTracks = 1 +track 0: + total output bytes = 55652 + sample count = 12 + format 0: + sampleMimeType = audio/flac + maxInputSize = 5776 + channelCount = 2 + sampleRate = 48000 + initializationData: + data = length 42, hash 83F6895 + sample 0: + time = 1792000 + flags = 1 + data = length 5102, hash 5A131071 + sample 1: + time = 1877333 + flags = 1 + data = length 5111, hash 3D9EBB3B + sample 2: + time = 1962666 + flags = 1 + data = length 5113, hash 61101D4F + sample 3: + time = 2048000 + flags = 1 + data = length 5229, hash D2E55742 + sample 4: + time = 2133333 + flags = 1 + data = length 5162, hash 7F2E97FA + sample 5: + time = 2218666 + flags = 1 + data = length 5255, hash D92A782 + sample 6: + time = 2304000 + flags = 1 + data = length 5196, hash 98FE5138 + sample 7: + time = 2389333 + flags = 1 + data = length 5214, hash 3D35C38C + sample 8: + time = 2474666 + flags = 1 + data = length 5211, hash 7E25420F + sample 9: + time = 2560000 + flags = 1 + data = length 5230, hash 2AD96FBC + sample 10: + time = 2645333 + flags = 1 + data = length 3384, hash 938BCDD9 + sample 11: + time = 2730666 + flags = 1 + data = length 445, hash A388E3D6 +tracksEnded = true diff --git a/testdata/src/test/assets/flac/bear_one_metadata_block_flac.3.dump b/testdata/src/test/assets/flac/bear_one_metadata_block_flac.3.dump new file mode 100644 index 0000000000..c699876269 --- /dev/null +++ b/testdata/src/test/assets/flac/bear_one_metadata_block_flac.3.dump @@ -0,0 +1,23 @@ +seekMap: + isSeekable = true + duration = 2741000 + getPosition(0) = [[timeUs=0, position=42]] + getPosition(1) = [[timeUs=1, position=42]] + getPosition(1370500) = [[timeUs=1370500, position=75036]] + getPosition(2741000) = [[timeUs=2741000, position=153139]] +numberOfTracks = 1 +track 0: + total output bytes = 445 + sample count = 1 + format 0: + sampleMimeType = audio/flac + maxInputSize = 5776 + channelCount = 2 + sampleRate = 48000 + initializationData: + data = length 42, hash 83F6895 + sample 0: + time = 2730666 + flags = 1 + data = length 445, hash A388E3D6 +tracksEnded = true diff --git a/library/core/src/test/assets/flac/bear_one_metadata_block.flac.0.dump b/testdata/src/test/assets/flac/bear_one_metadata_block_flac.unknown_length.dump similarity index 89% rename from library/core/src/test/assets/flac/bear_one_metadata_block.flac.0.dump rename to testdata/src/test/assets/flac/bear_one_metadata_block_flac.unknown_length.dump index e35dcc2081..80dd2e9045 100644 --- a/library/core/src/test/assets/flac/bear_one_metadata_block.flac.0.dump +++ b/testdata/src/test/assets/flac/bear_one_metadata_block_flac.unknown_length.dump @@ -4,30 +4,15 @@ seekMap: getPosition(0) = [[timeUs=0, position=0]] numberOfTracks = 1 track 0: - format: - bitrate = 1536000 - id = null - containerMimeType = null - sampleMimeType = audio/flac - maxInputSize = 5776 - width = -1 - height = -1 - frameRate = -1.0 - rotationDegrees = 0 - pixelWidthHeightRatio = 1.0 - channelCount = 2 - sampleRate = 48000 - pcmEncoding = -1 - encoderDelay = 0 - encoderPadding = 0 - subsampleOffsetUs = 9223372036854775807 - selectionFlags = 0 - language = null - drmInitData = - - initializationData: - data = length 42, hash 83F6895 total output bytes = 164431 sample count = 33 + format 0: + sampleMimeType = audio/flac + maxInputSize = 5776 + channelCount = 2 + sampleRate = 48000 + initializationData: + data = length 42, hash 83F6895 sample 0: time = 0 flags = 1 diff --git a/testdata/src/test/assets/flac/bear_one_metadata_block_raw.0.dump b/testdata/src/test/assets/flac/bear_one_metadata_block_raw.0.dump new file mode 100644 index 0000000000..172f9e44ec --- /dev/null +++ b/testdata/src/test/assets/flac/bear_one_metadata_block_raw.0.dump @@ -0,0 +1,152 @@ +seekMap: + isSeekable = true + duration = 2741000 + getPosition(0) = [[timeUs=0, position=42]] + getPosition(1) = [[timeUs=1, position=42]] + getPosition(1370500) = [[timeUs=1370500, position=75036]] + getPosition(2741000) = [[timeUs=2741000, position=153139]] +numberOfTracks = 1 +track 0: + total output bytes = 526272 + sample count = 33 + format 0: + averageBitrate = 1536000 + peakBitrate = 1536000 + sampleMimeType = audio/raw + maxInputSize = 16384 + channelCount = 2 + sampleRate = 48000 + pcmEncoding = 2 + sample 0: + time = 0 + flags = 1 + data = length 16384, hash 61D2C5C2 + sample 1: + time = 85333 + flags = 1 + data = length 16384, hash E6D7F214 + sample 2: + time = 170666 + flags = 1 + data = length 16384, hash 59BF0D5D + sample 3: + time = 256000 + flags = 1 + data = length 16384, hash 3625F468 + sample 4: + time = 341333 + flags = 1 + data = length 16384, hash F66A323 + sample 5: + time = 426666 + flags = 1 + data = length 16384, hash CDBAE629 + sample 6: + time = 512000 + flags = 1 + data = length 16384, hash 536F3A91 + sample 7: + time = 597333 + flags = 1 + data = length 16384, hash D4F35C9C + sample 8: + time = 682666 + flags = 1 + data = length 16384, hash EE04CEBF + sample 9: + time = 768000 + flags = 1 + data = length 16384, hash 647E2A67 + sample 10: + time = 853333 + flags = 1 + data = length 16384, hash 31583F2C + sample 11: + time = 938666 + flags = 1 + data = length 16384, hash E433A93D + sample 12: + time = 1024000 + flags = 1 + data = length 16384, hash 5E1C7051 + sample 13: + time = 1109333 + flags = 1 + data = length 16384, hash 43E6E358 + sample 14: + time = 1194666 + flags = 1 + data = length 16384, hash 5DC1B256 + sample 15: + time = 1280000 + flags = 1 + data = length 16384, hash 3D9D95CF + sample 16: + time = 1365333 + flags = 1 + data = length 16384, hash 2A5BD2C0 + sample 17: + time = 1450666 + flags = 1 + data = length 16384, hash 93E25061 + sample 18: + time = 1536000 + flags = 1 + data = length 16384, hash B81793D8 + sample 19: + time = 1621333 + flags = 1 + data = length 16384, hash 1A3BD49F + sample 20: + time = 1706666 + flags = 1 + data = length 16384, hash FB672FF1 + sample 21: + time = 1792000 + flags = 1 + data = length 16384, hash 48AB8B45 + sample 22: + time = 1877333 + flags = 1 + data = length 16384, hash 13C9640A + sample 23: + time = 1962666 + flags = 1 + data = length 16384, hash 499E4A0B + sample 24: + time = 2048000 + flags = 1 + data = length 16384, hash F9A783E6 + sample 25: + time = 2133333 + flags = 1 + data = length 16384, hash D2B77598 + sample 26: + time = 2218666 + flags = 1 + data = length 16384, hash CE5B826C + sample 27: + time = 2304000 + flags = 1 + data = length 16384, hash E99EE956 + sample 28: + time = 2389333 + flags = 1 + data = length 16384, hash F2DB1486 + sample 29: + time = 2474666 + flags = 1 + data = length 16384, hash 1636EAB + sample 30: + time = 2560000 + flags = 1 + data = length 16384, hash 23457C08 + sample 31: + time = 2645333 + flags = 1 + data = length 16384, hash 30EB8381 + sample 32: + time = 2730666 + flags = 1 + data = length 1984, hash 59CFDE1B +tracksEnded = true diff --git a/testdata/src/test/assets/flac/bear_one_metadata_block_raw.1.dump b/testdata/src/test/assets/flac/bear_one_metadata_block_raw.1.dump new file mode 100644 index 0000000000..cacb89d8d1 --- /dev/null +++ b/testdata/src/test/assets/flac/bear_one_metadata_block_raw.1.dump @@ -0,0 +1,112 @@ +seekMap: + isSeekable = true + duration = 2741000 + getPosition(0) = [[timeUs=0, position=42]] + getPosition(1) = [[timeUs=1, position=42]] + getPosition(1370500) = [[timeUs=1370500, position=75036]] + getPosition(2741000) = [[timeUs=2741000, position=153139]] +numberOfTracks = 1 +track 0: + total output bytes = 362432 + sample count = 23 + format 0: + averageBitrate = 1536000 + peakBitrate = 1536000 + sampleMimeType = audio/raw + maxInputSize = 16384 + channelCount = 2 + sampleRate = 48000 + pcmEncoding = 2 + sample 0: + time = 853333 + flags = 1 + data = length 16384, hash 31583F2C + sample 1: + time = 938666 + flags = 1 + data = length 16384, hash E433A93D + sample 2: + time = 1024000 + flags = 1 + data = length 16384, hash 5E1C7051 + sample 3: + time = 1109333 + flags = 1 + data = length 16384, hash 43E6E358 + sample 4: + time = 1194666 + flags = 1 + data = length 16384, hash 5DC1B256 + sample 5: + time = 1280000 + flags = 1 + data = length 16384, hash 3D9D95CF + sample 6: + time = 1365333 + flags = 1 + data = length 16384, hash 2A5BD2C0 + sample 7: + time = 1450666 + flags = 1 + data = length 16384, hash 93E25061 + sample 8: + time = 1536000 + flags = 1 + data = length 16384, hash B81793D8 + sample 9: + time = 1621333 + flags = 1 + data = length 16384, hash 1A3BD49F + sample 10: + time = 1706666 + flags = 1 + data = length 16384, hash FB672FF1 + sample 11: + time = 1792000 + flags = 1 + data = length 16384, hash 48AB8B45 + sample 12: + time = 1877333 + flags = 1 + data = length 16384, hash 13C9640A + sample 13: + time = 1962666 + flags = 1 + data = length 16384, hash 499E4A0B + sample 14: + time = 2048000 + flags = 1 + data = length 16384, hash F9A783E6 + sample 15: + time = 2133333 + flags = 1 + data = length 16384, hash D2B77598 + sample 16: + time = 2218666 + flags = 1 + data = length 16384, hash CE5B826C + sample 17: + time = 2304000 + flags = 1 + data = length 16384, hash E99EE956 + sample 18: + time = 2389333 + flags = 1 + data = length 16384, hash F2DB1486 + sample 19: + time = 2474666 + flags = 1 + data = length 16384, hash 1636EAB + sample 20: + time = 2560000 + flags = 1 + data = length 16384, hash 23457C08 + sample 21: + time = 2645333 + flags = 1 + data = length 16384, hash 30EB8381 + sample 22: + time = 2730666 + flags = 1 + data = length 1984, hash 59CFDE1B +tracksEnded = true diff --git a/testdata/src/test/assets/flac/bear_one_metadata_block_raw.2.dump b/testdata/src/test/assets/flac/bear_one_metadata_block_raw.2.dump new file mode 100644 index 0000000000..8285b73eca --- /dev/null +++ b/testdata/src/test/assets/flac/bear_one_metadata_block_raw.2.dump @@ -0,0 +1,68 @@ +seekMap: + isSeekable = true + duration = 2741000 + getPosition(0) = [[timeUs=0, position=42]] + getPosition(1) = [[timeUs=1, position=42]] + getPosition(1370500) = [[timeUs=1370500, position=75036]] + getPosition(2741000) = [[timeUs=2741000, position=153139]] +numberOfTracks = 1 +track 0: + total output bytes = 182208 + sample count = 12 + format 0: + averageBitrate = 1536000 + peakBitrate = 1536000 + sampleMimeType = audio/raw + maxInputSize = 16384 + channelCount = 2 + sampleRate = 48000 + pcmEncoding = 2 + sample 0: + time = 1792000 + flags = 1 + data = length 16384, hash 48AB8B45 + sample 1: + time = 1877333 + flags = 1 + data = length 16384, hash 13C9640A + sample 2: + time = 1962666 + flags = 1 + data = length 16384, hash 499E4A0B + sample 3: + time = 2048000 + flags = 1 + data = length 16384, hash F9A783E6 + sample 4: + time = 2133333 + flags = 1 + data = length 16384, hash D2B77598 + sample 5: + time = 2218666 + flags = 1 + data = length 16384, hash CE5B826C + sample 6: + time = 2304000 + flags = 1 + data = length 16384, hash E99EE956 + sample 7: + time = 2389333 + flags = 1 + data = length 16384, hash F2DB1486 + sample 8: + time = 2474666 + flags = 1 + data = length 16384, hash 1636EAB + sample 9: + time = 2560000 + flags = 1 + data = length 16384, hash 23457C08 + sample 10: + time = 2645333 + flags = 1 + data = length 16384, hash 30EB8381 + sample 11: + time = 2730666 + flags = 1 + data = length 1984, hash 59CFDE1B +tracksEnded = true diff --git a/testdata/src/test/assets/flac/bear_one_metadata_block_raw.3.dump b/testdata/src/test/assets/flac/bear_one_metadata_block_raw.3.dump new file mode 100644 index 0000000000..3223ab2a34 --- /dev/null +++ b/testdata/src/test/assets/flac/bear_one_metadata_block_raw.3.dump @@ -0,0 +1,24 @@ +seekMap: + isSeekable = true + duration = 2741000 + getPosition(0) = [[timeUs=0, position=42]] + getPosition(1) = [[timeUs=1, position=42]] + getPosition(1370500) = [[timeUs=1370500, position=75036]] + getPosition(2741000) = [[timeUs=2741000, position=153139]] +numberOfTracks = 1 +track 0: + total output bytes = 1984 + sample count = 1 + format 0: + averageBitrate = 1536000 + peakBitrate = 1536000 + sampleMimeType = audio/raw + maxInputSize = 16384 + channelCount = 2 + sampleRate = 48000 + pcmEncoding = 2 + sample 0: + time = 2730666 + flags = 1 + data = length 1984, hash 59CFDE1B +tracksEnded = true diff --git a/testdata/src/test/assets/flac/bear_one_metadata_block_raw.unknown_length.dump b/testdata/src/test/assets/flac/bear_one_metadata_block_raw.unknown_length.dump new file mode 100644 index 0000000000..0c9b3e7eb2 --- /dev/null +++ b/testdata/src/test/assets/flac/bear_one_metadata_block_raw.unknown_length.dump @@ -0,0 +1,149 @@ +seekMap: + isSeekable = false + duration = 2741000 + getPosition(0) = [[timeUs=0, position=0]] +numberOfTracks = 1 +track 0: + total output bytes = 526272 + sample count = 33 + format 0: + averageBitrate = 1536000 + peakBitrate = 1536000 + sampleMimeType = audio/raw + maxInputSize = 16384 + channelCount = 2 + sampleRate = 48000 + pcmEncoding = 2 + sample 0: + time = 0 + flags = 1 + data = length 16384, hash 61D2C5C2 + sample 1: + time = 85333 + flags = 1 + data = length 16384, hash E6D7F214 + sample 2: + time = 170666 + flags = 1 + data = length 16384, hash 59BF0D5D + sample 3: + time = 256000 + flags = 1 + data = length 16384, hash 3625F468 + sample 4: + time = 341333 + flags = 1 + data = length 16384, hash F66A323 + sample 5: + time = 426666 + flags = 1 + data = length 16384, hash CDBAE629 + sample 6: + time = 512000 + flags = 1 + data = length 16384, hash 536F3A91 + sample 7: + time = 597333 + flags = 1 + data = length 16384, hash D4F35C9C + sample 8: + time = 682666 + flags = 1 + data = length 16384, hash EE04CEBF + sample 9: + time = 768000 + flags = 1 + data = length 16384, hash 647E2A67 + sample 10: + time = 853333 + flags = 1 + data = length 16384, hash 31583F2C + sample 11: + time = 938666 + flags = 1 + data = length 16384, hash E433A93D + sample 12: + time = 1024000 + flags = 1 + data = length 16384, hash 5E1C7051 + sample 13: + time = 1109333 + flags = 1 + data = length 16384, hash 43E6E358 + sample 14: + time = 1194666 + flags = 1 + data = length 16384, hash 5DC1B256 + sample 15: + time = 1280000 + flags = 1 + data = length 16384, hash 3D9D95CF + sample 16: + time = 1365333 + flags = 1 + data = length 16384, hash 2A5BD2C0 + sample 17: + time = 1450666 + flags = 1 + data = length 16384, hash 93E25061 + sample 18: + time = 1536000 + flags = 1 + data = length 16384, hash B81793D8 + sample 19: + time = 1621333 + flags = 1 + data = length 16384, hash 1A3BD49F + sample 20: + time = 1706666 + flags = 1 + data = length 16384, hash FB672FF1 + sample 21: + time = 1792000 + flags = 1 + data = length 16384, hash 48AB8B45 + sample 22: + time = 1877333 + flags = 1 + data = length 16384, hash 13C9640A + sample 23: + time = 1962666 + flags = 1 + data = length 16384, hash 499E4A0B + sample 24: + time = 2048000 + flags = 1 + data = length 16384, hash F9A783E6 + sample 25: + time = 2133333 + flags = 1 + data = length 16384, hash D2B77598 + sample 26: + time = 2218666 + flags = 1 + data = length 16384, hash CE5B826C + sample 27: + time = 2304000 + flags = 1 + data = length 16384, hash E99EE956 + sample 28: + time = 2389333 + flags = 1 + data = length 16384, hash F2DB1486 + sample 29: + time = 2474666 + flags = 1 + data = length 16384, hash 1636EAB + sample 30: + time = 2560000 + flags = 1 + data = length 16384, hash 23457C08 + sample 31: + time = 2645333 + flags = 1 + data = length 16384, hash 30EB8381 + sample 32: + time = 2730666 + flags = 1 + data = length 1984, hash 59CFDE1B +tracksEnded = true diff --git a/testdata/src/test/assets/flac/bear_raw.0.dump b/testdata/src/test/assets/flac/bear_raw.0.dump new file mode 100644 index 0000000000..37eee44b2e --- /dev/null +++ b/testdata/src/test/assets/flac/bear_raw.0.dump @@ -0,0 +1,152 @@ +seekMap: + isSeekable = true + duration = 2741000 + getPosition(0) = [[timeUs=0, position=8880]] + getPosition(1) = [[timeUs=0, position=8880]] + getPosition(1370500) = [[timeUs=1365333, position=90825], [timeUs=1450666, position=96601]] + getPosition(2741000) = [[timeUs=2645333, position=169482]] +numberOfTracks = 1 +track 0: + total output bytes = 526272 + sample count = 33 + format 0: + averageBitrate = 1536000 + peakBitrate = 1536000 + sampleMimeType = audio/raw + maxInputSize = 16384 + channelCount = 2 + sampleRate = 48000 + pcmEncoding = 2 + sample 0: + time = 0 + flags = 1 + data = length 16384, hash 61D2C5C2 + sample 1: + time = 85333 + flags = 1 + data = length 16384, hash E6D7F214 + sample 2: + time = 170666 + flags = 1 + data = length 16384, hash 59BF0D5D + sample 3: + time = 256000 + flags = 1 + data = length 16384, hash 3625F468 + sample 4: + time = 341333 + flags = 1 + data = length 16384, hash F66A323 + sample 5: + time = 426666 + flags = 1 + data = length 16384, hash CDBAE629 + sample 6: + time = 512000 + flags = 1 + data = length 16384, hash 536F3A91 + sample 7: + time = 597333 + flags = 1 + data = length 16384, hash D4F35C9C + sample 8: + time = 682666 + flags = 1 + data = length 16384, hash EE04CEBF + sample 9: + time = 768000 + flags = 1 + data = length 16384, hash 647E2A67 + sample 10: + time = 853333 + flags = 1 + data = length 16384, hash 31583F2C + sample 11: + time = 938666 + flags = 1 + data = length 16384, hash E433A93D + sample 12: + time = 1024000 + flags = 1 + data = length 16384, hash 5E1C7051 + sample 13: + time = 1109333 + flags = 1 + data = length 16384, hash 43E6E358 + sample 14: + time = 1194666 + flags = 1 + data = length 16384, hash 5DC1B256 + sample 15: + time = 1280000 + flags = 1 + data = length 16384, hash 3D9D95CF + sample 16: + time = 1365333 + flags = 1 + data = length 16384, hash 2A5BD2C0 + sample 17: + time = 1450666 + flags = 1 + data = length 16384, hash 93E25061 + sample 18: + time = 1536000 + flags = 1 + data = length 16384, hash B81793D8 + sample 19: + time = 1621333 + flags = 1 + data = length 16384, hash 1A3BD49F + sample 20: + time = 1706666 + flags = 1 + data = length 16384, hash FB672FF1 + sample 21: + time = 1792000 + flags = 1 + data = length 16384, hash 48AB8B45 + sample 22: + time = 1877333 + flags = 1 + data = length 16384, hash 13C9640A + sample 23: + time = 1962666 + flags = 1 + data = length 16384, hash 499E4A0B + sample 24: + time = 2048000 + flags = 1 + data = length 16384, hash F9A783E6 + sample 25: + time = 2133333 + flags = 1 + data = length 16384, hash D2B77598 + sample 26: + time = 2218666 + flags = 1 + data = length 16384, hash CE5B826C + sample 27: + time = 2304000 + flags = 1 + data = length 16384, hash E99EE956 + sample 28: + time = 2389333 + flags = 1 + data = length 16384, hash F2DB1486 + sample 29: + time = 2474666 + flags = 1 + data = length 16384, hash 1636EAB + sample 30: + time = 2560000 + flags = 1 + data = length 16384, hash 23457C08 + sample 31: + time = 2645333 + flags = 1 + data = length 16384, hash 30EB8381 + sample 32: + time = 2730666 + flags = 1 + data = length 1984, hash 59CFDE1B +tracksEnded = true diff --git a/testdata/src/test/assets/flac/bear_raw.1.dump b/testdata/src/test/assets/flac/bear_raw.1.dump new file mode 100644 index 0000000000..2c299253e3 --- /dev/null +++ b/testdata/src/test/assets/flac/bear_raw.1.dump @@ -0,0 +1,112 @@ +seekMap: + isSeekable = true + duration = 2741000 + getPosition(0) = [[timeUs=0, position=8880]] + getPosition(1) = [[timeUs=0, position=8880]] + getPosition(1370500) = [[timeUs=1365333, position=90825], [timeUs=1450666, position=96601]] + getPosition(2741000) = [[timeUs=2645333, position=169482]] +numberOfTracks = 1 +track 0: + total output bytes = 362432 + sample count = 23 + format 0: + averageBitrate = 1536000 + peakBitrate = 1536000 + sampleMimeType = audio/raw + maxInputSize = 16384 + channelCount = 2 + sampleRate = 48000 + pcmEncoding = 2 + sample 0: + time = 853333 + flags = 1 + data = length 16384, hash 31583F2C + sample 1: + time = 938666 + flags = 1 + data = length 16384, hash E433A93D + sample 2: + time = 1024000 + flags = 1 + data = length 16384, hash 5E1C7051 + sample 3: + time = 1109333 + flags = 1 + data = length 16384, hash 43E6E358 + sample 4: + time = 1194666 + flags = 1 + data = length 16384, hash 5DC1B256 + sample 5: + time = 1280000 + flags = 1 + data = length 16384, hash 3D9D95CF + sample 6: + time = 1365333 + flags = 1 + data = length 16384, hash 2A5BD2C0 + sample 7: + time = 1450666 + flags = 1 + data = length 16384, hash 93E25061 + sample 8: + time = 1536000 + flags = 1 + data = length 16384, hash B81793D8 + sample 9: + time = 1621333 + flags = 1 + data = length 16384, hash 1A3BD49F + sample 10: + time = 1706666 + flags = 1 + data = length 16384, hash FB672FF1 + sample 11: + time = 1792000 + flags = 1 + data = length 16384, hash 48AB8B45 + sample 12: + time = 1877333 + flags = 1 + data = length 16384, hash 13C9640A + sample 13: + time = 1962666 + flags = 1 + data = length 16384, hash 499E4A0B + sample 14: + time = 2048000 + flags = 1 + data = length 16384, hash F9A783E6 + sample 15: + time = 2133333 + flags = 1 + data = length 16384, hash D2B77598 + sample 16: + time = 2218666 + flags = 1 + data = length 16384, hash CE5B826C + sample 17: + time = 2304000 + flags = 1 + data = length 16384, hash E99EE956 + sample 18: + time = 2389333 + flags = 1 + data = length 16384, hash F2DB1486 + sample 19: + time = 2474666 + flags = 1 + data = length 16384, hash 1636EAB + sample 20: + time = 2560000 + flags = 1 + data = length 16384, hash 23457C08 + sample 21: + time = 2645333 + flags = 1 + data = length 16384, hash 30EB8381 + sample 22: + time = 2730666 + flags = 1 + data = length 1984, hash 59CFDE1B +tracksEnded = true diff --git a/testdata/src/test/assets/flac/bear_raw.2.dump b/testdata/src/test/assets/flac/bear_raw.2.dump new file mode 100644 index 0000000000..86d36c9e24 --- /dev/null +++ b/testdata/src/test/assets/flac/bear_raw.2.dump @@ -0,0 +1,68 @@ +seekMap: + isSeekable = true + duration = 2741000 + getPosition(0) = [[timeUs=0, position=8880]] + getPosition(1) = [[timeUs=0, position=8880]] + getPosition(1370500) = [[timeUs=1365333, position=90825], [timeUs=1450666, position=96601]] + getPosition(2741000) = [[timeUs=2645333, position=169482]] +numberOfTracks = 1 +track 0: + total output bytes = 182208 + sample count = 12 + format 0: + averageBitrate = 1536000 + peakBitrate = 1536000 + sampleMimeType = audio/raw + maxInputSize = 16384 + channelCount = 2 + sampleRate = 48000 + pcmEncoding = 2 + sample 0: + time = 1792000 + flags = 1 + data = length 16384, hash 48AB8B45 + sample 1: + time = 1877333 + flags = 1 + data = length 16384, hash 13C9640A + sample 2: + time = 1962666 + flags = 1 + data = length 16384, hash 499E4A0B + sample 3: + time = 2048000 + flags = 1 + data = length 16384, hash F9A783E6 + sample 4: + time = 2133333 + flags = 1 + data = length 16384, hash D2B77598 + sample 5: + time = 2218666 + flags = 1 + data = length 16384, hash CE5B826C + sample 6: + time = 2304000 + flags = 1 + data = length 16384, hash E99EE956 + sample 7: + time = 2389333 + flags = 1 + data = length 16384, hash F2DB1486 + sample 8: + time = 2474666 + flags = 1 + data = length 16384, hash 1636EAB + sample 9: + time = 2560000 + flags = 1 + data = length 16384, hash 23457C08 + sample 10: + time = 2645333 + flags = 1 + data = length 16384, hash 30EB8381 + sample 11: + time = 2730666 + flags = 1 + data = length 1984, hash 59CFDE1B +tracksEnded = true diff --git a/testdata/src/test/assets/flac/bear_raw.3.dump b/testdata/src/test/assets/flac/bear_raw.3.dump new file mode 100644 index 0000000000..2ad86792ff --- /dev/null +++ b/testdata/src/test/assets/flac/bear_raw.3.dump @@ -0,0 +1,28 @@ +seekMap: + isSeekable = true + duration = 2741000 + getPosition(0) = [[timeUs=0, position=8880]] + getPosition(1) = [[timeUs=0, position=8880]] + getPosition(1370500) = [[timeUs=1365333, position=90825], [timeUs=1450666, position=96601]] + getPosition(2741000) = [[timeUs=2645333, position=169482]] +numberOfTracks = 1 +track 0: + total output bytes = 18368 + sample count = 2 + format 0: + averageBitrate = 1536000 + peakBitrate = 1536000 + sampleMimeType = audio/raw + maxInputSize = 16384 + channelCount = 2 + sampleRate = 48000 + pcmEncoding = 2 + sample 0: + time = 2645333 + flags = 1 + data = length 16384, hash 30EB8381 + sample 1: + time = 2730666 + flags = 1 + data = length 1984, hash 59CFDE1B +tracksEnded = true diff --git a/testdata/src/test/assets/flac/bear_raw.unknown_length.dump b/testdata/src/test/assets/flac/bear_raw.unknown_length.dump new file mode 100644 index 0000000000..37eee44b2e --- /dev/null +++ b/testdata/src/test/assets/flac/bear_raw.unknown_length.dump @@ -0,0 +1,152 @@ +seekMap: + isSeekable = true + duration = 2741000 + getPosition(0) = [[timeUs=0, position=8880]] + getPosition(1) = [[timeUs=0, position=8880]] + getPosition(1370500) = [[timeUs=1365333, position=90825], [timeUs=1450666, position=96601]] + getPosition(2741000) = [[timeUs=2645333, position=169482]] +numberOfTracks = 1 +track 0: + total output bytes = 526272 + sample count = 33 + format 0: + averageBitrate = 1536000 + peakBitrate = 1536000 + sampleMimeType = audio/raw + maxInputSize = 16384 + channelCount = 2 + sampleRate = 48000 + pcmEncoding = 2 + sample 0: + time = 0 + flags = 1 + data = length 16384, hash 61D2C5C2 + sample 1: + time = 85333 + flags = 1 + data = length 16384, hash E6D7F214 + sample 2: + time = 170666 + flags = 1 + data = length 16384, hash 59BF0D5D + sample 3: + time = 256000 + flags = 1 + data = length 16384, hash 3625F468 + sample 4: + time = 341333 + flags = 1 + data = length 16384, hash F66A323 + sample 5: + time = 426666 + flags = 1 + data = length 16384, hash CDBAE629 + sample 6: + time = 512000 + flags = 1 + data = length 16384, hash 536F3A91 + sample 7: + time = 597333 + flags = 1 + data = length 16384, hash D4F35C9C + sample 8: + time = 682666 + flags = 1 + data = length 16384, hash EE04CEBF + sample 9: + time = 768000 + flags = 1 + data = length 16384, hash 647E2A67 + sample 10: + time = 853333 + flags = 1 + data = length 16384, hash 31583F2C + sample 11: + time = 938666 + flags = 1 + data = length 16384, hash E433A93D + sample 12: + time = 1024000 + flags = 1 + data = length 16384, hash 5E1C7051 + sample 13: + time = 1109333 + flags = 1 + data = length 16384, hash 43E6E358 + sample 14: + time = 1194666 + flags = 1 + data = length 16384, hash 5DC1B256 + sample 15: + time = 1280000 + flags = 1 + data = length 16384, hash 3D9D95CF + sample 16: + time = 1365333 + flags = 1 + data = length 16384, hash 2A5BD2C0 + sample 17: + time = 1450666 + flags = 1 + data = length 16384, hash 93E25061 + sample 18: + time = 1536000 + flags = 1 + data = length 16384, hash B81793D8 + sample 19: + time = 1621333 + flags = 1 + data = length 16384, hash 1A3BD49F + sample 20: + time = 1706666 + flags = 1 + data = length 16384, hash FB672FF1 + sample 21: + time = 1792000 + flags = 1 + data = length 16384, hash 48AB8B45 + sample 22: + time = 1877333 + flags = 1 + data = length 16384, hash 13C9640A + sample 23: + time = 1962666 + flags = 1 + data = length 16384, hash 499E4A0B + sample 24: + time = 2048000 + flags = 1 + data = length 16384, hash F9A783E6 + sample 25: + time = 2133333 + flags = 1 + data = length 16384, hash D2B77598 + sample 26: + time = 2218666 + flags = 1 + data = length 16384, hash CE5B826C + sample 27: + time = 2304000 + flags = 1 + data = length 16384, hash E99EE956 + sample 28: + time = 2389333 + flags = 1 + data = length 16384, hash F2DB1486 + sample 29: + time = 2474666 + flags = 1 + data = length 16384, hash 1636EAB + sample 30: + time = 2560000 + flags = 1 + data = length 16384, hash 23457C08 + sample 31: + time = 2645333 + flags = 1 + data = length 16384, hash 30EB8381 + sample 32: + time = 2730666 + flags = 1 + data = length 1984, hash 59CFDE1B +tracksEnded = true diff --git a/library/core/src/test/assets/flac/bear_uncommon_sample_rate.flac b/testdata/src/test/assets/flac/bear_uncommon_sample_rate.flac similarity index 99% rename from library/core/src/test/assets/flac/bear_uncommon_sample_rate.flac rename to testdata/src/test/assets/flac/bear_uncommon_sample_rate.flac index 2fc58661eb..e2e0b6a83d 100644 Binary files a/library/core/src/test/assets/flac/bear_uncommon_sample_rate.flac and b/testdata/src/test/assets/flac/bear_uncommon_sample_rate.flac differ diff --git a/library/core/src/test/assets/flac/bear_uncommon_sample_rate.flac.0.dump b/testdata/src/test/assets/flac/bear_uncommon_sample_rate_flac.0.dump similarity index 85% rename from library/core/src/test/assets/flac/bear_uncommon_sample_rate.flac.0.dump rename to testdata/src/test/assets/flac/bear_uncommon_sample_rate_flac.0.dump index 6ad50afc29..5e197bd2bf 100644 --- a/library/core/src/test/assets/flac/bear_uncommon_sample_rate.flac.0.dump +++ b/testdata/src/test/assets/flac/bear_uncommon_sample_rate_flac.0.dump @@ -1,33 +1,21 @@ seekMap: - isSeekable = false + isSeekable = true duration = 2741000 - getPosition(0) = [[timeUs=0, position=0]] + getPosition(0) = [[timeUs=0, position=8230]] + getPosition(1) = [[timeUs=0, position=8230], [timeUs=104727, position=13645]] + getPosition(1370500) = [[timeUs=1361454, position=80172], [timeUs=1466181, position=86628]] + getPosition(2741000) = [[timeUs=2618181, position=148931]] numberOfTracks = 1 track 0: - format: - bitrate = 1408000 - id = null - containerMimeType = null - sampleMimeType = audio/flac - maxInputSize = 6456 - width = -1 - height = -1 - frameRate = -1.0 - rotationDegrees = 0 - pixelWidthHeightRatio = 1.0 - channelCount = 2 - sampleRate = 44000 - pcmEncoding = -1 - encoderDelay = 0 - encoderPadding = 0 - subsampleOffsetUs = 9223372036854775807 - selectionFlags = 0 - language = null - drmInitData = - - initializationData: - data = length 42, hash 7249A1B8 total output bytes = 144086 sample count = 27 + format 0: + sampleMimeType = audio/flac + maxInputSize = 6456 + channelCount = 2 + sampleRate = 44000 + initializationData: + data = length 42, hash 7249A1B8 sample 0: time = 0 flags = 1 diff --git a/testdata/src/test/assets/flac/bear_uncommon_sample_rate_flac.1.dump b/testdata/src/test/assets/flac/bear_uncommon_sample_rate_flac.1.dump new file mode 100644 index 0000000000..3845602fa4 --- /dev/null +++ b/testdata/src/test/assets/flac/bear_uncommon_sample_rate_flac.1.dump @@ -0,0 +1,95 @@ +seekMap: + isSeekable = true + duration = 2741000 + getPosition(0) = [[timeUs=0, position=8230]] + getPosition(1) = [[timeUs=0, position=8230], [timeUs=104727, position=13645]] + getPosition(1370500) = [[timeUs=1361454, position=80172], [timeUs=1466181, position=86628]] + getPosition(2741000) = [[timeUs=2618181, position=148931]] +numberOfTracks = 1 +track 0: + total output bytes = 100240 + sample count = 19 + format 0: + sampleMimeType = audio/flac + maxInputSize = 6456 + channelCount = 2 + sampleRate = 44000 + initializationData: + data = length 42, hash 7249A1B8 + sample 0: + time = 837818 + flags = 1 + data = length 5414, hash 90768345 + sample 1: + time = 942545 + flags = 1 + data = length 5531, hash 1CD2FF67 + sample 2: + time = 1047272 + flags = 1 + data = length 5870, hash A9A5CAEE + sample 3: + time = 1152000 + flags = 1 + data = length 5667, hash 875566A1 + sample 4: + time = 1256727 + flags = 1 + data = length 5614, hash 5573694C + sample 5: + time = 1361454 + flags = 1 + data = length 6456, hash 921F3DE7 + sample 6: + time = 1466181 + flags = 1 + data = length 5817, hash EBECBD16 + sample 7: + time = 1570909 + flags = 1 + data = length 5751, hash 4A7D4C6B + sample 8: + time = 1675636 + flags = 1 + data = length 5620, hash B78F8E8D + sample 9: + time = 1780363 + flags = 1 + data = length 5535, hash 8187C107 + sample 10: + time = 1885090 + flags = 1 + data = length 5517, hash 79FF36CB + sample 11: + time = 1989818 + flags = 1 + data = length 5716, hash 349FC281 + sample 12: + time = 2094545 + flags = 1 + data = length 5556, hash BE97B2CA + sample 13: + time = 2199272 + flags = 1 + data = length 5703, hash 531F9FE3 + sample 14: + time = 2304000 + flags = 1 + data = length 5652, hash 1277485D + sample 15: + time = 2408727 + flags = 1 + data = length 5607, hash 14862CB6 + sample 16: + time = 2513454 + flags = 1 + data = length 5829, hash FCAF2F1C + sample 17: + time = 2618181 + flags = 1 + data = length 2837, hash 10F1716E + sample 18: + time = 2722909 + flags = 1 + data = length 548, hash B46F603C +tracksEnded = true diff --git a/testdata/src/test/assets/flac/bear_uncommon_sample_rate_flac.2.dump b/testdata/src/test/assets/flac/bear_uncommon_sample_rate_flac.2.dump new file mode 100644 index 0000000000..1f68fe1905 --- /dev/null +++ b/testdata/src/test/assets/flac/bear_uncommon_sample_rate_flac.2.dump @@ -0,0 +1,59 @@ +seekMap: + isSeekable = true + duration = 2741000 + getPosition(0) = [[timeUs=0, position=8230]] + getPosition(1) = [[timeUs=0, position=8230], [timeUs=104727, position=13645]] + getPosition(1370500) = [[timeUs=1361454, position=80172], [timeUs=1466181, position=86628]] + getPosition(2741000) = [[timeUs=2618181, position=148931]] +numberOfTracks = 1 +track 0: + total output bytes = 48500 + sample count = 10 + format 0: + sampleMimeType = audio/flac + maxInputSize = 6456 + channelCount = 2 + sampleRate = 44000 + initializationData: + data = length 42, hash 7249A1B8 + sample 0: + time = 1780363 + flags = 1 + data = length 5535, hash 8187C107 + sample 1: + time = 1885090 + flags = 1 + data = length 5517, hash 79FF36CB + sample 2: + time = 1989818 + flags = 1 + data = length 5716, hash 349FC281 + sample 3: + time = 2094545 + flags = 1 + data = length 5556, hash BE97B2CA + sample 4: + time = 2199272 + flags = 1 + data = length 5703, hash 531F9FE3 + sample 5: + time = 2304000 + flags = 1 + data = length 5652, hash 1277485D + sample 6: + time = 2408727 + flags = 1 + data = length 5607, hash 14862CB6 + sample 7: + time = 2513454 + flags = 1 + data = length 5829, hash FCAF2F1C + sample 8: + time = 2618181 + flags = 1 + data = length 2837, hash 10F1716E + sample 9: + time = 2722909 + flags = 1 + data = length 548, hash B46F603C +tracksEnded = true diff --git a/testdata/src/test/assets/flac/bear_uncommon_sample_rate_flac.3.dump b/testdata/src/test/assets/flac/bear_uncommon_sample_rate_flac.3.dump new file mode 100644 index 0000000000..334f59a1d8 --- /dev/null +++ b/testdata/src/test/assets/flac/bear_uncommon_sample_rate_flac.3.dump @@ -0,0 +1,27 @@ +seekMap: + isSeekable = true + duration = 2741000 + getPosition(0) = [[timeUs=0, position=8230]] + getPosition(1) = [[timeUs=0, position=8230], [timeUs=104727, position=13645]] + getPosition(1370500) = [[timeUs=1361454, position=80172], [timeUs=1466181, position=86628]] + getPosition(2741000) = [[timeUs=2618181, position=148931]] +numberOfTracks = 1 +track 0: + total output bytes = 3385 + sample count = 2 + format 0: + sampleMimeType = audio/flac + maxInputSize = 6456 + channelCount = 2 + sampleRate = 44000 + initializationData: + data = length 42, hash 7249A1B8 + sample 0: + time = 2618181 + flags = 1 + data = length 2837, hash 10F1716E + sample 1: + time = 2722909 + flags = 1 + data = length 548, hash B46F603C +tracksEnded = true diff --git a/testdata/src/test/assets/flac/bear_uncommon_sample_rate_flac.unknown_length.dump b/testdata/src/test/assets/flac/bear_uncommon_sample_rate_flac.unknown_length.dump new file mode 100644 index 0000000000..5e197bd2bf --- /dev/null +++ b/testdata/src/test/assets/flac/bear_uncommon_sample_rate_flac.unknown_length.dump @@ -0,0 +1,127 @@ +seekMap: + isSeekable = true + duration = 2741000 + getPosition(0) = [[timeUs=0, position=8230]] + getPosition(1) = [[timeUs=0, position=8230], [timeUs=104727, position=13645]] + getPosition(1370500) = [[timeUs=1361454, position=80172], [timeUs=1466181, position=86628]] + getPosition(2741000) = [[timeUs=2618181, position=148931]] +numberOfTracks = 1 +track 0: + total output bytes = 144086 + sample count = 27 + format 0: + sampleMimeType = audio/flac + maxInputSize = 6456 + channelCount = 2 + sampleRate = 44000 + initializationData: + data = length 42, hash 7249A1B8 + sample 0: + time = 0 + flags = 1 + data = length 5415, hash 915DBC66 + sample 1: + time = 104727 + flags = 1 + data = length 5529, hash EFD564F7 + sample 2: + time = 209454 + flags = 1 + data = length 5480, hash ADA922FB + sample 3: + time = 314181 + flags = 1 + data = length 5290, hash 7BCEA5FC + sample 4: + time = 418909 + flags = 1 + data = length 5579, hash DBB36F37 + sample 5: + time = 523636 + flags = 1 + data = length 5423, hash AB53F799 + sample 6: + time = 628363 + flags = 1 + data = length 5583, hash 7243C284 + sample 7: + time = 733090 + flags = 1 + data = length 5547, hash 9DA9C99E + sample 8: + time = 837818 + flags = 1 + data = length 5414, hash 90768345 + sample 9: + time = 942545 + flags = 1 + data = length 5531, hash 1CD2FF67 + sample 10: + time = 1047272 + flags = 1 + data = length 5870, hash A9A5CAEE + sample 11: + time = 1152000 + flags = 1 + data = length 5667, hash 875566A1 + sample 12: + time = 1256727 + flags = 1 + data = length 5614, hash 5573694C + sample 13: + time = 1361454 + flags = 1 + data = length 6456, hash 921F3DE7 + sample 14: + time = 1466181 + flags = 1 + data = length 5817, hash EBECBD16 + sample 15: + time = 1570909 + flags = 1 + data = length 5751, hash 4A7D4C6B + sample 16: + time = 1675636 + flags = 1 + data = length 5620, hash B78F8E8D + sample 17: + time = 1780363 + flags = 1 + data = length 5535, hash 8187C107 + sample 18: + time = 1885090 + flags = 1 + data = length 5517, hash 79FF36CB + sample 19: + time = 1989818 + flags = 1 + data = length 5716, hash 349FC281 + sample 20: + time = 2094545 + flags = 1 + data = length 5556, hash BE97B2CA + sample 21: + time = 2199272 + flags = 1 + data = length 5703, hash 531F9FE3 + sample 22: + time = 2304000 + flags = 1 + data = length 5652, hash 1277485D + sample 23: + time = 2408727 + flags = 1 + data = length 5607, hash 14862CB6 + sample 24: + time = 2513454 + flags = 1 + data = length 5829, hash FCAF2F1C + sample 25: + time = 2618181 + flags = 1 + data = length 2837, hash 10F1716E + sample 26: + time = 2722909 + flags = 1 + data = length 548, hash B46F603C +tracksEnded = true diff --git a/testdata/src/test/assets/flac/bear_uncommon_sample_rate_raw.0.dump b/testdata/src/test/assets/flac/bear_uncommon_sample_rate_raw.0.dump new file mode 100644 index 0000000000..7c72df42b5 --- /dev/null +++ b/testdata/src/test/assets/flac/bear_uncommon_sample_rate_raw.0.dump @@ -0,0 +1,128 @@ +seekMap: + isSeekable = true + duration = 2741000 + getPosition(0) = [[timeUs=0, position=8230]] + getPosition(1) = [[timeUs=0, position=8230]] + getPosition(1370500) = [[timeUs=1361454, position=80172], [timeUs=1466181, position=86628]] + getPosition(2741000) = [[timeUs=2618181, position=148931]] +numberOfTracks = 1 +track 0: + total output bytes = 482416 + sample count = 27 + format 0: + averageBitrate = 1408000 + peakBitrate = 1408000 + sampleMimeType = audio/raw + maxInputSize = 18432 + channelCount = 2 + sampleRate = 44000 + pcmEncoding = 2 + sample 0: + time = 0 + flags = 1 + data = length 18432, hash A5825483 + sample 1: + time = 104727 + flags = 1 + data = length 18432, hash 98A0A8C9 + sample 2: + time = 209454 + flags = 1 + data = length 18432, hash 608A711B + sample 3: + time = 314181 + flags = 1 + data = length 18432, hash 1A9BCD1C + sample 4: + time = 418909 + flags = 1 + data = length 18432, hash 36E34B7F + sample 5: + time = 523636 + flags = 1 + data = length 18432, hash 29A75C1F + sample 6: + time = 628363 + flags = 1 + data = length 18432, hash 9E3A2940 + sample 7: + time = 733090 + flags = 1 + data = length 18432, hash A7DF91EC + sample 8: + time = 837818 + flags = 1 + data = length 18432, hash 2B548030 + sample 9: + time = 942545 + flags = 1 + data = length 18432, hash F905C735 + sample 10: + time = 1047272 + flags = 1 + data = length 18432, hash D157292A + sample 11: + time = 1152000 + flags = 1 + data = length 18432, hash 1F721B83 + sample 12: + time = 1256727 + flags = 1 + data = length 18432, hash AD520766 + sample 13: + time = 1361454 + flags = 1 + data = length 18432, hash 7C6777AC + sample 14: + time = 1466181 + flags = 1 + data = length 18432, hash CC5E4807 + sample 15: + time = 1570909 + flags = 1 + data = length 18432, hash 5542687C + sample 16: + time = 1675636 + flags = 1 + data = length 18432, hash 5C3E9A66 + sample 17: + time = 1780363 + flags = 1 + data = length 18432, hash E868F171 + sample 18: + time = 1885090 + flags = 1 + data = length 18432, hash 849820FB + sample 19: + time = 1989818 + flags = 1 + data = length 18432, hash 7C0F587C + sample 20: + time = 2094545 + flags = 1 + data = length 18432, hash C062C499 + sample 21: + time = 2199272 + flags = 1 + data = length 18432, hash 3A5DE9D0 + sample 22: + time = 2304000 + flags = 1 + data = length 18432, hash ACADCDDB + sample 23: + time = 2408727 + flags = 1 + data = length 18432, hash EEC815A8 + sample 24: + time = 2513454 + flags = 1 + data = length 18432, hash 29FC8C0B + sample 25: + time = 2618181 + flags = 1 + data = length 18432, hash 1C5969B5 + sample 26: + time = 2722909 + flags = 1 + data = length 3184, hash 1DB009F7 +tracksEnded = true diff --git a/testdata/src/test/assets/flac/bear_uncommon_sample_rate_raw.1.dump b/testdata/src/test/assets/flac/bear_uncommon_sample_rate_raw.1.dump new file mode 100644 index 0000000000..7da240be3d --- /dev/null +++ b/testdata/src/test/assets/flac/bear_uncommon_sample_rate_raw.1.dump @@ -0,0 +1,96 @@ +seekMap: + isSeekable = true + duration = 2741000 + getPosition(0) = [[timeUs=0, position=8230]] + getPosition(1) = [[timeUs=0, position=8230]] + getPosition(1370500) = [[timeUs=1361454, position=80172], [timeUs=1466181, position=86628]] + getPosition(2741000) = [[timeUs=2618181, position=148931]] +numberOfTracks = 1 +track 0: + total output bytes = 334960 + sample count = 19 + format 0: + averageBitrate = 1408000 + peakBitrate = 1408000 + sampleMimeType = audio/raw + maxInputSize = 18432 + channelCount = 2 + sampleRate = 44000 + pcmEncoding = 2 + sample 0: + time = 837818 + flags = 1 + data = length 18432, hash 2B548030 + sample 1: + time = 942545 + flags = 1 + data = length 18432, hash F905C735 + sample 2: + time = 1047272 + flags = 1 + data = length 18432, hash D157292A + sample 3: + time = 1152000 + flags = 1 + data = length 18432, hash 1F721B83 + sample 4: + time = 1256727 + flags = 1 + data = length 18432, hash AD520766 + sample 5: + time = 1361454 + flags = 1 + data = length 18432, hash 7C6777AC + sample 6: + time = 1466181 + flags = 1 + data = length 18432, hash CC5E4807 + sample 7: + time = 1570909 + flags = 1 + data = length 18432, hash 5542687C + sample 8: + time = 1675636 + flags = 1 + data = length 18432, hash 5C3E9A66 + sample 9: + time = 1780363 + flags = 1 + data = length 18432, hash E868F171 + sample 10: + time = 1885090 + flags = 1 + data = length 18432, hash 849820FB + sample 11: + time = 1989818 + flags = 1 + data = length 18432, hash 7C0F587C + sample 12: + time = 2094545 + flags = 1 + data = length 18432, hash C062C499 + sample 13: + time = 2199272 + flags = 1 + data = length 18432, hash 3A5DE9D0 + sample 14: + time = 2304000 + flags = 1 + data = length 18432, hash ACADCDDB + sample 15: + time = 2408727 + flags = 1 + data = length 18432, hash EEC815A8 + sample 16: + time = 2513454 + flags = 1 + data = length 18432, hash 29FC8C0B + sample 17: + time = 2618181 + flags = 1 + data = length 18432, hash 1C5969B5 + sample 18: + time = 2722909 + flags = 1 + data = length 3184, hash 1DB009F7 +tracksEnded = true diff --git a/testdata/src/test/assets/flac/bear_uncommon_sample_rate_raw.2.dump b/testdata/src/test/assets/flac/bear_uncommon_sample_rate_raw.2.dump new file mode 100644 index 0000000000..3943c77600 --- /dev/null +++ b/testdata/src/test/assets/flac/bear_uncommon_sample_rate_raw.2.dump @@ -0,0 +1,60 @@ +seekMap: + isSeekable = true + duration = 2741000 + getPosition(0) = [[timeUs=0, position=8230]] + getPosition(1) = [[timeUs=0, position=8230]] + getPosition(1370500) = [[timeUs=1361454, position=80172], [timeUs=1466181, position=86628]] + getPosition(2741000) = [[timeUs=2618181, position=148931]] +numberOfTracks = 1 +track 0: + total output bytes = 169072 + sample count = 10 + format 0: + averageBitrate = 1408000 + peakBitrate = 1408000 + sampleMimeType = audio/raw + maxInputSize = 18432 + channelCount = 2 + sampleRate = 44000 + pcmEncoding = 2 + sample 0: + time = 1780363 + flags = 1 + data = length 18432, hash E868F171 + sample 1: + time = 1885090 + flags = 1 + data = length 18432, hash 849820FB + sample 2: + time = 1989818 + flags = 1 + data = length 18432, hash 7C0F587C + sample 3: + time = 2094545 + flags = 1 + data = length 18432, hash C062C499 + sample 4: + time = 2199272 + flags = 1 + data = length 18432, hash 3A5DE9D0 + sample 5: + time = 2304000 + flags = 1 + data = length 18432, hash ACADCDDB + sample 6: + time = 2408727 + flags = 1 + data = length 18432, hash EEC815A8 + sample 7: + time = 2513454 + flags = 1 + data = length 18432, hash 29FC8C0B + sample 8: + time = 2618181 + flags = 1 + data = length 18432, hash 1C5969B5 + sample 9: + time = 2722909 + flags = 1 + data = length 3184, hash 1DB009F7 +tracksEnded = true diff --git a/testdata/src/test/assets/flac/bear_uncommon_sample_rate_raw.3.dump b/testdata/src/test/assets/flac/bear_uncommon_sample_rate_raw.3.dump new file mode 100644 index 0000000000..18231418b9 --- /dev/null +++ b/testdata/src/test/assets/flac/bear_uncommon_sample_rate_raw.3.dump @@ -0,0 +1,28 @@ +seekMap: + isSeekable = true + duration = 2741000 + getPosition(0) = [[timeUs=0, position=8230]] + getPosition(1) = [[timeUs=0, position=8230]] + getPosition(1370500) = [[timeUs=1361454, position=80172], [timeUs=1466181, position=86628]] + getPosition(2741000) = [[timeUs=2618181, position=148931]] +numberOfTracks = 1 +track 0: + total output bytes = 21616 + sample count = 2 + format 0: + averageBitrate = 1408000 + peakBitrate = 1408000 + sampleMimeType = audio/raw + maxInputSize = 18432 + channelCount = 2 + sampleRate = 44000 + pcmEncoding = 2 + sample 0: + time = 2618181 + flags = 1 + data = length 18432, hash 1C5969B5 + sample 1: + time = 2722909 + flags = 1 + data = length 3184, hash 1DB009F7 +tracksEnded = true diff --git a/testdata/src/test/assets/flac/bear_uncommon_sample_rate_raw.unknown_length.dump b/testdata/src/test/assets/flac/bear_uncommon_sample_rate_raw.unknown_length.dump new file mode 100644 index 0000000000..7c72df42b5 --- /dev/null +++ b/testdata/src/test/assets/flac/bear_uncommon_sample_rate_raw.unknown_length.dump @@ -0,0 +1,128 @@ +seekMap: + isSeekable = true + duration = 2741000 + getPosition(0) = [[timeUs=0, position=8230]] + getPosition(1) = [[timeUs=0, position=8230]] + getPosition(1370500) = [[timeUs=1361454, position=80172], [timeUs=1466181, position=86628]] + getPosition(2741000) = [[timeUs=2618181, position=148931]] +numberOfTracks = 1 +track 0: + total output bytes = 482416 + sample count = 27 + format 0: + averageBitrate = 1408000 + peakBitrate = 1408000 + sampleMimeType = audio/raw + maxInputSize = 18432 + channelCount = 2 + sampleRate = 44000 + pcmEncoding = 2 + sample 0: + time = 0 + flags = 1 + data = length 18432, hash A5825483 + sample 1: + time = 104727 + flags = 1 + data = length 18432, hash 98A0A8C9 + sample 2: + time = 209454 + flags = 1 + data = length 18432, hash 608A711B + sample 3: + time = 314181 + flags = 1 + data = length 18432, hash 1A9BCD1C + sample 4: + time = 418909 + flags = 1 + data = length 18432, hash 36E34B7F + sample 5: + time = 523636 + flags = 1 + data = length 18432, hash 29A75C1F + sample 6: + time = 628363 + flags = 1 + data = length 18432, hash 9E3A2940 + sample 7: + time = 733090 + flags = 1 + data = length 18432, hash A7DF91EC + sample 8: + time = 837818 + flags = 1 + data = length 18432, hash 2B548030 + sample 9: + time = 942545 + flags = 1 + data = length 18432, hash F905C735 + sample 10: + time = 1047272 + flags = 1 + data = length 18432, hash D157292A + sample 11: + time = 1152000 + flags = 1 + data = length 18432, hash 1F721B83 + sample 12: + time = 1256727 + flags = 1 + data = length 18432, hash AD520766 + sample 13: + time = 1361454 + flags = 1 + data = length 18432, hash 7C6777AC + sample 14: + time = 1466181 + flags = 1 + data = length 18432, hash CC5E4807 + sample 15: + time = 1570909 + flags = 1 + data = length 18432, hash 5542687C + sample 16: + time = 1675636 + flags = 1 + data = length 18432, hash 5C3E9A66 + sample 17: + time = 1780363 + flags = 1 + data = length 18432, hash E868F171 + sample 18: + time = 1885090 + flags = 1 + data = length 18432, hash 849820FB + sample 19: + time = 1989818 + flags = 1 + data = length 18432, hash 7C0F587C + sample 20: + time = 2094545 + flags = 1 + data = length 18432, hash C062C499 + sample 21: + time = 2199272 + flags = 1 + data = length 18432, hash 3A5DE9D0 + sample 22: + time = 2304000 + flags = 1 + data = length 18432, hash ACADCDDB + sample 23: + time = 2408727 + flags = 1 + data = length 18432, hash EEC815A8 + sample 24: + time = 2513454 + flags = 1 + data = length 18432, hash 29FC8C0B + sample 25: + time = 2618181 + flags = 1 + data = length 18432, hash 1C5969B5 + sample 26: + time = 2722909 + flags = 1 + data = length 3184, hash 1DB009F7 +tracksEnded = true diff --git a/library/core/src/test/assets/flac/bear_with_id3.flac b/testdata/src/test/assets/flac/bear_with_id3.flac similarity index 100% rename from library/core/src/test/assets/flac/bear_with_id3.flac rename to testdata/src/test/assets/flac/bear_with_id3.flac diff --git a/testdata/src/test/assets/flac/bear_with_id3_disabled_flac.0.dump b/testdata/src/test/assets/flac/bear_with_id3_disabled_flac.0.dump new file mode 100644 index 0000000000..3b3f6611be --- /dev/null +++ b/testdata/src/test/assets/flac/bear_with_id3_disabled_flac.0.dump @@ -0,0 +1,151 @@ +seekMap: + isSeekable = true + duration = 2741000 + getPosition(0) = [[timeUs=0, position=55284]] + getPosition(1) = [[timeUs=0, position=55284], [timeUs=85333, position=60314]] + getPosition(1370500) = [[timeUs=1365333, position=137229], [timeUs=1450666, position=143005]] + getPosition(2741000) = [[timeUs=2645333, position=215886]] +numberOfTracks = 1 +track 0: + total output bytes = 164431 + sample count = 33 + format 0: + sampleMimeType = audio/flac + maxInputSize = 5776 + channelCount = 2 + sampleRate = 48000 + initializationData: + data = length 42, hash 83F6895 + sample 0: + time = 0 + flags = 1 + data = length 5030, hash D2B60530 + sample 1: + time = 85333 + flags = 1 + data = length 5066, hash 4C932A54 + sample 2: + time = 170666 + flags = 1 + data = length 5112, hash 7E5A7B61 + sample 3: + time = 256000 + flags = 1 + data = length 5044, hash 7EF93F13 + sample 4: + time = 341333 + flags = 1 + data = length 4943, hash DE7E27F8 + sample 5: + time = 426666 + flags = 1 + data = length 5121, hash 6D0D0B40 + sample 6: + time = 512000 + flags = 1 + data = length 5068, hash 9924644F + sample 7: + time = 597333 + flags = 1 + data = length 5143, hash 6C34F0CE + sample 8: + time = 682666 + flags = 1 + data = length 5109, hash E3B7BEFB + sample 9: + time = 768000 + flags = 1 + data = length 5129, hash 44111D9B + sample 10: + time = 853333 + flags = 1 + data = length 5031, hash 9D55EA53 + sample 11: + time = 938666 + flags = 1 + data = length 5119, hash E1CB9BA6 + sample 12: + time = 1024000 + flags = 1 + data = length 5360, hash 17265C5D + sample 13: + time = 1109333 + flags = 1 + data = length 5340, hash A90FDDF1 + sample 14: + time = 1194666 + flags = 1 + data = length 5162, hash 31F65AD5 + sample 15: + time = 1280000 + flags = 1 + data = length 5168, hash F2394F2D + sample 16: + time = 1365333 + flags = 1 + data = length 5776, hash 58437AB3 + sample 17: + time = 1450666 + flags = 1 + data = length 5394, hash EBAB20A8 + sample 18: + time = 1536000 + flags = 1 + data = length 5168, hash BF37C7A5 + sample 19: + time = 1621333 + flags = 1 + data = length 5324, hash 59546B7B + sample 20: + time = 1706666 + flags = 1 + data = length 5172, hash 6036EF0B + sample 21: + time = 1792000 + flags = 1 + data = length 5102, hash 5A131071 + sample 22: + time = 1877333 + flags = 1 + data = length 5111, hash 3D9EBB3B + sample 23: + time = 1962666 + flags = 1 + data = length 5113, hash 61101D4F + sample 24: + time = 2048000 + flags = 1 + data = length 5229, hash D2E55742 + sample 25: + time = 2133333 + flags = 1 + data = length 5162, hash 7F2E97FA + sample 26: + time = 2218666 + flags = 1 + data = length 5255, hash D92A782 + sample 27: + time = 2304000 + flags = 1 + data = length 5196, hash 98FE5138 + sample 28: + time = 2389333 + flags = 1 + data = length 5214, hash 3D35C38C + sample 29: + time = 2474666 + flags = 1 + data = length 5211, hash 7E25420F + sample 30: + time = 2560000 + flags = 1 + data = length 5230, hash 2AD96FBC + sample 31: + time = 2645333 + flags = 1 + data = length 3384, hash 938BCDD9 + sample 32: + time = 2730666 + flags = 1 + data = length 445, hash A388E3D6 +tracksEnded = true diff --git a/testdata/src/test/assets/flac/bear_with_id3_disabled_flac.1.dump b/testdata/src/test/assets/flac/bear_with_id3_disabled_flac.1.dump new file mode 100644 index 0000000000..57501d7c1b --- /dev/null +++ b/testdata/src/test/assets/flac/bear_with_id3_disabled_flac.1.dump @@ -0,0 +1,111 @@ +seekMap: + isSeekable = true + duration = 2741000 + getPosition(0) = [[timeUs=0, position=55284]] + getPosition(1) = [[timeUs=0, position=55284], [timeUs=85333, position=60314]] + getPosition(1370500) = [[timeUs=1365333, position=137229], [timeUs=1450666, position=143005]] + getPosition(2741000) = [[timeUs=2645333, position=215886]] +numberOfTracks = 1 +track 0: + total output bytes = 113666 + sample count = 23 + format 0: + sampleMimeType = audio/flac + maxInputSize = 5776 + channelCount = 2 + sampleRate = 48000 + initializationData: + data = length 42, hash 83F6895 + sample 0: + time = 853333 + flags = 1 + data = length 5031, hash 9D55EA53 + sample 1: + time = 938666 + flags = 1 + data = length 5119, hash E1CB9BA6 + sample 2: + time = 1024000 + flags = 1 + data = length 5360, hash 17265C5D + sample 3: + time = 1109333 + flags = 1 + data = length 5340, hash A90FDDF1 + sample 4: + time = 1194666 + flags = 1 + data = length 5162, hash 31F65AD5 + sample 5: + time = 1280000 + flags = 1 + data = length 5168, hash F2394F2D + sample 6: + time = 1365333 + flags = 1 + data = length 5776, hash 58437AB3 + sample 7: + time = 1450666 + flags = 1 + data = length 5394, hash EBAB20A8 + sample 8: + time = 1536000 + flags = 1 + data = length 5168, hash BF37C7A5 + sample 9: + time = 1621333 + flags = 1 + data = length 5324, hash 59546B7B + sample 10: + time = 1706666 + flags = 1 + data = length 5172, hash 6036EF0B + sample 11: + time = 1792000 + flags = 1 + data = length 5102, hash 5A131071 + sample 12: + time = 1877333 + flags = 1 + data = length 5111, hash 3D9EBB3B + sample 13: + time = 1962666 + flags = 1 + data = length 5113, hash 61101D4F + sample 14: + time = 2048000 + flags = 1 + data = length 5229, hash D2E55742 + sample 15: + time = 2133333 + flags = 1 + data = length 5162, hash 7F2E97FA + sample 16: + time = 2218666 + flags = 1 + data = length 5255, hash D92A782 + sample 17: + time = 2304000 + flags = 1 + data = length 5196, hash 98FE5138 + sample 18: + time = 2389333 + flags = 1 + data = length 5214, hash 3D35C38C + sample 19: + time = 2474666 + flags = 1 + data = length 5211, hash 7E25420F + sample 20: + time = 2560000 + flags = 1 + data = length 5230, hash 2AD96FBC + sample 21: + time = 2645333 + flags = 1 + data = length 3384, hash 938BCDD9 + sample 22: + time = 2730666 + flags = 1 + data = length 445, hash A388E3D6 +tracksEnded = true diff --git a/testdata/src/test/assets/flac/bear_with_id3_disabled_flac.2.dump b/testdata/src/test/assets/flac/bear_with_id3_disabled_flac.2.dump new file mode 100644 index 0000000000..c931549e2e --- /dev/null +++ b/testdata/src/test/assets/flac/bear_with_id3_disabled_flac.2.dump @@ -0,0 +1,67 @@ +seekMap: + isSeekable = true + duration = 2741000 + getPosition(0) = [[timeUs=0, position=55284]] + getPosition(1) = [[timeUs=0, position=55284], [timeUs=85333, position=60314]] + getPosition(1370500) = [[timeUs=1365333, position=137229], [timeUs=1450666, position=143005]] + getPosition(2741000) = [[timeUs=2645333, position=215886]] +numberOfTracks = 1 +track 0: + total output bytes = 55652 + sample count = 12 + format 0: + sampleMimeType = audio/flac + maxInputSize = 5776 + channelCount = 2 + sampleRate = 48000 + initializationData: + data = length 42, hash 83F6895 + sample 0: + time = 1792000 + flags = 1 + data = length 5102, hash 5A131071 + sample 1: + time = 1877333 + flags = 1 + data = length 5111, hash 3D9EBB3B + sample 2: + time = 1962666 + flags = 1 + data = length 5113, hash 61101D4F + sample 3: + time = 2048000 + flags = 1 + data = length 5229, hash D2E55742 + sample 4: + time = 2133333 + flags = 1 + data = length 5162, hash 7F2E97FA + sample 5: + time = 2218666 + flags = 1 + data = length 5255, hash D92A782 + sample 6: + time = 2304000 + flags = 1 + data = length 5196, hash 98FE5138 + sample 7: + time = 2389333 + flags = 1 + data = length 5214, hash 3D35C38C + sample 8: + time = 2474666 + flags = 1 + data = length 5211, hash 7E25420F + sample 9: + time = 2560000 + flags = 1 + data = length 5230, hash 2AD96FBC + sample 10: + time = 2645333 + flags = 1 + data = length 3384, hash 938BCDD9 + sample 11: + time = 2730666 + flags = 1 + data = length 445, hash A388E3D6 +tracksEnded = true diff --git a/testdata/src/test/assets/flac/bear_with_id3_disabled_flac.3.dump b/testdata/src/test/assets/flac/bear_with_id3_disabled_flac.3.dump new file mode 100644 index 0000000000..1344af155c --- /dev/null +++ b/testdata/src/test/assets/flac/bear_with_id3_disabled_flac.3.dump @@ -0,0 +1,27 @@ +seekMap: + isSeekable = true + duration = 2741000 + getPosition(0) = [[timeUs=0, position=55284]] + getPosition(1) = [[timeUs=0, position=55284], [timeUs=85333, position=60314]] + getPosition(1370500) = [[timeUs=1365333, position=137229], [timeUs=1450666, position=143005]] + getPosition(2741000) = [[timeUs=2645333, position=215886]] +numberOfTracks = 1 +track 0: + total output bytes = 3829 + sample count = 2 + format 0: + sampleMimeType = audio/flac + maxInputSize = 5776 + channelCount = 2 + sampleRate = 48000 + initializationData: + data = length 42, hash 83F6895 + sample 0: + time = 2645333 + flags = 1 + data = length 3384, hash 938BCDD9 + sample 1: + time = 2730666 + flags = 1 + data = length 445, hash A388E3D6 +tracksEnded = true diff --git a/testdata/src/test/assets/flac/bear_with_id3_disabled_flac.unknown_length.dump b/testdata/src/test/assets/flac/bear_with_id3_disabled_flac.unknown_length.dump new file mode 100644 index 0000000000..3b3f6611be --- /dev/null +++ b/testdata/src/test/assets/flac/bear_with_id3_disabled_flac.unknown_length.dump @@ -0,0 +1,151 @@ +seekMap: + isSeekable = true + duration = 2741000 + getPosition(0) = [[timeUs=0, position=55284]] + getPosition(1) = [[timeUs=0, position=55284], [timeUs=85333, position=60314]] + getPosition(1370500) = [[timeUs=1365333, position=137229], [timeUs=1450666, position=143005]] + getPosition(2741000) = [[timeUs=2645333, position=215886]] +numberOfTracks = 1 +track 0: + total output bytes = 164431 + sample count = 33 + format 0: + sampleMimeType = audio/flac + maxInputSize = 5776 + channelCount = 2 + sampleRate = 48000 + initializationData: + data = length 42, hash 83F6895 + sample 0: + time = 0 + flags = 1 + data = length 5030, hash D2B60530 + sample 1: + time = 85333 + flags = 1 + data = length 5066, hash 4C932A54 + sample 2: + time = 170666 + flags = 1 + data = length 5112, hash 7E5A7B61 + sample 3: + time = 256000 + flags = 1 + data = length 5044, hash 7EF93F13 + sample 4: + time = 341333 + flags = 1 + data = length 4943, hash DE7E27F8 + sample 5: + time = 426666 + flags = 1 + data = length 5121, hash 6D0D0B40 + sample 6: + time = 512000 + flags = 1 + data = length 5068, hash 9924644F + sample 7: + time = 597333 + flags = 1 + data = length 5143, hash 6C34F0CE + sample 8: + time = 682666 + flags = 1 + data = length 5109, hash E3B7BEFB + sample 9: + time = 768000 + flags = 1 + data = length 5129, hash 44111D9B + sample 10: + time = 853333 + flags = 1 + data = length 5031, hash 9D55EA53 + sample 11: + time = 938666 + flags = 1 + data = length 5119, hash E1CB9BA6 + sample 12: + time = 1024000 + flags = 1 + data = length 5360, hash 17265C5D + sample 13: + time = 1109333 + flags = 1 + data = length 5340, hash A90FDDF1 + sample 14: + time = 1194666 + flags = 1 + data = length 5162, hash 31F65AD5 + sample 15: + time = 1280000 + flags = 1 + data = length 5168, hash F2394F2D + sample 16: + time = 1365333 + flags = 1 + data = length 5776, hash 58437AB3 + sample 17: + time = 1450666 + flags = 1 + data = length 5394, hash EBAB20A8 + sample 18: + time = 1536000 + flags = 1 + data = length 5168, hash BF37C7A5 + sample 19: + time = 1621333 + flags = 1 + data = length 5324, hash 59546B7B + sample 20: + time = 1706666 + flags = 1 + data = length 5172, hash 6036EF0B + sample 21: + time = 1792000 + flags = 1 + data = length 5102, hash 5A131071 + sample 22: + time = 1877333 + flags = 1 + data = length 5111, hash 3D9EBB3B + sample 23: + time = 1962666 + flags = 1 + data = length 5113, hash 61101D4F + sample 24: + time = 2048000 + flags = 1 + data = length 5229, hash D2E55742 + sample 25: + time = 2133333 + flags = 1 + data = length 5162, hash 7F2E97FA + sample 26: + time = 2218666 + flags = 1 + data = length 5255, hash D92A782 + sample 27: + time = 2304000 + flags = 1 + data = length 5196, hash 98FE5138 + sample 28: + time = 2389333 + flags = 1 + data = length 5214, hash 3D35C38C + sample 29: + time = 2474666 + flags = 1 + data = length 5211, hash 7E25420F + sample 30: + time = 2560000 + flags = 1 + data = length 5230, hash 2AD96FBC + sample 31: + time = 2645333 + flags = 1 + data = length 3384, hash 938BCDD9 + sample 32: + time = 2730666 + flags = 1 + data = length 445, hash A388E3D6 +tracksEnded = true diff --git a/extensions/flac/src/androidTest/assets/bear_with_id3.flac.0.dump b/testdata/src/test/assets/flac/bear_with_id3_disabled_raw.0.dump similarity index 89% rename from extensions/flac/src/androidTest/assets/bear_with_id3.flac.0.dump rename to testdata/src/test/assets/flac/bear_with_id3_disabled_raw.0.dump index 3a3ba57572..fcfa917208 100644 --- a/extensions/flac/src/androidTest/assets/bear_with_id3.flac.0.dump +++ b/testdata/src/test/assets/flac/bear_with_id3_disabled_raw.0.dump @@ -2,31 +2,21 @@ seekMap: isSeekable = true duration = 2741000 getPosition(0) = [[timeUs=0, position=55284]] + getPosition(1) = [[timeUs=0, position=55284]] + getPosition(1370500) = [[timeUs=1365333, position=137229], [timeUs=1450666, position=143005]] + getPosition(2741000) = [[timeUs=2645333, position=215886]] numberOfTracks = 1 track 0: - format: - bitrate = 1536000 - id = null - containerMimeType = null + total output bytes = 526272 + sample count = 33 + format 0: + averageBitrate = 1536000 + peakBitrate = 1536000 sampleMimeType = audio/raw maxInputSize = 16384 - width = -1 - height = -1 - frameRate = -1.0 - rotationDegrees = 0 - pixelWidthHeightRatio = 1.0 channelCount = 2 sampleRate = 48000 pcmEncoding = 2 - encoderDelay = 0 - encoderPadding = 0 - subsampleOffsetUs = 9223372036854775807 - selectionFlags = 0 - language = null - drmInitData = - - initializationData: - total output bytes = 526272 - sample count = 33 sample 0: time = 0 flags = 1 diff --git a/extensions/flac/src/androidTest/assets/bear_with_id3.flac.1.dump b/testdata/src/test/assets/flac/bear_with_id3_disabled_raw.1.dump similarity index 86% rename from extensions/flac/src/androidTest/assets/bear_with_id3.flac.1.dump rename to testdata/src/test/assets/flac/bear_with_id3_disabled_raw.1.dump index a07fcaa0a2..ad1c171a9a 100644 --- a/extensions/flac/src/androidTest/assets/bear_with_id3.flac.1.dump +++ b/testdata/src/test/assets/flac/bear_with_id3_disabled_raw.1.dump @@ -2,31 +2,21 @@ seekMap: isSeekable = true duration = 2741000 getPosition(0) = [[timeUs=0, position=55284]] + getPosition(1) = [[timeUs=0, position=55284]] + getPosition(1370500) = [[timeUs=1365333, position=137229], [timeUs=1450666, position=143005]] + getPosition(2741000) = [[timeUs=2645333, position=215886]] numberOfTracks = 1 track 0: - format: - bitrate = 1536000 - id = null - containerMimeType = null + total output bytes = 362432 + sample count = 23 + format 0: + averageBitrate = 1536000 + peakBitrate = 1536000 sampleMimeType = audio/raw maxInputSize = 16384 - width = -1 - height = -1 - frameRate = -1.0 - rotationDegrees = 0 - pixelWidthHeightRatio = 1.0 channelCount = 2 sampleRate = 48000 pcmEncoding = 2 - encoderDelay = 0 - encoderPadding = 0 - subsampleOffsetUs = 9223372036854775807 - selectionFlags = 0 - language = null - drmInitData = - - initializationData: - total output bytes = 362432 - sample count = 23 sample 0: time = 853333 flags = 1 diff --git a/extensions/flac/src/androidTest/assets/bear_with_id3.flac.2.dump b/testdata/src/test/assets/flac/bear_with_id3_disabled_raw.2.dump similarity index 78% rename from extensions/flac/src/androidTest/assets/bear_with_id3.flac.2.dump rename to testdata/src/test/assets/flac/bear_with_id3_disabled_raw.2.dump index c4d13dd2e6..e5f625e576 100644 --- a/extensions/flac/src/androidTest/assets/bear_with_id3.flac.2.dump +++ b/testdata/src/test/assets/flac/bear_with_id3_disabled_raw.2.dump @@ -2,31 +2,21 @@ seekMap: isSeekable = true duration = 2741000 getPosition(0) = [[timeUs=0, position=55284]] + getPosition(1) = [[timeUs=0, position=55284]] + getPosition(1370500) = [[timeUs=1365333, position=137229], [timeUs=1450666, position=143005]] + getPosition(2741000) = [[timeUs=2645333, position=215886]] numberOfTracks = 1 track 0: - format: - bitrate = 1536000 - id = null - containerMimeType = null + total output bytes = 182208 + sample count = 12 + format 0: + averageBitrate = 1536000 + peakBitrate = 1536000 sampleMimeType = audio/raw maxInputSize = 16384 - width = -1 - height = -1 - frameRate = -1.0 - rotationDegrees = 0 - pixelWidthHeightRatio = 1.0 channelCount = 2 sampleRate = 48000 pcmEncoding = 2 - encoderDelay = 0 - encoderPadding = 0 - subsampleOffsetUs = 9223372036854775807 - selectionFlags = 0 - language = null - drmInitData = - - initializationData: - total output bytes = 182208 - sample count = 12 sample 0: time = 1792000 flags = 1 diff --git a/extensions/flac/src/androidTest/assets/bear_with_id3.flac.3.dump b/testdata/src/test/assets/flac/bear_with_id3_disabled_raw.3.dump similarity index 57% rename from extensions/flac/src/androidTest/assets/bear_with_id3.flac.3.dump rename to testdata/src/test/assets/flac/bear_with_id3_disabled_raw.3.dump index 2f389909e7..1625462f01 100644 --- a/extensions/flac/src/androidTest/assets/bear_with_id3.flac.3.dump +++ b/testdata/src/test/assets/flac/bear_with_id3_disabled_raw.3.dump @@ -2,31 +2,21 @@ seekMap: isSeekable = true duration = 2741000 getPosition(0) = [[timeUs=0, position=55284]] + getPosition(1) = [[timeUs=0, position=55284]] + getPosition(1370500) = [[timeUs=1365333, position=137229], [timeUs=1450666, position=143005]] + getPosition(2741000) = [[timeUs=2645333, position=215886]] numberOfTracks = 1 track 0: - format: - bitrate = 1536000 - id = null - containerMimeType = null + total output bytes = 18368 + sample count = 2 + format 0: + averageBitrate = 1536000 + peakBitrate = 1536000 sampleMimeType = audio/raw maxInputSize = 16384 - width = -1 - height = -1 - frameRate = -1.0 - rotationDegrees = 0 - pixelWidthHeightRatio = 1.0 channelCount = 2 sampleRate = 48000 pcmEncoding = 2 - encoderDelay = 0 - encoderPadding = 0 - subsampleOffsetUs = 9223372036854775807 - selectionFlags = 0 - language = null - drmInitData = - - initializationData: - total output bytes = 18368 - sample count = 2 sample 0: time = 2645333 flags = 1 diff --git a/testdata/src/test/assets/flac/bear_with_id3_disabled_raw.unknown_length.dump b/testdata/src/test/assets/flac/bear_with_id3_disabled_raw.unknown_length.dump new file mode 100644 index 0000000000..fcfa917208 --- /dev/null +++ b/testdata/src/test/assets/flac/bear_with_id3_disabled_raw.unknown_length.dump @@ -0,0 +1,152 @@ +seekMap: + isSeekable = true + duration = 2741000 + getPosition(0) = [[timeUs=0, position=55284]] + getPosition(1) = [[timeUs=0, position=55284]] + getPosition(1370500) = [[timeUs=1365333, position=137229], [timeUs=1450666, position=143005]] + getPosition(2741000) = [[timeUs=2645333, position=215886]] +numberOfTracks = 1 +track 0: + total output bytes = 526272 + sample count = 33 + format 0: + averageBitrate = 1536000 + peakBitrate = 1536000 + sampleMimeType = audio/raw + maxInputSize = 16384 + channelCount = 2 + sampleRate = 48000 + pcmEncoding = 2 + sample 0: + time = 0 + flags = 1 + data = length 16384, hash 61D2C5C2 + sample 1: + time = 85333 + flags = 1 + data = length 16384, hash E6D7F214 + sample 2: + time = 170666 + flags = 1 + data = length 16384, hash 59BF0D5D + sample 3: + time = 256000 + flags = 1 + data = length 16384, hash 3625F468 + sample 4: + time = 341333 + flags = 1 + data = length 16384, hash F66A323 + sample 5: + time = 426666 + flags = 1 + data = length 16384, hash CDBAE629 + sample 6: + time = 512000 + flags = 1 + data = length 16384, hash 536F3A91 + sample 7: + time = 597333 + flags = 1 + data = length 16384, hash D4F35C9C + sample 8: + time = 682666 + flags = 1 + data = length 16384, hash EE04CEBF + sample 9: + time = 768000 + flags = 1 + data = length 16384, hash 647E2A67 + sample 10: + time = 853333 + flags = 1 + data = length 16384, hash 31583F2C + sample 11: + time = 938666 + flags = 1 + data = length 16384, hash E433A93D + sample 12: + time = 1024000 + flags = 1 + data = length 16384, hash 5E1C7051 + sample 13: + time = 1109333 + flags = 1 + data = length 16384, hash 43E6E358 + sample 14: + time = 1194666 + flags = 1 + data = length 16384, hash 5DC1B256 + sample 15: + time = 1280000 + flags = 1 + data = length 16384, hash 3D9D95CF + sample 16: + time = 1365333 + flags = 1 + data = length 16384, hash 2A5BD2C0 + sample 17: + time = 1450666 + flags = 1 + data = length 16384, hash 93E25061 + sample 18: + time = 1536000 + flags = 1 + data = length 16384, hash B81793D8 + sample 19: + time = 1621333 + flags = 1 + data = length 16384, hash 1A3BD49F + sample 20: + time = 1706666 + flags = 1 + data = length 16384, hash FB672FF1 + sample 21: + time = 1792000 + flags = 1 + data = length 16384, hash 48AB8B45 + sample 22: + time = 1877333 + flags = 1 + data = length 16384, hash 13C9640A + sample 23: + time = 1962666 + flags = 1 + data = length 16384, hash 499E4A0B + sample 24: + time = 2048000 + flags = 1 + data = length 16384, hash F9A783E6 + sample 25: + time = 2133333 + flags = 1 + data = length 16384, hash D2B77598 + sample 26: + time = 2218666 + flags = 1 + data = length 16384, hash CE5B826C + sample 27: + time = 2304000 + flags = 1 + data = length 16384, hash E99EE956 + sample 28: + time = 2389333 + flags = 1 + data = length 16384, hash F2DB1486 + sample 29: + time = 2474666 + flags = 1 + data = length 16384, hash 1636EAB + sample 30: + time = 2560000 + flags = 1 + data = length 16384, hash 23457C08 + sample 31: + time = 2645333 + flags = 1 + data = length 16384, hash 30EB8381 + sample 32: + time = 2730666 + flags = 1 + data = length 1984, hash 59CFDE1B +tracksEnded = true diff --git a/testdata/src/test/assets/flac/bear_with_id3_enabled_flac.0.dump b/testdata/src/test/assets/flac/bear_with_id3_enabled_flac.0.dump new file mode 100644 index 0000000000..d2a8d6a442 --- /dev/null +++ b/testdata/src/test/assets/flac/bear_with_id3_enabled_flac.0.dump @@ -0,0 +1,152 @@ +seekMap: + isSeekable = true + duration = 2741000 + getPosition(0) = [[timeUs=0, position=55284]] + getPosition(1) = [[timeUs=0, position=55284], [timeUs=85333, position=60314]] + getPosition(1370500) = [[timeUs=1365333, position=137229], [timeUs=1450666, position=143005]] + getPosition(2741000) = [[timeUs=2645333, position=215886]] +numberOfTracks = 1 +track 0: + total output bytes = 164431 + sample count = 33 + format 0: + sampleMimeType = audio/flac + maxInputSize = 5776 + channelCount = 2 + sampleRate = 48000 + metadata = entries=[TXXX: description=ID: value=105519843, TIT2: description=null: value=那么爱你为什么, TPE1: description=null: value=阿强, TALB: description=null: value=华丽的外衣, TXXX: description=ID: value=105519843, APIC: mimeType=image/jpeg, description=] + initializationData: + data = length 42, hash 83F6895 + sample 0: + time = 0 + flags = 1 + data = length 5030, hash D2B60530 + sample 1: + time = 85333 + flags = 1 + data = length 5066, hash 4C932A54 + sample 2: + time = 170666 + flags = 1 + data = length 5112, hash 7E5A7B61 + sample 3: + time = 256000 + flags = 1 + data = length 5044, hash 7EF93F13 + sample 4: + time = 341333 + flags = 1 + data = length 4943, hash DE7E27F8 + sample 5: + time = 426666 + flags = 1 + data = length 5121, hash 6D0D0B40 + sample 6: + time = 512000 + flags = 1 + data = length 5068, hash 9924644F + sample 7: + time = 597333 + flags = 1 + data = length 5143, hash 6C34F0CE + sample 8: + time = 682666 + flags = 1 + data = length 5109, hash E3B7BEFB + sample 9: + time = 768000 + flags = 1 + data = length 5129, hash 44111D9B + sample 10: + time = 853333 + flags = 1 + data = length 5031, hash 9D55EA53 + sample 11: + time = 938666 + flags = 1 + data = length 5119, hash E1CB9BA6 + sample 12: + time = 1024000 + flags = 1 + data = length 5360, hash 17265C5D + sample 13: + time = 1109333 + flags = 1 + data = length 5340, hash A90FDDF1 + sample 14: + time = 1194666 + flags = 1 + data = length 5162, hash 31F65AD5 + sample 15: + time = 1280000 + flags = 1 + data = length 5168, hash F2394F2D + sample 16: + time = 1365333 + flags = 1 + data = length 5776, hash 58437AB3 + sample 17: + time = 1450666 + flags = 1 + data = length 5394, hash EBAB20A8 + sample 18: + time = 1536000 + flags = 1 + data = length 5168, hash BF37C7A5 + sample 19: + time = 1621333 + flags = 1 + data = length 5324, hash 59546B7B + sample 20: + time = 1706666 + flags = 1 + data = length 5172, hash 6036EF0B + sample 21: + time = 1792000 + flags = 1 + data = length 5102, hash 5A131071 + sample 22: + time = 1877333 + flags = 1 + data = length 5111, hash 3D9EBB3B + sample 23: + time = 1962666 + flags = 1 + data = length 5113, hash 61101D4F + sample 24: + time = 2048000 + flags = 1 + data = length 5229, hash D2E55742 + sample 25: + time = 2133333 + flags = 1 + data = length 5162, hash 7F2E97FA + sample 26: + time = 2218666 + flags = 1 + data = length 5255, hash D92A782 + sample 27: + time = 2304000 + flags = 1 + data = length 5196, hash 98FE5138 + sample 28: + time = 2389333 + flags = 1 + data = length 5214, hash 3D35C38C + sample 29: + time = 2474666 + flags = 1 + data = length 5211, hash 7E25420F + sample 30: + time = 2560000 + flags = 1 + data = length 5230, hash 2AD96FBC + sample 31: + time = 2645333 + flags = 1 + data = length 3384, hash 938BCDD9 + sample 32: + time = 2730666 + flags = 1 + data = length 445, hash A388E3D6 +tracksEnded = true diff --git a/testdata/src/test/assets/flac/bear_with_id3_enabled_flac.1.dump b/testdata/src/test/assets/flac/bear_with_id3_enabled_flac.1.dump new file mode 100644 index 0000000000..250d1add95 --- /dev/null +++ b/testdata/src/test/assets/flac/bear_with_id3_enabled_flac.1.dump @@ -0,0 +1,112 @@ +seekMap: + isSeekable = true + duration = 2741000 + getPosition(0) = [[timeUs=0, position=55284]] + getPosition(1) = [[timeUs=0, position=55284], [timeUs=85333, position=60314]] + getPosition(1370500) = [[timeUs=1365333, position=137229], [timeUs=1450666, position=143005]] + getPosition(2741000) = [[timeUs=2645333, position=215886]] +numberOfTracks = 1 +track 0: + total output bytes = 113666 + sample count = 23 + format 0: + sampleMimeType = audio/flac + maxInputSize = 5776 + channelCount = 2 + sampleRate = 48000 + metadata = entries=[TXXX: description=ID: value=105519843, TIT2: description=null: value=那么爱你为什么, TPE1: description=null: value=阿强, TALB: description=null: value=华丽的外衣, TXXX: description=ID: value=105519843, APIC: mimeType=image/jpeg, description=] + initializationData: + data = length 42, hash 83F6895 + sample 0: + time = 853333 + flags = 1 + data = length 5031, hash 9D55EA53 + sample 1: + time = 938666 + flags = 1 + data = length 5119, hash E1CB9BA6 + sample 2: + time = 1024000 + flags = 1 + data = length 5360, hash 17265C5D + sample 3: + time = 1109333 + flags = 1 + data = length 5340, hash A90FDDF1 + sample 4: + time = 1194666 + flags = 1 + data = length 5162, hash 31F65AD5 + sample 5: + time = 1280000 + flags = 1 + data = length 5168, hash F2394F2D + sample 6: + time = 1365333 + flags = 1 + data = length 5776, hash 58437AB3 + sample 7: + time = 1450666 + flags = 1 + data = length 5394, hash EBAB20A8 + sample 8: + time = 1536000 + flags = 1 + data = length 5168, hash BF37C7A5 + sample 9: + time = 1621333 + flags = 1 + data = length 5324, hash 59546B7B + sample 10: + time = 1706666 + flags = 1 + data = length 5172, hash 6036EF0B + sample 11: + time = 1792000 + flags = 1 + data = length 5102, hash 5A131071 + sample 12: + time = 1877333 + flags = 1 + data = length 5111, hash 3D9EBB3B + sample 13: + time = 1962666 + flags = 1 + data = length 5113, hash 61101D4F + sample 14: + time = 2048000 + flags = 1 + data = length 5229, hash D2E55742 + sample 15: + time = 2133333 + flags = 1 + data = length 5162, hash 7F2E97FA + sample 16: + time = 2218666 + flags = 1 + data = length 5255, hash D92A782 + sample 17: + time = 2304000 + flags = 1 + data = length 5196, hash 98FE5138 + sample 18: + time = 2389333 + flags = 1 + data = length 5214, hash 3D35C38C + sample 19: + time = 2474666 + flags = 1 + data = length 5211, hash 7E25420F + sample 20: + time = 2560000 + flags = 1 + data = length 5230, hash 2AD96FBC + sample 21: + time = 2645333 + flags = 1 + data = length 3384, hash 938BCDD9 + sample 22: + time = 2730666 + flags = 1 + data = length 445, hash A388E3D6 +tracksEnded = true diff --git a/testdata/src/test/assets/flac/bear_with_id3_enabled_flac.2.dump b/testdata/src/test/assets/flac/bear_with_id3_enabled_flac.2.dump new file mode 100644 index 0000000000..e5057cff25 --- /dev/null +++ b/testdata/src/test/assets/flac/bear_with_id3_enabled_flac.2.dump @@ -0,0 +1,68 @@ +seekMap: + isSeekable = true + duration = 2741000 + getPosition(0) = [[timeUs=0, position=55284]] + getPosition(1) = [[timeUs=0, position=55284], [timeUs=85333, position=60314]] + getPosition(1370500) = [[timeUs=1365333, position=137229], [timeUs=1450666, position=143005]] + getPosition(2741000) = [[timeUs=2645333, position=215886]] +numberOfTracks = 1 +track 0: + total output bytes = 55652 + sample count = 12 + format 0: + sampleMimeType = audio/flac + maxInputSize = 5776 + channelCount = 2 + sampleRate = 48000 + metadata = entries=[TXXX: description=ID: value=105519843, TIT2: description=null: value=那么爱你为什么, TPE1: description=null: value=阿强, TALB: description=null: value=华丽的外衣, TXXX: description=ID: value=105519843, APIC: mimeType=image/jpeg, description=] + initializationData: + data = length 42, hash 83F6895 + sample 0: + time = 1792000 + flags = 1 + data = length 5102, hash 5A131071 + sample 1: + time = 1877333 + flags = 1 + data = length 5111, hash 3D9EBB3B + sample 2: + time = 1962666 + flags = 1 + data = length 5113, hash 61101D4F + sample 3: + time = 2048000 + flags = 1 + data = length 5229, hash D2E55742 + sample 4: + time = 2133333 + flags = 1 + data = length 5162, hash 7F2E97FA + sample 5: + time = 2218666 + flags = 1 + data = length 5255, hash D92A782 + sample 6: + time = 2304000 + flags = 1 + data = length 5196, hash 98FE5138 + sample 7: + time = 2389333 + flags = 1 + data = length 5214, hash 3D35C38C + sample 8: + time = 2474666 + flags = 1 + data = length 5211, hash 7E25420F + sample 9: + time = 2560000 + flags = 1 + data = length 5230, hash 2AD96FBC + sample 10: + time = 2645333 + flags = 1 + data = length 3384, hash 938BCDD9 + sample 11: + time = 2730666 + flags = 1 + data = length 445, hash A388E3D6 +tracksEnded = true diff --git a/testdata/src/test/assets/flac/bear_with_id3_enabled_flac.3.dump b/testdata/src/test/assets/flac/bear_with_id3_enabled_flac.3.dump new file mode 100644 index 0000000000..afaead1d88 --- /dev/null +++ b/testdata/src/test/assets/flac/bear_with_id3_enabled_flac.3.dump @@ -0,0 +1,28 @@ +seekMap: + isSeekable = true + duration = 2741000 + getPosition(0) = [[timeUs=0, position=55284]] + getPosition(1) = [[timeUs=0, position=55284], [timeUs=85333, position=60314]] + getPosition(1370500) = [[timeUs=1365333, position=137229], [timeUs=1450666, position=143005]] + getPosition(2741000) = [[timeUs=2645333, position=215886]] +numberOfTracks = 1 +track 0: + total output bytes = 3829 + sample count = 2 + format 0: + sampleMimeType = audio/flac + maxInputSize = 5776 + channelCount = 2 + sampleRate = 48000 + metadata = entries=[TXXX: description=ID: value=105519843, TIT2: description=null: value=那么爱你为什么, TPE1: description=null: value=阿强, TALB: description=null: value=华丽的外衣, TXXX: description=ID: value=105519843, APIC: mimeType=image/jpeg, description=] + initializationData: + data = length 42, hash 83F6895 + sample 0: + time = 2645333 + flags = 1 + data = length 3384, hash 938BCDD9 + sample 1: + time = 2730666 + flags = 1 + data = length 445, hash A388E3D6 +tracksEnded = true diff --git a/testdata/src/test/assets/flac/bear_with_id3_enabled_flac.unknown_length.dump b/testdata/src/test/assets/flac/bear_with_id3_enabled_flac.unknown_length.dump new file mode 100644 index 0000000000..d2a8d6a442 --- /dev/null +++ b/testdata/src/test/assets/flac/bear_with_id3_enabled_flac.unknown_length.dump @@ -0,0 +1,152 @@ +seekMap: + isSeekable = true + duration = 2741000 + getPosition(0) = [[timeUs=0, position=55284]] + getPosition(1) = [[timeUs=0, position=55284], [timeUs=85333, position=60314]] + getPosition(1370500) = [[timeUs=1365333, position=137229], [timeUs=1450666, position=143005]] + getPosition(2741000) = [[timeUs=2645333, position=215886]] +numberOfTracks = 1 +track 0: + total output bytes = 164431 + sample count = 33 + format 0: + sampleMimeType = audio/flac + maxInputSize = 5776 + channelCount = 2 + sampleRate = 48000 + metadata = entries=[TXXX: description=ID: value=105519843, TIT2: description=null: value=那么爱你为什么, TPE1: description=null: value=阿强, TALB: description=null: value=华丽的外衣, TXXX: description=ID: value=105519843, APIC: mimeType=image/jpeg, description=] + initializationData: + data = length 42, hash 83F6895 + sample 0: + time = 0 + flags = 1 + data = length 5030, hash D2B60530 + sample 1: + time = 85333 + flags = 1 + data = length 5066, hash 4C932A54 + sample 2: + time = 170666 + flags = 1 + data = length 5112, hash 7E5A7B61 + sample 3: + time = 256000 + flags = 1 + data = length 5044, hash 7EF93F13 + sample 4: + time = 341333 + flags = 1 + data = length 4943, hash DE7E27F8 + sample 5: + time = 426666 + flags = 1 + data = length 5121, hash 6D0D0B40 + sample 6: + time = 512000 + flags = 1 + data = length 5068, hash 9924644F + sample 7: + time = 597333 + flags = 1 + data = length 5143, hash 6C34F0CE + sample 8: + time = 682666 + flags = 1 + data = length 5109, hash E3B7BEFB + sample 9: + time = 768000 + flags = 1 + data = length 5129, hash 44111D9B + sample 10: + time = 853333 + flags = 1 + data = length 5031, hash 9D55EA53 + sample 11: + time = 938666 + flags = 1 + data = length 5119, hash E1CB9BA6 + sample 12: + time = 1024000 + flags = 1 + data = length 5360, hash 17265C5D + sample 13: + time = 1109333 + flags = 1 + data = length 5340, hash A90FDDF1 + sample 14: + time = 1194666 + flags = 1 + data = length 5162, hash 31F65AD5 + sample 15: + time = 1280000 + flags = 1 + data = length 5168, hash F2394F2D + sample 16: + time = 1365333 + flags = 1 + data = length 5776, hash 58437AB3 + sample 17: + time = 1450666 + flags = 1 + data = length 5394, hash EBAB20A8 + sample 18: + time = 1536000 + flags = 1 + data = length 5168, hash BF37C7A5 + sample 19: + time = 1621333 + flags = 1 + data = length 5324, hash 59546B7B + sample 20: + time = 1706666 + flags = 1 + data = length 5172, hash 6036EF0B + sample 21: + time = 1792000 + flags = 1 + data = length 5102, hash 5A131071 + sample 22: + time = 1877333 + flags = 1 + data = length 5111, hash 3D9EBB3B + sample 23: + time = 1962666 + flags = 1 + data = length 5113, hash 61101D4F + sample 24: + time = 2048000 + flags = 1 + data = length 5229, hash D2E55742 + sample 25: + time = 2133333 + flags = 1 + data = length 5162, hash 7F2E97FA + sample 26: + time = 2218666 + flags = 1 + data = length 5255, hash D92A782 + sample 27: + time = 2304000 + flags = 1 + data = length 5196, hash 98FE5138 + sample 28: + time = 2389333 + flags = 1 + data = length 5214, hash 3D35C38C + sample 29: + time = 2474666 + flags = 1 + data = length 5211, hash 7E25420F + sample 30: + time = 2560000 + flags = 1 + data = length 5230, hash 2AD96FBC + sample 31: + time = 2645333 + flags = 1 + data = length 3384, hash 938BCDD9 + sample 32: + time = 2730666 + flags = 1 + data = length 445, hash A388E3D6 +tracksEnded = true diff --git a/testdata/src/test/assets/flac/bear_with_id3_enabled_raw.0.dump b/testdata/src/test/assets/flac/bear_with_id3_enabled_raw.0.dump new file mode 100644 index 0000000000..ca9f1a74a1 --- /dev/null +++ b/testdata/src/test/assets/flac/bear_with_id3_enabled_raw.0.dump @@ -0,0 +1,153 @@ +seekMap: + isSeekable = true + duration = 2741000 + getPosition(0) = [[timeUs=0, position=55284]] + getPosition(1) = [[timeUs=0, position=55284]] + getPosition(1370500) = [[timeUs=1365333, position=137229], [timeUs=1450666, position=143005]] + getPosition(2741000) = [[timeUs=2645333, position=215886]] +numberOfTracks = 1 +track 0: + total output bytes = 526272 + sample count = 33 + format 0: + averageBitrate = 1536000 + peakBitrate = 1536000 + sampleMimeType = audio/raw + maxInputSize = 16384 + channelCount = 2 + sampleRate = 48000 + pcmEncoding = 2 + metadata = entries=[TXXX: description=ID: value=105519843, TIT2: description=null: value=那么爱你为什么, TPE1: description=null: value=阿强, TALB: description=null: value=华丽的外衣, TXXX: description=ID: value=105519843, APIC: mimeType=image/jpeg, description=] + sample 0: + time = 0 + flags = 1 + data = length 16384, hash 61D2C5C2 + sample 1: + time = 85333 + flags = 1 + data = length 16384, hash E6D7F214 + sample 2: + time = 170666 + flags = 1 + data = length 16384, hash 59BF0D5D + sample 3: + time = 256000 + flags = 1 + data = length 16384, hash 3625F468 + sample 4: + time = 341333 + flags = 1 + data = length 16384, hash F66A323 + sample 5: + time = 426666 + flags = 1 + data = length 16384, hash CDBAE629 + sample 6: + time = 512000 + flags = 1 + data = length 16384, hash 536F3A91 + sample 7: + time = 597333 + flags = 1 + data = length 16384, hash D4F35C9C + sample 8: + time = 682666 + flags = 1 + data = length 16384, hash EE04CEBF + sample 9: + time = 768000 + flags = 1 + data = length 16384, hash 647E2A67 + sample 10: + time = 853333 + flags = 1 + data = length 16384, hash 31583F2C + sample 11: + time = 938666 + flags = 1 + data = length 16384, hash E433A93D + sample 12: + time = 1024000 + flags = 1 + data = length 16384, hash 5E1C7051 + sample 13: + time = 1109333 + flags = 1 + data = length 16384, hash 43E6E358 + sample 14: + time = 1194666 + flags = 1 + data = length 16384, hash 5DC1B256 + sample 15: + time = 1280000 + flags = 1 + data = length 16384, hash 3D9D95CF + sample 16: + time = 1365333 + flags = 1 + data = length 16384, hash 2A5BD2C0 + sample 17: + time = 1450666 + flags = 1 + data = length 16384, hash 93E25061 + sample 18: + time = 1536000 + flags = 1 + data = length 16384, hash B81793D8 + sample 19: + time = 1621333 + flags = 1 + data = length 16384, hash 1A3BD49F + sample 20: + time = 1706666 + flags = 1 + data = length 16384, hash FB672FF1 + sample 21: + time = 1792000 + flags = 1 + data = length 16384, hash 48AB8B45 + sample 22: + time = 1877333 + flags = 1 + data = length 16384, hash 13C9640A + sample 23: + time = 1962666 + flags = 1 + data = length 16384, hash 499E4A0B + sample 24: + time = 2048000 + flags = 1 + data = length 16384, hash F9A783E6 + sample 25: + time = 2133333 + flags = 1 + data = length 16384, hash D2B77598 + sample 26: + time = 2218666 + flags = 1 + data = length 16384, hash CE5B826C + sample 27: + time = 2304000 + flags = 1 + data = length 16384, hash E99EE956 + sample 28: + time = 2389333 + flags = 1 + data = length 16384, hash F2DB1486 + sample 29: + time = 2474666 + flags = 1 + data = length 16384, hash 1636EAB + sample 30: + time = 2560000 + flags = 1 + data = length 16384, hash 23457C08 + sample 31: + time = 2645333 + flags = 1 + data = length 16384, hash 30EB8381 + sample 32: + time = 2730666 + flags = 1 + data = length 1984, hash 59CFDE1B +tracksEnded = true diff --git a/testdata/src/test/assets/flac/bear_with_id3_enabled_raw.1.dump b/testdata/src/test/assets/flac/bear_with_id3_enabled_raw.1.dump new file mode 100644 index 0000000000..36314d9433 --- /dev/null +++ b/testdata/src/test/assets/flac/bear_with_id3_enabled_raw.1.dump @@ -0,0 +1,113 @@ +seekMap: + isSeekable = true + duration = 2741000 + getPosition(0) = [[timeUs=0, position=55284]] + getPosition(1) = [[timeUs=0, position=55284]] + getPosition(1370500) = [[timeUs=1365333, position=137229], [timeUs=1450666, position=143005]] + getPosition(2741000) = [[timeUs=2645333, position=215886]] +numberOfTracks = 1 +track 0: + total output bytes = 362432 + sample count = 23 + format 0: + averageBitrate = 1536000 + peakBitrate = 1536000 + sampleMimeType = audio/raw + maxInputSize = 16384 + channelCount = 2 + sampleRate = 48000 + pcmEncoding = 2 + metadata = entries=[TXXX: description=ID: value=105519843, TIT2: description=null: value=那么爱你为什么, TPE1: description=null: value=阿强, TALB: description=null: value=华丽的外衣, TXXX: description=ID: value=105519843, APIC: mimeType=image/jpeg, description=] + sample 0: + time = 853333 + flags = 1 + data = length 16384, hash 31583F2C + sample 1: + time = 938666 + flags = 1 + data = length 16384, hash E433A93D + sample 2: + time = 1024000 + flags = 1 + data = length 16384, hash 5E1C7051 + sample 3: + time = 1109333 + flags = 1 + data = length 16384, hash 43E6E358 + sample 4: + time = 1194666 + flags = 1 + data = length 16384, hash 5DC1B256 + sample 5: + time = 1280000 + flags = 1 + data = length 16384, hash 3D9D95CF + sample 6: + time = 1365333 + flags = 1 + data = length 16384, hash 2A5BD2C0 + sample 7: + time = 1450666 + flags = 1 + data = length 16384, hash 93E25061 + sample 8: + time = 1536000 + flags = 1 + data = length 16384, hash B81793D8 + sample 9: + time = 1621333 + flags = 1 + data = length 16384, hash 1A3BD49F + sample 10: + time = 1706666 + flags = 1 + data = length 16384, hash FB672FF1 + sample 11: + time = 1792000 + flags = 1 + data = length 16384, hash 48AB8B45 + sample 12: + time = 1877333 + flags = 1 + data = length 16384, hash 13C9640A + sample 13: + time = 1962666 + flags = 1 + data = length 16384, hash 499E4A0B + sample 14: + time = 2048000 + flags = 1 + data = length 16384, hash F9A783E6 + sample 15: + time = 2133333 + flags = 1 + data = length 16384, hash D2B77598 + sample 16: + time = 2218666 + flags = 1 + data = length 16384, hash CE5B826C + sample 17: + time = 2304000 + flags = 1 + data = length 16384, hash E99EE956 + sample 18: + time = 2389333 + flags = 1 + data = length 16384, hash F2DB1486 + sample 19: + time = 2474666 + flags = 1 + data = length 16384, hash 1636EAB + sample 20: + time = 2560000 + flags = 1 + data = length 16384, hash 23457C08 + sample 21: + time = 2645333 + flags = 1 + data = length 16384, hash 30EB8381 + sample 22: + time = 2730666 + flags = 1 + data = length 1984, hash 59CFDE1B +tracksEnded = true diff --git a/testdata/src/test/assets/flac/bear_with_id3_enabled_raw.2.dump b/testdata/src/test/assets/flac/bear_with_id3_enabled_raw.2.dump new file mode 100644 index 0000000000..0e8cc73341 --- /dev/null +++ b/testdata/src/test/assets/flac/bear_with_id3_enabled_raw.2.dump @@ -0,0 +1,69 @@ +seekMap: + isSeekable = true + duration = 2741000 + getPosition(0) = [[timeUs=0, position=55284]] + getPosition(1) = [[timeUs=0, position=55284]] + getPosition(1370500) = [[timeUs=1365333, position=137229], [timeUs=1450666, position=143005]] + getPosition(2741000) = [[timeUs=2645333, position=215886]] +numberOfTracks = 1 +track 0: + total output bytes = 182208 + sample count = 12 + format 0: + averageBitrate = 1536000 + peakBitrate = 1536000 + sampleMimeType = audio/raw + maxInputSize = 16384 + channelCount = 2 + sampleRate = 48000 + pcmEncoding = 2 + metadata = entries=[TXXX: description=ID: value=105519843, TIT2: description=null: value=那么爱你为什么, TPE1: description=null: value=阿强, TALB: description=null: value=华丽的外衣, TXXX: description=ID: value=105519843, APIC: mimeType=image/jpeg, description=] + sample 0: + time = 1792000 + flags = 1 + data = length 16384, hash 48AB8B45 + sample 1: + time = 1877333 + flags = 1 + data = length 16384, hash 13C9640A + sample 2: + time = 1962666 + flags = 1 + data = length 16384, hash 499E4A0B + sample 3: + time = 2048000 + flags = 1 + data = length 16384, hash F9A783E6 + sample 4: + time = 2133333 + flags = 1 + data = length 16384, hash D2B77598 + sample 5: + time = 2218666 + flags = 1 + data = length 16384, hash CE5B826C + sample 6: + time = 2304000 + flags = 1 + data = length 16384, hash E99EE956 + sample 7: + time = 2389333 + flags = 1 + data = length 16384, hash F2DB1486 + sample 8: + time = 2474666 + flags = 1 + data = length 16384, hash 1636EAB + sample 9: + time = 2560000 + flags = 1 + data = length 16384, hash 23457C08 + sample 10: + time = 2645333 + flags = 1 + data = length 16384, hash 30EB8381 + sample 11: + time = 2730666 + flags = 1 + data = length 1984, hash 59CFDE1B +tracksEnded = true diff --git a/testdata/src/test/assets/flac/bear_with_id3_enabled_raw.3.dump b/testdata/src/test/assets/flac/bear_with_id3_enabled_raw.3.dump new file mode 100644 index 0000000000..8ef6f9cb33 --- /dev/null +++ b/testdata/src/test/assets/flac/bear_with_id3_enabled_raw.3.dump @@ -0,0 +1,29 @@ +seekMap: + isSeekable = true + duration = 2741000 + getPosition(0) = [[timeUs=0, position=55284]] + getPosition(1) = [[timeUs=0, position=55284]] + getPosition(1370500) = [[timeUs=1365333, position=137229], [timeUs=1450666, position=143005]] + getPosition(2741000) = [[timeUs=2645333, position=215886]] +numberOfTracks = 1 +track 0: + total output bytes = 18368 + sample count = 2 + format 0: + averageBitrate = 1536000 + peakBitrate = 1536000 + sampleMimeType = audio/raw + maxInputSize = 16384 + channelCount = 2 + sampleRate = 48000 + pcmEncoding = 2 + metadata = entries=[TXXX: description=ID: value=105519843, TIT2: description=null: value=那么爱你为什么, TPE1: description=null: value=阿强, TALB: description=null: value=华丽的外衣, TXXX: description=ID: value=105519843, APIC: mimeType=image/jpeg, description=] + sample 0: + time = 2645333 + flags = 1 + data = length 16384, hash 30EB8381 + sample 1: + time = 2730666 + flags = 1 + data = length 1984, hash 59CFDE1B +tracksEnded = true diff --git a/testdata/src/test/assets/flac/bear_with_id3_enabled_raw.unknown_length.dump b/testdata/src/test/assets/flac/bear_with_id3_enabled_raw.unknown_length.dump new file mode 100644 index 0000000000..ca9f1a74a1 --- /dev/null +++ b/testdata/src/test/assets/flac/bear_with_id3_enabled_raw.unknown_length.dump @@ -0,0 +1,153 @@ +seekMap: + isSeekable = true + duration = 2741000 + getPosition(0) = [[timeUs=0, position=55284]] + getPosition(1) = [[timeUs=0, position=55284]] + getPosition(1370500) = [[timeUs=1365333, position=137229], [timeUs=1450666, position=143005]] + getPosition(2741000) = [[timeUs=2645333, position=215886]] +numberOfTracks = 1 +track 0: + total output bytes = 526272 + sample count = 33 + format 0: + averageBitrate = 1536000 + peakBitrate = 1536000 + sampleMimeType = audio/raw + maxInputSize = 16384 + channelCount = 2 + sampleRate = 48000 + pcmEncoding = 2 + metadata = entries=[TXXX: description=ID: value=105519843, TIT2: description=null: value=那么爱你为什么, TPE1: description=null: value=阿强, TALB: description=null: value=华丽的外衣, TXXX: description=ID: value=105519843, APIC: mimeType=image/jpeg, description=] + sample 0: + time = 0 + flags = 1 + data = length 16384, hash 61D2C5C2 + sample 1: + time = 85333 + flags = 1 + data = length 16384, hash E6D7F214 + sample 2: + time = 170666 + flags = 1 + data = length 16384, hash 59BF0D5D + sample 3: + time = 256000 + flags = 1 + data = length 16384, hash 3625F468 + sample 4: + time = 341333 + flags = 1 + data = length 16384, hash F66A323 + sample 5: + time = 426666 + flags = 1 + data = length 16384, hash CDBAE629 + sample 6: + time = 512000 + flags = 1 + data = length 16384, hash 536F3A91 + sample 7: + time = 597333 + flags = 1 + data = length 16384, hash D4F35C9C + sample 8: + time = 682666 + flags = 1 + data = length 16384, hash EE04CEBF + sample 9: + time = 768000 + flags = 1 + data = length 16384, hash 647E2A67 + sample 10: + time = 853333 + flags = 1 + data = length 16384, hash 31583F2C + sample 11: + time = 938666 + flags = 1 + data = length 16384, hash E433A93D + sample 12: + time = 1024000 + flags = 1 + data = length 16384, hash 5E1C7051 + sample 13: + time = 1109333 + flags = 1 + data = length 16384, hash 43E6E358 + sample 14: + time = 1194666 + flags = 1 + data = length 16384, hash 5DC1B256 + sample 15: + time = 1280000 + flags = 1 + data = length 16384, hash 3D9D95CF + sample 16: + time = 1365333 + flags = 1 + data = length 16384, hash 2A5BD2C0 + sample 17: + time = 1450666 + flags = 1 + data = length 16384, hash 93E25061 + sample 18: + time = 1536000 + flags = 1 + data = length 16384, hash B81793D8 + sample 19: + time = 1621333 + flags = 1 + data = length 16384, hash 1A3BD49F + sample 20: + time = 1706666 + flags = 1 + data = length 16384, hash FB672FF1 + sample 21: + time = 1792000 + flags = 1 + data = length 16384, hash 48AB8B45 + sample 22: + time = 1877333 + flags = 1 + data = length 16384, hash 13C9640A + sample 23: + time = 1962666 + flags = 1 + data = length 16384, hash 499E4A0B + sample 24: + time = 2048000 + flags = 1 + data = length 16384, hash F9A783E6 + sample 25: + time = 2133333 + flags = 1 + data = length 16384, hash D2B77598 + sample 26: + time = 2218666 + flags = 1 + data = length 16384, hash CE5B826C + sample 27: + time = 2304000 + flags = 1 + data = length 16384, hash E99EE956 + sample 28: + time = 2389333 + flags = 1 + data = length 16384, hash F2DB1486 + sample 29: + time = 2474666 + flags = 1 + data = length 16384, hash 1636EAB + sample 30: + time = 2560000 + flags = 1 + data = length 16384, hash 23457C08 + sample 31: + time = 2645333 + flags = 1 + data = length 16384, hash 30EB8381 + sample 32: + time = 2730666 + flags = 1 + data = length 1984, hash 59CFDE1B +tracksEnded = true diff --git a/library/core/src/test/assets/flac/bear_with_picture.flac b/testdata/src/test/assets/flac/bear_with_picture.flac similarity index 100% rename from library/core/src/test/assets/flac/bear_with_picture.flac rename to testdata/src/test/assets/flac/bear_with_picture.flac diff --git a/testdata/src/test/assets/flac/bear_with_picture_flac.0.dump b/testdata/src/test/assets/flac/bear_with_picture_flac.0.dump new file mode 100644 index 0000000000..57c1816674 --- /dev/null +++ b/testdata/src/test/assets/flac/bear_with_picture_flac.0.dump @@ -0,0 +1,152 @@ +seekMap: + isSeekable = true + duration = 2741000 + getPosition(0) = [[timeUs=0, position=39868]] + getPosition(1) = [[timeUs=0, position=39868], [timeUs=85333, position=44898]] + getPosition(1370500) = [[timeUs=1365333, position=121813], [timeUs=1450666, position=127589]] + getPosition(2741000) = [[timeUs=2645333, position=200470]] +numberOfTracks = 1 +track 0: + total output bytes = 164431 + sample count = 33 + format 0: + sampleMimeType = audio/flac + maxInputSize = 5776 + channelCount = 2 + sampleRate = 48000 + metadata = entries=[Picture: mimeType=image/png, description=] + initializationData: + data = length 42, hash 83F6895 + sample 0: + time = 0 + flags = 1 + data = length 5030, hash D2B60530 + sample 1: + time = 85333 + flags = 1 + data = length 5066, hash 4C932A54 + sample 2: + time = 170666 + flags = 1 + data = length 5112, hash 7E5A7B61 + sample 3: + time = 256000 + flags = 1 + data = length 5044, hash 7EF93F13 + sample 4: + time = 341333 + flags = 1 + data = length 4943, hash DE7E27F8 + sample 5: + time = 426666 + flags = 1 + data = length 5121, hash 6D0D0B40 + sample 6: + time = 512000 + flags = 1 + data = length 5068, hash 9924644F + sample 7: + time = 597333 + flags = 1 + data = length 5143, hash 6C34F0CE + sample 8: + time = 682666 + flags = 1 + data = length 5109, hash E3B7BEFB + sample 9: + time = 768000 + flags = 1 + data = length 5129, hash 44111D9B + sample 10: + time = 853333 + flags = 1 + data = length 5031, hash 9D55EA53 + sample 11: + time = 938666 + flags = 1 + data = length 5119, hash E1CB9BA6 + sample 12: + time = 1024000 + flags = 1 + data = length 5360, hash 17265C5D + sample 13: + time = 1109333 + flags = 1 + data = length 5340, hash A90FDDF1 + sample 14: + time = 1194666 + flags = 1 + data = length 5162, hash 31F65AD5 + sample 15: + time = 1280000 + flags = 1 + data = length 5168, hash F2394F2D + sample 16: + time = 1365333 + flags = 1 + data = length 5776, hash 58437AB3 + sample 17: + time = 1450666 + flags = 1 + data = length 5394, hash EBAB20A8 + sample 18: + time = 1536000 + flags = 1 + data = length 5168, hash BF37C7A5 + sample 19: + time = 1621333 + flags = 1 + data = length 5324, hash 59546B7B + sample 20: + time = 1706666 + flags = 1 + data = length 5172, hash 6036EF0B + sample 21: + time = 1792000 + flags = 1 + data = length 5102, hash 5A131071 + sample 22: + time = 1877333 + flags = 1 + data = length 5111, hash 3D9EBB3B + sample 23: + time = 1962666 + flags = 1 + data = length 5113, hash 61101D4F + sample 24: + time = 2048000 + flags = 1 + data = length 5229, hash D2E55742 + sample 25: + time = 2133333 + flags = 1 + data = length 5162, hash 7F2E97FA + sample 26: + time = 2218666 + flags = 1 + data = length 5255, hash D92A782 + sample 27: + time = 2304000 + flags = 1 + data = length 5196, hash 98FE5138 + sample 28: + time = 2389333 + flags = 1 + data = length 5214, hash 3D35C38C + sample 29: + time = 2474666 + flags = 1 + data = length 5211, hash 7E25420F + sample 30: + time = 2560000 + flags = 1 + data = length 5230, hash 2AD96FBC + sample 31: + time = 2645333 + flags = 1 + data = length 3384, hash 938BCDD9 + sample 32: + time = 2730666 + flags = 1 + data = length 445, hash A388E3D6 +tracksEnded = true diff --git a/testdata/src/test/assets/flac/bear_with_picture_flac.1.dump b/testdata/src/test/assets/flac/bear_with_picture_flac.1.dump new file mode 100644 index 0000000000..90da891d74 --- /dev/null +++ b/testdata/src/test/assets/flac/bear_with_picture_flac.1.dump @@ -0,0 +1,112 @@ +seekMap: + isSeekable = true + duration = 2741000 + getPosition(0) = [[timeUs=0, position=39868]] + getPosition(1) = [[timeUs=0, position=39868], [timeUs=85333, position=44898]] + getPosition(1370500) = [[timeUs=1365333, position=121813], [timeUs=1450666, position=127589]] + getPosition(2741000) = [[timeUs=2645333, position=200470]] +numberOfTracks = 1 +track 0: + total output bytes = 113666 + sample count = 23 + format 0: + sampleMimeType = audio/flac + maxInputSize = 5776 + channelCount = 2 + sampleRate = 48000 + metadata = entries=[Picture: mimeType=image/png, description=] + initializationData: + data = length 42, hash 83F6895 + sample 0: + time = 853333 + flags = 1 + data = length 5031, hash 9D55EA53 + sample 1: + time = 938666 + flags = 1 + data = length 5119, hash E1CB9BA6 + sample 2: + time = 1024000 + flags = 1 + data = length 5360, hash 17265C5D + sample 3: + time = 1109333 + flags = 1 + data = length 5340, hash A90FDDF1 + sample 4: + time = 1194666 + flags = 1 + data = length 5162, hash 31F65AD5 + sample 5: + time = 1280000 + flags = 1 + data = length 5168, hash F2394F2D + sample 6: + time = 1365333 + flags = 1 + data = length 5776, hash 58437AB3 + sample 7: + time = 1450666 + flags = 1 + data = length 5394, hash EBAB20A8 + sample 8: + time = 1536000 + flags = 1 + data = length 5168, hash BF37C7A5 + sample 9: + time = 1621333 + flags = 1 + data = length 5324, hash 59546B7B + sample 10: + time = 1706666 + flags = 1 + data = length 5172, hash 6036EF0B + sample 11: + time = 1792000 + flags = 1 + data = length 5102, hash 5A131071 + sample 12: + time = 1877333 + flags = 1 + data = length 5111, hash 3D9EBB3B + sample 13: + time = 1962666 + flags = 1 + data = length 5113, hash 61101D4F + sample 14: + time = 2048000 + flags = 1 + data = length 5229, hash D2E55742 + sample 15: + time = 2133333 + flags = 1 + data = length 5162, hash 7F2E97FA + sample 16: + time = 2218666 + flags = 1 + data = length 5255, hash D92A782 + sample 17: + time = 2304000 + flags = 1 + data = length 5196, hash 98FE5138 + sample 18: + time = 2389333 + flags = 1 + data = length 5214, hash 3D35C38C + sample 19: + time = 2474666 + flags = 1 + data = length 5211, hash 7E25420F + sample 20: + time = 2560000 + flags = 1 + data = length 5230, hash 2AD96FBC + sample 21: + time = 2645333 + flags = 1 + data = length 3384, hash 938BCDD9 + sample 22: + time = 2730666 + flags = 1 + data = length 445, hash A388E3D6 +tracksEnded = true diff --git a/testdata/src/test/assets/flac/bear_with_picture_flac.2.dump b/testdata/src/test/assets/flac/bear_with_picture_flac.2.dump new file mode 100644 index 0000000000..f916615934 --- /dev/null +++ b/testdata/src/test/assets/flac/bear_with_picture_flac.2.dump @@ -0,0 +1,68 @@ +seekMap: + isSeekable = true + duration = 2741000 + getPosition(0) = [[timeUs=0, position=39868]] + getPosition(1) = [[timeUs=0, position=39868], [timeUs=85333, position=44898]] + getPosition(1370500) = [[timeUs=1365333, position=121813], [timeUs=1450666, position=127589]] + getPosition(2741000) = [[timeUs=2645333, position=200470]] +numberOfTracks = 1 +track 0: + total output bytes = 55652 + sample count = 12 + format 0: + sampleMimeType = audio/flac + maxInputSize = 5776 + channelCount = 2 + sampleRate = 48000 + metadata = entries=[Picture: mimeType=image/png, description=] + initializationData: + data = length 42, hash 83F6895 + sample 0: + time = 1792000 + flags = 1 + data = length 5102, hash 5A131071 + sample 1: + time = 1877333 + flags = 1 + data = length 5111, hash 3D9EBB3B + sample 2: + time = 1962666 + flags = 1 + data = length 5113, hash 61101D4F + sample 3: + time = 2048000 + flags = 1 + data = length 5229, hash D2E55742 + sample 4: + time = 2133333 + flags = 1 + data = length 5162, hash 7F2E97FA + sample 5: + time = 2218666 + flags = 1 + data = length 5255, hash D92A782 + sample 6: + time = 2304000 + flags = 1 + data = length 5196, hash 98FE5138 + sample 7: + time = 2389333 + flags = 1 + data = length 5214, hash 3D35C38C + sample 8: + time = 2474666 + flags = 1 + data = length 5211, hash 7E25420F + sample 9: + time = 2560000 + flags = 1 + data = length 5230, hash 2AD96FBC + sample 10: + time = 2645333 + flags = 1 + data = length 3384, hash 938BCDD9 + sample 11: + time = 2730666 + flags = 1 + data = length 445, hash A388E3D6 +tracksEnded = true diff --git a/testdata/src/test/assets/flac/bear_with_picture_flac.3.dump b/testdata/src/test/assets/flac/bear_with_picture_flac.3.dump new file mode 100644 index 0000000000..a3292df552 --- /dev/null +++ b/testdata/src/test/assets/flac/bear_with_picture_flac.3.dump @@ -0,0 +1,28 @@ +seekMap: + isSeekable = true + duration = 2741000 + getPosition(0) = [[timeUs=0, position=39868]] + getPosition(1) = [[timeUs=0, position=39868], [timeUs=85333, position=44898]] + getPosition(1370500) = [[timeUs=1365333, position=121813], [timeUs=1450666, position=127589]] + getPosition(2741000) = [[timeUs=2645333, position=200470]] +numberOfTracks = 1 +track 0: + total output bytes = 3829 + sample count = 2 + format 0: + sampleMimeType = audio/flac + maxInputSize = 5776 + channelCount = 2 + sampleRate = 48000 + metadata = entries=[Picture: mimeType=image/png, description=] + initializationData: + data = length 42, hash 83F6895 + sample 0: + time = 2645333 + flags = 1 + data = length 3384, hash 938BCDD9 + sample 1: + time = 2730666 + flags = 1 + data = length 445, hash A388E3D6 +tracksEnded = true diff --git a/testdata/src/test/assets/flac/bear_with_picture_flac.unknown_length.dump b/testdata/src/test/assets/flac/bear_with_picture_flac.unknown_length.dump new file mode 100644 index 0000000000..57c1816674 --- /dev/null +++ b/testdata/src/test/assets/flac/bear_with_picture_flac.unknown_length.dump @@ -0,0 +1,152 @@ +seekMap: + isSeekable = true + duration = 2741000 + getPosition(0) = [[timeUs=0, position=39868]] + getPosition(1) = [[timeUs=0, position=39868], [timeUs=85333, position=44898]] + getPosition(1370500) = [[timeUs=1365333, position=121813], [timeUs=1450666, position=127589]] + getPosition(2741000) = [[timeUs=2645333, position=200470]] +numberOfTracks = 1 +track 0: + total output bytes = 164431 + sample count = 33 + format 0: + sampleMimeType = audio/flac + maxInputSize = 5776 + channelCount = 2 + sampleRate = 48000 + metadata = entries=[Picture: mimeType=image/png, description=] + initializationData: + data = length 42, hash 83F6895 + sample 0: + time = 0 + flags = 1 + data = length 5030, hash D2B60530 + sample 1: + time = 85333 + flags = 1 + data = length 5066, hash 4C932A54 + sample 2: + time = 170666 + flags = 1 + data = length 5112, hash 7E5A7B61 + sample 3: + time = 256000 + flags = 1 + data = length 5044, hash 7EF93F13 + sample 4: + time = 341333 + flags = 1 + data = length 4943, hash DE7E27F8 + sample 5: + time = 426666 + flags = 1 + data = length 5121, hash 6D0D0B40 + sample 6: + time = 512000 + flags = 1 + data = length 5068, hash 9924644F + sample 7: + time = 597333 + flags = 1 + data = length 5143, hash 6C34F0CE + sample 8: + time = 682666 + flags = 1 + data = length 5109, hash E3B7BEFB + sample 9: + time = 768000 + flags = 1 + data = length 5129, hash 44111D9B + sample 10: + time = 853333 + flags = 1 + data = length 5031, hash 9D55EA53 + sample 11: + time = 938666 + flags = 1 + data = length 5119, hash E1CB9BA6 + sample 12: + time = 1024000 + flags = 1 + data = length 5360, hash 17265C5D + sample 13: + time = 1109333 + flags = 1 + data = length 5340, hash A90FDDF1 + sample 14: + time = 1194666 + flags = 1 + data = length 5162, hash 31F65AD5 + sample 15: + time = 1280000 + flags = 1 + data = length 5168, hash F2394F2D + sample 16: + time = 1365333 + flags = 1 + data = length 5776, hash 58437AB3 + sample 17: + time = 1450666 + flags = 1 + data = length 5394, hash EBAB20A8 + sample 18: + time = 1536000 + flags = 1 + data = length 5168, hash BF37C7A5 + sample 19: + time = 1621333 + flags = 1 + data = length 5324, hash 59546B7B + sample 20: + time = 1706666 + flags = 1 + data = length 5172, hash 6036EF0B + sample 21: + time = 1792000 + flags = 1 + data = length 5102, hash 5A131071 + sample 22: + time = 1877333 + flags = 1 + data = length 5111, hash 3D9EBB3B + sample 23: + time = 1962666 + flags = 1 + data = length 5113, hash 61101D4F + sample 24: + time = 2048000 + flags = 1 + data = length 5229, hash D2E55742 + sample 25: + time = 2133333 + flags = 1 + data = length 5162, hash 7F2E97FA + sample 26: + time = 2218666 + flags = 1 + data = length 5255, hash D92A782 + sample 27: + time = 2304000 + flags = 1 + data = length 5196, hash 98FE5138 + sample 28: + time = 2389333 + flags = 1 + data = length 5214, hash 3D35C38C + sample 29: + time = 2474666 + flags = 1 + data = length 5211, hash 7E25420F + sample 30: + time = 2560000 + flags = 1 + data = length 5230, hash 2AD96FBC + sample 31: + time = 2645333 + flags = 1 + data = length 3384, hash 938BCDD9 + sample 32: + time = 2730666 + flags = 1 + data = length 445, hash A388E3D6 +tracksEnded = true diff --git a/testdata/src/test/assets/flac/bear_with_picture_raw.0.dump b/testdata/src/test/assets/flac/bear_with_picture_raw.0.dump new file mode 100644 index 0000000000..0ca949ec09 --- /dev/null +++ b/testdata/src/test/assets/flac/bear_with_picture_raw.0.dump @@ -0,0 +1,153 @@ +seekMap: + isSeekable = true + duration = 2741000 + getPosition(0) = [[timeUs=0, position=39868]] + getPosition(1) = [[timeUs=0, position=39868]] + getPosition(1370500) = [[timeUs=1365333, position=121813], [timeUs=1450666, position=127589]] + getPosition(2741000) = [[timeUs=2645333, position=200470]] +numberOfTracks = 1 +track 0: + total output bytes = 526272 + sample count = 33 + format 0: + averageBitrate = 1536000 + peakBitrate = 1536000 + sampleMimeType = audio/raw + maxInputSize = 16384 + channelCount = 2 + sampleRate = 48000 + pcmEncoding = 2 + metadata = entries=[Picture: mimeType=image/png, description=] + sample 0: + time = 0 + flags = 1 + data = length 16384, hash 61D2C5C2 + sample 1: + time = 85333 + flags = 1 + data = length 16384, hash E6D7F214 + sample 2: + time = 170666 + flags = 1 + data = length 16384, hash 59BF0D5D + sample 3: + time = 256000 + flags = 1 + data = length 16384, hash 3625F468 + sample 4: + time = 341333 + flags = 1 + data = length 16384, hash F66A323 + sample 5: + time = 426666 + flags = 1 + data = length 16384, hash CDBAE629 + sample 6: + time = 512000 + flags = 1 + data = length 16384, hash 536F3A91 + sample 7: + time = 597333 + flags = 1 + data = length 16384, hash D4F35C9C + sample 8: + time = 682666 + flags = 1 + data = length 16384, hash EE04CEBF + sample 9: + time = 768000 + flags = 1 + data = length 16384, hash 647E2A67 + sample 10: + time = 853333 + flags = 1 + data = length 16384, hash 31583F2C + sample 11: + time = 938666 + flags = 1 + data = length 16384, hash E433A93D + sample 12: + time = 1024000 + flags = 1 + data = length 16384, hash 5E1C7051 + sample 13: + time = 1109333 + flags = 1 + data = length 16384, hash 43E6E358 + sample 14: + time = 1194666 + flags = 1 + data = length 16384, hash 5DC1B256 + sample 15: + time = 1280000 + flags = 1 + data = length 16384, hash 3D9D95CF + sample 16: + time = 1365333 + flags = 1 + data = length 16384, hash 2A5BD2C0 + sample 17: + time = 1450666 + flags = 1 + data = length 16384, hash 93E25061 + sample 18: + time = 1536000 + flags = 1 + data = length 16384, hash B81793D8 + sample 19: + time = 1621333 + flags = 1 + data = length 16384, hash 1A3BD49F + sample 20: + time = 1706666 + flags = 1 + data = length 16384, hash FB672FF1 + sample 21: + time = 1792000 + flags = 1 + data = length 16384, hash 48AB8B45 + sample 22: + time = 1877333 + flags = 1 + data = length 16384, hash 13C9640A + sample 23: + time = 1962666 + flags = 1 + data = length 16384, hash 499E4A0B + sample 24: + time = 2048000 + flags = 1 + data = length 16384, hash F9A783E6 + sample 25: + time = 2133333 + flags = 1 + data = length 16384, hash D2B77598 + sample 26: + time = 2218666 + flags = 1 + data = length 16384, hash CE5B826C + sample 27: + time = 2304000 + flags = 1 + data = length 16384, hash E99EE956 + sample 28: + time = 2389333 + flags = 1 + data = length 16384, hash F2DB1486 + sample 29: + time = 2474666 + flags = 1 + data = length 16384, hash 1636EAB + sample 30: + time = 2560000 + flags = 1 + data = length 16384, hash 23457C08 + sample 31: + time = 2645333 + flags = 1 + data = length 16384, hash 30EB8381 + sample 32: + time = 2730666 + flags = 1 + data = length 1984, hash 59CFDE1B +tracksEnded = true diff --git a/testdata/src/test/assets/flac/bear_with_picture_raw.1.dump b/testdata/src/test/assets/flac/bear_with_picture_raw.1.dump new file mode 100644 index 0000000000..075fcec267 --- /dev/null +++ b/testdata/src/test/assets/flac/bear_with_picture_raw.1.dump @@ -0,0 +1,113 @@ +seekMap: + isSeekable = true + duration = 2741000 + getPosition(0) = [[timeUs=0, position=39868]] + getPosition(1) = [[timeUs=0, position=39868]] + getPosition(1370500) = [[timeUs=1365333, position=121813], [timeUs=1450666, position=127589]] + getPosition(2741000) = [[timeUs=2645333, position=200470]] +numberOfTracks = 1 +track 0: + total output bytes = 362432 + sample count = 23 + format 0: + averageBitrate = 1536000 + peakBitrate = 1536000 + sampleMimeType = audio/raw + maxInputSize = 16384 + channelCount = 2 + sampleRate = 48000 + pcmEncoding = 2 + metadata = entries=[Picture: mimeType=image/png, description=] + sample 0: + time = 853333 + flags = 1 + data = length 16384, hash 31583F2C + sample 1: + time = 938666 + flags = 1 + data = length 16384, hash E433A93D + sample 2: + time = 1024000 + flags = 1 + data = length 16384, hash 5E1C7051 + sample 3: + time = 1109333 + flags = 1 + data = length 16384, hash 43E6E358 + sample 4: + time = 1194666 + flags = 1 + data = length 16384, hash 5DC1B256 + sample 5: + time = 1280000 + flags = 1 + data = length 16384, hash 3D9D95CF + sample 6: + time = 1365333 + flags = 1 + data = length 16384, hash 2A5BD2C0 + sample 7: + time = 1450666 + flags = 1 + data = length 16384, hash 93E25061 + sample 8: + time = 1536000 + flags = 1 + data = length 16384, hash B81793D8 + sample 9: + time = 1621333 + flags = 1 + data = length 16384, hash 1A3BD49F + sample 10: + time = 1706666 + flags = 1 + data = length 16384, hash FB672FF1 + sample 11: + time = 1792000 + flags = 1 + data = length 16384, hash 48AB8B45 + sample 12: + time = 1877333 + flags = 1 + data = length 16384, hash 13C9640A + sample 13: + time = 1962666 + flags = 1 + data = length 16384, hash 499E4A0B + sample 14: + time = 2048000 + flags = 1 + data = length 16384, hash F9A783E6 + sample 15: + time = 2133333 + flags = 1 + data = length 16384, hash D2B77598 + sample 16: + time = 2218666 + flags = 1 + data = length 16384, hash CE5B826C + sample 17: + time = 2304000 + flags = 1 + data = length 16384, hash E99EE956 + sample 18: + time = 2389333 + flags = 1 + data = length 16384, hash F2DB1486 + sample 19: + time = 2474666 + flags = 1 + data = length 16384, hash 1636EAB + sample 20: + time = 2560000 + flags = 1 + data = length 16384, hash 23457C08 + sample 21: + time = 2645333 + flags = 1 + data = length 16384, hash 30EB8381 + sample 22: + time = 2730666 + flags = 1 + data = length 1984, hash 59CFDE1B +tracksEnded = true diff --git a/testdata/src/test/assets/flac/bear_with_picture_raw.2.dump b/testdata/src/test/assets/flac/bear_with_picture_raw.2.dump new file mode 100644 index 0000000000..a49beeed80 --- /dev/null +++ b/testdata/src/test/assets/flac/bear_with_picture_raw.2.dump @@ -0,0 +1,69 @@ +seekMap: + isSeekable = true + duration = 2741000 + getPosition(0) = [[timeUs=0, position=39868]] + getPosition(1) = [[timeUs=0, position=39868]] + getPosition(1370500) = [[timeUs=1365333, position=121813], [timeUs=1450666, position=127589]] + getPosition(2741000) = [[timeUs=2645333, position=200470]] +numberOfTracks = 1 +track 0: + total output bytes = 182208 + sample count = 12 + format 0: + averageBitrate = 1536000 + peakBitrate = 1536000 + sampleMimeType = audio/raw + maxInputSize = 16384 + channelCount = 2 + sampleRate = 48000 + pcmEncoding = 2 + metadata = entries=[Picture: mimeType=image/png, description=] + sample 0: + time = 1792000 + flags = 1 + data = length 16384, hash 48AB8B45 + sample 1: + time = 1877333 + flags = 1 + data = length 16384, hash 13C9640A + sample 2: + time = 1962666 + flags = 1 + data = length 16384, hash 499E4A0B + sample 3: + time = 2048000 + flags = 1 + data = length 16384, hash F9A783E6 + sample 4: + time = 2133333 + flags = 1 + data = length 16384, hash D2B77598 + sample 5: + time = 2218666 + flags = 1 + data = length 16384, hash CE5B826C + sample 6: + time = 2304000 + flags = 1 + data = length 16384, hash E99EE956 + sample 7: + time = 2389333 + flags = 1 + data = length 16384, hash F2DB1486 + sample 8: + time = 2474666 + flags = 1 + data = length 16384, hash 1636EAB + sample 9: + time = 2560000 + flags = 1 + data = length 16384, hash 23457C08 + sample 10: + time = 2645333 + flags = 1 + data = length 16384, hash 30EB8381 + sample 11: + time = 2730666 + flags = 1 + data = length 1984, hash 59CFDE1B +tracksEnded = true diff --git a/testdata/src/test/assets/flac/bear_with_picture_raw.3.dump b/testdata/src/test/assets/flac/bear_with_picture_raw.3.dump new file mode 100644 index 0000000000..22330e462a --- /dev/null +++ b/testdata/src/test/assets/flac/bear_with_picture_raw.3.dump @@ -0,0 +1,29 @@ +seekMap: + isSeekable = true + duration = 2741000 + getPosition(0) = [[timeUs=0, position=39868]] + getPosition(1) = [[timeUs=0, position=39868]] + getPosition(1370500) = [[timeUs=1365333, position=121813], [timeUs=1450666, position=127589]] + getPosition(2741000) = [[timeUs=2645333, position=200470]] +numberOfTracks = 1 +track 0: + total output bytes = 18368 + sample count = 2 + format 0: + averageBitrate = 1536000 + peakBitrate = 1536000 + sampleMimeType = audio/raw + maxInputSize = 16384 + channelCount = 2 + sampleRate = 48000 + pcmEncoding = 2 + metadata = entries=[Picture: mimeType=image/png, description=] + sample 0: + time = 2645333 + flags = 1 + data = length 16384, hash 30EB8381 + sample 1: + time = 2730666 + flags = 1 + data = length 1984, hash 59CFDE1B +tracksEnded = true diff --git a/testdata/src/test/assets/flac/bear_with_picture_raw.unknown_length.dump b/testdata/src/test/assets/flac/bear_with_picture_raw.unknown_length.dump new file mode 100644 index 0000000000..0ca949ec09 --- /dev/null +++ b/testdata/src/test/assets/flac/bear_with_picture_raw.unknown_length.dump @@ -0,0 +1,153 @@ +seekMap: + isSeekable = true + duration = 2741000 + getPosition(0) = [[timeUs=0, position=39868]] + getPosition(1) = [[timeUs=0, position=39868]] + getPosition(1370500) = [[timeUs=1365333, position=121813], [timeUs=1450666, position=127589]] + getPosition(2741000) = [[timeUs=2645333, position=200470]] +numberOfTracks = 1 +track 0: + total output bytes = 526272 + sample count = 33 + format 0: + averageBitrate = 1536000 + peakBitrate = 1536000 + sampleMimeType = audio/raw + maxInputSize = 16384 + channelCount = 2 + sampleRate = 48000 + pcmEncoding = 2 + metadata = entries=[Picture: mimeType=image/png, description=] + sample 0: + time = 0 + flags = 1 + data = length 16384, hash 61D2C5C2 + sample 1: + time = 85333 + flags = 1 + data = length 16384, hash E6D7F214 + sample 2: + time = 170666 + flags = 1 + data = length 16384, hash 59BF0D5D + sample 3: + time = 256000 + flags = 1 + data = length 16384, hash 3625F468 + sample 4: + time = 341333 + flags = 1 + data = length 16384, hash F66A323 + sample 5: + time = 426666 + flags = 1 + data = length 16384, hash CDBAE629 + sample 6: + time = 512000 + flags = 1 + data = length 16384, hash 536F3A91 + sample 7: + time = 597333 + flags = 1 + data = length 16384, hash D4F35C9C + sample 8: + time = 682666 + flags = 1 + data = length 16384, hash EE04CEBF + sample 9: + time = 768000 + flags = 1 + data = length 16384, hash 647E2A67 + sample 10: + time = 853333 + flags = 1 + data = length 16384, hash 31583F2C + sample 11: + time = 938666 + flags = 1 + data = length 16384, hash E433A93D + sample 12: + time = 1024000 + flags = 1 + data = length 16384, hash 5E1C7051 + sample 13: + time = 1109333 + flags = 1 + data = length 16384, hash 43E6E358 + sample 14: + time = 1194666 + flags = 1 + data = length 16384, hash 5DC1B256 + sample 15: + time = 1280000 + flags = 1 + data = length 16384, hash 3D9D95CF + sample 16: + time = 1365333 + flags = 1 + data = length 16384, hash 2A5BD2C0 + sample 17: + time = 1450666 + flags = 1 + data = length 16384, hash 93E25061 + sample 18: + time = 1536000 + flags = 1 + data = length 16384, hash B81793D8 + sample 19: + time = 1621333 + flags = 1 + data = length 16384, hash 1A3BD49F + sample 20: + time = 1706666 + flags = 1 + data = length 16384, hash FB672FF1 + sample 21: + time = 1792000 + flags = 1 + data = length 16384, hash 48AB8B45 + sample 22: + time = 1877333 + flags = 1 + data = length 16384, hash 13C9640A + sample 23: + time = 1962666 + flags = 1 + data = length 16384, hash 499E4A0B + sample 24: + time = 2048000 + flags = 1 + data = length 16384, hash F9A783E6 + sample 25: + time = 2133333 + flags = 1 + data = length 16384, hash D2B77598 + sample 26: + time = 2218666 + flags = 1 + data = length 16384, hash CE5B826C + sample 27: + time = 2304000 + flags = 1 + data = length 16384, hash E99EE956 + sample 28: + time = 2389333 + flags = 1 + data = length 16384, hash F2DB1486 + sample 29: + time = 2474666 + flags = 1 + data = length 16384, hash 1636EAB + sample 30: + time = 2560000 + flags = 1 + data = length 16384, hash 23457C08 + sample 31: + time = 2645333 + flags = 1 + data = length 16384, hash 30EB8381 + sample 32: + time = 2730666 + flags = 1 + data = length 1984, hash 59CFDE1B +tracksEnded = true diff --git a/library/core/src/test/assets/flac/bear_with_vorbis_comments.flac b/testdata/src/test/assets/flac/bear_with_vorbis_comments.flac similarity index 100% rename from library/core/src/test/assets/flac/bear_with_vorbis_comments.flac rename to testdata/src/test/assets/flac/bear_with_vorbis_comments.flac diff --git a/testdata/src/test/assets/flac/bear_with_vorbis_comments_flac.0.dump b/testdata/src/test/assets/flac/bear_with_vorbis_comments_flac.0.dump new file mode 100644 index 0000000000..d5cdf8cc92 --- /dev/null +++ b/testdata/src/test/assets/flac/bear_with_vorbis_comments_flac.0.dump @@ -0,0 +1,152 @@ +seekMap: + isSeekable = true + duration = 2741000 + getPosition(0) = [[timeUs=0, position=8880]] + getPosition(1) = [[timeUs=0, position=8880], [timeUs=85333, position=13910]] + getPosition(1370500) = [[timeUs=1365333, position=90825], [timeUs=1450666, position=96601]] + getPosition(2741000) = [[timeUs=2645333, position=169482]] +numberOfTracks = 1 +track 0: + total output bytes = 164431 + sample count = 33 + format 0: + sampleMimeType = audio/flac + maxInputSize = 5776 + channelCount = 2 + sampleRate = 48000 + metadata = entries=[VC: TITLE=test title, VC: ARTIST=test artist] + initializationData: + data = length 42, hash 83F6895 + sample 0: + time = 0 + flags = 1 + data = length 5030, hash D2B60530 + sample 1: + time = 85333 + flags = 1 + data = length 5066, hash 4C932A54 + sample 2: + time = 170666 + flags = 1 + data = length 5112, hash 7E5A7B61 + sample 3: + time = 256000 + flags = 1 + data = length 5044, hash 7EF93F13 + sample 4: + time = 341333 + flags = 1 + data = length 4943, hash DE7E27F8 + sample 5: + time = 426666 + flags = 1 + data = length 5121, hash 6D0D0B40 + sample 6: + time = 512000 + flags = 1 + data = length 5068, hash 9924644F + sample 7: + time = 597333 + flags = 1 + data = length 5143, hash 6C34F0CE + sample 8: + time = 682666 + flags = 1 + data = length 5109, hash E3B7BEFB + sample 9: + time = 768000 + flags = 1 + data = length 5129, hash 44111D9B + sample 10: + time = 853333 + flags = 1 + data = length 5031, hash 9D55EA53 + sample 11: + time = 938666 + flags = 1 + data = length 5119, hash E1CB9BA6 + sample 12: + time = 1024000 + flags = 1 + data = length 5360, hash 17265C5D + sample 13: + time = 1109333 + flags = 1 + data = length 5340, hash A90FDDF1 + sample 14: + time = 1194666 + flags = 1 + data = length 5162, hash 31F65AD5 + sample 15: + time = 1280000 + flags = 1 + data = length 5168, hash F2394F2D + sample 16: + time = 1365333 + flags = 1 + data = length 5776, hash 58437AB3 + sample 17: + time = 1450666 + flags = 1 + data = length 5394, hash EBAB20A8 + sample 18: + time = 1536000 + flags = 1 + data = length 5168, hash BF37C7A5 + sample 19: + time = 1621333 + flags = 1 + data = length 5324, hash 59546B7B + sample 20: + time = 1706666 + flags = 1 + data = length 5172, hash 6036EF0B + sample 21: + time = 1792000 + flags = 1 + data = length 5102, hash 5A131071 + sample 22: + time = 1877333 + flags = 1 + data = length 5111, hash 3D9EBB3B + sample 23: + time = 1962666 + flags = 1 + data = length 5113, hash 61101D4F + sample 24: + time = 2048000 + flags = 1 + data = length 5229, hash D2E55742 + sample 25: + time = 2133333 + flags = 1 + data = length 5162, hash 7F2E97FA + sample 26: + time = 2218666 + flags = 1 + data = length 5255, hash D92A782 + sample 27: + time = 2304000 + flags = 1 + data = length 5196, hash 98FE5138 + sample 28: + time = 2389333 + flags = 1 + data = length 5214, hash 3D35C38C + sample 29: + time = 2474666 + flags = 1 + data = length 5211, hash 7E25420F + sample 30: + time = 2560000 + flags = 1 + data = length 5230, hash 2AD96FBC + sample 31: + time = 2645333 + flags = 1 + data = length 3384, hash 938BCDD9 + sample 32: + time = 2730666 + flags = 1 + data = length 445, hash A388E3D6 +tracksEnded = true diff --git a/testdata/src/test/assets/flac/bear_with_vorbis_comments_flac.1.dump b/testdata/src/test/assets/flac/bear_with_vorbis_comments_flac.1.dump new file mode 100644 index 0000000000..b3d7f93ada --- /dev/null +++ b/testdata/src/test/assets/flac/bear_with_vorbis_comments_flac.1.dump @@ -0,0 +1,112 @@ +seekMap: + isSeekable = true + duration = 2741000 + getPosition(0) = [[timeUs=0, position=8880]] + getPosition(1) = [[timeUs=0, position=8880], [timeUs=85333, position=13910]] + getPosition(1370500) = [[timeUs=1365333, position=90825], [timeUs=1450666, position=96601]] + getPosition(2741000) = [[timeUs=2645333, position=169482]] +numberOfTracks = 1 +track 0: + total output bytes = 113666 + sample count = 23 + format 0: + sampleMimeType = audio/flac + maxInputSize = 5776 + channelCount = 2 + sampleRate = 48000 + metadata = entries=[VC: TITLE=test title, VC: ARTIST=test artist] + initializationData: + data = length 42, hash 83F6895 + sample 0: + time = 853333 + flags = 1 + data = length 5031, hash 9D55EA53 + sample 1: + time = 938666 + flags = 1 + data = length 5119, hash E1CB9BA6 + sample 2: + time = 1024000 + flags = 1 + data = length 5360, hash 17265C5D + sample 3: + time = 1109333 + flags = 1 + data = length 5340, hash A90FDDF1 + sample 4: + time = 1194666 + flags = 1 + data = length 5162, hash 31F65AD5 + sample 5: + time = 1280000 + flags = 1 + data = length 5168, hash F2394F2D + sample 6: + time = 1365333 + flags = 1 + data = length 5776, hash 58437AB3 + sample 7: + time = 1450666 + flags = 1 + data = length 5394, hash EBAB20A8 + sample 8: + time = 1536000 + flags = 1 + data = length 5168, hash BF37C7A5 + sample 9: + time = 1621333 + flags = 1 + data = length 5324, hash 59546B7B + sample 10: + time = 1706666 + flags = 1 + data = length 5172, hash 6036EF0B + sample 11: + time = 1792000 + flags = 1 + data = length 5102, hash 5A131071 + sample 12: + time = 1877333 + flags = 1 + data = length 5111, hash 3D9EBB3B + sample 13: + time = 1962666 + flags = 1 + data = length 5113, hash 61101D4F + sample 14: + time = 2048000 + flags = 1 + data = length 5229, hash D2E55742 + sample 15: + time = 2133333 + flags = 1 + data = length 5162, hash 7F2E97FA + sample 16: + time = 2218666 + flags = 1 + data = length 5255, hash D92A782 + sample 17: + time = 2304000 + flags = 1 + data = length 5196, hash 98FE5138 + sample 18: + time = 2389333 + flags = 1 + data = length 5214, hash 3D35C38C + sample 19: + time = 2474666 + flags = 1 + data = length 5211, hash 7E25420F + sample 20: + time = 2560000 + flags = 1 + data = length 5230, hash 2AD96FBC + sample 21: + time = 2645333 + flags = 1 + data = length 3384, hash 938BCDD9 + sample 22: + time = 2730666 + flags = 1 + data = length 445, hash A388E3D6 +tracksEnded = true diff --git a/testdata/src/test/assets/flac/bear_with_vorbis_comments_flac.2.dump b/testdata/src/test/assets/flac/bear_with_vorbis_comments_flac.2.dump new file mode 100644 index 0000000000..58c8fb343f --- /dev/null +++ b/testdata/src/test/assets/flac/bear_with_vorbis_comments_flac.2.dump @@ -0,0 +1,68 @@ +seekMap: + isSeekable = true + duration = 2741000 + getPosition(0) = [[timeUs=0, position=8880]] + getPosition(1) = [[timeUs=0, position=8880], [timeUs=85333, position=13910]] + getPosition(1370500) = [[timeUs=1365333, position=90825], [timeUs=1450666, position=96601]] + getPosition(2741000) = [[timeUs=2645333, position=169482]] +numberOfTracks = 1 +track 0: + total output bytes = 55652 + sample count = 12 + format 0: + sampleMimeType = audio/flac + maxInputSize = 5776 + channelCount = 2 + sampleRate = 48000 + metadata = entries=[VC: TITLE=test title, VC: ARTIST=test artist] + initializationData: + data = length 42, hash 83F6895 + sample 0: + time = 1792000 + flags = 1 + data = length 5102, hash 5A131071 + sample 1: + time = 1877333 + flags = 1 + data = length 5111, hash 3D9EBB3B + sample 2: + time = 1962666 + flags = 1 + data = length 5113, hash 61101D4F + sample 3: + time = 2048000 + flags = 1 + data = length 5229, hash D2E55742 + sample 4: + time = 2133333 + flags = 1 + data = length 5162, hash 7F2E97FA + sample 5: + time = 2218666 + flags = 1 + data = length 5255, hash D92A782 + sample 6: + time = 2304000 + flags = 1 + data = length 5196, hash 98FE5138 + sample 7: + time = 2389333 + flags = 1 + data = length 5214, hash 3D35C38C + sample 8: + time = 2474666 + flags = 1 + data = length 5211, hash 7E25420F + sample 9: + time = 2560000 + flags = 1 + data = length 5230, hash 2AD96FBC + sample 10: + time = 2645333 + flags = 1 + data = length 3384, hash 938BCDD9 + sample 11: + time = 2730666 + flags = 1 + data = length 445, hash A388E3D6 +tracksEnded = true diff --git a/testdata/src/test/assets/flac/bear_with_vorbis_comments_flac.3.dump b/testdata/src/test/assets/flac/bear_with_vorbis_comments_flac.3.dump new file mode 100644 index 0000000000..7f2a7eb16b --- /dev/null +++ b/testdata/src/test/assets/flac/bear_with_vorbis_comments_flac.3.dump @@ -0,0 +1,28 @@ +seekMap: + isSeekable = true + duration = 2741000 + getPosition(0) = [[timeUs=0, position=8880]] + getPosition(1) = [[timeUs=0, position=8880], [timeUs=85333, position=13910]] + getPosition(1370500) = [[timeUs=1365333, position=90825], [timeUs=1450666, position=96601]] + getPosition(2741000) = [[timeUs=2645333, position=169482]] +numberOfTracks = 1 +track 0: + total output bytes = 3829 + sample count = 2 + format 0: + sampleMimeType = audio/flac + maxInputSize = 5776 + channelCount = 2 + sampleRate = 48000 + metadata = entries=[VC: TITLE=test title, VC: ARTIST=test artist] + initializationData: + data = length 42, hash 83F6895 + sample 0: + time = 2645333 + flags = 1 + data = length 3384, hash 938BCDD9 + sample 1: + time = 2730666 + flags = 1 + data = length 445, hash A388E3D6 +tracksEnded = true diff --git a/testdata/src/test/assets/flac/bear_with_vorbis_comments_flac.unknown_length.dump b/testdata/src/test/assets/flac/bear_with_vorbis_comments_flac.unknown_length.dump new file mode 100644 index 0000000000..d5cdf8cc92 --- /dev/null +++ b/testdata/src/test/assets/flac/bear_with_vorbis_comments_flac.unknown_length.dump @@ -0,0 +1,152 @@ +seekMap: + isSeekable = true + duration = 2741000 + getPosition(0) = [[timeUs=0, position=8880]] + getPosition(1) = [[timeUs=0, position=8880], [timeUs=85333, position=13910]] + getPosition(1370500) = [[timeUs=1365333, position=90825], [timeUs=1450666, position=96601]] + getPosition(2741000) = [[timeUs=2645333, position=169482]] +numberOfTracks = 1 +track 0: + total output bytes = 164431 + sample count = 33 + format 0: + sampleMimeType = audio/flac + maxInputSize = 5776 + channelCount = 2 + sampleRate = 48000 + metadata = entries=[VC: TITLE=test title, VC: ARTIST=test artist] + initializationData: + data = length 42, hash 83F6895 + sample 0: + time = 0 + flags = 1 + data = length 5030, hash D2B60530 + sample 1: + time = 85333 + flags = 1 + data = length 5066, hash 4C932A54 + sample 2: + time = 170666 + flags = 1 + data = length 5112, hash 7E5A7B61 + sample 3: + time = 256000 + flags = 1 + data = length 5044, hash 7EF93F13 + sample 4: + time = 341333 + flags = 1 + data = length 4943, hash DE7E27F8 + sample 5: + time = 426666 + flags = 1 + data = length 5121, hash 6D0D0B40 + sample 6: + time = 512000 + flags = 1 + data = length 5068, hash 9924644F + sample 7: + time = 597333 + flags = 1 + data = length 5143, hash 6C34F0CE + sample 8: + time = 682666 + flags = 1 + data = length 5109, hash E3B7BEFB + sample 9: + time = 768000 + flags = 1 + data = length 5129, hash 44111D9B + sample 10: + time = 853333 + flags = 1 + data = length 5031, hash 9D55EA53 + sample 11: + time = 938666 + flags = 1 + data = length 5119, hash E1CB9BA6 + sample 12: + time = 1024000 + flags = 1 + data = length 5360, hash 17265C5D + sample 13: + time = 1109333 + flags = 1 + data = length 5340, hash A90FDDF1 + sample 14: + time = 1194666 + flags = 1 + data = length 5162, hash 31F65AD5 + sample 15: + time = 1280000 + flags = 1 + data = length 5168, hash F2394F2D + sample 16: + time = 1365333 + flags = 1 + data = length 5776, hash 58437AB3 + sample 17: + time = 1450666 + flags = 1 + data = length 5394, hash EBAB20A8 + sample 18: + time = 1536000 + flags = 1 + data = length 5168, hash BF37C7A5 + sample 19: + time = 1621333 + flags = 1 + data = length 5324, hash 59546B7B + sample 20: + time = 1706666 + flags = 1 + data = length 5172, hash 6036EF0B + sample 21: + time = 1792000 + flags = 1 + data = length 5102, hash 5A131071 + sample 22: + time = 1877333 + flags = 1 + data = length 5111, hash 3D9EBB3B + sample 23: + time = 1962666 + flags = 1 + data = length 5113, hash 61101D4F + sample 24: + time = 2048000 + flags = 1 + data = length 5229, hash D2E55742 + sample 25: + time = 2133333 + flags = 1 + data = length 5162, hash 7F2E97FA + sample 26: + time = 2218666 + flags = 1 + data = length 5255, hash D92A782 + sample 27: + time = 2304000 + flags = 1 + data = length 5196, hash 98FE5138 + sample 28: + time = 2389333 + flags = 1 + data = length 5214, hash 3D35C38C + sample 29: + time = 2474666 + flags = 1 + data = length 5211, hash 7E25420F + sample 30: + time = 2560000 + flags = 1 + data = length 5230, hash 2AD96FBC + sample 31: + time = 2645333 + flags = 1 + data = length 3384, hash 938BCDD9 + sample 32: + time = 2730666 + flags = 1 + data = length 445, hash A388E3D6 +tracksEnded = true diff --git a/testdata/src/test/assets/flac/bear_with_vorbis_comments_raw.0.dump b/testdata/src/test/assets/flac/bear_with_vorbis_comments_raw.0.dump new file mode 100644 index 0000000000..d6faa106c6 --- /dev/null +++ b/testdata/src/test/assets/flac/bear_with_vorbis_comments_raw.0.dump @@ -0,0 +1,153 @@ +seekMap: + isSeekable = true + duration = 2741000 + getPosition(0) = [[timeUs=0, position=8880]] + getPosition(1) = [[timeUs=0, position=8880]] + getPosition(1370500) = [[timeUs=1365333, position=90825], [timeUs=1450666, position=96601]] + getPosition(2741000) = [[timeUs=2645333, position=169482]] +numberOfTracks = 1 +track 0: + total output bytes = 526272 + sample count = 33 + format 0: + averageBitrate = 1536000 + peakBitrate = 1536000 + sampleMimeType = audio/raw + maxInputSize = 16384 + channelCount = 2 + sampleRate = 48000 + pcmEncoding = 2 + metadata = entries=[VC: TITLE=test title, VC: ARTIST=test artist] + sample 0: + time = 0 + flags = 1 + data = length 16384, hash 61D2C5C2 + sample 1: + time = 85333 + flags = 1 + data = length 16384, hash E6D7F214 + sample 2: + time = 170666 + flags = 1 + data = length 16384, hash 59BF0D5D + sample 3: + time = 256000 + flags = 1 + data = length 16384, hash 3625F468 + sample 4: + time = 341333 + flags = 1 + data = length 16384, hash F66A323 + sample 5: + time = 426666 + flags = 1 + data = length 16384, hash CDBAE629 + sample 6: + time = 512000 + flags = 1 + data = length 16384, hash 536F3A91 + sample 7: + time = 597333 + flags = 1 + data = length 16384, hash D4F35C9C + sample 8: + time = 682666 + flags = 1 + data = length 16384, hash EE04CEBF + sample 9: + time = 768000 + flags = 1 + data = length 16384, hash 647E2A67 + sample 10: + time = 853333 + flags = 1 + data = length 16384, hash 31583F2C + sample 11: + time = 938666 + flags = 1 + data = length 16384, hash E433A93D + sample 12: + time = 1024000 + flags = 1 + data = length 16384, hash 5E1C7051 + sample 13: + time = 1109333 + flags = 1 + data = length 16384, hash 43E6E358 + sample 14: + time = 1194666 + flags = 1 + data = length 16384, hash 5DC1B256 + sample 15: + time = 1280000 + flags = 1 + data = length 16384, hash 3D9D95CF + sample 16: + time = 1365333 + flags = 1 + data = length 16384, hash 2A5BD2C0 + sample 17: + time = 1450666 + flags = 1 + data = length 16384, hash 93E25061 + sample 18: + time = 1536000 + flags = 1 + data = length 16384, hash B81793D8 + sample 19: + time = 1621333 + flags = 1 + data = length 16384, hash 1A3BD49F + sample 20: + time = 1706666 + flags = 1 + data = length 16384, hash FB672FF1 + sample 21: + time = 1792000 + flags = 1 + data = length 16384, hash 48AB8B45 + sample 22: + time = 1877333 + flags = 1 + data = length 16384, hash 13C9640A + sample 23: + time = 1962666 + flags = 1 + data = length 16384, hash 499E4A0B + sample 24: + time = 2048000 + flags = 1 + data = length 16384, hash F9A783E6 + sample 25: + time = 2133333 + flags = 1 + data = length 16384, hash D2B77598 + sample 26: + time = 2218666 + flags = 1 + data = length 16384, hash CE5B826C + sample 27: + time = 2304000 + flags = 1 + data = length 16384, hash E99EE956 + sample 28: + time = 2389333 + flags = 1 + data = length 16384, hash F2DB1486 + sample 29: + time = 2474666 + flags = 1 + data = length 16384, hash 1636EAB + sample 30: + time = 2560000 + flags = 1 + data = length 16384, hash 23457C08 + sample 31: + time = 2645333 + flags = 1 + data = length 16384, hash 30EB8381 + sample 32: + time = 2730666 + flags = 1 + data = length 1984, hash 59CFDE1B +tracksEnded = true diff --git a/testdata/src/test/assets/flac/bear_with_vorbis_comments_raw.1.dump b/testdata/src/test/assets/flac/bear_with_vorbis_comments_raw.1.dump new file mode 100644 index 0000000000..7e2bac3904 --- /dev/null +++ b/testdata/src/test/assets/flac/bear_with_vorbis_comments_raw.1.dump @@ -0,0 +1,113 @@ +seekMap: + isSeekable = true + duration = 2741000 + getPosition(0) = [[timeUs=0, position=8880]] + getPosition(1) = [[timeUs=0, position=8880]] + getPosition(1370500) = [[timeUs=1365333, position=90825], [timeUs=1450666, position=96601]] + getPosition(2741000) = [[timeUs=2645333, position=169482]] +numberOfTracks = 1 +track 0: + total output bytes = 362432 + sample count = 23 + format 0: + averageBitrate = 1536000 + peakBitrate = 1536000 + sampleMimeType = audio/raw + maxInputSize = 16384 + channelCount = 2 + sampleRate = 48000 + pcmEncoding = 2 + metadata = entries=[VC: TITLE=test title, VC: ARTIST=test artist] + sample 0: + time = 853333 + flags = 1 + data = length 16384, hash 31583F2C + sample 1: + time = 938666 + flags = 1 + data = length 16384, hash E433A93D + sample 2: + time = 1024000 + flags = 1 + data = length 16384, hash 5E1C7051 + sample 3: + time = 1109333 + flags = 1 + data = length 16384, hash 43E6E358 + sample 4: + time = 1194666 + flags = 1 + data = length 16384, hash 5DC1B256 + sample 5: + time = 1280000 + flags = 1 + data = length 16384, hash 3D9D95CF + sample 6: + time = 1365333 + flags = 1 + data = length 16384, hash 2A5BD2C0 + sample 7: + time = 1450666 + flags = 1 + data = length 16384, hash 93E25061 + sample 8: + time = 1536000 + flags = 1 + data = length 16384, hash B81793D8 + sample 9: + time = 1621333 + flags = 1 + data = length 16384, hash 1A3BD49F + sample 10: + time = 1706666 + flags = 1 + data = length 16384, hash FB672FF1 + sample 11: + time = 1792000 + flags = 1 + data = length 16384, hash 48AB8B45 + sample 12: + time = 1877333 + flags = 1 + data = length 16384, hash 13C9640A + sample 13: + time = 1962666 + flags = 1 + data = length 16384, hash 499E4A0B + sample 14: + time = 2048000 + flags = 1 + data = length 16384, hash F9A783E6 + sample 15: + time = 2133333 + flags = 1 + data = length 16384, hash D2B77598 + sample 16: + time = 2218666 + flags = 1 + data = length 16384, hash CE5B826C + sample 17: + time = 2304000 + flags = 1 + data = length 16384, hash E99EE956 + sample 18: + time = 2389333 + flags = 1 + data = length 16384, hash F2DB1486 + sample 19: + time = 2474666 + flags = 1 + data = length 16384, hash 1636EAB + sample 20: + time = 2560000 + flags = 1 + data = length 16384, hash 23457C08 + sample 21: + time = 2645333 + flags = 1 + data = length 16384, hash 30EB8381 + sample 22: + time = 2730666 + flags = 1 + data = length 1984, hash 59CFDE1B +tracksEnded = true diff --git a/testdata/src/test/assets/flac/bear_with_vorbis_comments_raw.2.dump b/testdata/src/test/assets/flac/bear_with_vorbis_comments_raw.2.dump new file mode 100644 index 0000000000..642a7a973a --- /dev/null +++ b/testdata/src/test/assets/flac/bear_with_vorbis_comments_raw.2.dump @@ -0,0 +1,69 @@ +seekMap: + isSeekable = true + duration = 2741000 + getPosition(0) = [[timeUs=0, position=8880]] + getPosition(1) = [[timeUs=0, position=8880]] + getPosition(1370500) = [[timeUs=1365333, position=90825], [timeUs=1450666, position=96601]] + getPosition(2741000) = [[timeUs=2645333, position=169482]] +numberOfTracks = 1 +track 0: + total output bytes = 182208 + sample count = 12 + format 0: + averageBitrate = 1536000 + peakBitrate = 1536000 + sampleMimeType = audio/raw + maxInputSize = 16384 + channelCount = 2 + sampleRate = 48000 + pcmEncoding = 2 + metadata = entries=[VC: TITLE=test title, VC: ARTIST=test artist] + sample 0: + time = 1792000 + flags = 1 + data = length 16384, hash 48AB8B45 + sample 1: + time = 1877333 + flags = 1 + data = length 16384, hash 13C9640A + sample 2: + time = 1962666 + flags = 1 + data = length 16384, hash 499E4A0B + sample 3: + time = 2048000 + flags = 1 + data = length 16384, hash F9A783E6 + sample 4: + time = 2133333 + flags = 1 + data = length 16384, hash D2B77598 + sample 5: + time = 2218666 + flags = 1 + data = length 16384, hash CE5B826C + sample 6: + time = 2304000 + flags = 1 + data = length 16384, hash E99EE956 + sample 7: + time = 2389333 + flags = 1 + data = length 16384, hash F2DB1486 + sample 8: + time = 2474666 + flags = 1 + data = length 16384, hash 1636EAB + sample 9: + time = 2560000 + flags = 1 + data = length 16384, hash 23457C08 + sample 10: + time = 2645333 + flags = 1 + data = length 16384, hash 30EB8381 + sample 11: + time = 2730666 + flags = 1 + data = length 1984, hash 59CFDE1B +tracksEnded = true diff --git a/testdata/src/test/assets/flac/bear_with_vorbis_comments_raw.3.dump b/testdata/src/test/assets/flac/bear_with_vorbis_comments_raw.3.dump new file mode 100644 index 0000000000..262c5dde56 --- /dev/null +++ b/testdata/src/test/assets/flac/bear_with_vorbis_comments_raw.3.dump @@ -0,0 +1,29 @@ +seekMap: + isSeekable = true + duration = 2741000 + getPosition(0) = [[timeUs=0, position=8880]] + getPosition(1) = [[timeUs=0, position=8880]] + getPosition(1370500) = [[timeUs=1365333, position=90825], [timeUs=1450666, position=96601]] + getPosition(2741000) = [[timeUs=2645333, position=169482]] +numberOfTracks = 1 +track 0: + total output bytes = 18368 + sample count = 2 + format 0: + averageBitrate = 1536000 + peakBitrate = 1536000 + sampleMimeType = audio/raw + maxInputSize = 16384 + channelCount = 2 + sampleRate = 48000 + pcmEncoding = 2 + metadata = entries=[VC: TITLE=test title, VC: ARTIST=test artist] + sample 0: + time = 2645333 + flags = 1 + data = length 16384, hash 30EB8381 + sample 1: + time = 2730666 + flags = 1 + data = length 1984, hash 59CFDE1B +tracksEnded = true diff --git a/testdata/src/test/assets/flac/bear_with_vorbis_comments_raw.unknown_length.dump b/testdata/src/test/assets/flac/bear_with_vorbis_comments_raw.unknown_length.dump new file mode 100644 index 0000000000..d6faa106c6 --- /dev/null +++ b/testdata/src/test/assets/flac/bear_with_vorbis_comments_raw.unknown_length.dump @@ -0,0 +1,153 @@ +seekMap: + isSeekable = true + duration = 2741000 + getPosition(0) = [[timeUs=0, position=8880]] + getPosition(1) = [[timeUs=0, position=8880]] + getPosition(1370500) = [[timeUs=1365333, position=90825], [timeUs=1450666, position=96601]] + getPosition(2741000) = [[timeUs=2645333, position=169482]] +numberOfTracks = 1 +track 0: + total output bytes = 526272 + sample count = 33 + format 0: + averageBitrate = 1536000 + peakBitrate = 1536000 + sampleMimeType = audio/raw + maxInputSize = 16384 + channelCount = 2 + sampleRate = 48000 + pcmEncoding = 2 + metadata = entries=[VC: TITLE=test title, VC: ARTIST=test artist] + sample 0: + time = 0 + flags = 1 + data = length 16384, hash 61D2C5C2 + sample 1: + time = 85333 + flags = 1 + data = length 16384, hash E6D7F214 + sample 2: + time = 170666 + flags = 1 + data = length 16384, hash 59BF0D5D + sample 3: + time = 256000 + flags = 1 + data = length 16384, hash 3625F468 + sample 4: + time = 341333 + flags = 1 + data = length 16384, hash F66A323 + sample 5: + time = 426666 + flags = 1 + data = length 16384, hash CDBAE629 + sample 6: + time = 512000 + flags = 1 + data = length 16384, hash 536F3A91 + sample 7: + time = 597333 + flags = 1 + data = length 16384, hash D4F35C9C + sample 8: + time = 682666 + flags = 1 + data = length 16384, hash EE04CEBF + sample 9: + time = 768000 + flags = 1 + data = length 16384, hash 647E2A67 + sample 10: + time = 853333 + flags = 1 + data = length 16384, hash 31583F2C + sample 11: + time = 938666 + flags = 1 + data = length 16384, hash E433A93D + sample 12: + time = 1024000 + flags = 1 + data = length 16384, hash 5E1C7051 + sample 13: + time = 1109333 + flags = 1 + data = length 16384, hash 43E6E358 + sample 14: + time = 1194666 + flags = 1 + data = length 16384, hash 5DC1B256 + sample 15: + time = 1280000 + flags = 1 + data = length 16384, hash 3D9D95CF + sample 16: + time = 1365333 + flags = 1 + data = length 16384, hash 2A5BD2C0 + sample 17: + time = 1450666 + flags = 1 + data = length 16384, hash 93E25061 + sample 18: + time = 1536000 + flags = 1 + data = length 16384, hash B81793D8 + sample 19: + time = 1621333 + flags = 1 + data = length 16384, hash 1A3BD49F + sample 20: + time = 1706666 + flags = 1 + data = length 16384, hash FB672FF1 + sample 21: + time = 1792000 + flags = 1 + data = length 16384, hash 48AB8B45 + sample 22: + time = 1877333 + flags = 1 + data = length 16384, hash 13C9640A + sample 23: + time = 1962666 + flags = 1 + data = length 16384, hash 499E4A0B + sample 24: + time = 2048000 + flags = 1 + data = length 16384, hash F9A783E6 + sample 25: + time = 2133333 + flags = 1 + data = length 16384, hash D2B77598 + sample 26: + time = 2218666 + flags = 1 + data = length 16384, hash CE5B826C + sample 27: + time = 2304000 + flags = 1 + data = length 16384, hash E99EE956 + sample 28: + time = 2389333 + flags = 1 + data = length 16384, hash F2DB1486 + sample 29: + time = 2474666 + flags = 1 + data = length 16384, hash 1636EAB + sample 30: + time = 2560000 + flags = 1 + data = length 16384, hash 23457C08 + sample 31: + time = 2645333 + flags = 1 + data = length 16384, hash 30EB8381 + sample 32: + time = 2730666 + flags = 1 + data = length 1984, hash 59CFDE1B +tracksEnded = true diff --git a/library/core/src/test/assets/flv/sample.flv b/testdata/src/test/assets/flv/sample.flv similarity index 100% rename from library/core/src/test/assets/flv/sample.flv rename to testdata/src/test/assets/flv/sample.flv diff --git a/library/core/src/test/assets/flv/sample.flv.0.dump b/testdata/src/test/assets/flv/sample.flv.0.dump similarity index 89% rename from library/core/src/test/assets/flv/sample.flv.0.dump rename to testdata/src/test/assets/flv/sample.flv.0.dump index 098311a310..8bf765bae3 100644 --- a/library/core/src/test/assets/flv/sample.flv.0.dump +++ b/testdata/src/test/assets/flv/sample.flv.0.dump @@ -4,30 +4,15 @@ seekMap: getPosition(0) = [[timeUs=0, position=0]] numberOfTracks = 2 track 8: - format: - bitrate = -1 - id = null - 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 = 9529 sample count = 45 + format 0: + sampleMimeType = audio/mp4a-latm + codecs = mp4a.40.2 + channelCount = 1 + sampleRate = 44100 + initializationData: + data = length 2, hash 5F7 sample 0: time = 112000 flags = 1 @@ -209,31 +194,15 @@ track 8: flags = 1 data = length 6, hash 31B22286 track 9: - format: - bitrate = -1 - id = null - containerMimeType = null + total output bytes = 89502 + sample count = 30 + format 0: sampleMimeType = video/avc - maxInputSize = -1 width = 1080 height = 720 - 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 30, hash F6F3D010 data = length 10, hash 7A0D0F2B - total output bytes = 89502 - sample count = 30 sample 0: time = 67000 flags = 1 diff --git a/testdata/src/test/assets/flv/sample.flv.unknown_length.dump b/testdata/src/test/assets/flv/sample.flv.unknown_length.dump new file mode 100644 index 0000000000..8bf765bae3 --- /dev/null +++ b/testdata/src/test/assets/flv/sample.flv.unknown_length.dump @@ -0,0 +1,326 @@ +seekMap: + isSeekable = false + duration = 1136000 + getPosition(0) = [[timeUs=0, position=0]] +numberOfTracks = 2 +track 8: + total output bytes = 9529 + sample count = 45 + format 0: + sampleMimeType = audio/mp4a-latm + codecs = mp4a.40.2 + channelCount = 1 + sampleRate = 44100 + initializationData: + data = length 2, hash 5F7 + sample 0: + time = 112000 + flags = 1 + data = length 23, hash 47DE9131 + sample 1: + time = 135000 + flags = 1 + data = length 6, hash 31EC5206 + sample 2: + time = 158000 + flags = 1 + data = length 148, hash 894A176B + sample 3: + time = 181000 + flags = 1 + data = length 189, hash CEF235A1 + sample 4: + time = 205000 + flags = 1 + data = length 205, hash BBF5F7B0 + sample 5: + time = 228000 + flags = 1 + data = length 210, hash F278B193 + sample 6: + time = 251000 + flags = 1 + data = length 210, hash 82DA1589 + sample 7: + time = 274000 + flags = 1 + data = length 207, hash 5BE231DF + sample 8: + time = 298000 + flags = 1 + data = length 225, hash 18819EE1 + sample 9: + time = 321000 + flags = 1 + data = length 215, hash CA7FA67B + sample 10: + time = 344000 + flags = 1 + data = length 211, hash 581A1C18 + sample 11: + time = 367000 + flags = 1 + data = length 216, hash ADB88187 + sample 12: + time = 390000 + flags = 1 + data = length 229, hash 2E8BA4DC + sample 13: + time = 414000 + flags = 1 + data = length 232, hash 22F0C510 + sample 14: + time = 437000 + flags = 1 + data = length 235, hash 867AD0DC + sample 15: + time = 460000 + flags = 1 + data = length 231, hash 84E823A8 + sample 16: + time = 483000 + flags = 1 + data = length 226, hash 1BEF3A95 + sample 17: + time = 507000 + flags = 1 + data = length 216, hash EAA345AE + sample 18: + time = 530000 + flags = 1 + data = length 229, hash 6957411F + sample 19: + time = 553000 + flags = 1 + data = length 219, hash 41275022 + sample 20: + time = 576000 + flags = 1 + data = length 241, hash 6495DF96 + sample 21: + time = 599000 + flags = 1 + data = length 228, hash 63D95906 + sample 22: + time = 623000 + flags = 1 + data = length 238, hash 34F676F9 + sample 23: + time = 646000 + flags = 1 + data = length 234, hash E5CBC045 + sample 24: + time = 669000 + flags = 1 + data = length 231, hash 5FC43661 + sample 25: + time = 692000 + flags = 1 + data = length 217, hash 682708ED + sample 26: + time = 716000 + flags = 1 + data = length 239, hash D43780FC + sample 27: + time = 739000 + flags = 1 + data = length 243, hash C5E17980 + sample 28: + time = 762000 + flags = 1 + data = length 231, hash AC5837BA + sample 29: + time = 785000 + flags = 1 + data = length 230, hash 169EE895 + sample 30: + time = 808000 + flags = 1 + data = length 238, hash C48FF3F1 + sample 31: + time = 832000 + flags = 1 + data = length 225, hash 531E4599 + sample 32: + time = 855000 + flags = 1 + data = length 232, hash CB3C6B8D + sample 33: + time = 878000 + flags = 1 + data = length 243, hash F8C94C7 + sample 34: + time = 901000 + flags = 1 + data = length 232, hash A646A7D0 + sample 35: + time = 925000 + flags = 1 + data = length 237, hash E8B787A5 + sample 36: + time = 948000 + flags = 1 + data = length 228, hash 3FA7A29F + sample 37: + time = 971000 + flags = 1 + data = length 235, hash B9B33B0A + sample 38: + time = 994000 + flags = 1 + data = length 264, hash 71A4869E + sample 39: + time = 1017000 + flags = 1 + data = length 257, hash D049B54C + sample 40: + time = 1041000 + flags = 1 + data = length 227, hash 66757231 + sample 41: + time = 1064000 + flags = 1 + data = length 227, hash BD374F1B + sample 42: + time = 1087000 + flags = 1 + data = length 235, hash 999477F6 + sample 43: + time = 1110000 + flags = 1 + data = length 229, hash FFF98DF0 + sample 44: + time = 1134000 + flags = 1 + data = length 6, hash 31B22286 +track 9: + total output bytes = 89502 + sample count = 30 + format 0: + sampleMimeType = video/avc + width = 1080 + height = 720 + initializationData: + data = length 30, hash F6F3D010 + data = length 10, hash 7A0D0F2B + sample 0: + time = 67000 + flags = 1 + data = length 36477, hash F0F36CFE + sample 1: + time = 134000 + flags = 0 + data = length 5341, hash 40B85E2 + sample 2: + time = 100000 + flags = 0 + data = length 596, hash 357B4D92 + sample 3: + time = 267000 + flags = 0 + data = length 7704, hash A39EDA06 + sample 4: + time = 200000 + flags = 0 + data = length 989, hash 2813C72D + sample 5: + time = 167000 + flags = 0 + data = length 721, hash C50D1C73 + sample 6: + time = 234000 + flags = 0 + data = length 519, hash 65FE1911 + sample 7: + time = 400000 + flags = 0 + data = length 6160, hash E1CAC0EC + sample 8: + time = 334000 + flags = 0 + data = length 953, hash 7160C661 + sample 9: + time = 300000 + flags = 0 + data = length 620, hash 7A7AE07C + sample 10: + time = 367000 + flags = 0 + data = length 405, hash 5CC7F4E7 + sample 11: + time = 500000 + flags = 0 + data = length 4852, hash 9DB6979D + sample 12: + time = 467000 + flags = 0 + data = length 547, hash E31A6979 + sample 13: + time = 434000 + flags = 0 + data = length 570, hash FEC40D00 + sample 14: + time = 634000 + flags = 0 + data = length 5525, hash 7C478F7E + sample 15: + time = 567000 + flags = 0 + data = length 1082, hash DA07059A + sample 16: + time = 534000 + flags = 0 + data = length 807, hash 93478E6B + sample 17: + time = 600000 + flags = 0 + data = length 744, hash 9A8E6026 + sample 18: + time = 767000 + flags = 0 + data = length 4732, hash C73B23C0 + sample 19: + time = 700000 + flags = 0 + data = length 1004, hash 8A19A228 + sample 20: + time = 667000 + flags = 0 + data = length 794, hash 8126022C + sample 21: + time = 734000 + flags = 0 + data = length 645, hash F08300E5 + sample 22: + time = 900000 + flags = 0 + data = length 2684, hash 727FE378 + sample 23: + time = 834000 + flags = 0 + data = length 787, hash 419A7821 + sample 24: + time = 800000 + flags = 0 + data = length 649, hash 5C159346 + sample 25: + time = 867000 + flags = 0 + data = length 509, hash F912D655 + sample 26: + time = 1034000 + flags = 0 + data = length 1226, hash 29815C21 + sample 27: + time = 967000 + flags = 0 + data = length 898, hash D997AD0A + sample 28: + time = 934000 + flags = 0 + data = length 476, hash A0423645 + sample 29: + time = 1000000 + flags = 0 + data = length 486, hash DDF32CBB +tracksEnded = true diff --git a/testdata/src/test/assets/id3/apic.id3 b/testdata/src/test/assets/id3/apic.id3 new file mode 100644 index 0000000000..06c130bd9a Binary files /dev/null and b/testdata/src/test/assets/id3/apic.id3 differ diff --git a/testdata/src/test/assets/id3/comm_apic.id3 b/testdata/src/test/assets/id3/comm_apic.id3 new file mode 100644 index 0000000000..683c7b191d Binary files /dev/null and b/testdata/src/test/assets/id3/comm_apic.id3 differ diff --git a/extensions/flac/src/androidTest/assets/bear-flac.mka b/testdata/src/test/assets/mka/bear-flac-16bit.mka similarity index 100% rename from extensions/flac/src/androidTest/assets/bear-flac.mka rename to testdata/src/test/assets/mka/bear-flac-16bit.mka diff --git a/testdata/src/test/assets/mka/bear-flac-16bit.mka.audiosink.dump b/testdata/src/test/assets/mka/bear-flac-16bit.mka.audiosink.dump new file mode 100644 index 0000000000..9615ff0a6e --- /dev/null +++ b/testdata/src/test/assets/mka/bear-flac-16bit.mka.audiosink.dump @@ -0,0 +1,91 @@ +config: + encoding = 2 (16 bit) + channel count = 2 + sample rate = 48000 +buffer: + time = 1000 + data = 1217833679 +buffer: + time = 97000 + data = 558614672 +buffer: + time = 193000 + data = -709714787 +buffer: + time = 289000 + data = 1367870571 +buffer: + time = 385000 + data = -141229457 +buffer: + time = 481000 + data = 1287758361 +buffer: + time = 577000 + data = 1125289147 +buffer: + time = 673000 + data = -1677383475 +buffer: + time = 769000 + data = 2130742861 +buffer: + time = 865000 + data = -1292320253 +buffer: + time = 961000 + data = -456587163 +buffer: + time = 1057000 + data = 748981534 +buffer: + time = 1153000 + data = 1550456016 +buffer: + time = 1249000 + data = 1657906039 +buffer: + time = 1345000 + data = -762677083 +buffer: + time = 1441000 + data = -1343810763 +buffer: + time = 1537000 + data = 1137318783 +buffer: + time = 1633000 + data = -1891318229 +buffer: + time = 1729000 + data = -472068495 +buffer: + time = 1825000 + data = 832315001 +buffer: + time = 1921000 + data = 2054935175 +buffer: + time = 2017000 + data = 57921641 +buffer: + time = 2113000 + data = 2132759067 +buffer: + time = 2209000 + data = -1742540521 +buffer: + time = 2305000 + data = 1657024301 +buffer: + time = 2401000 + data = -585080145 +buffer: + time = 2497000 + data = 427271397 +buffer: + time = 2593000 + data = -364201340 +buffer: + time = 2689000 + data = -627965287 diff --git a/testdata/src/test/assets/mka/bear-flac-24bit.mka b/testdata/src/test/assets/mka/bear-flac-24bit.mka new file mode 100644 index 0000000000..e6d124e0ce Binary files /dev/null and b/testdata/src/test/assets/mka/bear-flac-24bit.mka differ diff --git a/testdata/src/test/assets/mka/bear-flac-24bit.mka.audiosink.dump b/testdata/src/test/assets/mka/bear-flac-24bit.mka.audiosink.dump new file mode 100644 index 0000000000..efc3e0e9d0 --- /dev/null +++ b/testdata/src/test/assets/mka/bear-flac-24bit.mka.audiosink.dump @@ -0,0 +1,91 @@ +config: + encoding = 536870912 (24 bit) + channel count = 2 + sample rate = 48000 +buffer: + time = 0 + data = 225023649 +buffer: + time = 96000 + data = 455106306 +buffer: + time = 192000 + data = 2025727297 +buffer: + time = 288000 + data = 758514657 +buffer: + time = 384000 + data = 1044986473 +buffer: + time = 480000 + data = -2030029695 +buffer: + time = 576000 + data = 1907053281 +buffer: + time = 672000 + data = -1974954431 +buffer: + time = 768000 + data = -206248383 +buffer: + time = 864000 + data = 1484984417 +buffer: + time = 960000 + data = -1306117439 +buffer: + time = 1056000 + data = 692829792 +buffer: + time = 1152000 + data = 1070563058 +buffer: + time = 1248000 + data = -1444096479 +buffer: + time = 1344000 + data = 1753016419 +buffer: + time = 1440000 + data = 1947797953 +buffer: + time = 1536000 + data = 266121411 +buffer: + time = 1632000 + data = 1275494369 +buffer: + time = 1728000 + data = 372077825 +buffer: + time = 1824000 + data = -993079679 +buffer: + time = 1920000 + data = 177307937 +buffer: + time = 2016000 + data = 2037083009 +buffer: + time = 2112000 + data = -435776287 +buffer: + time = 2208000 + data = 1867447329 +buffer: + time = 2304000 + data = 1884495937 +buffer: + time = 2400000 + data = -804673375 +buffer: + time = 2496000 + data = -588531007 +buffer: + time = 2592000 + data = -1064642970 +buffer: + time = 2688000 + data = -1771406207 diff --git a/testdata/src/test/assets/mka/bear-opus-negative-gain.mka b/testdata/src/test/assets/mka/bear-opus-negative-gain.mka new file mode 100644 index 0000000000..3ee21adfd3 Binary files /dev/null and b/testdata/src/test/assets/mka/bear-opus-negative-gain.mka differ diff --git a/extensions/opus/src/androidTest/assets/bear-opus.webm b/testdata/src/test/assets/mka/bear-opus.mka similarity index 100% rename from extensions/opus/src/androidTest/assets/bear-opus.webm rename to testdata/src/test/assets/mka/bear-opus.mka diff --git a/testdata/src/test/assets/mkv/full_blocks.mkv b/testdata/src/test/assets/mkv/full_blocks.mkv new file mode 100644 index 0000000000..e8855b621d Binary files /dev/null and b/testdata/src/test/assets/mkv/full_blocks.mkv differ diff --git a/testdata/src/test/assets/mkv/full_blocks.mkv.0.dump b/testdata/src/test/assets/mkv/full_blocks.mkv.0.dump new file mode 100644 index 0000000000..3afc87eed2 --- /dev/null +++ b/testdata/src/test/assets/mkv/full_blocks.mkv.0.dump @@ -0,0 +1,29 @@ +seekMap: + isSeekable = true + duration = 8901000 + getPosition(0) = [[timeUs=0, position=5401]] + getPosition(1) = [[timeUs=0, position=5401], [timeUs=2345000, position=5401]] + getPosition(4450500) = [[timeUs=2345000, position=5401], [timeUs=4567000, position=5401]] + getPosition(8901000) = [[timeUs=4567000, position=5401]] +numberOfTracks = 1 +track 1: + total output bytes = 213 + sample count = 3 + format 0: + id = 1 + sampleMimeType = application/x-subrip + selectionFlags = 1 + language = und + sample 0: + time = 0 + flags = 1 + data = length 59, hash 1AD38625 + sample 1: + time = 2345000 + flags = 1 + data = length 95, hash F331C282 + sample 2: + time = 4567000 + flags = 1 + data = length 59, hash F8CD7C60 +tracksEnded = true diff --git a/testdata/src/test/assets/mkv/full_blocks.mkv.1.dump b/testdata/src/test/assets/mkv/full_blocks.mkv.1.dump new file mode 100644 index 0000000000..3afc87eed2 --- /dev/null +++ b/testdata/src/test/assets/mkv/full_blocks.mkv.1.dump @@ -0,0 +1,29 @@ +seekMap: + isSeekable = true + duration = 8901000 + getPosition(0) = [[timeUs=0, position=5401]] + getPosition(1) = [[timeUs=0, position=5401], [timeUs=2345000, position=5401]] + getPosition(4450500) = [[timeUs=2345000, position=5401], [timeUs=4567000, position=5401]] + getPosition(8901000) = [[timeUs=4567000, position=5401]] +numberOfTracks = 1 +track 1: + total output bytes = 213 + sample count = 3 + format 0: + id = 1 + sampleMimeType = application/x-subrip + selectionFlags = 1 + language = und + sample 0: + time = 0 + flags = 1 + data = length 59, hash 1AD38625 + sample 1: + time = 2345000 + flags = 1 + data = length 95, hash F331C282 + sample 2: + time = 4567000 + flags = 1 + data = length 59, hash F8CD7C60 +tracksEnded = true diff --git a/testdata/src/test/assets/mkv/full_blocks.mkv.2.dump b/testdata/src/test/assets/mkv/full_blocks.mkv.2.dump new file mode 100644 index 0000000000..3afc87eed2 --- /dev/null +++ b/testdata/src/test/assets/mkv/full_blocks.mkv.2.dump @@ -0,0 +1,29 @@ +seekMap: + isSeekable = true + duration = 8901000 + getPosition(0) = [[timeUs=0, position=5401]] + getPosition(1) = [[timeUs=0, position=5401], [timeUs=2345000, position=5401]] + getPosition(4450500) = [[timeUs=2345000, position=5401], [timeUs=4567000, position=5401]] + getPosition(8901000) = [[timeUs=4567000, position=5401]] +numberOfTracks = 1 +track 1: + total output bytes = 213 + sample count = 3 + format 0: + id = 1 + sampleMimeType = application/x-subrip + selectionFlags = 1 + language = und + sample 0: + time = 0 + flags = 1 + data = length 59, hash 1AD38625 + sample 1: + time = 2345000 + flags = 1 + data = length 95, hash F331C282 + sample 2: + time = 4567000 + flags = 1 + data = length 59, hash F8CD7C60 +tracksEnded = true diff --git a/testdata/src/test/assets/mkv/full_blocks.mkv.3.dump b/testdata/src/test/assets/mkv/full_blocks.mkv.3.dump new file mode 100644 index 0000000000..3afc87eed2 --- /dev/null +++ b/testdata/src/test/assets/mkv/full_blocks.mkv.3.dump @@ -0,0 +1,29 @@ +seekMap: + isSeekable = true + duration = 8901000 + getPosition(0) = [[timeUs=0, position=5401]] + getPosition(1) = [[timeUs=0, position=5401], [timeUs=2345000, position=5401]] + getPosition(4450500) = [[timeUs=2345000, position=5401], [timeUs=4567000, position=5401]] + getPosition(8901000) = [[timeUs=4567000, position=5401]] +numberOfTracks = 1 +track 1: + total output bytes = 213 + sample count = 3 + format 0: + id = 1 + sampleMimeType = application/x-subrip + selectionFlags = 1 + language = und + sample 0: + time = 0 + flags = 1 + data = length 59, hash 1AD38625 + sample 1: + time = 2345000 + flags = 1 + data = length 95, hash F331C282 + sample 2: + time = 4567000 + flags = 1 + data = length 59, hash F8CD7C60 +tracksEnded = true diff --git a/testdata/src/test/assets/mkv/full_blocks.mkv.unknown_length.dump b/testdata/src/test/assets/mkv/full_blocks.mkv.unknown_length.dump new file mode 100644 index 0000000000..3afc87eed2 --- /dev/null +++ b/testdata/src/test/assets/mkv/full_blocks.mkv.unknown_length.dump @@ -0,0 +1,29 @@ +seekMap: + isSeekable = true + duration = 8901000 + getPosition(0) = [[timeUs=0, position=5401]] + getPosition(1) = [[timeUs=0, position=5401], [timeUs=2345000, position=5401]] + getPosition(4450500) = [[timeUs=2345000, position=5401], [timeUs=4567000, position=5401]] + getPosition(8901000) = [[timeUs=4567000, position=5401]] +numberOfTracks = 1 +track 1: + total output bytes = 213 + sample count = 3 + format 0: + id = 1 + sampleMimeType = application/x-subrip + selectionFlags = 1 + language = und + sample 0: + time = 0 + flags = 1 + data = length 59, hash 1AD38625 + sample 1: + time = 2345000 + flags = 1 + data = length 95, hash F331C282 + sample 2: + time = 4567000 + flags = 1 + data = length 59, hash F8CD7C60 +tracksEnded = true diff --git a/library/core/src/test/assets/mkv/sample.mkv b/testdata/src/test/assets/mkv/sample.mkv similarity index 100% rename from library/core/src/test/assets/mkv/sample.mkv rename to testdata/src/test/assets/mkv/sample.mkv diff --git a/library/core/src/test/assets/mkv/sample.mkv.0.dump b/testdata/src/test/assets/mkv/sample.mkv.0.dump similarity index 88% rename from library/core/src/test/assets/mkv/sample.mkv.0.dump rename to testdata/src/test/assets/mkv/sample.mkv.0.dump index 847799396d..bb8c0632f3 100644 --- a/library/core/src/test/assets/mkv/sample.mkv.0.dump +++ b/testdata/src/test/assets/mkv/sample.mkv.0.dump @@ -1,34 +1,24 @@ seekMap: isSeekable = true - duration = 1072000 + duration = 1104000 getPosition(0) = [[timeUs=67000, position=5576]] + getPosition(1) = [[timeUs=67000, position=5576]] + getPosition(552000) = [[timeUs=547000, position=77334], [timeUs=567000, position=84155]] + getPosition(1104000) = [[timeUs=1035000, position=106570]] numberOfTracks = 2 track 1: - format: - bitrate = -1 + total output bytes = 89502 + sample count = 30 + format 0: id = 1 - containerMimeType = null sampleMimeType = video/avc - maxInputSize = -1 width = 1080 height = 720 - 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 = - + selectionFlags = 1 + language = und initializationData: data = length 30, hash F6F3D010 data = length 10, hash 7A0D0F2B - total output bytes = 89502 - sample count = 30 sample 0: time = 67000 flags = 1 @@ -150,29 +140,15 @@ track 1: flags = 0 data = length 486, hash DDF32CBB track 2: - format: - bitrate = -1 - id = 2 - containerMimeType = null - sampleMimeType = audio/ac3 - 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 = 1 - language = und - drmInitData = - - initializationData: total output bytes = 12120 sample count = 29 + format 0: + id = 2 + sampleMimeType = audio/ac3 + channelCount = 1 + sampleRate = 44100 + selectionFlags = 1 + language = und sample 0: time = 129000 flags = 1 diff --git a/library/core/src/test/assets/mkv/sample.mkv.1.dump b/testdata/src/test/assets/mkv/sample.mkv.1.dump similarity index 80% rename from library/core/src/test/assets/mkv/sample.mkv.1.dump rename to testdata/src/test/assets/mkv/sample.mkv.1.dump index 5caa638437..348f66e363 100644 --- a/library/core/src/test/assets/mkv/sample.mkv.1.dump +++ b/testdata/src/test/assets/mkv/sample.mkv.1.dump @@ -1,146 +1,114 @@ seekMap: isSeekable = true - duration = 1072000 + duration = 1104000 getPosition(0) = [[timeUs=67000, position=5576]] + getPosition(1) = [[timeUs=67000, position=5576]] + getPosition(552000) = [[timeUs=547000, position=77334], [timeUs=567000, position=84155]] + getPosition(1104000) = [[timeUs=1035000, position=106570]] numberOfTracks = 2 track 1: - format: - bitrate = -1 + total output bytes = 29422 + sample count = 20 + format 0: id = 1 - containerMimeType = null sampleMimeType = video/avc - maxInputSize = -1 width = 1080 height = 720 - 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 = - + selectionFlags = 1 + language = und initializationData: data = length 30, hash F6F3D010 data = length 10, hash 7A0D0F2B - total output bytes = 30995 - sample count = 22 sample 0: - time = 334000 - flags = 0 - data = length 953, hash 7160C661 - sample 1: - time = 300000 - flags = 0 - data = length 620, hash 7A7AE07C - sample 2: time = 367000 flags = 0 data = length 405, hash 5CC7F4E7 - sample 3: + sample 1: time = 500000 flags = 0 data = length 4852, hash 9DB6979D - sample 4: + sample 2: time = 467000 flags = 0 data = length 547, hash E31A6979 - sample 5: + sample 3: time = 434000 flags = 0 data = length 570, hash FEC40D00 - sample 6: + sample 4: time = 634000 flags = 0 data = length 5525, hash 7C478F7E - sample 7: + sample 5: time = 567000 flags = 0 data = length 1082, hash DA07059A - sample 8: + sample 6: time = 534000 flags = 0 data = length 807, hash 93478E6B - sample 9: + sample 7: time = 600000 flags = 0 data = length 744, hash 9A8E6026 - sample 10: + sample 8: time = 767000 flags = 0 data = length 4732, hash C73B23C0 - sample 11: + sample 9: time = 700000 flags = 0 data = length 1004, hash 8A19A228 - sample 12: + sample 10: time = 667000 flags = 0 data = length 794, hash 8126022C - sample 13: + sample 11: time = 734000 flags = 0 data = length 645, hash F08300E5 - sample 14: + sample 12: time = 900000 flags = 0 data = length 2684, hash 727FE378 - sample 15: + sample 13: time = 834000 flags = 0 data = length 787, hash 419A7821 - sample 16: + sample 14: time = 800000 flags = 0 data = length 649, hash 5C159346 - sample 17: + sample 15: time = 867000 flags = 0 data = length 509, hash F912D655 - sample 18: + sample 16: time = 1034000 flags = 0 data = length 1226, hash 29815C21 - sample 19: + sample 17: time = 967000 flags = 0 data = length 898, hash D997AD0A - sample 20: + sample 18: time = 934000 flags = 0 data = length 476, hash A0423645 - sample 21: + sample 19: time = 1000000 flags = 0 data = length 486, hash DDF32CBB track 2: - format: - bitrate = -1 - id = 2 - containerMimeType = null - sampleMimeType = audio/ac3 - 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 = 1 - language = und - drmInitData = - - initializationData: total output bytes = 8778 sample count = 21 + format 0: + id = 2 + sampleMimeType = audio/ac3 + channelCount = 1 + sampleRate = 44100 + selectionFlags = 1 + language = und sample 0: time = 408000 flags = 1 diff --git a/library/core/src/test/assets/mkv/sample.mkv.2.dump b/testdata/src/test/assets/mkv/sample.mkv.2.dump similarity index 68% rename from library/core/src/test/assets/mkv/sample.mkv.2.dump rename to testdata/src/test/assets/mkv/sample.mkv.2.dump index de4e2a58bf..e965e5039c 100644 --- a/library/core/src/test/assets/mkv/sample.mkv.2.dump +++ b/testdata/src/test/assets/mkv/sample.mkv.2.dump @@ -1,102 +1,70 @@ seekMap: isSeekable = true - duration = 1072000 + duration = 1104000 getPosition(0) = [[timeUs=67000, position=5576]] + getPosition(1) = [[timeUs=67000, position=5576]] + getPosition(552000) = [[timeUs=547000, position=77334], [timeUs=567000, position=84155]] + getPosition(1104000) = [[timeUs=1035000, position=106570]] numberOfTracks = 2 track 1: - format: - bitrate = -1 + total output bytes = 8360 + sample count = 9 + format 0: id = 1 - containerMimeType = null sampleMimeType = video/avc - maxInputSize = -1 width = 1080 height = 720 - 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 = - + selectionFlags = 1 + language = und initializationData: data = length 30, hash F6F3D010 data = length 10, hash 7A0D0F2B - total output bytes = 10158 - sample count = 11 sample 0: - time = 700000 - flags = 0 - data = length 1004, hash 8A19A228 - sample 1: - time = 667000 - flags = 0 - data = length 794, hash 8126022C - sample 2: time = 734000 flags = 0 data = length 645, hash F08300E5 - sample 3: + sample 1: time = 900000 flags = 0 data = length 2684, hash 727FE378 - sample 4: + sample 2: time = 834000 flags = 0 data = length 787, hash 419A7821 - sample 5: + sample 3: time = 800000 flags = 0 data = length 649, hash 5C159346 - sample 6: + sample 4: time = 867000 flags = 0 data = length 509, hash F912D655 - sample 7: + sample 5: time = 1034000 flags = 0 data = length 1226, hash 29815C21 - sample 8: + sample 6: time = 967000 flags = 0 data = length 898, hash D997AD0A - sample 9: + sample 7: time = 934000 flags = 0 data = length 476, hash A0423645 - sample 10: + sample 8: time = 1000000 flags = 0 data = length 486, hash DDF32CBB track 2: - format: - bitrate = -1 - id = 2 - containerMimeType = null - sampleMimeType = audio/ac3 - 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 = 1 - language = und - drmInitData = - - initializationData: total output bytes = 4180 sample count = 10 + format 0: + id = 2 + sampleMimeType = audio/ac3 + channelCount = 1 + sampleRate = 44100 + selectionFlags = 1 + language = und sample 0: time = 791000 flags = 1 diff --git a/library/core/src/test/assets/mkv/sample.mkv.3.dump b/testdata/src/test/assets/mkv/sample.mkv.3.dump similarity index 51% rename from library/core/src/test/assets/mkv/sample.mkv.3.dump rename to testdata/src/test/assets/mkv/sample.mkv.3.dump index 6034c54dec..ab6d8e06cd 100644 --- a/library/core/src/test/assets/mkv/sample.mkv.3.dump +++ b/testdata/src/test/assets/mkv/sample.mkv.3.dump @@ -1,58 +1,34 @@ seekMap: isSeekable = true - duration = 1072000 + duration = 1104000 getPosition(0) = [[timeUs=67000, position=5576]] + getPosition(1) = [[timeUs=67000, position=5576]] + getPosition(552000) = [[timeUs=547000, position=77334], [timeUs=567000, position=84155]] + getPosition(1104000) = [[timeUs=1035000, position=106570]] numberOfTracks = 2 track 1: - format: - bitrate = -1 + total output bytes = 0 + sample count = 0 + format 0: id = 1 - containerMimeType = null sampleMimeType = video/avc - maxInputSize = -1 width = 1080 height = 720 - 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 = - + selectionFlags = 1 + language = und initializationData: data = length 30, hash F6F3D010 data = length 10, hash 7A0D0F2B - total output bytes = 0 - sample count = 0 track 2: - format: - bitrate = -1 - id = 2 - containerMimeType = null - sampleMimeType = audio/ac3 - 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 = 1 - language = und - drmInitData = - - initializationData: total output bytes = 1254 sample count = 3 + format 0: + id = 2 + sampleMimeType = audio/ac3 + channelCount = 1 + sampleRate = 44100 + selectionFlags = 1 + language = und sample 0: time = 1035000 flags = 1 diff --git a/testdata/src/test/assets/mkv/sample.mkv.unknown_length.dump b/testdata/src/test/assets/mkv/sample.mkv.unknown_length.dump new file mode 100644 index 0000000000..bb8c0632f3 --- /dev/null +++ b/testdata/src/test/assets/mkv/sample.mkv.unknown_length.dump @@ -0,0 +1,268 @@ +seekMap: + isSeekable = true + duration = 1104000 + getPosition(0) = [[timeUs=67000, position=5576]] + getPosition(1) = [[timeUs=67000, position=5576]] + getPosition(552000) = [[timeUs=547000, position=77334], [timeUs=567000, position=84155]] + getPosition(1104000) = [[timeUs=1035000, position=106570]] +numberOfTracks = 2 +track 1: + total output bytes = 89502 + sample count = 30 + format 0: + id = 1 + sampleMimeType = video/avc + width = 1080 + height = 720 + selectionFlags = 1 + language = und + initializationData: + data = length 30, hash F6F3D010 + data = length 10, hash 7A0D0F2B + sample 0: + time = 67000 + flags = 1 + data = length 36477, hash F0F36CFE + sample 1: + time = 134000 + flags = 0 + data = length 5341, hash 40B85E2 + sample 2: + time = 100000 + flags = 0 + data = length 596, hash 357B4D92 + sample 3: + time = 267000 + flags = 0 + data = length 7704, hash A39EDA06 + sample 4: + time = 200000 + flags = 0 + data = length 989, hash 2813C72D + sample 5: + time = 167000 + flags = 0 + data = length 721, hash C50D1C73 + sample 6: + time = 234000 + flags = 0 + data = length 519, hash 65FE1911 + sample 7: + time = 400000 + flags = 0 + data = length 6160, hash E1CAC0EC + sample 8: + time = 334000 + flags = 0 + data = length 953, hash 7160C661 + sample 9: + time = 300000 + flags = 0 + data = length 620, hash 7A7AE07C + sample 10: + time = 367000 + flags = 0 + data = length 405, hash 5CC7F4E7 + sample 11: + time = 500000 + flags = 0 + data = length 4852, hash 9DB6979D + sample 12: + time = 467000 + flags = 0 + data = length 547, hash E31A6979 + sample 13: + time = 434000 + flags = 0 + data = length 570, hash FEC40D00 + sample 14: + time = 634000 + flags = 0 + data = length 5525, hash 7C478F7E + sample 15: + time = 567000 + flags = 0 + data = length 1082, hash DA07059A + sample 16: + time = 534000 + flags = 0 + data = length 807, hash 93478E6B + sample 17: + time = 600000 + flags = 0 + data = length 744, hash 9A8E6026 + sample 18: + time = 767000 + flags = 0 + data = length 4732, hash C73B23C0 + sample 19: + time = 700000 + flags = 0 + data = length 1004, hash 8A19A228 + sample 20: + time = 667000 + flags = 0 + data = length 794, hash 8126022C + sample 21: + time = 734000 + flags = 0 + data = length 645, hash F08300E5 + sample 22: + time = 900000 + flags = 0 + data = length 2684, hash 727FE378 + sample 23: + time = 834000 + flags = 0 + data = length 787, hash 419A7821 + sample 24: + time = 800000 + flags = 0 + data = length 649, hash 5C159346 + sample 25: + time = 867000 + flags = 0 + data = length 509, hash F912D655 + sample 26: + time = 1034000 + flags = 0 + data = length 1226, hash 29815C21 + sample 27: + time = 967000 + flags = 0 + data = length 898, hash D997AD0A + sample 28: + time = 934000 + flags = 0 + data = length 476, hash A0423645 + sample 29: + time = 1000000 + flags = 0 + data = length 486, hash DDF32CBB +track 2: + total output bytes = 12120 + sample count = 29 + format 0: + id = 2 + sampleMimeType = audio/ac3 + channelCount = 1 + sampleRate = 44100 + selectionFlags = 1 + language = und + sample 0: + time = 129000 + flags = 1 + data = length 416, hash 211F2286 + sample 1: + time = 164000 + flags = 1 + data = length 418, hash 77425A86 + sample 2: + time = 198829 + flags = 1 + data = length 418, hash A0FE5CA1 + sample 3: + time = 233000 + flags = 1 + data = length 418, hash 2309B066 + sample 4: + time = 268000 + flags = 1 + data = length 418, hash 928A653B + sample 5: + time = 303000 + flags = 1 + data = length 418, hash 3422F0CB + sample 6: + time = 337829 + flags = 1 + data = length 418, hash EFF43D5B + sample 7: + time = 373000 + flags = 1 + data = length 418, hash FC8093C7 + sample 8: + time = 408000 + flags = 1 + data = length 418, hash CCC08A16 + sample 9: + time = 443000 + flags = 1 + data = length 418, hash 2A6EE863 + sample 10: + time = 477829 + flags = 1 + data = length 418, hash D69A9251 + sample 11: + time = 512000 + flags = 1 + data = length 418, hash BCFB758D + sample 12: + time = 547000 + flags = 1 + data = length 418, hash 11B66799 + sample 13: + time = 581829 + flags = 1 + data = length 418, hash C824D392 + sample 14: + time = 617000 + flags = 1 + data = length 418, hash C167D872 + sample 15: + time = 652000 + flags = 1 + data = length 418, hash 4221C855 + sample 16: + time = 687000 + flags = 1 + data = length 418, hash 4D4FF934 + sample 17: + time = 721829 + flags = 1 + data = length 418, hash 984AA025 + sample 18: + time = 757000 + flags = 1 + data = length 418, hash BB788B46 + sample 19: + time = 791000 + flags = 1 + data = length 418, hash 9EFBFD97 + sample 20: + time = 826000 + flags = 1 + data = length 418, hash DF1A460C + sample 21: + time = 860829 + flags = 1 + data = length 418, hash 2BDB56A + sample 22: + time = 896000 + flags = 1 + data = length 418, hash CA230060 + sample 23: + time = 931000 + flags = 1 + data = length 418, hash D2F19F41 + sample 24: + time = 965000 + flags = 1 + data = length 418, hash AF392D79 + sample 25: + time = 999829 + flags = 1 + data = length 418, hash C5D7F2A3 + sample 26: + time = 1035000 + flags = 1 + data = length 418, hash 733A35AE + sample 27: + time = 1069829 + flags = 1 + data = length 418, hash DE46E5D3 + sample 28: + time = 1104000 + flags = 1 + data = length 418, hash 56AB8D37 +tracksEnded = true diff --git a/testdata/src/test/assets/mkv/sample_with_htc_rotation_track_name.mkv b/testdata/src/test/assets/mkv/sample_with_htc_rotation_track_name.mkv new file mode 100644 index 0000000000..02574ff79c Binary files /dev/null and b/testdata/src/test/assets/mkv/sample_with_htc_rotation_track_name.mkv differ diff --git a/testdata/src/test/assets/mkv/sample_with_htc_rotation_track_name.mkv.0.dump b/testdata/src/test/assets/mkv/sample_with_htc_rotation_track_name.mkv.0.dump new file mode 100644 index 0000000000..258efdc087 --- /dev/null +++ b/testdata/src/test/assets/mkv/sample_with_htc_rotation_track_name.mkv.0.dump @@ -0,0 +1,269 @@ +seekMap: + isSeekable = true + duration = 1071000 + getPosition(0) = [[timeUs=0, position=994]] + getPosition(1) = [[timeUs=0, position=994]] + getPosition(535500) = [[timeUs=0, position=994]] + getPosition(1071000) = [[timeUs=0, position=994]] +numberOfTracks = 2 +track 1: + total output bytes = 89502 + sample count = 30 + format 0: + id = 1 + sampleMimeType = video/avc + width = 1080 + height = 720 + rotationDegrees = 90 + selectionFlags = 1 + language = und + initializationData: + data = length 30, hash F6F3D010 + data = length 10, hash 7A0D0F2B + sample 0: + time = 0 + flags = 1 + data = length 36477, hash F0F36CFE + sample 1: + time = 67000 + flags = 0 + data = length 5341, hash 40B85E2 + sample 2: + time = 33000 + flags = 0 + data = length 596, hash 357B4D92 + sample 3: + time = 200000 + flags = 0 + data = length 7704, hash A39EDA06 + sample 4: + time = 133000 + flags = 0 + data = length 989, hash 2813C72D + sample 5: + time = 100000 + flags = 0 + data = length 721, hash C50D1C73 + sample 6: + time = 167000 + flags = 0 + data = length 519, hash 65FE1911 + sample 7: + time = 333000 + flags = 0 + data = length 6160, hash E1CAC0EC + sample 8: + time = 267000 + flags = 0 + data = length 953, hash 7160C661 + sample 9: + time = 233000 + flags = 0 + data = length 620, hash 7A7AE07C + sample 10: + time = 300000 + flags = 0 + data = length 405, hash 5CC7F4E7 + sample 11: + time = 433000 + flags = 0 + data = length 4852, hash 9DB6979D + sample 12: + time = 400000 + flags = 0 + data = length 547, hash E31A6979 + sample 13: + time = 367000 + flags = 0 + data = length 570, hash FEC40D00 + sample 14: + time = 567000 + flags = 0 + data = length 5525, hash 7C478F7E + sample 15: + time = 500000 + flags = 0 + data = length 1082, hash DA07059A + sample 16: + time = 467000 + flags = 0 + data = length 807, hash 93478E6B + sample 17: + time = 533000 + flags = 0 + data = length 744, hash 9A8E6026 + sample 18: + time = 700000 + flags = 0 + data = length 4732, hash C73B23C0 + sample 19: + time = 633000 + flags = 0 + data = length 1004, hash 8A19A228 + sample 20: + time = 600000 + flags = 0 + data = length 794, hash 8126022C + sample 21: + time = 667000 + flags = 0 + data = length 645, hash F08300E5 + sample 22: + time = 833000 + flags = 0 + data = length 2684, hash 727FE378 + sample 23: + time = 767000 + flags = 0 + data = length 787, hash 419A7821 + sample 24: + time = 733000 + flags = 0 + data = length 649, hash 5C159346 + sample 25: + time = 800000 + flags = 0 + data = length 509, hash F912D655 + sample 26: + time = 967000 + flags = 0 + data = length 1226, hash 29815C21 + sample 27: + time = 900000 + flags = 0 + data = length 898, hash D997AD0A + sample 28: + time = 867000 + flags = 0 + data = length 476, hash A0423645 + sample 29: + time = 933000 + flags = 0 + data = length 486, hash DDF32CBB +track 2: + total output bytes = 12120 + sample count = 29 + format 0: + id = 2 + sampleMimeType = audio/ac3 + channelCount = 1 + sampleRate = 44100 + selectionFlags = 1 + language = und + sample 0: + time = 62000 + flags = 1 + data = length 416, hash 211F2286 + sample 1: + time = 97000 + flags = 1 + data = length 418, hash 77425A86 + sample 2: + time = 131000 + flags = 1 + data = length 418, hash A0FE5CA1 + sample 3: + time = 166000 + flags = 1 + data = length 418, hash 2309B066 + sample 4: + time = 201000 + flags = 1 + data = length 418, hash 928A653B + sample 5: + time = 236000 + flags = 1 + data = length 418, hash 3422F0CB + sample 6: + time = 270000 + flags = 1 + data = length 418, hash EFF43D5B + sample 7: + time = 306000 + flags = 1 + data = length 418, hash FC8093C7 + sample 8: + time = 341000 + flags = 1 + data = length 418, hash CCC08A16 + sample 9: + time = 376000 + flags = 1 + data = length 418, hash 2A6EE863 + sample 10: + time = 410000 + flags = 1 + data = length 418, hash D69A9251 + sample 11: + time = 445000 + flags = 1 + data = length 418, hash BCFB758D + sample 12: + time = 480000 + flags = 1 + data = length 418, hash 11B66799 + sample 13: + time = 514000 + flags = 1 + data = length 418, hash C824D392 + sample 14: + time = 550000 + flags = 1 + data = length 418, hash C167D872 + sample 15: + time = 585000 + flags = 1 + data = length 418, hash 4221C855 + sample 16: + time = 620000 + flags = 1 + data = length 418, hash 4D4FF934 + sample 17: + time = 654000 + flags = 1 + data = length 418, hash 984AA025 + sample 18: + time = 690000 + flags = 1 + data = length 418, hash BB788B46 + sample 19: + time = 724000 + flags = 1 + data = length 418, hash 9EFBFD97 + sample 20: + time = 759000 + flags = 1 + data = length 418, hash DF1A460C + sample 21: + time = 793000 + flags = 1 + data = length 418, hash 2BDB56A + sample 22: + time = 829000 + flags = 1 + data = length 418, hash CA230060 + sample 23: + time = 864000 + flags = 1 + data = length 418, hash D2F19F41 + sample 24: + time = 898000 + flags = 1 + data = length 418, hash AF392D79 + sample 25: + time = 932000 + flags = 1 + data = length 418, hash C5D7F2A3 + sample 26: + time = 968000 + flags = 1 + data = length 418, hash 733A35AE + sample 27: + time = 1002000 + flags = 1 + data = length 418, hash DE46E5D3 + sample 28: + time = 1037000 + flags = 1 + data = length 418, hash 56AB8D37 +tracksEnded = true diff --git a/testdata/src/test/assets/mkv/sample_with_htc_rotation_track_name.mkv.1.dump b/testdata/src/test/assets/mkv/sample_with_htc_rotation_track_name.mkv.1.dump new file mode 100644 index 0000000000..258efdc087 --- /dev/null +++ b/testdata/src/test/assets/mkv/sample_with_htc_rotation_track_name.mkv.1.dump @@ -0,0 +1,269 @@ +seekMap: + isSeekable = true + duration = 1071000 + getPosition(0) = [[timeUs=0, position=994]] + getPosition(1) = [[timeUs=0, position=994]] + getPosition(535500) = [[timeUs=0, position=994]] + getPosition(1071000) = [[timeUs=0, position=994]] +numberOfTracks = 2 +track 1: + total output bytes = 89502 + sample count = 30 + format 0: + id = 1 + sampleMimeType = video/avc + width = 1080 + height = 720 + rotationDegrees = 90 + selectionFlags = 1 + language = und + initializationData: + data = length 30, hash F6F3D010 + data = length 10, hash 7A0D0F2B + sample 0: + time = 0 + flags = 1 + data = length 36477, hash F0F36CFE + sample 1: + time = 67000 + flags = 0 + data = length 5341, hash 40B85E2 + sample 2: + time = 33000 + flags = 0 + data = length 596, hash 357B4D92 + sample 3: + time = 200000 + flags = 0 + data = length 7704, hash A39EDA06 + sample 4: + time = 133000 + flags = 0 + data = length 989, hash 2813C72D + sample 5: + time = 100000 + flags = 0 + data = length 721, hash C50D1C73 + sample 6: + time = 167000 + flags = 0 + data = length 519, hash 65FE1911 + sample 7: + time = 333000 + flags = 0 + data = length 6160, hash E1CAC0EC + sample 8: + time = 267000 + flags = 0 + data = length 953, hash 7160C661 + sample 9: + time = 233000 + flags = 0 + data = length 620, hash 7A7AE07C + sample 10: + time = 300000 + flags = 0 + data = length 405, hash 5CC7F4E7 + sample 11: + time = 433000 + flags = 0 + data = length 4852, hash 9DB6979D + sample 12: + time = 400000 + flags = 0 + data = length 547, hash E31A6979 + sample 13: + time = 367000 + flags = 0 + data = length 570, hash FEC40D00 + sample 14: + time = 567000 + flags = 0 + data = length 5525, hash 7C478F7E + sample 15: + time = 500000 + flags = 0 + data = length 1082, hash DA07059A + sample 16: + time = 467000 + flags = 0 + data = length 807, hash 93478E6B + sample 17: + time = 533000 + flags = 0 + data = length 744, hash 9A8E6026 + sample 18: + time = 700000 + flags = 0 + data = length 4732, hash C73B23C0 + sample 19: + time = 633000 + flags = 0 + data = length 1004, hash 8A19A228 + sample 20: + time = 600000 + flags = 0 + data = length 794, hash 8126022C + sample 21: + time = 667000 + flags = 0 + data = length 645, hash F08300E5 + sample 22: + time = 833000 + flags = 0 + data = length 2684, hash 727FE378 + sample 23: + time = 767000 + flags = 0 + data = length 787, hash 419A7821 + sample 24: + time = 733000 + flags = 0 + data = length 649, hash 5C159346 + sample 25: + time = 800000 + flags = 0 + data = length 509, hash F912D655 + sample 26: + time = 967000 + flags = 0 + data = length 1226, hash 29815C21 + sample 27: + time = 900000 + flags = 0 + data = length 898, hash D997AD0A + sample 28: + time = 867000 + flags = 0 + data = length 476, hash A0423645 + sample 29: + time = 933000 + flags = 0 + data = length 486, hash DDF32CBB +track 2: + total output bytes = 12120 + sample count = 29 + format 0: + id = 2 + sampleMimeType = audio/ac3 + channelCount = 1 + sampleRate = 44100 + selectionFlags = 1 + language = und + sample 0: + time = 62000 + flags = 1 + data = length 416, hash 211F2286 + sample 1: + time = 97000 + flags = 1 + data = length 418, hash 77425A86 + sample 2: + time = 131000 + flags = 1 + data = length 418, hash A0FE5CA1 + sample 3: + time = 166000 + flags = 1 + data = length 418, hash 2309B066 + sample 4: + time = 201000 + flags = 1 + data = length 418, hash 928A653B + sample 5: + time = 236000 + flags = 1 + data = length 418, hash 3422F0CB + sample 6: + time = 270000 + flags = 1 + data = length 418, hash EFF43D5B + sample 7: + time = 306000 + flags = 1 + data = length 418, hash FC8093C7 + sample 8: + time = 341000 + flags = 1 + data = length 418, hash CCC08A16 + sample 9: + time = 376000 + flags = 1 + data = length 418, hash 2A6EE863 + sample 10: + time = 410000 + flags = 1 + data = length 418, hash D69A9251 + sample 11: + time = 445000 + flags = 1 + data = length 418, hash BCFB758D + sample 12: + time = 480000 + flags = 1 + data = length 418, hash 11B66799 + sample 13: + time = 514000 + flags = 1 + data = length 418, hash C824D392 + sample 14: + time = 550000 + flags = 1 + data = length 418, hash C167D872 + sample 15: + time = 585000 + flags = 1 + data = length 418, hash 4221C855 + sample 16: + time = 620000 + flags = 1 + data = length 418, hash 4D4FF934 + sample 17: + time = 654000 + flags = 1 + data = length 418, hash 984AA025 + sample 18: + time = 690000 + flags = 1 + data = length 418, hash BB788B46 + sample 19: + time = 724000 + flags = 1 + data = length 418, hash 9EFBFD97 + sample 20: + time = 759000 + flags = 1 + data = length 418, hash DF1A460C + sample 21: + time = 793000 + flags = 1 + data = length 418, hash 2BDB56A + sample 22: + time = 829000 + flags = 1 + data = length 418, hash CA230060 + sample 23: + time = 864000 + flags = 1 + data = length 418, hash D2F19F41 + sample 24: + time = 898000 + flags = 1 + data = length 418, hash AF392D79 + sample 25: + time = 932000 + flags = 1 + data = length 418, hash C5D7F2A3 + sample 26: + time = 968000 + flags = 1 + data = length 418, hash 733A35AE + sample 27: + time = 1002000 + flags = 1 + data = length 418, hash DE46E5D3 + sample 28: + time = 1037000 + flags = 1 + data = length 418, hash 56AB8D37 +tracksEnded = true diff --git a/testdata/src/test/assets/mkv/sample_with_htc_rotation_track_name.mkv.2.dump b/testdata/src/test/assets/mkv/sample_with_htc_rotation_track_name.mkv.2.dump new file mode 100644 index 0000000000..258efdc087 --- /dev/null +++ b/testdata/src/test/assets/mkv/sample_with_htc_rotation_track_name.mkv.2.dump @@ -0,0 +1,269 @@ +seekMap: + isSeekable = true + duration = 1071000 + getPosition(0) = [[timeUs=0, position=994]] + getPosition(1) = [[timeUs=0, position=994]] + getPosition(535500) = [[timeUs=0, position=994]] + getPosition(1071000) = [[timeUs=0, position=994]] +numberOfTracks = 2 +track 1: + total output bytes = 89502 + sample count = 30 + format 0: + id = 1 + sampleMimeType = video/avc + width = 1080 + height = 720 + rotationDegrees = 90 + selectionFlags = 1 + language = und + initializationData: + data = length 30, hash F6F3D010 + data = length 10, hash 7A0D0F2B + sample 0: + time = 0 + flags = 1 + data = length 36477, hash F0F36CFE + sample 1: + time = 67000 + flags = 0 + data = length 5341, hash 40B85E2 + sample 2: + time = 33000 + flags = 0 + data = length 596, hash 357B4D92 + sample 3: + time = 200000 + flags = 0 + data = length 7704, hash A39EDA06 + sample 4: + time = 133000 + flags = 0 + data = length 989, hash 2813C72D + sample 5: + time = 100000 + flags = 0 + data = length 721, hash C50D1C73 + sample 6: + time = 167000 + flags = 0 + data = length 519, hash 65FE1911 + sample 7: + time = 333000 + flags = 0 + data = length 6160, hash E1CAC0EC + sample 8: + time = 267000 + flags = 0 + data = length 953, hash 7160C661 + sample 9: + time = 233000 + flags = 0 + data = length 620, hash 7A7AE07C + sample 10: + time = 300000 + flags = 0 + data = length 405, hash 5CC7F4E7 + sample 11: + time = 433000 + flags = 0 + data = length 4852, hash 9DB6979D + sample 12: + time = 400000 + flags = 0 + data = length 547, hash E31A6979 + sample 13: + time = 367000 + flags = 0 + data = length 570, hash FEC40D00 + sample 14: + time = 567000 + flags = 0 + data = length 5525, hash 7C478F7E + sample 15: + time = 500000 + flags = 0 + data = length 1082, hash DA07059A + sample 16: + time = 467000 + flags = 0 + data = length 807, hash 93478E6B + sample 17: + time = 533000 + flags = 0 + data = length 744, hash 9A8E6026 + sample 18: + time = 700000 + flags = 0 + data = length 4732, hash C73B23C0 + sample 19: + time = 633000 + flags = 0 + data = length 1004, hash 8A19A228 + sample 20: + time = 600000 + flags = 0 + data = length 794, hash 8126022C + sample 21: + time = 667000 + flags = 0 + data = length 645, hash F08300E5 + sample 22: + time = 833000 + flags = 0 + data = length 2684, hash 727FE378 + sample 23: + time = 767000 + flags = 0 + data = length 787, hash 419A7821 + sample 24: + time = 733000 + flags = 0 + data = length 649, hash 5C159346 + sample 25: + time = 800000 + flags = 0 + data = length 509, hash F912D655 + sample 26: + time = 967000 + flags = 0 + data = length 1226, hash 29815C21 + sample 27: + time = 900000 + flags = 0 + data = length 898, hash D997AD0A + sample 28: + time = 867000 + flags = 0 + data = length 476, hash A0423645 + sample 29: + time = 933000 + flags = 0 + data = length 486, hash DDF32CBB +track 2: + total output bytes = 12120 + sample count = 29 + format 0: + id = 2 + sampleMimeType = audio/ac3 + channelCount = 1 + sampleRate = 44100 + selectionFlags = 1 + language = und + sample 0: + time = 62000 + flags = 1 + data = length 416, hash 211F2286 + sample 1: + time = 97000 + flags = 1 + data = length 418, hash 77425A86 + sample 2: + time = 131000 + flags = 1 + data = length 418, hash A0FE5CA1 + sample 3: + time = 166000 + flags = 1 + data = length 418, hash 2309B066 + sample 4: + time = 201000 + flags = 1 + data = length 418, hash 928A653B + sample 5: + time = 236000 + flags = 1 + data = length 418, hash 3422F0CB + sample 6: + time = 270000 + flags = 1 + data = length 418, hash EFF43D5B + sample 7: + time = 306000 + flags = 1 + data = length 418, hash FC8093C7 + sample 8: + time = 341000 + flags = 1 + data = length 418, hash CCC08A16 + sample 9: + time = 376000 + flags = 1 + data = length 418, hash 2A6EE863 + sample 10: + time = 410000 + flags = 1 + data = length 418, hash D69A9251 + sample 11: + time = 445000 + flags = 1 + data = length 418, hash BCFB758D + sample 12: + time = 480000 + flags = 1 + data = length 418, hash 11B66799 + sample 13: + time = 514000 + flags = 1 + data = length 418, hash C824D392 + sample 14: + time = 550000 + flags = 1 + data = length 418, hash C167D872 + sample 15: + time = 585000 + flags = 1 + data = length 418, hash 4221C855 + sample 16: + time = 620000 + flags = 1 + data = length 418, hash 4D4FF934 + sample 17: + time = 654000 + flags = 1 + data = length 418, hash 984AA025 + sample 18: + time = 690000 + flags = 1 + data = length 418, hash BB788B46 + sample 19: + time = 724000 + flags = 1 + data = length 418, hash 9EFBFD97 + sample 20: + time = 759000 + flags = 1 + data = length 418, hash DF1A460C + sample 21: + time = 793000 + flags = 1 + data = length 418, hash 2BDB56A + sample 22: + time = 829000 + flags = 1 + data = length 418, hash CA230060 + sample 23: + time = 864000 + flags = 1 + data = length 418, hash D2F19F41 + sample 24: + time = 898000 + flags = 1 + data = length 418, hash AF392D79 + sample 25: + time = 932000 + flags = 1 + data = length 418, hash C5D7F2A3 + sample 26: + time = 968000 + flags = 1 + data = length 418, hash 733A35AE + sample 27: + time = 1002000 + flags = 1 + data = length 418, hash DE46E5D3 + sample 28: + time = 1037000 + flags = 1 + data = length 418, hash 56AB8D37 +tracksEnded = true diff --git a/testdata/src/test/assets/mkv/sample_with_htc_rotation_track_name.mkv.3.dump b/testdata/src/test/assets/mkv/sample_with_htc_rotation_track_name.mkv.3.dump new file mode 100644 index 0000000000..258efdc087 --- /dev/null +++ b/testdata/src/test/assets/mkv/sample_with_htc_rotation_track_name.mkv.3.dump @@ -0,0 +1,269 @@ +seekMap: + isSeekable = true + duration = 1071000 + getPosition(0) = [[timeUs=0, position=994]] + getPosition(1) = [[timeUs=0, position=994]] + getPosition(535500) = [[timeUs=0, position=994]] + getPosition(1071000) = [[timeUs=0, position=994]] +numberOfTracks = 2 +track 1: + total output bytes = 89502 + sample count = 30 + format 0: + id = 1 + sampleMimeType = video/avc + width = 1080 + height = 720 + rotationDegrees = 90 + selectionFlags = 1 + language = und + initializationData: + data = length 30, hash F6F3D010 + data = length 10, hash 7A0D0F2B + sample 0: + time = 0 + flags = 1 + data = length 36477, hash F0F36CFE + sample 1: + time = 67000 + flags = 0 + data = length 5341, hash 40B85E2 + sample 2: + time = 33000 + flags = 0 + data = length 596, hash 357B4D92 + sample 3: + time = 200000 + flags = 0 + data = length 7704, hash A39EDA06 + sample 4: + time = 133000 + flags = 0 + data = length 989, hash 2813C72D + sample 5: + time = 100000 + flags = 0 + data = length 721, hash C50D1C73 + sample 6: + time = 167000 + flags = 0 + data = length 519, hash 65FE1911 + sample 7: + time = 333000 + flags = 0 + data = length 6160, hash E1CAC0EC + sample 8: + time = 267000 + flags = 0 + data = length 953, hash 7160C661 + sample 9: + time = 233000 + flags = 0 + data = length 620, hash 7A7AE07C + sample 10: + time = 300000 + flags = 0 + data = length 405, hash 5CC7F4E7 + sample 11: + time = 433000 + flags = 0 + data = length 4852, hash 9DB6979D + sample 12: + time = 400000 + flags = 0 + data = length 547, hash E31A6979 + sample 13: + time = 367000 + flags = 0 + data = length 570, hash FEC40D00 + sample 14: + time = 567000 + flags = 0 + data = length 5525, hash 7C478F7E + sample 15: + time = 500000 + flags = 0 + data = length 1082, hash DA07059A + sample 16: + time = 467000 + flags = 0 + data = length 807, hash 93478E6B + sample 17: + time = 533000 + flags = 0 + data = length 744, hash 9A8E6026 + sample 18: + time = 700000 + flags = 0 + data = length 4732, hash C73B23C0 + sample 19: + time = 633000 + flags = 0 + data = length 1004, hash 8A19A228 + sample 20: + time = 600000 + flags = 0 + data = length 794, hash 8126022C + sample 21: + time = 667000 + flags = 0 + data = length 645, hash F08300E5 + sample 22: + time = 833000 + flags = 0 + data = length 2684, hash 727FE378 + sample 23: + time = 767000 + flags = 0 + data = length 787, hash 419A7821 + sample 24: + time = 733000 + flags = 0 + data = length 649, hash 5C159346 + sample 25: + time = 800000 + flags = 0 + data = length 509, hash F912D655 + sample 26: + time = 967000 + flags = 0 + data = length 1226, hash 29815C21 + sample 27: + time = 900000 + flags = 0 + data = length 898, hash D997AD0A + sample 28: + time = 867000 + flags = 0 + data = length 476, hash A0423645 + sample 29: + time = 933000 + flags = 0 + data = length 486, hash DDF32CBB +track 2: + total output bytes = 12120 + sample count = 29 + format 0: + id = 2 + sampleMimeType = audio/ac3 + channelCount = 1 + sampleRate = 44100 + selectionFlags = 1 + language = und + sample 0: + time = 62000 + flags = 1 + data = length 416, hash 211F2286 + sample 1: + time = 97000 + flags = 1 + data = length 418, hash 77425A86 + sample 2: + time = 131000 + flags = 1 + data = length 418, hash A0FE5CA1 + sample 3: + time = 166000 + flags = 1 + data = length 418, hash 2309B066 + sample 4: + time = 201000 + flags = 1 + data = length 418, hash 928A653B + sample 5: + time = 236000 + flags = 1 + data = length 418, hash 3422F0CB + sample 6: + time = 270000 + flags = 1 + data = length 418, hash EFF43D5B + sample 7: + time = 306000 + flags = 1 + data = length 418, hash FC8093C7 + sample 8: + time = 341000 + flags = 1 + data = length 418, hash CCC08A16 + sample 9: + time = 376000 + flags = 1 + data = length 418, hash 2A6EE863 + sample 10: + time = 410000 + flags = 1 + data = length 418, hash D69A9251 + sample 11: + time = 445000 + flags = 1 + data = length 418, hash BCFB758D + sample 12: + time = 480000 + flags = 1 + data = length 418, hash 11B66799 + sample 13: + time = 514000 + flags = 1 + data = length 418, hash C824D392 + sample 14: + time = 550000 + flags = 1 + data = length 418, hash C167D872 + sample 15: + time = 585000 + flags = 1 + data = length 418, hash 4221C855 + sample 16: + time = 620000 + flags = 1 + data = length 418, hash 4D4FF934 + sample 17: + time = 654000 + flags = 1 + data = length 418, hash 984AA025 + sample 18: + time = 690000 + flags = 1 + data = length 418, hash BB788B46 + sample 19: + time = 724000 + flags = 1 + data = length 418, hash 9EFBFD97 + sample 20: + time = 759000 + flags = 1 + data = length 418, hash DF1A460C + sample 21: + time = 793000 + flags = 1 + data = length 418, hash 2BDB56A + sample 22: + time = 829000 + flags = 1 + data = length 418, hash CA230060 + sample 23: + time = 864000 + flags = 1 + data = length 418, hash D2F19F41 + sample 24: + time = 898000 + flags = 1 + data = length 418, hash AF392D79 + sample 25: + time = 932000 + flags = 1 + data = length 418, hash C5D7F2A3 + sample 26: + time = 968000 + flags = 1 + data = length 418, hash 733A35AE + sample 27: + time = 1002000 + flags = 1 + data = length 418, hash DE46E5D3 + sample 28: + time = 1037000 + flags = 1 + data = length 418, hash 56AB8D37 +tracksEnded = true diff --git a/testdata/src/test/assets/mkv/sample_with_htc_rotation_track_name.mkv.unknown_length.dump b/testdata/src/test/assets/mkv/sample_with_htc_rotation_track_name.mkv.unknown_length.dump new file mode 100644 index 0000000000..258efdc087 --- /dev/null +++ b/testdata/src/test/assets/mkv/sample_with_htc_rotation_track_name.mkv.unknown_length.dump @@ -0,0 +1,269 @@ +seekMap: + isSeekable = true + duration = 1071000 + getPosition(0) = [[timeUs=0, position=994]] + getPosition(1) = [[timeUs=0, position=994]] + getPosition(535500) = [[timeUs=0, position=994]] + getPosition(1071000) = [[timeUs=0, position=994]] +numberOfTracks = 2 +track 1: + total output bytes = 89502 + sample count = 30 + format 0: + id = 1 + sampleMimeType = video/avc + width = 1080 + height = 720 + rotationDegrees = 90 + selectionFlags = 1 + language = und + initializationData: + data = length 30, hash F6F3D010 + data = length 10, hash 7A0D0F2B + sample 0: + time = 0 + flags = 1 + data = length 36477, hash F0F36CFE + sample 1: + time = 67000 + flags = 0 + data = length 5341, hash 40B85E2 + sample 2: + time = 33000 + flags = 0 + data = length 596, hash 357B4D92 + sample 3: + time = 200000 + flags = 0 + data = length 7704, hash A39EDA06 + sample 4: + time = 133000 + flags = 0 + data = length 989, hash 2813C72D + sample 5: + time = 100000 + flags = 0 + data = length 721, hash C50D1C73 + sample 6: + time = 167000 + flags = 0 + data = length 519, hash 65FE1911 + sample 7: + time = 333000 + flags = 0 + data = length 6160, hash E1CAC0EC + sample 8: + time = 267000 + flags = 0 + data = length 953, hash 7160C661 + sample 9: + time = 233000 + flags = 0 + data = length 620, hash 7A7AE07C + sample 10: + time = 300000 + flags = 0 + data = length 405, hash 5CC7F4E7 + sample 11: + time = 433000 + flags = 0 + data = length 4852, hash 9DB6979D + sample 12: + time = 400000 + flags = 0 + data = length 547, hash E31A6979 + sample 13: + time = 367000 + flags = 0 + data = length 570, hash FEC40D00 + sample 14: + time = 567000 + flags = 0 + data = length 5525, hash 7C478F7E + sample 15: + time = 500000 + flags = 0 + data = length 1082, hash DA07059A + sample 16: + time = 467000 + flags = 0 + data = length 807, hash 93478E6B + sample 17: + time = 533000 + flags = 0 + data = length 744, hash 9A8E6026 + sample 18: + time = 700000 + flags = 0 + data = length 4732, hash C73B23C0 + sample 19: + time = 633000 + flags = 0 + data = length 1004, hash 8A19A228 + sample 20: + time = 600000 + flags = 0 + data = length 794, hash 8126022C + sample 21: + time = 667000 + flags = 0 + data = length 645, hash F08300E5 + sample 22: + time = 833000 + flags = 0 + data = length 2684, hash 727FE378 + sample 23: + time = 767000 + flags = 0 + data = length 787, hash 419A7821 + sample 24: + time = 733000 + flags = 0 + data = length 649, hash 5C159346 + sample 25: + time = 800000 + flags = 0 + data = length 509, hash F912D655 + sample 26: + time = 967000 + flags = 0 + data = length 1226, hash 29815C21 + sample 27: + time = 900000 + flags = 0 + data = length 898, hash D997AD0A + sample 28: + time = 867000 + flags = 0 + data = length 476, hash A0423645 + sample 29: + time = 933000 + flags = 0 + data = length 486, hash DDF32CBB +track 2: + total output bytes = 12120 + sample count = 29 + format 0: + id = 2 + sampleMimeType = audio/ac3 + channelCount = 1 + sampleRate = 44100 + selectionFlags = 1 + language = und + sample 0: + time = 62000 + flags = 1 + data = length 416, hash 211F2286 + sample 1: + time = 97000 + flags = 1 + data = length 418, hash 77425A86 + sample 2: + time = 131000 + flags = 1 + data = length 418, hash A0FE5CA1 + sample 3: + time = 166000 + flags = 1 + data = length 418, hash 2309B066 + sample 4: + time = 201000 + flags = 1 + data = length 418, hash 928A653B + sample 5: + time = 236000 + flags = 1 + data = length 418, hash 3422F0CB + sample 6: + time = 270000 + flags = 1 + data = length 418, hash EFF43D5B + sample 7: + time = 306000 + flags = 1 + data = length 418, hash FC8093C7 + sample 8: + time = 341000 + flags = 1 + data = length 418, hash CCC08A16 + sample 9: + time = 376000 + flags = 1 + data = length 418, hash 2A6EE863 + sample 10: + time = 410000 + flags = 1 + data = length 418, hash D69A9251 + sample 11: + time = 445000 + flags = 1 + data = length 418, hash BCFB758D + sample 12: + time = 480000 + flags = 1 + data = length 418, hash 11B66799 + sample 13: + time = 514000 + flags = 1 + data = length 418, hash C824D392 + sample 14: + time = 550000 + flags = 1 + data = length 418, hash C167D872 + sample 15: + time = 585000 + flags = 1 + data = length 418, hash 4221C855 + sample 16: + time = 620000 + flags = 1 + data = length 418, hash 4D4FF934 + sample 17: + time = 654000 + flags = 1 + data = length 418, hash 984AA025 + sample 18: + time = 690000 + flags = 1 + data = length 418, hash BB788B46 + sample 19: + time = 724000 + flags = 1 + data = length 418, hash 9EFBFD97 + sample 20: + time = 759000 + flags = 1 + data = length 418, hash DF1A460C + sample 21: + time = 793000 + flags = 1 + data = length 418, hash 2BDB56A + sample 22: + time = 829000 + flags = 1 + data = length 418, hash CA230060 + sample 23: + time = 864000 + flags = 1 + data = length 418, hash D2F19F41 + sample 24: + time = 898000 + flags = 1 + data = length 418, hash AF392D79 + sample 25: + time = 932000 + flags = 1 + data = length 418, hash C5D7F2A3 + sample 26: + time = 968000 + flags = 1 + data = length 418, hash 733A35AE + sample 27: + time = 1002000 + flags = 1 + data = length 418, hash DE46E5D3 + sample 28: + time = 1037000 + flags = 1 + data = length 418, hash 56AB8D37 +tracksEnded = true diff --git a/testdata/src/test/assets/mkv/sample_with_srt.mkv b/testdata/src/test/assets/mkv/sample_with_srt.mkv new file mode 100644 index 0000000000..bdd56d877f Binary files /dev/null and b/testdata/src/test/assets/mkv/sample_with_srt.mkv differ diff --git a/testdata/src/test/assets/mkv/sample_with_srt.mkv.0.dump b/testdata/src/test/assets/mkv/sample_with_srt.mkv.0.dump new file mode 100644 index 0000000000..1b6f763b56 --- /dev/null +++ b/testdata/src/test/assets/mkv/sample_with_srt.mkv.0.dump @@ -0,0 +1,280 @@ +seekMap: + isSeekable = true + duration = 1234000 + getPosition(0) = [[timeUs=0, position=1163]] + getPosition(1) = [[timeUs=0, position=1163]] + getPosition(617000) = [[timeUs=0, position=1163]] + getPosition(1234000) = [[timeUs=0, position=1163]] +numberOfTracks = 3 +track 1: + total output bytes = 89502 + sample count = 30 + format 0: + id = 1 + sampleMimeType = video/avc + width = 1080 + height = 720 + selectionFlags = 1 + language = und + initializationData: + data = length 30, hash F6F3D010 + data = length 10, hash 7A0D0F2B + sample 0: + time = 0 + flags = 1 + data = length 36477, hash F0F36CFE + sample 1: + time = 67000 + flags = 0 + data = length 5341, hash 40B85E2 + sample 2: + time = 33000 + flags = 0 + data = length 596, hash 357B4D92 + sample 3: + time = 200000 + flags = 0 + data = length 7704, hash A39EDA06 + sample 4: + time = 133000 + flags = 0 + data = length 989, hash 2813C72D + sample 5: + time = 100000 + flags = 0 + data = length 721, hash C50D1C73 + sample 6: + time = 167000 + flags = 0 + data = length 519, hash 65FE1911 + sample 7: + time = 333000 + flags = 0 + data = length 6160, hash E1CAC0EC + sample 8: + time = 267000 + flags = 0 + data = length 953, hash 7160C661 + sample 9: + time = 233000 + flags = 0 + data = length 620, hash 7A7AE07C + sample 10: + time = 300000 + flags = 0 + data = length 405, hash 5CC7F4E7 + sample 11: + time = 433000 + flags = 0 + data = length 4852, hash 9DB6979D + sample 12: + time = 400000 + flags = 0 + data = length 547, hash E31A6979 + sample 13: + time = 367000 + flags = 0 + data = length 570, hash FEC40D00 + sample 14: + time = 567000 + flags = 0 + data = length 5525, hash 7C478F7E + sample 15: + time = 500000 + flags = 0 + data = length 1082, hash DA07059A + sample 16: + time = 467000 + flags = 0 + data = length 807, hash 93478E6B + sample 17: + time = 533000 + flags = 0 + data = length 744, hash 9A8E6026 + sample 18: + time = 700000 + flags = 0 + data = length 4732, hash C73B23C0 + sample 19: + time = 633000 + flags = 0 + data = length 1004, hash 8A19A228 + sample 20: + time = 600000 + flags = 0 + data = length 794, hash 8126022C + sample 21: + time = 667000 + flags = 0 + data = length 645, hash F08300E5 + sample 22: + time = 833000 + flags = 0 + data = length 2684, hash 727FE378 + sample 23: + time = 767000 + flags = 0 + data = length 787, hash 419A7821 + sample 24: + time = 733000 + flags = 0 + data = length 649, hash 5C159346 + sample 25: + time = 800000 + flags = 0 + data = length 509, hash F912D655 + sample 26: + time = 967000 + flags = 0 + data = length 1226, hash 29815C21 + sample 27: + time = 900000 + flags = 0 + data = length 898, hash D997AD0A + sample 28: + time = 867000 + flags = 0 + data = length 476, hash A0423645 + sample 29: + time = 933000 + flags = 0 + data = length 486, hash DDF32CBB +track 2: + total output bytes = 12120 + sample count = 29 + format 0: + id = 2 + sampleMimeType = audio/ac3 + channelCount = 1 + sampleRate = 44100 + selectionFlags = 1 + language = und + sample 0: + time = 62000 + flags = 1 + data = length 416, hash 211F2286 + sample 1: + time = 97000 + flags = 1 + data = length 418, hash 77425A86 + sample 2: + time = 131000 + flags = 1 + data = length 418, hash A0FE5CA1 + sample 3: + time = 166000 + flags = 1 + data = length 418, hash 2309B066 + sample 4: + time = 201000 + flags = 1 + data = length 418, hash 928A653B + sample 5: + time = 236000 + flags = 1 + data = length 418, hash 3422F0CB + sample 6: + time = 270000 + flags = 1 + data = length 418, hash EFF43D5B + sample 7: + time = 306000 + flags = 1 + data = length 418, hash FC8093C7 + sample 8: + time = 341000 + flags = 1 + data = length 418, hash CCC08A16 + sample 9: + time = 376000 + flags = 1 + data = length 418, hash 2A6EE863 + sample 10: + time = 410000 + flags = 1 + data = length 418, hash D69A9251 + sample 11: + time = 445000 + flags = 1 + data = length 418, hash BCFB758D + sample 12: + time = 480000 + flags = 1 + data = length 418, hash 11B66799 + sample 13: + time = 514000 + flags = 1 + data = length 418, hash C824D392 + sample 14: + time = 550000 + flags = 1 + data = length 418, hash C167D872 + sample 15: + time = 585000 + flags = 1 + data = length 418, hash 4221C855 + sample 16: + time = 620000 + flags = 1 + data = length 418, hash 4D4FF934 + sample 17: + time = 654000 + flags = 1 + data = length 418, hash 984AA025 + sample 18: + time = 690000 + flags = 1 + data = length 418, hash BB788B46 + sample 19: + time = 724000 + flags = 1 + data = length 418, hash 9EFBFD97 + sample 20: + time = 759000 + flags = 1 + data = length 418, hash DF1A460C + sample 21: + time = 793000 + flags = 1 + data = length 418, hash 2BDB56A + sample 22: + time = 829000 + flags = 1 + data = length 418, hash CA230060 + sample 23: + time = 864000 + flags = 1 + data = length 418, hash D2F19F41 + sample 24: + time = 898000 + flags = 1 + data = length 418, hash AF392D79 + sample 25: + time = 932000 + flags = 1 + data = length 418, hash C5D7F2A3 + sample 26: + time = 968000 + flags = 1 + data = length 418, hash 733A35AE + sample 27: + time = 1002000 + flags = 1 + data = length 418, hash DE46E5D3 + sample 28: + time = 1037000 + flags = 1 + data = length 418, hash 56AB8D37 +track 3: + total output bytes = 59 + sample count = 1 + format 0: + id = 3 + sampleMimeType = application/x-subrip + language = en + label = Subs Label + sample 0: + time = 0 + flags = 1 + data = length 59, hash 1AD38625 +tracksEnded = true diff --git a/testdata/src/test/assets/mkv/sample_with_srt.mkv.1.dump b/testdata/src/test/assets/mkv/sample_with_srt.mkv.1.dump new file mode 100644 index 0000000000..1b6f763b56 --- /dev/null +++ b/testdata/src/test/assets/mkv/sample_with_srt.mkv.1.dump @@ -0,0 +1,280 @@ +seekMap: + isSeekable = true + duration = 1234000 + getPosition(0) = [[timeUs=0, position=1163]] + getPosition(1) = [[timeUs=0, position=1163]] + getPosition(617000) = [[timeUs=0, position=1163]] + getPosition(1234000) = [[timeUs=0, position=1163]] +numberOfTracks = 3 +track 1: + total output bytes = 89502 + sample count = 30 + format 0: + id = 1 + sampleMimeType = video/avc + width = 1080 + height = 720 + selectionFlags = 1 + language = und + initializationData: + data = length 30, hash F6F3D010 + data = length 10, hash 7A0D0F2B + sample 0: + time = 0 + flags = 1 + data = length 36477, hash F0F36CFE + sample 1: + time = 67000 + flags = 0 + data = length 5341, hash 40B85E2 + sample 2: + time = 33000 + flags = 0 + data = length 596, hash 357B4D92 + sample 3: + time = 200000 + flags = 0 + data = length 7704, hash A39EDA06 + sample 4: + time = 133000 + flags = 0 + data = length 989, hash 2813C72D + sample 5: + time = 100000 + flags = 0 + data = length 721, hash C50D1C73 + sample 6: + time = 167000 + flags = 0 + data = length 519, hash 65FE1911 + sample 7: + time = 333000 + flags = 0 + data = length 6160, hash E1CAC0EC + sample 8: + time = 267000 + flags = 0 + data = length 953, hash 7160C661 + sample 9: + time = 233000 + flags = 0 + data = length 620, hash 7A7AE07C + sample 10: + time = 300000 + flags = 0 + data = length 405, hash 5CC7F4E7 + sample 11: + time = 433000 + flags = 0 + data = length 4852, hash 9DB6979D + sample 12: + time = 400000 + flags = 0 + data = length 547, hash E31A6979 + sample 13: + time = 367000 + flags = 0 + data = length 570, hash FEC40D00 + sample 14: + time = 567000 + flags = 0 + data = length 5525, hash 7C478F7E + sample 15: + time = 500000 + flags = 0 + data = length 1082, hash DA07059A + sample 16: + time = 467000 + flags = 0 + data = length 807, hash 93478E6B + sample 17: + time = 533000 + flags = 0 + data = length 744, hash 9A8E6026 + sample 18: + time = 700000 + flags = 0 + data = length 4732, hash C73B23C0 + sample 19: + time = 633000 + flags = 0 + data = length 1004, hash 8A19A228 + sample 20: + time = 600000 + flags = 0 + data = length 794, hash 8126022C + sample 21: + time = 667000 + flags = 0 + data = length 645, hash F08300E5 + sample 22: + time = 833000 + flags = 0 + data = length 2684, hash 727FE378 + sample 23: + time = 767000 + flags = 0 + data = length 787, hash 419A7821 + sample 24: + time = 733000 + flags = 0 + data = length 649, hash 5C159346 + sample 25: + time = 800000 + flags = 0 + data = length 509, hash F912D655 + sample 26: + time = 967000 + flags = 0 + data = length 1226, hash 29815C21 + sample 27: + time = 900000 + flags = 0 + data = length 898, hash D997AD0A + sample 28: + time = 867000 + flags = 0 + data = length 476, hash A0423645 + sample 29: + time = 933000 + flags = 0 + data = length 486, hash DDF32CBB +track 2: + total output bytes = 12120 + sample count = 29 + format 0: + id = 2 + sampleMimeType = audio/ac3 + channelCount = 1 + sampleRate = 44100 + selectionFlags = 1 + language = und + sample 0: + time = 62000 + flags = 1 + data = length 416, hash 211F2286 + sample 1: + time = 97000 + flags = 1 + data = length 418, hash 77425A86 + sample 2: + time = 131000 + flags = 1 + data = length 418, hash A0FE5CA1 + sample 3: + time = 166000 + flags = 1 + data = length 418, hash 2309B066 + sample 4: + time = 201000 + flags = 1 + data = length 418, hash 928A653B + sample 5: + time = 236000 + flags = 1 + data = length 418, hash 3422F0CB + sample 6: + time = 270000 + flags = 1 + data = length 418, hash EFF43D5B + sample 7: + time = 306000 + flags = 1 + data = length 418, hash FC8093C7 + sample 8: + time = 341000 + flags = 1 + data = length 418, hash CCC08A16 + sample 9: + time = 376000 + flags = 1 + data = length 418, hash 2A6EE863 + sample 10: + time = 410000 + flags = 1 + data = length 418, hash D69A9251 + sample 11: + time = 445000 + flags = 1 + data = length 418, hash BCFB758D + sample 12: + time = 480000 + flags = 1 + data = length 418, hash 11B66799 + sample 13: + time = 514000 + flags = 1 + data = length 418, hash C824D392 + sample 14: + time = 550000 + flags = 1 + data = length 418, hash C167D872 + sample 15: + time = 585000 + flags = 1 + data = length 418, hash 4221C855 + sample 16: + time = 620000 + flags = 1 + data = length 418, hash 4D4FF934 + sample 17: + time = 654000 + flags = 1 + data = length 418, hash 984AA025 + sample 18: + time = 690000 + flags = 1 + data = length 418, hash BB788B46 + sample 19: + time = 724000 + flags = 1 + data = length 418, hash 9EFBFD97 + sample 20: + time = 759000 + flags = 1 + data = length 418, hash DF1A460C + sample 21: + time = 793000 + flags = 1 + data = length 418, hash 2BDB56A + sample 22: + time = 829000 + flags = 1 + data = length 418, hash CA230060 + sample 23: + time = 864000 + flags = 1 + data = length 418, hash D2F19F41 + sample 24: + time = 898000 + flags = 1 + data = length 418, hash AF392D79 + sample 25: + time = 932000 + flags = 1 + data = length 418, hash C5D7F2A3 + sample 26: + time = 968000 + flags = 1 + data = length 418, hash 733A35AE + sample 27: + time = 1002000 + flags = 1 + data = length 418, hash DE46E5D3 + sample 28: + time = 1037000 + flags = 1 + data = length 418, hash 56AB8D37 +track 3: + total output bytes = 59 + sample count = 1 + format 0: + id = 3 + sampleMimeType = application/x-subrip + language = en + label = Subs Label + sample 0: + time = 0 + flags = 1 + data = length 59, hash 1AD38625 +tracksEnded = true diff --git a/testdata/src/test/assets/mkv/sample_with_srt.mkv.2.dump b/testdata/src/test/assets/mkv/sample_with_srt.mkv.2.dump new file mode 100644 index 0000000000..1b6f763b56 --- /dev/null +++ b/testdata/src/test/assets/mkv/sample_with_srt.mkv.2.dump @@ -0,0 +1,280 @@ +seekMap: + isSeekable = true + duration = 1234000 + getPosition(0) = [[timeUs=0, position=1163]] + getPosition(1) = [[timeUs=0, position=1163]] + getPosition(617000) = [[timeUs=0, position=1163]] + getPosition(1234000) = [[timeUs=0, position=1163]] +numberOfTracks = 3 +track 1: + total output bytes = 89502 + sample count = 30 + format 0: + id = 1 + sampleMimeType = video/avc + width = 1080 + height = 720 + selectionFlags = 1 + language = und + initializationData: + data = length 30, hash F6F3D010 + data = length 10, hash 7A0D0F2B + sample 0: + time = 0 + flags = 1 + data = length 36477, hash F0F36CFE + sample 1: + time = 67000 + flags = 0 + data = length 5341, hash 40B85E2 + sample 2: + time = 33000 + flags = 0 + data = length 596, hash 357B4D92 + sample 3: + time = 200000 + flags = 0 + data = length 7704, hash A39EDA06 + sample 4: + time = 133000 + flags = 0 + data = length 989, hash 2813C72D + sample 5: + time = 100000 + flags = 0 + data = length 721, hash C50D1C73 + sample 6: + time = 167000 + flags = 0 + data = length 519, hash 65FE1911 + sample 7: + time = 333000 + flags = 0 + data = length 6160, hash E1CAC0EC + sample 8: + time = 267000 + flags = 0 + data = length 953, hash 7160C661 + sample 9: + time = 233000 + flags = 0 + data = length 620, hash 7A7AE07C + sample 10: + time = 300000 + flags = 0 + data = length 405, hash 5CC7F4E7 + sample 11: + time = 433000 + flags = 0 + data = length 4852, hash 9DB6979D + sample 12: + time = 400000 + flags = 0 + data = length 547, hash E31A6979 + sample 13: + time = 367000 + flags = 0 + data = length 570, hash FEC40D00 + sample 14: + time = 567000 + flags = 0 + data = length 5525, hash 7C478F7E + sample 15: + time = 500000 + flags = 0 + data = length 1082, hash DA07059A + sample 16: + time = 467000 + flags = 0 + data = length 807, hash 93478E6B + sample 17: + time = 533000 + flags = 0 + data = length 744, hash 9A8E6026 + sample 18: + time = 700000 + flags = 0 + data = length 4732, hash C73B23C0 + sample 19: + time = 633000 + flags = 0 + data = length 1004, hash 8A19A228 + sample 20: + time = 600000 + flags = 0 + data = length 794, hash 8126022C + sample 21: + time = 667000 + flags = 0 + data = length 645, hash F08300E5 + sample 22: + time = 833000 + flags = 0 + data = length 2684, hash 727FE378 + sample 23: + time = 767000 + flags = 0 + data = length 787, hash 419A7821 + sample 24: + time = 733000 + flags = 0 + data = length 649, hash 5C159346 + sample 25: + time = 800000 + flags = 0 + data = length 509, hash F912D655 + sample 26: + time = 967000 + flags = 0 + data = length 1226, hash 29815C21 + sample 27: + time = 900000 + flags = 0 + data = length 898, hash D997AD0A + sample 28: + time = 867000 + flags = 0 + data = length 476, hash A0423645 + sample 29: + time = 933000 + flags = 0 + data = length 486, hash DDF32CBB +track 2: + total output bytes = 12120 + sample count = 29 + format 0: + id = 2 + sampleMimeType = audio/ac3 + channelCount = 1 + sampleRate = 44100 + selectionFlags = 1 + language = und + sample 0: + time = 62000 + flags = 1 + data = length 416, hash 211F2286 + sample 1: + time = 97000 + flags = 1 + data = length 418, hash 77425A86 + sample 2: + time = 131000 + flags = 1 + data = length 418, hash A0FE5CA1 + sample 3: + time = 166000 + flags = 1 + data = length 418, hash 2309B066 + sample 4: + time = 201000 + flags = 1 + data = length 418, hash 928A653B + sample 5: + time = 236000 + flags = 1 + data = length 418, hash 3422F0CB + sample 6: + time = 270000 + flags = 1 + data = length 418, hash EFF43D5B + sample 7: + time = 306000 + flags = 1 + data = length 418, hash FC8093C7 + sample 8: + time = 341000 + flags = 1 + data = length 418, hash CCC08A16 + sample 9: + time = 376000 + flags = 1 + data = length 418, hash 2A6EE863 + sample 10: + time = 410000 + flags = 1 + data = length 418, hash D69A9251 + sample 11: + time = 445000 + flags = 1 + data = length 418, hash BCFB758D + sample 12: + time = 480000 + flags = 1 + data = length 418, hash 11B66799 + sample 13: + time = 514000 + flags = 1 + data = length 418, hash C824D392 + sample 14: + time = 550000 + flags = 1 + data = length 418, hash C167D872 + sample 15: + time = 585000 + flags = 1 + data = length 418, hash 4221C855 + sample 16: + time = 620000 + flags = 1 + data = length 418, hash 4D4FF934 + sample 17: + time = 654000 + flags = 1 + data = length 418, hash 984AA025 + sample 18: + time = 690000 + flags = 1 + data = length 418, hash BB788B46 + sample 19: + time = 724000 + flags = 1 + data = length 418, hash 9EFBFD97 + sample 20: + time = 759000 + flags = 1 + data = length 418, hash DF1A460C + sample 21: + time = 793000 + flags = 1 + data = length 418, hash 2BDB56A + sample 22: + time = 829000 + flags = 1 + data = length 418, hash CA230060 + sample 23: + time = 864000 + flags = 1 + data = length 418, hash D2F19F41 + sample 24: + time = 898000 + flags = 1 + data = length 418, hash AF392D79 + sample 25: + time = 932000 + flags = 1 + data = length 418, hash C5D7F2A3 + sample 26: + time = 968000 + flags = 1 + data = length 418, hash 733A35AE + sample 27: + time = 1002000 + flags = 1 + data = length 418, hash DE46E5D3 + sample 28: + time = 1037000 + flags = 1 + data = length 418, hash 56AB8D37 +track 3: + total output bytes = 59 + sample count = 1 + format 0: + id = 3 + sampleMimeType = application/x-subrip + language = en + label = Subs Label + sample 0: + time = 0 + flags = 1 + data = length 59, hash 1AD38625 +tracksEnded = true diff --git a/testdata/src/test/assets/mkv/sample_with_srt.mkv.3.dump b/testdata/src/test/assets/mkv/sample_with_srt.mkv.3.dump new file mode 100644 index 0000000000..1b6f763b56 --- /dev/null +++ b/testdata/src/test/assets/mkv/sample_with_srt.mkv.3.dump @@ -0,0 +1,280 @@ +seekMap: + isSeekable = true + duration = 1234000 + getPosition(0) = [[timeUs=0, position=1163]] + getPosition(1) = [[timeUs=0, position=1163]] + getPosition(617000) = [[timeUs=0, position=1163]] + getPosition(1234000) = [[timeUs=0, position=1163]] +numberOfTracks = 3 +track 1: + total output bytes = 89502 + sample count = 30 + format 0: + id = 1 + sampleMimeType = video/avc + width = 1080 + height = 720 + selectionFlags = 1 + language = und + initializationData: + data = length 30, hash F6F3D010 + data = length 10, hash 7A0D0F2B + sample 0: + time = 0 + flags = 1 + data = length 36477, hash F0F36CFE + sample 1: + time = 67000 + flags = 0 + data = length 5341, hash 40B85E2 + sample 2: + time = 33000 + flags = 0 + data = length 596, hash 357B4D92 + sample 3: + time = 200000 + flags = 0 + data = length 7704, hash A39EDA06 + sample 4: + time = 133000 + flags = 0 + data = length 989, hash 2813C72D + sample 5: + time = 100000 + flags = 0 + data = length 721, hash C50D1C73 + sample 6: + time = 167000 + flags = 0 + data = length 519, hash 65FE1911 + sample 7: + time = 333000 + flags = 0 + data = length 6160, hash E1CAC0EC + sample 8: + time = 267000 + flags = 0 + data = length 953, hash 7160C661 + sample 9: + time = 233000 + flags = 0 + data = length 620, hash 7A7AE07C + sample 10: + time = 300000 + flags = 0 + data = length 405, hash 5CC7F4E7 + sample 11: + time = 433000 + flags = 0 + data = length 4852, hash 9DB6979D + sample 12: + time = 400000 + flags = 0 + data = length 547, hash E31A6979 + sample 13: + time = 367000 + flags = 0 + data = length 570, hash FEC40D00 + sample 14: + time = 567000 + flags = 0 + data = length 5525, hash 7C478F7E + sample 15: + time = 500000 + flags = 0 + data = length 1082, hash DA07059A + sample 16: + time = 467000 + flags = 0 + data = length 807, hash 93478E6B + sample 17: + time = 533000 + flags = 0 + data = length 744, hash 9A8E6026 + sample 18: + time = 700000 + flags = 0 + data = length 4732, hash C73B23C0 + sample 19: + time = 633000 + flags = 0 + data = length 1004, hash 8A19A228 + sample 20: + time = 600000 + flags = 0 + data = length 794, hash 8126022C + sample 21: + time = 667000 + flags = 0 + data = length 645, hash F08300E5 + sample 22: + time = 833000 + flags = 0 + data = length 2684, hash 727FE378 + sample 23: + time = 767000 + flags = 0 + data = length 787, hash 419A7821 + sample 24: + time = 733000 + flags = 0 + data = length 649, hash 5C159346 + sample 25: + time = 800000 + flags = 0 + data = length 509, hash F912D655 + sample 26: + time = 967000 + flags = 0 + data = length 1226, hash 29815C21 + sample 27: + time = 900000 + flags = 0 + data = length 898, hash D997AD0A + sample 28: + time = 867000 + flags = 0 + data = length 476, hash A0423645 + sample 29: + time = 933000 + flags = 0 + data = length 486, hash DDF32CBB +track 2: + total output bytes = 12120 + sample count = 29 + format 0: + id = 2 + sampleMimeType = audio/ac3 + channelCount = 1 + sampleRate = 44100 + selectionFlags = 1 + language = und + sample 0: + time = 62000 + flags = 1 + data = length 416, hash 211F2286 + sample 1: + time = 97000 + flags = 1 + data = length 418, hash 77425A86 + sample 2: + time = 131000 + flags = 1 + data = length 418, hash A0FE5CA1 + sample 3: + time = 166000 + flags = 1 + data = length 418, hash 2309B066 + sample 4: + time = 201000 + flags = 1 + data = length 418, hash 928A653B + sample 5: + time = 236000 + flags = 1 + data = length 418, hash 3422F0CB + sample 6: + time = 270000 + flags = 1 + data = length 418, hash EFF43D5B + sample 7: + time = 306000 + flags = 1 + data = length 418, hash FC8093C7 + sample 8: + time = 341000 + flags = 1 + data = length 418, hash CCC08A16 + sample 9: + time = 376000 + flags = 1 + data = length 418, hash 2A6EE863 + sample 10: + time = 410000 + flags = 1 + data = length 418, hash D69A9251 + sample 11: + time = 445000 + flags = 1 + data = length 418, hash BCFB758D + sample 12: + time = 480000 + flags = 1 + data = length 418, hash 11B66799 + sample 13: + time = 514000 + flags = 1 + data = length 418, hash C824D392 + sample 14: + time = 550000 + flags = 1 + data = length 418, hash C167D872 + sample 15: + time = 585000 + flags = 1 + data = length 418, hash 4221C855 + sample 16: + time = 620000 + flags = 1 + data = length 418, hash 4D4FF934 + sample 17: + time = 654000 + flags = 1 + data = length 418, hash 984AA025 + sample 18: + time = 690000 + flags = 1 + data = length 418, hash BB788B46 + sample 19: + time = 724000 + flags = 1 + data = length 418, hash 9EFBFD97 + sample 20: + time = 759000 + flags = 1 + data = length 418, hash DF1A460C + sample 21: + time = 793000 + flags = 1 + data = length 418, hash 2BDB56A + sample 22: + time = 829000 + flags = 1 + data = length 418, hash CA230060 + sample 23: + time = 864000 + flags = 1 + data = length 418, hash D2F19F41 + sample 24: + time = 898000 + flags = 1 + data = length 418, hash AF392D79 + sample 25: + time = 932000 + flags = 1 + data = length 418, hash C5D7F2A3 + sample 26: + time = 968000 + flags = 1 + data = length 418, hash 733A35AE + sample 27: + time = 1002000 + flags = 1 + data = length 418, hash DE46E5D3 + sample 28: + time = 1037000 + flags = 1 + data = length 418, hash 56AB8D37 +track 3: + total output bytes = 59 + sample count = 1 + format 0: + id = 3 + sampleMimeType = application/x-subrip + language = en + label = Subs Label + sample 0: + time = 0 + flags = 1 + data = length 59, hash 1AD38625 +tracksEnded = true diff --git a/testdata/src/test/assets/mkv/sample_with_srt.mkv.unknown_length.dump b/testdata/src/test/assets/mkv/sample_with_srt.mkv.unknown_length.dump new file mode 100644 index 0000000000..1b6f763b56 --- /dev/null +++ b/testdata/src/test/assets/mkv/sample_with_srt.mkv.unknown_length.dump @@ -0,0 +1,280 @@ +seekMap: + isSeekable = true + duration = 1234000 + getPosition(0) = [[timeUs=0, position=1163]] + getPosition(1) = [[timeUs=0, position=1163]] + getPosition(617000) = [[timeUs=0, position=1163]] + getPosition(1234000) = [[timeUs=0, position=1163]] +numberOfTracks = 3 +track 1: + total output bytes = 89502 + sample count = 30 + format 0: + id = 1 + sampleMimeType = video/avc + width = 1080 + height = 720 + selectionFlags = 1 + language = und + initializationData: + data = length 30, hash F6F3D010 + data = length 10, hash 7A0D0F2B + sample 0: + time = 0 + flags = 1 + data = length 36477, hash F0F36CFE + sample 1: + time = 67000 + flags = 0 + data = length 5341, hash 40B85E2 + sample 2: + time = 33000 + flags = 0 + data = length 596, hash 357B4D92 + sample 3: + time = 200000 + flags = 0 + data = length 7704, hash A39EDA06 + sample 4: + time = 133000 + flags = 0 + data = length 989, hash 2813C72D + sample 5: + time = 100000 + flags = 0 + data = length 721, hash C50D1C73 + sample 6: + time = 167000 + flags = 0 + data = length 519, hash 65FE1911 + sample 7: + time = 333000 + flags = 0 + data = length 6160, hash E1CAC0EC + sample 8: + time = 267000 + flags = 0 + data = length 953, hash 7160C661 + sample 9: + time = 233000 + flags = 0 + data = length 620, hash 7A7AE07C + sample 10: + time = 300000 + flags = 0 + data = length 405, hash 5CC7F4E7 + sample 11: + time = 433000 + flags = 0 + data = length 4852, hash 9DB6979D + sample 12: + time = 400000 + flags = 0 + data = length 547, hash E31A6979 + sample 13: + time = 367000 + flags = 0 + data = length 570, hash FEC40D00 + sample 14: + time = 567000 + flags = 0 + data = length 5525, hash 7C478F7E + sample 15: + time = 500000 + flags = 0 + data = length 1082, hash DA07059A + sample 16: + time = 467000 + flags = 0 + data = length 807, hash 93478E6B + sample 17: + time = 533000 + flags = 0 + data = length 744, hash 9A8E6026 + sample 18: + time = 700000 + flags = 0 + data = length 4732, hash C73B23C0 + sample 19: + time = 633000 + flags = 0 + data = length 1004, hash 8A19A228 + sample 20: + time = 600000 + flags = 0 + data = length 794, hash 8126022C + sample 21: + time = 667000 + flags = 0 + data = length 645, hash F08300E5 + sample 22: + time = 833000 + flags = 0 + data = length 2684, hash 727FE378 + sample 23: + time = 767000 + flags = 0 + data = length 787, hash 419A7821 + sample 24: + time = 733000 + flags = 0 + data = length 649, hash 5C159346 + sample 25: + time = 800000 + flags = 0 + data = length 509, hash F912D655 + sample 26: + time = 967000 + flags = 0 + data = length 1226, hash 29815C21 + sample 27: + time = 900000 + flags = 0 + data = length 898, hash D997AD0A + sample 28: + time = 867000 + flags = 0 + data = length 476, hash A0423645 + sample 29: + time = 933000 + flags = 0 + data = length 486, hash DDF32CBB +track 2: + total output bytes = 12120 + sample count = 29 + format 0: + id = 2 + sampleMimeType = audio/ac3 + channelCount = 1 + sampleRate = 44100 + selectionFlags = 1 + language = und + sample 0: + time = 62000 + flags = 1 + data = length 416, hash 211F2286 + sample 1: + time = 97000 + flags = 1 + data = length 418, hash 77425A86 + sample 2: + time = 131000 + flags = 1 + data = length 418, hash A0FE5CA1 + sample 3: + time = 166000 + flags = 1 + data = length 418, hash 2309B066 + sample 4: + time = 201000 + flags = 1 + data = length 418, hash 928A653B + sample 5: + time = 236000 + flags = 1 + data = length 418, hash 3422F0CB + sample 6: + time = 270000 + flags = 1 + data = length 418, hash EFF43D5B + sample 7: + time = 306000 + flags = 1 + data = length 418, hash FC8093C7 + sample 8: + time = 341000 + flags = 1 + data = length 418, hash CCC08A16 + sample 9: + time = 376000 + flags = 1 + data = length 418, hash 2A6EE863 + sample 10: + time = 410000 + flags = 1 + data = length 418, hash D69A9251 + sample 11: + time = 445000 + flags = 1 + data = length 418, hash BCFB758D + sample 12: + time = 480000 + flags = 1 + data = length 418, hash 11B66799 + sample 13: + time = 514000 + flags = 1 + data = length 418, hash C824D392 + sample 14: + time = 550000 + flags = 1 + data = length 418, hash C167D872 + sample 15: + time = 585000 + flags = 1 + data = length 418, hash 4221C855 + sample 16: + time = 620000 + flags = 1 + data = length 418, hash 4D4FF934 + sample 17: + time = 654000 + flags = 1 + data = length 418, hash 984AA025 + sample 18: + time = 690000 + flags = 1 + data = length 418, hash BB788B46 + sample 19: + time = 724000 + flags = 1 + data = length 418, hash 9EFBFD97 + sample 20: + time = 759000 + flags = 1 + data = length 418, hash DF1A460C + sample 21: + time = 793000 + flags = 1 + data = length 418, hash 2BDB56A + sample 22: + time = 829000 + flags = 1 + data = length 418, hash CA230060 + sample 23: + time = 864000 + flags = 1 + data = length 418, hash D2F19F41 + sample 24: + time = 898000 + flags = 1 + data = length 418, hash AF392D79 + sample 25: + time = 932000 + flags = 1 + data = length 418, hash C5D7F2A3 + sample 26: + time = 968000 + flags = 1 + data = length 418, hash 733A35AE + sample 27: + time = 1002000 + flags = 1 + data = length 418, hash DE46E5D3 + sample 28: + time = 1037000 + flags = 1 + data = length 418, hash 56AB8D37 +track 3: + total output bytes = 59 + sample count = 1 + format 0: + id = 3 + sampleMimeType = application/x-subrip + language = en + label = Subs Label + sample 0: + time = 0 + flags = 1 + data = length 59, hash 1AD38625 +tracksEnded = true diff --git a/library/core/src/test/assets/mkv/subsample_encrypted_altref.webm b/testdata/src/test/assets/mkv/subsample_encrypted_altref.webm similarity index 100% rename from library/core/src/test/assets/mkv/subsample_encrypted_altref.webm rename to testdata/src/test/assets/mkv/subsample_encrypted_altref.webm diff --git a/library/core/src/test/assets/mkv/subsample_encrypted_altref.webm.0.dump b/testdata/src/test/assets/mkv/subsample_encrypted_altref.webm.0.dump similarity index 54% rename from library/core/src/test/assets/mkv/subsample_encrypted_altref.webm.0.dump rename to testdata/src/test/assets/mkv/subsample_encrypted_altref.webm.0.dump index 89a7514784..303654d721 100644 --- a/library/core/src/test/assets/mkv/subsample_encrypted_altref.webm.0.dump +++ b/testdata/src/test/assets/mkv/subsample_encrypted_altref.webm.0.dump @@ -4,29 +4,16 @@ seekMap: getPosition(0) = [[timeUs=0, position=0]] numberOfTracks = 1 track 1: - format: - bitrate = -1 - id = 1 - containerMimeType = null - sampleMimeType = video/x-vnd.on2.vp9 - maxInputSize = -1 - width = 360 - height = 240 - 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 = 1305012705 - initializationData: total output bytes = 39 sample count = 1 + format 0: + id = 1 + sampleMimeType = video/x-vnd.on2.vp9 + width = 360 + height = 240 + selectionFlags = 1 + language = en + drmInitData = 1305012705 sample 0: time = 0 flags = 1073741824 diff --git a/testdata/src/test/assets/mkv/subsample_encrypted_altref.webm.unknown_length.dump b/testdata/src/test/assets/mkv/subsample_encrypted_altref.webm.unknown_length.dump new file mode 100644 index 0000000000..303654d721 --- /dev/null +++ b/testdata/src/test/assets/mkv/subsample_encrypted_altref.webm.unknown_length.dump @@ -0,0 +1,23 @@ +seekMap: + isSeekable = false + duration = 1000 + getPosition(0) = [[timeUs=0, position=0]] +numberOfTracks = 1 +track 1: + total output bytes = 39 + sample count = 1 + format 0: + id = 1 + sampleMimeType = video/x-vnd.on2.vp9 + width = 360 + height = 240 + selectionFlags = 1 + language = en + drmInitData = 1305012705 + sample 0: + time = 0 + flags = 1073741824 + data = length 39, hash B7FE77F4 + crypto mode = 1 + encryption key = length 16, hash 4CE944CF +tracksEnded = true diff --git a/library/core/src/test/assets/mkv/subsample_encrypted_noaltref.webm b/testdata/src/test/assets/mkv/subsample_encrypted_noaltref.webm similarity index 100% rename from library/core/src/test/assets/mkv/subsample_encrypted_noaltref.webm rename to testdata/src/test/assets/mkv/subsample_encrypted_noaltref.webm diff --git a/library/core/src/test/assets/mkv/subsample_encrypted_noaltref.webm.0.dump b/testdata/src/test/assets/mkv/subsample_encrypted_noaltref.webm.0.dump similarity index 54% rename from library/core/src/test/assets/mkv/subsample_encrypted_noaltref.webm.0.dump rename to testdata/src/test/assets/mkv/subsample_encrypted_noaltref.webm.0.dump index 1caa3f9f27..af5a5af9a0 100644 --- a/library/core/src/test/assets/mkv/subsample_encrypted_noaltref.webm.0.dump +++ b/testdata/src/test/assets/mkv/subsample_encrypted_noaltref.webm.0.dump @@ -4,29 +4,16 @@ seekMap: getPosition(0) = [[timeUs=0, position=0]] numberOfTracks = 1 track 1: - format: - bitrate = -1 - id = 1 - containerMimeType = null - sampleMimeType = video/x-vnd.on2.vp9 - maxInputSize = -1 - width = 360 - height = 240 - 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 = 1305012705 - initializationData: total output bytes = 24 sample count = 1 + format 0: + id = 1 + sampleMimeType = video/x-vnd.on2.vp9 + width = 360 + height = 240 + selectionFlags = 1 + language = en + drmInitData = 1305012705 sample 0: time = 0 flags = 1073741824 diff --git a/testdata/src/test/assets/mkv/subsample_encrypted_noaltref.webm.unknown_length.dump b/testdata/src/test/assets/mkv/subsample_encrypted_noaltref.webm.unknown_length.dump new file mode 100644 index 0000000000..af5a5af9a0 --- /dev/null +++ b/testdata/src/test/assets/mkv/subsample_encrypted_noaltref.webm.unknown_length.dump @@ -0,0 +1,23 @@ +seekMap: + isSeekable = false + duration = 1000 + getPosition(0) = [[timeUs=0, position=0]] +numberOfTracks = 1 +track 1: + total output bytes = 24 + sample count = 1 + format 0: + id = 1 + sampleMimeType = video/x-vnd.on2.vp9 + width = 360 + height = 240 + selectionFlags = 1 + language = en + drmInitData = 1305012705 + sample 0: + time = 0 + flags = 1073741824 + data = length 24, hash E58668B1 + crypto mode = 1 + encryption key = length 16, hash 4CE944CF +tracksEnded = true diff --git a/library/core/src/androidTest/assets/binary/1024_incrementing_bytes.mp3 b/testdata/src/test/assets/mp3/1024_incrementing_bytes.mp3 similarity index 100% rename from library/core/src/androidTest/assets/binary/1024_incrementing_bytes.mp3 rename to testdata/src/test/assets/mp3/1024_incrementing_bytes.mp3 diff --git a/library/core/src/test/assets/mp3/bear.mp3 b/testdata/src/test/assets/mp3/bear-cbr-constant-frame-size-no-seek-table.mp3 similarity index 98% rename from library/core/src/test/assets/mp3/bear.mp3 rename to testdata/src/test/assets/mp3/bear-cbr-constant-frame-size-no-seek-table.mp3 index 0c1001ce39..ce318ca308 100644 Binary files a/library/core/src/test/assets/mp3/bear.mp3 and b/testdata/src/test/assets/mp3/bear-cbr-constant-frame-size-no-seek-table.mp3 differ diff --git a/testdata/src/test/assets/mp3/bear-cbr-variable-frame-size-no-seek-table.mp3 b/testdata/src/test/assets/mp3/bear-cbr-variable-frame-size-no-seek-table.mp3 new file mode 100644 index 0000000000..a329a49795 Binary files /dev/null and b/testdata/src/test/assets/mp3/bear-cbr-variable-frame-size-no-seek-table.mp3 differ diff --git a/testdata/src/test/assets/mp3/bear-cbr-variable-frame-size-no-seek-table.mp3.0.dump b/testdata/src/test/assets/mp3/bear-cbr-variable-frame-size-no-seek-table.mp3.0.dump new file mode 100644 index 0000000000..ba8b3d56bf --- /dev/null +++ b/testdata/src/test/assets/mp3/bear-cbr-variable-frame-size-no-seek-table.mp3.0.dump @@ -0,0 +1,449 @@ +seekMap: + isSeekable = true + duration = 2821187 + getPosition(0) = [[timeUs=0, position=240]] + getPosition(1) = [[timeUs=0, position=240], [timeUs=26062, position=657]] + getPosition(1410593) = [[timeUs=1407375, position=22758], [timeUs=1433437, position=23175]] + getPosition(2821187) = [[timeUs=2795125, position=44962]] +numberOfTracks = 1 +track 0: + total output bytes = 45139 + sample count = 108 + format 0: + sampleMimeType = audio/mpeg + maxInputSize = 4096 + channelCount = 2 + sampleRate = 44100 + sample 0: + time = 0 + flags = 1 + data = length 417, hash C4565176 + sample 1: + time = 26122 + flags = 1 + data = length 418, hash 70AEC448 + sample 2: + time = 52244 + flags = 1 + data = length 418, hash 835A8FB9 + sample 3: + time = 78367 + flags = 1 + data = length 418, hash 3A9672BF + sample 4: + time = 104489 + flags = 1 + data = length 418, hash 8DBE60F9 + sample 5: + time = 130612 + flags = 1 + data = length 418, hash 23D0867B + sample 6: + time = 156734 + flags = 1 + data = length 418, hash 7780AAB9 + sample 7: + time = 182857 + flags = 1 + data = length 418, hash 3F63B2D1 + sample 8: + time = 208979 + flags = 1 + data = length 418, hash 7A33CEBD + sample 9: + time = 235102 + flags = 1 + data = length 418, hash DF31D514 + sample 10: + time = 261224 + flags = 1 + data = length 418, hash 26FA2C86 + sample 11: + time = 287346 + flags = 1 + data = length 418, hash D9C7FB1 + sample 12: + time = 313469 + flags = 1 + data = length 418, hash B1C40DC8 + sample 13: + time = 339591 + flags = 1 + data = length 418, hash 1C953BEE + sample 14: + time = 365714 + flags = 1 + data = length 418, hash A6053C6 + sample 15: + time = 391836 + flags = 1 + data = length 418, hash 2D90325A + sample 16: + time = 417959 + flags = 1 + data = length 418, hash 11A84918 + sample 17: + time = 444081 + flags = 1 + data = length 418, hash 30F1A19A + sample 18: + time = 470204 + flags = 1 + data = length 418, hash 70EC67FF + sample 19: + time = 496326 + flags = 1 + data = length 418, hash 7BAF5828 + sample 20: + time = 522448 + flags = 1 + data = length 418, hash 8E43B85E + sample 21: + time = 548571 + flags = 1 + data = length 418, hash E9A5EE78 + sample 22: + time = 574693 + flags = 1 + data = length 418, hash F79931F8 + sample 23: + time = 600816 + flags = 1 + data = length 418, hash C0308B40 + sample 24: + time = 626938 + flags = 1 + data = length 418, hash 3D2E55B + sample 25: + time = 653061 + flags = 1 + data = length 417, hash D74A61AF + sample 26: + time = 679183 + flags = 1 + data = length 418, hash 96F104B1 + sample 27: + time = 705306 + flags = 1 + data = length 418, hash CE12216 + sample 28: + time = 731428 + flags = 1 + data = length 418, hash 899EA46D + sample 29: + time = 757551 + flags = 1 + data = length 418, hash 1208BBC5 + sample 30: + time = 783673 + flags = 1 + data = length 418, hash 49F22D4D + sample 31: + time = 809795 + flags = 1 + data = length 418, hash 56D959B0 + sample 32: + time = 835918 + flags = 1 + data = length 418, hash 5EC6FF8C + sample 33: + time = 862040 + flags = 1 + data = length 418, hash 380B6E00 + sample 34: + time = 888163 + flags = 1 + data = length 418, hash 19494E6B + sample 35: + time = 914285 + flags = 1 + data = length 418, hash C751B033 + sample 36: + time = 940408 + flags = 1 + data = length 418, hash 5F7C6DBA + sample 37: + time = 966530 + flags = 1 + data = length 418, hash D77E6530 + sample 38: + time = 992653 + flags = 1 + data = length 418, hash 48A694AB + sample 39: + time = 1018775 + flags = 1 + data = length 418, hash A979850E + sample 40: + time = 1044897 + flags = 1 + data = length 418, hash 7688E4B1 + sample 41: + time = 1071020 + flags = 1 + data = length 418, hash 255AF933 + sample 42: + time = 1097142 + flags = 1 + data = length 418, hash D58AC838 + sample 43: + time = 1123265 + flags = 1 + data = length 418, hash A38DC7B + sample 44: + time = 1149387 + flags = 1 + data = length 418, hash EA0CA21 + sample 45: + time = 1175510 + flags = 1 + data = length 418, hash DF99B54B + sample 46: + time = 1201632 + flags = 1 + data = length 418, hash A1532134 + sample 47: + time = 1227755 + flags = 1 + data = length 418, hash 520EC187 + sample 48: + time = 1253877 + flags = 1 + data = length 418, hash 5E38E4F + sample 49: + time = 1280000 + flags = 1 + data = length 417, hash 4D3526FB + sample 50: + time = 1306122 + flags = 1 + data = length 418, hash D99092CA + sample 51: + time = 1332244 + flags = 1 + data = length 418, hash EDB10D8E + sample 52: + time = 1358367 + flags = 1 + data = length 418, hash 5B5F6439 + sample 53: + time = 1384489 + flags = 1 + data = length 418, hash 947E2739 + sample 54: + time = 1410612 + flags = 1 + data = length 418, hash 8C1FF29C + sample 55: + time = 1436734 + flags = 1 + data = length 418, hash FEADC9C3 + sample 56: + time = 1462857 + flags = 1 + data = length 418, hash BB82E0C8 + sample 57: + time = 1488979 + flags = 1 + data = length 418, hash 8D1494AF + sample 58: + time = 1515102 + flags = 1 + data = length 418, hash E8C4265C + sample 59: + time = 1541224 + flags = 1 + data = length 418, hash BC8F59AE + sample 60: + time = 1567346 + flags = 1 + data = length 418, hash C8C5DCBD + sample 61: + time = 1593469 + flags = 1 + data = length 418, hash 43C3D85B + sample 62: + time = 1619591 + flags = 1 + data = length 418, hash 238C1AFE + sample 63: + time = 1645714 + flags = 1 + data = length 418, hash F6099191 + sample 64: + time = 1671836 + flags = 1 + data = length 418, hash D236BB0E + sample 65: + time = 1697959 + flags = 1 + data = length 418, hash 58B5B714 + sample 66: + time = 1724081 + flags = 1 + data = length 418, hash A9DDDD52 + sample 67: + time = 1750204 + flags = 1 + data = length 418, hash 85E7D11E + sample 68: + time = 1776326 + flags = 1 + data = length 418, hash 9E9D8FF4 + sample 69: + time = 1802448 + flags = 1 + data = length 418, hash 6FF9060D + sample 70: + time = 1828571 + flags = 1 + data = length 418, hash 4F1FC4F5 + sample 71: + time = 1854693 + flags = 1 + data = length 418, hash EF9885AA + sample 72: + time = 1880816 + flags = 1 + data = length 418, hash 7872C242 + sample 73: + time = 1906938 + flags = 1 + data = length 418, hash EB6FEAED + sample 74: + time = 1933061 + flags = 1 + data = length 417, hash B02D8CF0 + sample 75: + time = 1959183 + flags = 1 + data = length 418, hash EFB6C2DD + sample 76: + time = 1985306 + flags = 1 + data = length 418, hash B733E449 + sample 77: + time = 2011428 + flags = 1 + data = length 418, hash 617B155E + sample 78: + time = 2037551 + flags = 1 + data = length 418, hash AE626B2E + sample 79: + time = 2063673 + flags = 1 + data = length 418, hash F5E232C + sample 80: + time = 2089795 + flags = 1 + data = length 418, hash B5F4DC29 + sample 81: + time = 2115918 + flags = 1 + data = length 418, hash C791E3B5 + sample 82: + time = 2142040 + flags = 1 + data = length 418, hash F42A6BDB + sample 83: + time = 2168163 + flags = 1 + data = length 418, hash FDAEEFE6 + sample 84: + time = 2194285 + flags = 1 + data = length 418, hash 62AC2513 + sample 85: + time = 2220408 + flags = 1 + data = length 418, hash A4B46783 + sample 86: + time = 2246530 + flags = 1 + data = length 418, hash 9B7DFEFE + sample 87: + time = 2272653 + flags = 1 + data = length 418, hash 4010F89A + sample 88: + time = 2298775 + flags = 1 + data = length 418, hash 33467FC1 + sample 89: + time = 2324897 + flags = 1 + data = length 418, hash 1DFAE1E9 + sample 90: + time = 2351020 + flags = 1 + data = length 418, hash C208D375 + sample 91: + time = 2377142 + flags = 1 + data = length 418, hash CD430C30 + sample 92: + time = 2403265 + flags = 1 + data = length 418, hash 5A6F8065 + sample 93: + time = 2429387 + flags = 1 + data = length 418, hash 7177BD8B + sample 94: + time = 2455510 + flags = 1 + data = length 418, hash 51C1F29B + sample 95: + time = 2481632 + flags = 1 + data = length 418, hash 868A0084 + sample 96: + time = 2507755 + flags = 1 + data = length 418, hash 1E9C03E1 + sample 97: + time = 2533877 + flags = 1 + data = length 418, hash 10069B68 + sample 98: + time = 2560000 + flags = 1 + data = length 417, hash CC5B751D + sample 99: + time = 2586122 + flags = 1 + data = length 418, hash 837D650 + sample 100: + time = 2612244 + flags = 1 + data = length 418, hash 43B75632 + sample 101: + time = 2638367 + flags = 1 + data = length 418, hash 86E0652 + sample 102: + time = 2664489 + flags = 1 + data = length 418, hash 4DEC63E7 + sample 103: + time = 2690612 + flags = 1 + data = length 418, hash F094F330 + sample 104: + time = 2716734 + flags = 1 + data = length 418, hash 2C9CAA4 + sample 105: + time = 2742857 + flags = 1 + data = length 418, hash 1E903FFE + sample 106: + time = 2768979 + flags = 1 + data = length 418, hash F276CF72 + sample 107: + time = 2795102 + flags = 1 + data = length 418, hash 1C081463 +tracksEnded = true diff --git a/testdata/src/test/assets/mp3/bear-cbr-variable-frame-size-no-seek-table.mp3.1.dump b/testdata/src/test/assets/mp3/bear-cbr-variable-frame-size-no-seek-table.mp3.1.dump new file mode 100644 index 0000000000..723354f768 --- /dev/null +++ b/testdata/src/test/assets/mp3/bear-cbr-variable-frame-size-no-seek-table.mp3.1.dump @@ -0,0 +1,305 @@ +seekMap: + isSeekable = true + duration = 2821187 + getPosition(0) = [[timeUs=0, position=240]] + getPosition(1) = [[timeUs=0, position=240], [timeUs=26062, position=657]] + getPosition(1410593) = [[timeUs=1407375, position=22758], [timeUs=1433437, position=23175]] + getPosition(2821187) = [[timeUs=2795125, position=44962]] +numberOfTracks = 1 +track 0: + total output bytes = 30093 + sample count = 72 + format 0: + sampleMimeType = audio/mpeg + maxInputSize = 4096 + channelCount = 2 + sampleRate = 44100 + sample 0: + time = 940375 + flags = 1 + data = length 418, hash 5F7C6DBA + sample 1: + time = 966497 + flags = 1 + data = length 418, hash D77E6530 + sample 2: + time = 992619 + flags = 1 + data = length 418, hash 48A694AB + sample 3: + time = 1018742 + flags = 1 + data = length 418, hash A979850E + sample 4: + time = 1044864 + flags = 1 + data = length 418, hash 7688E4B1 + sample 5: + time = 1070987 + flags = 1 + data = length 418, hash 255AF933 + sample 6: + time = 1097109 + flags = 1 + data = length 418, hash D58AC838 + sample 7: + time = 1123232 + flags = 1 + data = length 418, hash A38DC7B + sample 8: + time = 1149354 + flags = 1 + data = length 418, hash EA0CA21 + sample 9: + time = 1175477 + flags = 1 + data = length 418, hash DF99B54B + sample 10: + time = 1201599 + flags = 1 + data = length 418, hash A1532134 + sample 11: + time = 1227721 + flags = 1 + data = length 418, hash 520EC187 + sample 12: + time = 1253844 + flags = 1 + data = length 418, hash 5E38E4F + sample 13: + time = 1279966 + flags = 1 + data = length 417, hash 4D3526FB + sample 14: + time = 1306089 + flags = 1 + data = length 418, hash D99092CA + sample 15: + time = 1332211 + flags = 1 + data = length 418, hash EDB10D8E + sample 16: + time = 1358334 + flags = 1 + data = length 418, hash 5B5F6439 + sample 17: + time = 1384456 + flags = 1 + data = length 418, hash 947E2739 + sample 18: + time = 1410579 + flags = 1 + data = length 418, hash 8C1FF29C + sample 19: + time = 1436701 + flags = 1 + data = length 418, hash FEADC9C3 + sample 20: + time = 1462823 + flags = 1 + data = length 418, hash BB82E0C8 + sample 21: + time = 1488946 + flags = 1 + data = length 418, hash 8D1494AF + sample 22: + time = 1515068 + flags = 1 + data = length 418, hash E8C4265C + sample 23: + time = 1541191 + flags = 1 + data = length 418, hash BC8F59AE + sample 24: + time = 1567313 + flags = 1 + data = length 418, hash C8C5DCBD + sample 25: + time = 1593436 + flags = 1 + data = length 418, hash 43C3D85B + sample 26: + time = 1619558 + flags = 1 + data = length 418, hash 238C1AFE + sample 27: + time = 1645681 + flags = 1 + data = length 418, hash F6099191 + sample 28: + time = 1671803 + flags = 1 + data = length 418, hash D236BB0E + sample 29: + time = 1697926 + flags = 1 + data = length 418, hash 58B5B714 + sample 30: + time = 1724048 + flags = 1 + data = length 418, hash A9DDDD52 + sample 31: + time = 1750170 + flags = 1 + data = length 418, hash 85E7D11E + sample 32: + time = 1776293 + flags = 1 + data = length 418, hash 9E9D8FF4 + sample 33: + time = 1802415 + flags = 1 + data = length 418, hash 6FF9060D + sample 34: + time = 1828538 + flags = 1 + data = length 418, hash 4F1FC4F5 + sample 35: + time = 1854660 + flags = 1 + data = length 418, hash EF9885AA + sample 36: + time = 1880783 + flags = 1 + data = length 418, hash 7872C242 + sample 37: + time = 1906905 + flags = 1 + data = length 418, hash EB6FEAED + sample 38: + time = 1933028 + flags = 1 + data = length 417, hash B02D8CF0 + sample 39: + time = 1959150 + flags = 1 + data = length 418, hash EFB6C2DD + sample 40: + time = 1985272 + flags = 1 + data = length 418, hash B733E449 + sample 41: + time = 2011395 + flags = 1 + data = length 418, hash 617B155E + sample 42: + time = 2037517 + flags = 1 + data = length 418, hash AE626B2E + sample 43: + time = 2063640 + flags = 1 + data = length 418, hash F5E232C + sample 44: + time = 2089762 + flags = 1 + data = length 418, hash B5F4DC29 + sample 45: + time = 2115885 + flags = 1 + data = length 418, hash C791E3B5 + sample 46: + time = 2142007 + flags = 1 + data = length 418, hash F42A6BDB + sample 47: + time = 2168130 + flags = 1 + data = length 418, hash FDAEEFE6 + sample 48: + time = 2194252 + flags = 1 + data = length 418, hash 62AC2513 + sample 49: + time = 2220375 + flags = 1 + data = length 418, hash A4B46783 + sample 50: + time = 2246497 + flags = 1 + data = length 418, hash 9B7DFEFE + sample 51: + time = 2272619 + flags = 1 + data = length 418, hash 4010F89A + sample 52: + time = 2298742 + flags = 1 + data = length 418, hash 33467FC1 + sample 53: + time = 2324864 + flags = 1 + data = length 418, hash 1DFAE1E9 + sample 54: + time = 2350987 + flags = 1 + data = length 418, hash C208D375 + sample 55: + time = 2377109 + flags = 1 + data = length 418, hash CD430C30 + sample 56: + time = 2403232 + flags = 1 + data = length 418, hash 5A6F8065 + sample 57: + time = 2429354 + flags = 1 + data = length 418, hash 7177BD8B + sample 58: + time = 2455477 + flags = 1 + data = length 418, hash 51C1F29B + sample 59: + time = 2481599 + flags = 1 + data = length 418, hash 868A0084 + sample 60: + time = 2507721 + flags = 1 + data = length 418, hash 1E9C03E1 + sample 61: + time = 2533844 + flags = 1 + data = length 418, hash 10069B68 + sample 62: + time = 2559966 + flags = 1 + data = length 417, hash CC5B751D + sample 63: + time = 2586089 + flags = 1 + data = length 418, hash 837D650 + sample 64: + time = 2612211 + flags = 1 + data = length 418, hash 43B75632 + sample 65: + time = 2638334 + flags = 1 + data = length 418, hash 86E0652 + sample 66: + time = 2664456 + flags = 1 + data = length 418, hash 4DEC63E7 + sample 67: + time = 2690579 + flags = 1 + data = length 418, hash F094F330 + sample 68: + time = 2716701 + flags = 1 + data = length 418, hash 2C9CAA4 + sample 69: + time = 2742823 + flags = 1 + data = length 418, hash 1E903FFE + sample 70: + time = 2768946 + flags = 1 + data = length 418, hash F276CF72 + sample 71: + time = 2795068 + flags = 1 + data = length 418, hash 1C081463 +tracksEnded = true diff --git a/testdata/src/test/assets/mp3/bear-cbr-variable-frame-size-no-seek-table.mp3.2.dump b/testdata/src/test/assets/mp3/bear-cbr-variable-frame-size-no-seek-table.mp3.2.dump new file mode 100644 index 0000000000..00bf3f6ad4 --- /dev/null +++ b/testdata/src/test/assets/mp3/bear-cbr-variable-frame-size-no-seek-table.mp3.2.dump @@ -0,0 +1,161 @@ +seekMap: + isSeekable = true + duration = 2821187 + getPosition(0) = [[timeUs=0, position=240]] + getPosition(1) = [[timeUs=0, position=240], [timeUs=26062, position=657]] + getPosition(1410593) = [[timeUs=1407375, position=22758], [timeUs=1433437, position=23175]] + getPosition(2821187) = [[timeUs=2795125, position=44962]] +numberOfTracks = 1 +track 0: + total output bytes = 15046 + sample count = 36 + format 0: + sampleMimeType = audio/mpeg + maxInputSize = 4096 + channelCount = 2 + sampleRate = 44100 + sample 0: + time = 1880812 + flags = 1 + data = length 418, hash 7872C242 + sample 1: + time = 1906934 + flags = 1 + data = length 418, hash EB6FEAED + sample 2: + time = 1933056 + flags = 1 + data = length 417, hash B02D8CF0 + sample 3: + time = 1959179 + flags = 1 + data = length 418, hash EFB6C2DD + sample 4: + time = 1985301 + flags = 1 + data = length 418, hash B733E449 + sample 5: + time = 2011424 + flags = 1 + data = length 418, hash 617B155E + sample 6: + time = 2037546 + flags = 1 + data = length 418, hash AE626B2E + sample 7: + time = 2063669 + flags = 1 + data = length 418, hash F5E232C + sample 8: + time = 2089791 + flags = 1 + data = length 418, hash B5F4DC29 + sample 9: + time = 2115914 + flags = 1 + data = length 418, hash C791E3B5 + sample 10: + time = 2142036 + flags = 1 + data = length 418, hash F42A6BDB + sample 11: + time = 2168158 + flags = 1 + data = length 418, hash FDAEEFE6 + sample 12: + time = 2194281 + flags = 1 + data = length 418, hash 62AC2513 + sample 13: + time = 2220403 + flags = 1 + data = length 418, hash A4B46783 + sample 14: + time = 2246526 + flags = 1 + data = length 418, hash 9B7DFEFE + sample 15: + time = 2272648 + flags = 1 + data = length 418, hash 4010F89A + sample 16: + time = 2298771 + flags = 1 + data = length 418, hash 33467FC1 + sample 17: + time = 2324893 + flags = 1 + data = length 418, hash 1DFAE1E9 + sample 18: + time = 2351016 + flags = 1 + data = length 418, hash C208D375 + sample 19: + time = 2377138 + flags = 1 + data = length 418, hash CD430C30 + sample 20: + time = 2403260 + flags = 1 + data = length 418, hash 5A6F8065 + sample 21: + time = 2429383 + flags = 1 + data = length 418, hash 7177BD8B + sample 22: + time = 2455505 + flags = 1 + data = length 418, hash 51C1F29B + sample 23: + time = 2481628 + flags = 1 + data = length 418, hash 868A0084 + sample 24: + time = 2507750 + flags = 1 + data = length 418, hash 1E9C03E1 + sample 25: + time = 2533873 + flags = 1 + data = length 418, hash 10069B68 + sample 26: + time = 2559995 + flags = 1 + data = length 417, hash CC5B751D + sample 27: + time = 2586118 + flags = 1 + data = length 418, hash 837D650 + sample 28: + time = 2612240 + flags = 1 + data = length 418, hash 43B75632 + sample 29: + time = 2638363 + flags = 1 + data = length 418, hash 86E0652 + sample 30: + time = 2664485 + flags = 1 + data = length 418, hash 4DEC63E7 + sample 31: + time = 2690607 + flags = 1 + data = length 418, hash F094F330 + sample 32: + time = 2716730 + flags = 1 + data = length 418, hash 2C9CAA4 + sample 33: + time = 2742852 + flags = 1 + data = length 418, hash 1E903FFE + sample 34: + time = 2768975 + flags = 1 + data = length 418, hash F276CF72 + sample 35: + time = 2795097 + flags = 1 + data = length 418, hash 1C081463 +tracksEnded = true diff --git a/testdata/src/test/assets/mp3/bear-cbr-variable-frame-size-no-seek-table.mp3.3.dump b/testdata/src/test/assets/mp3/bear-cbr-variable-frame-size-no-seek-table.mp3.3.dump new file mode 100644 index 0000000000..231aede4cc --- /dev/null +++ b/testdata/src/test/assets/mp3/bear-cbr-variable-frame-size-no-seek-table.mp3.3.dump @@ -0,0 +1,17 @@ +seekMap: + isSeekable = true + duration = 2821187 + getPosition(0) = [[timeUs=0, position=240]] + getPosition(1) = [[timeUs=0, position=240], [timeUs=26062, position=657]] + getPosition(1410593) = [[timeUs=1407375, position=22758], [timeUs=1433437, position=23175]] + getPosition(2821187) = [[timeUs=2795125, position=44962]] +numberOfTracks = 1 +track 0: + total output bytes = 0 + sample count = 0 + format 0: + sampleMimeType = audio/mpeg + maxInputSize = 4096 + channelCount = 2 + sampleRate = 44100 +tracksEnded = true diff --git a/testdata/src/test/assets/mp3/bear-cbr-variable-frame-size-no-seek-table.mp3.unknown_length.dump b/testdata/src/test/assets/mp3/bear-cbr-variable-frame-size-no-seek-table.mp3.unknown_length.dump new file mode 100644 index 0000000000..bedbf6f497 --- /dev/null +++ b/testdata/src/test/assets/mp3/bear-cbr-variable-frame-size-no-seek-table.mp3.unknown_length.dump @@ -0,0 +1,446 @@ +seekMap: + isSeekable = false + duration = UNSET TIME + getPosition(0) = [[timeUs=0, position=240]] +numberOfTracks = 1 +track 0: + total output bytes = 45139 + sample count = 108 + format 0: + sampleMimeType = audio/mpeg + maxInputSize = 4096 + channelCount = 2 + sampleRate = 44100 + sample 0: + time = 0 + flags = 1 + data = length 417, hash C4565176 + sample 1: + time = 26122 + flags = 1 + data = length 418, hash 70AEC448 + sample 2: + time = 52244 + flags = 1 + data = length 418, hash 835A8FB9 + sample 3: + time = 78367 + flags = 1 + data = length 418, hash 3A9672BF + sample 4: + time = 104489 + flags = 1 + data = length 418, hash 8DBE60F9 + sample 5: + time = 130612 + flags = 1 + data = length 418, hash 23D0867B + sample 6: + time = 156734 + flags = 1 + data = length 418, hash 7780AAB9 + sample 7: + time = 182857 + flags = 1 + data = length 418, hash 3F63B2D1 + sample 8: + time = 208979 + flags = 1 + data = length 418, hash 7A33CEBD + sample 9: + time = 235102 + flags = 1 + data = length 418, hash DF31D514 + sample 10: + time = 261224 + flags = 1 + data = length 418, hash 26FA2C86 + sample 11: + time = 287346 + flags = 1 + data = length 418, hash D9C7FB1 + sample 12: + time = 313469 + flags = 1 + data = length 418, hash B1C40DC8 + sample 13: + time = 339591 + flags = 1 + data = length 418, hash 1C953BEE + sample 14: + time = 365714 + flags = 1 + data = length 418, hash A6053C6 + sample 15: + time = 391836 + flags = 1 + data = length 418, hash 2D90325A + sample 16: + time = 417959 + flags = 1 + data = length 418, hash 11A84918 + sample 17: + time = 444081 + flags = 1 + data = length 418, hash 30F1A19A + sample 18: + time = 470204 + flags = 1 + data = length 418, hash 70EC67FF + sample 19: + time = 496326 + flags = 1 + data = length 418, hash 7BAF5828 + sample 20: + time = 522448 + flags = 1 + data = length 418, hash 8E43B85E + sample 21: + time = 548571 + flags = 1 + data = length 418, hash E9A5EE78 + sample 22: + time = 574693 + flags = 1 + data = length 418, hash F79931F8 + sample 23: + time = 600816 + flags = 1 + data = length 418, hash C0308B40 + sample 24: + time = 626938 + flags = 1 + data = length 418, hash 3D2E55B + sample 25: + time = 653061 + flags = 1 + data = length 417, hash D74A61AF + sample 26: + time = 679183 + flags = 1 + data = length 418, hash 96F104B1 + sample 27: + time = 705306 + flags = 1 + data = length 418, hash CE12216 + sample 28: + time = 731428 + flags = 1 + data = length 418, hash 899EA46D + sample 29: + time = 757551 + flags = 1 + data = length 418, hash 1208BBC5 + sample 30: + time = 783673 + flags = 1 + data = length 418, hash 49F22D4D + sample 31: + time = 809795 + flags = 1 + data = length 418, hash 56D959B0 + sample 32: + time = 835918 + flags = 1 + data = length 418, hash 5EC6FF8C + sample 33: + time = 862040 + flags = 1 + data = length 418, hash 380B6E00 + sample 34: + time = 888163 + flags = 1 + data = length 418, hash 19494E6B + sample 35: + time = 914285 + flags = 1 + data = length 418, hash C751B033 + sample 36: + time = 940408 + flags = 1 + data = length 418, hash 5F7C6DBA + sample 37: + time = 966530 + flags = 1 + data = length 418, hash D77E6530 + sample 38: + time = 992653 + flags = 1 + data = length 418, hash 48A694AB + sample 39: + time = 1018775 + flags = 1 + data = length 418, hash A979850E + sample 40: + time = 1044897 + flags = 1 + data = length 418, hash 7688E4B1 + sample 41: + time = 1071020 + flags = 1 + data = length 418, hash 255AF933 + sample 42: + time = 1097142 + flags = 1 + data = length 418, hash D58AC838 + sample 43: + time = 1123265 + flags = 1 + data = length 418, hash A38DC7B + sample 44: + time = 1149387 + flags = 1 + data = length 418, hash EA0CA21 + sample 45: + time = 1175510 + flags = 1 + data = length 418, hash DF99B54B + sample 46: + time = 1201632 + flags = 1 + data = length 418, hash A1532134 + sample 47: + time = 1227755 + flags = 1 + data = length 418, hash 520EC187 + sample 48: + time = 1253877 + flags = 1 + data = length 418, hash 5E38E4F + sample 49: + time = 1280000 + flags = 1 + data = length 417, hash 4D3526FB + sample 50: + time = 1306122 + flags = 1 + data = length 418, hash D99092CA + sample 51: + time = 1332244 + flags = 1 + data = length 418, hash EDB10D8E + sample 52: + time = 1358367 + flags = 1 + data = length 418, hash 5B5F6439 + sample 53: + time = 1384489 + flags = 1 + data = length 418, hash 947E2739 + sample 54: + time = 1410612 + flags = 1 + data = length 418, hash 8C1FF29C + sample 55: + time = 1436734 + flags = 1 + data = length 418, hash FEADC9C3 + sample 56: + time = 1462857 + flags = 1 + data = length 418, hash BB82E0C8 + sample 57: + time = 1488979 + flags = 1 + data = length 418, hash 8D1494AF + sample 58: + time = 1515102 + flags = 1 + data = length 418, hash E8C4265C + sample 59: + time = 1541224 + flags = 1 + data = length 418, hash BC8F59AE + sample 60: + time = 1567346 + flags = 1 + data = length 418, hash C8C5DCBD + sample 61: + time = 1593469 + flags = 1 + data = length 418, hash 43C3D85B + sample 62: + time = 1619591 + flags = 1 + data = length 418, hash 238C1AFE + sample 63: + time = 1645714 + flags = 1 + data = length 418, hash F6099191 + sample 64: + time = 1671836 + flags = 1 + data = length 418, hash D236BB0E + sample 65: + time = 1697959 + flags = 1 + data = length 418, hash 58B5B714 + sample 66: + time = 1724081 + flags = 1 + data = length 418, hash A9DDDD52 + sample 67: + time = 1750204 + flags = 1 + data = length 418, hash 85E7D11E + sample 68: + time = 1776326 + flags = 1 + data = length 418, hash 9E9D8FF4 + sample 69: + time = 1802448 + flags = 1 + data = length 418, hash 6FF9060D + sample 70: + time = 1828571 + flags = 1 + data = length 418, hash 4F1FC4F5 + sample 71: + time = 1854693 + flags = 1 + data = length 418, hash EF9885AA + sample 72: + time = 1880816 + flags = 1 + data = length 418, hash 7872C242 + sample 73: + time = 1906938 + flags = 1 + data = length 418, hash EB6FEAED + sample 74: + time = 1933061 + flags = 1 + data = length 417, hash B02D8CF0 + sample 75: + time = 1959183 + flags = 1 + data = length 418, hash EFB6C2DD + sample 76: + time = 1985306 + flags = 1 + data = length 418, hash B733E449 + sample 77: + time = 2011428 + flags = 1 + data = length 418, hash 617B155E + sample 78: + time = 2037551 + flags = 1 + data = length 418, hash AE626B2E + sample 79: + time = 2063673 + flags = 1 + data = length 418, hash F5E232C + sample 80: + time = 2089795 + flags = 1 + data = length 418, hash B5F4DC29 + sample 81: + time = 2115918 + flags = 1 + data = length 418, hash C791E3B5 + sample 82: + time = 2142040 + flags = 1 + data = length 418, hash F42A6BDB + sample 83: + time = 2168163 + flags = 1 + data = length 418, hash FDAEEFE6 + sample 84: + time = 2194285 + flags = 1 + data = length 418, hash 62AC2513 + sample 85: + time = 2220408 + flags = 1 + data = length 418, hash A4B46783 + sample 86: + time = 2246530 + flags = 1 + data = length 418, hash 9B7DFEFE + sample 87: + time = 2272653 + flags = 1 + data = length 418, hash 4010F89A + sample 88: + time = 2298775 + flags = 1 + data = length 418, hash 33467FC1 + sample 89: + time = 2324897 + flags = 1 + data = length 418, hash 1DFAE1E9 + sample 90: + time = 2351020 + flags = 1 + data = length 418, hash C208D375 + sample 91: + time = 2377142 + flags = 1 + data = length 418, hash CD430C30 + sample 92: + time = 2403265 + flags = 1 + data = length 418, hash 5A6F8065 + sample 93: + time = 2429387 + flags = 1 + data = length 418, hash 7177BD8B + sample 94: + time = 2455510 + flags = 1 + data = length 418, hash 51C1F29B + sample 95: + time = 2481632 + flags = 1 + data = length 418, hash 868A0084 + sample 96: + time = 2507755 + flags = 1 + data = length 418, hash 1E9C03E1 + sample 97: + time = 2533877 + flags = 1 + data = length 418, hash 10069B68 + sample 98: + time = 2560000 + flags = 1 + data = length 417, hash CC5B751D + sample 99: + time = 2586122 + flags = 1 + data = length 418, hash 837D650 + sample 100: + time = 2612244 + flags = 1 + data = length 418, hash 43B75632 + sample 101: + time = 2638367 + flags = 1 + data = length 418, hash 86E0652 + sample 102: + time = 2664489 + flags = 1 + data = length 418, hash 4DEC63E7 + sample 103: + time = 2690612 + flags = 1 + data = length 418, hash F094F330 + sample 104: + time = 2716734 + flags = 1 + data = length 418, hash 2C9CAA4 + sample 105: + time = 2742857 + flags = 1 + data = length 418, hash 1E903FFE + sample 106: + time = 2768979 + flags = 1 + data = length 418, hash F276CF72 + sample 107: + time = 2795102 + flags = 1 + data = length 418, hash 1C081463 +tracksEnded = true diff --git a/library/core/src/test/assets/mp3/bear.mp3.0.dump b/testdata/src/test/assets/mp3/bear-id3-disabled.0.dump similarity index 53% rename from library/core/src/test/assets/mp3/bear.mp3.0.dump rename to testdata/src/test/assets/mp3/bear-id3-disabled.0.dump index 5c8700fed1..a80bf5e315 100644 --- a/library/core/src/test/assets/mp3/bear.mp3.0.dump +++ b/testdata/src/test/assets/mp3/bear-id3-disabled.0.dump @@ -1,494 +1,487 @@ seekMap: isSeekable = true - duration = 2784000 - getPosition(0) = [[timeUs=0, position=201]] + duration = 2808000 + getPosition(0) = [[timeUs=0, position=39740]] + getPosition(1) = [[timeUs=0, position=39740]] + getPosition(1404000) = [[timeUs=1404000, position=58820]] + getPosition(2808000) = [[timeUs=2808000, position=77900]] numberOfTracks = 1 track 0: - format: - bitrate = -1 - id = null - containerMimeType = null + total output bytes = 38160 + sample count = 117 + format 0: sampleMimeType = audio/mpeg maxInputSize = 4096 - width = -1 - height = -1 - frameRate = -1.0 - rotationDegrees = 0 - pixelWidthHeightRatio = 1.0 channelCount = 2 sampleRate = 48000 - pcmEncoding = -1 - encoderDelay = 956 - encoderPadding = 3352 - subsampleOffsetUs = 9223372036854775807 - selectionFlags = 0 - language = null - drmInitData = - - initializationData: - total output bytes = 44544 - sample count = 116 + encoderDelay = 576 + encoderPadding = 576 sample 0: time = 0 flags = 1 - data = length 384, hash B1FBF8BD + data = length 96, hash 1F161542 sample 1: time = 24000 flags = 1 - data = length 384, hash 2B9A3B72 + data = length 768, hash CD1DC50F sample 2: time = 48000 flags = 1 - data = length 384, hash 33C65BA6 + data = length 336, hash 3F64124B sample 3: time = 72000 flags = 1 - data = length 384, hash E64FE475 + data = length 336, hash 8FFED94E sample 4: time = 96000 flags = 1 - data = length 384, hash E9122D34 + data = length 288, hash 9CD77D47 sample 5: time = 120000 flags = 1 - data = length 384, hash 9CC87327 + data = length 384, hash 24607BB5 sample 6: time = 144000 flags = 1 - data = length 384, hash 118CF6DA + data = length 480, hash 4937EBAB sample 7: time = 168000 flags = 1 - data = length 384, hash 9610D9D6 + data = length 336, hash 546342B1 sample 8: time = 192000 flags = 1 - data = length 384, hash 6ABFE405 + data = length 336, hash 79E0923F sample 9: time = 216000 flags = 1 - data = length 384, hash EE5C93A9 + data = length 336, hash AB1F3948 sample 10: time = 240000 flags = 1 - data = length 384, hash 44E0D140 + data = length 336, hash C3A4D888 sample 11: time = 264000 flags = 1 - data = length 384, hash 3B3DE1D6 + data = length 288, hash 7867DA45 sample 12: time = 288000 flags = 1 - data = length 384, hash 3A572E7C + data = length 336, hash B1240B73 sample 13: time = 312000 flags = 1 - data = length 384, hash 240316E1 + data = length 336, hash 94CFCD35 sample 14: time = 336000 flags = 1 - data = length 384, hash 9EDA9AA0 + data = length 288, hash 94F412C sample 15: time = 360000 flags = 1 - data = length 384, hash E31AB44F + data = length 336, hash A1D9FF41 sample 16: time = 384000 flags = 1 - data = length 384, hash A12497D6 + data = length 288, hash 2A8DA21B sample 17: time = 408000 flags = 1 - data = length 384, hash 8A179B75 + data = length 336, hash 6A429CE sample 18: time = 432000 flags = 1 - data = length 384, hash FCE9E107 + data = length 336, hash 68853982 sample 19: time = 456000 flags = 1 - data = length 384, hash 52CA9665 + data = length 384, hash 1D6F779C sample 20: time = 480000 flags = 1 - data = length 384, hash 9935EC4C + data = length 480, hash 6B31EBEE sample 21: time = 504000 flags = 1 - data = length 384, hash 33CA710A + data = length 336, hash 888335BE sample 22: time = 528000 flags = 1 - data = length 384, hash 45B5D69 + data = length 336, hash 6072AC8B sample 23: time = 552000 flags = 1 - data = length 384, hash 7CEC655D + data = length 336, hash C9D24234 sample 24: time = 576000 flags = 1 - data = length 384, hash 3B5D8310 + data = length 288, hash 52BF4D1E sample 25: time = 600000 flags = 1 - data = length 384, hash 3EB640F8 + data = length 336, hash F93F4F0 sample 26: time = 624000 flags = 1 - data = length 384, hash FAEC53B4 + data = length 336, hash 8617688A sample 27: time = 648000 flags = 1 - data = length 384, hash 92C8A6EE + data = length 480, hash FAB0D31B sample 28: time = 672000 flags = 1 - data = length 384, hash 7CBAAE91 + data = length 384, hash FA4B53E2 sample 29: time = 696000 flags = 1 - data = length 384, hash 74AC754E + data = length 336, hash 8C435F6A sample 30: time = 720000 flags = 1 - data = length 384, hash 8242C434 + data = length 336, hash 60D3F80C sample 31: time = 744000 flags = 1 - data = length 384, hash 686C06FB + data = length 336, hash DC15B68B sample 32: time = 768000 flags = 1 - data = length 384, hash 1D872A3F + data = length 288, hash FF3DF141 sample 33: time = 792000 flags = 1 - data = length 384, hash 900A20BC + data = length 336, hash A64B3042 sample 34: time = 816000 flags = 1 - data = length 384, hash B72FD8E7 + data = length 336, hash ACA622A1 sample 35: time = 840000 flags = 1 - data = length 384, hash 85C9A1FB + data = length 288, hash 3E34B8D4 sample 36: time = 864000 flags = 1 - data = length 384, hash 1600DF3 + data = length 288, hash 9B96F72A sample 37: time = 888000 flags = 1 - data = length 384, hash D6C2138A + data = length 336, hash E917C122 sample 38: time = 912000 flags = 1 - data = length 384, hash 737BA69E + data = length 336, hash 10ED1470 sample 39: time = 936000 flags = 1 - data = length 384, hash F7E344F4 + data = length 288, hash 706B8A7C sample 40: time = 960000 flags = 1 - data = length 384, hash 14EF6AFD + data = length 336, hash 71FFE4A0 sample 41: time = 984000 flags = 1 - data = length 384, hash 61C9B92C + data = length 336, hash D4160463 sample 42: time = 1008000 flags = 1 - data = length 384, hash ABE1368 + data = length 336, hash EC557B14 sample 43: time = 1032000 flags = 1 - data = length 384, hash 6A3B8547 + data = length 288, hash 5598CF8B sample 44: time = 1056000 flags = 1 - data = length 384, hash 30E905FA + data = length 336, hash 7E0AB41 sample 45: time = 1080000 flags = 1 - data = length 384, hash 21A267CD + data = length 336, hash 1C585FEF sample 46: time = 1104000 flags = 1 - data = length 384, hash D96A2651 + data = length 336, hash A4A4855E sample 47: time = 1128000 flags = 1 - data = length 384, hash 72340177 + data = length 336, hash CECA51D3 sample 48: time = 1152000 flags = 1 - data = length 384, hash 9345E744 + data = length 288, hash 2D362DC5 sample 49: time = 1176000 flags = 1 - data = length 384, hash FDE39E3A + data = length 336, hash 9EB2609D sample 50: time = 1200000 flags = 1 - data = length 384, hash F0B7465 + data = length 336, hash 28FFB3FE sample 51: time = 1224000 flags = 1 - data = length 384, hash 3693AB86 + data = length 288, hash 2AA2D216 sample 52: time = 1248000 flags = 1 - data = length 384, hash F39719B1 + data = length 336, hash CDBC7032 sample 53: time = 1272000 flags = 1 - data = length 384, hash DA3958DC + data = length 336, hash 25B13FE7 sample 54: time = 1296000 flags = 1 - data = length 384, hash FDC7599F + data = length 336, hash DB6BB1E sample 55: time = 1320000 flags = 1 - data = length 384, hash AEFF8471 + data = length 336, hash EBE951F4 sample 56: time = 1344000 flags = 1 - data = length 384, hash 89C92C19 + data = length 288, hash 9E2EBFF7 sample 57: time = 1368000 flags = 1 - data = length 384, hash 5C786A4B + data = length 336, hash 36A7D455 sample 58: time = 1392000 flags = 1 - data = length 384, hash 5ACA8B + data = length 336, hash 84545F8C sample 59: time = 1416000 flags = 1 - data = length 384, hash 7755974C + data = length 336, hash F66F3045 sample 60: time = 1440000 flags = 1 - data = length 384, hash 3934B73C + data = length 576, hash 5AB089EA sample 61: time = 1464000 flags = 1 - data = length 384, hash DDD70A2F + data = length 336, hash 8868086 sample 62: time = 1488000 flags = 1 - data = length 384, hash 8FACE2EF + data = length 336, hash D5EB6D63 sample 63: time = 1512000 flags = 1 - data = length 384, hash 4A602591 + data = length 288, hash 7A5374B7 sample 64: time = 1536000 flags = 1 - data = length 384, hash D019AA2D + data = length 336, hash BEB27A75 sample 65: time = 1560000 flags = 1 - data = length 384, hash 8A680B9D + data = length 336, hash E251E0FD sample 66: time = 1584000 flags = 1 - data = length 384, hash B655C959 + data = length 288, hash D54C970 sample 67: time = 1608000 flags = 1 - data = length 384, hash 2168336B + data = length 336, hash 52C473B9 sample 68: time = 1632000 flags = 1 - data = length 384, hash D77F6D31 + data = length 336, hash F5F13334 sample 69: time = 1656000 flags = 1 - data = length 384, hash 524B4B2F + data = length 480, hash A5F1E987 sample 70: time = 1680000 flags = 1 - data = length 384, hash 4752DDFC + data = length 288, hash 453A1267 sample 71: time = 1704000 flags = 1 - data = length 384, hash E786727F + data = length 288, hash 7C6C2EA9 sample 72: time = 1728000 flags = 1 - data = length 384, hash 5DA6FB8C + data = length 336, hash F4BFECA4 sample 73: time = 1752000 flags = 1 - data = length 384, hash 92F24269 + data = length 336, hash 751A395A sample 74: time = 1776000 flags = 1 - data = length 384, hash CD0A3BA1 + data = length 336, hash EE38DB02 sample 75: time = 1800000 flags = 1 - data = length 384, hash 7D00409F + data = length 336, hash F18837E2 sample 76: time = 1824000 flags = 1 - data = length 384, hash D7ADB5FA + data = length 336, hash ED36B78E sample 77: time = 1848000 flags = 1 - data = length 384, hash 4A140209 + data = length 336, hash B3D28289 sample 78: time = 1872000 flags = 1 - data = length 384, hash E801184A + data = length 288, hash 8BDE28E1 sample 79: time = 1896000 flags = 1 - data = length 384, hash 53C6CF9C + data = length 336, hash CFD5E966 sample 80: time = 1920000 flags = 1 - data = length 384, hash 19A8D99F + data = length 288, hash DC08E267 sample 81: time = 1944000 flags = 1 - data = length 384, hash E47EB43F + data = length 336, hash 6530CB78 sample 82: time = 1968000 flags = 1 - data = length 384, hash 4EA329E7 + data = length 336, hash 6CC6636E sample 83: time = 1992000 flags = 1 - data = length 384, hash 1CCAAE62 + data = length 336, hash 613047C1 sample 84: time = 2016000 flags = 1 - data = length 384, hash ED3F8C66 + data = length 288, hash CDC747BF sample 85: time = 2040000 flags = 1 - data = length 384, hash D3D646B6 + data = length 336, hash AF22AA74 sample 86: time = 2064000 flags = 1 - data = length 384, hash 68CD1574 + data = length 384, hash 82F326AA sample 87: time = 2088000 flags = 1 - data = length 384, hash 8CEAB382 + data = length 384, hash EDA26C4D sample 88: time = 2112000 flags = 1 - data = length 384, hash D54B1C48 + data = length 336, hash 94C643DC sample 89: time = 2136000 flags = 1 - data = length 384, hash FFE2EE90 + data = length 288, hash CB5D9C40 sample 90: time = 2160000 flags = 1 - data = length 384, hash BFE8A673 + data = length 336, hash 1E69DE3F sample 91: time = 2184000 flags = 1 - data = length 384, hash 978B1C92 + data = length 336, hash 7E472219 sample 92: time = 2208000 flags = 1 - data = length 384, hash 810CC71E + data = length 336, hash DA47B9FA sample 93: time = 2232000 flags = 1 - data = length 384, hash 44FE42D9 + data = length 336, hash DD0ABB7C sample 94: time = 2256000 flags = 1 - data = length 384, hash 2F5BB02C + data = length 288, hash DBF93FAC sample 95: time = 2280000 flags = 1 - data = length 384, hash 77DDB90 + data = length 336, hash 243F4B2 sample 96: time = 2304000 flags = 1 - data = length 384, hash 24FB5EDA + data = length 336, hash 2E881490 sample 97: time = 2328000 flags = 1 - data = length 384, hash E73203C6 + data = length 288, hash 1C28C8BE sample 98: time = 2352000 flags = 1 - data = length 384, hash 14B525F1 + data = length 336, hash C73E5D30 sample 99: time = 2376000 flags = 1 - data = length 384, hash 5E0F4E2E + data = length 288, hash 98B5BFF6 sample 100: time = 2400000 flags = 1 - data = length 384, hash 67EE4E31 + data = length 336, hash E0135533 sample 101: time = 2424000 flags = 1 - data = length 384, hash 2E04EC4C + data = length 336, hash D13C9DBC sample 102: time = 2448000 flags = 1 - data = length 384, hash 852CABA7 + data = length 336, hash 63D524CA sample 103: time = 2472000 flags = 1 - data = length 384, hash 19928903 + data = length 288, hash A28514C3 sample 104: time = 2496000 flags = 1 - data = length 384, hash 5DA42021 + data = length 336, hash 72B647FF sample 105: time = 2520000 flags = 1 - data = length 384, hash 45B20B7C + data = length 336, hash 8F740AB1 sample 106: time = 2544000 flags = 1 - data = length 384, hash D108A215 + data = length 336, hash 5E3C7E93 sample 107: time = 2568000 flags = 1 - data = length 384, hash BD25DB7C + data = length 336, hash 121B913B sample 108: time = 2592000 flags = 1 - data = length 384, hash DA7F9861 + data = length 336, hash 578FCCF2 sample 109: time = 2616000 flags = 1 - data = length 384, hash CCD576F + data = length 336, hash 5B5823DE sample 110: time = 2640000 flags = 1 - data = length 384, hash 405C1EB5 + data = length 384, hash D8B83F78 sample 111: time = 2664000 flags = 1 - data = length 384, hash 6640B74E + data = length 240, hash E649682F sample 112: time = 2688000 flags = 1 - data = length 384, hash B4E5937A + data = length 96, hash C559A6F4 sample 113: time = 2712000 flags = 1 - data = length 384, hash CEE17733 + data = length 96, hash 792796BC sample 114: time = 2736000 flags = 1 - data = length 384, hash 2A0DA733 + data = length 120, hash 8172CD0E sample 115: time = 2760000 flags = 1 - data = length 384, hash 97F4129B + data = length 120, hash F562B52F + sample 116: + time = 2784000 + flags = 1 + data = length 96, hash FF8D5B98 tracksEnded = true diff --git a/testdata/src/test/assets/mp3/bear-id3-disabled.1.dump b/testdata/src/test/assets/mp3/bear-id3-disabled.1.dump new file mode 100644 index 0000000000..27e36ebd7a --- /dev/null +++ b/testdata/src/test/assets/mp3/bear-id3-disabled.1.dump @@ -0,0 +1,339 @@ +seekMap: + isSeekable = true + duration = 2808000 + getPosition(0) = [[timeUs=0, position=39740]] + getPosition(1) = [[timeUs=0, position=39740]] + getPosition(1404000) = [[timeUs=1404000, position=58820]] + getPosition(2808000) = [[timeUs=2808000, position=77900]] +numberOfTracks = 1 +track 0: + total output bytes = 25344 + sample count = 80 + format 0: + sampleMimeType = audio/mpeg + maxInputSize = 4096 + channelCount = 2 + sampleRate = 48000 + encoderDelay = 576 + encoderPadding = 576 + sample 0: + time = 943000 + flags = 1 + data = length 336, hash E917C122 + sample 1: + time = 967000 + flags = 1 + data = length 336, hash 10ED1470 + sample 2: + time = 991000 + flags = 1 + data = length 288, hash 706B8A7C + sample 3: + time = 1015000 + flags = 1 + data = length 336, hash 71FFE4A0 + sample 4: + time = 1039000 + flags = 1 + data = length 336, hash D4160463 + sample 5: + time = 1063000 + flags = 1 + data = length 336, hash EC557B14 + sample 6: + time = 1087000 + flags = 1 + data = length 288, hash 5598CF8B + sample 7: + time = 1111000 + flags = 1 + data = length 336, hash 7E0AB41 + sample 8: + time = 1135000 + flags = 1 + data = length 336, hash 1C585FEF + sample 9: + time = 1159000 + flags = 1 + data = length 336, hash A4A4855E + sample 10: + time = 1183000 + flags = 1 + data = length 336, hash CECA51D3 + sample 11: + time = 1207000 + flags = 1 + data = length 288, hash 2D362DC5 + sample 12: + time = 1231000 + flags = 1 + data = length 336, hash 9EB2609D + sample 13: + time = 1255000 + flags = 1 + data = length 336, hash 28FFB3FE + sample 14: + time = 1279000 + flags = 1 + data = length 288, hash 2AA2D216 + sample 15: + time = 1303000 + flags = 1 + data = length 336, hash CDBC7032 + sample 16: + time = 1327000 + flags = 1 + data = length 336, hash 25B13FE7 + sample 17: + time = 1351000 + flags = 1 + data = length 336, hash DB6BB1E + sample 18: + time = 1375000 + flags = 1 + data = length 336, hash EBE951F4 + sample 19: + time = 1399000 + flags = 1 + data = length 288, hash 9E2EBFF7 + sample 20: + time = 1423000 + flags = 1 + data = length 336, hash 36A7D455 + sample 21: + time = 1447000 + flags = 1 + data = length 336, hash 84545F8C + sample 22: + time = 1471000 + flags = 1 + data = length 336, hash F66F3045 + sample 23: + time = 1495000 + flags = 1 + data = length 576, hash 5AB089EA + sample 24: + time = 1519000 + flags = 1 + data = length 336, hash 8868086 + sample 25: + time = 1543000 + flags = 1 + data = length 336, hash D5EB6D63 + sample 26: + time = 1567000 + flags = 1 + data = length 288, hash 7A5374B7 + sample 27: + time = 1591000 + flags = 1 + data = length 336, hash BEB27A75 + sample 28: + time = 1615000 + flags = 1 + data = length 336, hash E251E0FD + sample 29: + time = 1639000 + flags = 1 + data = length 288, hash D54C970 + sample 30: + time = 1663000 + flags = 1 + data = length 336, hash 52C473B9 + sample 31: + time = 1687000 + flags = 1 + data = length 336, hash F5F13334 + sample 32: + time = 1711000 + flags = 1 + data = length 480, hash A5F1E987 + sample 33: + time = 1735000 + flags = 1 + data = length 288, hash 453A1267 + sample 34: + time = 1759000 + flags = 1 + data = length 288, hash 7C6C2EA9 + sample 35: + time = 1783000 + flags = 1 + data = length 336, hash F4BFECA4 + sample 36: + time = 1807000 + flags = 1 + data = length 336, hash 751A395A + sample 37: + time = 1831000 + flags = 1 + data = length 336, hash EE38DB02 + sample 38: + time = 1855000 + flags = 1 + data = length 336, hash F18837E2 + sample 39: + time = 1879000 + flags = 1 + data = length 336, hash ED36B78E + sample 40: + time = 1903000 + flags = 1 + data = length 336, hash B3D28289 + sample 41: + time = 1927000 + flags = 1 + data = length 288, hash 8BDE28E1 + sample 42: + time = 1951000 + flags = 1 + data = length 336, hash CFD5E966 + sample 43: + time = 1975000 + flags = 1 + data = length 288, hash DC08E267 + sample 44: + time = 1999000 + flags = 1 + data = length 336, hash 6530CB78 + sample 45: + time = 2023000 + flags = 1 + data = length 336, hash 6CC6636E + sample 46: + time = 2047000 + flags = 1 + data = length 336, hash 613047C1 + sample 47: + time = 2071000 + flags = 1 + data = length 288, hash CDC747BF + sample 48: + time = 2095000 + flags = 1 + data = length 336, hash AF22AA74 + sample 49: + time = 2119000 + flags = 1 + data = length 384, hash 82F326AA + sample 50: + time = 2143000 + flags = 1 + data = length 384, hash EDA26C4D + sample 51: + time = 2167000 + flags = 1 + data = length 336, hash 94C643DC + sample 52: + time = 2191000 + flags = 1 + data = length 288, hash CB5D9C40 + sample 53: + time = 2215000 + flags = 1 + data = length 336, hash 1E69DE3F + sample 54: + time = 2239000 + flags = 1 + data = length 336, hash 7E472219 + sample 55: + time = 2263000 + flags = 1 + data = length 336, hash DA47B9FA + sample 56: + time = 2287000 + flags = 1 + data = length 336, hash DD0ABB7C + sample 57: + time = 2311000 + flags = 1 + data = length 288, hash DBF93FAC + sample 58: + time = 2335000 + flags = 1 + data = length 336, hash 243F4B2 + sample 59: + time = 2359000 + flags = 1 + data = length 336, hash 2E881490 + sample 60: + time = 2383000 + flags = 1 + data = length 288, hash 1C28C8BE + sample 61: + time = 2407000 + flags = 1 + data = length 336, hash C73E5D30 + sample 62: + time = 2431000 + flags = 1 + data = length 288, hash 98B5BFF6 + sample 63: + time = 2455000 + flags = 1 + data = length 336, hash E0135533 + sample 64: + time = 2479000 + flags = 1 + data = length 336, hash D13C9DBC + sample 65: + time = 2503000 + flags = 1 + data = length 336, hash 63D524CA + sample 66: + time = 2527000 + flags = 1 + data = length 288, hash A28514C3 + sample 67: + time = 2551000 + flags = 1 + data = length 336, hash 72B647FF + sample 68: + time = 2575000 + flags = 1 + data = length 336, hash 8F740AB1 + sample 69: + time = 2599000 + flags = 1 + data = length 336, hash 5E3C7E93 + sample 70: + time = 2623000 + flags = 1 + data = length 336, hash 121B913B + sample 71: + time = 2647000 + flags = 1 + data = length 336, hash 578FCCF2 + sample 72: + time = 2671000 + flags = 1 + data = length 336, hash 5B5823DE + sample 73: + time = 2695000 + flags = 1 + data = length 384, hash D8B83F78 + sample 74: + time = 2719000 + flags = 1 + data = length 240, hash E649682F + sample 75: + time = 2743000 + flags = 1 + data = length 96, hash C559A6F4 + sample 76: + time = 2767000 + flags = 1 + data = length 96, hash 792796BC + sample 77: + time = 2791000 + flags = 1 + data = length 120, hash 8172CD0E + sample 78: + time = 2815000 + flags = 1 + data = length 120, hash F562B52F + sample 79: + time = 2839000 + flags = 1 + data = length 96, hash FF8D5B98 +tracksEnded = true diff --git a/testdata/src/test/assets/mp3/bear-id3-disabled.2.dump b/testdata/src/test/assets/mp3/bear-id3-disabled.2.dump new file mode 100644 index 0000000000..356e7d9872 --- /dev/null +++ b/testdata/src/test/assets/mp3/bear-id3-disabled.2.dump @@ -0,0 +1,187 @@ +seekMap: + isSeekable = true + duration = 2808000 + getPosition(0) = [[timeUs=0, position=39740]] + getPosition(1) = [[timeUs=0, position=39740]] + getPosition(1404000) = [[timeUs=1404000, position=58820]] + getPosition(2808000) = [[timeUs=2808000, position=77900]] +numberOfTracks = 1 +track 0: + total output bytes = 12624 + sample count = 42 + format 0: + sampleMimeType = audio/mpeg + maxInputSize = 4096 + channelCount = 2 + sampleRate = 48000 + encoderDelay = 576 + encoderPadding = 576 + sample 0: + time = 1879000 + flags = 1 + data = length 336, hash F18837E2 + sample 1: + time = 1903000 + flags = 1 + data = length 336, hash ED36B78E + sample 2: + time = 1927000 + flags = 1 + data = length 336, hash B3D28289 + sample 3: + time = 1951000 + flags = 1 + data = length 288, hash 8BDE28E1 + sample 4: + time = 1975000 + flags = 1 + data = length 336, hash CFD5E966 + sample 5: + time = 1999000 + flags = 1 + data = length 288, hash DC08E267 + sample 6: + time = 2023000 + flags = 1 + data = length 336, hash 6530CB78 + sample 7: + time = 2047000 + flags = 1 + data = length 336, hash 6CC6636E + sample 8: + time = 2071000 + flags = 1 + data = length 336, hash 613047C1 + sample 9: + time = 2095000 + flags = 1 + data = length 288, hash CDC747BF + sample 10: + time = 2119000 + flags = 1 + data = length 336, hash AF22AA74 + sample 11: + time = 2143000 + flags = 1 + data = length 384, hash 82F326AA + sample 12: + time = 2167000 + flags = 1 + data = length 384, hash EDA26C4D + sample 13: + time = 2191000 + flags = 1 + data = length 336, hash 94C643DC + sample 14: + time = 2215000 + flags = 1 + data = length 288, hash CB5D9C40 + sample 15: + time = 2239000 + flags = 1 + data = length 336, hash 1E69DE3F + sample 16: + time = 2263000 + flags = 1 + data = length 336, hash 7E472219 + sample 17: + time = 2287000 + flags = 1 + data = length 336, hash DA47B9FA + sample 18: + time = 2311000 + flags = 1 + data = length 336, hash DD0ABB7C + sample 19: + time = 2335000 + flags = 1 + data = length 288, hash DBF93FAC + sample 20: + time = 2359000 + flags = 1 + data = length 336, hash 243F4B2 + sample 21: + time = 2383000 + flags = 1 + data = length 336, hash 2E881490 + sample 22: + time = 2407000 + flags = 1 + data = length 288, hash 1C28C8BE + sample 23: + time = 2431000 + flags = 1 + data = length 336, hash C73E5D30 + sample 24: + time = 2455000 + flags = 1 + data = length 288, hash 98B5BFF6 + sample 25: + time = 2479000 + flags = 1 + data = length 336, hash E0135533 + sample 26: + time = 2503000 + flags = 1 + data = length 336, hash D13C9DBC + sample 27: + time = 2527000 + flags = 1 + data = length 336, hash 63D524CA + sample 28: + time = 2551000 + flags = 1 + data = length 288, hash A28514C3 + sample 29: + time = 2575000 + flags = 1 + data = length 336, hash 72B647FF + sample 30: + time = 2599000 + flags = 1 + data = length 336, hash 8F740AB1 + sample 31: + time = 2623000 + flags = 1 + data = length 336, hash 5E3C7E93 + sample 32: + time = 2647000 + flags = 1 + data = length 336, hash 121B913B + sample 33: + time = 2671000 + flags = 1 + data = length 336, hash 578FCCF2 + sample 34: + time = 2695000 + flags = 1 + data = length 336, hash 5B5823DE + sample 35: + time = 2719000 + flags = 1 + data = length 384, hash D8B83F78 + sample 36: + time = 2743000 + flags = 1 + data = length 240, hash E649682F + sample 37: + time = 2767000 + flags = 1 + data = length 96, hash C559A6F4 + sample 38: + time = 2791000 + flags = 1 + data = length 96, hash 792796BC + sample 39: + time = 2815000 + flags = 1 + data = length 120, hash 8172CD0E + sample 40: + time = 2839000 + flags = 1 + data = length 120, hash F562B52F + sample 41: + time = 2863000 + flags = 1 + data = length 96, hash FF8D5B98 +tracksEnded = true diff --git a/testdata/src/test/assets/mp3/bear-id3-disabled.3.dump b/testdata/src/test/assets/mp3/bear-id3-disabled.3.dump new file mode 100644 index 0000000000..44c93750bc --- /dev/null +++ b/testdata/src/test/assets/mp3/bear-id3-disabled.3.dump @@ -0,0 +1,19 @@ +seekMap: + isSeekable = true + duration = 2808000 + getPosition(0) = [[timeUs=0, position=39740]] + getPosition(1) = [[timeUs=0, position=39740]] + getPosition(1404000) = [[timeUs=1404000, position=58820]] + getPosition(2808000) = [[timeUs=2808000, position=77900]] +numberOfTracks = 1 +track 0: + total output bytes = 0 + sample count = 0 + format 0: + sampleMimeType = audio/mpeg + maxInputSize = 4096 + channelCount = 2 + sampleRate = 48000 + encoderDelay = 576 + encoderPadding = 576 +tracksEnded = true diff --git a/testdata/src/test/assets/mp3/bear-id3-disabled.unknown_length.dump b/testdata/src/test/assets/mp3/bear-id3-disabled.unknown_length.dump new file mode 100644 index 0000000000..a80bf5e315 --- /dev/null +++ b/testdata/src/test/assets/mp3/bear-id3-disabled.unknown_length.dump @@ -0,0 +1,487 @@ +seekMap: + isSeekable = true + duration = 2808000 + getPosition(0) = [[timeUs=0, position=39740]] + getPosition(1) = [[timeUs=0, position=39740]] + getPosition(1404000) = [[timeUs=1404000, position=58820]] + getPosition(2808000) = [[timeUs=2808000, position=77900]] +numberOfTracks = 1 +track 0: + total output bytes = 38160 + sample count = 117 + format 0: + sampleMimeType = audio/mpeg + maxInputSize = 4096 + channelCount = 2 + sampleRate = 48000 + encoderDelay = 576 + encoderPadding = 576 + sample 0: + time = 0 + flags = 1 + data = length 96, hash 1F161542 + sample 1: + time = 24000 + flags = 1 + data = length 768, hash CD1DC50F + sample 2: + time = 48000 + flags = 1 + data = length 336, hash 3F64124B + sample 3: + time = 72000 + flags = 1 + data = length 336, hash 8FFED94E + sample 4: + time = 96000 + flags = 1 + data = length 288, hash 9CD77D47 + sample 5: + time = 120000 + flags = 1 + data = length 384, hash 24607BB5 + sample 6: + time = 144000 + flags = 1 + data = length 480, hash 4937EBAB + sample 7: + time = 168000 + flags = 1 + data = length 336, hash 546342B1 + sample 8: + time = 192000 + flags = 1 + data = length 336, hash 79E0923F + sample 9: + time = 216000 + flags = 1 + data = length 336, hash AB1F3948 + sample 10: + time = 240000 + flags = 1 + data = length 336, hash C3A4D888 + sample 11: + time = 264000 + flags = 1 + data = length 288, hash 7867DA45 + sample 12: + time = 288000 + flags = 1 + data = length 336, hash B1240B73 + sample 13: + time = 312000 + flags = 1 + data = length 336, hash 94CFCD35 + sample 14: + time = 336000 + flags = 1 + data = length 288, hash 94F412C + sample 15: + time = 360000 + flags = 1 + data = length 336, hash A1D9FF41 + sample 16: + time = 384000 + flags = 1 + data = length 288, hash 2A8DA21B + sample 17: + time = 408000 + flags = 1 + data = length 336, hash 6A429CE + sample 18: + time = 432000 + flags = 1 + data = length 336, hash 68853982 + sample 19: + time = 456000 + flags = 1 + data = length 384, hash 1D6F779C + sample 20: + time = 480000 + flags = 1 + data = length 480, hash 6B31EBEE + sample 21: + time = 504000 + flags = 1 + data = length 336, hash 888335BE + sample 22: + time = 528000 + flags = 1 + data = length 336, hash 6072AC8B + sample 23: + time = 552000 + flags = 1 + data = length 336, hash C9D24234 + sample 24: + time = 576000 + flags = 1 + data = length 288, hash 52BF4D1E + sample 25: + time = 600000 + flags = 1 + data = length 336, hash F93F4F0 + sample 26: + time = 624000 + flags = 1 + data = length 336, hash 8617688A + sample 27: + time = 648000 + flags = 1 + data = length 480, hash FAB0D31B + sample 28: + time = 672000 + flags = 1 + data = length 384, hash FA4B53E2 + sample 29: + time = 696000 + flags = 1 + data = length 336, hash 8C435F6A + sample 30: + time = 720000 + flags = 1 + data = length 336, hash 60D3F80C + sample 31: + time = 744000 + flags = 1 + data = length 336, hash DC15B68B + sample 32: + time = 768000 + flags = 1 + data = length 288, hash FF3DF141 + sample 33: + time = 792000 + flags = 1 + data = length 336, hash A64B3042 + sample 34: + time = 816000 + flags = 1 + data = length 336, hash ACA622A1 + sample 35: + time = 840000 + flags = 1 + data = length 288, hash 3E34B8D4 + sample 36: + time = 864000 + flags = 1 + data = length 288, hash 9B96F72A + sample 37: + time = 888000 + flags = 1 + data = length 336, hash E917C122 + sample 38: + time = 912000 + flags = 1 + data = length 336, hash 10ED1470 + sample 39: + time = 936000 + flags = 1 + data = length 288, hash 706B8A7C + sample 40: + time = 960000 + flags = 1 + data = length 336, hash 71FFE4A0 + sample 41: + time = 984000 + flags = 1 + data = length 336, hash D4160463 + sample 42: + time = 1008000 + flags = 1 + data = length 336, hash EC557B14 + sample 43: + time = 1032000 + flags = 1 + data = length 288, hash 5598CF8B + sample 44: + time = 1056000 + flags = 1 + data = length 336, hash 7E0AB41 + sample 45: + time = 1080000 + flags = 1 + data = length 336, hash 1C585FEF + sample 46: + time = 1104000 + flags = 1 + data = length 336, hash A4A4855E + sample 47: + time = 1128000 + flags = 1 + data = length 336, hash CECA51D3 + sample 48: + time = 1152000 + flags = 1 + data = length 288, hash 2D362DC5 + sample 49: + time = 1176000 + flags = 1 + data = length 336, hash 9EB2609D + sample 50: + time = 1200000 + flags = 1 + data = length 336, hash 28FFB3FE + sample 51: + time = 1224000 + flags = 1 + data = length 288, hash 2AA2D216 + sample 52: + time = 1248000 + flags = 1 + data = length 336, hash CDBC7032 + sample 53: + time = 1272000 + flags = 1 + data = length 336, hash 25B13FE7 + sample 54: + time = 1296000 + flags = 1 + data = length 336, hash DB6BB1E + sample 55: + time = 1320000 + flags = 1 + data = length 336, hash EBE951F4 + sample 56: + time = 1344000 + flags = 1 + data = length 288, hash 9E2EBFF7 + sample 57: + time = 1368000 + flags = 1 + data = length 336, hash 36A7D455 + sample 58: + time = 1392000 + flags = 1 + data = length 336, hash 84545F8C + sample 59: + time = 1416000 + flags = 1 + data = length 336, hash F66F3045 + sample 60: + time = 1440000 + flags = 1 + data = length 576, hash 5AB089EA + sample 61: + time = 1464000 + flags = 1 + data = length 336, hash 8868086 + sample 62: + time = 1488000 + flags = 1 + data = length 336, hash D5EB6D63 + sample 63: + time = 1512000 + flags = 1 + data = length 288, hash 7A5374B7 + sample 64: + time = 1536000 + flags = 1 + data = length 336, hash BEB27A75 + sample 65: + time = 1560000 + flags = 1 + data = length 336, hash E251E0FD + sample 66: + time = 1584000 + flags = 1 + data = length 288, hash D54C970 + sample 67: + time = 1608000 + flags = 1 + data = length 336, hash 52C473B9 + sample 68: + time = 1632000 + flags = 1 + data = length 336, hash F5F13334 + sample 69: + time = 1656000 + flags = 1 + data = length 480, hash A5F1E987 + sample 70: + time = 1680000 + flags = 1 + data = length 288, hash 453A1267 + sample 71: + time = 1704000 + flags = 1 + data = length 288, hash 7C6C2EA9 + sample 72: + time = 1728000 + flags = 1 + data = length 336, hash F4BFECA4 + sample 73: + time = 1752000 + flags = 1 + data = length 336, hash 751A395A + sample 74: + time = 1776000 + flags = 1 + data = length 336, hash EE38DB02 + sample 75: + time = 1800000 + flags = 1 + data = length 336, hash F18837E2 + sample 76: + time = 1824000 + flags = 1 + data = length 336, hash ED36B78E + sample 77: + time = 1848000 + flags = 1 + data = length 336, hash B3D28289 + sample 78: + time = 1872000 + flags = 1 + data = length 288, hash 8BDE28E1 + sample 79: + time = 1896000 + flags = 1 + data = length 336, hash CFD5E966 + sample 80: + time = 1920000 + flags = 1 + data = length 288, hash DC08E267 + sample 81: + time = 1944000 + flags = 1 + data = length 336, hash 6530CB78 + sample 82: + time = 1968000 + flags = 1 + data = length 336, hash 6CC6636E + sample 83: + time = 1992000 + flags = 1 + data = length 336, hash 613047C1 + sample 84: + time = 2016000 + flags = 1 + data = length 288, hash CDC747BF + sample 85: + time = 2040000 + flags = 1 + data = length 336, hash AF22AA74 + sample 86: + time = 2064000 + flags = 1 + data = length 384, hash 82F326AA + sample 87: + time = 2088000 + flags = 1 + data = length 384, hash EDA26C4D + sample 88: + time = 2112000 + flags = 1 + data = length 336, hash 94C643DC + sample 89: + time = 2136000 + flags = 1 + data = length 288, hash CB5D9C40 + sample 90: + time = 2160000 + flags = 1 + data = length 336, hash 1E69DE3F + sample 91: + time = 2184000 + flags = 1 + data = length 336, hash 7E472219 + sample 92: + time = 2208000 + flags = 1 + data = length 336, hash DA47B9FA + sample 93: + time = 2232000 + flags = 1 + data = length 336, hash DD0ABB7C + sample 94: + time = 2256000 + flags = 1 + data = length 288, hash DBF93FAC + sample 95: + time = 2280000 + flags = 1 + data = length 336, hash 243F4B2 + sample 96: + time = 2304000 + flags = 1 + data = length 336, hash 2E881490 + sample 97: + time = 2328000 + flags = 1 + data = length 288, hash 1C28C8BE + sample 98: + time = 2352000 + flags = 1 + data = length 336, hash C73E5D30 + sample 99: + time = 2376000 + flags = 1 + data = length 288, hash 98B5BFF6 + sample 100: + time = 2400000 + flags = 1 + data = length 336, hash E0135533 + sample 101: + time = 2424000 + flags = 1 + data = length 336, hash D13C9DBC + sample 102: + time = 2448000 + flags = 1 + data = length 336, hash 63D524CA + sample 103: + time = 2472000 + flags = 1 + data = length 288, hash A28514C3 + sample 104: + time = 2496000 + flags = 1 + data = length 336, hash 72B647FF + sample 105: + time = 2520000 + flags = 1 + data = length 336, hash 8F740AB1 + sample 106: + time = 2544000 + flags = 1 + data = length 336, hash 5E3C7E93 + sample 107: + time = 2568000 + flags = 1 + data = length 336, hash 121B913B + sample 108: + time = 2592000 + flags = 1 + data = length 336, hash 578FCCF2 + sample 109: + time = 2616000 + flags = 1 + data = length 336, hash 5B5823DE + sample 110: + time = 2640000 + flags = 1 + data = length 384, hash D8B83F78 + sample 111: + time = 2664000 + flags = 1 + data = length 240, hash E649682F + sample 112: + time = 2688000 + flags = 1 + data = length 96, hash C559A6F4 + sample 113: + time = 2712000 + flags = 1 + data = length 96, hash 792796BC + sample 114: + time = 2736000 + flags = 1 + data = length 120, hash 8172CD0E + sample 115: + time = 2760000 + flags = 1 + data = length 120, hash F562B52F + sample 116: + time = 2784000 + flags = 1 + data = length 96, hash FF8D5B98 +tracksEnded = true diff --git a/testdata/src/test/assets/mp3/bear-id3-enabled.0.dump b/testdata/src/test/assets/mp3/bear-id3-enabled.0.dump new file mode 100644 index 0000000000..c252057e47 --- /dev/null +++ b/testdata/src/test/assets/mp3/bear-id3-enabled.0.dump @@ -0,0 +1,488 @@ +seekMap: + isSeekable = true + duration = 2808000 + getPosition(0) = [[timeUs=0, position=39740]] + getPosition(1) = [[timeUs=0, position=39740]] + getPosition(1404000) = [[timeUs=1404000, position=58820]] + getPosition(2808000) = [[timeUs=2808000, position=77900]] +numberOfTracks = 1 +track 0: + total output bytes = 38160 + sample count = 117 + format 0: + sampleMimeType = audio/mpeg + maxInputSize = 4096 + channelCount = 2 + sampleRate = 48000 + encoderDelay = 576 + encoderPadding = 576 + metadata = entries=[TIT2: description=null: value=Test title, TPE1: description=null: value=Test Artist, TALB: description=null: value=Test Album, TXXX: description=Test description: value=Test user info, COMM: language=eng, description=Test description, WXXX: url=Test URL, TSSE: description=null: value=Lavf58.29.100, MLLT, PRIV: owner=test@gmail.com, UNKN, GEOB: mimeType=test/mime, filename=Testfilename.txt, description=Test description, CHAP, CHAP, CTOC, APIC: mimeType=image/jpeg, description=Test description] + sample 0: + time = 0 + flags = 1 + data = length 96, hash 1F161542 + sample 1: + time = 24000 + flags = 1 + data = length 768, hash CD1DC50F + sample 2: + time = 48000 + flags = 1 + data = length 336, hash 3F64124B + sample 3: + time = 72000 + flags = 1 + data = length 336, hash 8FFED94E + sample 4: + time = 96000 + flags = 1 + data = length 288, hash 9CD77D47 + sample 5: + time = 120000 + flags = 1 + data = length 384, hash 24607BB5 + sample 6: + time = 144000 + flags = 1 + data = length 480, hash 4937EBAB + sample 7: + time = 168000 + flags = 1 + data = length 336, hash 546342B1 + sample 8: + time = 192000 + flags = 1 + data = length 336, hash 79E0923F + sample 9: + time = 216000 + flags = 1 + data = length 336, hash AB1F3948 + sample 10: + time = 240000 + flags = 1 + data = length 336, hash C3A4D888 + sample 11: + time = 264000 + flags = 1 + data = length 288, hash 7867DA45 + sample 12: + time = 288000 + flags = 1 + data = length 336, hash B1240B73 + sample 13: + time = 312000 + flags = 1 + data = length 336, hash 94CFCD35 + sample 14: + time = 336000 + flags = 1 + data = length 288, hash 94F412C + sample 15: + time = 360000 + flags = 1 + data = length 336, hash A1D9FF41 + sample 16: + time = 384000 + flags = 1 + data = length 288, hash 2A8DA21B + sample 17: + time = 408000 + flags = 1 + data = length 336, hash 6A429CE + sample 18: + time = 432000 + flags = 1 + data = length 336, hash 68853982 + sample 19: + time = 456000 + flags = 1 + data = length 384, hash 1D6F779C + sample 20: + time = 480000 + flags = 1 + data = length 480, hash 6B31EBEE + sample 21: + time = 504000 + flags = 1 + data = length 336, hash 888335BE + sample 22: + time = 528000 + flags = 1 + data = length 336, hash 6072AC8B + sample 23: + time = 552000 + flags = 1 + data = length 336, hash C9D24234 + sample 24: + time = 576000 + flags = 1 + data = length 288, hash 52BF4D1E + sample 25: + time = 600000 + flags = 1 + data = length 336, hash F93F4F0 + sample 26: + time = 624000 + flags = 1 + data = length 336, hash 8617688A + sample 27: + time = 648000 + flags = 1 + data = length 480, hash FAB0D31B + sample 28: + time = 672000 + flags = 1 + data = length 384, hash FA4B53E2 + sample 29: + time = 696000 + flags = 1 + data = length 336, hash 8C435F6A + sample 30: + time = 720000 + flags = 1 + data = length 336, hash 60D3F80C + sample 31: + time = 744000 + flags = 1 + data = length 336, hash DC15B68B + sample 32: + time = 768000 + flags = 1 + data = length 288, hash FF3DF141 + sample 33: + time = 792000 + flags = 1 + data = length 336, hash A64B3042 + sample 34: + time = 816000 + flags = 1 + data = length 336, hash ACA622A1 + sample 35: + time = 840000 + flags = 1 + data = length 288, hash 3E34B8D4 + sample 36: + time = 864000 + flags = 1 + data = length 288, hash 9B96F72A + sample 37: + time = 888000 + flags = 1 + data = length 336, hash E917C122 + sample 38: + time = 912000 + flags = 1 + data = length 336, hash 10ED1470 + sample 39: + time = 936000 + flags = 1 + data = length 288, hash 706B8A7C + sample 40: + time = 960000 + flags = 1 + data = length 336, hash 71FFE4A0 + sample 41: + time = 984000 + flags = 1 + data = length 336, hash D4160463 + sample 42: + time = 1008000 + flags = 1 + data = length 336, hash EC557B14 + sample 43: + time = 1032000 + flags = 1 + data = length 288, hash 5598CF8B + sample 44: + time = 1056000 + flags = 1 + data = length 336, hash 7E0AB41 + sample 45: + time = 1080000 + flags = 1 + data = length 336, hash 1C585FEF + sample 46: + time = 1104000 + flags = 1 + data = length 336, hash A4A4855E + sample 47: + time = 1128000 + flags = 1 + data = length 336, hash CECA51D3 + sample 48: + time = 1152000 + flags = 1 + data = length 288, hash 2D362DC5 + sample 49: + time = 1176000 + flags = 1 + data = length 336, hash 9EB2609D + sample 50: + time = 1200000 + flags = 1 + data = length 336, hash 28FFB3FE + sample 51: + time = 1224000 + flags = 1 + data = length 288, hash 2AA2D216 + sample 52: + time = 1248000 + flags = 1 + data = length 336, hash CDBC7032 + sample 53: + time = 1272000 + flags = 1 + data = length 336, hash 25B13FE7 + sample 54: + time = 1296000 + flags = 1 + data = length 336, hash DB6BB1E + sample 55: + time = 1320000 + flags = 1 + data = length 336, hash EBE951F4 + sample 56: + time = 1344000 + flags = 1 + data = length 288, hash 9E2EBFF7 + sample 57: + time = 1368000 + flags = 1 + data = length 336, hash 36A7D455 + sample 58: + time = 1392000 + flags = 1 + data = length 336, hash 84545F8C + sample 59: + time = 1416000 + flags = 1 + data = length 336, hash F66F3045 + sample 60: + time = 1440000 + flags = 1 + data = length 576, hash 5AB089EA + sample 61: + time = 1464000 + flags = 1 + data = length 336, hash 8868086 + sample 62: + time = 1488000 + flags = 1 + data = length 336, hash D5EB6D63 + sample 63: + time = 1512000 + flags = 1 + data = length 288, hash 7A5374B7 + sample 64: + time = 1536000 + flags = 1 + data = length 336, hash BEB27A75 + sample 65: + time = 1560000 + flags = 1 + data = length 336, hash E251E0FD + sample 66: + time = 1584000 + flags = 1 + data = length 288, hash D54C970 + sample 67: + time = 1608000 + flags = 1 + data = length 336, hash 52C473B9 + sample 68: + time = 1632000 + flags = 1 + data = length 336, hash F5F13334 + sample 69: + time = 1656000 + flags = 1 + data = length 480, hash A5F1E987 + sample 70: + time = 1680000 + flags = 1 + data = length 288, hash 453A1267 + sample 71: + time = 1704000 + flags = 1 + data = length 288, hash 7C6C2EA9 + sample 72: + time = 1728000 + flags = 1 + data = length 336, hash F4BFECA4 + sample 73: + time = 1752000 + flags = 1 + data = length 336, hash 751A395A + sample 74: + time = 1776000 + flags = 1 + data = length 336, hash EE38DB02 + sample 75: + time = 1800000 + flags = 1 + data = length 336, hash F18837E2 + sample 76: + time = 1824000 + flags = 1 + data = length 336, hash ED36B78E + sample 77: + time = 1848000 + flags = 1 + data = length 336, hash B3D28289 + sample 78: + time = 1872000 + flags = 1 + data = length 288, hash 8BDE28E1 + sample 79: + time = 1896000 + flags = 1 + data = length 336, hash CFD5E966 + sample 80: + time = 1920000 + flags = 1 + data = length 288, hash DC08E267 + sample 81: + time = 1944000 + flags = 1 + data = length 336, hash 6530CB78 + sample 82: + time = 1968000 + flags = 1 + data = length 336, hash 6CC6636E + sample 83: + time = 1992000 + flags = 1 + data = length 336, hash 613047C1 + sample 84: + time = 2016000 + flags = 1 + data = length 288, hash CDC747BF + sample 85: + time = 2040000 + flags = 1 + data = length 336, hash AF22AA74 + sample 86: + time = 2064000 + flags = 1 + data = length 384, hash 82F326AA + sample 87: + time = 2088000 + flags = 1 + data = length 384, hash EDA26C4D + sample 88: + time = 2112000 + flags = 1 + data = length 336, hash 94C643DC + sample 89: + time = 2136000 + flags = 1 + data = length 288, hash CB5D9C40 + sample 90: + time = 2160000 + flags = 1 + data = length 336, hash 1E69DE3F + sample 91: + time = 2184000 + flags = 1 + data = length 336, hash 7E472219 + sample 92: + time = 2208000 + flags = 1 + data = length 336, hash DA47B9FA + sample 93: + time = 2232000 + flags = 1 + data = length 336, hash DD0ABB7C + sample 94: + time = 2256000 + flags = 1 + data = length 288, hash DBF93FAC + sample 95: + time = 2280000 + flags = 1 + data = length 336, hash 243F4B2 + sample 96: + time = 2304000 + flags = 1 + data = length 336, hash 2E881490 + sample 97: + time = 2328000 + flags = 1 + data = length 288, hash 1C28C8BE + sample 98: + time = 2352000 + flags = 1 + data = length 336, hash C73E5D30 + sample 99: + time = 2376000 + flags = 1 + data = length 288, hash 98B5BFF6 + sample 100: + time = 2400000 + flags = 1 + data = length 336, hash E0135533 + sample 101: + time = 2424000 + flags = 1 + data = length 336, hash D13C9DBC + sample 102: + time = 2448000 + flags = 1 + data = length 336, hash 63D524CA + sample 103: + time = 2472000 + flags = 1 + data = length 288, hash A28514C3 + sample 104: + time = 2496000 + flags = 1 + data = length 336, hash 72B647FF + sample 105: + time = 2520000 + flags = 1 + data = length 336, hash 8F740AB1 + sample 106: + time = 2544000 + flags = 1 + data = length 336, hash 5E3C7E93 + sample 107: + time = 2568000 + flags = 1 + data = length 336, hash 121B913B + sample 108: + time = 2592000 + flags = 1 + data = length 336, hash 578FCCF2 + sample 109: + time = 2616000 + flags = 1 + data = length 336, hash 5B5823DE + sample 110: + time = 2640000 + flags = 1 + data = length 384, hash D8B83F78 + sample 111: + time = 2664000 + flags = 1 + data = length 240, hash E649682F + sample 112: + time = 2688000 + flags = 1 + data = length 96, hash C559A6F4 + sample 113: + time = 2712000 + flags = 1 + data = length 96, hash 792796BC + sample 114: + time = 2736000 + flags = 1 + data = length 120, hash 8172CD0E + sample 115: + time = 2760000 + flags = 1 + data = length 120, hash F562B52F + sample 116: + time = 2784000 + flags = 1 + data = length 96, hash FF8D5B98 +tracksEnded = true diff --git a/testdata/src/test/assets/mp3/bear-id3-enabled.1.dump b/testdata/src/test/assets/mp3/bear-id3-enabled.1.dump new file mode 100644 index 0000000000..76fcbc0f8e --- /dev/null +++ b/testdata/src/test/assets/mp3/bear-id3-enabled.1.dump @@ -0,0 +1,340 @@ +seekMap: + isSeekable = true + duration = 2808000 + getPosition(0) = [[timeUs=0, position=39740]] + getPosition(1) = [[timeUs=0, position=39740]] + getPosition(1404000) = [[timeUs=1404000, position=58820]] + getPosition(2808000) = [[timeUs=2808000, position=77900]] +numberOfTracks = 1 +track 0: + total output bytes = 25344 + sample count = 80 + format 0: + sampleMimeType = audio/mpeg + maxInputSize = 4096 + channelCount = 2 + sampleRate = 48000 + encoderDelay = 576 + encoderPadding = 576 + metadata = entries=[TIT2: description=null: value=Test title, TPE1: description=null: value=Test Artist, TALB: description=null: value=Test Album, TXXX: description=Test description: value=Test user info, COMM: language=eng, description=Test description, WXXX: url=Test URL, TSSE: description=null: value=Lavf58.29.100, MLLT, PRIV: owner=test@gmail.com, UNKN, GEOB: mimeType=test/mime, filename=Testfilename.txt, description=Test description, CHAP, CHAP, CTOC, APIC: mimeType=image/jpeg, description=Test description] + sample 0: + time = 943000 + flags = 1 + data = length 336, hash E917C122 + sample 1: + time = 967000 + flags = 1 + data = length 336, hash 10ED1470 + sample 2: + time = 991000 + flags = 1 + data = length 288, hash 706B8A7C + sample 3: + time = 1015000 + flags = 1 + data = length 336, hash 71FFE4A0 + sample 4: + time = 1039000 + flags = 1 + data = length 336, hash D4160463 + sample 5: + time = 1063000 + flags = 1 + data = length 336, hash EC557B14 + sample 6: + time = 1087000 + flags = 1 + data = length 288, hash 5598CF8B + sample 7: + time = 1111000 + flags = 1 + data = length 336, hash 7E0AB41 + sample 8: + time = 1135000 + flags = 1 + data = length 336, hash 1C585FEF + sample 9: + time = 1159000 + flags = 1 + data = length 336, hash A4A4855E + sample 10: + time = 1183000 + flags = 1 + data = length 336, hash CECA51D3 + sample 11: + time = 1207000 + flags = 1 + data = length 288, hash 2D362DC5 + sample 12: + time = 1231000 + flags = 1 + data = length 336, hash 9EB2609D + sample 13: + time = 1255000 + flags = 1 + data = length 336, hash 28FFB3FE + sample 14: + time = 1279000 + flags = 1 + data = length 288, hash 2AA2D216 + sample 15: + time = 1303000 + flags = 1 + data = length 336, hash CDBC7032 + sample 16: + time = 1327000 + flags = 1 + data = length 336, hash 25B13FE7 + sample 17: + time = 1351000 + flags = 1 + data = length 336, hash DB6BB1E + sample 18: + time = 1375000 + flags = 1 + data = length 336, hash EBE951F4 + sample 19: + time = 1399000 + flags = 1 + data = length 288, hash 9E2EBFF7 + sample 20: + time = 1423000 + flags = 1 + data = length 336, hash 36A7D455 + sample 21: + time = 1447000 + flags = 1 + data = length 336, hash 84545F8C + sample 22: + time = 1471000 + flags = 1 + data = length 336, hash F66F3045 + sample 23: + time = 1495000 + flags = 1 + data = length 576, hash 5AB089EA + sample 24: + time = 1519000 + flags = 1 + data = length 336, hash 8868086 + sample 25: + time = 1543000 + flags = 1 + data = length 336, hash D5EB6D63 + sample 26: + time = 1567000 + flags = 1 + data = length 288, hash 7A5374B7 + sample 27: + time = 1591000 + flags = 1 + data = length 336, hash BEB27A75 + sample 28: + time = 1615000 + flags = 1 + data = length 336, hash E251E0FD + sample 29: + time = 1639000 + flags = 1 + data = length 288, hash D54C970 + sample 30: + time = 1663000 + flags = 1 + data = length 336, hash 52C473B9 + sample 31: + time = 1687000 + flags = 1 + data = length 336, hash F5F13334 + sample 32: + time = 1711000 + flags = 1 + data = length 480, hash A5F1E987 + sample 33: + time = 1735000 + flags = 1 + data = length 288, hash 453A1267 + sample 34: + time = 1759000 + flags = 1 + data = length 288, hash 7C6C2EA9 + sample 35: + time = 1783000 + flags = 1 + data = length 336, hash F4BFECA4 + sample 36: + time = 1807000 + flags = 1 + data = length 336, hash 751A395A + sample 37: + time = 1831000 + flags = 1 + data = length 336, hash EE38DB02 + sample 38: + time = 1855000 + flags = 1 + data = length 336, hash F18837E2 + sample 39: + time = 1879000 + flags = 1 + data = length 336, hash ED36B78E + sample 40: + time = 1903000 + flags = 1 + data = length 336, hash B3D28289 + sample 41: + time = 1927000 + flags = 1 + data = length 288, hash 8BDE28E1 + sample 42: + time = 1951000 + flags = 1 + data = length 336, hash CFD5E966 + sample 43: + time = 1975000 + flags = 1 + data = length 288, hash DC08E267 + sample 44: + time = 1999000 + flags = 1 + data = length 336, hash 6530CB78 + sample 45: + time = 2023000 + flags = 1 + data = length 336, hash 6CC6636E + sample 46: + time = 2047000 + flags = 1 + data = length 336, hash 613047C1 + sample 47: + time = 2071000 + flags = 1 + data = length 288, hash CDC747BF + sample 48: + time = 2095000 + flags = 1 + data = length 336, hash AF22AA74 + sample 49: + time = 2119000 + flags = 1 + data = length 384, hash 82F326AA + sample 50: + time = 2143000 + flags = 1 + data = length 384, hash EDA26C4D + sample 51: + time = 2167000 + flags = 1 + data = length 336, hash 94C643DC + sample 52: + time = 2191000 + flags = 1 + data = length 288, hash CB5D9C40 + sample 53: + time = 2215000 + flags = 1 + data = length 336, hash 1E69DE3F + sample 54: + time = 2239000 + flags = 1 + data = length 336, hash 7E472219 + sample 55: + time = 2263000 + flags = 1 + data = length 336, hash DA47B9FA + sample 56: + time = 2287000 + flags = 1 + data = length 336, hash DD0ABB7C + sample 57: + time = 2311000 + flags = 1 + data = length 288, hash DBF93FAC + sample 58: + time = 2335000 + flags = 1 + data = length 336, hash 243F4B2 + sample 59: + time = 2359000 + flags = 1 + data = length 336, hash 2E881490 + sample 60: + time = 2383000 + flags = 1 + data = length 288, hash 1C28C8BE + sample 61: + time = 2407000 + flags = 1 + data = length 336, hash C73E5D30 + sample 62: + time = 2431000 + flags = 1 + data = length 288, hash 98B5BFF6 + sample 63: + time = 2455000 + flags = 1 + data = length 336, hash E0135533 + sample 64: + time = 2479000 + flags = 1 + data = length 336, hash D13C9DBC + sample 65: + time = 2503000 + flags = 1 + data = length 336, hash 63D524CA + sample 66: + time = 2527000 + flags = 1 + data = length 288, hash A28514C3 + sample 67: + time = 2551000 + flags = 1 + data = length 336, hash 72B647FF + sample 68: + time = 2575000 + flags = 1 + data = length 336, hash 8F740AB1 + sample 69: + time = 2599000 + flags = 1 + data = length 336, hash 5E3C7E93 + sample 70: + time = 2623000 + flags = 1 + data = length 336, hash 121B913B + sample 71: + time = 2647000 + flags = 1 + data = length 336, hash 578FCCF2 + sample 72: + time = 2671000 + flags = 1 + data = length 336, hash 5B5823DE + sample 73: + time = 2695000 + flags = 1 + data = length 384, hash D8B83F78 + sample 74: + time = 2719000 + flags = 1 + data = length 240, hash E649682F + sample 75: + time = 2743000 + flags = 1 + data = length 96, hash C559A6F4 + sample 76: + time = 2767000 + flags = 1 + data = length 96, hash 792796BC + sample 77: + time = 2791000 + flags = 1 + data = length 120, hash 8172CD0E + sample 78: + time = 2815000 + flags = 1 + data = length 120, hash F562B52F + sample 79: + time = 2839000 + flags = 1 + data = length 96, hash FF8D5B98 +tracksEnded = true diff --git a/testdata/src/test/assets/mp3/bear-id3-enabled.2.dump b/testdata/src/test/assets/mp3/bear-id3-enabled.2.dump new file mode 100644 index 0000000000..4f9b29dc55 --- /dev/null +++ b/testdata/src/test/assets/mp3/bear-id3-enabled.2.dump @@ -0,0 +1,188 @@ +seekMap: + isSeekable = true + duration = 2808000 + getPosition(0) = [[timeUs=0, position=39740]] + getPosition(1) = [[timeUs=0, position=39740]] + getPosition(1404000) = [[timeUs=1404000, position=58820]] + getPosition(2808000) = [[timeUs=2808000, position=77900]] +numberOfTracks = 1 +track 0: + total output bytes = 12624 + sample count = 42 + format 0: + sampleMimeType = audio/mpeg + maxInputSize = 4096 + channelCount = 2 + sampleRate = 48000 + encoderDelay = 576 + encoderPadding = 576 + metadata = entries=[TIT2: description=null: value=Test title, TPE1: description=null: value=Test Artist, TALB: description=null: value=Test Album, TXXX: description=Test description: value=Test user info, COMM: language=eng, description=Test description, WXXX: url=Test URL, TSSE: description=null: value=Lavf58.29.100, MLLT, PRIV: owner=test@gmail.com, UNKN, GEOB: mimeType=test/mime, filename=Testfilename.txt, description=Test description, CHAP, CHAP, CTOC, APIC: mimeType=image/jpeg, description=Test description] + sample 0: + time = 1879000 + flags = 1 + data = length 336, hash F18837E2 + sample 1: + time = 1903000 + flags = 1 + data = length 336, hash ED36B78E + sample 2: + time = 1927000 + flags = 1 + data = length 336, hash B3D28289 + sample 3: + time = 1951000 + flags = 1 + data = length 288, hash 8BDE28E1 + sample 4: + time = 1975000 + flags = 1 + data = length 336, hash CFD5E966 + sample 5: + time = 1999000 + flags = 1 + data = length 288, hash DC08E267 + sample 6: + time = 2023000 + flags = 1 + data = length 336, hash 6530CB78 + sample 7: + time = 2047000 + flags = 1 + data = length 336, hash 6CC6636E + sample 8: + time = 2071000 + flags = 1 + data = length 336, hash 613047C1 + sample 9: + time = 2095000 + flags = 1 + data = length 288, hash CDC747BF + sample 10: + time = 2119000 + flags = 1 + data = length 336, hash AF22AA74 + sample 11: + time = 2143000 + flags = 1 + data = length 384, hash 82F326AA + sample 12: + time = 2167000 + flags = 1 + data = length 384, hash EDA26C4D + sample 13: + time = 2191000 + flags = 1 + data = length 336, hash 94C643DC + sample 14: + time = 2215000 + flags = 1 + data = length 288, hash CB5D9C40 + sample 15: + time = 2239000 + flags = 1 + data = length 336, hash 1E69DE3F + sample 16: + time = 2263000 + flags = 1 + data = length 336, hash 7E472219 + sample 17: + time = 2287000 + flags = 1 + data = length 336, hash DA47B9FA + sample 18: + time = 2311000 + flags = 1 + data = length 336, hash DD0ABB7C + sample 19: + time = 2335000 + flags = 1 + data = length 288, hash DBF93FAC + sample 20: + time = 2359000 + flags = 1 + data = length 336, hash 243F4B2 + sample 21: + time = 2383000 + flags = 1 + data = length 336, hash 2E881490 + sample 22: + time = 2407000 + flags = 1 + data = length 288, hash 1C28C8BE + sample 23: + time = 2431000 + flags = 1 + data = length 336, hash C73E5D30 + sample 24: + time = 2455000 + flags = 1 + data = length 288, hash 98B5BFF6 + sample 25: + time = 2479000 + flags = 1 + data = length 336, hash E0135533 + sample 26: + time = 2503000 + flags = 1 + data = length 336, hash D13C9DBC + sample 27: + time = 2527000 + flags = 1 + data = length 336, hash 63D524CA + sample 28: + time = 2551000 + flags = 1 + data = length 288, hash A28514C3 + sample 29: + time = 2575000 + flags = 1 + data = length 336, hash 72B647FF + sample 30: + time = 2599000 + flags = 1 + data = length 336, hash 8F740AB1 + sample 31: + time = 2623000 + flags = 1 + data = length 336, hash 5E3C7E93 + sample 32: + time = 2647000 + flags = 1 + data = length 336, hash 121B913B + sample 33: + time = 2671000 + flags = 1 + data = length 336, hash 578FCCF2 + sample 34: + time = 2695000 + flags = 1 + data = length 336, hash 5B5823DE + sample 35: + time = 2719000 + flags = 1 + data = length 384, hash D8B83F78 + sample 36: + time = 2743000 + flags = 1 + data = length 240, hash E649682F + sample 37: + time = 2767000 + flags = 1 + data = length 96, hash C559A6F4 + sample 38: + time = 2791000 + flags = 1 + data = length 96, hash 792796BC + sample 39: + time = 2815000 + flags = 1 + data = length 120, hash 8172CD0E + sample 40: + time = 2839000 + flags = 1 + data = length 120, hash F562B52F + sample 41: + time = 2863000 + flags = 1 + data = length 96, hash FF8D5B98 +tracksEnded = true diff --git a/testdata/src/test/assets/mp3/bear-id3-enabled.3.dump b/testdata/src/test/assets/mp3/bear-id3-enabled.3.dump new file mode 100644 index 0000000000..220965634f --- /dev/null +++ b/testdata/src/test/assets/mp3/bear-id3-enabled.3.dump @@ -0,0 +1,20 @@ +seekMap: + isSeekable = true + duration = 2808000 + getPosition(0) = [[timeUs=0, position=39740]] + getPosition(1) = [[timeUs=0, position=39740]] + getPosition(1404000) = [[timeUs=1404000, position=58820]] + getPosition(2808000) = [[timeUs=2808000, position=77900]] +numberOfTracks = 1 +track 0: + total output bytes = 0 + sample count = 0 + format 0: + sampleMimeType = audio/mpeg + maxInputSize = 4096 + channelCount = 2 + sampleRate = 48000 + encoderDelay = 576 + encoderPadding = 576 + metadata = entries=[TIT2: description=null: value=Test title, TPE1: description=null: value=Test Artist, TALB: description=null: value=Test Album, TXXX: description=Test description: value=Test user info, COMM: language=eng, description=Test description, WXXX: url=Test URL, TSSE: description=null: value=Lavf58.29.100, MLLT, PRIV: owner=test@gmail.com, UNKN, GEOB: mimeType=test/mime, filename=Testfilename.txt, description=Test description, CHAP, CHAP, CTOC, APIC: mimeType=image/jpeg, description=Test description] +tracksEnded = true diff --git a/testdata/src/test/assets/mp3/bear-id3-enabled.unknown_length.dump b/testdata/src/test/assets/mp3/bear-id3-enabled.unknown_length.dump new file mode 100644 index 0000000000..c252057e47 --- /dev/null +++ b/testdata/src/test/assets/mp3/bear-id3-enabled.unknown_length.dump @@ -0,0 +1,488 @@ +seekMap: + isSeekable = true + duration = 2808000 + getPosition(0) = [[timeUs=0, position=39740]] + getPosition(1) = [[timeUs=0, position=39740]] + getPosition(1404000) = [[timeUs=1404000, position=58820]] + getPosition(2808000) = [[timeUs=2808000, position=77900]] +numberOfTracks = 1 +track 0: + total output bytes = 38160 + sample count = 117 + format 0: + sampleMimeType = audio/mpeg + maxInputSize = 4096 + channelCount = 2 + sampleRate = 48000 + encoderDelay = 576 + encoderPadding = 576 + metadata = entries=[TIT2: description=null: value=Test title, TPE1: description=null: value=Test Artist, TALB: description=null: value=Test Album, TXXX: description=Test description: value=Test user info, COMM: language=eng, description=Test description, WXXX: url=Test URL, TSSE: description=null: value=Lavf58.29.100, MLLT, PRIV: owner=test@gmail.com, UNKN, GEOB: mimeType=test/mime, filename=Testfilename.txt, description=Test description, CHAP, CHAP, CTOC, APIC: mimeType=image/jpeg, description=Test description] + sample 0: + time = 0 + flags = 1 + data = length 96, hash 1F161542 + sample 1: + time = 24000 + flags = 1 + data = length 768, hash CD1DC50F + sample 2: + time = 48000 + flags = 1 + data = length 336, hash 3F64124B + sample 3: + time = 72000 + flags = 1 + data = length 336, hash 8FFED94E + sample 4: + time = 96000 + flags = 1 + data = length 288, hash 9CD77D47 + sample 5: + time = 120000 + flags = 1 + data = length 384, hash 24607BB5 + sample 6: + time = 144000 + flags = 1 + data = length 480, hash 4937EBAB + sample 7: + time = 168000 + flags = 1 + data = length 336, hash 546342B1 + sample 8: + time = 192000 + flags = 1 + data = length 336, hash 79E0923F + sample 9: + time = 216000 + flags = 1 + data = length 336, hash AB1F3948 + sample 10: + time = 240000 + flags = 1 + data = length 336, hash C3A4D888 + sample 11: + time = 264000 + flags = 1 + data = length 288, hash 7867DA45 + sample 12: + time = 288000 + flags = 1 + data = length 336, hash B1240B73 + sample 13: + time = 312000 + flags = 1 + data = length 336, hash 94CFCD35 + sample 14: + time = 336000 + flags = 1 + data = length 288, hash 94F412C + sample 15: + time = 360000 + flags = 1 + data = length 336, hash A1D9FF41 + sample 16: + time = 384000 + flags = 1 + data = length 288, hash 2A8DA21B + sample 17: + time = 408000 + flags = 1 + data = length 336, hash 6A429CE + sample 18: + time = 432000 + flags = 1 + data = length 336, hash 68853982 + sample 19: + time = 456000 + flags = 1 + data = length 384, hash 1D6F779C + sample 20: + time = 480000 + flags = 1 + data = length 480, hash 6B31EBEE + sample 21: + time = 504000 + flags = 1 + data = length 336, hash 888335BE + sample 22: + time = 528000 + flags = 1 + data = length 336, hash 6072AC8B + sample 23: + time = 552000 + flags = 1 + data = length 336, hash C9D24234 + sample 24: + time = 576000 + flags = 1 + data = length 288, hash 52BF4D1E + sample 25: + time = 600000 + flags = 1 + data = length 336, hash F93F4F0 + sample 26: + time = 624000 + flags = 1 + data = length 336, hash 8617688A + sample 27: + time = 648000 + flags = 1 + data = length 480, hash FAB0D31B + sample 28: + time = 672000 + flags = 1 + data = length 384, hash FA4B53E2 + sample 29: + time = 696000 + flags = 1 + data = length 336, hash 8C435F6A + sample 30: + time = 720000 + flags = 1 + data = length 336, hash 60D3F80C + sample 31: + time = 744000 + flags = 1 + data = length 336, hash DC15B68B + sample 32: + time = 768000 + flags = 1 + data = length 288, hash FF3DF141 + sample 33: + time = 792000 + flags = 1 + data = length 336, hash A64B3042 + sample 34: + time = 816000 + flags = 1 + data = length 336, hash ACA622A1 + sample 35: + time = 840000 + flags = 1 + data = length 288, hash 3E34B8D4 + sample 36: + time = 864000 + flags = 1 + data = length 288, hash 9B96F72A + sample 37: + time = 888000 + flags = 1 + data = length 336, hash E917C122 + sample 38: + time = 912000 + flags = 1 + data = length 336, hash 10ED1470 + sample 39: + time = 936000 + flags = 1 + data = length 288, hash 706B8A7C + sample 40: + time = 960000 + flags = 1 + data = length 336, hash 71FFE4A0 + sample 41: + time = 984000 + flags = 1 + data = length 336, hash D4160463 + sample 42: + time = 1008000 + flags = 1 + data = length 336, hash EC557B14 + sample 43: + time = 1032000 + flags = 1 + data = length 288, hash 5598CF8B + sample 44: + time = 1056000 + flags = 1 + data = length 336, hash 7E0AB41 + sample 45: + time = 1080000 + flags = 1 + data = length 336, hash 1C585FEF + sample 46: + time = 1104000 + flags = 1 + data = length 336, hash A4A4855E + sample 47: + time = 1128000 + flags = 1 + data = length 336, hash CECA51D3 + sample 48: + time = 1152000 + flags = 1 + data = length 288, hash 2D362DC5 + sample 49: + time = 1176000 + flags = 1 + data = length 336, hash 9EB2609D + sample 50: + time = 1200000 + flags = 1 + data = length 336, hash 28FFB3FE + sample 51: + time = 1224000 + flags = 1 + data = length 288, hash 2AA2D216 + sample 52: + time = 1248000 + flags = 1 + data = length 336, hash CDBC7032 + sample 53: + time = 1272000 + flags = 1 + data = length 336, hash 25B13FE7 + sample 54: + time = 1296000 + flags = 1 + data = length 336, hash DB6BB1E + sample 55: + time = 1320000 + flags = 1 + data = length 336, hash EBE951F4 + sample 56: + time = 1344000 + flags = 1 + data = length 288, hash 9E2EBFF7 + sample 57: + time = 1368000 + flags = 1 + data = length 336, hash 36A7D455 + sample 58: + time = 1392000 + flags = 1 + data = length 336, hash 84545F8C + sample 59: + time = 1416000 + flags = 1 + data = length 336, hash F66F3045 + sample 60: + time = 1440000 + flags = 1 + data = length 576, hash 5AB089EA + sample 61: + time = 1464000 + flags = 1 + data = length 336, hash 8868086 + sample 62: + time = 1488000 + flags = 1 + data = length 336, hash D5EB6D63 + sample 63: + time = 1512000 + flags = 1 + data = length 288, hash 7A5374B7 + sample 64: + time = 1536000 + flags = 1 + data = length 336, hash BEB27A75 + sample 65: + time = 1560000 + flags = 1 + data = length 336, hash E251E0FD + sample 66: + time = 1584000 + flags = 1 + data = length 288, hash D54C970 + sample 67: + time = 1608000 + flags = 1 + data = length 336, hash 52C473B9 + sample 68: + time = 1632000 + flags = 1 + data = length 336, hash F5F13334 + sample 69: + time = 1656000 + flags = 1 + data = length 480, hash A5F1E987 + sample 70: + time = 1680000 + flags = 1 + data = length 288, hash 453A1267 + sample 71: + time = 1704000 + flags = 1 + data = length 288, hash 7C6C2EA9 + sample 72: + time = 1728000 + flags = 1 + data = length 336, hash F4BFECA4 + sample 73: + time = 1752000 + flags = 1 + data = length 336, hash 751A395A + sample 74: + time = 1776000 + flags = 1 + data = length 336, hash EE38DB02 + sample 75: + time = 1800000 + flags = 1 + data = length 336, hash F18837E2 + sample 76: + time = 1824000 + flags = 1 + data = length 336, hash ED36B78E + sample 77: + time = 1848000 + flags = 1 + data = length 336, hash B3D28289 + sample 78: + time = 1872000 + flags = 1 + data = length 288, hash 8BDE28E1 + sample 79: + time = 1896000 + flags = 1 + data = length 336, hash CFD5E966 + sample 80: + time = 1920000 + flags = 1 + data = length 288, hash DC08E267 + sample 81: + time = 1944000 + flags = 1 + data = length 336, hash 6530CB78 + sample 82: + time = 1968000 + flags = 1 + data = length 336, hash 6CC6636E + sample 83: + time = 1992000 + flags = 1 + data = length 336, hash 613047C1 + sample 84: + time = 2016000 + flags = 1 + data = length 288, hash CDC747BF + sample 85: + time = 2040000 + flags = 1 + data = length 336, hash AF22AA74 + sample 86: + time = 2064000 + flags = 1 + data = length 384, hash 82F326AA + sample 87: + time = 2088000 + flags = 1 + data = length 384, hash EDA26C4D + sample 88: + time = 2112000 + flags = 1 + data = length 336, hash 94C643DC + sample 89: + time = 2136000 + flags = 1 + data = length 288, hash CB5D9C40 + sample 90: + time = 2160000 + flags = 1 + data = length 336, hash 1E69DE3F + sample 91: + time = 2184000 + flags = 1 + data = length 336, hash 7E472219 + sample 92: + time = 2208000 + flags = 1 + data = length 336, hash DA47B9FA + sample 93: + time = 2232000 + flags = 1 + data = length 336, hash DD0ABB7C + sample 94: + time = 2256000 + flags = 1 + data = length 288, hash DBF93FAC + sample 95: + time = 2280000 + flags = 1 + data = length 336, hash 243F4B2 + sample 96: + time = 2304000 + flags = 1 + data = length 336, hash 2E881490 + sample 97: + time = 2328000 + flags = 1 + data = length 288, hash 1C28C8BE + sample 98: + time = 2352000 + flags = 1 + data = length 336, hash C73E5D30 + sample 99: + time = 2376000 + flags = 1 + data = length 288, hash 98B5BFF6 + sample 100: + time = 2400000 + flags = 1 + data = length 336, hash E0135533 + sample 101: + time = 2424000 + flags = 1 + data = length 336, hash D13C9DBC + sample 102: + time = 2448000 + flags = 1 + data = length 336, hash 63D524CA + sample 103: + time = 2472000 + flags = 1 + data = length 288, hash A28514C3 + sample 104: + time = 2496000 + flags = 1 + data = length 336, hash 72B647FF + sample 105: + time = 2520000 + flags = 1 + data = length 336, hash 8F740AB1 + sample 106: + time = 2544000 + flags = 1 + data = length 336, hash 5E3C7E93 + sample 107: + time = 2568000 + flags = 1 + data = length 336, hash 121B913B + sample 108: + time = 2592000 + flags = 1 + data = length 336, hash 578FCCF2 + sample 109: + time = 2616000 + flags = 1 + data = length 336, hash 5B5823DE + sample 110: + time = 2640000 + flags = 1 + data = length 384, hash D8B83F78 + sample 111: + time = 2664000 + flags = 1 + data = length 240, hash E649682F + sample 112: + time = 2688000 + flags = 1 + data = length 96, hash C559A6F4 + sample 113: + time = 2712000 + flags = 1 + data = length 96, hash 792796BC + sample 114: + time = 2736000 + flags = 1 + data = length 120, hash 8172CD0E + sample 115: + time = 2760000 + flags = 1 + data = length 120, hash F562B52F + sample 116: + time = 2784000 + flags = 1 + data = length 96, hash FF8D5B98 +tracksEnded = true diff --git a/testdata/src/test/assets/mp3/bear-id3.mp3 b/testdata/src/test/assets/mp3/bear-id3.mp3 new file mode 100644 index 0000000000..9bd4f72be8 Binary files /dev/null and b/testdata/src/test/assets/mp3/bear-id3.mp3 differ diff --git a/testdata/src/test/assets/mp3/bear-vbr-no-seek-table.mp3 b/testdata/src/test/assets/mp3/bear-vbr-no-seek-table.mp3 new file mode 100644 index 0000000000..010fc3776f Binary files /dev/null and b/testdata/src/test/assets/mp3/bear-vbr-no-seek-table.mp3 differ diff --git a/testdata/src/test/assets/mp3/bear-vbr-no-seek-table.mp3.0.dump b/testdata/src/test/assets/mp3/bear-vbr-no-seek-table.mp3.0.dump new file mode 100644 index 0000000000..90edd282b2 --- /dev/null +++ b/testdata/src/test/assets/mp3/bear-vbr-no-seek-table.mp3.0.dump @@ -0,0 +1,485 @@ +seekMap: + isSeekable = true + duration = 2808000 + getPosition(0) = [[timeUs=0, position=224]] + getPosition(1) = [[timeUs=0, position=224], [timeUs=120000, position=2048]] + getPosition(1404000) = [[timeUs=1320000, position=18896], [timeUs=1440000, position=20528]] + getPosition(2808000) = [[timeUs=2760000, position=38168]] +numberOfTracks = 1 +track 0: + total output bytes = 38160 + sample count = 117 + format 0: + sampleMimeType = audio/mpeg + maxInputSize = 4096 + channelCount = 2 + sampleRate = 48000 + sample 0: + time = 0 + flags = 1 + data = length 96, hash 1F161542 + sample 1: + time = 24000 + flags = 1 + data = length 768, hash CD1DC50F + sample 2: + time = 48000 + flags = 1 + data = length 336, hash 3F64124B + sample 3: + time = 72000 + flags = 1 + data = length 336, hash 8FFED94E + sample 4: + time = 96000 + flags = 1 + data = length 288, hash 9CD77D47 + sample 5: + time = 120000 + flags = 1 + data = length 384, hash 24607BB5 + sample 6: + time = 144000 + flags = 1 + data = length 480, hash 4937EBAB + sample 7: + time = 168000 + flags = 1 + data = length 336, hash 546342B1 + sample 8: + time = 192000 + flags = 1 + data = length 336, hash 79E0923F + sample 9: + time = 216000 + flags = 1 + data = length 336, hash AB1F3948 + sample 10: + time = 240000 + flags = 1 + data = length 336, hash C3A4D888 + sample 11: + time = 264000 + flags = 1 + data = length 288, hash 7867DA45 + sample 12: + time = 288000 + flags = 1 + data = length 336, hash B1240B73 + sample 13: + time = 312000 + flags = 1 + data = length 336, hash 94CFCD35 + sample 14: + time = 336000 + flags = 1 + data = length 288, hash 94F412C + sample 15: + time = 360000 + flags = 1 + data = length 336, hash A1D9FF41 + sample 16: + time = 384000 + flags = 1 + data = length 288, hash 2A8DA21B + sample 17: + time = 408000 + flags = 1 + data = length 336, hash 6A429CE + sample 18: + time = 432000 + flags = 1 + data = length 336, hash 68853982 + sample 19: + time = 456000 + flags = 1 + data = length 384, hash 1D6F779C + sample 20: + time = 480000 + flags = 1 + data = length 480, hash 6B31EBEE + sample 21: + time = 504000 + flags = 1 + data = length 336, hash 888335BE + sample 22: + time = 528000 + flags = 1 + data = length 336, hash 6072AC8B + sample 23: + time = 552000 + flags = 1 + data = length 336, hash C9D24234 + sample 24: + time = 576000 + flags = 1 + data = length 288, hash 52BF4D1E + sample 25: + time = 600000 + flags = 1 + data = length 336, hash F93F4F0 + sample 26: + time = 624000 + flags = 1 + data = length 336, hash 8617688A + sample 27: + time = 648000 + flags = 1 + data = length 480, hash FAB0D31B + sample 28: + time = 672000 + flags = 1 + data = length 384, hash FA4B53E2 + sample 29: + time = 696000 + flags = 1 + data = length 336, hash 8C435F6A + sample 30: + time = 720000 + flags = 1 + data = length 336, hash 60D3F80C + sample 31: + time = 744000 + flags = 1 + data = length 336, hash DC15B68B + sample 32: + time = 768000 + flags = 1 + data = length 288, hash FF3DF141 + sample 33: + time = 792000 + flags = 1 + data = length 336, hash A64B3042 + sample 34: + time = 816000 + flags = 1 + data = length 336, hash ACA622A1 + sample 35: + time = 840000 + flags = 1 + data = length 288, hash 3E34B8D4 + sample 36: + time = 864000 + flags = 1 + data = length 288, hash 9B96F72A + sample 37: + time = 888000 + flags = 1 + data = length 336, hash E917C122 + sample 38: + time = 912000 + flags = 1 + data = length 336, hash 10ED1470 + sample 39: + time = 936000 + flags = 1 + data = length 288, hash 706B8A7C + sample 40: + time = 960000 + flags = 1 + data = length 336, hash 71FFE4A0 + sample 41: + time = 984000 + flags = 1 + data = length 336, hash D4160463 + sample 42: + time = 1008000 + flags = 1 + data = length 336, hash EC557B14 + sample 43: + time = 1032000 + flags = 1 + data = length 288, hash 5598CF8B + sample 44: + time = 1056000 + flags = 1 + data = length 336, hash 7E0AB41 + sample 45: + time = 1080000 + flags = 1 + data = length 336, hash 1C585FEF + sample 46: + time = 1104000 + flags = 1 + data = length 336, hash A4A4855E + sample 47: + time = 1128000 + flags = 1 + data = length 336, hash CECA51D3 + sample 48: + time = 1152000 + flags = 1 + data = length 288, hash 2D362DC5 + sample 49: + time = 1176000 + flags = 1 + data = length 336, hash 9EB2609D + sample 50: + time = 1200000 + flags = 1 + data = length 336, hash 28FFB3FE + sample 51: + time = 1224000 + flags = 1 + data = length 288, hash 2AA2D216 + sample 52: + time = 1248000 + flags = 1 + data = length 336, hash CDBC7032 + sample 53: + time = 1272000 + flags = 1 + data = length 336, hash 25B13FE7 + sample 54: + time = 1296000 + flags = 1 + data = length 336, hash DB6BB1E + sample 55: + time = 1320000 + flags = 1 + data = length 336, hash EBE951F4 + sample 56: + time = 1344000 + flags = 1 + data = length 288, hash 9E2EBFF7 + sample 57: + time = 1368000 + flags = 1 + data = length 336, hash 36A7D455 + sample 58: + time = 1392000 + flags = 1 + data = length 336, hash 84545F8C + sample 59: + time = 1416000 + flags = 1 + data = length 336, hash F66F3045 + sample 60: + time = 1440000 + flags = 1 + data = length 576, hash 5AB089EA + sample 61: + time = 1464000 + flags = 1 + data = length 336, hash 8868086 + sample 62: + time = 1488000 + flags = 1 + data = length 336, hash D5EB6D63 + sample 63: + time = 1512000 + flags = 1 + data = length 288, hash 7A5374B7 + sample 64: + time = 1536000 + flags = 1 + data = length 336, hash BEB27A75 + sample 65: + time = 1560000 + flags = 1 + data = length 336, hash E251E0FD + sample 66: + time = 1584000 + flags = 1 + data = length 288, hash D54C970 + sample 67: + time = 1608000 + flags = 1 + data = length 336, hash 52C473B9 + sample 68: + time = 1632000 + flags = 1 + data = length 336, hash F5F13334 + sample 69: + time = 1656000 + flags = 1 + data = length 480, hash A5F1E987 + sample 70: + time = 1680000 + flags = 1 + data = length 288, hash 453A1267 + sample 71: + time = 1704000 + flags = 1 + data = length 288, hash 7C6C2EA9 + sample 72: + time = 1728000 + flags = 1 + data = length 336, hash F4BFECA4 + sample 73: + time = 1752000 + flags = 1 + data = length 336, hash 751A395A + sample 74: + time = 1776000 + flags = 1 + data = length 336, hash EE38DB02 + sample 75: + time = 1800000 + flags = 1 + data = length 336, hash F18837E2 + sample 76: + time = 1824000 + flags = 1 + data = length 336, hash ED36B78E + sample 77: + time = 1848000 + flags = 1 + data = length 336, hash B3D28289 + sample 78: + time = 1872000 + flags = 1 + data = length 288, hash 8BDE28E1 + sample 79: + time = 1896000 + flags = 1 + data = length 336, hash CFD5E966 + sample 80: + time = 1920000 + flags = 1 + data = length 288, hash DC08E267 + sample 81: + time = 1944000 + flags = 1 + data = length 336, hash 6530CB78 + sample 82: + time = 1968000 + flags = 1 + data = length 336, hash 6CC6636E + sample 83: + time = 1992000 + flags = 1 + data = length 336, hash 613047C1 + sample 84: + time = 2016000 + flags = 1 + data = length 288, hash CDC747BF + sample 85: + time = 2040000 + flags = 1 + data = length 336, hash AF22AA74 + sample 86: + time = 2064000 + flags = 1 + data = length 384, hash 82F326AA + sample 87: + time = 2088000 + flags = 1 + data = length 384, hash EDA26C4D + sample 88: + time = 2112000 + flags = 1 + data = length 336, hash 94C643DC + sample 89: + time = 2136000 + flags = 1 + data = length 288, hash CB5D9C40 + sample 90: + time = 2160000 + flags = 1 + data = length 336, hash 1E69DE3F + sample 91: + time = 2184000 + flags = 1 + data = length 336, hash 7E472219 + sample 92: + time = 2208000 + flags = 1 + data = length 336, hash DA47B9FA + sample 93: + time = 2232000 + flags = 1 + data = length 336, hash DD0ABB7C + sample 94: + time = 2256000 + flags = 1 + data = length 288, hash DBF93FAC + sample 95: + time = 2280000 + flags = 1 + data = length 336, hash 243F4B2 + sample 96: + time = 2304000 + flags = 1 + data = length 336, hash 2E881490 + sample 97: + time = 2328000 + flags = 1 + data = length 288, hash 1C28C8BE + sample 98: + time = 2352000 + flags = 1 + data = length 336, hash C73E5D30 + sample 99: + time = 2376000 + flags = 1 + data = length 288, hash 98B5BFF6 + sample 100: + time = 2400000 + flags = 1 + data = length 336, hash E0135533 + sample 101: + time = 2424000 + flags = 1 + data = length 336, hash D13C9DBC + sample 102: + time = 2448000 + flags = 1 + data = length 336, hash 63D524CA + sample 103: + time = 2472000 + flags = 1 + data = length 288, hash A28514C3 + sample 104: + time = 2496000 + flags = 1 + data = length 336, hash 72B647FF + sample 105: + time = 2520000 + flags = 1 + data = length 336, hash 8F740AB1 + sample 106: + time = 2544000 + flags = 1 + data = length 336, hash 5E3C7E93 + sample 107: + time = 2568000 + flags = 1 + data = length 336, hash 121B913B + sample 108: + time = 2592000 + flags = 1 + data = length 336, hash 578FCCF2 + sample 109: + time = 2616000 + flags = 1 + data = length 336, hash 5B5823DE + sample 110: + time = 2640000 + flags = 1 + data = length 384, hash D8B83F78 + sample 111: + time = 2664000 + flags = 1 + data = length 240, hash E649682F + sample 112: + time = 2688000 + flags = 1 + data = length 96, hash C559A6F4 + sample 113: + time = 2712000 + flags = 1 + data = length 96, hash 792796BC + sample 114: + time = 2736000 + flags = 1 + data = length 120, hash 8172CD0E + sample 115: + time = 2760000 + flags = 1 + data = length 120, hash F562B52F + sample 116: + time = 2784000 + flags = 1 + data = length 96, hash FF8D5B98 +tracksEnded = true diff --git a/testdata/src/test/assets/mp3/bear-vbr-no-seek-table.mp3.1.dump b/testdata/src/test/assets/mp3/bear-vbr-no-seek-table.mp3.1.dump new file mode 100644 index 0000000000..8a06d2bba7 --- /dev/null +++ b/testdata/src/test/assets/mp3/bear-vbr-no-seek-table.mp3.1.dump @@ -0,0 +1,345 @@ +seekMap: + isSeekable = true + duration = 2808000 + getPosition(0) = [[timeUs=0, position=224]] + getPosition(1) = [[timeUs=0, position=224], [timeUs=120000, position=2048]] + getPosition(1404000) = [[timeUs=1320000, position=18896], [timeUs=1440000, position=20528]] + getPosition(2808000) = [[timeUs=2760000, position=38168]] +numberOfTracks = 1 +track 0: + total output bytes = 25920 + sample count = 82 + format 0: + sampleMimeType = audio/mpeg + maxInputSize = 4096 + channelCount = 2 + sampleRate = 48000 + sample 0: + time = 840000 + flags = 1 + data = length 288, hash 3E34B8D4 + sample 1: + time = 864000 + flags = 1 + data = length 288, hash 9B96F72A + sample 2: + time = 888000 + flags = 1 + data = length 336, hash E917C122 + sample 3: + time = 912000 + flags = 1 + data = length 336, hash 10ED1470 + sample 4: + time = 936000 + flags = 1 + data = length 288, hash 706B8A7C + sample 5: + time = 960000 + flags = 1 + data = length 336, hash 71FFE4A0 + sample 6: + time = 984000 + flags = 1 + data = length 336, hash D4160463 + sample 7: + time = 1008000 + flags = 1 + data = length 336, hash EC557B14 + sample 8: + time = 1032000 + flags = 1 + data = length 288, hash 5598CF8B + sample 9: + time = 1056000 + flags = 1 + data = length 336, hash 7E0AB41 + sample 10: + time = 1080000 + flags = 1 + data = length 336, hash 1C585FEF + sample 11: + time = 1104000 + flags = 1 + data = length 336, hash A4A4855E + sample 12: + time = 1128000 + flags = 1 + data = length 336, hash CECA51D3 + sample 13: + time = 1152000 + flags = 1 + data = length 288, hash 2D362DC5 + sample 14: + time = 1176000 + flags = 1 + data = length 336, hash 9EB2609D + sample 15: + time = 1200000 + flags = 1 + data = length 336, hash 28FFB3FE + sample 16: + time = 1224000 + flags = 1 + data = length 288, hash 2AA2D216 + sample 17: + time = 1248000 + flags = 1 + data = length 336, hash CDBC7032 + sample 18: + time = 1272000 + flags = 1 + data = length 336, hash 25B13FE7 + sample 19: + time = 1296000 + flags = 1 + data = length 336, hash DB6BB1E + sample 20: + time = 1320000 + flags = 1 + data = length 336, hash EBE951F4 + sample 21: + time = 1344000 + flags = 1 + data = length 288, hash 9E2EBFF7 + sample 22: + time = 1368000 + flags = 1 + data = length 336, hash 36A7D455 + sample 23: + time = 1392000 + flags = 1 + data = length 336, hash 84545F8C + sample 24: + time = 1416000 + flags = 1 + data = length 336, hash F66F3045 + sample 25: + time = 1440000 + flags = 1 + data = length 576, hash 5AB089EA + sample 26: + time = 1464000 + flags = 1 + data = length 336, hash 8868086 + sample 27: + time = 1488000 + flags = 1 + data = length 336, hash D5EB6D63 + sample 28: + time = 1512000 + flags = 1 + data = length 288, hash 7A5374B7 + sample 29: + time = 1536000 + flags = 1 + data = length 336, hash BEB27A75 + sample 30: + time = 1560000 + flags = 1 + data = length 336, hash E251E0FD + sample 31: + time = 1584000 + flags = 1 + data = length 288, hash D54C970 + sample 32: + time = 1608000 + flags = 1 + data = length 336, hash 52C473B9 + sample 33: + time = 1632000 + flags = 1 + data = length 336, hash F5F13334 + sample 34: + time = 1656000 + flags = 1 + data = length 480, hash A5F1E987 + sample 35: + time = 1680000 + flags = 1 + data = length 288, hash 453A1267 + sample 36: + time = 1704000 + flags = 1 + data = length 288, hash 7C6C2EA9 + sample 37: + time = 1728000 + flags = 1 + data = length 336, hash F4BFECA4 + sample 38: + time = 1752000 + flags = 1 + data = length 336, hash 751A395A + sample 39: + time = 1776000 + flags = 1 + data = length 336, hash EE38DB02 + sample 40: + time = 1800000 + flags = 1 + data = length 336, hash F18837E2 + sample 41: + time = 1824000 + flags = 1 + data = length 336, hash ED36B78E + sample 42: + time = 1848000 + flags = 1 + data = length 336, hash B3D28289 + sample 43: + time = 1872000 + flags = 1 + data = length 288, hash 8BDE28E1 + sample 44: + time = 1896000 + flags = 1 + data = length 336, hash CFD5E966 + sample 45: + time = 1920000 + flags = 1 + data = length 288, hash DC08E267 + sample 46: + time = 1944000 + flags = 1 + data = length 336, hash 6530CB78 + sample 47: + time = 1968000 + flags = 1 + data = length 336, hash 6CC6636E + sample 48: + time = 1992000 + flags = 1 + data = length 336, hash 613047C1 + sample 49: + time = 2016000 + flags = 1 + data = length 288, hash CDC747BF + sample 50: + time = 2040000 + flags = 1 + data = length 336, hash AF22AA74 + sample 51: + time = 2064000 + flags = 1 + data = length 384, hash 82F326AA + sample 52: + time = 2088000 + flags = 1 + data = length 384, hash EDA26C4D + sample 53: + time = 2112000 + flags = 1 + data = length 336, hash 94C643DC + sample 54: + time = 2136000 + flags = 1 + data = length 288, hash CB5D9C40 + sample 55: + time = 2160000 + flags = 1 + data = length 336, hash 1E69DE3F + sample 56: + time = 2184000 + flags = 1 + data = length 336, hash 7E472219 + sample 57: + time = 2208000 + flags = 1 + data = length 336, hash DA47B9FA + sample 58: + time = 2232000 + flags = 1 + data = length 336, hash DD0ABB7C + sample 59: + time = 2256000 + flags = 1 + data = length 288, hash DBF93FAC + sample 60: + time = 2280000 + flags = 1 + data = length 336, hash 243F4B2 + sample 61: + time = 2304000 + flags = 1 + data = length 336, hash 2E881490 + sample 62: + time = 2328000 + flags = 1 + data = length 288, hash 1C28C8BE + sample 63: + time = 2352000 + flags = 1 + data = length 336, hash C73E5D30 + sample 64: + time = 2376000 + flags = 1 + data = length 288, hash 98B5BFF6 + sample 65: + time = 2400000 + flags = 1 + data = length 336, hash E0135533 + sample 66: + time = 2424000 + flags = 1 + data = length 336, hash D13C9DBC + sample 67: + time = 2448000 + flags = 1 + data = length 336, hash 63D524CA + sample 68: + time = 2472000 + flags = 1 + data = length 288, hash A28514C3 + sample 69: + time = 2496000 + flags = 1 + data = length 336, hash 72B647FF + sample 70: + time = 2520000 + flags = 1 + data = length 336, hash 8F740AB1 + sample 71: + time = 2544000 + flags = 1 + data = length 336, hash 5E3C7E93 + sample 72: + time = 2568000 + flags = 1 + data = length 336, hash 121B913B + sample 73: + time = 2592000 + flags = 1 + data = length 336, hash 578FCCF2 + sample 74: + time = 2616000 + flags = 1 + data = length 336, hash 5B5823DE + sample 75: + time = 2640000 + flags = 1 + data = length 384, hash D8B83F78 + sample 76: + time = 2664000 + flags = 1 + data = length 240, hash E649682F + sample 77: + time = 2688000 + flags = 1 + data = length 96, hash C559A6F4 + sample 78: + time = 2712000 + flags = 1 + data = length 96, hash 792796BC + sample 79: + time = 2736000 + flags = 1 + data = length 120, hash 8172CD0E + sample 80: + time = 2760000 + flags = 1 + data = length 120, hash F562B52F + sample 81: + time = 2784000 + flags = 1 + data = length 96, hash FF8D5B98 +tracksEnded = true diff --git a/testdata/src/test/assets/mp3/bear-vbr-no-seek-table.mp3.2.dump b/testdata/src/test/assets/mp3/bear-vbr-no-seek-table.mp3.2.dump new file mode 100644 index 0000000000..32a412e8ca --- /dev/null +++ b/testdata/src/test/assets/mp3/bear-vbr-no-seek-table.mp3.2.dump @@ -0,0 +1,185 @@ +seekMap: + isSeekable = true + duration = 2808000 + getPosition(0) = [[timeUs=0, position=224]] + getPosition(1) = [[timeUs=0, position=224], [timeUs=120000, position=2048]] + getPosition(1404000) = [[timeUs=1320000, position=18896], [timeUs=1440000, position=20528]] + getPosition(2808000) = [[timeUs=2760000, position=38168]] +numberOfTracks = 1 +track 0: + total output bytes = 12624 + sample count = 42 + format 0: + sampleMimeType = audio/mpeg + maxInputSize = 4096 + channelCount = 2 + sampleRate = 48000 + sample 0: + time = 1800000 + flags = 1 + data = length 336, hash F18837E2 + sample 1: + time = 1824000 + flags = 1 + data = length 336, hash ED36B78E + sample 2: + time = 1848000 + flags = 1 + data = length 336, hash B3D28289 + sample 3: + time = 1872000 + flags = 1 + data = length 288, hash 8BDE28E1 + sample 4: + time = 1896000 + flags = 1 + data = length 336, hash CFD5E966 + sample 5: + time = 1920000 + flags = 1 + data = length 288, hash DC08E267 + sample 6: + time = 1944000 + flags = 1 + data = length 336, hash 6530CB78 + sample 7: + time = 1968000 + flags = 1 + data = length 336, hash 6CC6636E + sample 8: + time = 1992000 + flags = 1 + data = length 336, hash 613047C1 + sample 9: + time = 2016000 + flags = 1 + data = length 288, hash CDC747BF + sample 10: + time = 2040000 + flags = 1 + data = length 336, hash AF22AA74 + sample 11: + time = 2064000 + flags = 1 + data = length 384, hash 82F326AA + sample 12: + time = 2088000 + flags = 1 + data = length 384, hash EDA26C4D + sample 13: + time = 2112000 + flags = 1 + data = length 336, hash 94C643DC + sample 14: + time = 2136000 + flags = 1 + data = length 288, hash CB5D9C40 + sample 15: + time = 2160000 + flags = 1 + data = length 336, hash 1E69DE3F + sample 16: + time = 2184000 + flags = 1 + data = length 336, hash 7E472219 + sample 17: + time = 2208000 + flags = 1 + data = length 336, hash DA47B9FA + sample 18: + time = 2232000 + flags = 1 + data = length 336, hash DD0ABB7C + sample 19: + time = 2256000 + flags = 1 + data = length 288, hash DBF93FAC + sample 20: + time = 2280000 + flags = 1 + data = length 336, hash 243F4B2 + sample 21: + time = 2304000 + flags = 1 + data = length 336, hash 2E881490 + sample 22: + time = 2328000 + flags = 1 + data = length 288, hash 1C28C8BE + sample 23: + time = 2352000 + flags = 1 + data = length 336, hash C73E5D30 + sample 24: + time = 2376000 + flags = 1 + data = length 288, hash 98B5BFF6 + sample 25: + time = 2400000 + flags = 1 + data = length 336, hash E0135533 + sample 26: + time = 2424000 + flags = 1 + data = length 336, hash D13C9DBC + sample 27: + time = 2448000 + flags = 1 + data = length 336, hash 63D524CA + sample 28: + time = 2472000 + flags = 1 + data = length 288, hash A28514C3 + sample 29: + time = 2496000 + flags = 1 + data = length 336, hash 72B647FF + sample 30: + time = 2520000 + flags = 1 + data = length 336, hash 8F740AB1 + sample 31: + time = 2544000 + flags = 1 + data = length 336, hash 5E3C7E93 + sample 32: + time = 2568000 + flags = 1 + data = length 336, hash 121B913B + sample 33: + time = 2592000 + flags = 1 + data = length 336, hash 578FCCF2 + sample 34: + time = 2616000 + flags = 1 + data = length 336, hash 5B5823DE + sample 35: + time = 2640000 + flags = 1 + data = length 384, hash D8B83F78 + sample 36: + time = 2664000 + flags = 1 + data = length 240, hash E649682F + sample 37: + time = 2688000 + flags = 1 + data = length 96, hash C559A6F4 + sample 38: + time = 2712000 + flags = 1 + data = length 96, hash 792796BC + sample 39: + time = 2736000 + flags = 1 + data = length 120, hash 8172CD0E + sample 40: + time = 2760000 + flags = 1 + data = length 120, hash F562B52F + sample 41: + time = 2784000 + flags = 1 + data = length 96, hash FF8D5B98 +tracksEnded = true diff --git a/testdata/src/test/assets/mp3/bear-vbr-no-seek-table.mp3.3.dump b/testdata/src/test/assets/mp3/bear-vbr-no-seek-table.mp3.3.dump new file mode 100644 index 0000000000..41e499b4a8 --- /dev/null +++ b/testdata/src/test/assets/mp3/bear-vbr-no-seek-table.mp3.3.dump @@ -0,0 +1,25 @@ +seekMap: + isSeekable = true + duration = 2808000 + getPosition(0) = [[timeUs=0, position=224]] + getPosition(1) = [[timeUs=0, position=224], [timeUs=120000, position=2048]] + getPosition(1404000) = [[timeUs=1320000, position=18896], [timeUs=1440000, position=20528]] + getPosition(2808000) = [[timeUs=2760000, position=38168]] +numberOfTracks = 1 +track 0: + total output bytes = 216 + sample count = 2 + format 0: + sampleMimeType = audio/mpeg + maxInputSize = 4096 + channelCount = 2 + sampleRate = 48000 + sample 0: + time = 2760000 + flags = 1 + data = length 120, hash F562B52F + sample 1: + time = 2784000 + flags = 1 + data = length 96, hash FF8D5B98 +tracksEnded = true diff --git a/testdata/src/test/assets/mp3/bear-vbr-no-seek-table.mp3.unknown_length.dump b/testdata/src/test/assets/mp3/bear-vbr-no-seek-table.mp3.unknown_length.dump new file mode 100644 index 0000000000..90edd282b2 --- /dev/null +++ b/testdata/src/test/assets/mp3/bear-vbr-no-seek-table.mp3.unknown_length.dump @@ -0,0 +1,485 @@ +seekMap: + isSeekable = true + duration = 2808000 + getPosition(0) = [[timeUs=0, position=224]] + getPosition(1) = [[timeUs=0, position=224], [timeUs=120000, position=2048]] + getPosition(1404000) = [[timeUs=1320000, position=18896], [timeUs=1440000, position=20528]] + getPosition(2808000) = [[timeUs=2760000, position=38168]] +numberOfTracks = 1 +track 0: + total output bytes = 38160 + sample count = 117 + format 0: + sampleMimeType = audio/mpeg + maxInputSize = 4096 + channelCount = 2 + sampleRate = 48000 + sample 0: + time = 0 + flags = 1 + data = length 96, hash 1F161542 + sample 1: + time = 24000 + flags = 1 + data = length 768, hash CD1DC50F + sample 2: + time = 48000 + flags = 1 + data = length 336, hash 3F64124B + sample 3: + time = 72000 + flags = 1 + data = length 336, hash 8FFED94E + sample 4: + time = 96000 + flags = 1 + data = length 288, hash 9CD77D47 + sample 5: + time = 120000 + flags = 1 + data = length 384, hash 24607BB5 + sample 6: + time = 144000 + flags = 1 + data = length 480, hash 4937EBAB + sample 7: + time = 168000 + flags = 1 + data = length 336, hash 546342B1 + sample 8: + time = 192000 + flags = 1 + data = length 336, hash 79E0923F + sample 9: + time = 216000 + flags = 1 + data = length 336, hash AB1F3948 + sample 10: + time = 240000 + flags = 1 + data = length 336, hash C3A4D888 + sample 11: + time = 264000 + flags = 1 + data = length 288, hash 7867DA45 + sample 12: + time = 288000 + flags = 1 + data = length 336, hash B1240B73 + sample 13: + time = 312000 + flags = 1 + data = length 336, hash 94CFCD35 + sample 14: + time = 336000 + flags = 1 + data = length 288, hash 94F412C + sample 15: + time = 360000 + flags = 1 + data = length 336, hash A1D9FF41 + sample 16: + time = 384000 + flags = 1 + data = length 288, hash 2A8DA21B + sample 17: + time = 408000 + flags = 1 + data = length 336, hash 6A429CE + sample 18: + time = 432000 + flags = 1 + data = length 336, hash 68853982 + sample 19: + time = 456000 + flags = 1 + data = length 384, hash 1D6F779C + sample 20: + time = 480000 + flags = 1 + data = length 480, hash 6B31EBEE + sample 21: + time = 504000 + flags = 1 + data = length 336, hash 888335BE + sample 22: + time = 528000 + flags = 1 + data = length 336, hash 6072AC8B + sample 23: + time = 552000 + flags = 1 + data = length 336, hash C9D24234 + sample 24: + time = 576000 + flags = 1 + data = length 288, hash 52BF4D1E + sample 25: + time = 600000 + flags = 1 + data = length 336, hash F93F4F0 + sample 26: + time = 624000 + flags = 1 + data = length 336, hash 8617688A + sample 27: + time = 648000 + flags = 1 + data = length 480, hash FAB0D31B + sample 28: + time = 672000 + flags = 1 + data = length 384, hash FA4B53E2 + sample 29: + time = 696000 + flags = 1 + data = length 336, hash 8C435F6A + sample 30: + time = 720000 + flags = 1 + data = length 336, hash 60D3F80C + sample 31: + time = 744000 + flags = 1 + data = length 336, hash DC15B68B + sample 32: + time = 768000 + flags = 1 + data = length 288, hash FF3DF141 + sample 33: + time = 792000 + flags = 1 + data = length 336, hash A64B3042 + sample 34: + time = 816000 + flags = 1 + data = length 336, hash ACA622A1 + sample 35: + time = 840000 + flags = 1 + data = length 288, hash 3E34B8D4 + sample 36: + time = 864000 + flags = 1 + data = length 288, hash 9B96F72A + sample 37: + time = 888000 + flags = 1 + data = length 336, hash E917C122 + sample 38: + time = 912000 + flags = 1 + data = length 336, hash 10ED1470 + sample 39: + time = 936000 + flags = 1 + data = length 288, hash 706B8A7C + sample 40: + time = 960000 + flags = 1 + data = length 336, hash 71FFE4A0 + sample 41: + time = 984000 + flags = 1 + data = length 336, hash D4160463 + sample 42: + time = 1008000 + flags = 1 + data = length 336, hash EC557B14 + sample 43: + time = 1032000 + flags = 1 + data = length 288, hash 5598CF8B + sample 44: + time = 1056000 + flags = 1 + data = length 336, hash 7E0AB41 + sample 45: + time = 1080000 + flags = 1 + data = length 336, hash 1C585FEF + sample 46: + time = 1104000 + flags = 1 + data = length 336, hash A4A4855E + sample 47: + time = 1128000 + flags = 1 + data = length 336, hash CECA51D3 + sample 48: + time = 1152000 + flags = 1 + data = length 288, hash 2D362DC5 + sample 49: + time = 1176000 + flags = 1 + data = length 336, hash 9EB2609D + sample 50: + time = 1200000 + flags = 1 + data = length 336, hash 28FFB3FE + sample 51: + time = 1224000 + flags = 1 + data = length 288, hash 2AA2D216 + sample 52: + time = 1248000 + flags = 1 + data = length 336, hash CDBC7032 + sample 53: + time = 1272000 + flags = 1 + data = length 336, hash 25B13FE7 + sample 54: + time = 1296000 + flags = 1 + data = length 336, hash DB6BB1E + sample 55: + time = 1320000 + flags = 1 + data = length 336, hash EBE951F4 + sample 56: + time = 1344000 + flags = 1 + data = length 288, hash 9E2EBFF7 + sample 57: + time = 1368000 + flags = 1 + data = length 336, hash 36A7D455 + sample 58: + time = 1392000 + flags = 1 + data = length 336, hash 84545F8C + sample 59: + time = 1416000 + flags = 1 + data = length 336, hash F66F3045 + sample 60: + time = 1440000 + flags = 1 + data = length 576, hash 5AB089EA + sample 61: + time = 1464000 + flags = 1 + data = length 336, hash 8868086 + sample 62: + time = 1488000 + flags = 1 + data = length 336, hash D5EB6D63 + sample 63: + time = 1512000 + flags = 1 + data = length 288, hash 7A5374B7 + sample 64: + time = 1536000 + flags = 1 + data = length 336, hash BEB27A75 + sample 65: + time = 1560000 + flags = 1 + data = length 336, hash E251E0FD + sample 66: + time = 1584000 + flags = 1 + data = length 288, hash D54C970 + sample 67: + time = 1608000 + flags = 1 + data = length 336, hash 52C473B9 + sample 68: + time = 1632000 + flags = 1 + data = length 336, hash F5F13334 + sample 69: + time = 1656000 + flags = 1 + data = length 480, hash A5F1E987 + sample 70: + time = 1680000 + flags = 1 + data = length 288, hash 453A1267 + sample 71: + time = 1704000 + flags = 1 + data = length 288, hash 7C6C2EA9 + sample 72: + time = 1728000 + flags = 1 + data = length 336, hash F4BFECA4 + sample 73: + time = 1752000 + flags = 1 + data = length 336, hash 751A395A + sample 74: + time = 1776000 + flags = 1 + data = length 336, hash EE38DB02 + sample 75: + time = 1800000 + flags = 1 + data = length 336, hash F18837E2 + sample 76: + time = 1824000 + flags = 1 + data = length 336, hash ED36B78E + sample 77: + time = 1848000 + flags = 1 + data = length 336, hash B3D28289 + sample 78: + time = 1872000 + flags = 1 + data = length 288, hash 8BDE28E1 + sample 79: + time = 1896000 + flags = 1 + data = length 336, hash CFD5E966 + sample 80: + time = 1920000 + flags = 1 + data = length 288, hash DC08E267 + sample 81: + time = 1944000 + flags = 1 + data = length 336, hash 6530CB78 + sample 82: + time = 1968000 + flags = 1 + data = length 336, hash 6CC6636E + sample 83: + time = 1992000 + flags = 1 + data = length 336, hash 613047C1 + sample 84: + time = 2016000 + flags = 1 + data = length 288, hash CDC747BF + sample 85: + time = 2040000 + flags = 1 + data = length 336, hash AF22AA74 + sample 86: + time = 2064000 + flags = 1 + data = length 384, hash 82F326AA + sample 87: + time = 2088000 + flags = 1 + data = length 384, hash EDA26C4D + sample 88: + time = 2112000 + flags = 1 + data = length 336, hash 94C643DC + sample 89: + time = 2136000 + flags = 1 + data = length 288, hash CB5D9C40 + sample 90: + time = 2160000 + flags = 1 + data = length 336, hash 1E69DE3F + sample 91: + time = 2184000 + flags = 1 + data = length 336, hash 7E472219 + sample 92: + time = 2208000 + flags = 1 + data = length 336, hash DA47B9FA + sample 93: + time = 2232000 + flags = 1 + data = length 336, hash DD0ABB7C + sample 94: + time = 2256000 + flags = 1 + data = length 288, hash DBF93FAC + sample 95: + time = 2280000 + flags = 1 + data = length 336, hash 243F4B2 + sample 96: + time = 2304000 + flags = 1 + data = length 336, hash 2E881490 + sample 97: + time = 2328000 + flags = 1 + data = length 288, hash 1C28C8BE + sample 98: + time = 2352000 + flags = 1 + data = length 336, hash C73E5D30 + sample 99: + time = 2376000 + flags = 1 + data = length 288, hash 98B5BFF6 + sample 100: + time = 2400000 + flags = 1 + data = length 336, hash E0135533 + sample 101: + time = 2424000 + flags = 1 + data = length 336, hash D13C9DBC + sample 102: + time = 2448000 + flags = 1 + data = length 336, hash 63D524CA + sample 103: + time = 2472000 + flags = 1 + data = length 288, hash A28514C3 + sample 104: + time = 2496000 + flags = 1 + data = length 336, hash 72B647FF + sample 105: + time = 2520000 + flags = 1 + data = length 336, hash 8F740AB1 + sample 106: + time = 2544000 + flags = 1 + data = length 336, hash 5E3C7E93 + sample 107: + time = 2568000 + flags = 1 + data = length 336, hash 121B913B + sample 108: + time = 2592000 + flags = 1 + data = length 336, hash 578FCCF2 + sample 109: + time = 2616000 + flags = 1 + data = length 336, hash 5B5823DE + sample 110: + time = 2640000 + flags = 1 + data = length 384, hash D8B83F78 + sample 111: + time = 2664000 + flags = 1 + data = length 240, hash E649682F + sample 112: + time = 2688000 + flags = 1 + data = length 96, hash C559A6F4 + sample 113: + time = 2712000 + flags = 1 + data = length 96, hash 792796BC + sample 114: + time = 2736000 + flags = 1 + data = length 120, hash 8172CD0E + sample 115: + time = 2760000 + flags = 1 + data = length 120, hash F562B52F + sample 116: + time = 2784000 + flags = 1 + data = length 96, hash FF8D5B98 +tracksEnded = true diff --git a/testdata/src/test/assets/mp3/bear-vbr-xing-header.mp3 b/testdata/src/test/assets/mp3/bear-vbr-xing-header.mp3 new file mode 100644 index 0000000000..8b32dcdd58 Binary files /dev/null and b/testdata/src/test/assets/mp3/bear-vbr-xing-header.mp3 differ diff --git a/testdata/src/test/assets/mp3/bear-vbr-xing-header.mp3.0.dump b/testdata/src/test/assets/mp3/bear-vbr-xing-header.mp3.0.dump new file mode 100644 index 0000000000..20a69e34a8 --- /dev/null +++ b/testdata/src/test/assets/mp3/bear-vbr-xing-header.mp3.0.dump @@ -0,0 +1,488 @@ +seekMap: + isSeekable = true + duration = 2808000 + getPosition(0) = [[timeUs=0, position=237]] + getPosition(1) = [[timeUs=1, position=237]] + getPosition(1404000) = [[timeUs=1404000, position=20120]] + getPosition(2808000) = [[timeUs=2808000, position=38396]] +numberOfTracks = 1 +track 0: + total output bytes = 38160 + sample count = 117 + format 0: + sampleMimeType = audio/mpeg + maxInputSize = 4096 + channelCount = 2 + sampleRate = 48000 + encoderDelay = 576 + encoderPadding = 576 + metadata = entries=[TSSE: description=null: value=Lavf58.29.100] + sample 0: + time = 0 + flags = 1 + data = length 96, hash 1F161542 + sample 1: + time = 24000 + flags = 1 + data = length 768, hash CD1DC50F + sample 2: + time = 48000 + flags = 1 + data = length 336, hash 3F64124B + sample 3: + time = 72000 + flags = 1 + data = length 336, hash 8FFED94E + sample 4: + time = 96000 + flags = 1 + data = length 288, hash 9CD77D47 + sample 5: + time = 120000 + flags = 1 + data = length 384, hash 24607BB5 + sample 6: + time = 144000 + flags = 1 + data = length 480, hash 4937EBAB + sample 7: + time = 168000 + flags = 1 + data = length 336, hash 546342B1 + sample 8: + time = 192000 + flags = 1 + data = length 336, hash 79E0923F + sample 9: + time = 216000 + flags = 1 + data = length 336, hash AB1F3948 + sample 10: + time = 240000 + flags = 1 + data = length 336, hash C3A4D888 + sample 11: + time = 264000 + flags = 1 + data = length 288, hash 7867DA45 + sample 12: + time = 288000 + flags = 1 + data = length 336, hash B1240B73 + sample 13: + time = 312000 + flags = 1 + data = length 336, hash 94CFCD35 + sample 14: + time = 336000 + flags = 1 + data = length 288, hash 94F412C + sample 15: + time = 360000 + flags = 1 + data = length 336, hash A1D9FF41 + sample 16: + time = 384000 + flags = 1 + data = length 288, hash 2A8DA21B + sample 17: + time = 408000 + flags = 1 + data = length 336, hash 6A429CE + sample 18: + time = 432000 + flags = 1 + data = length 336, hash 68853982 + sample 19: + time = 456000 + flags = 1 + data = length 384, hash 1D6F779C + sample 20: + time = 480000 + flags = 1 + data = length 480, hash 6B31EBEE + sample 21: + time = 504000 + flags = 1 + data = length 336, hash 888335BE + sample 22: + time = 528000 + flags = 1 + data = length 336, hash 6072AC8B + sample 23: + time = 552000 + flags = 1 + data = length 336, hash C9D24234 + sample 24: + time = 576000 + flags = 1 + data = length 288, hash 52BF4D1E + sample 25: + time = 600000 + flags = 1 + data = length 336, hash F93F4F0 + sample 26: + time = 624000 + flags = 1 + data = length 336, hash 8617688A + sample 27: + time = 648000 + flags = 1 + data = length 480, hash FAB0D31B + sample 28: + time = 672000 + flags = 1 + data = length 384, hash FA4B53E2 + sample 29: + time = 696000 + flags = 1 + data = length 336, hash 8C435F6A + sample 30: + time = 720000 + flags = 1 + data = length 336, hash 60D3F80C + sample 31: + time = 744000 + flags = 1 + data = length 336, hash DC15B68B + sample 32: + time = 768000 + flags = 1 + data = length 288, hash FF3DF141 + sample 33: + time = 792000 + flags = 1 + data = length 336, hash A64B3042 + sample 34: + time = 816000 + flags = 1 + data = length 336, hash ACA622A1 + sample 35: + time = 840000 + flags = 1 + data = length 288, hash 3E34B8D4 + sample 36: + time = 864000 + flags = 1 + data = length 288, hash 9B96F72A + sample 37: + time = 888000 + flags = 1 + data = length 336, hash E917C122 + sample 38: + time = 912000 + flags = 1 + data = length 336, hash 10ED1470 + sample 39: + time = 936000 + flags = 1 + data = length 288, hash 706B8A7C + sample 40: + time = 960000 + flags = 1 + data = length 336, hash 71FFE4A0 + sample 41: + time = 984000 + flags = 1 + data = length 336, hash D4160463 + sample 42: + time = 1008000 + flags = 1 + data = length 336, hash EC557B14 + sample 43: + time = 1032000 + flags = 1 + data = length 288, hash 5598CF8B + sample 44: + time = 1056000 + flags = 1 + data = length 336, hash 7E0AB41 + sample 45: + time = 1080000 + flags = 1 + data = length 336, hash 1C585FEF + sample 46: + time = 1104000 + flags = 1 + data = length 336, hash A4A4855E + sample 47: + time = 1128000 + flags = 1 + data = length 336, hash CECA51D3 + sample 48: + time = 1152000 + flags = 1 + data = length 288, hash 2D362DC5 + sample 49: + time = 1176000 + flags = 1 + data = length 336, hash 9EB2609D + sample 50: + time = 1200000 + flags = 1 + data = length 336, hash 28FFB3FE + sample 51: + time = 1224000 + flags = 1 + data = length 288, hash 2AA2D216 + sample 52: + time = 1248000 + flags = 1 + data = length 336, hash CDBC7032 + sample 53: + time = 1272000 + flags = 1 + data = length 336, hash 25B13FE7 + sample 54: + time = 1296000 + flags = 1 + data = length 336, hash DB6BB1E + sample 55: + time = 1320000 + flags = 1 + data = length 336, hash EBE951F4 + sample 56: + time = 1344000 + flags = 1 + data = length 288, hash 9E2EBFF7 + sample 57: + time = 1368000 + flags = 1 + data = length 336, hash 36A7D455 + sample 58: + time = 1392000 + flags = 1 + data = length 336, hash 84545F8C + sample 59: + time = 1416000 + flags = 1 + data = length 336, hash F66F3045 + sample 60: + time = 1440000 + flags = 1 + data = length 576, hash 5AB089EA + sample 61: + time = 1464000 + flags = 1 + data = length 336, hash 8868086 + sample 62: + time = 1488000 + flags = 1 + data = length 336, hash D5EB6D63 + sample 63: + time = 1512000 + flags = 1 + data = length 288, hash 7A5374B7 + sample 64: + time = 1536000 + flags = 1 + data = length 336, hash BEB27A75 + sample 65: + time = 1560000 + flags = 1 + data = length 336, hash E251E0FD + sample 66: + time = 1584000 + flags = 1 + data = length 288, hash D54C970 + sample 67: + time = 1608000 + flags = 1 + data = length 336, hash 52C473B9 + sample 68: + time = 1632000 + flags = 1 + data = length 336, hash F5F13334 + sample 69: + time = 1656000 + flags = 1 + data = length 480, hash A5F1E987 + sample 70: + time = 1680000 + flags = 1 + data = length 288, hash 453A1267 + sample 71: + time = 1704000 + flags = 1 + data = length 288, hash 7C6C2EA9 + sample 72: + time = 1728000 + flags = 1 + data = length 336, hash F4BFECA4 + sample 73: + time = 1752000 + flags = 1 + data = length 336, hash 751A395A + sample 74: + time = 1776000 + flags = 1 + data = length 336, hash EE38DB02 + sample 75: + time = 1800000 + flags = 1 + data = length 336, hash F18837E2 + sample 76: + time = 1824000 + flags = 1 + data = length 336, hash ED36B78E + sample 77: + time = 1848000 + flags = 1 + data = length 336, hash B3D28289 + sample 78: + time = 1872000 + flags = 1 + data = length 288, hash 8BDE28E1 + sample 79: + time = 1896000 + flags = 1 + data = length 336, hash CFD5E966 + sample 80: + time = 1920000 + flags = 1 + data = length 288, hash DC08E267 + sample 81: + time = 1944000 + flags = 1 + data = length 336, hash 6530CB78 + sample 82: + time = 1968000 + flags = 1 + data = length 336, hash 6CC6636E + sample 83: + time = 1992000 + flags = 1 + data = length 336, hash 613047C1 + sample 84: + time = 2016000 + flags = 1 + data = length 288, hash CDC747BF + sample 85: + time = 2040000 + flags = 1 + data = length 336, hash AF22AA74 + sample 86: + time = 2064000 + flags = 1 + data = length 384, hash 82F326AA + sample 87: + time = 2088000 + flags = 1 + data = length 384, hash EDA26C4D + sample 88: + time = 2112000 + flags = 1 + data = length 336, hash 94C643DC + sample 89: + time = 2136000 + flags = 1 + data = length 288, hash CB5D9C40 + sample 90: + time = 2160000 + flags = 1 + data = length 336, hash 1E69DE3F + sample 91: + time = 2184000 + flags = 1 + data = length 336, hash 7E472219 + sample 92: + time = 2208000 + flags = 1 + data = length 336, hash DA47B9FA + sample 93: + time = 2232000 + flags = 1 + data = length 336, hash DD0ABB7C + sample 94: + time = 2256000 + flags = 1 + data = length 288, hash DBF93FAC + sample 95: + time = 2280000 + flags = 1 + data = length 336, hash 243F4B2 + sample 96: + time = 2304000 + flags = 1 + data = length 336, hash 2E881490 + sample 97: + time = 2328000 + flags = 1 + data = length 288, hash 1C28C8BE + sample 98: + time = 2352000 + flags = 1 + data = length 336, hash C73E5D30 + sample 99: + time = 2376000 + flags = 1 + data = length 288, hash 98B5BFF6 + sample 100: + time = 2400000 + flags = 1 + data = length 336, hash E0135533 + sample 101: + time = 2424000 + flags = 1 + data = length 336, hash D13C9DBC + sample 102: + time = 2448000 + flags = 1 + data = length 336, hash 63D524CA + sample 103: + time = 2472000 + flags = 1 + data = length 288, hash A28514C3 + sample 104: + time = 2496000 + flags = 1 + data = length 336, hash 72B647FF + sample 105: + time = 2520000 + flags = 1 + data = length 336, hash 8F740AB1 + sample 106: + time = 2544000 + flags = 1 + data = length 336, hash 5E3C7E93 + sample 107: + time = 2568000 + flags = 1 + data = length 336, hash 121B913B + sample 108: + time = 2592000 + flags = 1 + data = length 336, hash 578FCCF2 + sample 109: + time = 2616000 + flags = 1 + data = length 336, hash 5B5823DE + sample 110: + time = 2640000 + flags = 1 + data = length 384, hash D8B83F78 + sample 111: + time = 2664000 + flags = 1 + data = length 240, hash E649682F + sample 112: + time = 2688000 + flags = 1 + data = length 96, hash C559A6F4 + sample 113: + time = 2712000 + flags = 1 + data = length 96, hash 792796BC + sample 114: + time = 2736000 + flags = 1 + data = length 120, hash 8172CD0E + sample 115: + time = 2760000 + flags = 1 + data = length 120, hash F562B52F + sample 116: + time = 2784000 + flags = 1 + data = length 96, hash FF8D5B98 +tracksEnded = true diff --git a/testdata/src/test/assets/mp3/bear-vbr-xing-header.mp3.1.dump b/testdata/src/test/assets/mp3/bear-vbr-xing-header.mp3.1.dump new file mode 100644 index 0000000000..ef3785beb3 --- /dev/null +++ b/testdata/src/test/assets/mp3/bear-vbr-xing-header.mp3.1.dump @@ -0,0 +1,328 @@ +seekMap: + isSeekable = true + duration = 2808000 + getPosition(0) = [[timeUs=0, position=237]] + getPosition(1) = [[timeUs=1, position=237]] + getPosition(1404000) = [[timeUs=1404000, position=20120]] + getPosition(2808000) = [[timeUs=2808000, position=38396]] +numberOfTracks = 1 +track 0: + total output bytes = 24384 + sample count = 77 + format 0: + sampleMimeType = audio/mpeg + maxInputSize = 4096 + channelCount = 2 + sampleRate = 48000 + encoderDelay = 576 + encoderPadding = 576 + metadata = entries=[TSSE: description=null: value=Lavf58.29.100] + sample 0: + time = 958041 + flags = 1 + data = length 336, hash 71FFE4A0 + sample 1: + time = 982041 + flags = 1 + data = length 336, hash D4160463 + sample 2: + time = 1006041 + flags = 1 + data = length 336, hash EC557B14 + sample 3: + time = 1030041 + flags = 1 + data = length 288, hash 5598CF8B + sample 4: + time = 1054041 + flags = 1 + data = length 336, hash 7E0AB41 + sample 5: + time = 1078041 + flags = 1 + data = length 336, hash 1C585FEF + sample 6: + time = 1102041 + flags = 1 + data = length 336, hash A4A4855E + sample 7: + time = 1126041 + flags = 1 + data = length 336, hash CECA51D3 + sample 8: + time = 1150041 + flags = 1 + data = length 288, hash 2D362DC5 + sample 9: + time = 1174041 + flags = 1 + data = length 336, hash 9EB2609D + sample 10: + time = 1198041 + flags = 1 + data = length 336, hash 28FFB3FE + sample 11: + time = 1222041 + flags = 1 + data = length 288, hash 2AA2D216 + sample 12: + time = 1246041 + flags = 1 + data = length 336, hash CDBC7032 + sample 13: + time = 1270041 + flags = 1 + data = length 336, hash 25B13FE7 + sample 14: + time = 1294041 + flags = 1 + data = length 336, hash DB6BB1E + sample 15: + time = 1318041 + flags = 1 + data = length 336, hash EBE951F4 + sample 16: + time = 1342041 + flags = 1 + data = length 288, hash 9E2EBFF7 + sample 17: + time = 1366041 + flags = 1 + data = length 336, hash 36A7D455 + sample 18: + time = 1390041 + flags = 1 + data = length 336, hash 84545F8C + sample 19: + time = 1414041 + flags = 1 + data = length 336, hash F66F3045 + sample 20: + time = 1438041 + flags = 1 + data = length 576, hash 5AB089EA + sample 21: + time = 1462041 + flags = 1 + data = length 336, hash 8868086 + sample 22: + time = 1486041 + flags = 1 + data = length 336, hash D5EB6D63 + sample 23: + time = 1510041 + flags = 1 + data = length 288, hash 7A5374B7 + sample 24: + time = 1534041 + flags = 1 + data = length 336, hash BEB27A75 + sample 25: + time = 1558041 + flags = 1 + data = length 336, hash E251E0FD + sample 26: + time = 1582041 + flags = 1 + data = length 288, hash D54C970 + sample 27: + time = 1606041 + flags = 1 + data = length 336, hash 52C473B9 + sample 28: + time = 1630041 + flags = 1 + data = length 336, hash F5F13334 + sample 29: + time = 1654041 + flags = 1 + data = length 480, hash A5F1E987 + sample 30: + time = 1678041 + flags = 1 + data = length 288, hash 453A1267 + sample 31: + time = 1702041 + flags = 1 + data = length 288, hash 7C6C2EA9 + sample 32: + time = 1726041 + flags = 1 + data = length 336, hash F4BFECA4 + sample 33: + time = 1750041 + flags = 1 + data = length 336, hash 751A395A + sample 34: + time = 1774041 + flags = 1 + data = length 336, hash EE38DB02 + sample 35: + time = 1798041 + flags = 1 + data = length 336, hash F18837E2 + sample 36: + time = 1822041 + flags = 1 + data = length 336, hash ED36B78E + sample 37: + time = 1846041 + flags = 1 + data = length 336, hash B3D28289 + sample 38: + time = 1870041 + flags = 1 + data = length 288, hash 8BDE28E1 + sample 39: + time = 1894041 + flags = 1 + data = length 336, hash CFD5E966 + sample 40: + time = 1918041 + flags = 1 + data = length 288, hash DC08E267 + sample 41: + time = 1942041 + flags = 1 + data = length 336, hash 6530CB78 + sample 42: + time = 1966041 + flags = 1 + data = length 336, hash 6CC6636E + sample 43: + time = 1990041 + flags = 1 + data = length 336, hash 613047C1 + sample 44: + time = 2014041 + flags = 1 + data = length 288, hash CDC747BF + sample 45: + time = 2038041 + flags = 1 + data = length 336, hash AF22AA74 + sample 46: + time = 2062041 + flags = 1 + data = length 384, hash 82F326AA + sample 47: + time = 2086041 + flags = 1 + data = length 384, hash EDA26C4D + sample 48: + time = 2110041 + flags = 1 + data = length 336, hash 94C643DC + sample 49: + time = 2134041 + flags = 1 + data = length 288, hash CB5D9C40 + sample 50: + time = 2158041 + flags = 1 + data = length 336, hash 1E69DE3F + sample 51: + time = 2182041 + flags = 1 + data = length 336, hash 7E472219 + sample 52: + time = 2206041 + flags = 1 + data = length 336, hash DA47B9FA + sample 53: + time = 2230041 + flags = 1 + data = length 336, hash DD0ABB7C + sample 54: + time = 2254041 + flags = 1 + data = length 288, hash DBF93FAC + sample 55: + time = 2278041 + flags = 1 + data = length 336, hash 243F4B2 + sample 56: + time = 2302041 + flags = 1 + data = length 336, hash 2E881490 + sample 57: + time = 2326041 + flags = 1 + data = length 288, hash 1C28C8BE + sample 58: + time = 2350041 + flags = 1 + data = length 336, hash C73E5D30 + sample 59: + time = 2374041 + flags = 1 + data = length 288, hash 98B5BFF6 + sample 60: + time = 2398041 + flags = 1 + data = length 336, hash E0135533 + sample 61: + time = 2422041 + flags = 1 + data = length 336, hash D13C9DBC + sample 62: + time = 2446041 + flags = 1 + data = length 336, hash 63D524CA + sample 63: + time = 2470041 + flags = 1 + data = length 288, hash A28514C3 + sample 64: + time = 2494041 + flags = 1 + data = length 336, hash 72B647FF + sample 65: + time = 2518041 + flags = 1 + data = length 336, hash 8F740AB1 + sample 66: + time = 2542041 + flags = 1 + data = length 336, hash 5E3C7E93 + sample 67: + time = 2566041 + flags = 1 + data = length 336, hash 121B913B + sample 68: + time = 2590041 + flags = 1 + data = length 336, hash 578FCCF2 + sample 69: + time = 2614041 + flags = 1 + data = length 336, hash 5B5823DE + sample 70: + time = 2638041 + flags = 1 + data = length 384, hash D8B83F78 + sample 71: + time = 2662041 + flags = 1 + data = length 240, hash E649682F + sample 72: + time = 2686041 + flags = 1 + data = length 96, hash C559A6F4 + sample 73: + time = 2710041 + flags = 1 + data = length 96, hash 792796BC + sample 74: + time = 2734041 + flags = 1 + data = length 120, hash 8172CD0E + sample 75: + time = 2758041 + flags = 1 + data = length 120, hash F562B52F + sample 76: + time = 2782041 + flags = 1 + data = length 96, hash FF8D5B98 +tracksEnded = true diff --git a/testdata/src/test/assets/mp3/bear-vbr-xing-header.mp3.2.dump b/testdata/src/test/assets/mp3/bear-vbr-xing-header.mp3.2.dump new file mode 100644 index 0000000000..12697f6d12 --- /dev/null +++ b/testdata/src/test/assets/mp3/bear-vbr-xing-header.mp3.2.dump @@ -0,0 +1,172 @@ +seekMap: + isSeekable = true + duration = 2808000 + getPosition(0) = [[timeUs=0, position=237]] + getPosition(1) = [[timeUs=1, position=237]] + getPosition(1404000) = [[timeUs=1404000, position=20120]] + getPosition(2808000) = [[timeUs=2808000, position=38396]] +numberOfTracks = 1 +track 0: + total output bytes = 11328 + sample count = 38 + format 0: + sampleMimeType = audio/mpeg + maxInputSize = 4096 + channelCount = 2 + sampleRate = 48000 + encoderDelay = 576 + encoderPadding = 576 + metadata = entries=[TSSE: description=null: value=Lavf58.29.100] + sample 0: + time = 1886772 + flags = 1 + data = length 336, hash CFD5E966 + sample 1: + time = 1910772 + flags = 1 + data = length 288, hash DC08E267 + sample 2: + time = 1934772 + flags = 1 + data = length 336, hash 6530CB78 + sample 3: + time = 1958772 + flags = 1 + data = length 336, hash 6CC6636E + sample 4: + time = 1982772 + flags = 1 + data = length 336, hash 613047C1 + sample 5: + time = 2006772 + flags = 1 + data = length 288, hash CDC747BF + sample 6: + time = 2030772 + flags = 1 + data = length 336, hash AF22AA74 + sample 7: + time = 2054772 + flags = 1 + data = length 384, hash 82F326AA + sample 8: + time = 2078772 + flags = 1 + data = length 384, hash EDA26C4D + sample 9: + time = 2102772 + flags = 1 + data = length 336, hash 94C643DC + sample 10: + time = 2126772 + flags = 1 + data = length 288, hash CB5D9C40 + sample 11: + time = 2150772 + flags = 1 + data = length 336, hash 1E69DE3F + sample 12: + time = 2174772 + flags = 1 + data = length 336, hash 7E472219 + sample 13: + time = 2198772 + flags = 1 + data = length 336, hash DA47B9FA + sample 14: + time = 2222772 + flags = 1 + data = length 336, hash DD0ABB7C + sample 15: + time = 2246772 + flags = 1 + data = length 288, hash DBF93FAC + sample 16: + time = 2270772 + flags = 1 + data = length 336, hash 243F4B2 + sample 17: + time = 2294772 + flags = 1 + data = length 336, hash 2E881490 + sample 18: + time = 2318772 + flags = 1 + data = length 288, hash 1C28C8BE + sample 19: + time = 2342772 + flags = 1 + data = length 336, hash C73E5D30 + sample 20: + time = 2366772 + flags = 1 + data = length 288, hash 98B5BFF6 + sample 21: + time = 2390772 + flags = 1 + data = length 336, hash E0135533 + sample 22: + time = 2414772 + flags = 1 + data = length 336, hash D13C9DBC + sample 23: + time = 2438772 + flags = 1 + data = length 336, hash 63D524CA + sample 24: + time = 2462772 + flags = 1 + data = length 288, hash A28514C3 + sample 25: + time = 2486772 + flags = 1 + data = length 336, hash 72B647FF + sample 26: + time = 2510772 + flags = 1 + data = length 336, hash 8F740AB1 + sample 27: + time = 2534772 + flags = 1 + data = length 336, hash 5E3C7E93 + sample 28: + time = 2558772 + flags = 1 + data = length 336, hash 121B913B + sample 29: + time = 2582772 + flags = 1 + data = length 336, hash 578FCCF2 + sample 30: + time = 2606772 + flags = 1 + data = length 336, hash 5B5823DE + sample 31: + time = 2630772 + flags = 1 + data = length 384, hash D8B83F78 + sample 32: + time = 2654772 + flags = 1 + data = length 240, hash E649682F + sample 33: + time = 2678772 + flags = 1 + data = length 96, hash C559A6F4 + sample 34: + time = 2702772 + flags = 1 + data = length 96, hash 792796BC + sample 35: + time = 2726772 + flags = 1 + data = length 120, hash 8172CD0E + sample 36: + time = 2750772 + flags = 1 + data = length 120, hash F562B52F + sample 37: + time = 2774772 + flags = 1 + data = length 96, hash FF8D5B98 +tracksEnded = true diff --git a/testdata/src/test/assets/mp3/bear-vbr-xing-header.mp3.3.dump b/testdata/src/test/assets/mp3/bear-vbr-xing-header.mp3.3.dump new file mode 100644 index 0000000000..2ab479e633 --- /dev/null +++ b/testdata/src/test/assets/mp3/bear-vbr-xing-header.mp3.3.dump @@ -0,0 +1,20 @@ +seekMap: + isSeekable = true + duration = 2808000 + getPosition(0) = [[timeUs=0, position=237]] + getPosition(1) = [[timeUs=1, position=237]] + getPosition(1404000) = [[timeUs=1404000, position=20120]] + getPosition(2808000) = [[timeUs=2808000, position=38396]] +numberOfTracks = 1 +track 0: + total output bytes = 0 + sample count = 0 + format 0: + sampleMimeType = audio/mpeg + maxInputSize = 4096 + channelCount = 2 + sampleRate = 48000 + encoderDelay = 576 + encoderPadding = 576 + metadata = entries=[TSSE: description=null: value=Lavf58.29.100] +tracksEnded = true diff --git a/testdata/src/test/assets/mp3/bear-vbr-xing-header.mp3.unknown_length.dump b/testdata/src/test/assets/mp3/bear-vbr-xing-header.mp3.unknown_length.dump new file mode 100644 index 0000000000..20a69e34a8 --- /dev/null +++ b/testdata/src/test/assets/mp3/bear-vbr-xing-header.mp3.unknown_length.dump @@ -0,0 +1,488 @@ +seekMap: + isSeekable = true + duration = 2808000 + getPosition(0) = [[timeUs=0, position=237]] + getPosition(1) = [[timeUs=1, position=237]] + getPosition(1404000) = [[timeUs=1404000, position=20120]] + getPosition(2808000) = [[timeUs=2808000, position=38396]] +numberOfTracks = 1 +track 0: + total output bytes = 38160 + sample count = 117 + format 0: + sampleMimeType = audio/mpeg + maxInputSize = 4096 + channelCount = 2 + sampleRate = 48000 + encoderDelay = 576 + encoderPadding = 576 + metadata = entries=[TSSE: description=null: value=Lavf58.29.100] + sample 0: + time = 0 + flags = 1 + data = length 96, hash 1F161542 + sample 1: + time = 24000 + flags = 1 + data = length 768, hash CD1DC50F + sample 2: + time = 48000 + flags = 1 + data = length 336, hash 3F64124B + sample 3: + time = 72000 + flags = 1 + data = length 336, hash 8FFED94E + sample 4: + time = 96000 + flags = 1 + data = length 288, hash 9CD77D47 + sample 5: + time = 120000 + flags = 1 + data = length 384, hash 24607BB5 + sample 6: + time = 144000 + flags = 1 + data = length 480, hash 4937EBAB + sample 7: + time = 168000 + flags = 1 + data = length 336, hash 546342B1 + sample 8: + time = 192000 + flags = 1 + data = length 336, hash 79E0923F + sample 9: + time = 216000 + flags = 1 + data = length 336, hash AB1F3948 + sample 10: + time = 240000 + flags = 1 + data = length 336, hash C3A4D888 + sample 11: + time = 264000 + flags = 1 + data = length 288, hash 7867DA45 + sample 12: + time = 288000 + flags = 1 + data = length 336, hash B1240B73 + sample 13: + time = 312000 + flags = 1 + data = length 336, hash 94CFCD35 + sample 14: + time = 336000 + flags = 1 + data = length 288, hash 94F412C + sample 15: + time = 360000 + flags = 1 + data = length 336, hash A1D9FF41 + sample 16: + time = 384000 + flags = 1 + data = length 288, hash 2A8DA21B + sample 17: + time = 408000 + flags = 1 + data = length 336, hash 6A429CE + sample 18: + time = 432000 + flags = 1 + data = length 336, hash 68853982 + sample 19: + time = 456000 + flags = 1 + data = length 384, hash 1D6F779C + sample 20: + time = 480000 + flags = 1 + data = length 480, hash 6B31EBEE + sample 21: + time = 504000 + flags = 1 + data = length 336, hash 888335BE + sample 22: + time = 528000 + flags = 1 + data = length 336, hash 6072AC8B + sample 23: + time = 552000 + flags = 1 + data = length 336, hash C9D24234 + sample 24: + time = 576000 + flags = 1 + data = length 288, hash 52BF4D1E + sample 25: + time = 600000 + flags = 1 + data = length 336, hash F93F4F0 + sample 26: + time = 624000 + flags = 1 + data = length 336, hash 8617688A + sample 27: + time = 648000 + flags = 1 + data = length 480, hash FAB0D31B + sample 28: + time = 672000 + flags = 1 + data = length 384, hash FA4B53E2 + sample 29: + time = 696000 + flags = 1 + data = length 336, hash 8C435F6A + sample 30: + time = 720000 + flags = 1 + data = length 336, hash 60D3F80C + sample 31: + time = 744000 + flags = 1 + data = length 336, hash DC15B68B + sample 32: + time = 768000 + flags = 1 + data = length 288, hash FF3DF141 + sample 33: + time = 792000 + flags = 1 + data = length 336, hash A64B3042 + sample 34: + time = 816000 + flags = 1 + data = length 336, hash ACA622A1 + sample 35: + time = 840000 + flags = 1 + data = length 288, hash 3E34B8D4 + sample 36: + time = 864000 + flags = 1 + data = length 288, hash 9B96F72A + sample 37: + time = 888000 + flags = 1 + data = length 336, hash E917C122 + sample 38: + time = 912000 + flags = 1 + data = length 336, hash 10ED1470 + sample 39: + time = 936000 + flags = 1 + data = length 288, hash 706B8A7C + sample 40: + time = 960000 + flags = 1 + data = length 336, hash 71FFE4A0 + sample 41: + time = 984000 + flags = 1 + data = length 336, hash D4160463 + sample 42: + time = 1008000 + flags = 1 + data = length 336, hash EC557B14 + sample 43: + time = 1032000 + flags = 1 + data = length 288, hash 5598CF8B + sample 44: + time = 1056000 + flags = 1 + data = length 336, hash 7E0AB41 + sample 45: + time = 1080000 + flags = 1 + data = length 336, hash 1C585FEF + sample 46: + time = 1104000 + flags = 1 + data = length 336, hash A4A4855E + sample 47: + time = 1128000 + flags = 1 + data = length 336, hash CECA51D3 + sample 48: + time = 1152000 + flags = 1 + data = length 288, hash 2D362DC5 + sample 49: + time = 1176000 + flags = 1 + data = length 336, hash 9EB2609D + sample 50: + time = 1200000 + flags = 1 + data = length 336, hash 28FFB3FE + sample 51: + time = 1224000 + flags = 1 + data = length 288, hash 2AA2D216 + sample 52: + time = 1248000 + flags = 1 + data = length 336, hash CDBC7032 + sample 53: + time = 1272000 + flags = 1 + data = length 336, hash 25B13FE7 + sample 54: + time = 1296000 + flags = 1 + data = length 336, hash DB6BB1E + sample 55: + time = 1320000 + flags = 1 + data = length 336, hash EBE951F4 + sample 56: + time = 1344000 + flags = 1 + data = length 288, hash 9E2EBFF7 + sample 57: + time = 1368000 + flags = 1 + data = length 336, hash 36A7D455 + sample 58: + time = 1392000 + flags = 1 + data = length 336, hash 84545F8C + sample 59: + time = 1416000 + flags = 1 + data = length 336, hash F66F3045 + sample 60: + time = 1440000 + flags = 1 + data = length 576, hash 5AB089EA + sample 61: + time = 1464000 + flags = 1 + data = length 336, hash 8868086 + sample 62: + time = 1488000 + flags = 1 + data = length 336, hash D5EB6D63 + sample 63: + time = 1512000 + flags = 1 + data = length 288, hash 7A5374B7 + sample 64: + time = 1536000 + flags = 1 + data = length 336, hash BEB27A75 + sample 65: + time = 1560000 + flags = 1 + data = length 336, hash E251E0FD + sample 66: + time = 1584000 + flags = 1 + data = length 288, hash D54C970 + sample 67: + time = 1608000 + flags = 1 + data = length 336, hash 52C473B9 + sample 68: + time = 1632000 + flags = 1 + data = length 336, hash F5F13334 + sample 69: + time = 1656000 + flags = 1 + data = length 480, hash A5F1E987 + sample 70: + time = 1680000 + flags = 1 + data = length 288, hash 453A1267 + sample 71: + time = 1704000 + flags = 1 + data = length 288, hash 7C6C2EA9 + sample 72: + time = 1728000 + flags = 1 + data = length 336, hash F4BFECA4 + sample 73: + time = 1752000 + flags = 1 + data = length 336, hash 751A395A + sample 74: + time = 1776000 + flags = 1 + data = length 336, hash EE38DB02 + sample 75: + time = 1800000 + flags = 1 + data = length 336, hash F18837E2 + sample 76: + time = 1824000 + flags = 1 + data = length 336, hash ED36B78E + sample 77: + time = 1848000 + flags = 1 + data = length 336, hash B3D28289 + sample 78: + time = 1872000 + flags = 1 + data = length 288, hash 8BDE28E1 + sample 79: + time = 1896000 + flags = 1 + data = length 336, hash CFD5E966 + sample 80: + time = 1920000 + flags = 1 + data = length 288, hash DC08E267 + sample 81: + time = 1944000 + flags = 1 + data = length 336, hash 6530CB78 + sample 82: + time = 1968000 + flags = 1 + data = length 336, hash 6CC6636E + sample 83: + time = 1992000 + flags = 1 + data = length 336, hash 613047C1 + sample 84: + time = 2016000 + flags = 1 + data = length 288, hash CDC747BF + sample 85: + time = 2040000 + flags = 1 + data = length 336, hash AF22AA74 + sample 86: + time = 2064000 + flags = 1 + data = length 384, hash 82F326AA + sample 87: + time = 2088000 + flags = 1 + data = length 384, hash EDA26C4D + sample 88: + time = 2112000 + flags = 1 + data = length 336, hash 94C643DC + sample 89: + time = 2136000 + flags = 1 + data = length 288, hash CB5D9C40 + sample 90: + time = 2160000 + flags = 1 + data = length 336, hash 1E69DE3F + sample 91: + time = 2184000 + flags = 1 + data = length 336, hash 7E472219 + sample 92: + time = 2208000 + flags = 1 + data = length 336, hash DA47B9FA + sample 93: + time = 2232000 + flags = 1 + data = length 336, hash DD0ABB7C + sample 94: + time = 2256000 + flags = 1 + data = length 288, hash DBF93FAC + sample 95: + time = 2280000 + flags = 1 + data = length 336, hash 243F4B2 + sample 96: + time = 2304000 + flags = 1 + data = length 336, hash 2E881490 + sample 97: + time = 2328000 + flags = 1 + data = length 288, hash 1C28C8BE + sample 98: + time = 2352000 + flags = 1 + data = length 336, hash C73E5D30 + sample 99: + time = 2376000 + flags = 1 + data = length 288, hash 98B5BFF6 + sample 100: + time = 2400000 + flags = 1 + data = length 336, hash E0135533 + sample 101: + time = 2424000 + flags = 1 + data = length 336, hash D13C9DBC + sample 102: + time = 2448000 + flags = 1 + data = length 336, hash 63D524CA + sample 103: + time = 2472000 + flags = 1 + data = length 288, hash A28514C3 + sample 104: + time = 2496000 + flags = 1 + data = length 336, hash 72B647FF + sample 105: + time = 2520000 + flags = 1 + data = length 336, hash 8F740AB1 + sample 106: + time = 2544000 + flags = 1 + data = length 336, hash 5E3C7E93 + sample 107: + time = 2568000 + flags = 1 + data = length 336, hash 121B913B + sample 108: + time = 2592000 + flags = 1 + data = length 336, hash 578FCCF2 + sample 109: + time = 2616000 + flags = 1 + data = length 336, hash 5B5823DE + sample 110: + time = 2640000 + flags = 1 + data = length 384, hash D8B83F78 + sample 111: + time = 2664000 + flags = 1 + data = length 240, hash E649682F + sample 112: + time = 2688000 + flags = 1 + data = length 96, hash C559A6F4 + sample 113: + time = 2712000 + flags = 1 + data = length 96, hash 792796BC + sample 114: + time = 2736000 + flags = 1 + data = length 120, hash 8172CD0E + sample 115: + time = 2760000 + flags = 1 + data = length 120, hash F562B52F + sample 116: + time = 2784000 + flags = 1 + data = length 96, hash FF8D5B98 +tracksEnded = true diff --git a/library/core/src/test/assets/mp3/play-trimmed.mp3 b/testdata/src/test/assets/mp3/play-trimmed.mp3 similarity index 100% rename from library/core/src/test/assets/mp3/play-trimmed.mp3 rename to testdata/src/test/assets/mp3/play-trimmed.mp3 diff --git a/testdata/src/test/assets/mp3/play-trimmed.mp3.0.dump b/testdata/src/test/assets/mp3/play-trimmed.mp3.0.dump new file mode 100644 index 0000000000..0fd5622999 --- /dev/null +++ b/testdata/src/test/assets/mp3/play-trimmed.mp3.0.dump @@ -0,0 +1,21 @@ +seekMap: + isSeekable = true + duration = 26125 + getPosition(0) = [[timeUs=0, position=0]] + getPosition(1) = [[timeUs=0, position=0]] + getPosition(13062) = [[timeUs=0, position=0]] + getPosition(26125) = [[timeUs=0, position=0]] +numberOfTracks = 1 +track 0: + total output bytes = 418 + sample count = 1 + format 0: + sampleMimeType = audio/mpeg + maxInputSize = 4096 + channelCount = 2 + sampleRate = 44100 + sample 0: + time = 0 + flags = 1 + data = length 418, hash B819987 +tracksEnded = true diff --git a/testdata/src/test/assets/mp3/play-trimmed.mp3.1.dump b/testdata/src/test/assets/mp3/play-trimmed.mp3.1.dump new file mode 100644 index 0000000000..0fd5622999 --- /dev/null +++ b/testdata/src/test/assets/mp3/play-trimmed.mp3.1.dump @@ -0,0 +1,21 @@ +seekMap: + isSeekable = true + duration = 26125 + getPosition(0) = [[timeUs=0, position=0]] + getPosition(1) = [[timeUs=0, position=0]] + getPosition(13062) = [[timeUs=0, position=0]] + getPosition(26125) = [[timeUs=0, position=0]] +numberOfTracks = 1 +track 0: + total output bytes = 418 + sample count = 1 + format 0: + sampleMimeType = audio/mpeg + maxInputSize = 4096 + channelCount = 2 + sampleRate = 44100 + sample 0: + time = 0 + flags = 1 + data = length 418, hash B819987 +tracksEnded = true diff --git a/testdata/src/test/assets/mp3/play-trimmed.mp3.2.dump b/testdata/src/test/assets/mp3/play-trimmed.mp3.2.dump new file mode 100644 index 0000000000..0fd5622999 --- /dev/null +++ b/testdata/src/test/assets/mp3/play-trimmed.mp3.2.dump @@ -0,0 +1,21 @@ +seekMap: + isSeekable = true + duration = 26125 + getPosition(0) = [[timeUs=0, position=0]] + getPosition(1) = [[timeUs=0, position=0]] + getPosition(13062) = [[timeUs=0, position=0]] + getPosition(26125) = [[timeUs=0, position=0]] +numberOfTracks = 1 +track 0: + total output bytes = 418 + sample count = 1 + format 0: + sampleMimeType = audio/mpeg + maxInputSize = 4096 + channelCount = 2 + sampleRate = 44100 + sample 0: + time = 0 + flags = 1 + data = length 418, hash B819987 +tracksEnded = true diff --git a/testdata/src/test/assets/mp3/play-trimmed.mp3.3.dump b/testdata/src/test/assets/mp3/play-trimmed.mp3.3.dump new file mode 100644 index 0000000000..0fd5622999 --- /dev/null +++ b/testdata/src/test/assets/mp3/play-trimmed.mp3.3.dump @@ -0,0 +1,21 @@ +seekMap: + isSeekable = true + duration = 26125 + getPosition(0) = [[timeUs=0, position=0]] + getPosition(1) = [[timeUs=0, position=0]] + getPosition(13062) = [[timeUs=0, position=0]] + getPosition(26125) = [[timeUs=0, position=0]] +numberOfTracks = 1 +track 0: + total output bytes = 418 + sample count = 1 + format 0: + sampleMimeType = audio/mpeg + maxInputSize = 4096 + channelCount = 2 + sampleRate = 44100 + sample 0: + time = 0 + flags = 1 + data = length 418, hash B819987 +tracksEnded = true diff --git a/testdata/src/test/assets/mp3/play-trimmed.mp3.unknown_length.dump b/testdata/src/test/assets/mp3/play-trimmed.mp3.unknown_length.dump new file mode 100644 index 0000000000..2e747d9b21 --- /dev/null +++ b/testdata/src/test/assets/mp3/play-trimmed.mp3.unknown_length.dump @@ -0,0 +1,18 @@ +seekMap: + isSeekable = false + duration = UNSET TIME + getPosition(0) = [[timeUs=0, position=0]] +numberOfTracks = 1 +track 0: + total output bytes = 418 + sample count = 1 + format 0: + sampleMimeType = audio/mpeg + maxInputSize = 4096 + channelCount = 2 + sampleRate = 44100 + sample 0: + time = 0 + flags = 1 + data = length 418, hash B819987 +tracksEnded = true diff --git a/testdata/src/test/assets/mp4/midroll-5s.mp4 b/testdata/src/test/assets/mp4/midroll-5s.mp4 new file mode 100644 index 0000000000..b7f574a31d Binary files /dev/null and b/testdata/src/test/assets/mp4/midroll-5s.mp4 differ diff --git a/testdata/src/test/assets/mp4/postroll-5s.mp4 b/testdata/src/test/assets/mp4/postroll-5s.mp4 new file mode 100644 index 0000000000..23057d59e8 Binary files /dev/null and b/testdata/src/test/assets/mp4/postroll-5s.mp4 differ diff --git a/testdata/src/test/assets/mp4/preroll-5s.mp4 b/testdata/src/test/assets/mp4/preroll-5s.mp4 new file mode 100644 index 0000000000..d2e63d0e30 Binary files /dev/null and b/testdata/src/test/assets/mp4/preroll-5s.mp4 differ diff --git a/library/core/src/test/assets/mp4/sample.mp4 b/testdata/src/test/assets/mp4/sample.mp4 similarity index 100% rename from library/core/src/test/assets/mp4/sample.mp4 rename to testdata/src/test/assets/mp4/sample.mp4 diff --git a/library/core/src/test/assets/mp4/sample.mp4.0.dump b/testdata/src/test/assets/mp4/sample.mp4.0.dump similarity index 91% rename from library/core/src/test/assets/mp4/sample.mp4.0.dump rename to testdata/src/test/assets/mp4/sample.mp4.0.dump index 37e1054f79..c1d6530965 100644 --- a/library/core/src/test/assets/mp4/sample.mp4.0.dump +++ b/testdata/src/test/assets/mp4/sample.mp4.0.dump @@ -2,33 +2,23 @@ seekMap: isSeekable = true duration = 1024000 getPosition(0) = [[timeUs=0, position=48]] + getPosition(1) = [[timeUs=0, position=48]] + getPosition(512000) = [[timeUs=0, position=48]] + getPosition(1024000) = [[timeUs=0, position=48]] numberOfTracks = 2 track 0: - format: - bitrate = -1 + total output bytes = 89876 + sample count = 30 + format 0: id = 1 - containerMimeType = null sampleMimeType = video/avc maxInputSize = 36722 width = 1080 height = 720 frameRate = 29.970028 - 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 29, hash 4746B5D9 data = length 10, hash 7A0D0F2B - total output bytes = 89876 - sample count = 30 sample 0: time = 0 flags = 1 @@ -150,30 +140,19 @@ track 0: flags = 536870912 data = length 568, hash 4FE5C8EA track 1: - format: - bitrate = -1 - id = 2 - containerMimeType = null - sampleMimeType = audio/mp4a-latm - maxInputSize = 294 - 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: - data = length 2, hash 5F7 total output bytes = 9529 sample count = 45 + format 0: + id = 2 + sampleMimeType = audio/mp4a-latm + codecs = mp4a.40.2 + maxInputSize = 294 + channelCount = 1 + sampleRate = 44100 + language = und + metadata = entries=[TSSE: description=null: value=Lavf56.1.0] + initializationData: + data = length 2, hash 5F7 sample 0: time = 44000 flags = 1 diff --git a/library/core/src/test/assets/mp4/sample.mp4.1.dump b/testdata/src/test/assets/mp4/sample.mp4.1.dump similarity index 90% rename from library/core/src/test/assets/mp4/sample.mp4.1.dump rename to testdata/src/test/assets/mp4/sample.mp4.1.dump index 6284e85034..0cba489724 100644 --- a/library/core/src/test/assets/mp4/sample.mp4.1.dump +++ b/testdata/src/test/assets/mp4/sample.mp4.1.dump @@ -2,33 +2,23 @@ seekMap: isSeekable = true duration = 1024000 getPosition(0) = [[timeUs=0, position=48]] + getPosition(1) = [[timeUs=0, position=48]] + getPosition(512000) = [[timeUs=0, position=48]] + getPosition(1024000) = [[timeUs=0, position=48]] numberOfTracks = 2 track 0: - format: - bitrate = -1 + total output bytes = 89876 + sample count = 30 + format 0: id = 1 - containerMimeType = null sampleMimeType = video/avc maxInputSize = 36722 width = 1080 height = 720 frameRate = 29.970028 - 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 29, hash 4746B5D9 data = length 10, hash 7A0D0F2B - total output bytes = 89876 - sample count = 30 sample 0: time = 0 flags = 1 @@ -150,30 +140,19 @@ track 0: flags = 536870912 data = length 568, hash 4FE5C8EA track 1: - format: - bitrate = -1 - id = 2 - containerMimeType = null - sampleMimeType = audio/mp4a-latm - maxInputSize = 294 - 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: - data = length 2, hash 5F7 total output bytes = 7464 sample count = 33 + format 0: + id = 2 + sampleMimeType = audio/mp4a-latm + codecs = mp4a.40.2 + maxInputSize = 294 + channelCount = 1 + sampleRate = 44100 + language = und + metadata = entries=[TSSE: description=null: value=Lavf56.1.0] + initializationData: + data = length 2, hash 5F7 sample 0: time = 322639 flags = 1 diff --git a/library/core/src/test/assets/mp4/sample.mp4.2.dump b/testdata/src/test/assets/mp4/sample.mp4.2.dump similarity index 87% rename from library/core/src/test/assets/mp4/sample.mp4.2.dump rename to testdata/src/test/assets/mp4/sample.mp4.2.dump index 15b56a036f..453c1677bf 100644 --- a/library/core/src/test/assets/mp4/sample.mp4.2.dump +++ b/testdata/src/test/assets/mp4/sample.mp4.2.dump @@ -2,33 +2,23 @@ seekMap: isSeekable = true duration = 1024000 getPosition(0) = [[timeUs=0, position=48]] + getPosition(1) = [[timeUs=0, position=48]] + getPosition(512000) = [[timeUs=0, position=48]] + getPosition(1024000) = [[timeUs=0, position=48]] numberOfTracks = 2 track 0: - format: - bitrate = -1 + total output bytes = 89876 + sample count = 30 + format 0: id = 1 - containerMimeType = null sampleMimeType = video/avc maxInputSize = 36722 width = 1080 height = 720 frameRate = 29.970028 - 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 29, hash 4746B5D9 data = length 10, hash 7A0D0F2B - total output bytes = 89876 - sample count = 30 sample 0: time = 0 flags = 1 @@ -150,30 +140,19 @@ track 0: flags = 536870912 data = length 568, hash 4FE5C8EA track 1: - format: - bitrate = -1 - id = 2 - containerMimeType = null - sampleMimeType = audio/mp4a-latm - maxInputSize = 294 - 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: - data = length 2, hash 5F7 total output bytes = 4019 sample count = 18 + format 0: + id = 2 + sampleMimeType = audio/mp4a-latm + codecs = mp4a.40.2 + maxInputSize = 294 + channelCount = 1 + sampleRate = 44100 + language = und + metadata = entries=[TSSE: description=null: value=Lavf56.1.0] + initializationData: + data = length 2, hash 5F7 sample 0: time = 670938 flags = 1 diff --git a/library/core/src/test/assets/mp4/sample.mp4.3.dump b/testdata/src/test/assets/mp4/sample.mp4.3.dump similarity index 84% rename from library/core/src/test/assets/mp4/sample.mp4.3.dump rename to testdata/src/test/assets/mp4/sample.mp4.3.dump index 073d5c774a..0fe653ac2c 100644 --- a/library/core/src/test/assets/mp4/sample.mp4.3.dump +++ b/testdata/src/test/assets/mp4/sample.mp4.3.dump @@ -2,33 +2,23 @@ seekMap: isSeekable = true duration = 1024000 getPosition(0) = [[timeUs=0, position=48]] + getPosition(1) = [[timeUs=0, position=48]] + getPosition(512000) = [[timeUs=0, position=48]] + getPosition(1024000) = [[timeUs=0, position=48]] numberOfTracks = 2 track 0: - format: - bitrate = -1 + total output bytes = 89876 + sample count = 30 + format 0: id = 1 - containerMimeType = null sampleMimeType = video/avc maxInputSize = 36722 width = 1080 height = 720 frameRate = 29.970028 - 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 29, hash 4746B5D9 data = length 10, hash 7A0D0F2B - total output bytes = 89876 - sample count = 30 sample 0: time = 0 flags = 1 @@ -150,30 +140,19 @@ track 0: flags = 536870912 data = length 568, hash 4FE5C8EA track 1: - format: - bitrate = -1 - id = 2 - containerMimeType = null - sampleMimeType = audio/mp4a-latm - maxInputSize = 294 - 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: - data = length 2, hash 5F7 total output bytes = 470 sample count = 3 + format 0: + id = 2 + sampleMimeType = audio/mp4a-latm + codecs = mp4a.40.2 + maxInputSize = 294 + channelCount = 1 + sampleRate = 44100 + language = und + metadata = entries=[TSSE: description=null: value=Lavf56.1.0] + initializationData: + data = length 2, hash 5F7 sample 0: time = 1019238 flags = 1 diff --git a/testdata/src/test/assets/mp4/sample.mp4.unknown_length.dump b/testdata/src/test/assets/mp4/sample.mp4.unknown_length.dump new file mode 100644 index 0000000000..c1d6530965 --- /dev/null +++ b/testdata/src/test/assets/mp4/sample.mp4.unknown_length.dump @@ -0,0 +1,336 @@ +seekMap: + isSeekable = true + duration = 1024000 + getPosition(0) = [[timeUs=0, position=48]] + getPosition(1) = [[timeUs=0, position=48]] + getPosition(512000) = [[timeUs=0, position=48]] + getPosition(1024000) = [[timeUs=0, position=48]] +numberOfTracks = 2 +track 0: + total output bytes = 89876 + sample count = 30 + format 0: + id = 1 + sampleMimeType = video/avc + maxInputSize = 36722 + width = 1080 + height = 720 + frameRate = 29.970028 + initializationData: + data = length 29, hash 4746B5D9 + data = length 10, hash 7A0D0F2B + sample 0: + time = 0 + flags = 1 + data = length 36692, hash D216076E + sample 1: + time = 66733 + flags = 0 + data = length 5312, hash D45D3CA0 + sample 2: + time = 33366 + flags = 0 + data = length 599, hash 1BE7812D + sample 3: + time = 200200 + flags = 0 + data = length 7735, hash 4490F110 + sample 4: + time = 133466 + flags = 0 + data = length 987, hash 560B5036 + sample 5: + time = 100100 + flags = 0 + data = length 673, hash ED7CD8C7 + sample 6: + time = 166833 + flags = 0 + data = length 523, hash 3020DF50 + sample 7: + time = 333666 + flags = 0 + data = length 6061, hash 736C72B2 + sample 8: + time = 266933 + flags = 0 + data = length 992, hash FE132F23 + sample 9: + time = 233566 + flags = 0 + data = length 623, hash 5B2C1816 + sample 10: + time = 300300 + flags = 0 + data = length 421, hash 742E69C1 + sample 11: + time = 433766 + flags = 0 + data = length 4899, hash F72F86A1 + sample 12: + time = 400400 + flags = 0 + data = length 568, hash 519A8E50 + sample 13: + time = 367033 + flags = 0 + data = length 620, hash 3990AA39 + sample 14: + time = 567233 + flags = 0 + data = length 5450, hash F06EC4AA + sample 15: + time = 500500 + flags = 0 + data = length 1051, hash 92DFA63A + sample 16: + time = 467133 + flags = 0 + data = length 874, hash 69587FB4 + sample 17: + time = 533866 + flags = 0 + data = length 781, hash 36BE495B + sample 18: + time = 700700 + flags = 0 + data = length 4725, hash AC0C8CD3 + sample 19: + time = 633966 + flags = 0 + data = length 1022, hash 5D8BFF34 + sample 20: + time = 600600 + flags = 0 + data = length 790, hash 99413A99 + sample 21: + time = 667333 + flags = 0 + data = length 610, hash 5E129290 + sample 22: + time = 834166 + flags = 0 + data = length 2751, hash 769974CB + sample 23: + time = 767433 + flags = 0 + data = length 745, hash B78A477A + sample 24: + time = 734066 + flags = 0 + data = length 621, hash CF741E7A + sample 25: + time = 800800 + flags = 0 + data = length 505, hash 1DB4894E + sample 26: + time = 967633 + flags = 0 + data = length 1268, hash C15348DC + sample 27: + time = 900900 + flags = 0 + data = length 880, hash C2DE85D0 + sample 28: + time = 867533 + flags = 0 + data = length 530, hash C98BC6A8 + sample 29: + time = 934266 + flags = 536870912 + data = length 568, hash 4FE5C8EA +track 1: + total output bytes = 9529 + sample count = 45 + format 0: + id = 2 + sampleMimeType = audio/mp4a-latm + codecs = mp4a.40.2 + maxInputSize = 294 + channelCount = 1 + sampleRate = 44100 + language = und + metadata = entries=[TSSE: description=null: value=Lavf56.1.0] + initializationData: + data = length 2, hash 5F7 + sample 0: + time = 44000 + flags = 1 + data = length 23, hash 47DE9131 + sample 1: + time = 67219 + flags = 1 + data = length 6, hash 31EC5206 + sample 2: + time = 90439 + flags = 1 + data = length 148, hash 894A176B + sample 3: + time = 113659 + flags = 1 + data = length 189, hash CEF235A1 + sample 4: + time = 136879 + flags = 1 + data = length 205, hash BBF5F7B0 + sample 5: + time = 160099 + flags = 1 + data = length 210, hash F278B193 + sample 6: + time = 183319 + flags = 1 + data = length 210, hash 82DA1589 + sample 7: + time = 206539 + flags = 1 + data = length 207, hash 5BE231DF + sample 8: + time = 229759 + flags = 1 + data = length 225, hash 18819EE1 + sample 9: + time = 252979 + flags = 1 + data = length 215, hash CA7FA67B + sample 10: + time = 276199 + flags = 1 + data = length 211, hash 581A1C18 + sample 11: + time = 299419 + flags = 1 + data = length 216, hash ADB88187 + sample 12: + time = 322639 + flags = 1 + data = length 229, hash 2E8BA4DC + sample 13: + time = 345859 + flags = 1 + data = length 232, hash 22F0C510 + sample 14: + time = 369079 + flags = 1 + data = length 235, hash 867AD0DC + sample 15: + time = 392299 + flags = 1 + data = length 231, hash 84E823A8 + sample 16: + time = 415519 + flags = 1 + data = length 226, hash 1BEF3A95 + sample 17: + time = 438739 + flags = 1 + data = length 216, hash EAA345AE + sample 18: + time = 461959 + flags = 1 + data = length 229, hash 6957411F + sample 19: + time = 485179 + flags = 1 + data = length 219, hash 41275022 + sample 20: + time = 508399 + flags = 1 + data = length 241, hash 6495DF96 + sample 21: + time = 531619 + flags = 1 + data = length 228, hash 63D95906 + sample 22: + time = 554839 + flags = 1 + data = length 238, hash 34F676F9 + sample 23: + time = 578058 + flags = 1 + data = length 234, hash E5CBC045 + sample 24: + time = 601278 + flags = 1 + data = length 231, hash 5FC43661 + sample 25: + time = 624498 + flags = 1 + data = length 217, hash 682708ED + sample 26: + time = 647718 + flags = 1 + data = length 239, hash D43780FC + sample 27: + time = 670938 + flags = 1 + data = length 243, hash C5E17980 + sample 28: + time = 694158 + flags = 1 + data = length 231, hash AC5837BA + sample 29: + time = 717378 + flags = 1 + data = length 230, hash 169EE895 + sample 30: + time = 740598 + flags = 1 + data = length 238, hash C48FF3F1 + sample 31: + time = 763818 + flags = 1 + data = length 225, hash 531E4599 + sample 32: + time = 787038 + flags = 1 + data = length 232, hash CB3C6B8D + sample 33: + time = 810258 + flags = 1 + data = length 243, hash F8C94C7 + sample 34: + time = 833478 + flags = 1 + data = length 232, hash A646A7D0 + sample 35: + time = 856698 + flags = 1 + data = length 237, hash E8B787A5 + sample 36: + time = 879918 + flags = 1 + data = length 228, hash 3FA7A29F + sample 37: + time = 903138 + flags = 1 + data = length 235, hash B9B33B0A + sample 38: + time = 926358 + flags = 1 + data = length 264, hash 71A4869E + sample 39: + time = 949578 + flags = 1 + data = length 257, hash D049B54C + sample 40: + time = 972798 + flags = 1 + data = length 227, hash 66757231 + sample 41: + time = 996018 + flags = 1 + data = length 227, hash BD374F1B + sample 42: + time = 1019238 + flags = 1 + data = length 235, hash 999477F6 + sample 43: + time = 1042458 + flags = 1 + data = length 229, hash FFF98DF0 + sample 44: + time = 1065678 + flags = 536870913 + data = length 6, hash 31B22286 +tracksEnded = true diff --git a/testdata/src/test/assets/mp4/sample_ac3.mp4 b/testdata/src/test/assets/mp4/sample_ac3.mp4 new file mode 100644 index 0000000000..2ec78814f2 Binary files /dev/null and b/testdata/src/test/assets/mp4/sample_ac3.mp4 differ diff --git a/testdata/src/test/assets/mp4/sample_ac3.mp4.0.dump b/testdata/src/test/assets/mp4/sample_ac3.mp4.0.dump new file mode 100644 index 0000000000..c2e51faaef --- /dev/null +++ b/testdata/src/test/assets/mp4/sample_ac3.mp4.0.dump @@ -0,0 +1,55 @@ +seekMap: + isSeekable = true + duration = 288333 + getPosition(0) = [[timeUs=0, position=609]] + getPosition(1) = [[timeUs=1, position=609]] + getPosition(144166) = [[timeUs=144166, position=6753]] + getPosition(288333) = [[timeUs=288333, position=12897]] +numberOfTracks = 1 +track 0: + total output bytes = 13824 + sample count = 9 + format 0: + id = 1 + sampleMimeType = audio/ac3 + maxInputSize = 1566 + channelCount = 6 + sampleRate = 48000 + language = und + sample 0: + time = 0 + flags = 1 + data = length 1536, hash 7108D5C2 + sample 1: + time = 32000 + flags = 1 + data = length 1536, hash 80BF3B34 + sample 2: + time = 64000 + flags = 1 + data = length 1536, hash 5D09685 + sample 3: + time = 96000 + flags = 1 + data = length 1536, hash A9A24E44 + sample 4: + time = 128000 + flags = 1 + data = length 1536, hash 6F856273 + sample 5: + time = 160000 + flags = 1 + data = length 1536, hash B1737D3C + sample 6: + time = 192000 + flags = 1 + data = length 1536, hash 98FDEB9D + sample 7: + time = 224000 + flags = 1 + data = length 1536, hash 99B9B943 + sample 8: + time = 256000 + flags = 536870913 + data = length 1536, hash AAD9FCD2 +tracksEnded = true diff --git a/testdata/src/test/assets/mp4/sample_ac3.mp4.1.dump b/testdata/src/test/assets/mp4/sample_ac3.mp4.1.dump new file mode 100644 index 0000000000..80f0790cd0 --- /dev/null +++ b/testdata/src/test/assets/mp4/sample_ac3.mp4.1.dump @@ -0,0 +1,43 @@ +seekMap: + isSeekable = true + duration = 288333 + getPosition(0) = [[timeUs=0, position=609]] + getPosition(1) = [[timeUs=1, position=609]] + getPosition(144166) = [[timeUs=144166, position=6753]] + getPosition(288333) = [[timeUs=288333, position=12897]] +numberOfTracks = 1 +track 0: + total output bytes = 9216 + sample count = 6 + format 0: + id = 1 + sampleMimeType = audio/ac3 + maxInputSize = 1566 + channelCount = 6 + sampleRate = 48000 + language = und + sample 0: + time = 96000 + flags = 1 + data = length 1536, hash A9A24E44 + sample 1: + time = 128000 + flags = 1 + data = length 1536, hash 6F856273 + sample 2: + time = 160000 + flags = 1 + data = length 1536, hash B1737D3C + sample 3: + time = 192000 + flags = 1 + data = length 1536, hash 98FDEB9D + sample 4: + time = 224000 + flags = 1 + data = length 1536, hash 99B9B943 + sample 5: + time = 256000 + flags = 536870913 + data = length 1536, hash AAD9FCD2 +tracksEnded = true diff --git a/testdata/src/test/assets/mp4/sample_ac3.mp4.2.dump b/testdata/src/test/assets/mp4/sample_ac3.mp4.2.dump new file mode 100644 index 0000000000..a8d1588940 --- /dev/null +++ b/testdata/src/test/assets/mp4/sample_ac3.mp4.2.dump @@ -0,0 +1,31 @@ +seekMap: + isSeekable = true + duration = 288333 + getPosition(0) = [[timeUs=0, position=609]] + getPosition(1) = [[timeUs=1, position=609]] + getPosition(144166) = [[timeUs=144166, position=6753]] + getPosition(288333) = [[timeUs=288333, position=12897]] +numberOfTracks = 1 +track 0: + total output bytes = 4608 + sample count = 3 + format 0: + id = 1 + sampleMimeType = audio/ac3 + maxInputSize = 1566 + channelCount = 6 + sampleRate = 48000 + language = und + sample 0: + time = 192000 + flags = 1 + data = length 1536, hash 98FDEB9D + sample 1: + time = 224000 + flags = 1 + data = length 1536, hash 99B9B943 + sample 2: + time = 256000 + flags = 536870913 + data = length 1536, hash AAD9FCD2 +tracksEnded = true diff --git a/testdata/src/test/assets/mp4/sample_ac3.mp4.3.dump b/testdata/src/test/assets/mp4/sample_ac3.mp4.3.dump new file mode 100644 index 0000000000..17bf79c850 --- /dev/null +++ b/testdata/src/test/assets/mp4/sample_ac3.mp4.3.dump @@ -0,0 +1,23 @@ +seekMap: + isSeekable = true + duration = 288333 + getPosition(0) = [[timeUs=0, position=609]] + getPosition(1) = [[timeUs=1, position=609]] + getPosition(144166) = [[timeUs=144166, position=6753]] + getPosition(288333) = [[timeUs=288333, position=12897]] +numberOfTracks = 1 +track 0: + total output bytes = 1536 + sample count = 1 + format 0: + id = 1 + sampleMimeType = audio/ac3 + maxInputSize = 1566 + channelCount = 6 + sampleRate = 48000 + language = und + sample 0: + time = 256000 + flags = 536870913 + data = length 1536, hash AAD9FCD2 +tracksEnded = true diff --git a/testdata/src/test/assets/mp4/sample_ac3.mp4.unknown_length.dump b/testdata/src/test/assets/mp4/sample_ac3.mp4.unknown_length.dump new file mode 100644 index 0000000000..c2e51faaef --- /dev/null +++ b/testdata/src/test/assets/mp4/sample_ac3.mp4.unknown_length.dump @@ -0,0 +1,55 @@ +seekMap: + isSeekable = true + duration = 288333 + getPosition(0) = [[timeUs=0, position=609]] + getPosition(1) = [[timeUs=1, position=609]] + getPosition(144166) = [[timeUs=144166, position=6753]] + getPosition(288333) = [[timeUs=288333, position=12897]] +numberOfTracks = 1 +track 0: + total output bytes = 13824 + sample count = 9 + format 0: + id = 1 + sampleMimeType = audio/ac3 + maxInputSize = 1566 + channelCount = 6 + sampleRate = 48000 + language = und + sample 0: + time = 0 + flags = 1 + data = length 1536, hash 7108D5C2 + sample 1: + time = 32000 + flags = 1 + data = length 1536, hash 80BF3B34 + sample 2: + time = 64000 + flags = 1 + data = length 1536, hash 5D09685 + sample 3: + time = 96000 + flags = 1 + data = length 1536, hash A9A24E44 + sample 4: + time = 128000 + flags = 1 + data = length 1536, hash 6F856273 + sample 5: + time = 160000 + flags = 1 + data = length 1536, hash B1737D3C + sample 6: + time = 192000 + flags = 1 + data = length 1536, hash 98FDEB9D + sample 7: + time = 224000 + flags = 1 + data = length 1536, hash 99B9B943 + sample 8: + time = 256000 + flags = 536870913 + data = length 1536, hash AAD9FCD2 +tracksEnded = true diff --git a/testdata/src/test/assets/mp4/sample_ac3_fragmented.mp4 b/testdata/src/test/assets/mp4/sample_ac3_fragmented.mp4 new file mode 100644 index 0000000000..d8b0a6ca52 Binary files /dev/null and b/testdata/src/test/assets/mp4/sample_ac3_fragmented.mp4 differ diff --git a/testdata/src/test/assets/mp4/sample_ac3_fragmented.mp4.0.dump b/testdata/src/test/assets/mp4/sample_ac3_fragmented.mp4.0.dump new file mode 100644 index 0000000000..3724592554 --- /dev/null +++ b/testdata/src/test/assets/mp4/sample_ac3_fragmented.mp4.0.dump @@ -0,0 +1,54 @@ +seekMap: + isSeekable = true + duration = 288000 + getPosition(0) = [[timeUs=0, position=636]] + getPosition(1) = [[timeUs=0, position=636]] + getPosition(144000) = [[timeUs=0, position=636]] + getPosition(288000) = [[timeUs=0, position=636]] +numberOfTracks = 1 +track 0: + total output bytes = 13824 + sample count = 9 + format 0: + id = 1 + sampleMimeType = audio/ac3 + channelCount = 6 + sampleRate = 48000 + language = und + sample 0: + time = 0 + flags = 1 + data = length 1536, hash 7108D5C2 + sample 1: + time = 32000 + flags = 1 + data = length 1536, hash 80BF3B34 + sample 2: + time = 64000 + flags = 1 + data = length 1536, hash 5D09685 + sample 3: + time = 96000 + flags = 1 + data = length 1536, hash A9A24E44 + sample 4: + time = 128000 + flags = 1 + data = length 1536, hash 6F856273 + sample 5: + time = 160000 + flags = 1 + data = length 1536, hash B1737D3C + sample 6: + time = 192000 + flags = 1 + data = length 1536, hash 98FDEB9D + sample 7: + time = 224000 + flags = 1 + data = length 1536, hash 99B9B943 + sample 8: + time = 256000 + flags = 1 + data = length 1536, hash AAD9FCD2 +tracksEnded = true diff --git a/testdata/src/test/assets/mp4/sample_ac3_fragmented.mp4.1.dump b/testdata/src/test/assets/mp4/sample_ac3_fragmented.mp4.1.dump new file mode 100644 index 0000000000..e9019d4ab1 --- /dev/null +++ b/testdata/src/test/assets/mp4/sample_ac3_fragmented.mp4.1.dump @@ -0,0 +1,46 @@ +seekMap: + isSeekable = true + duration = 288000 + getPosition(0) = [[timeUs=0, position=636]] + getPosition(1) = [[timeUs=0, position=636]] + getPosition(144000) = [[timeUs=0, position=636]] + getPosition(288000) = [[timeUs=0, position=636]] +numberOfTracks = 1 +track 0: + total output bytes = 10752 + sample count = 7 + format 0: + id = 1 + sampleMimeType = audio/ac3 + channelCount = 6 + sampleRate = 48000 + language = und + sample 0: + time = 64000 + flags = 1 + data = length 1536, hash 5D09685 + sample 1: + time = 96000 + flags = 1 + data = length 1536, hash A9A24E44 + sample 2: + time = 128000 + flags = 1 + data = length 1536, hash 6F856273 + sample 3: + time = 160000 + flags = 1 + data = length 1536, hash B1737D3C + sample 4: + time = 192000 + flags = 1 + data = length 1536, hash 98FDEB9D + sample 5: + time = 224000 + flags = 1 + data = length 1536, hash 99B9B943 + sample 6: + time = 256000 + flags = 1 + data = length 1536, hash AAD9FCD2 +tracksEnded = true diff --git a/testdata/src/test/assets/mp4/sample_ac3_fragmented.mp4.2.dump b/testdata/src/test/assets/mp4/sample_ac3_fragmented.mp4.2.dump new file mode 100644 index 0000000000..2b9cb1cd52 --- /dev/null +++ b/testdata/src/test/assets/mp4/sample_ac3_fragmented.mp4.2.dump @@ -0,0 +1,34 @@ +seekMap: + isSeekable = true + duration = 288000 + getPosition(0) = [[timeUs=0, position=636]] + getPosition(1) = [[timeUs=0, position=636]] + getPosition(144000) = [[timeUs=0, position=636]] + getPosition(288000) = [[timeUs=0, position=636]] +numberOfTracks = 1 +track 0: + total output bytes = 6144 + sample count = 4 + format 0: + id = 1 + sampleMimeType = audio/ac3 + channelCount = 6 + sampleRate = 48000 + language = und + sample 0: + time = 160000 + flags = 1 + data = length 1536, hash B1737D3C + sample 1: + time = 192000 + flags = 1 + data = length 1536, hash 98FDEB9D + sample 2: + time = 224000 + flags = 1 + data = length 1536, hash 99B9B943 + sample 3: + time = 256000 + flags = 1 + data = length 1536, hash AAD9FCD2 +tracksEnded = true diff --git a/testdata/src/test/assets/mp4/sample_ac3_fragmented.mp4.3.dump b/testdata/src/test/assets/mp4/sample_ac3_fragmented.mp4.3.dump new file mode 100644 index 0000000000..eb313f941d --- /dev/null +++ b/testdata/src/test/assets/mp4/sample_ac3_fragmented.mp4.3.dump @@ -0,0 +1,22 @@ +seekMap: + isSeekable = true + duration = 288000 + getPosition(0) = [[timeUs=0, position=636]] + getPosition(1) = [[timeUs=0, position=636]] + getPosition(144000) = [[timeUs=0, position=636]] + getPosition(288000) = [[timeUs=0, position=636]] +numberOfTracks = 1 +track 0: + total output bytes = 1536 + sample count = 1 + format 0: + id = 1 + sampleMimeType = audio/ac3 + channelCount = 6 + sampleRate = 48000 + language = und + sample 0: + time = 256000 + flags = 1 + data = length 1536, hash AAD9FCD2 +tracksEnded = true diff --git a/testdata/src/test/assets/mp4/sample_ac3_fragmented.mp4.unknown_length.dump b/testdata/src/test/assets/mp4/sample_ac3_fragmented.mp4.unknown_length.dump new file mode 100644 index 0000000000..3724592554 --- /dev/null +++ b/testdata/src/test/assets/mp4/sample_ac3_fragmented.mp4.unknown_length.dump @@ -0,0 +1,54 @@ +seekMap: + isSeekable = true + duration = 288000 + getPosition(0) = [[timeUs=0, position=636]] + getPosition(1) = [[timeUs=0, position=636]] + getPosition(144000) = [[timeUs=0, position=636]] + getPosition(288000) = [[timeUs=0, position=636]] +numberOfTracks = 1 +track 0: + total output bytes = 13824 + sample count = 9 + format 0: + id = 1 + sampleMimeType = audio/ac3 + channelCount = 6 + sampleRate = 48000 + language = und + sample 0: + time = 0 + flags = 1 + data = length 1536, hash 7108D5C2 + sample 1: + time = 32000 + flags = 1 + data = length 1536, hash 80BF3B34 + sample 2: + time = 64000 + flags = 1 + data = length 1536, hash 5D09685 + sample 3: + time = 96000 + flags = 1 + data = length 1536, hash A9A24E44 + sample 4: + time = 128000 + flags = 1 + data = length 1536, hash 6F856273 + sample 5: + time = 160000 + flags = 1 + data = length 1536, hash B1737D3C + sample 6: + time = 192000 + flags = 1 + data = length 1536, hash 98FDEB9D + sample 7: + time = 224000 + flags = 1 + data = length 1536, hash 99B9B943 + sample 8: + time = 256000 + flags = 1 + data = length 1536, hash AAD9FCD2 +tracksEnded = true diff --git a/testdata/src/test/assets/mp4/sample_ac4.mp4 b/testdata/src/test/assets/mp4/sample_ac4.mp4 new file mode 100644 index 0000000000..d649632c74 Binary files /dev/null and b/testdata/src/test/assets/mp4/sample_ac4.mp4 differ diff --git a/testdata/src/test/assets/mp4/sample_ac4.mp4.0.dump b/testdata/src/test/assets/mp4/sample_ac4.mp4.0.dump new file mode 100644 index 0000000000..8a8abf17a2 --- /dev/null +++ b/testdata/src/test/assets/mp4/sample_ac4.mp4.0.dump @@ -0,0 +1,95 @@ +seekMap: + isSeekable = true + duration = 760000 + getPosition(0) = [[timeUs=0, position=758]] + getPosition(1) = [[timeUs=1, position=758]] + getPosition(380000) = [[timeUs=380000, position=758]] + getPosition(760000) = [[timeUs=760000, position=758]] +numberOfTracks = 1 +track 0: + total output bytes = 7613 + sample count = 19 + format 0: + id = 1 + sampleMimeType = audio/ac4 + maxInputSize = 622 + channelCount = 2 + sampleRate = 48000 + language = und + sample 0: + time = 0 + flags = 1 + data = length 367, hash D2762FA + sample 1: + time = 40000 + flags = 0 + data = length 367, hash BDD3224A + sample 2: + time = 80000 + flags = 0 + data = length 367, hash 9302227B + sample 3: + time = 120000 + flags = 0 + data = length 367, hash 72996003 + sample 4: + time = 160000 + flags = 0 + data = length 367, hash 88AE5A1B + sample 5: + time = 200000 + flags = 0 + data = length 367, hash E5346FE3 + sample 6: + time = 240000 + flags = 0 + data = length 367, hash CE558362 + sample 7: + time = 280000 + flags = 0 + data = length 367, hash 51AD3043 + sample 8: + time = 320000 + flags = 0 + data = length 367, hash EB72E95B + sample 9: + time = 360000 + flags = 0 + data = length 367, hash 47F8FF23 + sample 10: + time = 400000 + flags = 0 + data = length 367, hash 8133883D + sample 11: + time = 440000 + flags = 0 + data = length 495, hash E14BDFEE + sample 12: + time = 480000 + flags = 0 + data = length 520, hash FEE56928 + sample 13: + time = 519999 + flags = 0 + data = length 599, hash 41F496C5 + sample 14: + time = 560000 + flags = 0 + data = length 436, hash 76D6404 + sample 15: + time = 600000 + flags = 0 + data = length 366, hash 56D49D4D + sample 16: + time = 640000 + flags = 0 + data = length 393, hash 822FC8 + sample 17: + time = 680000 + flags = 0 + data = length 374, hash FA8AE217 + sample 18: + time = 720000 + flags = 536870912 + data = length 393, hash 8506A1B +tracksEnded = true diff --git a/testdata/src/test/assets/mp4/sample_ac4.mp4.1.dump b/testdata/src/test/assets/mp4/sample_ac4.mp4.1.dump new file mode 100644 index 0000000000..8a8abf17a2 --- /dev/null +++ b/testdata/src/test/assets/mp4/sample_ac4.mp4.1.dump @@ -0,0 +1,95 @@ +seekMap: + isSeekable = true + duration = 760000 + getPosition(0) = [[timeUs=0, position=758]] + getPosition(1) = [[timeUs=1, position=758]] + getPosition(380000) = [[timeUs=380000, position=758]] + getPosition(760000) = [[timeUs=760000, position=758]] +numberOfTracks = 1 +track 0: + total output bytes = 7613 + sample count = 19 + format 0: + id = 1 + sampleMimeType = audio/ac4 + maxInputSize = 622 + channelCount = 2 + sampleRate = 48000 + language = und + sample 0: + time = 0 + flags = 1 + data = length 367, hash D2762FA + sample 1: + time = 40000 + flags = 0 + data = length 367, hash BDD3224A + sample 2: + time = 80000 + flags = 0 + data = length 367, hash 9302227B + sample 3: + time = 120000 + flags = 0 + data = length 367, hash 72996003 + sample 4: + time = 160000 + flags = 0 + data = length 367, hash 88AE5A1B + sample 5: + time = 200000 + flags = 0 + data = length 367, hash E5346FE3 + sample 6: + time = 240000 + flags = 0 + data = length 367, hash CE558362 + sample 7: + time = 280000 + flags = 0 + data = length 367, hash 51AD3043 + sample 8: + time = 320000 + flags = 0 + data = length 367, hash EB72E95B + sample 9: + time = 360000 + flags = 0 + data = length 367, hash 47F8FF23 + sample 10: + time = 400000 + flags = 0 + data = length 367, hash 8133883D + sample 11: + time = 440000 + flags = 0 + data = length 495, hash E14BDFEE + sample 12: + time = 480000 + flags = 0 + data = length 520, hash FEE56928 + sample 13: + time = 519999 + flags = 0 + data = length 599, hash 41F496C5 + sample 14: + time = 560000 + flags = 0 + data = length 436, hash 76D6404 + sample 15: + time = 600000 + flags = 0 + data = length 366, hash 56D49D4D + sample 16: + time = 640000 + flags = 0 + data = length 393, hash 822FC8 + sample 17: + time = 680000 + flags = 0 + data = length 374, hash FA8AE217 + sample 18: + time = 720000 + flags = 536870912 + data = length 393, hash 8506A1B +tracksEnded = true diff --git a/testdata/src/test/assets/mp4/sample_ac4.mp4.2.dump b/testdata/src/test/assets/mp4/sample_ac4.mp4.2.dump new file mode 100644 index 0000000000..8a8abf17a2 --- /dev/null +++ b/testdata/src/test/assets/mp4/sample_ac4.mp4.2.dump @@ -0,0 +1,95 @@ +seekMap: + isSeekable = true + duration = 760000 + getPosition(0) = [[timeUs=0, position=758]] + getPosition(1) = [[timeUs=1, position=758]] + getPosition(380000) = [[timeUs=380000, position=758]] + getPosition(760000) = [[timeUs=760000, position=758]] +numberOfTracks = 1 +track 0: + total output bytes = 7613 + sample count = 19 + format 0: + id = 1 + sampleMimeType = audio/ac4 + maxInputSize = 622 + channelCount = 2 + sampleRate = 48000 + language = und + sample 0: + time = 0 + flags = 1 + data = length 367, hash D2762FA + sample 1: + time = 40000 + flags = 0 + data = length 367, hash BDD3224A + sample 2: + time = 80000 + flags = 0 + data = length 367, hash 9302227B + sample 3: + time = 120000 + flags = 0 + data = length 367, hash 72996003 + sample 4: + time = 160000 + flags = 0 + data = length 367, hash 88AE5A1B + sample 5: + time = 200000 + flags = 0 + data = length 367, hash E5346FE3 + sample 6: + time = 240000 + flags = 0 + data = length 367, hash CE558362 + sample 7: + time = 280000 + flags = 0 + data = length 367, hash 51AD3043 + sample 8: + time = 320000 + flags = 0 + data = length 367, hash EB72E95B + sample 9: + time = 360000 + flags = 0 + data = length 367, hash 47F8FF23 + sample 10: + time = 400000 + flags = 0 + data = length 367, hash 8133883D + sample 11: + time = 440000 + flags = 0 + data = length 495, hash E14BDFEE + sample 12: + time = 480000 + flags = 0 + data = length 520, hash FEE56928 + sample 13: + time = 519999 + flags = 0 + data = length 599, hash 41F496C5 + sample 14: + time = 560000 + flags = 0 + data = length 436, hash 76D6404 + sample 15: + time = 600000 + flags = 0 + data = length 366, hash 56D49D4D + sample 16: + time = 640000 + flags = 0 + data = length 393, hash 822FC8 + sample 17: + time = 680000 + flags = 0 + data = length 374, hash FA8AE217 + sample 18: + time = 720000 + flags = 536870912 + data = length 393, hash 8506A1B +tracksEnded = true diff --git a/testdata/src/test/assets/mp4/sample_ac4.mp4.3.dump b/testdata/src/test/assets/mp4/sample_ac4.mp4.3.dump new file mode 100644 index 0000000000..8a8abf17a2 --- /dev/null +++ b/testdata/src/test/assets/mp4/sample_ac4.mp4.3.dump @@ -0,0 +1,95 @@ +seekMap: + isSeekable = true + duration = 760000 + getPosition(0) = [[timeUs=0, position=758]] + getPosition(1) = [[timeUs=1, position=758]] + getPosition(380000) = [[timeUs=380000, position=758]] + getPosition(760000) = [[timeUs=760000, position=758]] +numberOfTracks = 1 +track 0: + total output bytes = 7613 + sample count = 19 + format 0: + id = 1 + sampleMimeType = audio/ac4 + maxInputSize = 622 + channelCount = 2 + sampleRate = 48000 + language = und + sample 0: + time = 0 + flags = 1 + data = length 367, hash D2762FA + sample 1: + time = 40000 + flags = 0 + data = length 367, hash BDD3224A + sample 2: + time = 80000 + flags = 0 + data = length 367, hash 9302227B + sample 3: + time = 120000 + flags = 0 + data = length 367, hash 72996003 + sample 4: + time = 160000 + flags = 0 + data = length 367, hash 88AE5A1B + sample 5: + time = 200000 + flags = 0 + data = length 367, hash E5346FE3 + sample 6: + time = 240000 + flags = 0 + data = length 367, hash CE558362 + sample 7: + time = 280000 + flags = 0 + data = length 367, hash 51AD3043 + sample 8: + time = 320000 + flags = 0 + data = length 367, hash EB72E95B + sample 9: + time = 360000 + flags = 0 + data = length 367, hash 47F8FF23 + sample 10: + time = 400000 + flags = 0 + data = length 367, hash 8133883D + sample 11: + time = 440000 + flags = 0 + data = length 495, hash E14BDFEE + sample 12: + time = 480000 + flags = 0 + data = length 520, hash FEE56928 + sample 13: + time = 519999 + flags = 0 + data = length 599, hash 41F496C5 + sample 14: + time = 560000 + flags = 0 + data = length 436, hash 76D6404 + sample 15: + time = 600000 + flags = 0 + data = length 366, hash 56D49D4D + sample 16: + time = 640000 + flags = 0 + data = length 393, hash 822FC8 + sample 17: + time = 680000 + flags = 0 + data = length 374, hash FA8AE217 + sample 18: + time = 720000 + flags = 536870912 + data = length 393, hash 8506A1B +tracksEnded = true diff --git a/testdata/src/test/assets/mp4/sample_ac4.mp4.unknown_length.dump b/testdata/src/test/assets/mp4/sample_ac4.mp4.unknown_length.dump new file mode 100644 index 0000000000..8a8abf17a2 --- /dev/null +++ b/testdata/src/test/assets/mp4/sample_ac4.mp4.unknown_length.dump @@ -0,0 +1,95 @@ +seekMap: + isSeekable = true + duration = 760000 + getPosition(0) = [[timeUs=0, position=758]] + getPosition(1) = [[timeUs=1, position=758]] + getPosition(380000) = [[timeUs=380000, position=758]] + getPosition(760000) = [[timeUs=760000, position=758]] +numberOfTracks = 1 +track 0: + total output bytes = 7613 + sample count = 19 + format 0: + id = 1 + sampleMimeType = audio/ac4 + maxInputSize = 622 + channelCount = 2 + sampleRate = 48000 + language = und + sample 0: + time = 0 + flags = 1 + data = length 367, hash D2762FA + sample 1: + time = 40000 + flags = 0 + data = length 367, hash BDD3224A + sample 2: + time = 80000 + flags = 0 + data = length 367, hash 9302227B + sample 3: + time = 120000 + flags = 0 + data = length 367, hash 72996003 + sample 4: + time = 160000 + flags = 0 + data = length 367, hash 88AE5A1B + sample 5: + time = 200000 + flags = 0 + data = length 367, hash E5346FE3 + sample 6: + time = 240000 + flags = 0 + data = length 367, hash CE558362 + sample 7: + time = 280000 + flags = 0 + data = length 367, hash 51AD3043 + sample 8: + time = 320000 + flags = 0 + data = length 367, hash EB72E95B + sample 9: + time = 360000 + flags = 0 + data = length 367, hash 47F8FF23 + sample 10: + time = 400000 + flags = 0 + data = length 367, hash 8133883D + sample 11: + time = 440000 + flags = 0 + data = length 495, hash E14BDFEE + sample 12: + time = 480000 + flags = 0 + data = length 520, hash FEE56928 + sample 13: + time = 519999 + flags = 0 + data = length 599, hash 41F496C5 + sample 14: + time = 560000 + flags = 0 + data = length 436, hash 76D6404 + sample 15: + time = 600000 + flags = 0 + data = length 366, hash 56D49D4D + sample 16: + time = 640000 + flags = 0 + data = length 393, hash 822FC8 + sample 17: + time = 680000 + flags = 0 + data = length 374, hash FA8AE217 + sample 18: + time = 720000 + flags = 536870912 + data = length 393, hash 8506A1B +tracksEnded = true diff --git a/testdata/src/test/assets/mp4/sample_ac4_fragmented.mp4 b/testdata/src/test/assets/mp4/sample_ac4_fragmented.mp4 new file mode 100644 index 0000000000..2056348768 Binary files /dev/null and b/testdata/src/test/assets/mp4/sample_ac4_fragmented.mp4 differ diff --git a/testdata/src/test/assets/mp4/sample_ac4_fragmented.mp4.0.dump b/testdata/src/test/assets/mp4/sample_ac4_fragmented.mp4.0.dump new file mode 100644 index 0000000000..8868166056 --- /dev/null +++ b/testdata/src/test/assets/mp4/sample_ac4_fragmented.mp4.0.dump @@ -0,0 +1,94 @@ +seekMap: + isSeekable = true + duration = 760000 + getPosition(0) = [[timeUs=0, position=685]] + getPosition(1) = [[timeUs=0, position=685]] + getPosition(380000) = [[timeUs=0, position=685]] + getPosition(760000) = [[timeUs=0, position=685]] +numberOfTracks = 1 +track 0: + total output bytes = 7613 + sample count = 19 + format 0: + id = 1 + sampleMimeType = audio/ac4 + channelCount = 2 + sampleRate = 48000 + language = und + sample 0: + time = 0 + flags = 1 + data = length 367, hash D2762FA + sample 1: + time = 40000 + flags = 1 + data = length 367, hash BDD3224A + sample 2: + time = 80000 + flags = 1 + data = length 367, hash 9302227B + sample 3: + time = 120000 + flags = 1 + data = length 367, hash 72996003 + sample 4: + time = 160000 + flags = 1 + data = length 367, hash 88AE5A1B + sample 5: + time = 200000 + flags = 1 + data = length 367, hash E5346FE3 + sample 6: + time = 240000 + flags = 1 + data = length 367, hash CE558362 + sample 7: + time = 280000 + flags = 1 + data = length 367, hash 51AD3043 + sample 8: + time = 320000 + flags = 1 + data = length 367, hash EB72E95B + sample 9: + time = 360000 + flags = 1 + data = length 367, hash 47F8FF23 + sample 10: + time = 400000 + flags = 1 + data = length 367, hash 8133883D + sample 11: + time = 440000 + flags = 1 + data = length 495, hash E14BDFEE + sample 12: + time = 480000 + flags = 1 + data = length 520, hash FEE56928 + sample 13: + time = 519999 + flags = 1 + data = length 599, hash 41F496C5 + sample 14: + time = 560000 + flags = 1 + data = length 436, hash 76D6404 + sample 15: + time = 600000 + flags = 1 + data = length 366, hash 56D49D4D + sample 16: + time = 640000 + flags = 1 + data = length 393, hash 822FC8 + sample 17: + time = 680000 + flags = 1 + data = length 374, hash FA8AE217 + sample 18: + time = 720000 + flags = 1 + data = length 393, hash 8506A1B +tracksEnded = true diff --git a/testdata/src/test/assets/mp4/sample_ac4_fragmented.mp4.1.dump b/testdata/src/test/assets/mp4/sample_ac4_fragmented.mp4.1.dump new file mode 100644 index 0000000000..e23156e0fb --- /dev/null +++ b/testdata/src/test/assets/mp4/sample_ac4_fragmented.mp4.1.dump @@ -0,0 +1,70 @@ +seekMap: + isSeekable = true + duration = 760000 + getPosition(0) = [[timeUs=0, position=685]] + getPosition(1) = [[timeUs=0, position=685]] + getPosition(380000) = [[timeUs=0, position=685]] + getPosition(760000) = [[timeUs=0, position=685]] +numberOfTracks = 1 +track 0: + total output bytes = 5411 + sample count = 13 + format 0: + id = 1 + sampleMimeType = audio/ac4 + channelCount = 2 + sampleRate = 48000 + language = und + sample 0: + time = 240000 + flags = 1 + data = length 367, hash CE558362 + sample 1: + time = 280000 + flags = 1 + data = length 367, hash 51AD3043 + sample 2: + time = 320000 + flags = 1 + data = length 367, hash EB72E95B + sample 3: + time = 360000 + flags = 1 + data = length 367, hash 47F8FF23 + sample 4: + time = 400000 + flags = 1 + data = length 367, hash 8133883D + sample 5: + time = 440000 + flags = 1 + data = length 495, hash E14BDFEE + sample 6: + time = 480000 + flags = 1 + data = length 520, hash FEE56928 + sample 7: + time = 519999 + flags = 1 + data = length 599, hash 41F496C5 + sample 8: + time = 560000 + flags = 1 + data = length 436, hash 76D6404 + sample 9: + time = 600000 + flags = 1 + data = length 366, hash 56D49D4D + sample 10: + time = 640000 + flags = 1 + data = length 393, hash 822FC8 + sample 11: + time = 680000 + flags = 1 + data = length 374, hash FA8AE217 + sample 12: + time = 720000 + flags = 1 + data = length 393, hash 8506A1B +tracksEnded = true diff --git a/testdata/src/test/assets/mp4/sample_ac4_fragmented.mp4.2.dump b/testdata/src/test/assets/mp4/sample_ac4_fragmented.mp4.2.dump new file mode 100644 index 0000000000..82a11a4800 --- /dev/null +++ b/testdata/src/test/assets/mp4/sample_ac4_fragmented.mp4.2.dump @@ -0,0 +1,46 @@ +seekMap: + isSeekable = true + duration = 760000 + getPosition(0) = [[timeUs=0, position=685]] + getPosition(1) = [[timeUs=0, position=685]] + getPosition(380000) = [[timeUs=0, position=685]] + getPosition(760000) = [[timeUs=0, position=685]] +numberOfTracks = 1 +track 0: + total output bytes = 3081 + sample count = 7 + format 0: + id = 1 + sampleMimeType = audio/ac4 + channelCount = 2 + sampleRate = 48000 + language = und + sample 0: + time = 480000 + flags = 1 + data = length 520, hash FEE56928 + sample 1: + time = 519999 + flags = 1 + data = length 599, hash 41F496C5 + sample 2: + time = 560000 + flags = 1 + data = length 436, hash 76D6404 + sample 3: + time = 600000 + flags = 1 + data = length 366, hash 56D49D4D + sample 4: + time = 640000 + flags = 1 + data = length 393, hash 822FC8 + sample 5: + time = 680000 + flags = 1 + data = length 374, hash FA8AE217 + sample 6: + time = 720000 + flags = 1 + data = length 393, hash 8506A1B +tracksEnded = true diff --git a/testdata/src/test/assets/mp4/sample_ac4_fragmented.mp4.3.dump b/testdata/src/test/assets/mp4/sample_ac4_fragmented.mp4.3.dump new file mode 100644 index 0000000000..120dd66851 --- /dev/null +++ b/testdata/src/test/assets/mp4/sample_ac4_fragmented.mp4.3.dump @@ -0,0 +1,22 @@ +seekMap: + isSeekable = true + duration = 760000 + getPosition(0) = [[timeUs=0, position=685]] + getPosition(1) = [[timeUs=0, position=685]] + getPosition(380000) = [[timeUs=0, position=685]] + getPosition(760000) = [[timeUs=0, position=685]] +numberOfTracks = 1 +track 0: + total output bytes = 393 + sample count = 1 + format 0: + id = 1 + sampleMimeType = audio/ac4 + channelCount = 2 + sampleRate = 48000 + language = und + sample 0: + time = 720000 + flags = 1 + data = length 393, hash 8506A1B +tracksEnded = true diff --git a/testdata/src/test/assets/mp4/sample_ac4_fragmented.mp4.unknown_length.dump b/testdata/src/test/assets/mp4/sample_ac4_fragmented.mp4.unknown_length.dump new file mode 100644 index 0000000000..8868166056 --- /dev/null +++ b/testdata/src/test/assets/mp4/sample_ac4_fragmented.mp4.unknown_length.dump @@ -0,0 +1,94 @@ +seekMap: + isSeekable = true + duration = 760000 + getPosition(0) = [[timeUs=0, position=685]] + getPosition(1) = [[timeUs=0, position=685]] + getPosition(380000) = [[timeUs=0, position=685]] + getPosition(760000) = [[timeUs=0, position=685]] +numberOfTracks = 1 +track 0: + total output bytes = 7613 + sample count = 19 + format 0: + id = 1 + sampleMimeType = audio/ac4 + channelCount = 2 + sampleRate = 48000 + language = und + sample 0: + time = 0 + flags = 1 + data = length 367, hash D2762FA + sample 1: + time = 40000 + flags = 1 + data = length 367, hash BDD3224A + sample 2: + time = 80000 + flags = 1 + data = length 367, hash 9302227B + sample 3: + time = 120000 + flags = 1 + data = length 367, hash 72996003 + sample 4: + time = 160000 + flags = 1 + data = length 367, hash 88AE5A1B + sample 5: + time = 200000 + flags = 1 + data = length 367, hash E5346FE3 + sample 6: + time = 240000 + flags = 1 + data = length 367, hash CE558362 + sample 7: + time = 280000 + flags = 1 + data = length 367, hash 51AD3043 + sample 8: + time = 320000 + flags = 1 + data = length 367, hash EB72E95B + sample 9: + time = 360000 + flags = 1 + data = length 367, hash 47F8FF23 + sample 10: + time = 400000 + flags = 1 + data = length 367, hash 8133883D + sample 11: + time = 440000 + flags = 1 + data = length 495, hash E14BDFEE + sample 12: + time = 480000 + flags = 1 + data = length 520, hash FEE56928 + sample 13: + time = 519999 + flags = 1 + data = length 599, hash 41F496C5 + sample 14: + time = 560000 + flags = 1 + data = length 436, hash 76D6404 + sample 15: + time = 600000 + flags = 1 + data = length 366, hash 56D49D4D + sample 16: + time = 640000 + flags = 1 + data = length 393, hash 822FC8 + sample 17: + time = 680000 + flags = 1 + data = length 374, hash FA8AE217 + sample 18: + time = 720000 + flags = 1 + data = length 393, hash 8506A1B +tracksEnded = true diff --git a/testdata/src/test/assets/mp4/sample_ac4_protected.mp4 b/testdata/src/test/assets/mp4/sample_ac4_protected.mp4 new file mode 100644 index 0000000000..e3a4f6d6c6 Binary files /dev/null and b/testdata/src/test/assets/mp4/sample_ac4_protected.mp4 differ diff --git a/testdata/src/test/assets/mp4/sample_ac4_protected.mp4.0.dump b/testdata/src/test/assets/mp4/sample_ac4_protected.mp4.0.dump new file mode 100644 index 0000000000..0d0b8317e0 --- /dev/null +++ b/testdata/src/test/assets/mp4/sample_ac4_protected.mp4.0.dump @@ -0,0 +1,133 @@ +seekMap: + isSeekable = true + duration = 760000 + getPosition(0) = [[timeUs=0, position=950]] + getPosition(1) = [[timeUs=0, position=950]] + getPosition(380000) = [[timeUs=0, position=950]] + getPosition(760000) = [[timeUs=0, position=950]] +numberOfTracks = 1 +track 0: + total output bytes = 7936 + sample count = 19 + format 0: + id = 1 + sampleMimeType = audio/ac4 + channelCount = 2 + sampleRate = 48000 + language = und + drmInitData = -1683793742 + sample 0: + time = 0 + flags = 1073741825 + data = length 384, hash 96EFFFF3 + crypto mode = 1 + encryption key = length 16, hash 9FDDEA52 + sample 1: + time = 40000 + flags = 1073741825 + data = length 384, hash 899279C6 + crypto mode = 1 + encryption key = length 16, hash 9FDDEA52 + sample 2: + time = 80000 + flags = 1073741825 + data = length 384, hash 9EA9F45 + crypto mode = 1 + encryption key = length 16, hash 9FDDEA52 + sample 3: + time = 120000 + flags = 1073741825 + data = length 384, hash 82D362A9 + crypto mode = 1 + encryption key = length 16, hash 9FDDEA52 + sample 4: + time = 160000 + flags = 1073741825 + data = length 384, hash B8705CFB + crypto mode = 1 + encryption key = length 16, hash 9FDDEA52 + sample 5: + time = 200000 + flags = 1073741825 + data = length 384, hash 58B5628E + crypto mode = 1 + encryption key = length 16, hash 9FDDEA52 + sample 6: + time = 240000 + flags = 1073741825 + data = length 384, hash 87F3C13B + crypto mode = 1 + encryption key = length 16, hash 9FDDEA52 + sample 7: + time = 280000 + flags = 1073741825 + data = length 384, hash 54333DC5 + crypto mode = 1 + encryption key = length 16, hash 9FDDEA52 + sample 8: + time = 320000 + flags = 1073741825 + data = length 384, hash 1C49C4B3 + crypto mode = 1 + encryption key = length 16, hash 9FDDEA52 + sample 9: + time = 360000 + flags = 1073741825 + data = length 384, hash 5FDC324F + crypto mode = 1 + encryption key = length 16, hash 9FDDEA52 + sample 10: + time = 400000 + flags = 1073741825 + data = length 384, hash B2A7F444 + crypto mode = 1 + encryption key = length 16, hash 9FDDEA52 + sample 11: + time = 440000 + flags = 1073741825 + data = length 512, hash 5FD06C1E + crypto mode = 1 + encryption key = length 16, hash 9FDDEA52 + sample 12: + time = 480000 + flags = 1073741825 + data = length 537, hash 7ABBDCB + crypto mode = 1 + encryption key = length 16, hash 9FDDEA52 + sample 13: + time = 519999 + flags = 1073741825 + data = length 616, hash 3F657E23 + crypto mode = 1 + encryption key = length 16, hash 9FDDEA52 + sample 14: + time = 560000 + flags = 1073741825 + data = length 453, hash 8FCF0529 + crypto mode = 1 + encryption key = length 16, hash 9FDDEA52 + sample 15: + time = 600000 + flags = 1073741825 + data = length 383, hash 7F8C9E19 + crypto mode = 1 + encryption key = length 16, hash 9FDDEA52 + sample 16: + time = 640000 + flags = 1073741825 + data = length 410, hash 3727858D + crypto mode = 1 + encryption key = length 16, hash 9FDDEA52 + sample 17: + time = 680000 + flags = 1073741825 + data = length 391, hash E2931212 + crypto mode = 1 + encryption key = length 16, hash 9FDDEA52 + sample 18: + time = 720000 + flags = 1073741825 + data = length 410, hash 63017D46 + crypto mode = 1 + encryption key = length 16, hash 9FDDEA52 +tracksEnded = true diff --git a/testdata/src/test/assets/mp4/sample_ac4_protected.mp4.1.dump b/testdata/src/test/assets/mp4/sample_ac4_protected.mp4.1.dump new file mode 100644 index 0000000000..aeffcabdbd --- /dev/null +++ b/testdata/src/test/assets/mp4/sample_ac4_protected.mp4.1.dump @@ -0,0 +1,97 @@ +seekMap: + isSeekable = true + duration = 760000 + getPosition(0) = [[timeUs=0, position=950]] + getPosition(1) = [[timeUs=0, position=950]] + getPosition(380000) = [[timeUs=0, position=950]] + getPosition(760000) = [[timeUs=0, position=950]] +numberOfTracks = 1 +track 0: + total output bytes = 5632 + sample count = 13 + format 0: + id = 1 + sampleMimeType = audio/ac4 + channelCount = 2 + sampleRate = 48000 + language = und + drmInitData = -1683793742 + sample 0: + time = 240000 + flags = 1073741825 + data = length 384, hash 87F3C13B + crypto mode = 1 + encryption key = length 16, hash 9FDDEA52 + sample 1: + time = 280000 + flags = 1073741825 + data = length 384, hash 54333DC5 + crypto mode = 1 + encryption key = length 16, hash 9FDDEA52 + sample 2: + time = 320000 + flags = 1073741825 + data = length 384, hash 1C49C4B3 + crypto mode = 1 + encryption key = length 16, hash 9FDDEA52 + sample 3: + time = 360000 + flags = 1073741825 + data = length 384, hash 5FDC324F + crypto mode = 1 + encryption key = length 16, hash 9FDDEA52 + sample 4: + time = 400000 + flags = 1073741825 + data = length 384, hash B2A7F444 + crypto mode = 1 + encryption key = length 16, hash 9FDDEA52 + sample 5: + time = 440000 + flags = 1073741825 + data = length 512, hash 5FD06C1E + crypto mode = 1 + encryption key = length 16, hash 9FDDEA52 + sample 6: + time = 480000 + flags = 1073741825 + data = length 537, hash 7ABBDCB + crypto mode = 1 + encryption key = length 16, hash 9FDDEA52 + sample 7: + time = 519999 + flags = 1073741825 + data = length 616, hash 3F657E23 + crypto mode = 1 + encryption key = length 16, hash 9FDDEA52 + sample 8: + time = 560000 + flags = 1073741825 + data = length 453, hash 8FCF0529 + crypto mode = 1 + encryption key = length 16, hash 9FDDEA52 + sample 9: + time = 600000 + flags = 1073741825 + data = length 383, hash 7F8C9E19 + crypto mode = 1 + encryption key = length 16, hash 9FDDEA52 + sample 10: + time = 640000 + flags = 1073741825 + data = length 410, hash 3727858D + crypto mode = 1 + encryption key = length 16, hash 9FDDEA52 + sample 11: + time = 680000 + flags = 1073741825 + data = length 391, hash E2931212 + crypto mode = 1 + encryption key = length 16, hash 9FDDEA52 + sample 12: + time = 720000 + flags = 1073741825 + data = length 410, hash 63017D46 + crypto mode = 1 + encryption key = length 16, hash 9FDDEA52 +tracksEnded = true diff --git a/testdata/src/test/assets/mp4/sample_ac4_protected.mp4.2.dump b/testdata/src/test/assets/mp4/sample_ac4_protected.mp4.2.dump new file mode 100644 index 0000000000..ce0badc5c9 --- /dev/null +++ b/testdata/src/test/assets/mp4/sample_ac4_protected.mp4.2.dump @@ -0,0 +1,61 @@ +seekMap: + isSeekable = true + duration = 760000 + getPosition(0) = [[timeUs=0, position=950]] + getPosition(1) = [[timeUs=0, position=950]] + getPosition(380000) = [[timeUs=0, position=950]] + getPosition(760000) = [[timeUs=0, position=950]] +numberOfTracks = 1 +track 0: + total output bytes = 3200 + sample count = 7 + format 0: + id = 1 + sampleMimeType = audio/ac4 + channelCount = 2 + sampleRate = 48000 + language = und + drmInitData = -1683793742 + sample 0: + time = 480000 + flags = 1073741825 + data = length 537, hash 7ABBDCB + crypto mode = 1 + encryption key = length 16, hash 9FDDEA52 + sample 1: + time = 519999 + flags = 1073741825 + data = length 616, hash 3F657E23 + crypto mode = 1 + encryption key = length 16, hash 9FDDEA52 + sample 2: + time = 560000 + flags = 1073741825 + data = length 453, hash 8FCF0529 + crypto mode = 1 + encryption key = length 16, hash 9FDDEA52 + sample 3: + time = 600000 + flags = 1073741825 + data = length 383, hash 7F8C9E19 + crypto mode = 1 + encryption key = length 16, hash 9FDDEA52 + sample 4: + time = 640000 + flags = 1073741825 + data = length 410, hash 3727858D + crypto mode = 1 + encryption key = length 16, hash 9FDDEA52 + sample 5: + time = 680000 + flags = 1073741825 + data = length 391, hash E2931212 + crypto mode = 1 + encryption key = length 16, hash 9FDDEA52 + sample 6: + time = 720000 + flags = 1073741825 + data = length 410, hash 63017D46 + crypto mode = 1 + encryption key = length 16, hash 9FDDEA52 +tracksEnded = true diff --git a/testdata/src/test/assets/mp4/sample_ac4_protected.mp4.3.dump b/testdata/src/test/assets/mp4/sample_ac4_protected.mp4.3.dump new file mode 100644 index 0000000000..3a857ff758 --- /dev/null +++ b/testdata/src/test/assets/mp4/sample_ac4_protected.mp4.3.dump @@ -0,0 +1,25 @@ +seekMap: + isSeekable = true + duration = 760000 + getPosition(0) = [[timeUs=0, position=950]] + getPosition(1) = [[timeUs=0, position=950]] + getPosition(380000) = [[timeUs=0, position=950]] + getPosition(760000) = [[timeUs=0, position=950]] +numberOfTracks = 1 +track 0: + total output bytes = 410 + sample count = 1 + format 0: + id = 1 + sampleMimeType = audio/ac4 + channelCount = 2 + sampleRate = 48000 + language = und + drmInitData = -1683793742 + sample 0: + time = 720000 + flags = 1073741825 + data = length 410, hash 63017D46 + crypto mode = 1 + encryption key = length 16, hash 9FDDEA52 +tracksEnded = true diff --git a/testdata/src/test/assets/mp4/sample_ac4_protected.mp4.unknown_length.dump b/testdata/src/test/assets/mp4/sample_ac4_protected.mp4.unknown_length.dump new file mode 100644 index 0000000000..0d0b8317e0 --- /dev/null +++ b/testdata/src/test/assets/mp4/sample_ac4_protected.mp4.unknown_length.dump @@ -0,0 +1,133 @@ +seekMap: + isSeekable = true + duration = 760000 + getPosition(0) = [[timeUs=0, position=950]] + getPosition(1) = [[timeUs=0, position=950]] + getPosition(380000) = [[timeUs=0, position=950]] + getPosition(760000) = [[timeUs=0, position=950]] +numberOfTracks = 1 +track 0: + total output bytes = 7936 + sample count = 19 + format 0: + id = 1 + sampleMimeType = audio/ac4 + channelCount = 2 + sampleRate = 48000 + language = und + drmInitData = -1683793742 + sample 0: + time = 0 + flags = 1073741825 + data = length 384, hash 96EFFFF3 + crypto mode = 1 + encryption key = length 16, hash 9FDDEA52 + sample 1: + time = 40000 + flags = 1073741825 + data = length 384, hash 899279C6 + crypto mode = 1 + encryption key = length 16, hash 9FDDEA52 + sample 2: + time = 80000 + flags = 1073741825 + data = length 384, hash 9EA9F45 + crypto mode = 1 + encryption key = length 16, hash 9FDDEA52 + sample 3: + time = 120000 + flags = 1073741825 + data = length 384, hash 82D362A9 + crypto mode = 1 + encryption key = length 16, hash 9FDDEA52 + sample 4: + time = 160000 + flags = 1073741825 + data = length 384, hash B8705CFB + crypto mode = 1 + encryption key = length 16, hash 9FDDEA52 + sample 5: + time = 200000 + flags = 1073741825 + data = length 384, hash 58B5628E + crypto mode = 1 + encryption key = length 16, hash 9FDDEA52 + sample 6: + time = 240000 + flags = 1073741825 + data = length 384, hash 87F3C13B + crypto mode = 1 + encryption key = length 16, hash 9FDDEA52 + sample 7: + time = 280000 + flags = 1073741825 + data = length 384, hash 54333DC5 + crypto mode = 1 + encryption key = length 16, hash 9FDDEA52 + sample 8: + time = 320000 + flags = 1073741825 + data = length 384, hash 1C49C4B3 + crypto mode = 1 + encryption key = length 16, hash 9FDDEA52 + sample 9: + time = 360000 + flags = 1073741825 + data = length 384, hash 5FDC324F + crypto mode = 1 + encryption key = length 16, hash 9FDDEA52 + sample 10: + time = 400000 + flags = 1073741825 + data = length 384, hash B2A7F444 + crypto mode = 1 + encryption key = length 16, hash 9FDDEA52 + sample 11: + time = 440000 + flags = 1073741825 + data = length 512, hash 5FD06C1E + crypto mode = 1 + encryption key = length 16, hash 9FDDEA52 + sample 12: + time = 480000 + flags = 1073741825 + data = length 537, hash 7ABBDCB + crypto mode = 1 + encryption key = length 16, hash 9FDDEA52 + sample 13: + time = 519999 + flags = 1073741825 + data = length 616, hash 3F657E23 + crypto mode = 1 + encryption key = length 16, hash 9FDDEA52 + sample 14: + time = 560000 + flags = 1073741825 + data = length 453, hash 8FCF0529 + crypto mode = 1 + encryption key = length 16, hash 9FDDEA52 + sample 15: + time = 600000 + flags = 1073741825 + data = length 383, hash 7F8C9E19 + crypto mode = 1 + encryption key = length 16, hash 9FDDEA52 + sample 16: + time = 640000 + flags = 1073741825 + data = length 410, hash 3727858D + crypto mode = 1 + encryption key = length 16, hash 9FDDEA52 + sample 17: + time = 680000 + flags = 1073741825 + data = length 391, hash E2931212 + crypto mode = 1 + encryption key = length 16, hash 9FDDEA52 + sample 18: + time = 720000 + flags = 1073741825 + data = length 410, hash 63017D46 + crypto mode = 1 + encryption key = length 16, hash 9FDDEA52 +tracksEnded = true diff --git a/testdata/src/test/assets/mp4/sample_android_slow_motion.mp4 b/testdata/src/test/assets/mp4/sample_android_slow_motion.mp4 new file mode 100644 index 0000000000..e5594c83e1 Binary files /dev/null and b/testdata/src/test/assets/mp4/sample_android_slow_motion.mp4 differ diff --git a/testdata/src/test/assets/mp4/sample_android_slow_motion.mp4.0.dump b/testdata/src/test/assets/mp4/sample_android_slow_motion.mp4.0.dump new file mode 100644 index 0000000000..e8b96af424 --- /dev/null +++ b/testdata/src/test/assets/mp4/sample_android_slow_motion.mp4.0.dump @@ -0,0 +1,51 @@ +seekMap: + isSeekable = true + duration = 526000 + getPosition(0) = [[timeUs=0, position=1161]] + getPosition(1) = [[timeUs=0, position=1161]] + getPosition(263000) = [[timeUs=0, position=1161]] + getPosition(526000) = [[timeUs=0, position=1161]] +numberOfTracks = 1 +track 0: + total output bytes = 42320 + sample count = 7 + format 0: + id = 1 + sampleMimeType = video/avc + maxInputSize = 34686 + width = 1280 + height = 720 + frameRate = 13.307984 + metadata = entries=[mdta: key=com.android.capture.fps] + initializationData: + data = length 22, hash 4CF81805 + data = length 9, hash FBAFBA1C + sample 0: + time = 0 + flags = 1 + data = length 34656, hash D92B66FF + sample 1: + time = 325344 + flags = 0 + data = length 768, hash D0C3B229 + sample 2: + time = 358677 + flags = 0 + data = length 1184, hash C598EFC0 + sample 3: + time = 392011 + flags = 0 + data = length 576, hash 667AEC2C + sample 4: + time = 425344 + flags = 0 + data = length 1456, hash 430D1498 + sample 5: + time = 458677 + flags = 0 + data = length 1280, hash 12267E0E + sample 6: + time = 492011 + flags = 536870912 + data = length 2400, hash FBCB42C +tracksEnded = true diff --git a/testdata/src/test/assets/mp4/sample_android_slow_motion.mp4.1.dump b/testdata/src/test/assets/mp4/sample_android_slow_motion.mp4.1.dump new file mode 100644 index 0000000000..e8b96af424 --- /dev/null +++ b/testdata/src/test/assets/mp4/sample_android_slow_motion.mp4.1.dump @@ -0,0 +1,51 @@ +seekMap: + isSeekable = true + duration = 526000 + getPosition(0) = [[timeUs=0, position=1161]] + getPosition(1) = [[timeUs=0, position=1161]] + getPosition(263000) = [[timeUs=0, position=1161]] + getPosition(526000) = [[timeUs=0, position=1161]] +numberOfTracks = 1 +track 0: + total output bytes = 42320 + sample count = 7 + format 0: + id = 1 + sampleMimeType = video/avc + maxInputSize = 34686 + width = 1280 + height = 720 + frameRate = 13.307984 + metadata = entries=[mdta: key=com.android.capture.fps] + initializationData: + data = length 22, hash 4CF81805 + data = length 9, hash FBAFBA1C + sample 0: + time = 0 + flags = 1 + data = length 34656, hash D92B66FF + sample 1: + time = 325344 + flags = 0 + data = length 768, hash D0C3B229 + sample 2: + time = 358677 + flags = 0 + data = length 1184, hash C598EFC0 + sample 3: + time = 392011 + flags = 0 + data = length 576, hash 667AEC2C + sample 4: + time = 425344 + flags = 0 + data = length 1456, hash 430D1498 + sample 5: + time = 458677 + flags = 0 + data = length 1280, hash 12267E0E + sample 6: + time = 492011 + flags = 536870912 + data = length 2400, hash FBCB42C +tracksEnded = true diff --git a/testdata/src/test/assets/mp4/sample_android_slow_motion.mp4.2.dump b/testdata/src/test/assets/mp4/sample_android_slow_motion.mp4.2.dump new file mode 100644 index 0000000000..e8b96af424 --- /dev/null +++ b/testdata/src/test/assets/mp4/sample_android_slow_motion.mp4.2.dump @@ -0,0 +1,51 @@ +seekMap: + isSeekable = true + duration = 526000 + getPosition(0) = [[timeUs=0, position=1161]] + getPosition(1) = [[timeUs=0, position=1161]] + getPosition(263000) = [[timeUs=0, position=1161]] + getPosition(526000) = [[timeUs=0, position=1161]] +numberOfTracks = 1 +track 0: + total output bytes = 42320 + sample count = 7 + format 0: + id = 1 + sampleMimeType = video/avc + maxInputSize = 34686 + width = 1280 + height = 720 + frameRate = 13.307984 + metadata = entries=[mdta: key=com.android.capture.fps] + initializationData: + data = length 22, hash 4CF81805 + data = length 9, hash FBAFBA1C + sample 0: + time = 0 + flags = 1 + data = length 34656, hash D92B66FF + sample 1: + time = 325344 + flags = 0 + data = length 768, hash D0C3B229 + sample 2: + time = 358677 + flags = 0 + data = length 1184, hash C598EFC0 + sample 3: + time = 392011 + flags = 0 + data = length 576, hash 667AEC2C + sample 4: + time = 425344 + flags = 0 + data = length 1456, hash 430D1498 + sample 5: + time = 458677 + flags = 0 + data = length 1280, hash 12267E0E + sample 6: + time = 492011 + flags = 536870912 + data = length 2400, hash FBCB42C +tracksEnded = true diff --git a/testdata/src/test/assets/mp4/sample_android_slow_motion.mp4.3.dump b/testdata/src/test/assets/mp4/sample_android_slow_motion.mp4.3.dump new file mode 100644 index 0000000000..e8b96af424 --- /dev/null +++ b/testdata/src/test/assets/mp4/sample_android_slow_motion.mp4.3.dump @@ -0,0 +1,51 @@ +seekMap: + isSeekable = true + duration = 526000 + getPosition(0) = [[timeUs=0, position=1161]] + getPosition(1) = [[timeUs=0, position=1161]] + getPosition(263000) = [[timeUs=0, position=1161]] + getPosition(526000) = [[timeUs=0, position=1161]] +numberOfTracks = 1 +track 0: + total output bytes = 42320 + sample count = 7 + format 0: + id = 1 + sampleMimeType = video/avc + maxInputSize = 34686 + width = 1280 + height = 720 + frameRate = 13.307984 + metadata = entries=[mdta: key=com.android.capture.fps] + initializationData: + data = length 22, hash 4CF81805 + data = length 9, hash FBAFBA1C + sample 0: + time = 0 + flags = 1 + data = length 34656, hash D92B66FF + sample 1: + time = 325344 + flags = 0 + data = length 768, hash D0C3B229 + sample 2: + time = 358677 + flags = 0 + data = length 1184, hash C598EFC0 + sample 3: + time = 392011 + flags = 0 + data = length 576, hash 667AEC2C + sample 4: + time = 425344 + flags = 0 + data = length 1456, hash 430D1498 + sample 5: + time = 458677 + flags = 0 + data = length 1280, hash 12267E0E + sample 6: + time = 492011 + flags = 536870912 + data = length 2400, hash FBCB42C +tracksEnded = true diff --git a/testdata/src/test/assets/mp4/sample_android_slow_motion.mp4.unknown_length.dump b/testdata/src/test/assets/mp4/sample_android_slow_motion.mp4.unknown_length.dump new file mode 100644 index 0000000000..e8b96af424 --- /dev/null +++ b/testdata/src/test/assets/mp4/sample_android_slow_motion.mp4.unknown_length.dump @@ -0,0 +1,51 @@ +seekMap: + isSeekable = true + duration = 526000 + getPosition(0) = [[timeUs=0, position=1161]] + getPosition(1) = [[timeUs=0, position=1161]] + getPosition(263000) = [[timeUs=0, position=1161]] + getPosition(526000) = [[timeUs=0, position=1161]] +numberOfTracks = 1 +track 0: + total output bytes = 42320 + sample count = 7 + format 0: + id = 1 + sampleMimeType = video/avc + maxInputSize = 34686 + width = 1280 + height = 720 + frameRate = 13.307984 + metadata = entries=[mdta: key=com.android.capture.fps] + initializationData: + data = length 22, hash 4CF81805 + data = length 9, hash FBAFBA1C + sample 0: + time = 0 + flags = 1 + data = length 34656, hash D92B66FF + sample 1: + time = 325344 + flags = 0 + data = length 768, hash D0C3B229 + sample 2: + time = 358677 + flags = 0 + data = length 1184, hash C598EFC0 + sample 3: + time = 392011 + flags = 0 + data = length 576, hash 667AEC2C + sample 4: + time = 425344 + flags = 0 + data = length 1456, hash 430D1498 + sample 5: + time = 458677 + flags = 0 + data = length 1280, hash 12267E0E + sample 6: + time = 492011 + flags = 536870912 + data = length 2400, hash FBCB42C +tracksEnded = true diff --git a/testdata/src/test/assets/mp4/sample_eac3.mp4 b/testdata/src/test/assets/mp4/sample_eac3.mp4 new file mode 100644 index 0000000000..2bb1689bb1 Binary files /dev/null and b/testdata/src/test/assets/mp4/sample_eac3.mp4 differ diff --git a/testdata/src/test/assets/mp4/sample_eac3.mp4.0.dump b/testdata/src/test/assets/mp4/sample_eac3.mp4.0.dump new file mode 100644 index 0000000000..8000864576 --- /dev/null +++ b/testdata/src/test/assets/mp4/sample_eac3.mp4.0.dump @@ -0,0 +1,235 @@ +seekMap: + isSeekable = true + duration = 1728000 + getPosition(0) = [[timeUs=0, position=898]] + getPosition(1) = [[timeUs=1, position=898]] + getPosition(864000) = [[timeUs=864000, position=108898]] + getPosition(1728000) = [[timeUs=1728000, position=212898]] +numberOfTracks = 1 +track 0: + total output bytes = 216000 + sample count = 54 + format 0: + id = 1 + sampleMimeType = audio/eac3 + maxInputSize = 4030 + channelCount = 6 + sampleRate = 48000 + language = und + sample 0: + time = 0 + flags = 1 + data = length 4000, hash BAEAFB2A + sample 1: + time = 32000 + flags = 1 + data = length 4000, hash E3C5EBF0 + sample 2: + time = 64000 + flags = 1 + data = length 4000, hash 32E0F957 + sample 3: + time = 96000 + flags = 1 + data = length 4000, hash 5354CC5D + sample 4: + time = 128000 + flags = 1 + data = length 4000, hash FF834906 + sample 5: + time = 160000 + flags = 1 + data = length 4000, hash 6F571E61 + sample 6: + time = 192000 + flags = 1 + data = length 4000, hash 5C931F6B + sample 7: + time = 224000 + flags = 1 + data = length 4000, hash B1FB2E57 + sample 8: + time = 256000 + flags = 1 + data = length 4000, hash C71240EB + sample 9: + time = 288000 + flags = 1 + data = length 4000, hash C3E302EE + sample 10: + time = 320000 + flags = 1 + data = length 4000, hash 7994C27B + sample 11: + time = 352000 + flags = 1 + data = length 4000, hash 1ED4E6F3 + sample 12: + time = 384000 + flags = 1 + data = length 4000, hash 1D5E6AAC + sample 13: + time = 416000 + flags = 1 + data = length 4000, hash 30058F51 + sample 14: + time = 448000 + flags = 1 + data = length 4000, hash 15DD0E4A + sample 15: + time = 480000 + flags = 1 + data = length 4000, hash 37BE7C15 + sample 16: + time = 512000 + flags = 1 + data = length 4000, hash 7CFDD34B + sample 17: + time = 544000 + flags = 1 + data = length 4000, hash 27F20D29 + sample 18: + time = 576000 + flags = 1 + data = length 4000, hash 6F565894 + sample 19: + time = 608000 + flags = 1 + data = length 4000, hash A6F07C4A + sample 20: + time = 640000 + flags = 1 + data = length 4000, hash 3A0CA15C + sample 21: + time = 672000 + flags = 1 + data = length 4000, hash DB365414 + sample 22: + time = 704000 + flags = 1 + data = length 4000, hash 31E08469 + sample 23: + time = 736000 + flags = 1 + data = length 4000, hash 315F5C28 + sample 24: + time = 768000 + flags = 1 + data = length 4000, hash CC65DF80 + sample 25: + time = 800000 + flags = 1 + data = length 4000, hash 503FB64C + sample 26: + time = 832000 + flags = 1 + data = length 4000, hash 817CF735 + sample 27: + time = 864000 + flags = 1 + data = length 4000, hash 37391ADA + sample 28: + time = 896000 + flags = 1 + data = length 4000, hash 37391ADA + sample 29: + time = 928000 + flags = 1 + data = length 4000, hash 64DBF751 + sample 30: + time = 960000 + flags = 1 + data = length 4000, hash 81AE828E + sample 31: + time = 992000 + flags = 1 + data = length 4000, hash 767D6C98 + sample 32: + time = 1024000 + flags = 1 + data = length 4000, hash A5F6D4E + sample 33: + time = 1056000 + flags = 1 + data = length 4000, hash EABC6B0D + sample 34: + time = 1088000 + flags = 1 + data = length 4000, hash F47EF742 + sample 35: + time = 1120000 + flags = 1 + data = length 4000, hash 9B2549DA + sample 36: + time = 1152000 + flags = 1 + data = length 4000, hash A12733C9 + sample 37: + time = 1184000 + flags = 1 + data = length 4000, hash 95F62E99 + sample 38: + time = 1216000 + flags = 1 + data = length 4000, hash A4D858 + sample 39: + time = 1248000 + flags = 1 + data = length 4000, hash A4D858 + sample 40: + time = 1280000 + flags = 1 + data = length 4000, hash 22C1A129 + sample 41: + time = 1312000 + flags = 1 + data = length 4000, hash 2C51E4A1 + sample 42: + time = 1344000 + flags = 1 + data = length 4000, hash 3782E8BB + sample 43: + time = 1376000 + flags = 1 + data = length 4000, hash 2C51E4A1 + sample 44: + time = 1408000 + flags = 1 + data = length 4000, hash BDB3D129 + sample 45: + time = 1440000 + flags = 1 + data = length 4000, hash F642A55 + sample 46: + time = 1472000 + flags = 1 + data = length 4000, hash 32F259F4 + sample 47: + time = 1504000 + flags = 1 + data = length 4000, hash 4C987B7C + sample 48: + time = 1536000 + flags = 1 + data = length 4000, hash 57C98E1C + sample 49: + time = 1568000 + flags = 1 + data = length 4000, hash 4C987B7C + sample 50: + time = 1600000 + flags = 1 + data = length 4000, hash 4C987B7C + sample 51: + time = 1632000 + flags = 1 + data = length 4000, hash 4C987B7C + sample 52: + time = 1664000 + flags = 1 + data = length 4000, hash 4C987B7C + sample 53: + time = 1696000 + flags = 536870913 + data = length 4000, hash 4C987B7C +tracksEnded = true diff --git a/testdata/src/test/assets/mp4/sample_eac3.mp4.1.dump b/testdata/src/test/assets/mp4/sample_eac3.mp4.1.dump new file mode 100644 index 0000000000..49ab3da0aa --- /dev/null +++ b/testdata/src/test/assets/mp4/sample_eac3.mp4.1.dump @@ -0,0 +1,163 @@ +seekMap: + isSeekable = true + duration = 1728000 + getPosition(0) = [[timeUs=0, position=898]] + getPosition(1) = [[timeUs=1, position=898]] + getPosition(864000) = [[timeUs=864000, position=108898]] + getPosition(1728000) = [[timeUs=1728000, position=212898]] +numberOfTracks = 1 +track 0: + total output bytes = 144000 + sample count = 36 + format 0: + id = 1 + sampleMimeType = audio/eac3 + maxInputSize = 4030 + channelCount = 6 + sampleRate = 48000 + language = und + sample 0: + time = 576000 + flags = 1 + data = length 4000, hash 6F565894 + sample 1: + time = 608000 + flags = 1 + data = length 4000, hash A6F07C4A + sample 2: + time = 640000 + flags = 1 + data = length 4000, hash 3A0CA15C + sample 3: + time = 672000 + flags = 1 + data = length 4000, hash DB365414 + sample 4: + time = 704000 + flags = 1 + data = length 4000, hash 31E08469 + sample 5: + time = 736000 + flags = 1 + data = length 4000, hash 315F5C28 + sample 6: + time = 768000 + flags = 1 + data = length 4000, hash CC65DF80 + sample 7: + time = 800000 + flags = 1 + data = length 4000, hash 503FB64C + sample 8: + time = 832000 + flags = 1 + data = length 4000, hash 817CF735 + sample 9: + time = 864000 + flags = 1 + data = length 4000, hash 37391ADA + sample 10: + time = 896000 + flags = 1 + data = length 4000, hash 37391ADA + sample 11: + time = 928000 + flags = 1 + data = length 4000, hash 64DBF751 + sample 12: + time = 960000 + flags = 1 + data = length 4000, hash 81AE828E + sample 13: + time = 992000 + flags = 1 + data = length 4000, hash 767D6C98 + sample 14: + time = 1024000 + flags = 1 + data = length 4000, hash A5F6D4E + sample 15: + time = 1056000 + flags = 1 + data = length 4000, hash EABC6B0D + sample 16: + time = 1088000 + flags = 1 + data = length 4000, hash F47EF742 + sample 17: + time = 1120000 + flags = 1 + data = length 4000, hash 9B2549DA + sample 18: + time = 1152000 + flags = 1 + data = length 4000, hash A12733C9 + sample 19: + time = 1184000 + flags = 1 + data = length 4000, hash 95F62E99 + sample 20: + time = 1216000 + flags = 1 + data = length 4000, hash A4D858 + sample 21: + time = 1248000 + flags = 1 + data = length 4000, hash A4D858 + sample 22: + time = 1280000 + flags = 1 + data = length 4000, hash 22C1A129 + sample 23: + time = 1312000 + flags = 1 + data = length 4000, hash 2C51E4A1 + sample 24: + time = 1344000 + flags = 1 + data = length 4000, hash 3782E8BB + sample 25: + time = 1376000 + flags = 1 + data = length 4000, hash 2C51E4A1 + sample 26: + time = 1408000 + flags = 1 + data = length 4000, hash BDB3D129 + sample 27: + time = 1440000 + flags = 1 + data = length 4000, hash F642A55 + sample 28: + time = 1472000 + flags = 1 + data = length 4000, hash 32F259F4 + sample 29: + time = 1504000 + flags = 1 + data = length 4000, hash 4C987B7C + sample 30: + time = 1536000 + flags = 1 + data = length 4000, hash 57C98E1C + sample 31: + time = 1568000 + flags = 1 + data = length 4000, hash 4C987B7C + sample 32: + time = 1600000 + flags = 1 + data = length 4000, hash 4C987B7C + sample 33: + time = 1632000 + flags = 1 + data = length 4000, hash 4C987B7C + sample 34: + time = 1664000 + flags = 1 + data = length 4000, hash 4C987B7C + sample 35: + time = 1696000 + flags = 536870913 + data = length 4000, hash 4C987B7C +tracksEnded = true diff --git a/testdata/src/test/assets/mp4/sample_eac3.mp4.2.dump b/testdata/src/test/assets/mp4/sample_eac3.mp4.2.dump new file mode 100644 index 0000000000..19bfc7c5fa --- /dev/null +++ b/testdata/src/test/assets/mp4/sample_eac3.mp4.2.dump @@ -0,0 +1,91 @@ +seekMap: + isSeekable = true + duration = 1728000 + getPosition(0) = [[timeUs=0, position=898]] + getPosition(1) = [[timeUs=1, position=898]] + getPosition(864000) = [[timeUs=864000, position=108898]] + getPosition(1728000) = [[timeUs=1728000, position=212898]] +numberOfTracks = 1 +track 0: + total output bytes = 72000 + sample count = 18 + format 0: + id = 1 + sampleMimeType = audio/eac3 + maxInputSize = 4030 + channelCount = 6 + sampleRate = 48000 + language = und + sample 0: + time = 1152000 + flags = 1 + data = length 4000, hash A12733C9 + sample 1: + time = 1184000 + flags = 1 + data = length 4000, hash 95F62E99 + sample 2: + time = 1216000 + flags = 1 + data = length 4000, hash A4D858 + sample 3: + time = 1248000 + flags = 1 + data = length 4000, hash A4D858 + sample 4: + time = 1280000 + flags = 1 + data = length 4000, hash 22C1A129 + sample 5: + time = 1312000 + flags = 1 + data = length 4000, hash 2C51E4A1 + sample 6: + time = 1344000 + flags = 1 + data = length 4000, hash 3782E8BB + sample 7: + time = 1376000 + flags = 1 + data = length 4000, hash 2C51E4A1 + sample 8: + time = 1408000 + flags = 1 + data = length 4000, hash BDB3D129 + sample 9: + time = 1440000 + flags = 1 + data = length 4000, hash F642A55 + sample 10: + time = 1472000 + flags = 1 + data = length 4000, hash 32F259F4 + sample 11: + time = 1504000 + flags = 1 + data = length 4000, hash 4C987B7C + sample 12: + time = 1536000 + flags = 1 + data = length 4000, hash 57C98E1C + sample 13: + time = 1568000 + flags = 1 + data = length 4000, hash 4C987B7C + sample 14: + time = 1600000 + flags = 1 + data = length 4000, hash 4C987B7C + sample 15: + time = 1632000 + flags = 1 + data = length 4000, hash 4C987B7C + sample 16: + time = 1664000 + flags = 1 + data = length 4000, hash 4C987B7C + sample 17: + time = 1696000 + flags = 536870913 + data = length 4000, hash 4C987B7C +tracksEnded = true diff --git a/testdata/src/test/assets/mp4/sample_eac3.mp4.3.dump b/testdata/src/test/assets/mp4/sample_eac3.mp4.3.dump new file mode 100644 index 0000000000..d34514d8a8 --- /dev/null +++ b/testdata/src/test/assets/mp4/sample_eac3.mp4.3.dump @@ -0,0 +1,23 @@ +seekMap: + isSeekable = true + duration = 1728000 + getPosition(0) = [[timeUs=0, position=898]] + getPosition(1) = [[timeUs=1, position=898]] + getPosition(864000) = [[timeUs=864000, position=108898]] + getPosition(1728000) = [[timeUs=1728000, position=212898]] +numberOfTracks = 1 +track 0: + total output bytes = 4000 + sample count = 1 + format 0: + id = 1 + sampleMimeType = audio/eac3 + maxInputSize = 4030 + channelCount = 6 + sampleRate = 48000 + language = und + sample 0: + time = 1696000 + flags = 536870913 + data = length 4000, hash 4C987B7C +tracksEnded = true diff --git a/testdata/src/test/assets/mp4/sample_eac3.mp4.unknown_length.dump b/testdata/src/test/assets/mp4/sample_eac3.mp4.unknown_length.dump new file mode 100644 index 0000000000..8000864576 --- /dev/null +++ b/testdata/src/test/assets/mp4/sample_eac3.mp4.unknown_length.dump @@ -0,0 +1,235 @@ +seekMap: + isSeekable = true + duration = 1728000 + getPosition(0) = [[timeUs=0, position=898]] + getPosition(1) = [[timeUs=1, position=898]] + getPosition(864000) = [[timeUs=864000, position=108898]] + getPosition(1728000) = [[timeUs=1728000, position=212898]] +numberOfTracks = 1 +track 0: + total output bytes = 216000 + sample count = 54 + format 0: + id = 1 + sampleMimeType = audio/eac3 + maxInputSize = 4030 + channelCount = 6 + sampleRate = 48000 + language = und + sample 0: + time = 0 + flags = 1 + data = length 4000, hash BAEAFB2A + sample 1: + time = 32000 + flags = 1 + data = length 4000, hash E3C5EBF0 + sample 2: + time = 64000 + flags = 1 + data = length 4000, hash 32E0F957 + sample 3: + time = 96000 + flags = 1 + data = length 4000, hash 5354CC5D + sample 4: + time = 128000 + flags = 1 + data = length 4000, hash FF834906 + sample 5: + time = 160000 + flags = 1 + data = length 4000, hash 6F571E61 + sample 6: + time = 192000 + flags = 1 + data = length 4000, hash 5C931F6B + sample 7: + time = 224000 + flags = 1 + data = length 4000, hash B1FB2E57 + sample 8: + time = 256000 + flags = 1 + data = length 4000, hash C71240EB + sample 9: + time = 288000 + flags = 1 + data = length 4000, hash C3E302EE + sample 10: + time = 320000 + flags = 1 + data = length 4000, hash 7994C27B + sample 11: + time = 352000 + flags = 1 + data = length 4000, hash 1ED4E6F3 + sample 12: + time = 384000 + flags = 1 + data = length 4000, hash 1D5E6AAC + sample 13: + time = 416000 + flags = 1 + data = length 4000, hash 30058F51 + sample 14: + time = 448000 + flags = 1 + data = length 4000, hash 15DD0E4A + sample 15: + time = 480000 + flags = 1 + data = length 4000, hash 37BE7C15 + sample 16: + time = 512000 + flags = 1 + data = length 4000, hash 7CFDD34B + sample 17: + time = 544000 + flags = 1 + data = length 4000, hash 27F20D29 + sample 18: + time = 576000 + flags = 1 + data = length 4000, hash 6F565894 + sample 19: + time = 608000 + flags = 1 + data = length 4000, hash A6F07C4A + sample 20: + time = 640000 + flags = 1 + data = length 4000, hash 3A0CA15C + sample 21: + time = 672000 + flags = 1 + data = length 4000, hash DB365414 + sample 22: + time = 704000 + flags = 1 + data = length 4000, hash 31E08469 + sample 23: + time = 736000 + flags = 1 + data = length 4000, hash 315F5C28 + sample 24: + time = 768000 + flags = 1 + data = length 4000, hash CC65DF80 + sample 25: + time = 800000 + flags = 1 + data = length 4000, hash 503FB64C + sample 26: + time = 832000 + flags = 1 + data = length 4000, hash 817CF735 + sample 27: + time = 864000 + flags = 1 + data = length 4000, hash 37391ADA + sample 28: + time = 896000 + flags = 1 + data = length 4000, hash 37391ADA + sample 29: + time = 928000 + flags = 1 + data = length 4000, hash 64DBF751 + sample 30: + time = 960000 + flags = 1 + data = length 4000, hash 81AE828E + sample 31: + time = 992000 + flags = 1 + data = length 4000, hash 767D6C98 + sample 32: + time = 1024000 + flags = 1 + data = length 4000, hash A5F6D4E + sample 33: + time = 1056000 + flags = 1 + data = length 4000, hash EABC6B0D + sample 34: + time = 1088000 + flags = 1 + data = length 4000, hash F47EF742 + sample 35: + time = 1120000 + flags = 1 + data = length 4000, hash 9B2549DA + sample 36: + time = 1152000 + flags = 1 + data = length 4000, hash A12733C9 + sample 37: + time = 1184000 + flags = 1 + data = length 4000, hash 95F62E99 + sample 38: + time = 1216000 + flags = 1 + data = length 4000, hash A4D858 + sample 39: + time = 1248000 + flags = 1 + data = length 4000, hash A4D858 + sample 40: + time = 1280000 + flags = 1 + data = length 4000, hash 22C1A129 + sample 41: + time = 1312000 + flags = 1 + data = length 4000, hash 2C51E4A1 + sample 42: + time = 1344000 + flags = 1 + data = length 4000, hash 3782E8BB + sample 43: + time = 1376000 + flags = 1 + data = length 4000, hash 2C51E4A1 + sample 44: + time = 1408000 + flags = 1 + data = length 4000, hash BDB3D129 + sample 45: + time = 1440000 + flags = 1 + data = length 4000, hash F642A55 + sample 46: + time = 1472000 + flags = 1 + data = length 4000, hash 32F259F4 + sample 47: + time = 1504000 + flags = 1 + data = length 4000, hash 4C987B7C + sample 48: + time = 1536000 + flags = 1 + data = length 4000, hash 57C98E1C + sample 49: + time = 1568000 + flags = 1 + data = length 4000, hash 4C987B7C + sample 50: + time = 1600000 + flags = 1 + data = length 4000, hash 4C987B7C + sample 51: + time = 1632000 + flags = 1 + data = length 4000, hash 4C987B7C + sample 52: + time = 1664000 + flags = 1 + data = length 4000, hash 4C987B7C + sample 53: + time = 1696000 + flags = 536870913 + data = length 4000, hash 4C987B7C +tracksEnded = true diff --git a/testdata/src/test/assets/mp4/sample_eac3_fragmented.mp4 b/testdata/src/test/assets/mp4/sample_eac3_fragmented.mp4 new file mode 100644 index 0000000000..ebd36986fb Binary files /dev/null and b/testdata/src/test/assets/mp4/sample_eac3_fragmented.mp4 differ diff --git a/testdata/src/test/assets/mp4/sample_eac3_fragmented.mp4.0.dump b/testdata/src/test/assets/mp4/sample_eac3_fragmented.mp4.0.dump new file mode 100644 index 0000000000..a7f3c63f8d --- /dev/null +++ b/testdata/src/test/assets/mp4/sample_eac3_fragmented.mp4.0.dump @@ -0,0 +1,234 @@ +seekMap: + isSeekable = true + duration = 1728000 + getPosition(0) = [[timeUs=0, position=638]] + getPosition(1) = [[timeUs=0, position=638]] + getPosition(864000) = [[timeUs=0, position=638]] + getPosition(1728000) = [[timeUs=0, position=638]] +numberOfTracks = 1 +track 0: + total output bytes = 216000 + sample count = 54 + format 0: + id = 1 + sampleMimeType = audio/eac3 + channelCount = 6 + sampleRate = 48000 + language = und + sample 0: + time = 0 + flags = 1 + data = length 4000, hash BAEAFB2A + sample 1: + time = 32000 + flags = 1 + data = length 4000, hash E3C5EBF0 + sample 2: + time = 64000 + flags = 1 + data = length 4000, hash 32E0F957 + sample 3: + time = 96000 + flags = 1 + data = length 4000, hash 5354CC5D + sample 4: + time = 128000 + flags = 1 + data = length 4000, hash FF834906 + sample 5: + time = 160000 + flags = 1 + data = length 4000, hash 6F571E61 + sample 6: + time = 192000 + flags = 1 + data = length 4000, hash 5C931F6B + sample 7: + time = 224000 + flags = 1 + data = length 4000, hash B1FB2E57 + sample 8: + time = 256000 + flags = 1 + data = length 4000, hash C71240EB + sample 9: + time = 288000 + flags = 1 + data = length 4000, hash C3E302EE + sample 10: + time = 320000 + flags = 1 + data = length 4000, hash 7994C27B + sample 11: + time = 352000 + flags = 1 + data = length 4000, hash 1ED4E6F3 + sample 12: + time = 384000 + flags = 1 + data = length 4000, hash 1D5E6AAC + sample 13: + time = 416000 + flags = 1 + data = length 4000, hash 30058F51 + sample 14: + time = 448000 + flags = 1 + data = length 4000, hash 15DD0E4A + sample 15: + time = 480000 + flags = 1 + data = length 4000, hash 37BE7C15 + sample 16: + time = 512000 + flags = 1 + data = length 4000, hash 7CFDD34B + sample 17: + time = 544000 + flags = 1 + data = length 4000, hash 27F20D29 + sample 18: + time = 576000 + flags = 1 + data = length 4000, hash 6F565894 + sample 19: + time = 608000 + flags = 1 + data = length 4000, hash A6F07C4A + sample 20: + time = 640000 + flags = 1 + data = length 4000, hash 3A0CA15C + sample 21: + time = 672000 + flags = 1 + data = length 4000, hash DB365414 + sample 22: + time = 704000 + flags = 1 + data = length 4000, hash 31E08469 + sample 23: + time = 736000 + flags = 1 + data = length 4000, hash 315F5C28 + sample 24: + time = 768000 + flags = 1 + data = length 4000, hash CC65DF80 + sample 25: + time = 800000 + flags = 1 + data = length 4000, hash 503FB64C + sample 26: + time = 832000 + flags = 1 + data = length 4000, hash 817CF735 + sample 27: + time = 864000 + flags = 1 + data = length 4000, hash 37391ADA + sample 28: + time = 896000 + flags = 1 + data = length 4000, hash 37391ADA + sample 29: + time = 928000 + flags = 1 + data = length 4000, hash 64DBF751 + sample 30: + time = 960000 + flags = 1 + data = length 4000, hash 81AE828E + sample 31: + time = 992000 + flags = 1 + data = length 4000, hash 767D6C98 + sample 32: + time = 1024000 + flags = 1 + data = length 4000, hash A5F6D4E + sample 33: + time = 1056000 + flags = 1 + data = length 4000, hash EABC6B0D + sample 34: + time = 1088000 + flags = 1 + data = length 4000, hash F47EF742 + sample 35: + time = 1120000 + flags = 1 + data = length 4000, hash 9B2549DA + sample 36: + time = 1152000 + flags = 1 + data = length 4000, hash A12733C9 + sample 37: + time = 1184000 + flags = 1 + data = length 4000, hash 95F62E99 + sample 38: + time = 1216000 + flags = 1 + data = length 4000, hash A4D858 + sample 39: + time = 1248000 + flags = 1 + data = length 4000, hash A4D858 + sample 40: + time = 1280000 + flags = 1 + data = length 4000, hash 22C1A129 + sample 41: + time = 1312000 + flags = 1 + data = length 4000, hash 2C51E4A1 + sample 42: + time = 1344000 + flags = 1 + data = length 4000, hash 3782E8BB + sample 43: + time = 1376000 + flags = 1 + data = length 4000, hash 2C51E4A1 + sample 44: + time = 1408000 + flags = 1 + data = length 4000, hash BDB3D129 + sample 45: + time = 1440000 + flags = 1 + data = length 4000, hash F642A55 + sample 46: + time = 1472000 + flags = 1 + data = length 4000, hash 32F259F4 + sample 47: + time = 1504000 + flags = 1 + data = length 4000, hash 4C987B7C + sample 48: + time = 1536000 + flags = 1 + data = length 4000, hash 57C98E1C + sample 49: + time = 1568000 + flags = 1 + data = length 4000, hash 4C987B7C + sample 50: + time = 1600000 + flags = 1 + data = length 4000, hash 4C987B7C + sample 51: + time = 1632000 + flags = 1 + data = length 4000, hash 4C987B7C + sample 52: + time = 1664000 + flags = 1 + data = length 4000, hash 4C987B7C + sample 53: + time = 1696000 + flags = 1 + data = length 4000, hash 4C987B7C +tracksEnded = true diff --git a/testdata/src/test/assets/mp4/sample_eac3_fragmented.mp4.1.dump b/testdata/src/test/assets/mp4/sample_eac3_fragmented.mp4.1.dump new file mode 100644 index 0000000000..a627d00633 --- /dev/null +++ b/testdata/src/test/assets/mp4/sample_eac3_fragmented.mp4.1.dump @@ -0,0 +1,166 @@ +seekMap: + isSeekable = true + duration = 1728000 + getPosition(0) = [[timeUs=0, position=638]] + getPosition(1) = [[timeUs=0, position=638]] + getPosition(864000) = [[timeUs=0, position=638]] + getPosition(1728000) = [[timeUs=0, position=638]] +numberOfTracks = 1 +track 0: + total output bytes = 148000 + sample count = 37 + format 0: + id = 1 + sampleMimeType = audio/eac3 + channelCount = 6 + sampleRate = 48000 + language = und + sample 0: + time = 544000 + flags = 1 + data = length 4000, hash 27F20D29 + sample 1: + time = 576000 + flags = 1 + data = length 4000, hash 6F565894 + sample 2: + time = 608000 + flags = 1 + data = length 4000, hash A6F07C4A + sample 3: + time = 640000 + flags = 1 + data = length 4000, hash 3A0CA15C + sample 4: + time = 672000 + flags = 1 + data = length 4000, hash DB365414 + sample 5: + time = 704000 + flags = 1 + data = length 4000, hash 31E08469 + sample 6: + time = 736000 + flags = 1 + data = length 4000, hash 315F5C28 + sample 7: + time = 768000 + flags = 1 + data = length 4000, hash CC65DF80 + sample 8: + time = 800000 + flags = 1 + data = length 4000, hash 503FB64C + sample 9: + time = 832000 + flags = 1 + data = length 4000, hash 817CF735 + sample 10: + time = 864000 + flags = 1 + data = length 4000, hash 37391ADA + sample 11: + time = 896000 + flags = 1 + data = length 4000, hash 37391ADA + sample 12: + time = 928000 + flags = 1 + data = length 4000, hash 64DBF751 + sample 13: + time = 960000 + flags = 1 + data = length 4000, hash 81AE828E + sample 14: + time = 992000 + flags = 1 + data = length 4000, hash 767D6C98 + sample 15: + time = 1024000 + flags = 1 + data = length 4000, hash A5F6D4E + sample 16: + time = 1056000 + flags = 1 + data = length 4000, hash EABC6B0D + sample 17: + time = 1088000 + flags = 1 + data = length 4000, hash F47EF742 + sample 18: + time = 1120000 + flags = 1 + data = length 4000, hash 9B2549DA + sample 19: + time = 1152000 + flags = 1 + data = length 4000, hash A12733C9 + sample 20: + time = 1184000 + flags = 1 + data = length 4000, hash 95F62E99 + sample 21: + time = 1216000 + flags = 1 + data = length 4000, hash A4D858 + sample 22: + time = 1248000 + flags = 1 + data = length 4000, hash A4D858 + sample 23: + time = 1280000 + flags = 1 + data = length 4000, hash 22C1A129 + sample 24: + time = 1312000 + flags = 1 + data = length 4000, hash 2C51E4A1 + sample 25: + time = 1344000 + flags = 1 + data = length 4000, hash 3782E8BB + sample 26: + time = 1376000 + flags = 1 + data = length 4000, hash 2C51E4A1 + sample 27: + time = 1408000 + flags = 1 + data = length 4000, hash BDB3D129 + sample 28: + time = 1440000 + flags = 1 + data = length 4000, hash F642A55 + sample 29: + time = 1472000 + flags = 1 + data = length 4000, hash 32F259F4 + sample 30: + time = 1504000 + flags = 1 + data = length 4000, hash 4C987B7C + sample 31: + time = 1536000 + flags = 1 + data = length 4000, hash 57C98E1C + sample 32: + time = 1568000 + flags = 1 + data = length 4000, hash 4C987B7C + sample 33: + time = 1600000 + flags = 1 + data = length 4000, hash 4C987B7C + sample 34: + time = 1632000 + flags = 1 + data = length 4000, hash 4C987B7C + sample 35: + time = 1664000 + flags = 1 + data = length 4000, hash 4C987B7C + sample 36: + time = 1696000 + flags = 1 + data = length 4000, hash 4C987B7C +tracksEnded = true diff --git a/testdata/src/test/assets/mp4/sample_eac3_fragmented.mp4.2.dump b/testdata/src/test/assets/mp4/sample_eac3_fragmented.mp4.2.dump new file mode 100644 index 0000000000..31013410b6 --- /dev/null +++ b/testdata/src/test/assets/mp4/sample_eac3_fragmented.mp4.2.dump @@ -0,0 +1,94 @@ +seekMap: + isSeekable = true + duration = 1728000 + getPosition(0) = [[timeUs=0, position=638]] + getPosition(1) = [[timeUs=0, position=638]] + getPosition(864000) = [[timeUs=0, position=638]] + getPosition(1728000) = [[timeUs=0, position=638]] +numberOfTracks = 1 +track 0: + total output bytes = 76000 + sample count = 19 + format 0: + id = 1 + sampleMimeType = audio/eac3 + channelCount = 6 + sampleRate = 48000 + language = und + sample 0: + time = 1120000 + flags = 1 + data = length 4000, hash 9B2549DA + sample 1: + time = 1152000 + flags = 1 + data = length 4000, hash A12733C9 + sample 2: + time = 1184000 + flags = 1 + data = length 4000, hash 95F62E99 + sample 3: + time = 1216000 + flags = 1 + data = length 4000, hash A4D858 + sample 4: + time = 1248000 + flags = 1 + data = length 4000, hash A4D858 + sample 5: + time = 1280000 + flags = 1 + data = length 4000, hash 22C1A129 + sample 6: + time = 1312000 + flags = 1 + data = length 4000, hash 2C51E4A1 + sample 7: + time = 1344000 + flags = 1 + data = length 4000, hash 3782E8BB + sample 8: + time = 1376000 + flags = 1 + data = length 4000, hash 2C51E4A1 + sample 9: + time = 1408000 + flags = 1 + data = length 4000, hash BDB3D129 + sample 10: + time = 1440000 + flags = 1 + data = length 4000, hash F642A55 + sample 11: + time = 1472000 + flags = 1 + data = length 4000, hash 32F259F4 + sample 12: + time = 1504000 + flags = 1 + data = length 4000, hash 4C987B7C + sample 13: + time = 1536000 + flags = 1 + data = length 4000, hash 57C98E1C + sample 14: + time = 1568000 + flags = 1 + data = length 4000, hash 4C987B7C + sample 15: + time = 1600000 + flags = 1 + data = length 4000, hash 4C987B7C + sample 16: + time = 1632000 + flags = 1 + data = length 4000, hash 4C987B7C + sample 17: + time = 1664000 + flags = 1 + data = length 4000, hash 4C987B7C + sample 18: + time = 1696000 + flags = 1 + data = length 4000, hash 4C987B7C +tracksEnded = true diff --git a/testdata/src/test/assets/mp4/sample_eac3_fragmented.mp4.3.dump b/testdata/src/test/assets/mp4/sample_eac3_fragmented.mp4.3.dump new file mode 100644 index 0000000000..13ff558eaa --- /dev/null +++ b/testdata/src/test/assets/mp4/sample_eac3_fragmented.mp4.3.dump @@ -0,0 +1,22 @@ +seekMap: + isSeekable = true + duration = 1728000 + getPosition(0) = [[timeUs=0, position=638]] + getPosition(1) = [[timeUs=0, position=638]] + getPosition(864000) = [[timeUs=0, position=638]] + getPosition(1728000) = [[timeUs=0, position=638]] +numberOfTracks = 1 +track 0: + total output bytes = 4000 + sample count = 1 + format 0: + id = 1 + sampleMimeType = audio/eac3 + channelCount = 6 + sampleRate = 48000 + language = und + sample 0: + time = 1696000 + flags = 1 + data = length 4000, hash 4C987B7C +tracksEnded = true diff --git a/testdata/src/test/assets/mp4/sample_eac3_fragmented.mp4.unknown_length.dump b/testdata/src/test/assets/mp4/sample_eac3_fragmented.mp4.unknown_length.dump new file mode 100644 index 0000000000..a7f3c63f8d --- /dev/null +++ b/testdata/src/test/assets/mp4/sample_eac3_fragmented.mp4.unknown_length.dump @@ -0,0 +1,234 @@ +seekMap: + isSeekable = true + duration = 1728000 + getPosition(0) = [[timeUs=0, position=638]] + getPosition(1) = [[timeUs=0, position=638]] + getPosition(864000) = [[timeUs=0, position=638]] + getPosition(1728000) = [[timeUs=0, position=638]] +numberOfTracks = 1 +track 0: + total output bytes = 216000 + sample count = 54 + format 0: + id = 1 + sampleMimeType = audio/eac3 + channelCount = 6 + sampleRate = 48000 + language = und + sample 0: + time = 0 + flags = 1 + data = length 4000, hash BAEAFB2A + sample 1: + time = 32000 + flags = 1 + data = length 4000, hash E3C5EBF0 + sample 2: + time = 64000 + flags = 1 + data = length 4000, hash 32E0F957 + sample 3: + time = 96000 + flags = 1 + data = length 4000, hash 5354CC5D + sample 4: + time = 128000 + flags = 1 + data = length 4000, hash FF834906 + sample 5: + time = 160000 + flags = 1 + data = length 4000, hash 6F571E61 + sample 6: + time = 192000 + flags = 1 + data = length 4000, hash 5C931F6B + sample 7: + time = 224000 + flags = 1 + data = length 4000, hash B1FB2E57 + sample 8: + time = 256000 + flags = 1 + data = length 4000, hash C71240EB + sample 9: + time = 288000 + flags = 1 + data = length 4000, hash C3E302EE + sample 10: + time = 320000 + flags = 1 + data = length 4000, hash 7994C27B + sample 11: + time = 352000 + flags = 1 + data = length 4000, hash 1ED4E6F3 + sample 12: + time = 384000 + flags = 1 + data = length 4000, hash 1D5E6AAC + sample 13: + time = 416000 + flags = 1 + data = length 4000, hash 30058F51 + sample 14: + time = 448000 + flags = 1 + data = length 4000, hash 15DD0E4A + sample 15: + time = 480000 + flags = 1 + data = length 4000, hash 37BE7C15 + sample 16: + time = 512000 + flags = 1 + data = length 4000, hash 7CFDD34B + sample 17: + time = 544000 + flags = 1 + data = length 4000, hash 27F20D29 + sample 18: + time = 576000 + flags = 1 + data = length 4000, hash 6F565894 + sample 19: + time = 608000 + flags = 1 + data = length 4000, hash A6F07C4A + sample 20: + time = 640000 + flags = 1 + data = length 4000, hash 3A0CA15C + sample 21: + time = 672000 + flags = 1 + data = length 4000, hash DB365414 + sample 22: + time = 704000 + flags = 1 + data = length 4000, hash 31E08469 + sample 23: + time = 736000 + flags = 1 + data = length 4000, hash 315F5C28 + sample 24: + time = 768000 + flags = 1 + data = length 4000, hash CC65DF80 + sample 25: + time = 800000 + flags = 1 + data = length 4000, hash 503FB64C + sample 26: + time = 832000 + flags = 1 + data = length 4000, hash 817CF735 + sample 27: + time = 864000 + flags = 1 + data = length 4000, hash 37391ADA + sample 28: + time = 896000 + flags = 1 + data = length 4000, hash 37391ADA + sample 29: + time = 928000 + flags = 1 + data = length 4000, hash 64DBF751 + sample 30: + time = 960000 + flags = 1 + data = length 4000, hash 81AE828E + sample 31: + time = 992000 + flags = 1 + data = length 4000, hash 767D6C98 + sample 32: + time = 1024000 + flags = 1 + data = length 4000, hash A5F6D4E + sample 33: + time = 1056000 + flags = 1 + data = length 4000, hash EABC6B0D + sample 34: + time = 1088000 + flags = 1 + data = length 4000, hash F47EF742 + sample 35: + time = 1120000 + flags = 1 + data = length 4000, hash 9B2549DA + sample 36: + time = 1152000 + flags = 1 + data = length 4000, hash A12733C9 + sample 37: + time = 1184000 + flags = 1 + data = length 4000, hash 95F62E99 + sample 38: + time = 1216000 + flags = 1 + data = length 4000, hash A4D858 + sample 39: + time = 1248000 + flags = 1 + data = length 4000, hash A4D858 + sample 40: + time = 1280000 + flags = 1 + data = length 4000, hash 22C1A129 + sample 41: + time = 1312000 + flags = 1 + data = length 4000, hash 2C51E4A1 + sample 42: + time = 1344000 + flags = 1 + data = length 4000, hash 3782E8BB + sample 43: + time = 1376000 + flags = 1 + data = length 4000, hash 2C51E4A1 + sample 44: + time = 1408000 + flags = 1 + data = length 4000, hash BDB3D129 + sample 45: + time = 1440000 + flags = 1 + data = length 4000, hash F642A55 + sample 46: + time = 1472000 + flags = 1 + data = length 4000, hash 32F259F4 + sample 47: + time = 1504000 + flags = 1 + data = length 4000, hash 4C987B7C + sample 48: + time = 1536000 + flags = 1 + data = length 4000, hash 57C98E1C + sample 49: + time = 1568000 + flags = 1 + data = length 4000, hash 4C987B7C + sample 50: + time = 1600000 + flags = 1 + data = length 4000, hash 4C987B7C + sample 51: + time = 1632000 + flags = 1 + data = length 4000, hash 4C987B7C + sample 52: + time = 1664000 + flags = 1 + data = length 4000, hash 4C987B7C + sample 53: + time = 1696000 + flags = 1 + data = length 4000, hash 4C987B7C +tracksEnded = true diff --git a/testdata/src/test/assets/mp4/sample_eac3joc.mp4 b/testdata/src/test/assets/mp4/sample_eac3joc.mp4 new file mode 100644 index 0000000000..cd1fdccdb8 Binary files /dev/null and b/testdata/src/test/assets/mp4/sample_eac3joc.mp4 differ diff --git a/testdata/src/test/assets/mp4/sample_eac3joc.mp4.0.dump b/testdata/src/test/assets/mp4/sample_eac3joc.mp4.0.dump new file mode 100644 index 0000000000..ecc28b7208 --- /dev/null +++ b/testdata/src/test/assets/mp4/sample_eac3joc.mp4.0.dump @@ -0,0 +1,275 @@ +seekMap: + isSeekable = true + duration = 2048000 + getPosition(0) = [[timeUs=0, position=956]] + getPosition(1) = [[timeUs=1, position=956]] + getPosition(1024000) = [[timeUs=1024000, position=82876]] + getPosition(2048000) = [[timeUs=2048000, position=162236]] +numberOfTracks = 1 +track 0: + total output bytes = 163840 + sample count = 64 + format 0: + id = 1 + sampleMimeType = audio/eac3-joc + maxInputSize = 2590 + channelCount = 6 + sampleRate = 48000 + language = und + sample 0: + time = 0 + flags = 1 + data = length 2560, hash 882594AD + sample 1: + time = 32000 + flags = 1 + data = length 2560, hash 41EC8B22 + sample 2: + time = 64000 + flags = 1 + data = length 2560, hash 67E6EFD4 + sample 3: + time = 96000 + flags = 1 + data = length 2560, hash A7E66AFD + sample 4: + time = 128000 + flags = 1 + data = length 2560, hash 3924116 + sample 5: + time = 160000 + flags = 1 + data = length 2560, hash 64DCE40B + sample 6: + time = 192000 + flags = 1 + data = length 2560, hash F2E0DA64 + sample 7: + time = 224000 + flags = 1 + data = length 2560, hash C156258B + sample 8: + time = 256000 + flags = 1 + data = length 2560, hash D8DBDCDE + sample 9: + time = 288000 + flags = 1 + data = length 2560, hash C11B2F25 + sample 10: + time = 320000 + flags = 1 + data = length 2560, hash B3C5612 + sample 11: + time = 352000 + flags = 1 + data = length 2560, hash A94B15D0 + sample 12: + time = 384000 + flags = 1 + data = length 2560, hash 12E4E306 + sample 13: + time = 416000 + flags = 1 + data = length 2560, hash 11CB959F + sample 14: + time = 448000 + flags = 1 + data = length 2560, hash B6433844 + sample 15: + time = 480000 + flags = 1 + data = length 2560, hash EA6DEB89 + sample 16: + time = 512000 + flags = 1 + data = length 2560, hash 6D65CBD9 + sample 17: + time = 544000 + flags = 1 + data = length 2560, hash A5D635C5 + sample 18: + time = 576000 + flags = 1 + data = length 2560, hash 992E36AB + sample 19: + time = 608000 + flags = 1 + data = length 2560, hash 1EC4E5AF + sample 20: + time = 640000 + flags = 1 + data = length 2560, hash DCFEB7D2 + sample 21: + time = 672000 + flags = 1 + data = length 2560, hash 45EFC639 + sample 22: + time = 704000 + flags = 1 + data = length 2560, hash F598673 + sample 23: + time = 736000 + flags = 1 + data = length 2560, hash 89E4E5EC + sample 24: + time = 768000 + flags = 1 + data = length 2560, hash FBE2532B + sample 25: + time = 800000 + flags = 1 + data = length 2560, hash 9CE5F83B + sample 26: + time = 832000 + flags = 1 + data = length 2560, hash 6ED49E2C + sample 27: + time = 864000 + flags = 1 + data = length 2560, hash BC52F8F3 + sample 28: + time = 896000 + flags = 1 + data = length 2560, hash 759203E2 + sample 29: + time = 928000 + flags = 1 + data = length 2560, hash D5D31AE9 + sample 30: + time = 960000 + flags = 1 + data = length 2560, hash 640A24ED + sample 31: + time = 992000 + flags = 1 + data = length 2560, hash 19B52B8B + sample 32: + time = 1024000 + flags = 1 + data = length 2560, hash 5DA977C3 + sample 33: + time = 1056000 + flags = 1 + data = length 2560, hash 982879DD + sample 34: + time = 1088000 + flags = 1 + data = length 2560, hash A7656B9C + sample 35: + time = 1120000 + flags = 1 + data = length 2560, hash 445CCC67 + sample 36: + time = 1152000 + flags = 1 + data = length 2560, hash ACD5CB5C + sample 37: + time = 1184000 + flags = 1 + data = length 2560, hash 175BBF26 + sample 38: + time = 1216000 + flags = 1 + data = length 2560, hash DBCBEB0 + sample 39: + time = 1248000 + flags = 1 + data = length 2560, hash DA39D991 + sample 40: + time = 1280000 + flags = 1 + data = length 2560, hash F08CC8E2 + sample 41: + time = 1312000 + flags = 1 + data = length 2560, hash 6B0842D7 + sample 42: + time = 1344000 + flags = 1 + data = length 2560, hash 9FE87594 + sample 43: + time = 1376000 + flags = 1 + data = length 2560, hash 8E62CE19 + sample 44: + time = 1408000 + flags = 1 + data = length 2560, hash 5FDC4084 + sample 45: + time = 1440000 + flags = 1 + data = length 2560, hash C32DAEE1 + sample 46: + time = 1472000 + flags = 1 + data = length 2560, hash BBEFB568 + sample 47: + time = 1504000 + flags = 1 + data = length 2560, hash 20504279 + sample 48: + time = 1536000 + flags = 1 + data = length 2560, hash 3B8192D2 + sample 49: + time = 1568000 + flags = 1 + data = length 2560, hash 4206B48 + sample 50: + time = 1600000 + flags = 1 + data = length 2560, hash B195AB53 + sample 51: + time = 1632000 + flags = 1 + data = length 2560, hash 3AA8E25F + sample 52: + time = 1664000 + flags = 1 + data = length 2560, hash BC227D7B + sample 53: + time = 1696000 + flags = 1 + data = length 2560, hash 6A34F7EA + sample 54: + time = 1728000 + flags = 1 + data = length 2560, hash F1E731C4 + sample 55: + time = 1760000 + flags = 1 + data = length 2560, hash 9CC406 + sample 56: + time = 1792000 + flags = 1 + data = length 2560, hash A1532233 + sample 57: + time = 1824000 + flags = 1 + data = length 2560, hash 98E49039 + sample 58: + time = 1856000 + flags = 1 + data = length 2560, hash 3F8B6DC0 + sample 59: + time = 1888000 + flags = 1 + data = length 2560, hash 4E7BF79F + sample 60: + time = 1920000 + flags = 1 + data = length 2560, hash 6DD6F2D7 + sample 61: + time = 1952000 + flags = 1 + data = length 2560, hash A05C0EC2 + sample 62: + time = 1984000 + flags = 1 + data = length 2560, hash 10C62F30 + sample 63: + time = 2016000 + flags = 536870913 + data = length 2560, hash EE4F848A +tracksEnded = true diff --git a/testdata/src/test/assets/mp4/sample_eac3joc.mp4.1.dump b/testdata/src/test/assets/mp4/sample_eac3joc.mp4.1.dump new file mode 100644 index 0000000000..d9ed0c417d --- /dev/null +++ b/testdata/src/test/assets/mp4/sample_eac3joc.mp4.1.dump @@ -0,0 +1,191 @@ +seekMap: + isSeekable = true + duration = 2048000 + getPosition(0) = [[timeUs=0, position=956]] + getPosition(1) = [[timeUs=1, position=956]] + getPosition(1024000) = [[timeUs=1024000, position=82876]] + getPosition(2048000) = [[timeUs=2048000, position=162236]] +numberOfTracks = 1 +track 0: + total output bytes = 110080 + sample count = 43 + format 0: + id = 1 + sampleMimeType = audio/eac3-joc + maxInputSize = 2590 + channelCount = 6 + sampleRate = 48000 + language = und + sample 0: + time = 672000 + flags = 1 + data = length 2560, hash 45EFC639 + sample 1: + time = 704000 + flags = 1 + data = length 2560, hash F598673 + sample 2: + time = 736000 + flags = 1 + data = length 2560, hash 89E4E5EC + sample 3: + time = 768000 + flags = 1 + data = length 2560, hash FBE2532B + sample 4: + time = 800000 + flags = 1 + data = length 2560, hash 9CE5F83B + sample 5: + time = 832000 + flags = 1 + data = length 2560, hash 6ED49E2C + sample 6: + time = 864000 + flags = 1 + data = length 2560, hash BC52F8F3 + sample 7: + time = 896000 + flags = 1 + data = length 2560, hash 759203E2 + sample 8: + time = 928000 + flags = 1 + data = length 2560, hash D5D31AE9 + sample 9: + time = 960000 + flags = 1 + data = length 2560, hash 640A24ED + sample 10: + time = 992000 + flags = 1 + data = length 2560, hash 19B52B8B + sample 11: + time = 1024000 + flags = 1 + data = length 2560, hash 5DA977C3 + sample 12: + time = 1056000 + flags = 1 + data = length 2560, hash 982879DD + sample 13: + time = 1088000 + flags = 1 + data = length 2560, hash A7656B9C + sample 14: + time = 1120000 + flags = 1 + data = length 2560, hash 445CCC67 + sample 15: + time = 1152000 + flags = 1 + data = length 2560, hash ACD5CB5C + sample 16: + time = 1184000 + flags = 1 + data = length 2560, hash 175BBF26 + sample 17: + time = 1216000 + flags = 1 + data = length 2560, hash DBCBEB0 + sample 18: + time = 1248000 + flags = 1 + data = length 2560, hash DA39D991 + sample 19: + time = 1280000 + flags = 1 + data = length 2560, hash F08CC8E2 + sample 20: + time = 1312000 + flags = 1 + data = length 2560, hash 6B0842D7 + sample 21: + time = 1344000 + flags = 1 + data = length 2560, hash 9FE87594 + sample 22: + time = 1376000 + flags = 1 + data = length 2560, hash 8E62CE19 + sample 23: + time = 1408000 + flags = 1 + data = length 2560, hash 5FDC4084 + sample 24: + time = 1440000 + flags = 1 + data = length 2560, hash C32DAEE1 + sample 25: + time = 1472000 + flags = 1 + data = length 2560, hash BBEFB568 + sample 26: + time = 1504000 + flags = 1 + data = length 2560, hash 20504279 + sample 27: + time = 1536000 + flags = 1 + data = length 2560, hash 3B8192D2 + sample 28: + time = 1568000 + flags = 1 + data = length 2560, hash 4206B48 + sample 29: + time = 1600000 + flags = 1 + data = length 2560, hash B195AB53 + sample 30: + time = 1632000 + flags = 1 + data = length 2560, hash 3AA8E25F + sample 31: + time = 1664000 + flags = 1 + data = length 2560, hash BC227D7B + sample 32: + time = 1696000 + flags = 1 + data = length 2560, hash 6A34F7EA + sample 33: + time = 1728000 + flags = 1 + data = length 2560, hash F1E731C4 + sample 34: + time = 1760000 + flags = 1 + data = length 2560, hash 9CC406 + sample 35: + time = 1792000 + flags = 1 + data = length 2560, hash A1532233 + sample 36: + time = 1824000 + flags = 1 + data = length 2560, hash 98E49039 + sample 37: + time = 1856000 + flags = 1 + data = length 2560, hash 3F8B6DC0 + sample 38: + time = 1888000 + flags = 1 + data = length 2560, hash 4E7BF79F + sample 39: + time = 1920000 + flags = 1 + data = length 2560, hash 6DD6F2D7 + sample 40: + time = 1952000 + flags = 1 + data = length 2560, hash A05C0EC2 + sample 41: + time = 1984000 + flags = 1 + data = length 2560, hash 10C62F30 + sample 42: + time = 2016000 + flags = 536870913 + data = length 2560, hash EE4F848A +tracksEnded = true diff --git a/testdata/src/test/assets/mp4/sample_eac3joc.mp4.2.dump b/testdata/src/test/assets/mp4/sample_eac3joc.mp4.2.dump new file mode 100644 index 0000000000..741d5199ea --- /dev/null +++ b/testdata/src/test/assets/mp4/sample_eac3joc.mp4.2.dump @@ -0,0 +1,107 @@ +seekMap: + isSeekable = true + duration = 2048000 + getPosition(0) = [[timeUs=0, position=956]] + getPosition(1) = [[timeUs=1, position=956]] + getPosition(1024000) = [[timeUs=1024000, position=82876]] + getPosition(2048000) = [[timeUs=2048000, position=162236]] +numberOfTracks = 1 +track 0: + total output bytes = 56320 + sample count = 22 + format 0: + id = 1 + sampleMimeType = audio/eac3-joc + maxInputSize = 2590 + channelCount = 6 + sampleRate = 48000 + language = und + sample 0: + time = 1344000 + flags = 1 + data = length 2560, hash 9FE87594 + sample 1: + time = 1376000 + flags = 1 + data = length 2560, hash 8E62CE19 + sample 2: + time = 1408000 + flags = 1 + data = length 2560, hash 5FDC4084 + sample 3: + time = 1440000 + flags = 1 + data = length 2560, hash C32DAEE1 + sample 4: + time = 1472000 + flags = 1 + data = length 2560, hash BBEFB568 + sample 5: + time = 1504000 + flags = 1 + data = length 2560, hash 20504279 + sample 6: + time = 1536000 + flags = 1 + data = length 2560, hash 3B8192D2 + sample 7: + time = 1568000 + flags = 1 + data = length 2560, hash 4206B48 + sample 8: + time = 1600000 + flags = 1 + data = length 2560, hash B195AB53 + sample 9: + time = 1632000 + flags = 1 + data = length 2560, hash 3AA8E25F + sample 10: + time = 1664000 + flags = 1 + data = length 2560, hash BC227D7B + sample 11: + time = 1696000 + flags = 1 + data = length 2560, hash 6A34F7EA + sample 12: + time = 1728000 + flags = 1 + data = length 2560, hash F1E731C4 + sample 13: + time = 1760000 + flags = 1 + data = length 2560, hash 9CC406 + sample 14: + time = 1792000 + flags = 1 + data = length 2560, hash A1532233 + sample 15: + time = 1824000 + flags = 1 + data = length 2560, hash 98E49039 + sample 16: + time = 1856000 + flags = 1 + data = length 2560, hash 3F8B6DC0 + sample 17: + time = 1888000 + flags = 1 + data = length 2560, hash 4E7BF79F + sample 18: + time = 1920000 + flags = 1 + data = length 2560, hash 6DD6F2D7 + sample 19: + time = 1952000 + flags = 1 + data = length 2560, hash A05C0EC2 + sample 20: + time = 1984000 + flags = 1 + data = length 2560, hash 10C62F30 + sample 21: + time = 2016000 + flags = 536870913 + data = length 2560, hash EE4F848A +tracksEnded = true diff --git a/testdata/src/test/assets/mp4/sample_eac3joc.mp4.3.dump b/testdata/src/test/assets/mp4/sample_eac3joc.mp4.3.dump new file mode 100644 index 0000000000..98fe8c793d --- /dev/null +++ b/testdata/src/test/assets/mp4/sample_eac3joc.mp4.3.dump @@ -0,0 +1,23 @@ +seekMap: + isSeekable = true + duration = 2048000 + getPosition(0) = [[timeUs=0, position=956]] + getPosition(1) = [[timeUs=1, position=956]] + getPosition(1024000) = [[timeUs=1024000, position=82876]] + getPosition(2048000) = [[timeUs=2048000, position=162236]] +numberOfTracks = 1 +track 0: + total output bytes = 2560 + sample count = 1 + format 0: + id = 1 + sampleMimeType = audio/eac3-joc + maxInputSize = 2590 + channelCount = 6 + sampleRate = 48000 + language = und + sample 0: + time = 2016000 + flags = 536870913 + data = length 2560, hash EE4F848A +tracksEnded = true diff --git a/testdata/src/test/assets/mp4/sample_eac3joc.mp4.unknown_length.dump b/testdata/src/test/assets/mp4/sample_eac3joc.mp4.unknown_length.dump new file mode 100644 index 0000000000..ecc28b7208 --- /dev/null +++ b/testdata/src/test/assets/mp4/sample_eac3joc.mp4.unknown_length.dump @@ -0,0 +1,275 @@ +seekMap: + isSeekable = true + duration = 2048000 + getPosition(0) = [[timeUs=0, position=956]] + getPosition(1) = [[timeUs=1, position=956]] + getPosition(1024000) = [[timeUs=1024000, position=82876]] + getPosition(2048000) = [[timeUs=2048000, position=162236]] +numberOfTracks = 1 +track 0: + total output bytes = 163840 + sample count = 64 + format 0: + id = 1 + sampleMimeType = audio/eac3-joc + maxInputSize = 2590 + channelCount = 6 + sampleRate = 48000 + language = und + sample 0: + time = 0 + flags = 1 + data = length 2560, hash 882594AD + sample 1: + time = 32000 + flags = 1 + data = length 2560, hash 41EC8B22 + sample 2: + time = 64000 + flags = 1 + data = length 2560, hash 67E6EFD4 + sample 3: + time = 96000 + flags = 1 + data = length 2560, hash A7E66AFD + sample 4: + time = 128000 + flags = 1 + data = length 2560, hash 3924116 + sample 5: + time = 160000 + flags = 1 + data = length 2560, hash 64DCE40B + sample 6: + time = 192000 + flags = 1 + data = length 2560, hash F2E0DA64 + sample 7: + time = 224000 + flags = 1 + data = length 2560, hash C156258B + sample 8: + time = 256000 + flags = 1 + data = length 2560, hash D8DBDCDE + sample 9: + time = 288000 + flags = 1 + data = length 2560, hash C11B2F25 + sample 10: + time = 320000 + flags = 1 + data = length 2560, hash B3C5612 + sample 11: + time = 352000 + flags = 1 + data = length 2560, hash A94B15D0 + sample 12: + time = 384000 + flags = 1 + data = length 2560, hash 12E4E306 + sample 13: + time = 416000 + flags = 1 + data = length 2560, hash 11CB959F + sample 14: + time = 448000 + flags = 1 + data = length 2560, hash B6433844 + sample 15: + time = 480000 + flags = 1 + data = length 2560, hash EA6DEB89 + sample 16: + time = 512000 + flags = 1 + data = length 2560, hash 6D65CBD9 + sample 17: + time = 544000 + flags = 1 + data = length 2560, hash A5D635C5 + sample 18: + time = 576000 + flags = 1 + data = length 2560, hash 992E36AB + sample 19: + time = 608000 + flags = 1 + data = length 2560, hash 1EC4E5AF + sample 20: + time = 640000 + flags = 1 + data = length 2560, hash DCFEB7D2 + sample 21: + time = 672000 + flags = 1 + data = length 2560, hash 45EFC639 + sample 22: + time = 704000 + flags = 1 + data = length 2560, hash F598673 + sample 23: + time = 736000 + flags = 1 + data = length 2560, hash 89E4E5EC + sample 24: + time = 768000 + flags = 1 + data = length 2560, hash FBE2532B + sample 25: + time = 800000 + flags = 1 + data = length 2560, hash 9CE5F83B + sample 26: + time = 832000 + flags = 1 + data = length 2560, hash 6ED49E2C + sample 27: + time = 864000 + flags = 1 + data = length 2560, hash BC52F8F3 + sample 28: + time = 896000 + flags = 1 + data = length 2560, hash 759203E2 + sample 29: + time = 928000 + flags = 1 + data = length 2560, hash D5D31AE9 + sample 30: + time = 960000 + flags = 1 + data = length 2560, hash 640A24ED + sample 31: + time = 992000 + flags = 1 + data = length 2560, hash 19B52B8B + sample 32: + time = 1024000 + flags = 1 + data = length 2560, hash 5DA977C3 + sample 33: + time = 1056000 + flags = 1 + data = length 2560, hash 982879DD + sample 34: + time = 1088000 + flags = 1 + data = length 2560, hash A7656B9C + sample 35: + time = 1120000 + flags = 1 + data = length 2560, hash 445CCC67 + sample 36: + time = 1152000 + flags = 1 + data = length 2560, hash ACD5CB5C + sample 37: + time = 1184000 + flags = 1 + data = length 2560, hash 175BBF26 + sample 38: + time = 1216000 + flags = 1 + data = length 2560, hash DBCBEB0 + sample 39: + time = 1248000 + flags = 1 + data = length 2560, hash DA39D991 + sample 40: + time = 1280000 + flags = 1 + data = length 2560, hash F08CC8E2 + sample 41: + time = 1312000 + flags = 1 + data = length 2560, hash 6B0842D7 + sample 42: + time = 1344000 + flags = 1 + data = length 2560, hash 9FE87594 + sample 43: + time = 1376000 + flags = 1 + data = length 2560, hash 8E62CE19 + sample 44: + time = 1408000 + flags = 1 + data = length 2560, hash 5FDC4084 + sample 45: + time = 1440000 + flags = 1 + data = length 2560, hash C32DAEE1 + sample 46: + time = 1472000 + flags = 1 + data = length 2560, hash BBEFB568 + sample 47: + time = 1504000 + flags = 1 + data = length 2560, hash 20504279 + sample 48: + time = 1536000 + flags = 1 + data = length 2560, hash 3B8192D2 + sample 49: + time = 1568000 + flags = 1 + data = length 2560, hash 4206B48 + sample 50: + time = 1600000 + flags = 1 + data = length 2560, hash B195AB53 + sample 51: + time = 1632000 + flags = 1 + data = length 2560, hash 3AA8E25F + sample 52: + time = 1664000 + flags = 1 + data = length 2560, hash BC227D7B + sample 53: + time = 1696000 + flags = 1 + data = length 2560, hash 6A34F7EA + sample 54: + time = 1728000 + flags = 1 + data = length 2560, hash F1E731C4 + sample 55: + time = 1760000 + flags = 1 + data = length 2560, hash 9CC406 + sample 56: + time = 1792000 + flags = 1 + data = length 2560, hash A1532233 + sample 57: + time = 1824000 + flags = 1 + data = length 2560, hash 98E49039 + sample 58: + time = 1856000 + flags = 1 + data = length 2560, hash 3F8B6DC0 + sample 59: + time = 1888000 + flags = 1 + data = length 2560, hash 4E7BF79F + sample 60: + time = 1920000 + flags = 1 + data = length 2560, hash 6DD6F2D7 + sample 61: + time = 1952000 + flags = 1 + data = length 2560, hash A05C0EC2 + sample 62: + time = 1984000 + flags = 1 + data = length 2560, hash 10C62F30 + sample 63: + time = 2016000 + flags = 536870913 + data = length 2560, hash EE4F848A +tracksEnded = true diff --git a/testdata/src/test/assets/mp4/sample_eac3joc_fragmented.mp4 b/testdata/src/test/assets/mp4/sample_eac3joc_fragmented.mp4 new file mode 100644 index 0000000000..fbe922c3c5 Binary files /dev/null and b/testdata/src/test/assets/mp4/sample_eac3joc_fragmented.mp4 differ diff --git a/testdata/src/test/assets/mp4/sample_eac3joc_fragmented.mp4.0.dump b/testdata/src/test/assets/mp4/sample_eac3joc_fragmented.mp4.0.dump new file mode 100644 index 0000000000..c5902f5d19 --- /dev/null +++ b/testdata/src/test/assets/mp4/sample_eac3joc_fragmented.mp4.0.dump @@ -0,0 +1,274 @@ +seekMap: + isSeekable = true + duration = 2048000 + getPosition(0) = [[timeUs=0, position=640]] + getPosition(1) = [[timeUs=0, position=640]] + getPosition(1024000) = [[timeUs=0, position=640]] + getPosition(2048000) = [[timeUs=0, position=640]] +numberOfTracks = 1 +track 0: + total output bytes = 163840 + sample count = 64 + format 0: + id = 1 + sampleMimeType = audio/eac3-joc + channelCount = 6 + sampleRate = 48000 + language = und + sample 0: + time = 0 + flags = 1 + data = length 2560, hash 882594AD + sample 1: + time = 32000 + flags = 1 + data = length 2560, hash 41EC8B22 + sample 2: + time = 64000 + flags = 1 + data = length 2560, hash 67E6EFD4 + sample 3: + time = 96000 + flags = 1 + data = length 2560, hash A7E66AFD + sample 4: + time = 128000 + flags = 1 + data = length 2560, hash 3924116 + sample 5: + time = 160000 + flags = 1 + data = length 2560, hash 64DCE40B + sample 6: + time = 192000 + flags = 1 + data = length 2560, hash F2E0DA64 + sample 7: + time = 224000 + flags = 1 + data = length 2560, hash C156258B + sample 8: + time = 256000 + flags = 1 + data = length 2560, hash D8DBDCDE + sample 9: + time = 288000 + flags = 1 + data = length 2560, hash C11B2F25 + sample 10: + time = 320000 + flags = 1 + data = length 2560, hash B3C5612 + sample 11: + time = 352000 + flags = 1 + data = length 2560, hash A94B15D0 + sample 12: + time = 384000 + flags = 1 + data = length 2560, hash 12E4E306 + sample 13: + time = 416000 + flags = 1 + data = length 2560, hash 11CB959F + sample 14: + time = 448000 + flags = 1 + data = length 2560, hash B6433844 + sample 15: + time = 480000 + flags = 1 + data = length 2560, hash EA6DEB89 + sample 16: + time = 512000 + flags = 1 + data = length 2560, hash 6D65CBD9 + sample 17: + time = 544000 + flags = 1 + data = length 2560, hash A5D635C5 + sample 18: + time = 576000 + flags = 1 + data = length 2560, hash 992E36AB + sample 19: + time = 608000 + flags = 1 + data = length 2560, hash 1EC4E5AF + sample 20: + time = 640000 + flags = 1 + data = length 2560, hash DCFEB7D2 + sample 21: + time = 672000 + flags = 1 + data = length 2560, hash 45EFC639 + sample 22: + time = 704000 + flags = 1 + data = length 2560, hash F598673 + sample 23: + time = 736000 + flags = 1 + data = length 2560, hash 89E4E5EC + sample 24: + time = 768000 + flags = 1 + data = length 2560, hash FBE2532B + sample 25: + time = 800000 + flags = 1 + data = length 2560, hash 9CE5F83B + sample 26: + time = 832000 + flags = 1 + data = length 2560, hash 6ED49E2C + sample 27: + time = 864000 + flags = 1 + data = length 2560, hash BC52F8F3 + sample 28: + time = 896000 + flags = 1 + data = length 2560, hash 759203E2 + sample 29: + time = 928000 + flags = 1 + data = length 2560, hash D5D31AE9 + sample 30: + time = 960000 + flags = 1 + data = length 2560, hash 640A24ED + sample 31: + time = 992000 + flags = 1 + data = length 2560, hash 19B52B8B + sample 32: + time = 1024000 + flags = 1 + data = length 2560, hash 5DA977C3 + sample 33: + time = 1056000 + flags = 1 + data = length 2560, hash 982879DD + sample 34: + time = 1088000 + flags = 1 + data = length 2560, hash A7656B9C + sample 35: + time = 1120000 + flags = 1 + data = length 2560, hash 445CCC67 + sample 36: + time = 1152000 + flags = 1 + data = length 2560, hash ACD5CB5C + sample 37: + time = 1184000 + flags = 1 + data = length 2560, hash 175BBF26 + sample 38: + time = 1216000 + flags = 1 + data = length 2560, hash DBCBEB0 + sample 39: + time = 1248000 + flags = 1 + data = length 2560, hash DA39D991 + sample 40: + time = 1280000 + flags = 1 + data = length 2560, hash F08CC8E2 + sample 41: + time = 1312000 + flags = 1 + data = length 2560, hash 6B0842D7 + sample 42: + time = 1344000 + flags = 1 + data = length 2560, hash 9FE87594 + sample 43: + time = 1376000 + flags = 1 + data = length 2560, hash 8E62CE19 + sample 44: + time = 1408000 + flags = 1 + data = length 2560, hash 5FDC4084 + sample 45: + time = 1440000 + flags = 1 + data = length 2560, hash C32DAEE1 + sample 46: + time = 1472000 + flags = 1 + data = length 2560, hash BBEFB568 + sample 47: + time = 1504000 + flags = 1 + data = length 2560, hash 20504279 + sample 48: + time = 1536000 + flags = 1 + data = length 2560, hash 3B8192D2 + sample 49: + time = 1568000 + flags = 1 + data = length 2560, hash 4206B48 + sample 50: + time = 1600000 + flags = 1 + data = length 2560, hash B195AB53 + sample 51: + time = 1632000 + flags = 1 + data = length 2560, hash 3AA8E25F + sample 52: + time = 1664000 + flags = 1 + data = length 2560, hash BC227D7B + sample 53: + time = 1696000 + flags = 1 + data = length 2560, hash 6A34F7EA + sample 54: + time = 1728000 + flags = 1 + data = length 2560, hash F1E731C4 + sample 55: + time = 1760000 + flags = 1 + data = length 2560, hash 9CC406 + sample 56: + time = 1792000 + flags = 1 + data = length 2560, hash A1532233 + sample 57: + time = 1824000 + flags = 1 + data = length 2560, hash 98E49039 + sample 58: + time = 1856000 + flags = 1 + data = length 2560, hash 3F8B6DC0 + sample 59: + time = 1888000 + flags = 1 + data = length 2560, hash 4E7BF79F + sample 60: + time = 1920000 + flags = 1 + data = length 2560, hash 6DD6F2D7 + sample 61: + time = 1952000 + flags = 1 + data = length 2560, hash A05C0EC2 + sample 62: + time = 1984000 + flags = 1 + data = length 2560, hash 10C62F30 + sample 63: + time = 2016000 + flags = 1 + data = length 2560, hash EE4F848A +tracksEnded = true diff --git a/testdata/src/test/assets/mp4/sample_eac3joc_fragmented.mp4.1.dump b/testdata/src/test/assets/mp4/sample_eac3joc_fragmented.mp4.1.dump new file mode 100644 index 0000000000..8fa0cbf7fe --- /dev/null +++ b/testdata/src/test/assets/mp4/sample_eac3joc_fragmented.mp4.1.dump @@ -0,0 +1,190 @@ +seekMap: + isSeekable = true + duration = 2048000 + getPosition(0) = [[timeUs=0, position=640]] + getPosition(1) = [[timeUs=0, position=640]] + getPosition(1024000) = [[timeUs=0, position=640]] + getPosition(2048000) = [[timeUs=0, position=640]] +numberOfTracks = 1 +track 0: + total output bytes = 110080 + sample count = 43 + format 0: + id = 1 + sampleMimeType = audio/eac3-joc + channelCount = 6 + sampleRate = 48000 + language = und + sample 0: + time = 672000 + flags = 1 + data = length 2560, hash 45EFC639 + sample 1: + time = 704000 + flags = 1 + data = length 2560, hash F598673 + sample 2: + time = 736000 + flags = 1 + data = length 2560, hash 89E4E5EC + sample 3: + time = 768000 + flags = 1 + data = length 2560, hash FBE2532B + sample 4: + time = 800000 + flags = 1 + data = length 2560, hash 9CE5F83B + sample 5: + time = 832000 + flags = 1 + data = length 2560, hash 6ED49E2C + sample 6: + time = 864000 + flags = 1 + data = length 2560, hash BC52F8F3 + sample 7: + time = 896000 + flags = 1 + data = length 2560, hash 759203E2 + sample 8: + time = 928000 + flags = 1 + data = length 2560, hash D5D31AE9 + sample 9: + time = 960000 + flags = 1 + data = length 2560, hash 640A24ED + sample 10: + time = 992000 + flags = 1 + data = length 2560, hash 19B52B8B + sample 11: + time = 1024000 + flags = 1 + data = length 2560, hash 5DA977C3 + sample 12: + time = 1056000 + flags = 1 + data = length 2560, hash 982879DD + sample 13: + time = 1088000 + flags = 1 + data = length 2560, hash A7656B9C + sample 14: + time = 1120000 + flags = 1 + data = length 2560, hash 445CCC67 + sample 15: + time = 1152000 + flags = 1 + data = length 2560, hash ACD5CB5C + sample 16: + time = 1184000 + flags = 1 + data = length 2560, hash 175BBF26 + sample 17: + time = 1216000 + flags = 1 + data = length 2560, hash DBCBEB0 + sample 18: + time = 1248000 + flags = 1 + data = length 2560, hash DA39D991 + sample 19: + time = 1280000 + flags = 1 + data = length 2560, hash F08CC8E2 + sample 20: + time = 1312000 + flags = 1 + data = length 2560, hash 6B0842D7 + sample 21: + time = 1344000 + flags = 1 + data = length 2560, hash 9FE87594 + sample 22: + time = 1376000 + flags = 1 + data = length 2560, hash 8E62CE19 + sample 23: + time = 1408000 + flags = 1 + data = length 2560, hash 5FDC4084 + sample 24: + time = 1440000 + flags = 1 + data = length 2560, hash C32DAEE1 + sample 25: + time = 1472000 + flags = 1 + data = length 2560, hash BBEFB568 + sample 26: + time = 1504000 + flags = 1 + data = length 2560, hash 20504279 + sample 27: + time = 1536000 + flags = 1 + data = length 2560, hash 3B8192D2 + sample 28: + time = 1568000 + flags = 1 + data = length 2560, hash 4206B48 + sample 29: + time = 1600000 + flags = 1 + data = length 2560, hash B195AB53 + sample 30: + time = 1632000 + flags = 1 + data = length 2560, hash 3AA8E25F + sample 31: + time = 1664000 + flags = 1 + data = length 2560, hash BC227D7B + sample 32: + time = 1696000 + flags = 1 + data = length 2560, hash 6A34F7EA + sample 33: + time = 1728000 + flags = 1 + data = length 2560, hash F1E731C4 + sample 34: + time = 1760000 + flags = 1 + data = length 2560, hash 9CC406 + sample 35: + time = 1792000 + flags = 1 + data = length 2560, hash A1532233 + sample 36: + time = 1824000 + flags = 1 + data = length 2560, hash 98E49039 + sample 37: + time = 1856000 + flags = 1 + data = length 2560, hash 3F8B6DC0 + sample 38: + time = 1888000 + flags = 1 + data = length 2560, hash 4E7BF79F + sample 39: + time = 1920000 + flags = 1 + data = length 2560, hash 6DD6F2D7 + sample 40: + time = 1952000 + flags = 1 + data = length 2560, hash A05C0EC2 + sample 41: + time = 1984000 + flags = 1 + data = length 2560, hash 10C62F30 + sample 42: + time = 2016000 + flags = 1 + data = length 2560, hash EE4F848A +tracksEnded = true diff --git a/testdata/src/test/assets/mp4/sample_eac3joc_fragmented.mp4.2.dump b/testdata/src/test/assets/mp4/sample_eac3joc_fragmented.mp4.2.dump new file mode 100644 index 0000000000..603ca0de80 --- /dev/null +++ b/testdata/src/test/assets/mp4/sample_eac3joc_fragmented.mp4.2.dump @@ -0,0 +1,106 @@ +seekMap: + isSeekable = true + duration = 2048000 + getPosition(0) = [[timeUs=0, position=640]] + getPosition(1) = [[timeUs=0, position=640]] + getPosition(1024000) = [[timeUs=0, position=640]] + getPosition(2048000) = [[timeUs=0, position=640]] +numberOfTracks = 1 +track 0: + total output bytes = 56320 + sample count = 22 + format 0: + id = 1 + sampleMimeType = audio/eac3-joc + channelCount = 6 + sampleRate = 48000 + language = und + sample 0: + time = 1344000 + flags = 1 + data = length 2560, hash 9FE87594 + sample 1: + time = 1376000 + flags = 1 + data = length 2560, hash 8E62CE19 + sample 2: + time = 1408000 + flags = 1 + data = length 2560, hash 5FDC4084 + sample 3: + time = 1440000 + flags = 1 + data = length 2560, hash C32DAEE1 + sample 4: + time = 1472000 + flags = 1 + data = length 2560, hash BBEFB568 + sample 5: + time = 1504000 + flags = 1 + data = length 2560, hash 20504279 + sample 6: + time = 1536000 + flags = 1 + data = length 2560, hash 3B8192D2 + sample 7: + time = 1568000 + flags = 1 + data = length 2560, hash 4206B48 + sample 8: + time = 1600000 + flags = 1 + data = length 2560, hash B195AB53 + sample 9: + time = 1632000 + flags = 1 + data = length 2560, hash 3AA8E25F + sample 10: + time = 1664000 + flags = 1 + data = length 2560, hash BC227D7B + sample 11: + time = 1696000 + flags = 1 + data = length 2560, hash 6A34F7EA + sample 12: + time = 1728000 + flags = 1 + data = length 2560, hash F1E731C4 + sample 13: + time = 1760000 + flags = 1 + data = length 2560, hash 9CC406 + sample 14: + time = 1792000 + flags = 1 + data = length 2560, hash A1532233 + sample 15: + time = 1824000 + flags = 1 + data = length 2560, hash 98E49039 + sample 16: + time = 1856000 + flags = 1 + data = length 2560, hash 3F8B6DC0 + sample 17: + time = 1888000 + flags = 1 + data = length 2560, hash 4E7BF79F + sample 18: + time = 1920000 + flags = 1 + data = length 2560, hash 6DD6F2D7 + sample 19: + time = 1952000 + flags = 1 + data = length 2560, hash A05C0EC2 + sample 20: + time = 1984000 + flags = 1 + data = length 2560, hash 10C62F30 + sample 21: + time = 2016000 + flags = 1 + data = length 2560, hash EE4F848A +tracksEnded = true diff --git a/testdata/src/test/assets/mp4/sample_eac3joc_fragmented.mp4.3.dump b/testdata/src/test/assets/mp4/sample_eac3joc_fragmented.mp4.3.dump new file mode 100644 index 0000000000..cd42dac917 --- /dev/null +++ b/testdata/src/test/assets/mp4/sample_eac3joc_fragmented.mp4.3.dump @@ -0,0 +1,22 @@ +seekMap: + isSeekable = true + duration = 2048000 + getPosition(0) = [[timeUs=0, position=640]] + getPosition(1) = [[timeUs=0, position=640]] + getPosition(1024000) = [[timeUs=0, position=640]] + getPosition(2048000) = [[timeUs=0, position=640]] +numberOfTracks = 1 +track 0: + total output bytes = 2560 + sample count = 1 + format 0: + id = 1 + sampleMimeType = audio/eac3-joc + channelCount = 6 + sampleRate = 48000 + language = und + sample 0: + time = 2016000 + flags = 1 + data = length 2560, hash EE4F848A +tracksEnded = true diff --git a/testdata/src/test/assets/mp4/sample_eac3joc_fragmented.mp4.unknown_length.dump b/testdata/src/test/assets/mp4/sample_eac3joc_fragmented.mp4.unknown_length.dump new file mode 100644 index 0000000000..c5902f5d19 --- /dev/null +++ b/testdata/src/test/assets/mp4/sample_eac3joc_fragmented.mp4.unknown_length.dump @@ -0,0 +1,274 @@ +seekMap: + isSeekable = true + duration = 2048000 + getPosition(0) = [[timeUs=0, position=640]] + getPosition(1) = [[timeUs=0, position=640]] + getPosition(1024000) = [[timeUs=0, position=640]] + getPosition(2048000) = [[timeUs=0, position=640]] +numberOfTracks = 1 +track 0: + total output bytes = 163840 + sample count = 64 + format 0: + id = 1 + sampleMimeType = audio/eac3-joc + channelCount = 6 + sampleRate = 48000 + language = und + sample 0: + time = 0 + flags = 1 + data = length 2560, hash 882594AD + sample 1: + time = 32000 + flags = 1 + data = length 2560, hash 41EC8B22 + sample 2: + time = 64000 + flags = 1 + data = length 2560, hash 67E6EFD4 + sample 3: + time = 96000 + flags = 1 + data = length 2560, hash A7E66AFD + sample 4: + time = 128000 + flags = 1 + data = length 2560, hash 3924116 + sample 5: + time = 160000 + flags = 1 + data = length 2560, hash 64DCE40B + sample 6: + time = 192000 + flags = 1 + data = length 2560, hash F2E0DA64 + sample 7: + time = 224000 + flags = 1 + data = length 2560, hash C156258B + sample 8: + time = 256000 + flags = 1 + data = length 2560, hash D8DBDCDE + sample 9: + time = 288000 + flags = 1 + data = length 2560, hash C11B2F25 + sample 10: + time = 320000 + flags = 1 + data = length 2560, hash B3C5612 + sample 11: + time = 352000 + flags = 1 + data = length 2560, hash A94B15D0 + sample 12: + time = 384000 + flags = 1 + data = length 2560, hash 12E4E306 + sample 13: + time = 416000 + flags = 1 + data = length 2560, hash 11CB959F + sample 14: + time = 448000 + flags = 1 + data = length 2560, hash B6433844 + sample 15: + time = 480000 + flags = 1 + data = length 2560, hash EA6DEB89 + sample 16: + time = 512000 + flags = 1 + data = length 2560, hash 6D65CBD9 + sample 17: + time = 544000 + flags = 1 + data = length 2560, hash A5D635C5 + sample 18: + time = 576000 + flags = 1 + data = length 2560, hash 992E36AB + sample 19: + time = 608000 + flags = 1 + data = length 2560, hash 1EC4E5AF + sample 20: + time = 640000 + flags = 1 + data = length 2560, hash DCFEB7D2 + sample 21: + time = 672000 + flags = 1 + data = length 2560, hash 45EFC639 + sample 22: + time = 704000 + flags = 1 + data = length 2560, hash F598673 + sample 23: + time = 736000 + flags = 1 + data = length 2560, hash 89E4E5EC + sample 24: + time = 768000 + flags = 1 + data = length 2560, hash FBE2532B + sample 25: + time = 800000 + flags = 1 + data = length 2560, hash 9CE5F83B + sample 26: + time = 832000 + flags = 1 + data = length 2560, hash 6ED49E2C + sample 27: + time = 864000 + flags = 1 + data = length 2560, hash BC52F8F3 + sample 28: + time = 896000 + flags = 1 + data = length 2560, hash 759203E2 + sample 29: + time = 928000 + flags = 1 + data = length 2560, hash D5D31AE9 + sample 30: + time = 960000 + flags = 1 + data = length 2560, hash 640A24ED + sample 31: + time = 992000 + flags = 1 + data = length 2560, hash 19B52B8B + sample 32: + time = 1024000 + flags = 1 + data = length 2560, hash 5DA977C3 + sample 33: + time = 1056000 + flags = 1 + data = length 2560, hash 982879DD + sample 34: + time = 1088000 + flags = 1 + data = length 2560, hash A7656B9C + sample 35: + time = 1120000 + flags = 1 + data = length 2560, hash 445CCC67 + sample 36: + time = 1152000 + flags = 1 + data = length 2560, hash ACD5CB5C + sample 37: + time = 1184000 + flags = 1 + data = length 2560, hash 175BBF26 + sample 38: + time = 1216000 + flags = 1 + data = length 2560, hash DBCBEB0 + sample 39: + time = 1248000 + flags = 1 + data = length 2560, hash DA39D991 + sample 40: + time = 1280000 + flags = 1 + data = length 2560, hash F08CC8E2 + sample 41: + time = 1312000 + flags = 1 + data = length 2560, hash 6B0842D7 + sample 42: + time = 1344000 + flags = 1 + data = length 2560, hash 9FE87594 + sample 43: + time = 1376000 + flags = 1 + data = length 2560, hash 8E62CE19 + sample 44: + time = 1408000 + flags = 1 + data = length 2560, hash 5FDC4084 + sample 45: + time = 1440000 + flags = 1 + data = length 2560, hash C32DAEE1 + sample 46: + time = 1472000 + flags = 1 + data = length 2560, hash BBEFB568 + sample 47: + time = 1504000 + flags = 1 + data = length 2560, hash 20504279 + sample 48: + time = 1536000 + flags = 1 + data = length 2560, hash 3B8192D2 + sample 49: + time = 1568000 + flags = 1 + data = length 2560, hash 4206B48 + sample 50: + time = 1600000 + flags = 1 + data = length 2560, hash B195AB53 + sample 51: + time = 1632000 + flags = 1 + data = length 2560, hash 3AA8E25F + sample 52: + time = 1664000 + flags = 1 + data = length 2560, hash BC227D7B + sample 53: + time = 1696000 + flags = 1 + data = length 2560, hash 6A34F7EA + sample 54: + time = 1728000 + flags = 1 + data = length 2560, hash F1E731C4 + sample 55: + time = 1760000 + flags = 1 + data = length 2560, hash 9CC406 + sample 56: + time = 1792000 + flags = 1 + data = length 2560, hash A1532233 + sample 57: + time = 1824000 + flags = 1 + data = length 2560, hash 98E49039 + sample 58: + time = 1856000 + flags = 1 + data = length 2560, hash 3F8B6DC0 + sample 59: + time = 1888000 + flags = 1 + data = length 2560, hash 4E7BF79F + sample 60: + time = 1920000 + flags = 1 + data = length 2560, hash 6DD6F2D7 + sample 61: + time = 1952000 + flags = 1 + data = length 2560, hash A05C0EC2 + sample 62: + time = 1984000 + flags = 1 + data = length 2560, hash 10C62F30 + sample 63: + time = 2016000 + flags = 1 + data = length 2560, hash EE4F848A +tracksEnded = true diff --git a/library/core/src/test/assets/mp4/sample_fragmented.mp4 b/testdata/src/test/assets/mp4/sample_fragmented.mp4 similarity index 100% rename from library/core/src/test/assets/mp4/sample_fragmented.mp4 rename to testdata/src/test/assets/mp4/sample_fragmented.mp4 diff --git a/library/core/src/test/assets/mp4/sample_fragmented.mp4.0.dump b/testdata/src/test/assets/mp4/sample_fragmented.mp4.0.dump similarity index 72% rename from library/core/src/test/assets/mp4/sample_fragmented.mp4.0.dump rename to testdata/src/test/assets/mp4/sample_fragmented.mp4.0.dump index faa8a015ca..2a5848f5a4 100644 --- a/library/core/src/test/assets/mp4/sample_fragmented.mp4.0.dump +++ b/testdata/src/test/assets/mp4/sample_fragmented.mp4.0.dump @@ -4,358 +4,330 @@ seekMap: getPosition(0) = [[timeUs=0, position=1828]] numberOfTracks = 2 track 0: - format: - bitrate = -1 + total output bytes = 85933 + sample count = 30 + format 0: id = 1 - containerMimeType = null sampleMimeType = video/avc - maxInputSize = -1 width = 1080 height = 720 - 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 29, hash 4746B5D9 data = length 10, hash 7A0D0F2B - total output bytes = 85933 - sample count = 30 sample 0: - time = 66000 + time = 66733 flags = 1 data = length 38070, hash B58E1AEE sample 1: - time = 199000 + time = 200199 flags = 0 data = length 8340, hash 8AC449FF sample 2: - time = 132000 + time = 133466 flags = 0 data = length 1295, hash C0DA5090 sample 3: - time = 100000 + time = 100100 flags = 0 data = length 469, hash D6E0A200 sample 4: - time = 166000 + time = 166832 flags = 0 data = length 564, hash E5F56C5B sample 5: - time = 332000 + time = 333666 flags = 0 data = length 6075, hash 8756E49E sample 6: - time = 266000 + time = 266933 flags = 0 data = length 847, hash DCC2B618 sample 7: - time = 233000 + time = 233566 flags = 0 data = length 455, hash B9CCE047 sample 8: - time = 299000 + time = 300299 flags = 0 data = length 467, hash 69806D94 sample 9: - time = 466000 + time = 467133 flags = 0 data = length 4549, hash 3944F501 sample 10: - time = 399000 + time = 400399 flags = 0 data = length 1087, hash 491BF106 sample 11: - time = 367000 + time = 367033 flags = 0 data = length 380, hash 5FED016A sample 12: - time = 433000 + time = 433766 flags = 0 data = length 455, hash 8A0610 sample 13: - time = 599000 + time = 600599 flags = 0 data = length 5190, hash B9031D8 sample 14: - time = 533000 + time = 533866 flags = 0 data = length 1071, hash 684E7DC8 sample 15: - time = 500000 + time = 500500 flags = 0 data = length 653, hash 8494F326 sample 16: - time = 566000 + time = 567232 flags = 0 data = length 485, hash 2CCC85F4 sample 17: - time = 733000 + time = 734066 flags = 0 data = length 4884, hash D16B6A96 sample 18: - time = 666000 + time = 667333 flags = 0 data = length 997, hash 164FF210 sample 19: - time = 633000 + time = 633966 flags = 0 data = length 640, hash F664125B sample 20: - time = 700000 + time = 700699 flags = 0 data = length 491, hash B5930C7C sample 21: - time = 866000 + time = 867533 flags = 0 data = length 2989, hash 92CF4FCF sample 22: - time = 800000 + time = 800799 flags = 0 data = length 838, hash 294A3451 sample 23: - time = 767000 + time = 767433 flags = 0 data = length 544, hash FCCE2DE6 sample 24: - time = 833000 + time = 834166 flags = 0 data = length 329, hash A654FFA1 sample 25: - time = 1000000 + time = 1000999 flags = 0 data = length 1517, hash 5F7EBF8B sample 26: - time = 933000 + time = 934266 flags = 0 data = length 803, hash 7A5C4C1D sample 27: - time = 900000 + time = 900900 flags = 0 data = length 415, hash B31BBC3B sample 28: - time = 967000 + time = 967632 flags = 0 data = length 415, hash 850DFEA3 sample 29: - time = 1033000 + time = 1034366 flags = 0 data = length 619, hash AB5E56CA track 1: - format: - bitrate = -1 - id = 2 - 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 = und - drmInitData = - - initializationData: - data = length 5, hash 2B7623A total output bytes = 18257 sample count = 46 + format 0: + id = 2 + sampleMimeType = audio/mp4a-latm + codecs = mp4a.40.2 + channelCount = 1 + sampleRate = 44100 + language = und + initializationData: + data = length 5, hash 2B7623A sample 0: time = 0 flags = 1 data = length 18, hash 96519432 sample 1: - time = 23000 + time = 23219 flags = 1 data = length 4, hash EE9DF sample 2: - time = 46000 + time = 46439 flags = 1 data = length 4, hash EEDBF sample 3: - time = 69000 + time = 69659 flags = 1 data = length 157, hash E2F078F4 sample 4: - time = 92000 + time = 92879 flags = 1 data = length 371, hash B9471F94 sample 5: - time = 116000 + time = 116099 flags = 1 data = length 373, hash 2AB265CB sample 6: - time = 139000 + time = 139319 flags = 1 data = length 402, hash 1295477C sample 7: - time = 162000 + time = 162539 flags = 1 data = length 455, hash 2D8146C8 sample 8: - time = 185000 + time = 185759 flags = 1 data = length 434, hash F2C5D287 sample 9: - time = 208000 + time = 208979 flags = 1 data = length 450, hash 84143FCD sample 10: - time = 232000 + time = 232199 flags = 1 data = length 429, hash EF769D50 sample 11: - time = 255000 + time = 255419 flags = 1 data = length 450, hash EC3DE692 sample 12: - time = 278000 + time = 278639 flags = 1 data = length 447, hash 3E519E13 sample 13: - time = 301000 + time = 301859 flags = 1 data = length 457, hash 1E4F23A0 sample 14: - time = 325000 + time = 325079 flags = 1 data = length 447, hash A439EA97 sample 15: - time = 348000 + time = 348299 flags = 1 data = length 456, hash 1E9034C6 sample 16: - time = 371000 + time = 371519 flags = 1 data = length 398, hash 99DB7345 sample 17: - time = 394000 + time = 394739 flags = 1 data = length 474, hash 3F05F10A sample 18: - time = 417000 + time = 417959 flags = 1 data = length 416, hash C105EE09 sample 19: - time = 441000 + time = 441179 flags = 1 data = length 454, hash 5FDBE458 sample 20: - time = 464000 + time = 464399 flags = 1 data = length 438, hash 41A93AC3 sample 21: - time = 487000 + time = 487619 flags = 1 data = length 443, hash 10FDA652 sample 22: - time = 510000 + time = 510839 flags = 1 data = length 412, hash 1F791E25 sample 23: - time = 534000 + time = 534058 flags = 1 data = length 482, hash A6D983D sample 24: - time = 557000 + time = 557278 flags = 1 data = length 386, hash BED7392F sample 25: - time = 580000 + time = 580498 flags = 1 data = length 463, hash 5309F8C9 sample 26: - time = 603000 + time = 603718 flags = 1 data = length 394, hash 21C7321F sample 27: - time = 626000 + time = 626938 flags = 1 data = length 489, hash 71B4730D sample 28: - time = 650000 + time = 650158 flags = 1 data = length 403, hash D9C6DE89 sample 29: - time = 673000 + time = 673378 flags = 1 data = length 447, hash 9B14B73B sample 30: - time = 696000 + time = 696598 flags = 1 data = length 439, hash 4760D35B sample 31: - time = 719000 + time = 719818 flags = 1 data = length 463, hash 1601F88D sample 32: - time = 743000 + time = 743038 flags = 1 data = length 423, hash D4AE6773 sample 33: - time = 766000 + time = 766258 flags = 1 data = length 497, hash A3C674D3 sample 34: - time = 789000 + time = 789478 flags = 1 data = length 419, hash D3734A1F sample 35: - time = 812000 + time = 812698 flags = 1 data = length 474, hash DFB41F9 sample 36: - time = 835000 + time = 835918 flags = 1 data = length 413, hash 53E7CB9F sample 37: - time = 859000 + time = 859138 flags = 1 data = length 445, hash D15B0E39 sample 38: - time = 882000 + time = 882358 flags = 1 data = length 453, hash 77ED81E4 sample 39: - time = 905000 + time = 905578 flags = 1 data = length 545, hash 3321AEB9 sample 40: - time = 928000 + time = 928798 flags = 1 data = length 317, hash F557D0E sample 41: - time = 952000 + time = 952018 flags = 1 data = length 537, hash ED58CF7B sample 42: - time = 975000 + time = 975238 flags = 1 data = length 458, hash 51CDAA10 sample 43: - time = 998000 + time = 998458 flags = 1 data = length 465, hash CBA1EFD7 sample 44: - time = 1021000 + time = 1021678 flags = 1 data = length 446, hash D6735B8A sample 45: - time = 1044000 + time = 1044897 flags = 1 data = length 10, hash A453EEBE tracksEnded = true diff --git a/testdata/src/test/assets/mp4/sample_fragmented.mp4.unknown_length.dump b/testdata/src/test/assets/mp4/sample_fragmented.mp4.unknown_length.dump new file mode 100644 index 0000000000..2a5848f5a4 --- /dev/null +++ b/testdata/src/test/assets/mp4/sample_fragmented.mp4.unknown_length.dump @@ -0,0 +1,333 @@ +seekMap: + isSeekable = false + duration = UNSET TIME + getPosition(0) = [[timeUs=0, position=1828]] +numberOfTracks = 2 +track 0: + total output bytes = 85933 + sample count = 30 + format 0: + id = 1 + sampleMimeType = video/avc + width = 1080 + height = 720 + initializationData: + data = length 29, hash 4746B5D9 + data = length 10, hash 7A0D0F2B + sample 0: + time = 66733 + flags = 1 + data = length 38070, hash B58E1AEE + sample 1: + time = 200199 + flags = 0 + data = length 8340, hash 8AC449FF + sample 2: + time = 133466 + flags = 0 + data = length 1295, hash C0DA5090 + sample 3: + time = 100100 + flags = 0 + data = length 469, hash D6E0A200 + sample 4: + time = 166832 + flags = 0 + data = length 564, hash E5F56C5B + sample 5: + time = 333666 + flags = 0 + data = length 6075, hash 8756E49E + sample 6: + time = 266933 + flags = 0 + data = length 847, hash DCC2B618 + sample 7: + time = 233566 + flags = 0 + data = length 455, hash B9CCE047 + sample 8: + time = 300299 + flags = 0 + data = length 467, hash 69806D94 + sample 9: + time = 467133 + flags = 0 + data = length 4549, hash 3944F501 + sample 10: + time = 400399 + flags = 0 + data = length 1087, hash 491BF106 + sample 11: + time = 367033 + flags = 0 + data = length 380, hash 5FED016A + sample 12: + time = 433766 + flags = 0 + data = length 455, hash 8A0610 + sample 13: + time = 600599 + flags = 0 + data = length 5190, hash B9031D8 + sample 14: + time = 533866 + flags = 0 + data = length 1071, hash 684E7DC8 + sample 15: + time = 500500 + flags = 0 + data = length 653, hash 8494F326 + sample 16: + time = 567232 + flags = 0 + data = length 485, hash 2CCC85F4 + sample 17: + time = 734066 + flags = 0 + data = length 4884, hash D16B6A96 + sample 18: + time = 667333 + flags = 0 + data = length 997, hash 164FF210 + sample 19: + time = 633966 + flags = 0 + data = length 640, hash F664125B + sample 20: + time = 700699 + flags = 0 + data = length 491, hash B5930C7C + sample 21: + time = 867533 + flags = 0 + data = length 2989, hash 92CF4FCF + sample 22: + time = 800799 + flags = 0 + data = length 838, hash 294A3451 + sample 23: + time = 767433 + flags = 0 + data = length 544, hash FCCE2DE6 + sample 24: + time = 834166 + flags = 0 + data = length 329, hash A654FFA1 + sample 25: + time = 1000999 + flags = 0 + data = length 1517, hash 5F7EBF8B + sample 26: + time = 934266 + flags = 0 + data = length 803, hash 7A5C4C1D + sample 27: + time = 900900 + flags = 0 + data = length 415, hash B31BBC3B + sample 28: + time = 967632 + flags = 0 + data = length 415, hash 850DFEA3 + sample 29: + time = 1034366 + flags = 0 + data = length 619, hash AB5E56CA +track 1: + total output bytes = 18257 + sample count = 46 + format 0: + id = 2 + sampleMimeType = audio/mp4a-latm + codecs = mp4a.40.2 + channelCount = 1 + sampleRate = 44100 + language = und + initializationData: + data = length 5, hash 2B7623A + sample 0: + time = 0 + flags = 1 + data = length 18, hash 96519432 + sample 1: + time = 23219 + flags = 1 + data = length 4, hash EE9DF + sample 2: + time = 46439 + flags = 1 + data = length 4, hash EEDBF + sample 3: + time = 69659 + flags = 1 + data = length 157, hash E2F078F4 + sample 4: + time = 92879 + flags = 1 + data = length 371, hash B9471F94 + sample 5: + time = 116099 + flags = 1 + data = length 373, hash 2AB265CB + sample 6: + time = 139319 + flags = 1 + data = length 402, hash 1295477C + sample 7: + time = 162539 + flags = 1 + data = length 455, hash 2D8146C8 + sample 8: + time = 185759 + flags = 1 + data = length 434, hash F2C5D287 + sample 9: + time = 208979 + flags = 1 + data = length 450, hash 84143FCD + sample 10: + time = 232199 + flags = 1 + data = length 429, hash EF769D50 + sample 11: + time = 255419 + flags = 1 + data = length 450, hash EC3DE692 + sample 12: + time = 278639 + flags = 1 + data = length 447, hash 3E519E13 + sample 13: + time = 301859 + flags = 1 + data = length 457, hash 1E4F23A0 + sample 14: + time = 325079 + flags = 1 + data = length 447, hash A439EA97 + sample 15: + time = 348299 + flags = 1 + data = length 456, hash 1E9034C6 + sample 16: + time = 371519 + flags = 1 + data = length 398, hash 99DB7345 + sample 17: + time = 394739 + flags = 1 + data = length 474, hash 3F05F10A + sample 18: + time = 417959 + flags = 1 + data = length 416, hash C105EE09 + sample 19: + time = 441179 + flags = 1 + data = length 454, hash 5FDBE458 + sample 20: + time = 464399 + flags = 1 + data = length 438, hash 41A93AC3 + sample 21: + time = 487619 + flags = 1 + data = length 443, hash 10FDA652 + sample 22: + time = 510839 + flags = 1 + data = length 412, hash 1F791E25 + sample 23: + time = 534058 + flags = 1 + data = length 482, hash A6D983D + sample 24: + time = 557278 + flags = 1 + data = length 386, hash BED7392F + sample 25: + time = 580498 + flags = 1 + data = length 463, hash 5309F8C9 + sample 26: + time = 603718 + flags = 1 + data = length 394, hash 21C7321F + sample 27: + time = 626938 + flags = 1 + data = length 489, hash 71B4730D + sample 28: + time = 650158 + flags = 1 + data = length 403, hash D9C6DE89 + sample 29: + time = 673378 + flags = 1 + data = length 447, hash 9B14B73B + sample 30: + time = 696598 + flags = 1 + data = length 439, hash 4760D35B + sample 31: + time = 719818 + flags = 1 + data = length 463, hash 1601F88D + sample 32: + time = 743038 + flags = 1 + data = length 423, hash D4AE6773 + sample 33: + time = 766258 + flags = 1 + data = length 497, hash A3C674D3 + sample 34: + time = 789478 + flags = 1 + data = length 419, hash D3734A1F + sample 35: + time = 812698 + flags = 1 + data = length 474, hash DFB41F9 + sample 36: + time = 835918 + flags = 1 + data = length 413, hash 53E7CB9F + sample 37: + time = 859138 + flags = 1 + data = length 445, hash D15B0E39 + sample 38: + time = 882358 + flags = 1 + data = length 453, hash 77ED81E4 + sample 39: + time = 905578 + flags = 1 + data = length 545, hash 3321AEB9 + sample 40: + time = 928798 + flags = 1 + data = length 317, hash F557D0E + sample 41: + time = 952018 + flags = 1 + data = length 537, hash ED58CF7B + sample 42: + time = 975238 + flags = 1 + data = length 458, hash 51CDAA10 + sample 43: + time = 998458 + flags = 1 + data = length 465, hash CBA1EFD7 + sample 44: + time = 1021678 + flags = 1 + data = length 446, hash D6735B8A + sample 45: + time = 1044897 + flags = 1 + data = length 10, hash A453EEBE +tracksEnded = true diff --git a/library/core/src/test/assets/mp4/sample_fragmented_seekable.mp4 b/testdata/src/test/assets/mp4/sample_fragmented_seekable.mp4 similarity index 100% rename from library/core/src/test/assets/mp4/sample_fragmented_seekable.mp4 rename to testdata/src/test/assets/mp4/sample_fragmented_seekable.mp4 diff --git a/library/core/src/test/assets/mp4/sample_fragmented_seekable.mp4.0.dump b/testdata/src/test/assets/mp4/sample_fragmented_seekable.mp4.0.dump similarity index 72% rename from library/core/src/test/assets/mp4/sample_fragmented_seekable.mp4.0.dump rename to testdata/src/test/assets/mp4/sample_fragmented_seekable.mp4.0.dump index 04e2f6f0a0..5dc1f1db59 100644 --- a/library/core/src/test/assets/mp4/sample_fragmented_seekable.mp4.0.dump +++ b/testdata/src/test/assets/mp4/sample_fragmented_seekable.mp4.0.dump @@ -2,360 +2,335 @@ seekMap: isSeekable = true duration = 1067733 getPosition(0) = [[timeUs=66733, position=1325]] + getPosition(1) = [[timeUs=66733, position=1325]] + getPosition(533866) = [[timeUs=66733, position=1325]] + getPosition(1067733) = [[timeUs=66733, position=1325]] numberOfTracks = 2 track 0: - format: - bitrate = -1 + total output bytes = 85933 + sample count = 30 + format 0: id = 1 - containerMimeType = null sampleMimeType = video/avc - maxInputSize = -1 width = 1080 height = 720 - 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 29, hash 4746B5D9 data = length 10, hash 7A0D0F2B - total output bytes = 85933 - sample count = 30 sample 0: - time = 66000 + time = 66733 flags = 1 data = length 38070, hash B58E1AEE sample 1: - time = 199000 + time = 200199 flags = 0 data = length 8340, hash 8AC449FF sample 2: - time = 132000 + time = 133466 flags = 0 data = length 1295, hash C0DA5090 sample 3: - time = 100000 + time = 100100 flags = 0 data = length 469, hash D6E0A200 sample 4: - time = 166000 + time = 166832 flags = 0 data = length 564, hash E5F56C5B sample 5: - time = 332000 + time = 333666 flags = 0 data = length 6075, hash 8756E49E sample 6: - time = 266000 + time = 266933 flags = 0 data = length 847, hash DCC2B618 sample 7: - time = 233000 + time = 233566 flags = 0 data = length 455, hash B9CCE047 sample 8: - time = 299000 + time = 300299 flags = 0 data = length 467, hash 69806D94 sample 9: - time = 466000 + time = 467133 flags = 0 data = length 4549, hash 3944F501 sample 10: - time = 399000 + time = 400399 flags = 0 data = length 1087, hash 491BF106 sample 11: - time = 367000 + time = 367033 flags = 0 data = length 380, hash 5FED016A sample 12: - time = 433000 + time = 433766 flags = 0 data = length 455, hash 8A0610 sample 13: - time = 599000 + time = 600599 flags = 0 data = length 5190, hash B9031D8 sample 14: - time = 533000 + time = 533866 flags = 0 data = length 1071, hash 684E7DC8 sample 15: - time = 500000 + time = 500500 flags = 0 data = length 653, hash 8494F326 sample 16: - time = 566000 + time = 567232 flags = 0 data = length 485, hash 2CCC85F4 sample 17: - time = 733000 + time = 734066 flags = 0 data = length 4884, hash D16B6A96 sample 18: - time = 666000 + time = 667333 flags = 0 data = length 997, hash 164FF210 sample 19: - time = 633000 + time = 633966 flags = 0 data = length 640, hash F664125B sample 20: - time = 700000 + time = 700699 flags = 0 data = length 491, hash B5930C7C sample 21: - time = 866000 + time = 867533 flags = 0 data = length 2989, hash 92CF4FCF sample 22: - time = 800000 + time = 800799 flags = 0 data = length 838, hash 294A3451 sample 23: - time = 767000 + time = 767433 flags = 0 data = length 544, hash FCCE2DE6 sample 24: - time = 833000 + time = 834166 flags = 0 data = length 329, hash A654FFA1 sample 25: - time = 1000000 + time = 1000999 flags = 0 data = length 1517, hash 5F7EBF8B sample 26: - time = 933000 + time = 934266 flags = 0 data = length 803, hash 7A5C4C1D sample 27: - time = 900000 + time = 900900 flags = 0 data = length 415, hash B31BBC3B sample 28: - time = 967000 + time = 967632 flags = 0 data = length 415, hash 850DFEA3 sample 29: - time = 1033000 + time = 1034366 flags = 0 data = length 619, hash AB5E56CA track 1: - format: - bitrate = -1 - id = 2 - 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 = und - drmInitData = - - initializationData: - data = length 5, hash 2B7623A total output bytes = 18257 sample count = 46 + format 0: + id = 2 + sampleMimeType = audio/mp4a-latm + codecs = mp4a.40.2 + channelCount = 1 + sampleRate = 44100 + language = und + initializationData: + data = length 5, hash 2B7623A sample 0: time = 0 flags = 1 data = length 18, hash 96519432 sample 1: - time = 23000 + time = 23219 flags = 1 data = length 4, hash EE9DF sample 2: - time = 46000 + time = 46439 flags = 1 data = length 4, hash EEDBF sample 3: - time = 69000 + time = 69659 flags = 1 data = length 157, hash E2F078F4 sample 4: - time = 92000 + time = 92879 flags = 1 data = length 371, hash B9471F94 sample 5: - time = 116000 + time = 116099 flags = 1 data = length 373, hash 2AB265CB sample 6: - time = 139000 + time = 139319 flags = 1 data = length 402, hash 1295477C sample 7: - time = 162000 + time = 162539 flags = 1 data = length 455, hash 2D8146C8 sample 8: - time = 185000 + time = 185759 flags = 1 data = length 434, hash F2C5D287 sample 9: - time = 208000 + time = 208979 flags = 1 data = length 450, hash 84143FCD sample 10: - time = 232000 + time = 232199 flags = 1 data = length 429, hash EF769D50 sample 11: - time = 255000 + time = 255419 flags = 1 data = length 450, hash EC3DE692 sample 12: - time = 278000 + time = 278639 flags = 1 data = length 447, hash 3E519E13 sample 13: - time = 301000 + time = 301859 flags = 1 data = length 457, hash 1E4F23A0 sample 14: - time = 325000 + time = 325079 flags = 1 data = length 447, hash A439EA97 sample 15: - time = 348000 + time = 348299 flags = 1 data = length 456, hash 1E9034C6 sample 16: - time = 371000 + time = 371519 flags = 1 data = length 398, hash 99DB7345 sample 17: - time = 394000 + time = 394739 flags = 1 data = length 474, hash 3F05F10A sample 18: - time = 417000 + time = 417959 flags = 1 data = length 416, hash C105EE09 sample 19: - time = 441000 + time = 441179 flags = 1 data = length 454, hash 5FDBE458 sample 20: - time = 464000 + time = 464399 flags = 1 data = length 438, hash 41A93AC3 sample 21: - time = 487000 + time = 487619 flags = 1 data = length 443, hash 10FDA652 sample 22: - time = 510000 + time = 510839 flags = 1 data = length 412, hash 1F791E25 sample 23: - time = 534000 + time = 534058 flags = 1 data = length 482, hash A6D983D sample 24: - time = 557000 + time = 557278 flags = 1 data = length 386, hash BED7392F sample 25: - time = 580000 + time = 580498 flags = 1 data = length 463, hash 5309F8C9 sample 26: - time = 603000 + time = 603718 flags = 1 data = length 394, hash 21C7321F sample 27: - time = 626000 + time = 626938 flags = 1 data = length 489, hash 71B4730D sample 28: - time = 650000 + time = 650158 flags = 1 data = length 403, hash D9C6DE89 sample 29: - time = 673000 + time = 673378 flags = 1 data = length 447, hash 9B14B73B sample 30: - time = 696000 + time = 696598 flags = 1 data = length 439, hash 4760D35B sample 31: - time = 719000 + time = 719818 flags = 1 data = length 463, hash 1601F88D sample 32: - time = 743000 + time = 743038 flags = 1 data = length 423, hash D4AE6773 sample 33: - time = 766000 + time = 766258 flags = 1 data = length 497, hash A3C674D3 sample 34: - time = 789000 + time = 789478 flags = 1 data = length 419, hash D3734A1F sample 35: - time = 812000 + time = 812698 flags = 1 data = length 474, hash DFB41F9 sample 36: - time = 835000 + time = 835918 flags = 1 data = length 413, hash 53E7CB9F sample 37: - time = 859000 + time = 859138 flags = 1 data = length 445, hash D15B0E39 sample 38: - time = 882000 + time = 882358 flags = 1 data = length 453, hash 77ED81E4 sample 39: - time = 905000 + time = 905578 flags = 1 data = length 545, hash 3321AEB9 sample 40: - time = 928000 + time = 928798 flags = 1 data = length 317, hash F557D0E sample 41: - time = 952000 + time = 952018 flags = 1 data = length 537, hash ED58CF7B sample 42: - time = 975000 + time = 975238 flags = 1 data = length 458, hash 51CDAA10 sample 43: - time = 998000 + time = 998458 flags = 1 data = length 465, hash CBA1EFD7 sample 44: - time = 1021000 + time = 1021678 flags = 1 data = length 446, hash D6735B8A sample 45: - time = 1044000 + time = 1044897 flags = 1 data = length 10, hash A453EEBE tracksEnded = true diff --git a/library/core/src/test/assets/mp4/sample_fragmented_seekable.mp4.1.dump b/testdata/src/test/assets/mp4/sample_fragmented_seekable.mp4.1.dump similarity index 71% rename from library/core/src/test/assets/mp4/sample_fragmented_seekable.mp4.1.dump rename to testdata/src/test/assets/mp4/sample_fragmented_seekable.mp4.1.dump index 48a7623a7d..aab2beb27c 100644 --- a/library/core/src/test/assets/mp4/sample_fragmented_seekable.mp4.1.dump +++ b/testdata/src/test/assets/mp4/sample_fragmented_seekable.mp4.1.dump @@ -2,300 +2,275 @@ seekMap: isSeekable = true duration = 1067733 getPosition(0) = [[timeUs=66733, position=1325]] + getPosition(1) = [[timeUs=66733, position=1325]] + getPosition(533866) = [[timeUs=66733, position=1325]] + getPosition(1067733) = [[timeUs=66733, position=1325]] numberOfTracks = 2 track 0: - format: - bitrate = -1 + total output bytes = 85933 + sample count = 30 + format 0: id = 1 - containerMimeType = null sampleMimeType = video/avc - maxInputSize = -1 width = 1080 height = 720 - 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 29, hash 4746B5D9 data = length 10, hash 7A0D0F2B - total output bytes = 85933 - sample count = 30 sample 0: - time = 66000 + time = 66733 flags = 1 data = length 38070, hash B58E1AEE sample 1: - time = 199000 + time = 200199 flags = 0 data = length 8340, hash 8AC449FF sample 2: - time = 132000 + time = 133466 flags = 0 data = length 1295, hash C0DA5090 sample 3: - time = 100000 + time = 100100 flags = 0 data = length 469, hash D6E0A200 sample 4: - time = 166000 + time = 166832 flags = 0 data = length 564, hash E5F56C5B sample 5: - time = 332000 + time = 333666 flags = 0 data = length 6075, hash 8756E49E sample 6: - time = 266000 + time = 266933 flags = 0 data = length 847, hash DCC2B618 sample 7: - time = 233000 + time = 233566 flags = 0 data = length 455, hash B9CCE047 sample 8: - time = 299000 + time = 300299 flags = 0 data = length 467, hash 69806D94 sample 9: - time = 466000 + time = 467133 flags = 0 data = length 4549, hash 3944F501 sample 10: - time = 399000 + time = 400399 flags = 0 data = length 1087, hash 491BF106 sample 11: - time = 367000 + time = 367033 flags = 0 data = length 380, hash 5FED016A sample 12: - time = 433000 + time = 433766 flags = 0 data = length 455, hash 8A0610 sample 13: - time = 599000 + time = 600599 flags = 0 data = length 5190, hash B9031D8 sample 14: - time = 533000 + time = 533866 flags = 0 data = length 1071, hash 684E7DC8 sample 15: - time = 500000 + time = 500500 flags = 0 data = length 653, hash 8494F326 sample 16: - time = 566000 + time = 567232 flags = 0 data = length 485, hash 2CCC85F4 sample 17: - time = 733000 + time = 734066 flags = 0 data = length 4884, hash D16B6A96 sample 18: - time = 666000 + time = 667333 flags = 0 data = length 997, hash 164FF210 sample 19: - time = 633000 + time = 633966 flags = 0 data = length 640, hash F664125B sample 20: - time = 700000 + time = 700699 flags = 0 data = length 491, hash B5930C7C sample 21: - time = 866000 + time = 867533 flags = 0 data = length 2989, hash 92CF4FCF sample 22: - time = 800000 + time = 800799 flags = 0 data = length 838, hash 294A3451 sample 23: - time = 767000 + time = 767433 flags = 0 data = length 544, hash FCCE2DE6 sample 24: - time = 833000 + time = 834166 flags = 0 data = length 329, hash A654FFA1 sample 25: - time = 1000000 + time = 1000999 flags = 0 data = length 1517, hash 5F7EBF8B sample 26: - time = 933000 + time = 934266 flags = 0 data = length 803, hash 7A5C4C1D sample 27: - time = 900000 + time = 900900 flags = 0 data = length 415, hash B31BBC3B sample 28: - time = 967000 + time = 967632 flags = 0 data = length 415, hash 850DFEA3 sample 29: - time = 1033000 + time = 1034366 flags = 0 data = length 619, hash AB5E56CA track 1: - format: - bitrate = -1 - id = 2 - 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 = und - drmInitData = - - initializationData: - data = length 5, hash 2B7623A total output bytes = 13359 sample count = 31 + format 0: + id = 2 + sampleMimeType = audio/mp4a-latm + codecs = mp4a.40.2 + channelCount = 1 + sampleRate = 44100 + language = und + initializationData: + data = length 5, hash 2B7623A sample 0: - time = 348000 + time = 348299 flags = 1 data = length 456, hash 1E9034C6 sample 1: - time = 371000 + time = 371519 flags = 1 data = length 398, hash 99DB7345 sample 2: - time = 394000 + time = 394739 flags = 1 data = length 474, hash 3F05F10A sample 3: - time = 417000 + time = 417959 flags = 1 data = length 416, hash C105EE09 sample 4: - time = 441000 + time = 441179 flags = 1 data = length 454, hash 5FDBE458 sample 5: - time = 464000 + time = 464399 flags = 1 data = length 438, hash 41A93AC3 sample 6: - time = 487000 + time = 487619 flags = 1 data = length 443, hash 10FDA652 sample 7: - time = 510000 + time = 510839 flags = 1 data = length 412, hash 1F791E25 sample 8: - time = 534000 + time = 534058 flags = 1 data = length 482, hash A6D983D sample 9: - time = 557000 + time = 557278 flags = 1 data = length 386, hash BED7392F sample 10: - time = 580000 + time = 580498 flags = 1 data = length 463, hash 5309F8C9 sample 11: - time = 603000 + time = 603718 flags = 1 data = length 394, hash 21C7321F sample 12: - time = 626000 + time = 626938 flags = 1 data = length 489, hash 71B4730D sample 13: - time = 650000 + time = 650158 flags = 1 data = length 403, hash D9C6DE89 sample 14: - time = 673000 + time = 673378 flags = 1 data = length 447, hash 9B14B73B sample 15: - time = 696000 + time = 696598 flags = 1 data = length 439, hash 4760D35B sample 16: - time = 719000 + time = 719818 flags = 1 data = length 463, hash 1601F88D sample 17: - time = 743000 + time = 743038 flags = 1 data = length 423, hash D4AE6773 sample 18: - time = 766000 + time = 766258 flags = 1 data = length 497, hash A3C674D3 sample 19: - time = 789000 + time = 789478 flags = 1 data = length 419, hash D3734A1F sample 20: - time = 812000 + time = 812698 flags = 1 data = length 474, hash DFB41F9 sample 21: - time = 835000 + time = 835918 flags = 1 data = length 413, hash 53E7CB9F sample 22: - time = 859000 + time = 859138 flags = 1 data = length 445, hash D15B0E39 sample 23: - time = 882000 + time = 882358 flags = 1 data = length 453, hash 77ED81E4 sample 24: - time = 905000 + time = 905578 flags = 1 data = length 545, hash 3321AEB9 sample 25: - time = 928000 + time = 928798 flags = 1 data = length 317, hash F557D0E sample 26: - time = 952000 + time = 952018 flags = 1 data = length 537, hash ED58CF7B sample 27: - time = 975000 + time = 975238 flags = 1 data = length 458, hash 51CDAA10 sample 28: - time = 998000 + time = 998458 flags = 1 data = length 465, hash CBA1EFD7 sample 29: - time = 1021000 + time = 1021678 flags = 1 data = length 446, hash D6735B8A sample 30: - time = 1044000 + time = 1044897 flags = 1 data = length 10, hash A453EEBE tracksEnded = true diff --git a/library/core/src/test/assets/mp4/sample_fragmented_seekable.mp4.2.dump b/testdata/src/test/assets/mp4/sample_fragmented_seekable.mp4.2.dump similarity index 69% rename from library/core/src/test/assets/mp4/sample_fragmented_seekable.mp4.2.dump rename to testdata/src/test/assets/mp4/sample_fragmented_seekable.mp4.2.dump index 7522891e14..c1d569c0c9 100644 --- a/library/core/src/test/assets/mp4/sample_fragmented_seekable.mp4.2.dump +++ b/testdata/src/test/assets/mp4/sample_fragmented_seekable.mp4.2.dump @@ -2,240 +2,215 @@ seekMap: isSeekable = true duration = 1067733 getPosition(0) = [[timeUs=66733, position=1325]] + getPosition(1) = [[timeUs=66733, position=1325]] + getPosition(533866) = [[timeUs=66733, position=1325]] + getPosition(1067733) = [[timeUs=66733, position=1325]] numberOfTracks = 2 track 0: - format: - bitrate = -1 + total output bytes = 85933 + sample count = 30 + format 0: id = 1 - containerMimeType = null sampleMimeType = video/avc - maxInputSize = -1 width = 1080 height = 720 - 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 29, hash 4746B5D9 data = length 10, hash 7A0D0F2B - total output bytes = 85933 - sample count = 30 sample 0: - time = 66000 + time = 66733 flags = 1 data = length 38070, hash B58E1AEE sample 1: - time = 199000 + time = 200199 flags = 0 data = length 8340, hash 8AC449FF sample 2: - time = 132000 + time = 133466 flags = 0 data = length 1295, hash C0DA5090 sample 3: - time = 100000 + time = 100100 flags = 0 data = length 469, hash D6E0A200 sample 4: - time = 166000 + time = 166832 flags = 0 data = length 564, hash E5F56C5B sample 5: - time = 332000 + time = 333666 flags = 0 data = length 6075, hash 8756E49E sample 6: - time = 266000 + time = 266933 flags = 0 data = length 847, hash DCC2B618 sample 7: - time = 233000 + time = 233566 flags = 0 data = length 455, hash B9CCE047 sample 8: - time = 299000 + time = 300299 flags = 0 data = length 467, hash 69806D94 sample 9: - time = 466000 + time = 467133 flags = 0 data = length 4549, hash 3944F501 sample 10: - time = 399000 + time = 400399 flags = 0 data = length 1087, hash 491BF106 sample 11: - time = 367000 + time = 367033 flags = 0 data = length 380, hash 5FED016A sample 12: - time = 433000 + time = 433766 flags = 0 data = length 455, hash 8A0610 sample 13: - time = 599000 + time = 600599 flags = 0 data = length 5190, hash B9031D8 sample 14: - time = 533000 + time = 533866 flags = 0 data = length 1071, hash 684E7DC8 sample 15: - time = 500000 + time = 500500 flags = 0 data = length 653, hash 8494F326 sample 16: - time = 566000 + time = 567232 flags = 0 data = length 485, hash 2CCC85F4 sample 17: - time = 733000 + time = 734066 flags = 0 data = length 4884, hash D16B6A96 sample 18: - time = 666000 + time = 667333 flags = 0 data = length 997, hash 164FF210 sample 19: - time = 633000 + time = 633966 flags = 0 data = length 640, hash F664125B sample 20: - time = 700000 + time = 700699 flags = 0 data = length 491, hash B5930C7C sample 21: - time = 866000 + time = 867533 flags = 0 data = length 2989, hash 92CF4FCF sample 22: - time = 800000 + time = 800799 flags = 0 data = length 838, hash 294A3451 sample 23: - time = 767000 + time = 767433 flags = 0 data = length 544, hash FCCE2DE6 sample 24: - time = 833000 + time = 834166 flags = 0 data = length 329, hash A654FFA1 sample 25: - time = 1000000 + time = 1000999 flags = 0 data = length 1517, hash 5F7EBF8B sample 26: - time = 933000 + time = 934266 flags = 0 data = length 803, hash 7A5C4C1D sample 27: - time = 900000 + time = 900900 flags = 0 data = length 415, hash B31BBC3B sample 28: - time = 967000 + time = 967632 flags = 0 data = length 415, hash 850DFEA3 sample 29: - time = 1033000 + time = 1034366 flags = 0 data = length 619, hash AB5E56CA track 1: - format: - bitrate = -1 - id = 2 - 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 = und - drmInitData = - - initializationData: - data = length 5, hash 2B7623A total output bytes = 6804 sample count = 16 + format 0: + id = 2 + sampleMimeType = audio/mp4a-latm + codecs = mp4a.40.2 + channelCount = 1 + sampleRate = 44100 + language = und + initializationData: + data = length 5, hash 2B7623A sample 0: - time = 696000 + time = 696598 flags = 1 data = length 439, hash 4760D35B sample 1: - time = 719000 + time = 719818 flags = 1 data = length 463, hash 1601F88D sample 2: - time = 743000 + time = 743038 flags = 1 data = length 423, hash D4AE6773 sample 3: - time = 766000 + time = 766258 flags = 1 data = length 497, hash A3C674D3 sample 4: - time = 789000 + time = 789478 flags = 1 data = length 419, hash D3734A1F sample 5: - time = 812000 + time = 812698 flags = 1 data = length 474, hash DFB41F9 sample 6: - time = 835000 + time = 835918 flags = 1 data = length 413, hash 53E7CB9F sample 7: - time = 859000 + time = 859138 flags = 1 data = length 445, hash D15B0E39 sample 8: - time = 882000 + time = 882358 flags = 1 data = length 453, hash 77ED81E4 sample 9: - time = 905000 + time = 905578 flags = 1 data = length 545, hash 3321AEB9 sample 10: - time = 928000 + time = 928798 flags = 1 data = length 317, hash F557D0E sample 11: - time = 952000 + time = 952018 flags = 1 data = length 537, hash ED58CF7B sample 12: - time = 975000 + time = 975238 flags = 1 data = length 458, hash 51CDAA10 sample 13: - time = 998000 + time = 998458 flags = 1 data = length 465, hash CBA1EFD7 sample 14: - time = 1021000 + time = 1021678 flags = 1 data = length 446, hash D6735B8A sample 15: - time = 1044000 + time = 1044897 flags = 1 data = length 10, hash A453EEBE tracksEnded = true diff --git a/library/core/src/test/assets/mp4/sample_fragmented_seekable.mp4.3.dump b/testdata/src/test/assets/mp4/sample_fragmented_seekable.mp4.3.dump similarity index 67% rename from library/core/src/test/assets/mp4/sample_fragmented_seekable.mp4.3.dump rename to testdata/src/test/assets/mp4/sample_fragmented_seekable.mp4.3.dump index afd24e40ce..dd915bcb08 100644 --- a/library/core/src/test/assets/mp4/sample_fragmented_seekable.mp4.3.dump +++ b/testdata/src/test/assets/mp4/sample_fragmented_seekable.mp4.3.dump @@ -2,180 +2,155 @@ seekMap: isSeekable = true duration = 1067733 getPosition(0) = [[timeUs=66733, position=1325]] + getPosition(1) = [[timeUs=66733, position=1325]] + getPosition(533866) = [[timeUs=66733, position=1325]] + getPosition(1067733) = [[timeUs=66733, position=1325]] numberOfTracks = 2 track 0: - format: - bitrate = -1 + total output bytes = 85933 + sample count = 30 + format 0: id = 1 - containerMimeType = null sampleMimeType = video/avc - maxInputSize = -1 width = 1080 height = 720 - 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 29, hash 4746B5D9 data = length 10, hash 7A0D0F2B - total output bytes = 85933 - sample count = 30 sample 0: - time = 66000 + time = 66733 flags = 1 data = length 38070, hash B58E1AEE sample 1: - time = 199000 + time = 200199 flags = 0 data = length 8340, hash 8AC449FF sample 2: - time = 132000 + time = 133466 flags = 0 data = length 1295, hash C0DA5090 sample 3: - time = 100000 + time = 100100 flags = 0 data = length 469, hash D6E0A200 sample 4: - time = 166000 + time = 166832 flags = 0 data = length 564, hash E5F56C5B sample 5: - time = 332000 + time = 333666 flags = 0 data = length 6075, hash 8756E49E sample 6: - time = 266000 + time = 266933 flags = 0 data = length 847, hash DCC2B618 sample 7: - time = 233000 + time = 233566 flags = 0 data = length 455, hash B9CCE047 sample 8: - time = 299000 + time = 300299 flags = 0 data = length 467, hash 69806D94 sample 9: - time = 466000 + time = 467133 flags = 0 data = length 4549, hash 3944F501 sample 10: - time = 399000 + time = 400399 flags = 0 data = length 1087, hash 491BF106 sample 11: - time = 367000 + time = 367033 flags = 0 data = length 380, hash 5FED016A sample 12: - time = 433000 + time = 433766 flags = 0 data = length 455, hash 8A0610 sample 13: - time = 599000 + time = 600599 flags = 0 data = length 5190, hash B9031D8 sample 14: - time = 533000 + time = 533866 flags = 0 data = length 1071, hash 684E7DC8 sample 15: - time = 500000 + time = 500500 flags = 0 data = length 653, hash 8494F326 sample 16: - time = 566000 + time = 567232 flags = 0 data = length 485, hash 2CCC85F4 sample 17: - time = 733000 + time = 734066 flags = 0 data = length 4884, hash D16B6A96 sample 18: - time = 666000 + time = 667333 flags = 0 data = length 997, hash 164FF210 sample 19: - time = 633000 + time = 633966 flags = 0 data = length 640, hash F664125B sample 20: - time = 700000 + time = 700699 flags = 0 data = length 491, hash B5930C7C sample 21: - time = 866000 + time = 867533 flags = 0 data = length 2989, hash 92CF4FCF sample 22: - time = 800000 + time = 800799 flags = 0 data = length 838, hash 294A3451 sample 23: - time = 767000 + time = 767433 flags = 0 data = length 544, hash FCCE2DE6 sample 24: - time = 833000 + time = 834166 flags = 0 data = length 329, hash A654FFA1 sample 25: - time = 1000000 + time = 1000999 flags = 0 data = length 1517, hash 5F7EBF8B sample 26: - time = 933000 + time = 934266 flags = 0 data = length 803, hash 7A5C4C1D sample 27: - time = 900000 + time = 900900 flags = 0 data = length 415, hash B31BBC3B sample 28: - time = 967000 + time = 967632 flags = 0 data = length 415, hash 850DFEA3 sample 29: - time = 1033000 + time = 1034366 flags = 0 data = length 619, hash AB5E56CA track 1: - format: - bitrate = -1 - id = 2 - 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 = und - drmInitData = - - initializationData: - data = length 5, hash 2B7623A total output bytes = 10 sample count = 1 + format 0: + id = 2 + sampleMimeType = audio/mp4a-latm + codecs = mp4a.40.2 + channelCount = 1 + sampleRate = 44100 + language = und + initializationData: + data = length 5, hash 2B7623A sample 0: - time = 1044000 + time = 1044897 flags = 1 data = length 10, hash A453EEBE tracksEnded = true diff --git a/testdata/src/test/assets/mp4/sample_fragmented_seekable.mp4.unknown_length.dump b/testdata/src/test/assets/mp4/sample_fragmented_seekable.mp4.unknown_length.dump new file mode 100644 index 0000000000..5dc1f1db59 --- /dev/null +++ b/testdata/src/test/assets/mp4/sample_fragmented_seekable.mp4.unknown_length.dump @@ -0,0 +1,336 @@ +seekMap: + isSeekable = true + duration = 1067733 + getPosition(0) = [[timeUs=66733, position=1325]] + getPosition(1) = [[timeUs=66733, position=1325]] + getPosition(533866) = [[timeUs=66733, position=1325]] + getPosition(1067733) = [[timeUs=66733, position=1325]] +numberOfTracks = 2 +track 0: + total output bytes = 85933 + sample count = 30 + format 0: + id = 1 + sampleMimeType = video/avc + width = 1080 + height = 720 + initializationData: + data = length 29, hash 4746B5D9 + data = length 10, hash 7A0D0F2B + sample 0: + time = 66733 + flags = 1 + data = length 38070, hash B58E1AEE + sample 1: + time = 200199 + flags = 0 + data = length 8340, hash 8AC449FF + sample 2: + time = 133466 + flags = 0 + data = length 1295, hash C0DA5090 + sample 3: + time = 100100 + flags = 0 + data = length 469, hash D6E0A200 + sample 4: + time = 166832 + flags = 0 + data = length 564, hash E5F56C5B + sample 5: + time = 333666 + flags = 0 + data = length 6075, hash 8756E49E + sample 6: + time = 266933 + flags = 0 + data = length 847, hash DCC2B618 + sample 7: + time = 233566 + flags = 0 + data = length 455, hash B9CCE047 + sample 8: + time = 300299 + flags = 0 + data = length 467, hash 69806D94 + sample 9: + time = 467133 + flags = 0 + data = length 4549, hash 3944F501 + sample 10: + time = 400399 + flags = 0 + data = length 1087, hash 491BF106 + sample 11: + time = 367033 + flags = 0 + data = length 380, hash 5FED016A + sample 12: + time = 433766 + flags = 0 + data = length 455, hash 8A0610 + sample 13: + time = 600599 + flags = 0 + data = length 5190, hash B9031D8 + sample 14: + time = 533866 + flags = 0 + data = length 1071, hash 684E7DC8 + sample 15: + time = 500500 + flags = 0 + data = length 653, hash 8494F326 + sample 16: + time = 567232 + flags = 0 + data = length 485, hash 2CCC85F4 + sample 17: + time = 734066 + flags = 0 + data = length 4884, hash D16B6A96 + sample 18: + time = 667333 + flags = 0 + data = length 997, hash 164FF210 + sample 19: + time = 633966 + flags = 0 + data = length 640, hash F664125B + sample 20: + time = 700699 + flags = 0 + data = length 491, hash B5930C7C + sample 21: + time = 867533 + flags = 0 + data = length 2989, hash 92CF4FCF + sample 22: + time = 800799 + flags = 0 + data = length 838, hash 294A3451 + sample 23: + time = 767433 + flags = 0 + data = length 544, hash FCCE2DE6 + sample 24: + time = 834166 + flags = 0 + data = length 329, hash A654FFA1 + sample 25: + time = 1000999 + flags = 0 + data = length 1517, hash 5F7EBF8B + sample 26: + time = 934266 + flags = 0 + data = length 803, hash 7A5C4C1D + sample 27: + time = 900900 + flags = 0 + data = length 415, hash B31BBC3B + sample 28: + time = 967632 + flags = 0 + data = length 415, hash 850DFEA3 + sample 29: + time = 1034366 + flags = 0 + data = length 619, hash AB5E56CA +track 1: + total output bytes = 18257 + sample count = 46 + format 0: + id = 2 + sampleMimeType = audio/mp4a-latm + codecs = mp4a.40.2 + channelCount = 1 + sampleRate = 44100 + language = und + initializationData: + data = length 5, hash 2B7623A + sample 0: + time = 0 + flags = 1 + data = length 18, hash 96519432 + sample 1: + time = 23219 + flags = 1 + data = length 4, hash EE9DF + sample 2: + time = 46439 + flags = 1 + data = length 4, hash EEDBF + sample 3: + time = 69659 + flags = 1 + data = length 157, hash E2F078F4 + sample 4: + time = 92879 + flags = 1 + data = length 371, hash B9471F94 + sample 5: + time = 116099 + flags = 1 + data = length 373, hash 2AB265CB + sample 6: + time = 139319 + flags = 1 + data = length 402, hash 1295477C + sample 7: + time = 162539 + flags = 1 + data = length 455, hash 2D8146C8 + sample 8: + time = 185759 + flags = 1 + data = length 434, hash F2C5D287 + sample 9: + time = 208979 + flags = 1 + data = length 450, hash 84143FCD + sample 10: + time = 232199 + flags = 1 + data = length 429, hash EF769D50 + sample 11: + time = 255419 + flags = 1 + data = length 450, hash EC3DE692 + sample 12: + time = 278639 + flags = 1 + data = length 447, hash 3E519E13 + sample 13: + time = 301859 + flags = 1 + data = length 457, hash 1E4F23A0 + sample 14: + time = 325079 + flags = 1 + data = length 447, hash A439EA97 + sample 15: + time = 348299 + flags = 1 + data = length 456, hash 1E9034C6 + sample 16: + time = 371519 + flags = 1 + data = length 398, hash 99DB7345 + sample 17: + time = 394739 + flags = 1 + data = length 474, hash 3F05F10A + sample 18: + time = 417959 + flags = 1 + data = length 416, hash C105EE09 + sample 19: + time = 441179 + flags = 1 + data = length 454, hash 5FDBE458 + sample 20: + time = 464399 + flags = 1 + data = length 438, hash 41A93AC3 + sample 21: + time = 487619 + flags = 1 + data = length 443, hash 10FDA652 + sample 22: + time = 510839 + flags = 1 + data = length 412, hash 1F791E25 + sample 23: + time = 534058 + flags = 1 + data = length 482, hash A6D983D + sample 24: + time = 557278 + flags = 1 + data = length 386, hash BED7392F + sample 25: + time = 580498 + flags = 1 + data = length 463, hash 5309F8C9 + sample 26: + time = 603718 + flags = 1 + data = length 394, hash 21C7321F + sample 27: + time = 626938 + flags = 1 + data = length 489, hash 71B4730D + sample 28: + time = 650158 + flags = 1 + data = length 403, hash D9C6DE89 + sample 29: + time = 673378 + flags = 1 + data = length 447, hash 9B14B73B + sample 30: + time = 696598 + flags = 1 + data = length 439, hash 4760D35B + sample 31: + time = 719818 + flags = 1 + data = length 463, hash 1601F88D + sample 32: + time = 743038 + flags = 1 + data = length 423, hash D4AE6773 + sample 33: + time = 766258 + flags = 1 + data = length 497, hash A3C674D3 + sample 34: + time = 789478 + flags = 1 + data = length 419, hash D3734A1F + sample 35: + time = 812698 + flags = 1 + data = length 474, hash DFB41F9 + sample 36: + time = 835918 + flags = 1 + data = length 413, hash 53E7CB9F + sample 37: + time = 859138 + flags = 1 + data = length 445, hash D15B0E39 + sample 38: + time = 882358 + flags = 1 + data = length 453, hash 77ED81E4 + sample 39: + time = 905578 + flags = 1 + data = length 545, hash 3321AEB9 + sample 40: + time = 928798 + flags = 1 + data = length 317, hash F557D0E + sample 41: + time = 952018 + flags = 1 + data = length 537, hash ED58CF7B + sample 42: + time = 975238 + flags = 1 + data = length 458, hash 51CDAA10 + sample 43: + time = 998458 + flags = 1 + data = length 465, hash CBA1EFD7 + sample 44: + time = 1021678 + flags = 1 + data = length 446, hash D6735B8A + sample 45: + time = 1044897 + flags = 1 + data = length 10, hash A453EEBE +tracksEnded = true diff --git a/library/core/src/test/assets/mp4/sample_fragmented_sei.mp4 b/testdata/src/test/assets/mp4/sample_fragmented_sei.mp4 similarity index 100% rename from library/core/src/test/assets/mp4/sample_fragmented_sei.mp4 rename to testdata/src/test/assets/mp4/sample_fragmented_sei.mp4 diff --git a/library/core/src/test/assets/mp4/sample_fragmented_sei.mp4.0.dump b/testdata/src/test/assets/mp4/sample_fragmented_sei.mp4.0.dump similarity index 68% rename from library/core/src/test/assets/mp4/sample_fragmented_sei.mp4.0.dump rename to testdata/src/test/assets/mp4/sample_fragmented_sei.mp4.0.dump index 87f2cc6714..341fba46b9 100644 --- a/library/core/src/test/assets/mp4/sample_fragmented_sei.mp4.0.dump +++ b/testdata/src/test/assets/mp4/sample_fragmented_sei.mp4.0.dump @@ -4,382 +4,335 @@ seekMap: getPosition(0) = [[timeUs=0, position=1828]] numberOfTracks = 3 track 0: - format: - bitrate = -1 + total output bytes = 85933 + sample count = 30 + format 0: id = 1 - containerMimeType = null sampleMimeType = video/avc - maxInputSize = -1 width = 1080 height = 720 - 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 29, hash 4746B5D9 data = length 10, hash 7A0D0F2B - total output bytes = 85933 - sample count = 30 sample 0: - time = 66000 + time = 66733 flags = 1 data = length 38070, hash B58E1AEE sample 1: - time = 199000 + time = 200199 flags = 0 data = length 8340, hash 8AC449FF sample 2: - time = 132000 + time = 133466 flags = 0 data = length 1295, hash C0DA5090 sample 3: - time = 100000 + time = 100100 flags = 0 data = length 469, hash D6E0A200 sample 4: - time = 166000 + time = 166832 flags = 0 data = length 564, hash E5F56C5B sample 5: - time = 332000 + time = 333666 flags = 0 data = length 6075, hash 8756E49E sample 6: - time = 266000 + time = 266933 flags = 0 data = length 847, hash DCC2B618 sample 7: - time = 233000 + time = 233566 flags = 0 data = length 455, hash B9CCE047 sample 8: - time = 299000 + time = 300299 flags = 0 data = length 467, hash 69806D94 sample 9: - time = 466000 + time = 467133 flags = 0 data = length 4549, hash 3944F501 sample 10: - time = 399000 + time = 400399 flags = 0 data = length 1087, hash 491BF106 sample 11: - time = 367000 + time = 367033 flags = 0 data = length 380, hash 5FED016A sample 12: - time = 433000 + time = 433766 flags = 0 data = length 455, hash 8A0610 sample 13: - time = 599000 + time = 600599 flags = 0 data = length 5190, hash B9031D8 sample 14: - time = 533000 + time = 533866 flags = 0 data = length 1071, hash 684E7DC8 sample 15: - time = 500000 + time = 500500 flags = 0 data = length 653, hash 8494F326 sample 16: - time = 566000 + time = 567232 flags = 0 data = length 485, hash 2CCC85F4 sample 17: - time = 733000 + time = 734066 flags = 0 data = length 4884, hash D16B6A96 sample 18: - time = 666000 + time = 667333 flags = 0 data = length 997, hash 164FF210 sample 19: - time = 633000 + time = 633966 flags = 0 data = length 640, hash F664125B sample 20: - time = 700000 + time = 700699 flags = 0 data = length 491, hash B5930C7C sample 21: - time = 866000 + time = 867533 flags = 0 data = length 2989, hash 92CF4FCF sample 22: - time = 800000 + time = 800799 flags = 0 data = length 838, hash 294A3451 sample 23: - time = 767000 + time = 767433 flags = 0 data = length 544, hash FCCE2DE6 sample 24: - time = 833000 + time = 834166 flags = 0 data = length 329, hash A654FFA1 sample 25: - time = 1000000 + time = 1000999 flags = 0 data = length 1517, hash 5F7EBF8B sample 26: - time = 933000 + time = 934266 flags = 0 data = length 803, hash 7A5C4C1D sample 27: - time = 900000 + time = 900900 flags = 0 data = length 415, hash B31BBC3B sample 28: - time = 967000 + time = 967632 flags = 0 data = length 415, hash 850DFEA3 sample 29: - time = 1033000 + time = 1034366 flags = 0 data = length 619, hash AB5E56CA track 1: - format: - bitrate = -1 - id = 2 - 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 = und - drmInitData = - - initializationData: - data = length 5, hash 2B7623A total output bytes = 18257 sample count = 46 + format 0: + id = 2 + sampleMimeType = audio/mp4a-latm + codecs = mp4a.40.2 + channelCount = 1 + sampleRate = 44100 + language = und + initializationData: + data = length 5, hash 2B7623A sample 0: time = 0 flags = 1 data = length 18, hash 96519432 sample 1: - time = 23000 + time = 23219 flags = 1 data = length 4, hash EE9DF sample 2: - time = 46000 + time = 46439 flags = 1 data = length 4, hash EEDBF sample 3: - time = 69000 + time = 69659 flags = 1 data = length 157, hash E2F078F4 sample 4: - time = 92000 + time = 92879 flags = 1 data = length 371, hash B9471F94 sample 5: - time = 116000 + time = 116099 flags = 1 data = length 373, hash 2AB265CB sample 6: - time = 139000 + time = 139319 flags = 1 data = length 402, hash 1295477C sample 7: - time = 162000 + time = 162539 flags = 1 data = length 455, hash 2D8146C8 sample 8: - time = 185000 + time = 185759 flags = 1 data = length 434, hash F2C5D287 sample 9: - time = 208000 + time = 208979 flags = 1 data = length 450, hash 84143FCD sample 10: - time = 232000 + time = 232199 flags = 1 data = length 429, hash EF769D50 sample 11: - time = 255000 + time = 255419 flags = 1 data = length 450, hash EC3DE692 sample 12: - time = 278000 + time = 278639 flags = 1 data = length 447, hash 3E519E13 sample 13: - time = 301000 + time = 301859 flags = 1 data = length 457, hash 1E4F23A0 sample 14: - time = 325000 + time = 325079 flags = 1 data = length 447, hash A439EA97 sample 15: - time = 348000 + time = 348299 flags = 1 data = length 456, hash 1E9034C6 sample 16: - time = 371000 + time = 371519 flags = 1 data = length 398, hash 99DB7345 sample 17: - time = 394000 + time = 394739 flags = 1 data = length 474, hash 3F05F10A sample 18: - time = 417000 + time = 417959 flags = 1 data = length 416, hash C105EE09 sample 19: - time = 441000 + time = 441179 flags = 1 data = length 454, hash 5FDBE458 sample 20: - time = 464000 + time = 464399 flags = 1 data = length 438, hash 41A93AC3 sample 21: - time = 487000 + time = 487619 flags = 1 data = length 443, hash 10FDA652 sample 22: - time = 510000 + time = 510839 flags = 1 data = length 412, hash 1F791E25 sample 23: - time = 534000 + time = 534058 flags = 1 data = length 482, hash A6D983D sample 24: - time = 557000 + time = 557278 flags = 1 data = length 386, hash BED7392F sample 25: - time = 580000 + time = 580498 flags = 1 data = length 463, hash 5309F8C9 sample 26: - time = 603000 + time = 603718 flags = 1 data = length 394, hash 21C7321F sample 27: - time = 626000 + time = 626938 flags = 1 data = length 489, hash 71B4730D sample 28: - time = 650000 + time = 650158 flags = 1 data = length 403, hash D9C6DE89 sample 29: - time = 673000 + time = 673378 flags = 1 data = length 447, hash 9B14B73B sample 30: - time = 696000 + time = 696598 flags = 1 data = length 439, hash 4760D35B sample 31: - time = 719000 + time = 719818 flags = 1 data = length 463, hash 1601F88D sample 32: - time = 743000 + time = 743038 flags = 1 data = length 423, hash D4AE6773 sample 33: - time = 766000 + time = 766258 flags = 1 data = length 497, hash A3C674D3 sample 34: - time = 789000 + time = 789478 flags = 1 data = length 419, hash D3734A1F sample 35: - time = 812000 + time = 812698 flags = 1 data = length 474, hash DFB41F9 sample 36: - time = 835000 + time = 835918 flags = 1 data = length 413, hash 53E7CB9F sample 37: - time = 859000 + time = 859138 flags = 1 data = length 445, hash D15B0E39 sample 38: - time = 882000 + time = 882358 flags = 1 data = length 453, hash 77ED81E4 sample 39: - time = 905000 + time = 905578 flags = 1 data = length 545, hash 3321AEB9 sample 40: - time = 928000 + time = 928798 flags = 1 data = length 317, hash F557D0E sample 41: - time = 952000 + time = 952018 flags = 1 data = length 537, hash ED58CF7B sample 42: - time = 975000 + time = 975238 flags = 1 data = length 458, hash 51CDAA10 sample 43: - time = 998000 + time = 998458 flags = 1 data = length 465, hash CBA1EFD7 sample 44: - time = 1021000 + time = 1021678 flags = 1 data = length 446, hash D6735B8A sample 45: - time = 1044000 + time = 1044897 flags = 1 data = length 10, hash A453EEBE track 3: - format: - bitrate = -1 - id = null - 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 + format 0: + sampleMimeType = application/cea-608 tracksEnded = true diff --git a/testdata/src/test/assets/mp4/sample_fragmented_sei.mp4.unknown_length.dump b/testdata/src/test/assets/mp4/sample_fragmented_sei.mp4.unknown_length.dump new file mode 100644 index 0000000000..341fba46b9 --- /dev/null +++ b/testdata/src/test/assets/mp4/sample_fragmented_sei.mp4.unknown_length.dump @@ -0,0 +1,338 @@ +seekMap: + isSeekable = false + duration = UNSET TIME + getPosition(0) = [[timeUs=0, position=1828]] +numberOfTracks = 3 +track 0: + total output bytes = 85933 + sample count = 30 + format 0: + id = 1 + sampleMimeType = video/avc + width = 1080 + height = 720 + initializationData: + data = length 29, hash 4746B5D9 + data = length 10, hash 7A0D0F2B + sample 0: + time = 66733 + flags = 1 + data = length 38070, hash B58E1AEE + sample 1: + time = 200199 + flags = 0 + data = length 8340, hash 8AC449FF + sample 2: + time = 133466 + flags = 0 + data = length 1295, hash C0DA5090 + sample 3: + time = 100100 + flags = 0 + data = length 469, hash D6E0A200 + sample 4: + time = 166832 + flags = 0 + data = length 564, hash E5F56C5B + sample 5: + time = 333666 + flags = 0 + data = length 6075, hash 8756E49E + sample 6: + time = 266933 + flags = 0 + data = length 847, hash DCC2B618 + sample 7: + time = 233566 + flags = 0 + data = length 455, hash B9CCE047 + sample 8: + time = 300299 + flags = 0 + data = length 467, hash 69806D94 + sample 9: + time = 467133 + flags = 0 + data = length 4549, hash 3944F501 + sample 10: + time = 400399 + flags = 0 + data = length 1087, hash 491BF106 + sample 11: + time = 367033 + flags = 0 + data = length 380, hash 5FED016A + sample 12: + time = 433766 + flags = 0 + data = length 455, hash 8A0610 + sample 13: + time = 600599 + flags = 0 + data = length 5190, hash B9031D8 + sample 14: + time = 533866 + flags = 0 + data = length 1071, hash 684E7DC8 + sample 15: + time = 500500 + flags = 0 + data = length 653, hash 8494F326 + sample 16: + time = 567232 + flags = 0 + data = length 485, hash 2CCC85F4 + sample 17: + time = 734066 + flags = 0 + data = length 4884, hash D16B6A96 + sample 18: + time = 667333 + flags = 0 + data = length 997, hash 164FF210 + sample 19: + time = 633966 + flags = 0 + data = length 640, hash F664125B + sample 20: + time = 700699 + flags = 0 + data = length 491, hash B5930C7C + sample 21: + time = 867533 + flags = 0 + data = length 2989, hash 92CF4FCF + sample 22: + time = 800799 + flags = 0 + data = length 838, hash 294A3451 + sample 23: + time = 767433 + flags = 0 + data = length 544, hash FCCE2DE6 + sample 24: + time = 834166 + flags = 0 + data = length 329, hash A654FFA1 + sample 25: + time = 1000999 + flags = 0 + data = length 1517, hash 5F7EBF8B + sample 26: + time = 934266 + flags = 0 + data = length 803, hash 7A5C4C1D + sample 27: + time = 900900 + flags = 0 + data = length 415, hash B31BBC3B + sample 28: + time = 967632 + flags = 0 + data = length 415, hash 850DFEA3 + sample 29: + time = 1034366 + flags = 0 + data = length 619, hash AB5E56CA +track 1: + total output bytes = 18257 + sample count = 46 + format 0: + id = 2 + sampleMimeType = audio/mp4a-latm + codecs = mp4a.40.2 + channelCount = 1 + sampleRate = 44100 + language = und + initializationData: + data = length 5, hash 2B7623A + sample 0: + time = 0 + flags = 1 + data = length 18, hash 96519432 + sample 1: + time = 23219 + flags = 1 + data = length 4, hash EE9DF + sample 2: + time = 46439 + flags = 1 + data = length 4, hash EEDBF + sample 3: + time = 69659 + flags = 1 + data = length 157, hash E2F078F4 + sample 4: + time = 92879 + flags = 1 + data = length 371, hash B9471F94 + sample 5: + time = 116099 + flags = 1 + data = length 373, hash 2AB265CB + sample 6: + time = 139319 + flags = 1 + data = length 402, hash 1295477C + sample 7: + time = 162539 + flags = 1 + data = length 455, hash 2D8146C8 + sample 8: + time = 185759 + flags = 1 + data = length 434, hash F2C5D287 + sample 9: + time = 208979 + flags = 1 + data = length 450, hash 84143FCD + sample 10: + time = 232199 + flags = 1 + data = length 429, hash EF769D50 + sample 11: + time = 255419 + flags = 1 + data = length 450, hash EC3DE692 + sample 12: + time = 278639 + flags = 1 + data = length 447, hash 3E519E13 + sample 13: + time = 301859 + flags = 1 + data = length 457, hash 1E4F23A0 + sample 14: + time = 325079 + flags = 1 + data = length 447, hash A439EA97 + sample 15: + time = 348299 + flags = 1 + data = length 456, hash 1E9034C6 + sample 16: + time = 371519 + flags = 1 + data = length 398, hash 99DB7345 + sample 17: + time = 394739 + flags = 1 + data = length 474, hash 3F05F10A + sample 18: + time = 417959 + flags = 1 + data = length 416, hash C105EE09 + sample 19: + time = 441179 + flags = 1 + data = length 454, hash 5FDBE458 + sample 20: + time = 464399 + flags = 1 + data = length 438, hash 41A93AC3 + sample 21: + time = 487619 + flags = 1 + data = length 443, hash 10FDA652 + sample 22: + time = 510839 + flags = 1 + data = length 412, hash 1F791E25 + sample 23: + time = 534058 + flags = 1 + data = length 482, hash A6D983D + sample 24: + time = 557278 + flags = 1 + data = length 386, hash BED7392F + sample 25: + time = 580498 + flags = 1 + data = length 463, hash 5309F8C9 + sample 26: + time = 603718 + flags = 1 + data = length 394, hash 21C7321F + sample 27: + time = 626938 + flags = 1 + data = length 489, hash 71B4730D + sample 28: + time = 650158 + flags = 1 + data = length 403, hash D9C6DE89 + sample 29: + time = 673378 + flags = 1 + data = length 447, hash 9B14B73B + sample 30: + time = 696598 + flags = 1 + data = length 439, hash 4760D35B + sample 31: + time = 719818 + flags = 1 + data = length 463, hash 1601F88D + sample 32: + time = 743038 + flags = 1 + data = length 423, hash D4AE6773 + sample 33: + time = 766258 + flags = 1 + data = length 497, hash A3C674D3 + sample 34: + time = 789478 + flags = 1 + data = length 419, hash D3734A1F + sample 35: + time = 812698 + flags = 1 + data = length 474, hash DFB41F9 + sample 36: + time = 835918 + flags = 1 + data = length 413, hash 53E7CB9F + sample 37: + time = 859138 + flags = 1 + data = length 445, hash D15B0E39 + sample 38: + time = 882358 + flags = 1 + data = length 453, hash 77ED81E4 + sample 39: + time = 905578 + flags = 1 + data = length 545, hash 3321AEB9 + sample 40: + time = 928798 + flags = 1 + data = length 317, hash F557D0E + sample 41: + time = 952018 + flags = 1 + data = length 537, hash ED58CF7B + sample 42: + time = 975238 + flags = 1 + data = length 458, hash 51CDAA10 + sample 43: + time = 998458 + flags = 1 + data = length 465, hash CBA1EFD7 + sample 44: + time = 1021678 + flags = 1 + data = length 446, hash D6735B8A + sample 45: + time = 1044897 + flags = 1 + data = length 10, hash A453EEBE +track 3: + total output bytes = 0 + sample count = 0 + format 0: + sampleMimeType = application/cea-608 +tracksEnded = true diff --git a/testdata/src/test/assets/mp4/sample_mdat_too_long.mp4 b/testdata/src/test/assets/mp4/sample_mdat_too_long.mp4 new file mode 100644 index 0000000000..f50d4f49de Binary files /dev/null and b/testdata/src/test/assets/mp4/sample_mdat_too_long.mp4 differ diff --git a/testdata/src/test/assets/mp4/sample_mdat_too_long.mp4.0.dump b/testdata/src/test/assets/mp4/sample_mdat_too_long.mp4.0.dump new file mode 100644 index 0000000000..287d52240f --- /dev/null +++ b/testdata/src/test/assets/mp4/sample_mdat_too_long.mp4.0.dump @@ -0,0 +1,336 @@ +seekMap: + isSeekable = true + duration = 1024000 + getPosition(0) = [[timeUs=0, position=2192]] + getPosition(1) = [[timeUs=0, position=2192]] + getPosition(512000) = [[timeUs=0, position=2192]] + getPosition(1024000) = [[timeUs=0, position=2192]] +numberOfTracks = 2 +track 0: + total output bytes = 89876 + sample count = 30 + format 0: + id = 1 + sampleMimeType = video/avc + maxInputSize = 36722 + width = 1080 + height = 720 + frameRate = 29.970028 + initializationData: + data = length 29, hash 4746B5D9 + data = length 10, hash 7A0D0F2B + sample 0: + time = 0 + flags = 1 + data = length 36692, hash D216076E + sample 1: + time = 66733 + flags = 0 + data = length 5312, hash D45D3CA0 + sample 2: + time = 33366 + flags = 0 + data = length 599, hash 1BE7812D + sample 3: + time = 200200 + flags = 0 + data = length 7735, hash 4490F110 + sample 4: + time = 133466 + flags = 0 + data = length 987, hash 560B5036 + sample 5: + time = 100100 + flags = 0 + data = length 673, hash ED7CD8C7 + sample 6: + time = 166833 + flags = 0 + data = length 523, hash 3020DF50 + sample 7: + time = 333666 + flags = 0 + data = length 6061, hash 736C72B2 + sample 8: + time = 266933 + flags = 0 + data = length 992, hash FE132F23 + sample 9: + time = 233566 + flags = 0 + data = length 623, hash 5B2C1816 + sample 10: + time = 300300 + flags = 0 + data = length 421, hash 742E69C1 + sample 11: + time = 433766 + flags = 0 + data = length 4899, hash F72F86A1 + sample 12: + time = 400400 + flags = 0 + data = length 568, hash 519A8E50 + sample 13: + time = 367033 + flags = 0 + data = length 620, hash 3990AA39 + sample 14: + time = 567233 + flags = 0 + data = length 5450, hash F06EC4AA + sample 15: + time = 500500 + flags = 0 + data = length 1051, hash 92DFA63A + sample 16: + time = 467133 + flags = 0 + data = length 874, hash 69587FB4 + sample 17: + time = 533866 + flags = 0 + data = length 781, hash 36BE495B + sample 18: + time = 700700 + flags = 0 + data = length 4725, hash AC0C8CD3 + sample 19: + time = 633966 + flags = 0 + data = length 1022, hash 5D8BFF34 + sample 20: + time = 600600 + flags = 0 + data = length 790, hash 99413A99 + sample 21: + time = 667333 + flags = 0 + data = length 610, hash 5E129290 + sample 22: + time = 834166 + flags = 0 + data = length 2751, hash 769974CB + sample 23: + time = 767433 + flags = 0 + data = length 745, hash B78A477A + sample 24: + time = 734066 + flags = 0 + data = length 621, hash CF741E7A + sample 25: + time = 800800 + flags = 0 + data = length 505, hash 1DB4894E + sample 26: + time = 967633 + flags = 0 + data = length 1268, hash C15348DC + sample 27: + time = 900900 + flags = 0 + data = length 880, hash C2DE85D0 + sample 28: + time = 867533 + flags = 0 + data = length 530, hash C98BC6A8 + sample 29: + time = 934266 + flags = 536870912 + data = length 568, hash 4FE5C8EA +track 1: + total output bytes = 9529 + sample count = 45 + format 0: + id = 2 + sampleMimeType = audio/mp4a-latm + codecs = mp4a.40.2 + maxInputSize = 294 + channelCount = 1 + sampleRate = 44100 + language = und + metadata = entries=[TSSE: description=null: value=Lavf56.1.0] + initializationData: + data = length 2, hash 5F7 + sample 0: + time = 44000 + flags = 1 + data = length 23, hash 47DE9131 + sample 1: + time = 67219 + flags = 1 + data = length 6, hash 31EC5206 + sample 2: + time = 90439 + flags = 1 + data = length 148, hash 894A176B + sample 3: + time = 113659 + flags = 1 + data = length 189, hash CEF235A1 + sample 4: + time = 136879 + flags = 1 + data = length 205, hash BBF5F7B0 + sample 5: + time = 160099 + flags = 1 + data = length 210, hash F278B193 + sample 6: + time = 183319 + flags = 1 + data = length 210, hash 82DA1589 + sample 7: + time = 206539 + flags = 1 + data = length 207, hash 5BE231DF + sample 8: + time = 229759 + flags = 1 + data = length 225, hash 18819EE1 + sample 9: + time = 252979 + flags = 1 + data = length 215, hash CA7FA67B + sample 10: + time = 276199 + flags = 1 + data = length 211, hash 581A1C18 + sample 11: + time = 299419 + flags = 1 + data = length 216, hash ADB88187 + sample 12: + time = 322639 + flags = 1 + data = length 229, hash 2E8BA4DC + sample 13: + time = 345859 + flags = 1 + data = length 232, hash 22F0C510 + sample 14: + time = 369079 + flags = 1 + data = length 235, hash 867AD0DC + sample 15: + time = 392299 + flags = 1 + data = length 231, hash 84E823A8 + sample 16: + time = 415519 + flags = 1 + data = length 226, hash 1BEF3A95 + sample 17: + time = 438739 + flags = 1 + data = length 216, hash EAA345AE + sample 18: + time = 461959 + flags = 1 + data = length 229, hash 6957411F + sample 19: + time = 485179 + flags = 1 + data = length 219, hash 41275022 + sample 20: + time = 508399 + flags = 1 + data = length 241, hash 6495DF96 + sample 21: + time = 531619 + flags = 1 + data = length 228, hash 63D95906 + sample 22: + time = 554839 + flags = 1 + data = length 238, hash 34F676F9 + sample 23: + time = 578058 + flags = 1 + data = length 234, hash E5CBC045 + sample 24: + time = 601278 + flags = 1 + data = length 231, hash 5FC43661 + sample 25: + time = 624498 + flags = 1 + data = length 217, hash 682708ED + sample 26: + time = 647718 + flags = 1 + data = length 239, hash D43780FC + sample 27: + time = 670938 + flags = 1 + data = length 243, hash C5E17980 + sample 28: + time = 694158 + flags = 1 + data = length 231, hash AC5837BA + sample 29: + time = 717378 + flags = 1 + data = length 230, hash 169EE895 + sample 30: + time = 740598 + flags = 1 + data = length 238, hash C48FF3F1 + sample 31: + time = 763818 + flags = 1 + data = length 225, hash 531E4599 + sample 32: + time = 787038 + flags = 1 + data = length 232, hash CB3C6B8D + sample 33: + time = 810258 + flags = 1 + data = length 243, hash F8C94C7 + sample 34: + time = 833478 + flags = 1 + data = length 232, hash A646A7D0 + sample 35: + time = 856698 + flags = 1 + data = length 237, hash E8B787A5 + sample 36: + time = 879918 + flags = 1 + data = length 228, hash 3FA7A29F + sample 37: + time = 903138 + flags = 1 + data = length 235, hash B9B33B0A + sample 38: + time = 926358 + flags = 1 + data = length 264, hash 71A4869E + sample 39: + time = 949578 + flags = 1 + data = length 257, hash D049B54C + sample 40: + time = 972798 + flags = 1 + data = length 227, hash 66757231 + sample 41: + time = 996018 + flags = 1 + data = length 227, hash BD374F1B + sample 42: + time = 1019238 + flags = 1 + data = length 235, hash 999477F6 + sample 43: + time = 1042458 + flags = 1 + data = length 229, hash FFF98DF0 + sample 44: + time = 1065678 + flags = 536870913 + data = length 6, hash 31B22286 +tracksEnded = true diff --git a/testdata/src/test/assets/mp4/sample_mdat_too_long.mp4.1.dump b/testdata/src/test/assets/mp4/sample_mdat_too_long.mp4.1.dump new file mode 100644 index 0000000000..5c559c7a15 --- /dev/null +++ b/testdata/src/test/assets/mp4/sample_mdat_too_long.mp4.1.dump @@ -0,0 +1,288 @@ +seekMap: + isSeekable = true + duration = 1024000 + getPosition(0) = [[timeUs=0, position=2192]] + getPosition(1) = [[timeUs=0, position=2192]] + getPosition(512000) = [[timeUs=0, position=2192]] + getPosition(1024000) = [[timeUs=0, position=2192]] +numberOfTracks = 2 +track 0: + total output bytes = 89876 + sample count = 30 + format 0: + id = 1 + sampleMimeType = video/avc + maxInputSize = 36722 + width = 1080 + height = 720 + frameRate = 29.970028 + initializationData: + data = length 29, hash 4746B5D9 + data = length 10, hash 7A0D0F2B + sample 0: + time = 0 + flags = 1 + data = length 36692, hash D216076E + sample 1: + time = 66733 + flags = 0 + data = length 5312, hash D45D3CA0 + sample 2: + time = 33366 + flags = 0 + data = length 599, hash 1BE7812D + sample 3: + time = 200200 + flags = 0 + data = length 7735, hash 4490F110 + sample 4: + time = 133466 + flags = 0 + data = length 987, hash 560B5036 + sample 5: + time = 100100 + flags = 0 + data = length 673, hash ED7CD8C7 + sample 6: + time = 166833 + flags = 0 + data = length 523, hash 3020DF50 + sample 7: + time = 333666 + flags = 0 + data = length 6061, hash 736C72B2 + sample 8: + time = 266933 + flags = 0 + data = length 992, hash FE132F23 + sample 9: + time = 233566 + flags = 0 + data = length 623, hash 5B2C1816 + sample 10: + time = 300300 + flags = 0 + data = length 421, hash 742E69C1 + sample 11: + time = 433766 + flags = 0 + data = length 4899, hash F72F86A1 + sample 12: + time = 400400 + flags = 0 + data = length 568, hash 519A8E50 + sample 13: + time = 367033 + flags = 0 + data = length 620, hash 3990AA39 + sample 14: + time = 567233 + flags = 0 + data = length 5450, hash F06EC4AA + sample 15: + time = 500500 + flags = 0 + data = length 1051, hash 92DFA63A + sample 16: + time = 467133 + flags = 0 + data = length 874, hash 69587FB4 + sample 17: + time = 533866 + flags = 0 + data = length 781, hash 36BE495B + sample 18: + time = 700700 + flags = 0 + data = length 4725, hash AC0C8CD3 + sample 19: + time = 633966 + flags = 0 + data = length 1022, hash 5D8BFF34 + sample 20: + time = 600600 + flags = 0 + data = length 790, hash 99413A99 + sample 21: + time = 667333 + flags = 0 + data = length 610, hash 5E129290 + sample 22: + time = 834166 + flags = 0 + data = length 2751, hash 769974CB + sample 23: + time = 767433 + flags = 0 + data = length 745, hash B78A477A + sample 24: + time = 734066 + flags = 0 + data = length 621, hash CF741E7A + sample 25: + time = 800800 + flags = 0 + data = length 505, hash 1DB4894E + sample 26: + time = 967633 + flags = 0 + data = length 1268, hash C15348DC + sample 27: + time = 900900 + flags = 0 + data = length 880, hash C2DE85D0 + sample 28: + time = 867533 + flags = 0 + data = length 530, hash C98BC6A8 + sample 29: + time = 934266 + flags = 536870912 + data = length 568, hash 4FE5C8EA +track 1: + total output bytes = 7464 + sample count = 33 + format 0: + id = 2 + sampleMimeType = audio/mp4a-latm + codecs = mp4a.40.2 + maxInputSize = 294 + channelCount = 1 + sampleRate = 44100 + language = und + metadata = entries=[TSSE: description=null: value=Lavf56.1.0] + initializationData: + data = length 2, hash 5F7 + sample 0: + time = 322639 + flags = 1 + data = length 229, hash 2E8BA4DC + sample 1: + time = 345859 + flags = 1 + data = length 232, hash 22F0C510 + sample 2: + time = 369079 + flags = 1 + data = length 235, hash 867AD0DC + sample 3: + time = 392299 + flags = 1 + data = length 231, hash 84E823A8 + sample 4: + time = 415519 + flags = 1 + data = length 226, hash 1BEF3A95 + sample 5: + time = 438739 + flags = 1 + data = length 216, hash EAA345AE + sample 6: + time = 461959 + flags = 1 + data = length 229, hash 6957411F + sample 7: + time = 485179 + flags = 1 + data = length 219, hash 41275022 + sample 8: + time = 508399 + flags = 1 + data = length 241, hash 6495DF96 + sample 9: + time = 531619 + flags = 1 + data = length 228, hash 63D95906 + sample 10: + time = 554839 + flags = 1 + data = length 238, hash 34F676F9 + sample 11: + time = 578058 + flags = 1 + data = length 234, hash E5CBC045 + sample 12: + time = 601278 + flags = 1 + data = length 231, hash 5FC43661 + sample 13: + time = 624498 + flags = 1 + data = length 217, hash 682708ED + sample 14: + time = 647718 + flags = 1 + data = length 239, hash D43780FC + sample 15: + time = 670938 + flags = 1 + data = length 243, hash C5E17980 + sample 16: + time = 694158 + flags = 1 + data = length 231, hash AC5837BA + sample 17: + time = 717378 + flags = 1 + data = length 230, hash 169EE895 + sample 18: + time = 740598 + flags = 1 + data = length 238, hash C48FF3F1 + sample 19: + time = 763818 + flags = 1 + data = length 225, hash 531E4599 + sample 20: + time = 787038 + flags = 1 + data = length 232, hash CB3C6B8D + sample 21: + time = 810258 + flags = 1 + data = length 243, hash F8C94C7 + sample 22: + time = 833478 + flags = 1 + data = length 232, hash A646A7D0 + sample 23: + time = 856698 + flags = 1 + data = length 237, hash E8B787A5 + sample 24: + time = 879918 + flags = 1 + data = length 228, hash 3FA7A29F + sample 25: + time = 903138 + flags = 1 + data = length 235, hash B9B33B0A + sample 26: + time = 926358 + flags = 1 + data = length 264, hash 71A4869E + sample 27: + time = 949578 + flags = 1 + data = length 257, hash D049B54C + sample 28: + time = 972798 + flags = 1 + data = length 227, hash 66757231 + sample 29: + time = 996018 + flags = 1 + data = length 227, hash BD374F1B + sample 30: + time = 1019238 + flags = 1 + data = length 235, hash 999477F6 + sample 31: + time = 1042458 + flags = 1 + data = length 229, hash FFF98DF0 + sample 32: + time = 1065678 + flags = 536870913 + data = length 6, hash 31B22286 +tracksEnded = true diff --git a/testdata/src/test/assets/mp4/sample_mdat_too_long.mp4.2.dump b/testdata/src/test/assets/mp4/sample_mdat_too_long.mp4.2.dump new file mode 100644 index 0000000000..34b0c2099f --- /dev/null +++ b/testdata/src/test/assets/mp4/sample_mdat_too_long.mp4.2.dump @@ -0,0 +1,228 @@ +seekMap: + isSeekable = true + duration = 1024000 + getPosition(0) = [[timeUs=0, position=2192]] + getPosition(1) = [[timeUs=0, position=2192]] + getPosition(512000) = [[timeUs=0, position=2192]] + getPosition(1024000) = [[timeUs=0, position=2192]] +numberOfTracks = 2 +track 0: + total output bytes = 89876 + sample count = 30 + format 0: + id = 1 + sampleMimeType = video/avc + maxInputSize = 36722 + width = 1080 + height = 720 + frameRate = 29.970028 + initializationData: + data = length 29, hash 4746B5D9 + data = length 10, hash 7A0D0F2B + sample 0: + time = 0 + flags = 1 + data = length 36692, hash D216076E + sample 1: + time = 66733 + flags = 0 + data = length 5312, hash D45D3CA0 + sample 2: + time = 33366 + flags = 0 + data = length 599, hash 1BE7812D + sample 3: + time = 200200 + flags = 0 + data = length 7735, hash 4490F110 + sample 4: + time = 133466 + flags = 0 + data = length 987, hash 560B5036 + sample 5: + time = 100100 + flags = 0 + data = length 673, hash ED7CD8C7 + sample 6: + time = 166833 + flags = 0 + data = length 523, hash 3020DF50 + sample 7: + time = 333666 + flags = 0 + data = length 6061, hash 736C72B2 + sample 8: + time = 266933 + flags = 0 + data = length 992, hash FE132F23 + sample 9: + time = 233566 + flags = 0 + data = length 623, hash 5B2C1816 + sample 10: + time = 300300 + flags = 0 + data = length 421, hash 742E69C1 + sample 11: + time = 433766 + flags = 0 + data = length 4899, hash F72F86A1 + sample 12: + time = 400400 + flags = 0 + data = length 568, hash 519A8E50 + sample 13: + time = 367033 + flags = 0 + data = length 620, hash 3990AA39 + sample 14: + time = 567233 + flags = 0 + data = length 5450, hash F06EC4AA + sample 15: + time = 500500 + flags = 0 + data = length 1051, hash 92DFA63A + sample 16: + time = 467133 + flags = 0 + data = length 874, hash 69587FB4 + sample 17: + time = 533866 + flags = 0 + data = length 781, hash 36BE495B + sample 18: + time = 700700 + flags = 0 + data = length 4725, hash AC0C8CD3 + sample 19: + time = 633966 + flags = 0 + data = length 1022, hash 5D8BFF34 + sample 20: + time = 600600 + flags = 0 + data = length 790, hash 99413A99 + sample 21: + time = 667333 + flags = 0 + data = length 610, hash 5E129290 + sample 22: + time = 834166 + flags = 0 + data = length 2751, hash 769974CB + sample 23: + time = 767433 + flags = 0 + data = length 745, hash B78A477A + sample 24: + time = 734066 + flags = 0 + data = length 621, hash CF741E7A + sample 25: + time = 800800 + flags = 0 + data = length 505, hash 1DB4894E + sample 26: + time = 967633 + flags = 0 + data = length 1268, hash C15348DC + sample 27: + time = 900900 + flags = 0 + data = length 880, hash C2DE85D0 + sample 28: + time = 867533 + flags = 0 + data = length 530, hash C98BC6A8 + sample 29: + time = 934266 + flags = 536870912 + data = length 568, hash 4FE5C8EA +track 1: + total output bytes = 4019 + sample count = 18 + format 0: + id = 2 + sampleMimeType = audio/mp4a-latm + codecs = mp4a.40.2 + maxInputSize = 294 + channelCount = 1 + sampleRate = 44100 + language = und + metadata = entries=[TSSE: description=null: value=Lavf56.1.0] + initializationData: + data = length 2, hash 5F7 + sample 0: + time = 670938 + flags = 1 + data = length 243, hash C5E17980 + sample 1: + time = 694158 + flags = 1 + data = length 231, hash AC5837BA + sample 2: + time = 717378 + flags = 1 + data = length 230, hash 169EE895 + sample 3: + time = 740598 + flags = 1 + data = length 238, hash C48FF3F1 + sample 4: + time = 763818 + flags = 1 + data = length 225, hash 531E4599 + sample 5: + time = 787038 + flags = 1 + data = length 232, hash CB3C6B8D + sample 6: + time = 810258 + flags = 1 + data = length 243, hash F8C94C7 + sample 7: + time = 833478 + flags = 1 + data = length 232, hash A646A7D0 + sample 8: + time = 856698 + flags = 1 + data = length 237, hash E8B787A5 + sample 9: + time = 879918 + flags = 1 + data = length 228, hash 3FA7A29F + sample 10: + time = 903138 + flags = 1 + data = length 235, hash B9B33B0A + sample 11: + time = 926358 + flags = 1 + data = length 264, hash 71A4869E + sample 12: + time = 949578 + flags = 1 + data = length 257, hash D049B54C + sample 13: + time = 972798 + flags = 1 + data = length 227, hash 66757231 + sample 14: + time = 996018 + flags = 1 + data = length 227, hash BD374F1B + sample 15: + time = 1019238 + flags = 1 + data = length 235, hash 999477F6 + sample 16: + time = 1042458 + flags = 1 + data = length 229, hash FFF98DF0 + sample 17: + time = 1065678 + flags = 536870913 + data = length 6, hash 31B22286 +tracksEnded = true diff --git a/testdata/src/test/assets/mp4/sample_mdat_too_long.mp4.3.dump b/testdata/src/test/assets/mp4/sample_mdat_too_long.mp4.3.dump new file mode 100644 index 0000000000..c1dc5e9a3f --- /dev/null +++ b/testdata/src/test/assets/mp4/sample_mdat_too_long.mp4.3.dump @@ -0,0 +1,168 @@ +seekMap: + isSeekable = true + duration = 1024000 + getPosition(0) = [[timeUs=0, position=2192]] + getPosition(1) = [[timeUs=0, position=2192]] + getPosition(512000) = [[timeUs=0, position=2192]] + getPosition(1024000) = [[timeUs=0, position=2192]] +numberOfTracks = 2 +track 0: + total output bytes = 89876 + sample count = 30 + format 0: + id = 1 + sampleMimeType = video/avc + maxInputSize = 36722 + width = 1080 + height = 720 + frameRate = 29.970028 + initializationData: + data = length 29, hash 4746B5D9 + data = length 10, hash 7A0D0F2B + sample 0: + time = 0 + flags = 1 + data = length 36692, hash D216076E + sample 1: + time = 66733 + flags = 0 + data = length 5312, hash D45D3CA0 + sample 2: + time = 33366 + flags = 0 + data = length 599, hash 1BE7812D + sample 3: + time = 200200 + flags = 0 + data = length 7735, hash 4490F110 + sample 4: + time = 133466 + flags = 0 + data = length 987, hash 560B5036 + sample 5: + time = 100100 + flags = 0 + data = length 673, hash ED7CD8C7 + sample 6: + time = 166833 + flags = 0 + data = length 523, hash 3020DF50 + sample 7: + time = 333666 + flags = 0 + data = length 6061, hash 736C72B2 + sample 8: + time = 266933 + flags = 0 + data = length 992, hash FE132F23 + sample 9: + time = 233566 + flags = 0 + data = length 623, hash 5B2C1816 + sample 10: + time = 300300 + flags = 0 + data = length 421, hash 742E69C1 + sample 11: + time = 433766 + flags = 0 + data = length 4899, hash F72F86A1 + sample 12: + time = 400400 + flags = 0 + data = length 568, hash 519A8E50 + sample 13: + time = 367033 + flags = 0 + data = length 620, hash 3990AA39 + sample 14: + time = 567233 + flags = 0 + data = length 5450, hash F06EC4AA + sample 15: + time = 500500 + flags = 0 + data = length 1051, hash 92DFA63A + sample 16: + time = 467133 + flags = 0 + data = length 874, hash 69587FB4 + sample 17: + time = 533866 + flags = 0 + data = length 781, hash 36BE495B + sample 18: + time = 700700 + flags = 0 + data = length 4725, hash AC0C8CD3 + sample 19: + time = 633966 + flags = 0 + data = length 1022, hash 5D8BFF34 + sample 20: + time = 600600 + flags = 0 + data = length 790, hash 99413A99 + sample 21: + time = 667333 + flags = 0 + data = length 610, hash 5E129290 + sample 22: + time = 834166 + flags = 0 + data = length 2751, hash 769974CB + sample 23: + time = 767433 + flags = 0 + data = length 745, hash B78A477A + sample 24: + time = 734066 + flags = 0 + data = length 621, hash CF741E7A + sample 25: + time = 800800 + flags = 0 + data = length 505, hash 1DB4894E + sample 26: + time = 967633 + flags = 0 + data = length 1268, hash C15348DC + sample 27: + time = 900900 + flags = 0 + data = length 880, hash C2DE85D0 + sample 28: + time = 867533 + flags = 0 + data = length 530, hash C98BC6A8 + sample 29: + time = 934266 + flags = 536870912 + data = length 568, hash 4FE5C8EA +track 1: + total output bytes = 470 + sample count = 3 + format 0: + id = 2 + sampleMimeType = audio/mp4a-latm + codecs = mp4a.40.2 + maxInputSize = 294 + channelCount = 1 + sampleRate = 44100 + language = und + metadata = entries=[TSSE: description=null: value=Lavf56.1.0] + initializationData: + data = length 2, hash 5F7 + sample 0: + time = 1019238 + flags = 1 + data = length 235, hash 999477F6 + sample 1: + time = 1042458 + flags = 1 + data = length 229, hash FFF98DF0 + sample 2: + time = 1065678 + flags = 536870913 + data = length 6, hash 31B22286 +tracksEnded = true diff --git a/testdata/src/test/assets/mp4/sample_mdat_too_long.mp4.unknown_length.dump b/testdata/src/test/assets/mp4/sample_mdat_too_long.mp4.unknown_length.dump new file mode 100644 index 0000000000..287d52240f --- /dev/null +++ b/testdata/src/test/assets/mp4/sample_mdat_too_long.mp4.unknown_length.dump @@ -0,0 +1,336 @@ +seekMap: + isSeekable = true + duration = 1024000 + getPosition(0) = [[timeUs=0, position=2192]] + getPosition(1) = [[timeUs=0, position=2192]] + getPosition(512000) = [[timeUs=0, position=2192]] + getPosition(1024000) = [[timeUs=0, position=2192]] +numberOfTracks = 2 +track 0: + total output bytes = 89876 + sample count = 30 + format 0: + id = 1 + sampleMimeType = video/avc + maxInputSize = 36722 + width = 1080 + height = 720 + frameRate = 29.970028 + initializationData: + data = length 29, hash 4746B5D9 + data = length 10, hash 7A0D0F2B + sample 0: + time = 0 + flags = 1 + data = length 36692, hash D216076E + sample 1: + time = 66733 + flags = 0 + data = length 5312, hash D45D3CA0 + sample 2: + time = 33366 + flags = 0 + data = length 599, hash 1BE7812D + sample 3: + time = 200200 + flags = 0 + data = length 7735, hash 4490F110 + sample 4: + time = 133466 + flags = 0 + data = length 987, hash 560B5036 + sample 5: + time = 100100 + flags = 0 + data = length 673, hash ED7CD8C7 + sample 6: + time = 166833 + flags = 0 + data = length 523, hash 3020DF50 + sample 7: + time = 333666 + flags = 0 + data = length 6061, hash 736C72B2 + sample 8: + time = 266933 + flags = 0 + data = length 992, hash FE132F23 + sample 9: + time = 233566 + flags = 0 + data = length 623, hash 5B2C1816 + sample 10: + time = 300300 + flags = 0 + data = length 421, hash 742E69C1 + sample 11: + time = 433766 + flags = 0 + data = length 4899, hash F72F86A1 + sample 12: + time = 400400 + flags = 0 + data = length 568, hash 519A8E50 + sample 13: + time = 367033 + flags = 0 + data = length 620, hash 3990AA39 + sample 14: + time = 567233 + flags = 0 + data = length 5450, hash F06EC4AA + sample 15: + time = 500500 + flags = 0 + data = length 1051, hash 92DFA63A + sample 16: + time = 467133 + flags = 0 + data = length 874, hash 69587FB4 + sample 17: + time = 533866 + flags = 0 + data = length 781, hash 36BE495B + sample 18: + time = 700700 + flags = 0 + data = length 4725, hash AC0C8CD3 + sample 19: + time = 633966 + flags = 0 + data = length 1022, hash 5D8BFF34 + sample 20: + time = 600600 + flags = 0 + data = length 790, hash 99413A99 + sample 21: + time = 667333 + flags = 0 + data = length 610, hash 5E129290 + sample 22: + time = 834166 + flags = 0 + data = length 2751, hash 769974CB + sample 23: + time = 767433 + flags = 0 + data = length 745, hash B78A477A + sample 24: + time = 734066 + flags = 0 + data = length 621, hash CF741E7A + sample 25: + time = 800800 + flags = 0 + data = length 505, hash 1DB4894E + sample 26: + time = 967633 + flags = 0 + data = length 1268, hash C15348DC + sample 27: + time = 900900 + flags = 0 + data = length 880, hash C2DE85D0 + sample 28: + time = 867533 + flags = 0 + data = length 530, hash C98BC6A8 + sample 29: + time = 934266 + flags = 536870912 + data = length 568, hash 4FE5C8EA +track 1: + total output bytes = 9529 + sample count = 45 + format 0: + id = 2 + sampleMimeType = audio/mp4a-latm + codecs = mp4a.40.2 + maxInputSize = 294 + channelCount = 1 + sampleRate = 44100 + language = und + metadata = entries=[TSSE: description=null: value=Lavf56.1.0] + initializationData: + data = length 2, hash 5F7 + sample 0: + time = 44000 + flags = 1 + data = length 23, hash 47DE9131 + sample 1: + time = 67219 + flags = 1 + data = length 6, hash 31EC5206 + sample 2: + time = 90439 + flags = 1 + data = length 148, hash 894A176B + sample 3: + time = 113659 + flags = 1 + data = length 189, hash CEF235A1 + sample 4: + time = 136879 + flags = 1 + data = length 205, hash BBF5F7B0 + sample 5: + time = 160099 + flags = 1 + data = length 210, hash F278B193 + sample 6: + time = 183319 + flags = 1 + data = length 210, hash 82DA1589 + sample 7: + time = 206539 + flags = 1 + data = length 207, hash 5BE231DF + sample 8: + time = 229759 + flags = 1 + data = length 225, hash 18819EE1 + sample 9: + time = 252979 + flags = 1 + data = length 215, hash CA7FA67B + sample 10: + time = 276199 + flags = 1 + data = length 211, hash 581A1C18 + sample 11: + time = 299419 + flags = 1 + data = length 216, hash ADB88187 + sample 12: + time = 322639 + flags = 1 + data = length 229, hash 2E8BA4DC + sample 13: + time = 345859 + flags = 1 + data = length 232, hash 22F0C510 + sample 14: + time = 369079 + flags = 1 + data = length 235, hash 867AD0DC + sample 15: + time = 392299 + flags = 1 + data = length 231, hash 84E823A8 + sample 16: + time = 415519 + flags = 1 + data = length 226, hash 1BEF3A95 + sample 17: + time = 438739 + flags = 1 + data = length 216, hash EAA345AE + sample 18: + time = 461959 + flags = 1 + data = length 229, hash 6957411F + sample 19: + time = 485179 + flags = 1 + data = length 219, hash 41275022 + sample 20: + time = 508399 + flags = 1 + data = length 241, hash 6495DF96 + sample 21: + time = 531619 + flags = 1 + data = length 228, hash 63D95906 + sample 22: + time = 554839 + flags = 1 + data = length 238, hash 34F676F9 + sample 23: + time = 578058 + flags = 1 + data = length 234, hash E5CBC045 + sample 24: + time = 601278 + flags = 1 + data = length 231, hash 5FC43661 + sample 25: + time = 624498 + flags = 1 + data = length 217, hash 682708ED + sample 26: + time = 647718 + flags = 1 + data = length 239, hash D43780FC + sample 27: + time = 670938 + flags = 1 + data = length 243, hash C5E17980 + sample 28: + time = 694158 + flags = 1 + data = length 231, hash AC5837BA + sample 29: + time = 717378 + flags = 1 + data = length 230, hash 169EE895 + sample 30: + time = 740598 + flags = 1 + data = length 238, hash C48FF3F1 + sample 31: + time = 763818 + flags = 1 + data = length 225, hash 531E4599 + sample 32: + time = 787038 + flags = 1 + data = length 232, hash CB3C6B8D + sample 33: + time = 810258 + flags = 1 + data = length 243, hash F8C94C7 + sample 34: + time = 833478 + flags = 1 + data = length 232, hash A646A7D0 + sample 35: + time = 856698 + flags = 1 + data = length 237, hash E8B787A5 + sample 36: + time = 879918 + flags = 1 + data = length 228, hash 3FA7A29F + sample 37: + time = 903138 + flags = 1 + data = length 235, hash B9B33B0A + sample 38: + time = 926358 + flags = 1 + data = length 264, hash 71A4869E + sample 39: + time = 949578 + flags = 1 + data = length 257, hash D049B54C + sample 40: + time = 972798 + flags = 1 + data = length 227, hash 66757231 + sample 41: + time = 996018 + flags = 1 + data = length 227, hash BD374F1B + sample 42: + time = 1019238 + flags = 1 + data = length 235, hash 999477F6 + sample 43: + time = 1042458 + flags = 1 + data = length 229, hash FFF98DF0 + sample 44: + time = 1065678 + flags = 536870913 + data = length 6, hash 31B22286 +tracksEnded = true diff --git a/library/core/src/androidTest/assets/mp4/testvid_1022ms.mp4 b/testdata/src/test/assets/mp4/testvid_1022ms.mp4 similarity index 100% rename from library/core/src/androidTest/assets/mp4/testvid_1022ms.mp4 rename to testdata/src/test/assets/mp4/testvid_1022ms.mp4 diff --git a/library/core/src/androidTest/assets/mp4/video000.png b/testdata/src/test/assets/mp4/testvid_1022ms_000.png similarity index 100% rename from library/core/src/androidTest/assets/mp4/video000.png rename to testdata/src/test/assets/mp4/testvid_1022ms_000.png diff --git a/library/core/src/androidTest/assets/mp4/video014.png b/testdata/src/test/assets/mp4/testvid_1022ms_014.png similarity index 100% rename from library/core/src/androidTest/assets/mp4/video014.png rename to testdata/src/test/assets/mp4/testvid_1022ms_014.png diff --git a/library/core/src/androidTest/assets/mp4/video015.png b/testdata/src/test/assets/mp4/testvid_1022ms_015.png similarity index 100% rename from library/core/src/androidTest/assets/mp4/video015.png rename to testdata/src/test/assets/mp4/testvid_1022ms_015.png diff --git a/library/core/src/androidTest/assets/mp4/video016.png b/testdata/src/test/assets/mp4/testvid_1022ms_016.png similarity index 100% rename from library/core/src/androidTest/assets/mp4/video016.png rename to testdata/src/test/assets/mp4/testvid_1022ms_016.png diff --git a/library/core/src/androidTest/assets/mp4/video029.png b/testdata/src/test/assets/mp4/testvid_1022ms_029.png similarity index 100% rename from library/core/src/androidTest/assets/mp4/video029.png rename to testdata/src/test/assets/mp4/testvid_1022ms_029.png diff --git a/library/dash/src/test/assets/sample_mpd b/testdata/src/test/assets/mpd/sample_mpd similarity index 100% rename from library/dash/src/test/assets/sample_mpd rename to testdata/src/test/assets/mpd/sample_mpd diff --git a/testdata/src/test/assets/mpd/sample_mpd_asset_identifier b/testdata/src/test/assets/mpd/sample_mpd_asset_identifier new file mode 100644 index 0000000000..ff5bc874b9 --- /dev/null +++ b/testdata/src/test/assets/mpd/sample_mpd_asset_identifier @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + http://www.dummy.url/ + + + + + + http://www.dummy.url/ + + + + diff --git a/library/dash/src/test/assets/sample_mpd_event_stream b/testdata/src/test/assets/mpd/sample_mpd_event_stream similarity index 100% rename from library/dash/src/test/assets/sample_mpd_event_stream rename to testdata/src/test/assets/mpd/sample_mpd_event_stream diff --git a/library/dash/src/test/assets/sample_mpd_labels b/testdata/src/test/assets/mpd/sample_mpd_labels similarity index 100% rename from library/dash/src/test/assets/sample_mpd_labels rename to testdata/src/test/assets/mpd/sample_mpd_labels diff --git a/library/dash/src/test/assets/sample_mpd_segment_template b/testdata/src/test/assets/mpd/sample_mpd_segment_template similarity index 100% rename from library/dash/src/test/assets/sample_mpd_segment_template rename to testdata/src/test/assets/mpd/sample_mpd_segment_template diff --git a/testdata/src/test/assets/mpd/sample_mpd_text b/testdata/src/test/assets/mpd/sample_mpd_text new file mode 100644 index 0000000000..d3235d5ed0 --- /dev/null +++ b/testdata/src/test/assets/mpd/sample_mpd_text @@ -0,0 +1,25 @@ + + + + + + + + + + + https://test.com/0 + + + + + https://test.com/0 + + + + + https://test.com/0 + + + + diff --git a/testdata/src/test/assets/mpd/sample_mpd_trick_play b/testdata/src/test/assets/mpd/sample_mpd_trick_play new file mode 100644 index 0000000000..b35c906b5f --- /dev/null +++ b/testdata/src/test/assets/mpd/sample_mpd_trick_play @@ -0,0 +1,32 @@ + + + + + + + + + + + https://test.com/0 + + + + + https://test.com/0 + + + + + + https://test.com/0 + + + + + + https://test.com/0 + + + + diff --git a/library/dash/src/test/assets/sample_mpd_unknown_mime_type b/testdata/src/test/assets/mpd/sample_mpd_unknown_mime_type similarity index 100% rename from library/dash/src/test/assets/sample_mpd_unknown_mime_type rename to testdata/src/test/assets/mpd/sample_mpd_unknown_mime_type diff --git a/library/core/src/test/assets/offline/action_file_for_download_index_upgrade.exi b/testdata/src/test/assets/offline/action_file_for_download_index_upgrade.exi similarity index 100% rename from library/core/src/test/assets/offline/action_file_for_download_index_upgrade.exi rename to testdata/src/test/assets/offline/action_file_for_download_index_upgrade.exi diff --git a/library/core/src/test/assets/offline/action_file_incomplete_header.exi b/testdata/src/test/assets/offline/action_file_incomplete_header.exi similarity index 100% rename from library/core/src/test/assets/offline/action_file_incomplete_header.exi rename to testdata/src/test/assets/offline/action_file_incomplete_header.exi diff --git a/library/core/src/test/assets/offline/action_file_no_data.exi b/testdata/src/test/assets/offline/action_file_no_data.exi similarity index 100% rename from library/core/src/test/assets/offline/action_file_no_data.exi rename to testdata/src/test/assets/offline/action_file_no_data.exi diff --git a/library/core/src/test/assets/offline/action_file_one_action.exi b/testdata/src/test/assets/offline/action_file_one_action.exi similarity index 100% rename from library/core/src/test/assets/offline/action_file_one_action.exi rename to testdata/src/test/assets/offline/action_file_one_action.exi diff --git a/library/core/src/test/assets/offline/action_file_two_actions.exi b/testdata/src/test/assets/offline/action_file_two_actions.exi similarity index 100% rename from library/core/src/test/assets/offline/action_file_two_actions.exi rename to testdata/src/test/assets/offline/action_file_two_actions.exi diff --git a/library/core/src/test/assets/offline/action_file_unsupported_version.exi b/testdata/src/test/assets/offline/action_file_unsupported_version.exi similarity index 100% rename from library/core/src/test/assets/offline/action_file_unsupported_version.exi rename to testdata/src/test/assets/offline/action_file_unsupported_version.exi diff --git a/library/core/src/test/assets/offline/action_file_zero_actions.exi b/testdata/src/test/assets/offline/action_file_zero_actions.exi similarity index 100% rename from library/core/src/test/assets/offline/action_file_zero_actions.exi rename to testdata/src/test/assets/offline/action_file_zero_actions.exi diff --git a/library/core/src/test/assets/ogg/bear.opus b/testdata/src/test/assets/ogg/bear.opus similarity index 100% rename from library/core/src/test/assets/ogg/bear.opus rename to testdata/src/test/assets/ogg/bear.opus diff --git a/library/core/src/test/assets/ogg/bear.opus.0.dump b/testdata/src/test/assets/ogg/bear.opus.0.dump similarity index 98% rename from library/core/src/test/assets/ogg/bear.opus.0.dump rename to testdata/src/test/assets/ogg/bear.opus.0.dump index f8eadb16fa..2dccaa69ac 100644 --- a/library/core/src/test/assets/ogg/bear.opus.0.dump +++ b/testdata/src/test/assets/ogg/bear.opus.0.dump @@ -2,34 +2,21 @@ seekMap: isSeekable = true duration = 2747500 getPosition(0) = [[timeUs=0, position=125]] + getPosition(1) = [[timeUs=1, position=125]] + getPosition(1373750) = [[timeUs=1373750, position=125]] + getPosition(2747500) = [[timeUs=2747500, position=125]] numberOfTracks = 1 track 0: - format: - bitrate = -1 - id = null - containerMimeType = null + total output bytes = 25541 + sample count = 275 + format 0: sampleMimeType = audio/opus - maxInputSize = -1 - width = -1 - height = -1 - frameRate = -1.0 - rotationDegrees = 0 - pixelWidthHeightRatio = 1.0 channelCount = 2 sampleRate = 48000 - pcmEncoding = -1 - encoderDelay = 0 - encoderPadding = 0 - subsampleOffsetUs = 9223372036854775807 - selectionFlags = 0 - language = null - drmInitData = - initializationData: data = length 19, hash BFE794DB data = length 8, hash CA22068C data = length 8, hash 79C07075 - total output bytes = 25541 - sample count = 275 sample 0: time = 0 flags = 1 diff --git a/library/core/src/test/assets/ogg/bear.opus.1.dump b/testdata/src/test/assets/ogg/bear.opus.1.dump similarity index 97% rename from library/core/src/test/assets/ogg/bear.opus.1.dump rename to testdata/src/test/assets/ogg/bear.opus.1.dump index 593116a22e..c5786e3492 100644 --- a/library/core/src/test/assets/ogg/bear.opus.1.dump +++ b/testdata/src/test/assets/ogg/bear.opus.1.dump @@ -2,34 +2,21 @@ seekMap: isSeekable = true duration = 2747500 getPosition(0) = [[timeUs=0, position=125]] + getPosition(1) = [[timeUs=1, position=125]] + getPosition(1373750) = [[timeUs=1373750, position=125]] + getPosition(2747500) = [[timeUs=2747500, position=125]] numberOfTracks = 1 track 0: - format: - bitrate = -1 - id = null - containerMimeType = null + total output bytes = 17031 + sample count = 184 + format 0: sampleMimeType = audio/opus - maxInputSize = -1 - width = -1 - height = -1 - frameRate = -1.0 - rotationDegrees = 0 - pixelWidthHeightRatio = 1.0 channelCount = 2 sampleRate = 48000 - pcmEncoding = -1 - encoderDelay = 0 - encoderPadding = 0 - subsampleOffsetUs = 9223372036854775807 - selectionFlags = 0 - language = null - drmInitData = - initializationData: data = length 19, hash BFE794DB data = length 8, hash CA22068C data = length 8, hash 79C07075 - total output bytes = 17031 - sample count = 184 sample 0: time = 910000 flags = 1 diff --git a/library/core/src/test/assets/ogg/bear.opus.2.dump b/testdata/src/test/assets/ogg/bear.opus.2.dump similarity index 95% rename from library/core/src/test/assets/ogg/bear.opus.2.dump rename to testdata/src/test/assets/ogg/bear.opus.2.dump index beabde35c8..14d68f3b8f 100644 --- a/library/core/src/test/assets/ogg/bear.opus.2.dump +++ b/testdata/src/test/assets/ogg/bear.opus.2.dump @@ -2,34 +2,21 @@ seekMap: isSeekable = true duration = 2747500 getPosition(0) = [[timeUs=0, position=125]] + getPosition(1) = [[timeUs=1, position=125]] + getPosition(1373750) = [[timeUs=1373750, position=125]] + getPosition(2747500) = [[timeUs=2747500, position=125]] numberOfTracks = 1 track 0: - format: - bitrate = -1 - id = null - containerMimeType = null + total output bytes = 8698 + sample count = 92 + format 0: sampleMimeType = audio/opus - maxInputSize = -1 - width = -1 - height = -1 - frameRate = -1.0 - rotationDegrees = 0 - pixelWidthHeightRatio = 1.0 channelCount = 2 sampleRate = 48000 - pcmEncoding = -1 - encoderDelay = 0 - encoderPadding = 0 - subsampleOffsetUs = 9223372036854775807 - selectionFlags = 0 - language = null - drmInitData = - initializationData: data = length 19, hash BFE794DB data = length 8, hash CA22068C data = length 8, hash 79C07075 - total output bytes = 8698 - sample count = 92 sample 0: time = 1830000 flags = 1 diff --git a/library/core/src/test/assets/ogg/bear.opus.3.dump b/testdata/src/test/assets/ogg/bear.opus.3.dump similarity index 56% rename from library/core/src/test/assets/ogg/bear.opus.3.dump rename to testdata/src/test/assets/ogg/bear.opus.3.dump index d0f3e2948b..b4d5e824e3 100644 --- a/library/core/src/test/assets/ogg/bear.opus.3.dump +++ b/testdata/src/test/assets/ogg/bear.opus.3.dump @@ -2,34 +2,21 @@ seekMap: isSeekable = true duration = 2747500 getPosition(0) = [[timeUs=0, position=125]] + getPosition(1) = [[timeUs=1, position=125]] + getPosition(1373750) = [[timeUs=1373750, position=125]] + getPosition(2747500) = [[timeUs=2747500, position=125]] numberOfTracks = 1 track 0: - format: - bitrate = -1 - id = null - containerMimeType = null + total output bytes = 126 + sample count = 1 + format 0: sampleMimeType = audio/opus - maxInputSize = -1 - width = -1 - height = -1 - frameRate = -1.0 - rotationDegrees = 0 - pixelWidthHeightRatio = 1.0 channelCount = 2 sampleRate = 48000 - pcmEncoding = -1 - encoderDelay = 0 - encoderPadding = 0 - subsampleOffsetUs = 9223372036854775807 - selectionFlags = 0 - language = null - drmInitData = - initializationData: data = length 19, hash BFE794DB data = length 8, hash CA22068C data = length 8, hash 79C07075 - total output bytes = 126 - sample count = 1 sample 0: time = 2741000 flags = 1 diff --git a/library/core/src/test/assets/ogg/bear.opus.unklen.dump b/testdata/src/test/assets/ogg/bear.opus.unknown_length.dump similarity index 98% rename from library/core/src/test/assets/ogg/bear.opus.unklen.dump rename to testdata/src/test/assets/ogg/bear.opus.unknown_length.dump index ec8f8b8665..aa34535336 100644 --- a/library/core/src/test/assets/ogg/bear.opus.unklen.dump +++ b/testdata/src/test/assets/ogg/bear.opus.unknown_length.dump @@ -4,32 +4,16 @@ seekMap: getPosition(0) = [[timeUs=0, position=0]] numberOfTracks = 1 track 0: - format: - bitrate = -1 - id = null - containerMimeType = null + total output bytes = 25541 + sample count = 275 + format 0: sampleMimeType = audio/opus - maxInputSize = -1 - width = -1 - height = -1 - frameRate = -1.0 - rotationDegrees = 0 - pixelWidthHeightRatio = 1.0 channelCount = 2 sampleRate = 48000 - pcmEncoding = -1 - encoderDelay = 0 - encoderPadding = 0 - subsampleOffsetUs = 9223372036854775807 - selectionFlags = 0 - language = null - drmInitData = - initializationData: data = length 19, hash BFE794DB data = length 8, hash CA22068C data = length 8, hash 79C07075 - total output bytes = 25541 - sample count = 275 sample 0: time = 0 flags = 1 diff --git a/library/core/src/test/assets/ogg/bear_flac.ogg b/testdata/src/test/assets/ogg/bear_flac.ogg similarity index 100% rename from library/core/src/test/assets/ogg/bear_flac.ogg rename to testdata/src/test/assets/ogg/bear_flac.ogg diff --git a/library/core/src/test/assets/ogg/bear_flac.ogg.0.dump b/testdata/src/test/assets/ogg/bear_flac.ogg.0.dump similarity index 89% rename from library/core/src/test/assets/ogg/bear_flac.ogg.0.dump rename to testdata/src/test/assets/ogg/bear_flac.ogg.0.dump index 365040c46c..f303cda1c0 100644 --- a/library/core/src/test/assets/ogg/bear_flac.ogg.0.dump +++ b/testdata/src/test/assets/ogg/bear_flac.ogg.0.dump @@ -2,32 +2,20 @@ seekMap: isSeekable = true duration = 2741000 getPosition(0) = [[timeUs=0, position=8457]] + getPosition(1) = [[timeUs=0, position=8457]] + getPosition(1370500) = [[timeUs=0, position=8457]] + getPosition(2741000) = [[timeUs=0, position=8457]] numberOfTracks = 1 track 0: - format: - bitrate = 1536000 - id = null - containerMimeType = null - sampleMimeType = audio/flac - maxInputSize = 5776 - width = -1 - height = -1 - frameRate = -1.0 - rotationDegrees = 0 - pixelWidthHeightRatio = 1.0 - channelCount = 2 - sampleRate = 48000 - pcmEncoding = -1 - encoderDelay = 0 - encoderPadding = 0 - subsampleOffsetUs = 9223372036854775807 - selectionFlags = 0 - language = null - drmInitData = - - initializationData: - data = length 42, hash 83F6895 total output bytes = 164431 sample count = 33 + format 0: + sampleMimeType = audio/flac + maxInputSize = 5776 + channelCount = 2 + sampleRate = 48000 + initializationData: + data = length 42, hash 83F6895 sample 0: time = 0 flags = 1 diff --git a/library/core/src/test/assets/ogg/bear_flac.ogg.1.dump b/testdata/src/test/assets/ogg/bear_flac.ogg.1.dump similarity index 86% rename from library/core/src/test/assets/ogg/bear_flac.ogg.1.dump rename to testdata/src/test/assets/ogg/bear_flac.ogg.1.dump index ff020b32fd..0d4ab25dbd 100644 --- a/library/core/src/test/assets/ogg/bear_flac.ogg.1.dump +++ b/testdata/src/test/assets/ogg/bear_flac.ogg.1.dump @@ -2,32 +2,20 @@ seekMap: isSeekable = true duration = 2741000 getPosition(0) = [[timeUs=0, position=8457]] + getPosition(1) = [[timeUs=0, position=8457]] + getPosition(1370500) = [[timeUs=0, position=8457]] + getPosition(2741000) = [[timeUs=0, position=8457]] numberOfTracks = 1 track 0: - format: - bitrate = 1536000 - id = null - containerMimeType = null - sampleMimeType = audio/flac - maxInputSize = 5776 - width = -1 - height = -1 - frameRate = -1.0 - rotationDegrees = 0 - pixelWidthHeightRatio = 1.0 - channelCount = 2 - sampleRate = 48000 - pcmEncoding = -1 - encoderDelay = 0 - encoderPadding = 0 - subsampleOffsetUs = 9223372036854775807 - selectionFlags = 0 - language = null - drmInitData = - - initializationData: - data = length 42, hash 83F6895 total output bytes = 113666 sample count = 23 + format 0: + sampleMimeType = audio/flac + maxInputSize = 5776 + channelCount = 2 + sampleRate = 48000 + initializationData: + data = length 42, hash 83F6895 sample 0: time = 853333 flags = 1 diff --git a/library/core/src/test/assets/ogg/bear_flac.ogg.2.dump b/testdata/src/test/assets/ogg/bear_flac.ogg.2.dump similarity index 79% rename from library/core/src/test/assets/ogg/bear_flac.ogg.2.dump rename to testdata/src/test/assets/ogg/bear_flac.ogg.2.dump index 88deeaebd3..9a03aa3292 100644 --- a/library/core/src/test/assets/ogg/bear_flac.ogg.2.dump +++ b/testdata/src/test/assets/ogg/bear_flac.ogg.2.dump @@ -2,32 +2,20 @@ seekMap: isSeekable = true duration = 2741000 getPosition(0) = [[timeUs=0, position=8457]] + getPosition(1) = [[timeUs=0, position=8457]] + getPosition(1370500) = [[timeUs=0, position=8457]] + getPosition(2741000) = [[timeUs=0, position=8457]] numberOfTracks = 1 track 0: - format: - bitrate = 1536000 - id = null - containerMimeType = null - sampleMimeType = audio/flac - maxInputSize = 5776 - width = -1 - height = -1 - frameRate = -1.0 - rotationDegrees = 0 - pixelWidthHeightRatio = 1.0 - channelCount = 2 - sampleRate = 48000 - pcmEncoding = -1 - encoderDelay = 0 - encoderPadding = 0 - subsampleOffsetUs = 9223372036854775807 - selectionFlags = 0 - language = null - drmInitData = - - initializationData: - data = length 42, hash 83F6895 total output bytes = 55652 sample count = 12 + format 0: + sampleMimeType = audio/flac + maxInputSize = 5776 + channelCount = 2 + sampleRate = 48000 + initializationData: + data = length 42, hash 83F6895 sample 0: time = 1792000 flags = 1 diff --git a/library/core/src/test/assets/ogg/bear_flac.ogg.3.dump b/testdata/src/test/assets/ogg/bear_flac.ogg.3.dump similarity index 54% rename from library/core/src/test/assets/ogg/bear_flac.ogg.3.dump rename to testdata/src/test/assets/ogg/bear_flac.ogg.3.dump index 2eb7be2454..ed1ca0357a 100644 --- a/library/core/src/test/assets/ogg/bear_flac.ogg.3.dump +++ b/testdata/src/test/assets/ogg/bear_flac.ogg.3.dump @@ -2,32 +2,20 @@ seekMap: isSeekable = true duration = 2741000 getPosition(0) = [[timeUs=0, position=8457]] + getPosition(1) = [[timeUs=0, position=8457]] + getPosition(1370500) = [[timeUs=0, position=8457]] + getPosition(2741000) = [[timeUs=0, position=8457]] numberOfTracks = 1 track 0: - format: - bitrate = 1536000 - id = null - containerMimeType = null - sampleMimeType = audio/flac - maxInputSize = 5776 - width = -1 - height = -1 - frameRate = -1.0 - rotationDegrees = 0 - pixelWidthHeightRatio = 1.0 - channelCount = 2 - sampleRate = 48000 - pcmEncoding = -1 - encoderDelay = 0 - encoderPadding = 0 - subsampleOffsetUs = 9223372036854775807 - selectionFlags = 0 - language = null - drmInitData = - - initializationData: - data = length 42, hash 83F6895 total output bytes = 445 sample count = 1 + format 0: + sampleMimeType = audio/flac + maxInputSize = 5776 + channelCount = 2 + sampleRate = 48000 + initializationData: + data = length 42, hash 83F6895 sample 0: time = 2730666 flags = 1 diff --git a/library/core/src/test/assets/ogg/bear_flac.ogg.unklen.dump b/testdata/src/test/assets/ogg/bear_flac.ogg.unknown_length.dump similarity index 89% rename from library/core/src/test/assets/ogg/bear_flac.ogg.unklen.dump rename to testdata/src/test/assets/ogg/bear_flac.ogg.unknown_length.dump index 365040c46c..f303cda1c0 100644 --- a/library/core/src/test/assets/ogg/bear_flac.ogg.unklen.dump +++ b/testdata/src/test/assets/ogg/bear_flac.ogg.unknown_length.dump @@ -2,32 +2,20 @@ seekMap: isSeekable = true duration = 2741000 getPosition(0) = [[timeUs=0, position=8457]] + getPosition(1) = [[timeUs=0, position=8457]] + getPosition(1370500) = [[timeUs=0, position=8457]] + getPosition(2741000) = [[timeUs=0, position=8457]] numberOfTracks = 1 track 0: - format: - bitrate = 1536000 - id = null - containerMimeType = null - sampleMimeType = audio/flac - maxInputSize = 5776 - width = -1 - height = -1 - frameRate = -1.0 - rotationDegrees = 0 - pixelWidthHeightRatio = 1.0 - channelCount = 2 - sampleRate = 48000 - pcmEncoding = -1 - encoderDelay = 0 - encoderPadding = 0 - subsampleOffsetUs = 9223372036854775807 - selectionFlags = 0 - language = null - drmInitData = - - initializationData: - data = length 42, hash 83F6895 total output bytes = 164431 sample count = 33 + format 0: + sampleMimeType = audio/flac + maxInputSize = 5776 + channelCount = 2 + sampleRate = 48000 + initializationData: + data = length 42, hash 83F6895 sample 0: time = 0 flags = 1 diff --git a/library/core/src/test/assets/ogg/bear_flac_noseektable.ogg b/testdata/src/test/assets/ogg/bear_flac_noseektable.ogg similarity index 100% rename from library/core/src/test/assets/ogg/bear_flac_noseektable.ogg rename to testdata/src/test/assets/ogg/bear_flac_noseektable.ogg diff --git a/library/core/src/test/assets/ogg/bear_flac_noseektable.ogg.0.dump b/testdata/src/test/assets/ogg/bear_flac_noseektable.ogg.0.dump similarity index 89% rename from library/core/src/test/assets/ogg/bear_flac_noseektable.ogg.0.dump rename to testdata/src/test/assets/ogg/bear_flac_noseektable.ogg.0.dump index c07b2f3844..101b6db26a 100644 --- a/library/core/src/test/assets/ogg/bear_flac_noseektable.ogg.0.dump +++ b/testdata/src/test/assets/ogg/bear_flac_noseektable.ogg.0.dump @@ -2,32 +2,20 @@ seekMap: isSeekable = true duration = 2741000 getPosition(0) = [[timeUs=0, position=8407]] + getPosition(1) = [[timeUs=1, position=8407]] + getPosition(1370500) = [[timeUs=1370500, position=61076]] + getPosition(2741000) = [[timeUs=2741000, position=143746]] numberOfTracks = 1 track 0: - format: - bitrate = 1536000 - id = null - containerMimeType = null - sampleMimeType = audio/flac - maxInputSize = 5776 - width = -1 - height = -1 - frameRate = -1.0 - rotationDegrees = 0 - pixelWidthHeightRatio = 1.0 - channelCount = 2 - sampleRate = 48000 - pcmEncoding = -1 - encoderDelay = 0 - encoderPadding = 0 - subsampleOffsetUs = 9223372036854775807 - selectionFlags = 0 - language = null - drmInitData = - - initializationData: - data = length 42, hash 83F6895 total output bytes = 164431 sample count = 33 + format 0: + sampleMimeType = audio/flac + maxInputSize = 5776 + channelCount = 2 + sampleRate = 48000 + initializationData: + data = length 42, hash 83F6895 sample 0: time = 0 flags = 1 diff --git a/library/core/src/test/assets/ogg/bear_flac_noseektable.ogg.1.dump b/testdata/src/test/assets/ogg/bear_flac_noseektable.ogg.1.dump similarity index 86% rename from library/core/src/test/assets/ogg/bear_flac_noseektable.ogg.1.dump rename to testdata/src/test/assets/ogg/bear_flac_noseektable.ogg.1.dump index a7fce3c901..f90ed83365 100644 --- a/library/core/src/test/assets/ogg/bear_flac_noseektable.ogg.1.dump +++ b/testdata/src/test/assets/ogg/bear_flac_noseektable.ogg.1.dump @@ -2,32 +2,20 @@ seekMap: isSeekable = true duration = 2741000 getPosition(0) = [[timeUs=0, position=8407]] + getPosition(1) = [[timeUs=1, position=8407]] + getPosition(1370500) = [[timeUs=1370500, position=61076]] + getPosition(2741000) = [[timeUs=2741000, position=143746]] numberOfTracks = 1 track 0: - format: - bitrate = 1536000 - id = null - containerMimeType = null - sampleMimeType = audio/flac - maxInputSize = 5776 - width = -1 - height = -1 - frameRate = -1.0 - rotationDegrees = 0 - pixelWidthHeightRatio = 1.0 - channelCount = 2 - sampleRate = 48000 - pcmEncoding = -1 - encoderDelay = 0 - encoderPadding = 0 - subsampleOffsetUs = 9223372036854775807 - selectionFlags = 0 - language = null - drmInitData = - - initializationData: - data = length 42, hash 83F6895 total output bytes = 113666 sample count = 23 + format 0: + sampleMimeType = audio/flac + maxInputSize = 5776 + channelCount = 2 + sampleRate = 48000 + initializationData: + data = length 42, hash 83F6895 sample 0: time = 853333 flags = 1 diff --git a/library/core/src/test/assets/ogg/bear_flac_noseektable.ogg.2.dump b/testdata/src/test/assets/ogg/bear_flac_noseektable.ogg.2.dump similarity index 79% rename from library/core/src/test/assets/ogg/bear_flac_noseektable.ogg.2.dump rename to testdata/src/test/assets/ogg/bear_flac_noseektable.ogg.2.dump index d05d36bd1e..3bef927e79 100644 --- a/library/core/src/test/assets/ogg/bear_flac_noseektable.ogg.2.dump +++ b/testdata/src/test/assets/ogg/bear_flac_noseektable.ogg.2.dump @@ -2,32 +2,20 @@ seekMap: isSeekable = true duration = 2741000 getPosition(0) = [[timeUs=0, position=8407]] + getPosition(1) = [[timeUs=1, position=8407]] + getPosition(1370500) = [[timeUs=1370500, position=61076]] + getPosition(2741000) = [[timeUs=2741000, position=143746]] numberOfTracks = 1 track 0: - format: - bitrate = 1536000 - id = null - containerMimeType = null - sampleMimeType = audio/flac - maxInputSize = 5776 - width = -1 - height = -1 - frameRate = -1.0 - rotationDegrees = 0 - pixelWidthHeightRatio = 1.0 - channelCount = 2 - sampleRate = 48000 - pcmEncoding = -1 - encoderDelay = 0 - encoderPadding = 0 - subsampleOffsetUs = 9223372036854775807 - selectionFlags = 0 - language = null - drmInitData = - - initializationData: - data = length 42, hash 83F6895 total output bytes = 55652 sample count = 12 + format 0: + sampleMimeType = audio/flac + maxInputSize = 5776 + channelCount = 2 + sampleRate = 48000 + initializationData: + data = length 42, hash 83F6895 sample 0: time = 1792000 flags = 1 diff --git a/library/core/src/test/assets/ogg/bear_flac_noseektable.ogg.3.dump b/testdata/src/test/assets/ogg/bear_flac_noseektable.ogg.3.dump similarity index 54% rename from library/core/src/test/assets/ogg/bear_flac_noseektable.ogg.3.dump rename to testdata/src/test/assets/ogg/bear_flac_noseektable.ogg.3.dump index 376cb68499..1916e6bb84 100644 --- a/library/core/src/test/assets/ogg/bear_flac_noseektable.ogg.3.dump +++ b/testdata/src/test/assets/ogg/bear_flac_noseektable.ogg.3.dump @@ -2,32 +2,20 @@ seekMap: isSeekable = true duration = 2741000 getPosition(0) = [[timeUs=0, position=8407]] + getPosition(1) = [[timeUs=1, position=8407]] + getPosition(1370500) = [[timeUs=1370500, position=61076]] + getPosition(2741000) = [[timeUs=2741000, position=143746]] numberOfTracks = 1 track 0: - format: - bitrate = 1536000 - id = null - containerMimeType = null - sampleMimeType = audio/flac - maxInputSize = 5776 - width = -1 - height = -1 - frameRate = -1.0 - rotationDegrees = 0 - pixelWidthHeightRatio = 1.0 - channelCount = 2 - sampleRate = 48000 - pcmEncoding = -1 - encoderDelay = 0 - encoderPadding = 0 - subsampleOffsetUs = 9223372036854775807 - selectionFlags = 0 - language = null - drmInitData = - - initializationData: - data = length 42, hash 83F6895 total output bytes = 445 sample count = 1 + format 0: + sampleMimeType = audio/flac + maxInputSize = 5776 + channelCount = 2 + sampleRate = 48000 + initializationData: + data = length 42, hash 83F6895 sample 0: time = 2730666 flags = 1 diff --git a/library/core/src/test/assets/ogg/bear_flac_noseektable.ogg.unklen.dump b/testdata/src/test/assets/ogg/bear_flac_noseektable.ogg.unknown_length.dump similarity index 89% rename from library/core/src/test/assets/ogg/bear_flac_noseektable.ogg.unklen.dump rename to testdata/src/test/assets/ogg/bear_flac_noseektable.ogg.unknown_length.dump index 44a93a6037..bc49c37893 100644 --- a/library/core/src/test/assets/ogg/bear_flac_noseektable.ogg.unklen.dump +++ b/testdata/src/test/assets/ogg/bear_flac_noseektable.ogg.unknown_length.dump @@ -4,30 +4,15 @@ seekMap: getPosition(0) = [[timeUs=0, position=0]] numberOfTracks = 1 track 0: - format: - bitrate = 1536000 - id = null - containerMimeType = null - sampleMimeType = audio/flac - maxInputSize = 5776 - width = -1 - height = -1 - frameRate = -1.0 - rotationDegrees = 0 - pixelWidthHeightRatio = 1.0 - channelCount = 2 - sampleRate = 48000 - pcmEncoding = -1 - encoderDelay = 0 - encoderPadding = 0 - subsampleOffsetUs = 9223372036854775807 - selectionFlags = 0 - language = null - drmInitData = - - initializationData: - data = length 42, hash 83F6895 total output bytes = 164431 sample count = 33 + format 0: + sampleMimeType = audio/flac + maxInputSize = 5776 + channelCount = 2 + sampleRate = 48000 + initializationData: + data = length 42, hash 83F6895 sample 0: time = 0 flags = 1 diff --git a/library/core/src/test/assets/ogg/bear_vorbis.ogg b/testdata/src/test/assets/ogg/bear_vorbis.ogg similarity index 100% rename from library/core/src/test/assets/ogg/bear_vorbis.ogg rename to testdata/src/test/assets/ogg/bear_vorbis.ogg diff --git a/library/core/src/test/assets/ogg/bear_vorbis.ogg.0.dump b/testdata/src/test/assets/ogg/bear_vorbis.ogg.0.dump similarity index 97% rename from library/core/src/test/assets/ogg/bear_vorbis.ogg.0.dump rename to testdata/src/test/assets/ogg/bear_vorbis.ogg.0.dump index 138e13c54d..7c00eb044d 100644 --- a/library/core/src/test/assets/ogg/bear_vorbis.ogg.0.dump +++ b/testdata/src/test/assets/ogg/bear_vorbis.ogg.0.dump @@ -2,33 +2,21 @@ seekMap: isSeekable = true duration = 2741000 getPosition(0) = [[timeUs=0, position=3995]] + getPosition(1) = [[timeUs=1, position=3995]] + getPosition(1370500) = [[timeUs=1370500, position=3995]] + getPosition(2741000) = [[timeUs=2741000, position=3995]] numberOfTracks = 1 track 0: - format: - bitrate = 112000 - id = null - containerMimeType = null + total output bytes = 26873 + sample count = 180 + format 0: + averageBitrate = 112000 sampleMimeType = audio/vorbis - maxInputSize = -1 - width = -1 - height = -1 - frameRate = -1.0 - rotationDegrees = 0 - pixelWidthHeightRatio = 1.0 channelCount = 2 sampleRate = 48000 - pcmEncoding = -1 - encoderDelay = 0 - encoderPadding = 0 - subsampleOffsetUs = 9223372036854775807 - selectionFlags = 0 - language = null - drmInitData = - initializationData: data = length 30, hash 9A8FF207 data = length 3832, hash 8A406249 - total output bytes = 26873 - sample count = 180 sample 0: time = 0 flags = 1 diff --git a/library/core/src/test/assets/ogg/bear_vorbis.ogg.1.dump b/testdata/src/test/assets/ogg/bear_vorbis.ogg.1.dump similarity index 96% rename from library/core/src/test/assets/ogg/bear_vorbis.ogg.1.dump rename to testdata/src/test/assets/ogg/bear_vorbis.ogg.1.dump index 6b37dfb6cf..2faeecb70e 100644 --- a/library/core/src/test/assets/ogg/bear_vorbis.ogg.1.dump +++ b/testdata/src/test/assets/ogg/bear_vorbis.ogg.1.dump @@ -2,33 +2,21 @@ seekMap: isSeekable = true duration = 2741000 getPosition(0) = [[timeUs=0, position=3995]] + getPosition(1) = [[timeUs=1, position=3995]] + getPosition(1370500) = [[timeUs=1370500, position=3995]] + getPosition(2741000) = [[timeUs=2741000, position=3995]] numberOfTracks = 1 track 0: - format: - bitrate = 112000 - id = null - containerMimeType = null + total output bytes = 17598 + sample count = 109 + format 0: + averageBitrate = 112000 sampleMimeType = audio/vorbis - maxInputSize = -1 - width = -1 - height = -1 - frameRate = -1.0 - rotationDegrees = 0 - pixelWidthHeightRatio = 1.0 channelCount = 2 sampleRate = 48000 - pcmEncoding = -1 - encoderDelay = 0 - encoderPadding = 0 - subsampleOffsetUs = 9223372036854775807 - selectionFlags = 0 - language = null - drmInitData = - initializationData: data = length 30, hash 9A8FF207 data = length 3832, hash 8A406249 - total output bytes = 17598 - sample count = 109 sample 0: time = 896000 flags = 1 diff --git a/library/core/src/test/assets/ogg/bear_vorbis.ogg.2.dump b/testdata/src/test/assets/ogg/bear_vorbis.ogg.2.dump similarity index 92% rename from library/core/src/test/assets/ogg/bear_vorbis.ogg.2.dump rename to testdata/src/test/assets/ogg/bear_vorbis.ogg.2.dump index 9620979357..3536830154 100644 --- a/library/core/src/test/assets/ogg/bear_vorbis.ogg.2.dump +++ b/testdata/src/test/assets/ogg/bear_vorbis.ogg.2.dump @@ -2,33 +2,21 @@ seekMap: isSeekable = true duration = 2741000 getPosition(0) = [[timeUs=0, position=3995]] + getPosition(1) = [[timeUs=1, position=3995]] + getPosition(1370500) = [[timeUs=1370500, position=3995]] + getPosition(2741000) = [[timeUs=2741000, position=3995]] numberOfTracks = 1 track 0: - format: - bitrate = 112000 - id = null - containerMimeType = null + total output bytes = 8658 + sample count = 49 + format 0: + averageBitrate = 112000 sampleMimeType = audio/vorbis - maxInputSize = -1 - width = -1 - height = -1 - frameRate = -1.0 - rotationDegrees = 0 - pixelWidthHeightRatio = 1.0 channelCount = 2 sampleRate = 48000 - pcmEncoding = -1 - encoderDelay = 0 - encoderPadding = 0 - subsampleOffsetUs = 9223372036854775807 - selectionFlags = 0 - language = null - drmInitData = - initializationData: data = length 30, hash 9A8FF207 data = length 3832, hash 8A406249 - total output bytes = 8658 - sample count = 49 sample 0: time = 1821333 flags = 1 diff --git a/testdata/src/test/assets/ogg/bear_vorbis.ogg.3.dump b/testdata/src/test/assets/ogg/bear_vorbis.ogg.3.dump new file mode 100644 index 0000000000..1975852f49 --- /dev/null +++ b/testdata/src/test/assets/ogg/bear_vorbis.ogg.3.dump @@ -0,0 +1,20 @@ +seekMap: + isSeekable = true + duration = 2741000 + getPosition(0) = [[timeUs=0, position=3995]] + getPosition(1) = [[timeUs=1, position=3995]] + getPosition(1370500) = [[timeUs=1370500, position=3995]] + getPosition(2741000) = [[timeUs=2741000, position=3995]] +numberOfTracks = 1 +track 0: + total output bytes = 0 + sample count = 0 + format 0: + averageBitrate = 112000 + sampleMimeType = audio/vorbis + channelCount = 2 + sampleRate = 48000 + initializationData: + data = length 30, hash 9A8FF207 + data = length 3832, hash 8A406249 +tracksEnded = true diff --git a/library/core/src/test/assets/ogg/bear_vorbis.ogg.unklen.dump b/testdata/src/test/assets/ogg/bear_vorbis.ogg.unknown_length.dump similarity index 97% rename from library/core/src/test/assets/ogg/bear_vorbis.ogg.unklen.dump rename to testdata/src/test/assets/ogg/bear_vorbis.ogg.unknown_length.dump index 2686f740db..9830a08357 100644 --- a/library/core/src/test/assets/ogg/bear_vorbis.ogg.unklen.dump +++ b/testdata/src/test/assets/ogg/bear_vorbis.ogg.unknown_length.dump @@ -4,31 +4,16 @@ seekMap: getPosition(0) = [[timeUs=0, position=0]] numberOfTracks = 1 track 0: - format: - bitrate = 112000 - id = null - containerMimeType = null + total output bytes = 26873 + sample count = 180 + format 0: + averageBitrate = 112000 sampleMimeType = audio/vorbis - maxInputSize = -1 - width = -1 - height = -1 - frameRate = -1.0 - rotationDegrees = 0 - pixelWidthHeightRatio = 1.0 channelCount = 2 sampleRate = 48000 - pcmEncoding = -1 - encoderDelay = 0 - encoderPadding = 0 - subsampleOffsetUs = 9223372036854775807 - selectionFlags = 0 - language = null - drmInitData = - initializationData: data = length 30, hash 9A8FF207 data = length 3832, hash 8A406249 - total output bytes = 26873 - sample count = 180 sample 0: time = 0 flags = 1 diff --git a/testdata/src/test/assets/ogg/continued_packet_at_start b/testdata/src/test/assets/ogg/continued_packet_at_start new file mode 100644 index 0000000000..ed26aa991f Binary files /dev/null and b/testdata/src/test/assets/ogg/continued_packet_at_start differ diff --git a/testdata/src/test/assets/ogg/continued_packet_over_four_pages b/testdata/src/test/assets/ogg/continued_packet_over_four_pages new file mode 100644 index 0000000000..c5d407a978 Binary files /dev/null and b/testdata/src/test/assets/ogg/continued_packet_over_four_pages differ diff --git a/testdata/src/test/assets/ogg/continued_packet_over_two_pages b/testdata/src/test/assets/ogg/continued_packet_over_two_pages new file mode 100644 index 0000000000..176a7e8401 Binary files /dev/null and b/testdata/src/test/assets/ogg/continued_packet_over_two_pages differ diff --git a/testdata/src/test/assets/ogg/eof_header b/testdata/src/test/assets/ogg/eof_header new file mode 100644 index 0000000000..6b12292e66 Binary files /dev/null and b/testdata/src/test/assets/ogg/eof_header differ diff --git a/testdata/src/test/assets/ogg/flac_header b/testdata/src/test/assets/ogg/flac_header new file mode 100644 index 0000000000..35082319c2 Binary files /dev/null and b/testdata/src/test/assets/ogg/flac_header differ diff --git a/testdata/src/test/assets/ogg/four_packets_with_empty_page b/testdata/src/test/assets/ogg/four_packets_with_empty_page new file mode 100644 index 0000000000..2b991c3aa8 Binary files /dev/null and b/testdata/src/test/assets/ogg/four_packets_with_empty_page differ diff --git a/testdata/src/test/assets/ogg/invalid_header b/testdata/src/test/assets/ogg/invalid_header new file mode 100644 index 0000000000..071e04d058 Binary files /dev/null and b/testdata/src/test/assets/ogg/invalid_header differ diff --git a/testdata/src/test/assets/ogg/invalid_ogg_header b/testdata/src/test/assets/ogg/invalid_ogg_header new file mode 100644 index 0000000000..ebafb5205e Binary files /dev/null and b/testdata/src/test/assets/ogg/invalid_ogg_header differ diff --git a/testdata/src/test/assets/ogg/opus_header b/testdata/src/test/assets/ogg/opus_header new file mode 100644 index 0000000000..071e7cf6bb Binary files /dev/null and b/testdata/src/test/assets/ogg/opus_header differ diff --git a/testdata/src/test/assets/ogg/packet_with_zero_size_terminator b/testdata/src/test/assets/ogg/packet_with_zero_size_terminator new file mode 100644 index 0000000000..d7fbc60810 Binary files /dev/null and b/testdata/src/test/assets/ogg/packet_with_zero_size_terminator differ diff --git a/testdata/src/test/assets/ogg/page_header b/testdata/src/test/assets/ogg/page_header new file mode 100644 index 0000000000..c9688a52f0 Binary files /dev/null and b/testdata/src/test/assets/ogg/page_header differ diff --git a/testdata/src/test/assets/ogg/random_1000_pages b/testdata/src/test/assets/ogg/random_1000_pages new file mode 100644 index 0000000000..ef8552ff3d Binary files /dev/null and b/testdata/src/test/assets/ogg/random_1000_pages differ diff --git a/testdata/src/test/assets/ogg/three_headers b/testdata/src/test/assets/ogg/three_headers new file mode 100644 index 0000000000..d59ff413eb Binary files /dev/null and b/testdata/src/test/assets/ogg/three_headers differ diff --git a/testdata/src/test/assets/ogg/vorbis_header b/testdata/src/test/assets/ogg/vorbis_header new file mode 100644 index 0000000000..bad7fa758a Binary files /dev/null and b/testdata/src/test/assets/ogg/vorbis_header differ diff --git a/testdata/src/test/assets/ogg/zero_sized_packets_at_end_of_stream b/testdata/src/test/assets/ogg/zero_sized_packets_at_end_of_stream new file mode 100644 index 0000000000..e35059a89f Binary files /dev/null and b/testdata/src/test/assets/ogg/zero_sized_packets_at_end_of_stream differ diff --git a/library/core/src/test/assets/rawcc/sample.rawcc b/testdata/src/test/assets/rawcc/sample.rawcc similarity index 100% rename from library/core/src/test/assets/rawcc/sample.rawcc rename to testdata/src/test/assets/rawcc/sample.rawcc diff --git a/library/core/src/test/assets/rawcc/sample.rawcc.0.dump b/testdata/src/test/assets/rawcc/sample.rawcc.0.dump similarity index 96% rename from library/core/src/test/assets/rawcc/sample.rawcc.0.dump rename to testdata/src/test/assets/rawcc/sample.rawcc.0.dump index adeaaf6a37..32c591be32 100644 --- a/library/core/src/test/assets/rawcc/sample.rawcc.0.dump +++ b/testdata/src/test/assets/rawcc/sample.rawcc.0.dump @@ -4,29 +4,11 @@ seekMap: getPosition(0) = [[timeUs=0, position=0]] numberOfTracks = 1 track 0: - format: - bitrate = -1 - id = null - 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 = 978 sample count = 150 + format 0: + sampleMimeType = application/cea-608 + codecs = cea608 sample 0: time = 37657512133 flags = 1 diff --git a/testdata/src/test/assets/rawcc/sample.rawcc.unknown_length.dump b/testdata/src/test/assets/rawcc/sample.rawcc.unknown_length.dump new file mode 100644 index 0000000000..32c591be32 --- /dev/null +++ b/testdata/src/test/assets/rawcc/sample.rawcc.unknown_length.dump @@ -0,0 +1,612 @@ +seekMap: + isSeekable = false + duration = UNSET TIME + getPosition(0) = [[timeUs=0, position=0]] +numberOfTracks = 1 +track 0: + total output bytes = 978 + sample count = 150 + format 0: + sampleMimeType = application/cea-608 + codecs = cea608 + sample 0: + time = 37657512133 + flags = 1 + data = length 3, hash 7363 + sample 1: + time = 37657528822 + flags = 1 + data = length 3, hash 7724 + sample 2: + time = 37657545511 + flags = 1 + data = length 3, hash 766F + sample 3: + time = 37657562177 + flags = 1 + data = length 3, hash 7724 + sample 4: + time = 37657578866 + flags = 1 + data = length 3, hash 767E + sample 5: + time = 37657595555 + flags = 1 + data = length 3, hash 7724 + sample 6: + time = 37657612244 + flags = 1 + data = length 15, hash E4359178 + sample 7: + time = 37657628911 + flags = 1 + data = length 3, hash 7724 + sample 8: + time = 37657645600 + flags = 1 + data = length 12, hash 15EBEB66 + sample 9: + time = 37657662288 + flags = 1 + data = length 3, hash 7724 + sample 10: + time = 37657678977 + flags = 1 + data = length 3, hash 761D + sample 11: + time = 37657695644 + flags = 1 + data = length 3, hash 7724 + sample 12: + time = 37657712333 + flags = 1 + data = length 30, hash E181418F + sample 13: + time = 37657729022 + flags = 1 + data = length 6, hash 36289CE2 + sample 14: + time = 37657745711 + flags = 1 + data = length 12, hash 3C304F5B + sample 15: + time = 37657762377 + flags = 1 + data = length 3, hash 7724 + sample 16: + time = 37657779066 + flags = 1 + data = length 12, hash 88DD8EF6 + sample 17: + time = 37657795755 + flags = 1 + data = length 3, hash 7724 + sample 18: + time = 37657812444 + flags = 1 + data = length 12, hash 8B411833 + sample 19: + time = 37657829111 + flags = 1 + data = length 3, hash 7724 + sample 20: + time = 37657845800 + flags = 1 + data = length 12, hash 742A2DF1 + sample 21: + time = 37657862488 + flags = 1 + data = length 3, hash 7724 + sample 22: + time = 37657879177 + flags = 1 + data = length 12, hash 9A2ECBEE + sample 23: + time = 37657895844 + flags = 1 + data = length 3, hash 7724 + sample 24: + time = 37657912533 + flags = 1 + data = length 12, hash 562688EA + sample 25: + time = 37657929222 + flags = 1 + data = length 3, hash 7724 + sample 26: + time = 37657945911 + flags = 1 + data = length 12, hash ADE4B953 + sample 27: + time = 37657962577 + flags = 1 + data = length 3, hash 7724 + sample 28: + time = 37657979266 + flags = 1 + data = length 12, hash F927E3E5 + sample 29: + time = 37657995955 + flags = 1 + data = length 3, hash 7724 + sample 30: + time = 37658012644 + flags = 1 + data = length 12, hash EA327945 + sample 31: + time = 37658029311 + flags = 1 + data = length 3, hash 7724 + sample 32: + time = 37658046000 + flags = 1 + data = length 12, hash 3E5DA13C + sample 33: + time = 37658062688 + flags = 1 + data = length 3, hash 7724 + sample 34: + time = 37658079377 + flags = 1 + data = length 12, hash BF646AE3 + sample 35: + time = 37658096044 + flags = 1 + data = length 3, hash 7724 + sample 36: + time = 37658112733 + flags = 1 + data = length 12, hash 41E3BA78 + sample 37: + time = 37658129422 + flags = 1 + data = length 3, hash 7724 + sample 38: + time = 37658146111 + flags = 1 + data = length 12, hash A2945EF6 + sample 39: + time = 37658162777 + flags = 1 + data = length 3, hash 7724 + sample 40: + time = 37658179466 + flags = 1 + data = length 12, hash 26735812 + sample 41: + time = 37658196155 + flags = 1 + data = length 3, hash 7724 + sample 42: + time = 37658212844 + flags = 1 + data = length 12, hash DC14D3D8 + sample 43: + time = 37658229511 + flags = 1 + data = length 3, hash 7724 + sample 44: + time = 37658246200 + flags = 1 + data = length 12, hash 882191BE + sample 45: + time = 37658262888 + flags = 1 + data = length 3, hash 7724 + sample 46: + time = 37658279577 + flags = 1 + data = length 12, hash 8B4886B1 + sample 47: + time = 37658296244 + flags = 1 + data = length 3, hash 7724 + sample 48: + time = 37658312933 + flags = 1 + data = length 12, hash 98D98F96 + sample 49: + time = 37658329622 + flags = 1 + data = length 3, hash 7724 + sample 50: + time = 37658346311 + flags = 1 + data = length 30, hash CF8E53E3 + sample 51: + time = 37658362977 + flags = 1 + data = length 6, hash 36289CE2 + sample 52: + time = 37658379666 + flags = 1 + data = length 12, hash F883C9EE + sample 53: + time = 37658396355 + flags = 1 + data = length 3, hash 7724 + sample 54: + time = 37658413044 + flags = 1 + data = length 12, hash 6E6B2B9C + sample 55: + time = 37658429711 + flags = 1 + data = length 3, hash 7724 + sample 56: + time = 37658446400 + flags = 1 + data = length 12, hash B4FE7F08 + sample 57: + time = 37658463088 + flags = 1 + data = length 3, hash 7724 + sample 58: + time = 37658479777 + flags = 1 + data = length 12, hash 5A1EA7C7 + sample 59: + time = 37658496444 + flags = 1 + data = length 3, hash 7724 + sample 60: + time = 37658513133 + flags = 1 + data = length 12, hash 46BD6CC9 + sample 61: + time = 37658529822 + flags = 1 + data = length 3, hash 7724 + sample 62: + time = 37658546511 + flags = 1 + data = length 12, hash 1B1E2554 + sample 63: + time = 37658563177 + flags = 1 + data = length 3, hash 7724 + sample 64: + time = 37658579866 + flags = 1 + data = length 12, hash 91FCC537 + sample 65: + time = 37658596555 + flags = 1 + data = length 3, hash 7724 + sample 66: + time = 37658613244 + flags = 1 + data = length 12, hash A9355E1B + sample 67: + time = 37658629911 + flags = 1 + data = length 3, hash 7724 + sample 68: + time = 37658646600 + flags = 1 + data = length 12, hash 2511F69B + sample 69: + time = 37658663288 + flags = 1 + data = length 3, hash 7724 + sample 70: + time = 37658679977 + flags = 1 + data = length 12, hash 90925736 + sample 71: + time = 37658696644 + flags = 1 + data = length 3, hash 7724 + sample 72: + time = 37658713333 + flags = 1 + data = length 21, hash 431EEE30 + sample 73: + time = 37658730022 + flags = 1 + data = length 3, hash 7724 + sample 74: + time = 37658746711 + flags = 1 + data = length 12, hash 7BDEF631 + sample 75: + time = 37658763377 + flags = 1 + data = length 3, hash 7724 + sample 76: + time = 37658780066 + flags = 1 + data = length 12, hash A2EEF59E + sample 77: + time = 37658796755 + flags = 1 + data = length 3, hash 7724 + sample 78: + time = 37658813444 + flags = 1 + data = length 12, hash BFC6C022 + sample 79: + time = 37658830111 + flags = 1 + data = length 3, hash 7724 + sample 80: + time = 37658846800 + flags = 1 + data = length 12, hash CD4D8FCA + sample 81: + time = 37658863488 + flags = 1 + data = length 3, hash 7724 + sample 82: + time = 37658880177 + flags = 1 + data = length 12, hash 2BDE8EFA + sample 83: + time = 37658896844 + flags = 1 + data = length 3, hash 7724 + sample 84: + time = 37658913533 + flags = 1 + data = length 12, hash 8C858812 + sample 85: + time = 37658930222 + flags = 1 + data = length 3, hash 7724 + sample 86: + time = 37658946911 + flags = 1 + data = length 12, hash DE7D0E31 + sample 87: + time = 37658963577 + flags = 1 + data = length 3, hash 7724 + sample 88: + time = 37658980266 + flags = 1 + data = length 3, hash 7363 + sample 89: + time = 37658996955 + flags = 1 + data = length 3, hash 7724 + sample 90: + time = 37659013644 + flags = 1 + data = length 3, hash 7363 + sample 91: + time = 37659030311 + flags = 1 + data = length 3, hash 7724 + sample 92: + time = 37659047000 + flags = 1 + data = length 3, hash 7363 + sample 93: + time = 37659063688 + flags = 1 + data = length 3, hash 7724 + sample 94: + time = 37659080377 + flags = 1 + data = length 3, hash 7363 + sample 95: + time = 37659097044 + flags = 1 + data = length 3, hash 7724 + sample 96: + time = 37659113733 + flags = 1 + data = length 3, hash 7363 + sample 97: + time = 37659130422 + flags = 1 + data = length 3, hash 7724 + sample 98: + time = 37659147111 + flags = 1 + data = length 3, hash 7363 + sample 99: + time = 37659163777 + flags = 1 + data = length 3, hash 7724 + sample 100: + time = 37659180466 + flags = 1 + data = length 3, hash 7363 + sample 101: + time = 37659197155 + flags = 1 + data = length 3, hash 7724 + sample 102: + time = 37659213844 + flags = 1 + data = length 3, hash 7363 + sample 103: + time = 37659230511 + flags = 1 + data = length 3, hash 7724 + sample 104: + time = 37659247200 + flags = 1 + data = length 3, hash 7363 + sample 105: + time = 37659263888 + flags = 1 + data = length 3, hash 7724 + sample 106: + time = 37659280577 + flags = 1 + data = length 3, hash 7363 + sample 107: + time = 37659297244 + flags = 1 + data = length 3, hash 7724 + sample 108: + time = 37659313933 + flags = 1 + data = length 3, hash 7363 + sample 109: + time = 37659330622 + flags = 1 + data = length 3, hash 7724 + sample 110: + time = 37659347311 + flags = 1 + data = length 3, hash 7363 + sample 111: + time = 37659363977 + flags = 1 + data = length 3, hash 7724 + sample 112: + time = 37659380666 + flags = 1 + data = length 3, hash 7363 + sample 113: + time = 37659397355 + flags = 1 + data = length 3, hash 7724 + sample 114: + time = 37659414044 + flags = 1 + data = length 3, hash 7363 + sample 115: + time = 37659430711 + flags = 1 + data = length 3, hash 7724 + sample 116: + time = 37659447400 + flags = 1 + data = length 3, hash 7363 + sample 117: + time = 37659464088 + flags = 1 + data = length 3, hash 7724 + sample 118: + time = 37659480777 + flags = 1 + data = length 3, hash 7363 + sample 119: + time = 37659497444 + flags = 1 + data = length 3, hash 7724 + sample 120: + time = 37659514133 + flags = 1 + data = length 3, hash 7363 + sample 121: + time = 37659530822 + flags = 1 + data = length 3, hash 7724 + sample 122: + time = 37659547511 + flags = 1 + data = length 3, hash 7363 + sample 123: + time = 37659564177 + flags = 1 + data = length 3, hash 7724 + sample 124: + time = 37659580866 + flags = 1 + data = length 3, hash 7363 + sample 125: + time = 37659597555 + flags = 1 + data = length 3, hash 7724 + sample 126: + time = 37659614244 + flags = 1 + data = length 3, hash 766F + sample 127: + time = 37659630911 + flags = 1 + data = length 3, hash 7724 + sample 128: + time = 37659647600 + flags = 1 + data = length 3, hash 767E + sample 129: + time = 37659664288 + flags = 1 + data = length 3, hash 7724 + sample 130: + time = 37659680977 + flags = 1 + data = length 15, hash 191B585A + sample 131: + time = 37659697644 + flags = 1 + data = length 3, hash 7724 + sample 132: + time = 37659714333 + flags = 1 + data = length 12, hash 15EC5FC5 + sample 133: + time = 37659731022 + flags = 1 + data = length 3, hash 7724 + sample 134: + time = 37659747711 + flags = 1 + data = length 3, hash 76A1 + sample 135: + time = 37659764377 + flags = 1 + data = length 3, hash 7724 + sample 136: + time = 37659781066 + flags = 1 + data = length 30, hash E8012479 + sample 137: + time = 37659797755 + flags = 1 + data = length 6, hash 36289D5E + sample 138: + time = 37659814444 + flags = 1 + data = length 12, hash D32F29F3 + sample 139: + time = 37659831111 + flags = 1 + data = length 3, hash 7724 + sample 140: + time = 37659847800 + flags = 1 + data = length 21, hash 6258623 + sample 141: + time = 37659864488 + flags = 1 + data = length 3, hash 7724 + sample 142: + time = 37659881177 + flags = 1 + data = length 12, hash FE69ABA2 + sample 143: + time = 37659897844 + flags = 1 + data = length 3, hash 7724 + sample 144: + time = 37659914533 + flags = 1 + data = length 12, hash 958D0815 + sample 145: + time = 37659931222 + flags = 1 + data = length 3, hash 7724 + sample 146: + time = 37659947911 + flags = 1 + data = length 12, hash FF57BFD8 + sample 147: + time = 37659964577 + flags = 1 + data = length 3, hash 7724 + sample 148: + time = 37659981266 + flags = 1 + data = length 12, hash 922122E7 + sample 149: + time = 37659997955 + flags = 1 + data = length 3, hash 7724 +tracksEnded = true diff --git a/library/smoothstreaming/src/test/assets/sample_ismc_1 b/testdata/src/test/assets/smooth-streaming/sample_ismc_1 similarity index 100% rename from library/smoothstreaming/src/test/assets/sample_ismc_1 rename to testdata/src/test/assets/smooth-streaming/sample_ismc_1 diff --git a/library/smoothstreaming/src/test/assets/sample_ismc_2 b/testdata/src/test/assets/smooth-streaming/sample_ismc_2 similarity index 100% rename from library/smoothstreaming/src/test/assets/sample_ismc_2 rename to testdata/src/test/assets/smooth-streaming/sample_ismc_2 diff --git a/library/core/src/test/assets/ssa/empty b/testdata/src/test/assets/ssa/empty similarity index 100% rename from library/core/src/test/assets/ssa/empty rename to testdata/src/test/assets/ssa/empty diff --git a/testdata/src/test/assets/ssa/invalid_positioning b/testdata/src/test/assets/ssa/invalid_positioning new file mode 100644 index 0000000000..ade4cce9c4 --- /dev/null +++ b/testdata/src/test/assets/ssa/invalid_positioning @@ -0,0 +1,16 @@ +[Script Info] +Title: SomeTitle +PlayResX: 300 +PlayResY: 200 + +[V4+ Styles] +! Alignment is set to 4 - i.e. middle-left +Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding +Style: Default,Open Sans Semibold,36,&H00FFFFFF,&H000000FF,&H00020713,&H00000000,-1,0,0,0,100,100,0,0,1,1.7,0,4,0,0,28,1 + +[Events] +Format: Layer, Start, End, Style, Name, Text +Dialogue: 0,0:00:00.00,0:00:01.23,Default,Olly,{\pos(-5,50)}First subtitle (negative \pos()). +Dialogue: 0,0:00:02.34,0:00:03.45,Default,Olly,{\move(-5,50,-5,50)}Second subtitle (negative \move()). +Dialogue: 0,0:00:04:56,0:00:08:90,Default,Olly,{\an11}Third subtitle (invalid alignment). +Dialogue: 0,0:00:09:56,0:00:12:90,Default,Olly,\pos(150,100) Fourth subtitle (no braces). diff --git a/library/core/src/test/assets/ssa/invalid_timecodes b/testdata/src/test/assets/ssa/invalid_timecodes similarity index 100% rename from library/core/src/test/assets/ssa/invalid_timecodes rename to testdata/src/test/assets/ssa/invalid_timecodes diff --git a/testdata/src/test/assets/ssa/overlapping_timecodes b/testdata/src/test/assets/ssa/overlapping_timecodes new file mode 100644 index 0000000000..2093a96ac5 --- /dev/null +++ b/testdata/src/test/assets/ssa/overlapping_timecodes @@ -0,0 +1,12 @@ +[Script Info] +Title: SomeTitle + +[Events] +Format: Start, End, Text +Dialogue: 0:00:01.00,0:00:04.23,First subtitle - end overlaps second +Dialogue: 0:00:02.00,0:00:05.23,Second subtitle - beginning overlaps first +Dialogue: 0:00:08.44,0:00:09.44,Fourth subtitle - same timings as fifth +Dialogue: 0:00:06.00,0:00:08.44,Third subtitle - out of order +Dialogue: 0:00:08.44,0:00:09.44,Fifth subtitle - same timings as fourth +Dialogue: 0:00:10.72,0:00:15.65,Sixth subtitle - fully encompasses seventh +Dialogue: 0:00:13.22,0:00:14.22,Seventh subtitle - nested fully inside sixth diff --git a/testdata/src/test/assets/ssa/positioning b/testdata/src/test/assets/ssa/positioning new file mode 100644 index 0000000000..af19fc3724 --- /dev/null +++ b/testdata/src/test/assets/ssa/positioning @@ -0,0 +1,18 @@ +[Script Info] +Title: SomeTitle +PlayResX: 300 +PlayResY: 202 + +[V4+ Styles] +! Alignment is set to 4 - i.e. middle-left +Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding +Style: Default,Open Sans Semibold,36,&H00FFFFFF,&H000000FF,&H00020713,&H00000000,-1,0,0,0,100,100,0,0,1,1.7,0,4,0,0,28,1 + +[Events] +Format: Layer, Start, End, Style, Name, Text +Dialogue: 0,0:00:00.00,0:00:01.23,Default,Olly,{\pos(150,50.5)}First subtitle. +Dialogue: 0,0:00:02.34,0:00:03.45,Default,Olly,Second subtitle{\pos(75,50.5)}. +Dialogue: 0,0:00:04:56,0:00:08:90,Default,Olly,{\pos(150,100)}Third subtitle{\pos(75,101)}, (only last counts). +Dialogue: 0,0:00:09:56,0:00:12:90,Default,Olly,{\move(150,100,150,50.5)}Fourth subtitle. +Dialogue: 0,0:00:13:56,0:00:15:90,Default,Olly,{ \pos( 150, 101 ) }Fifth subtitle {\an2}(alignment override, spaces around pos arguments). +Dialogue: 0,0:00:16:56,0:00:19:90,Default,Olly,{\pos(150,101)\an9}Sixth subtitle (multiple overrides in same braces). diff --git a/testdata/src/test/assets/ssa/positioning_without_playres b/testdata/src/test/assets/ssa/positioning_without_playres new file mode 100644 index 0000000000..75b7967b34 --- /dev/null +++ b/testdata/src/test/assets/ssa/positioning_without_playres @@ -0,0 +1,7 @@ +[Script Info] +Title: SomeTitle +PlayResX: 300 + +[Events] +Format: Layer, Start, End, Style, Name, Text +Dialogue: 0,0:00:00.00,0:00:01.23,Default,Olly,{\pos(150,50)}First subtitle. diff --git a/library/core/src/test/assets/ssa/typical b/testdata/src/test/assets/ssa/typical similarity index 61% rename from library/core/src/test/assets/ssa/typical rename to testdata/src/test/assets/ssa/typical index 4542af1217..3d36503251 100644 --- a/library/core/src/test/assets/ssa/typical +++ b/testdata/src/test/assets/ssa/typical @@ -7,6 +7,6 @@ Style: Default,Open Sans Semibold,36,&H00FFFFFF,&H000000FF,&H00020713,&H00000000 [Events] Format: Layer, Start, End, Style, Name, Text -Dialogue: 0,0:00:00.00,0:00:01.23,Default,Olly,This is the first subtitle{ignored}. -Dialogue: 0,0:00:02.34,0:00:03.45,Default,Olly,This is the second subtitle \nwith a newline \Nand another. -Dialogue: 0,0:00:04:56,0:00:08:90,Default,Olly,This is the third subtitle, with a comma. +Dialogue: 0,0:00:00.00,0:00:01.23,Default ,Olly,This is the first subtitle{ignored}. +Dialogue: 0,0:00:02.34,0:00:03.45,Default ,Olly,This is the second subtitle \nwith a newline \Nand another. +Dialogue: 0,0:00:04:56,0:00:08:90,Default ,Olly,This is the third subtitle, with a comma. diff --git a/library/core/src/test/assets/ssa/typical_dialogue b/testdata/src/test/assets/ssa/typical_dialogue similarity index 100% rename from library/core/src/test/assets/ssa/typical_dialogue rename to testdata/src/test/assets/ssa/typical_dialogue diff --git a/library/core/src/test/assets/ssa/typical_format b/testdata/src/test/assets/ssa/typical_format similarity index 100% rename from library/core/src/test/assets/ssa/typical_format rename to testdata/src/test/assets/ssa/typical_format diff --git a/library/core/src/test/assets/ssa/typical_header b/testdata/src/test/assets/ssa/typical_header similarity index 100% rename from library/core/src/test/assets/ssa/typical_header rename to testdata/src/test/assets/ssa/typical_header diff --git a/library/core/src/test/assets/subrip/empty b/testdata/src/test/assets/subrip/empty similarity index 100% rename from library/core/src/test/assets/subrip/empty rename to testdata/src/test/assets/subrip/empty diff --git a/library/core/src/test/assets/subrip/typical b/testdata/src/test/assets/subrip/typical similarity index 86% rename from library/core/src/test/assets/subrip/typical rename to testdata/src/test/assets/subrip/typical index 1331f75651..cc9a3da871 100644 --- a/library/core/src/test/assets/subrip/typical +++ b/testdata/src/test/assets/subrip/typical @@ -8,5 +8,5 @@ This is the second subtitle. Second subtitle with second line. 3 -00:00:04,567 --> 00:00:08,901 +02:00:04,567 --> 02:00:08,901 This is the third subtitle. diff --git a/library/core/src/test/assets/subrip/typical_extra_blank_line b/testdata/src/test/assets/subrip/typical_extra_blank_line similarity index 86% rename from library/core/src/test/assets/subrip/typical_extra_blank_line rename to testdata/src/test/assets/subrip/typical_extra_blank_line index f5882a1d68..392cb7e91c 100644 --- a/library/core/src/test/assets/subrip/typical_extra_blank_line +++ b/testdata/src/test/assets/subrip/typical_extra_blank_line @@ -9,5 +9,5 @@ This is the second subtitle. Second subtitle with second line. 3 -00:00:04,567 --> 00:00:08,901 +02:00:04,567 --> 02:00:08,901 This is the third subtitle. diff --git a/library/core/src/test/assets/subrip/typical_missing_sequence b/testdata/src/test/assets/subrip/typical_missing_sequence similarity index 86% rename from library/core/src/test/assets/subrip/typical_missing_sequence rename to testdata/src/test/assets/subrip/typical_missing_sequence index 56d49ac63c..e75711d7a8 100644 --- a/library/core/src/test/assets/subrip/typical_missing_sequence +++ b/testdata/src/test/assets/subrip/typical_missing_sequence @@ -7,5 +7,5 @@ This is the second subtitle. Second subtitle with second line. 3 -00:00:04,567 --> 00:00:08,901 +02:00:04,567 --> 02:00:08,901 This is the third subtitle. diff --git a/library/core/src/test/assets/subrip/typical_missing_timecode b/testdata/src/test/assets/subrip/typical_missing_timecode similarity index 74% rename from library/core/src/test/assets/subrip/typical_missing_timecode rename to testdata/src/test/assets/subrip/typical_missing_timecode index cd25ffca3b..bce61a77f9 100644 --- a/library/core/src/test/assets/subrip/typical_missing_timecode +++ b/testdata/src/test/assets/subrip/typical_missing_timecode @@ -7,13 +7,13 @@ This is the second subtitle. Second subtitle with second line. 3 -00:00:04,567 --> 00:00:08,901 +02:00:04,567 --> 02:00:08,901 This is the third subtitle. 4 - --> 00:00:10,901 + --> 02:00:10,901 This is the fourth subtitle. 5 -00:00:12,901 --> +02:00:12,901 --> This is the fifth subtitle. diff --git a/library/core/src/test/assets/subrip/typical_negative_timestamps b/testdata/src/test/assets/subrip/typical_negative_timestamps similarity index 86% rename from library/core/src/test/assets/subrip/typical_negative_timestamps rename to testdata/src/test/assets/subrip/typical_negative_timestamps index 2a47c0993b..1df7bf68e3 100644 --- a/library/core/src/test/assets/subrip/typical_negative_timestamps +++ b/testdata/src/test/assets/subrip/typical_negative_timestamps @@ -8,5 +8,5 @@ This is the second subtitle. Second subtitle with second line. 3 -00:00:04,567 --> 00:00:08,901 +02:00:04,567 --> 02:00:08,901 This is the third subtitle. diff --git a/testdata/src/test/assets/subrip/typical_no_hours_and_millis b/testdata/src/test/assets/subrip/typical_no_hours_and_millis new file mode 100644 index 0000000000..5340fc72c2 --- /dev/null +++ b/testdata/src/test/assets/subrip/typical_no_hours_and_millis @@ -0,0 +1,12 @@ +1 +00:00,000 --> 00:01,234 +This is the first subtitle. + +2 +00:00:02 --> 00:00:03 +This is the second subtitle. +Second subtitle with second line. + +3 +02:00:04,567 --> 02:00:08,901 +This is the third subtitle. diff --git a/library/core/src/test/assets/subrip/typical_unexpected_end b/testdata/src/test/assets/subrip/typical_unexpected_end similarity index 100% rename from library/core/src/test/assets/subrip/typical_unexpected_end rename to testdata/src/test/assets/subrip/typical_unexpected_end diff --git a/library/core/src/test/assets/subrip/typical_with_byte_order_mark b/testdata/src/test/assets/subrip/typical_with_byte_order_mark similarity index 86% rename from library/core/src/test/assets/subrip/typical_with_byte_order_mark rename to testdata/src/test/assets/subrip/typical_with_byte_order_mark index 4f5b32f4d7..050e1c02a6 100644 --- a/library/core/src/test/assets/subrip/typical_with_byte_order_mark +++ b/testdata/src/test/assets/subrip/typical_with_byte_order_mark @@ -8,5 +8,5 @@ This is the second subtitle. Second subtitle with second line. 3 -00:00:04,567 --> 00:00:08,901 +02:00:04,567 --> 02:00:08,901 This is the third subtitle. diff --git a/library/core/src/test/assets/subrip/typical_with_tags b/testdata/src/test/assets/subrip/typical_with_tags similarity index 100% rename from library/core/src/test/assets/subrip/typical_with_tags rename to testdata/src/test/assets/subrip/typical_with_tags diff --git a/library/core/src/test/assets/ts/bbb_2500ms.ts b/testdata/src/test/assets/ts/bbb_2500ms.ts similarity index 100% rename from library/core/src/test/assets/ts/bbb_2500ms.ts rename to testdata/src/test/assets/ts/bbb_2500ms.ts diff --git a/library/core/src/test/assets/ts/elephants_dream.mpg b/testdata/src/test/assets/ts/elephants_dream.mpg similarity index 100% rename from library/core/src/test/assets/ts/elephants_dream.mpg rename to testdata/src/test/assets/ts/elephants_dream.mpg diff --git a/library/core/src/test/assets/ts/sample.ac3 b/testdata/src/test/assets/ts/sample.ac3 similarity index 100% rename from library/core/src/test/assets/ts/sample.ac3 rename to testdata/src/test/assets/ts/sample.ac3 diff --git a/library/core/src/test/assets/ts/sample.ac3.0.dump b/testdata/src/test/assets/ts/sample.ac3.0.dump similarity index 70% rename from library/core/src/test/assets/ts/sample.ac3.0.dump rename to testdata/src/test/assets/ts/sample.ac3.0.dump index a1d29a77dc..3f582caedd 100644 --- a/library/core/src/test/assets/ts/sample.ac3.0.dump +++ b/testdata/src/test/assets/ts/sample.ac3.0.dump @@ -4,29 +4,13 @@ seekMap: getPosition(0) = [[timeUs=0, position=0]] numberOfTracks = 1 track 0: - format: - bitrate = -1 - id = 0 - containerMimeType = null - sampleMimeType = audio/ac3 - maxInputSize = -1 - width = -1 - height = -1 - frameRate = -1.0 - rotationDegrees = 0 - pixelWidthHeightRatio = 1.0 - channelCount = 6 - sampleRate = 48000 - pcmEncoding = -1 - encoderDelay = 0 - encoderPadding = 0 - subsampleOffsetUs = 9223372036854775807 - selectionFlags = 0 - language = null - drmInitData = - - initializationData: total output bytes = 13281 sample count = 8 + format 0: + id = 0 + sampleMimeType = audio/ac3 + channelCount = 6 + sampleRate = 48000 sample 0: time = 0 flags = 1 diff --git a/testdata/src/test/assets/ts/sample.ac3.unknown_length.dump b/testdata/src/test/assets/ts/sample.ac3.unknown_length.dump new file mode 100644 index 0000000000..3f582caedd --- /dev/null +++ b/testdata/src/test/assets/ts/sample.ac3.unknown_length.dump @@ -0,0 +1,46 @@ +seekMap: + isSeekable = false + duration = UNSET TIME + getPosition(0) = [[timeUs=0, position=0]] +numberOfTracks = 1 +track 0: + total output bytes = 13281 + sample count = 8 + format 0: + id = 0 + sampleMimeType = audio/ac3 + channelCount = 6 + sampleRate = 48000 + sample 0: + time = 0 + flags = 1 + data = length 1536, hash 7108D5C2 + sample 1: + time = 32000 + flags = 1 + data = length 1536, hash 80BF3B34 + sample 2: + time = 64000 + flags = 1 + data = length 1536, hash 5D09685 + sample 3: + time = 96000 + flags = 1 + data = length 1536, hash A9A24E44 + sample 4: + time = 128000 + flags = 1 + data = length 1536, hash 6F856273 + sample 5: + time = 160000 + flags = 1 + data = length 1536, hash B1737D3C + sample 6: + time = 192000 + flags = 1 + data = length 1536, hash 98FDEB9D + sample 7: + time = 224000 + flags = 1 + data = length 1536, hash 99B9B943 +tracksEnded = true diff --git a/library/core/src/test/assets/ts/sample.ac4 b/testdata/src/test/assets/ts/sample.ac4 similarity index 100% rename from library/core/src/test/assets/ts/sample.ac4 rename to testdata/src/test/assets/ts/sample.ac4 diff --git a/library/core/src/test/assets/ts/sample.ac4.0.dump b/testdata/src/test/assets/ts/sample.ac4.0.dump similarity index 82% rename from library/core/src/test/assets/ts/sample.ac4.0.dump rename to testdata/src/test/assets/ts/sample.ac4.0.dump index 03ae07707a..3b82b0bd2d 100644 --- a/library/core/src/test/assets/ts/sample.ac4.0.dump +++ b/testdata/src/test/assets/ts/sample.ac4.0.dump @@ -4,29 +4,13 @@ seekMap: getPosition(0) = [[timeUs=0, position=0]] numberOfTracks = 1 track 0: - format: - bitrate = -1 - id = 0 - containerMimeType = null - sampleMimeType = audio/ac4 - maxInputSize = -1 - width = -1 - height = -1 - frameRate = -1.0 - rotationDegrees = 0 - pixelWidthHeightRatio = 1.0 - channelCount = 2 - sampleRate = 48000 - pcmEncoding = -1 - encoderDelay = 0 - encoderPadding = 0 - subsampleOffsetUs = 9223372036854775807 - selectionFlags = 0 - language = null - drmInitData = - - initializationData: total output bytes = 7594 sample count = 19 + format 0: + id = 0 + sampleMimeType = audio/ac4 + channelCount = 2 + sampleRate = 48000 sample 0: time = 0 flags = 1 diff --git a/testdata/src/test/assets/ts/sample.ac4.unknown_length.dump b/testdata/src/test/assets/ts/sample.ac4.unknown_length.dump new file mode 100644 index 0000000000..3b82b0bd2d --- /dev/null +++ b/testdata/src/test/assets/ts/sample.ac4.unknown_length.dump @@ -0,0 +1,90 @@ +seekMap: + isSeekable = false + duration = UNSET TIME + getPosition(0) = [[timeUs=0, position=0]] +numberOfTracks = 1 +track 0: + total output bytes = 7594 + sample count = 19 + format 0: + id = 0 + sampleMimeType = audio/ac4 + channelCount = 2 + sampleRate = 48000 + sample 0: + time = 0 + flags = 1 + data = length 366, hash B4277F9E + sample 1: + time = 40000 + flags = 1 + data = length 366, hash E8E0A142 + sample 2: + time = 80000 + flags = 1 + data = length 366, hash 2E5073D0 + sample 3: + time = 120000 + flags = 1 + data = length 366, hash 850E71D8 + sample 4: + time = 160000 + flags = 1 + data = length 366, hash 69CD444E + sample 5: + time = 200000 + flags = 1 + data = length 366, hash BD24F36D + sample 6: + time = 240000 + flags = 1 + data = length 366, hash E24F2490 + sample 7: + time = 280000 + flags = 1 + data = length 366, hash EE6F1F06 + sample 8: + time = 320000 + flags = 1 + data = length 366, hash 2DAB000F + sample 9: + time = 360000 + flags = 1 + data = length 366, hash 8102B7EC + sample 10: + time = 400000 + flags = 1 + data = length 366, hash 55BF59AC + sample 11: + time = 440000 + flags = 1 + data = length 494, hash CBC2E09F + sample 12: + time = 480000 + flags = 1 + data = length 519, hash 9DAF56E9 + sample 13: + time = 520000 + flags = 1 + data = length 598, hash 8169EE2 + sample 14: + time = 560000 + flags = 1 + data = length 435, hash 28C21246 + sample 15: + time = 600000 + flags = 1 + data = length 365, hash FF14716D + sample 16: + time = 640000 + flags = 1 + data = length 392, hash 4CC96B29 + sample 17: + time = 680000 + flags = 1 + data = length 373, hash D7AC6D4E + sample 18: + time = 720000 + flags = 1 + data = length 392, hash 99F2511F +tracksEnded = true diff --git a/library/core/src/test/assets/ts/sample.adts b/testdata/src/test/assets/ts/sample.adts similarity index 100% rename from library/core/src/test/assets/ts/sample.adts rename to testdata/src/test/assets/ts/sample.adts diff --git a/library/core/src/test/assets/ts/sample.adts.0.dump b/testdata/src/test/assets/ts/sample.adts.0.dump similarity index 94% rename from library/core/src/test/assets/ts/sample.adts.0.dump rename to testdata/src/test/assets/ts/sample.adts.0.dump index 93d7b776c0..ddfea3e99b 100644 --- a/library/core/src/test/assets/ts/sample.adts.0.dump +++ b/testdata/src/test/assets/ts/sample.adts.0.dump @@ -4,30 +4,16 @@ seekMap: 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 + format 0: + id = 0 + sampleMimeType = audio/mp4a-latm + codecs = mp4a.40.2 + channelCount = 1 + sampleRate = 44100 + initializationData: + data = length 2, hash 5F7 sample 0: time = 0 flags = 1 @@ -605,27 +591,9 @@ track 0: 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 + format 0: + id = 1 + sampleMimeType = application/id3 tracksEnded = true diff --git a/library/core/src/test/assets/ts/sample_cbs.adts.unklen.dump b/testdata/src/test/assets/ts/sample.adts.unknown_length.dump similarity index 94% rename from library/core/src/test/assets/ts/sample_cbs.adts.unklen.dump rename to testdata/src/test/assets/ts/sample.adts.unknown_length.dump index 93d7b776c0..ddfea3e99b 100644 --- a/library/core/src/test/assets/ts/sample_cbs.adts.unklen.dump +++ b/testdata/src/test/assets/ts/sample.adts.unknown_length.dump @@ -4,30 +4,16 @@ seekMap: 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 + format 0: + id = 0 + sampleMimeType = audio/mp4a-latm + codecs = mp4a.40.2 + channelCount = 1 + sampleRate = 44100 + initializationData: + data = length 2, hash 5F7 sample 0: time = 0 flags = 1 @@ -605,27 +591,9 @@ track 0: 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 + format 0: + id = 1 + sampleMimeType = application/id3 tracksEnded = true diff --git a/library/core/src/test/assets/ts/sample.eac3 b/testdata/src/test/assets/ts/sample.eac3 similarity index 100% rename from library/core/src/test/assets/ts/sample.eac3 rename to testdata/src/test/assets/ts/sample.eac3 diff --git a/library/core/src/test/assets/ts/sample.eac3.0.dump b/testdata/src/test/assets/ts/sample.eac3.0.dump similarity index 92% rename from library/core/src/test/assets/ts/sample.eac3.0.dump rename to testdata/src/test/assets/ts/sample.eac3.0.dump index b0b2779958..f3d9d3997d 100644 --- a/library/core/src/test/assets/ts/sample.eac3.0.dump +++ b/testdata/src/test/assets/ts/sample.eac3.0.dump @@ -4,29 +4,13 @@ seekMap: getPosition(0) = [[timeUs=0, position=0]] numberOfTracks = 1 track 0: - format: - bitrate = -1 - id = 0 - containerMimeType = null - sampleMimeType = audio/eac3 - maxInputSize = -1 - width = -1 - height = -1 - frameRate = -1.0 - rotationDegrees = 0 - pixelWidthHeightRatio = 1.0 - channelCount = 6 - sampleRate = 48000 - pcmEncoding = -1 - encoderDelay = 0 - encoderPadding = 0 - subsampleOffsetUs = 9223372036854775807 - selectionFlags = 0 - language = null - drmInitData = - - initializationData: total output bytes = 216000 sample count = 54 + format 0: + id = 0 + sampleMimeType = audio/eac3 + channelCount = 6 + sampleRate = 48000 sample 0: time = 0 flags = 1 diff --git a/testdata/src/test/assets/ts/sample.eac3.unknown_length.dump b/testdata/src/test/assets/ts/sample.eac3.unknown_length.dump new file mode 100644 index 0000000000..f3d9d3997d --- /dev/null +++ b/testdata/src/test/assets/ts/sample.eac3.unknown_length.dump @@ -0,0 +1,230 @@ +seekMap: + isSeekable = false + duration = UNSET TIME + getPosition(0) = [[timeUs=0, position=0]] +numberOfTracks = 1 +track 0: + total output bytes = 216000 + sample count = 54 + format 0: + id = 0 + sampleMimeType = audio/eac3 + channelCount = 6 + sampleRate = 48000 + sample 0: + time = 0 + flags = 1 + data = length 4000, hash BAEAFB2A + sample 1: + time = 5333 + flags = 1 + data = length 4000, hash E3C5EBF0 + sample 2: + time = 10666 + flags = 1 + data = length 4000, hash 32E0F957 + sample 3: + time = 15999 + flags = 1 + data = length 4000, hash 5354CC5D + sample 4: + time = 21332 + flags = 1 + data = length 4000, hash FF834906 + sample 5: + time = 26665 + flags = 1 + data = length 4000, hash 6F571E61 + sample 6: + time = 31998 + flags = 1 + data = length 4000, hash 5C931F6B + sample 7: + time = 37331 + flags = 1 + data = length 4000, hash B1FB2E57 + sample 8: + time = 42664 + flags = 1 + data = length 4000, hash C71240EB + sample 9: + time = 47997 + flags = 1 + data = length 4000, hash C3E302EE + sample 10: + time = 53330 + flags = 1 + data = length 4000, hash 7994C27B + sample 11: + time = 58663 + flags = 1 + data = length 4000, hash 1ED4E6F3 + sample 12: + time = 63996 + flags = 1 + data = length 4000, hash 1D5E6AAC + sample 13: + time = 69329 + flags = 1 + data = length 4000, hash 30058F51 + sample 14: + time = 74662 + flags = 1 + data = length 4000, hash 15DD0E4A + sample 15: + time = 79995 + flags = 1 + data = length 4000, hash 37BE7C15 + sample 16: + time = 85328 + flags = 1 + data = length 4000, hash 7CFDD34B + sample 17: + time = 90661 + flags = 1 + data = length 4000, hash 27F20D29 + sample 18: + time = 95994 + flags = 1 + data = length 4000, hash 6F565894 + sample 19: + time = 101327 + flags = 1 + data = length 4000, hash A6F07C4A + sample 20: + time = 106660 + flags = 1 + data = length 4000, hash 3A0CA15C + sample 21: + time = 111993 + flags = 1 + data = length 4000, hash DB365414 + sample 22: + time = 117326 + flags = 1 + data = length 4000, hash 31E08469 + sample 23: + time = 122659 + flags = 1 + data = length 4000, hash 315F5C28 + sample 24: + time = 127992 + flags = 1 + data = length 4000, hash CC65DF80 + sample 25: + time = 133325 + flags = 1 + data = length 4000, hash 503FB64C + sample 26: + time = 138658 + flags = 1 + data = length 4000, hash 817CF735 + sample 27: + time = 143991 + flags = 1 + data = length 4000, hash 37391ADA + sample 28: + time = 149324 + flags = 1 + data = length 4000, hash 37391ADA + sample 29: + time = 154657 + flags = 1 + data = length 4000, hash 64DBF751 + sample 30: + time = 159990 + flags = 1 + data = length 4000, hash 81AE828E + sample 31: + time = 165323 + flags = 1 + data = length 4000, hash 767D6C98 + sample 32: + time = 170656 + flags = 1 + data = length 4000, hash A5F6D4E + sample 33: + time = 175989 + flags = 1 + data = length 4000, hash EABC6B0D + sample 34: + time = 181322 + flags = 1 + data = length 4000, hash F47EF742 + sample 35: + time = 186655 + flags = 1 + data = length 4000, hash 9B2549DA + sample 36: + time = 191988 + flags = 1 + data = length 4000, hash A12733C9 + sample 37: + time = 197321 + flags = 1 + data = length 4000, hash 95F62E99 + sample 38: + time = 202654 + flags = 1 + data = length 4000, hash A4D858 + sample 39: + time = 207987 + flags = 1 + data = length 4000, hash A4D858 + sample 40: + time = 213320 + flags = 1 + data = length 4000, hash 22C1A129 + sample 41: + time = 218653 + flags = 1 + data = length 4000, hash 2C51E4A1 + sample 42: + time = 223986 + flags = 1 + data = length 4000, hash 3782E8BB + sample 43: + time = 229319 + flags = 1 + data = length 4000, hash 2C51E4A1 + sample 44: + time = 234652 + flags = 1 + data = length 4000, hash BDB3D129 + sample 45: + time = 239985 + flags = 1 + data = length 4000, hash F642A55 + sample 46: + time = 245318 + flags = 1 + data = length 4000, hash 32F259F4 + sample 47: + time = 250651 + flags = 1 + data = length 4000, hash 4C987B7C + sample 48: + time = 255984 + flags = 1 + data = length 4000, hash 57C98E1C + sample 49: + time = 261317 + flags = 1 + data = length 4000, hash 4C987B7C + sample 50: + time = 266650 + flags = 1 + data = length 4000, hash 4C987B7C + sample 51: + time = 271983 + flags = 1 + data = length 4000, hash 4C987B7C + sample 52: + time = 277316 + flags = 1 + data = length 4000, hash 4C987B7C + sample 53: + time = 282649 + flags = 1 + data = length 4000, hash 4C987B7C +tracksEnded = true diff --git a/testdata/src/test/assets/ts/sample_ac3.ps b/testdata/src/test/assets/ts/sample_ac3.ps new file mode 100644 index 0000000000..a255996824 Binary files /dev/null and b/testdata/src/test/assets/ts/sample_ac3.ps differ diff --git a/testdata/src/test/assets/ts/sample_ac3.ps.0.dump b/testdata/src/test/assets/ts/sample_ac3.ps.0.dump new file mode 100644 index 0000000000..27d0c450fd --- /dev/null +++ b/testdata/src/test/assets/ts/sample_ac3.ps.0.dump @@ -0,0 +1,29 @@ +seekMap: + isSeekable = true + duration = 0 + getPosition(0) = [[timeUs=0, position=0]] + getPosition(1) = [[timeUs=1, position=0]] + getPosition(0) = [[timeUs=0, position=0]] + getPosition(0) = [[timeUs=0, position=0]] +numberOfTracks = 1 +track 189: + total output bytes = 1252 + sample count = 3 + format 0: + id = 189 + sampleMimeType = audio/ac3 + channelCount = 1 + sampleRate = 44100 + sample 0: + time = 0 + flags = 1 + data = length 416, hash 6B14E268 + sample 1: + time = 34829 + flags = 1 + data = length 418, hash BC27DF0B + sample 2: + time = 69658 + flags = 1 + data = length 418, hash BC27DF0B +tracksEnded = false diff --git a/testdata/src/test/assets/ts/sample_ac3.ps.1.dump b/testdata/src/test/assets/ts/sample_ac3.ps.1.dump new file mode 100644 index 0000000000..27d0c450fd --- /dev/null +++ b/testdata/src/test/assets/ts/sample_ac3.ps.1.dump @@ -0,0 +1,29 @@ +seekMap: + isSeekable = true + duration = 0 + getPosition(0) = [[timeUs=0, position=0]] + getPosition(1) = [[timeUs=1, position=0]] + getPosition(0) = [[timeUs=0, position=0]] + getPosition(0) = [[timeUs=0, position=0]] +numberOfTracks = 1 +track 189: + total output bytes = 1252 + sample count = 3 + format 0: + id = 189 + sampleMimeType = audio/ac3 + channelCount = 1 + sampleRate = 44100 + sample 0: + time = 0 + flags = 1 + data = length 416, hash 6B14E268 + sample 1: + time = 34829 + flags = 1 + data = length 418, hash BC27DF0B + sample 2: + time = 69658 + flags = 1 + data = length 418, hash BC27DF0B +tracksEnded = false diff --git a/testdata/src/test/assets/ts/sample_ac3.ps.2.dump b/testdata/src/test/assets/ts/sample_ac3.ps.2.dump new file mode 100644 index 0000000000..27d0c450fd --- /dev/null +++ b/testdata/src/test/assets/ts/sample_ac3.ps.2.dump @@ -0,0 +1,29 @@ +seekMap: + isSeekable = true + duration = 0 + getPosition(0) = [[timeUs=0, position=0]] + getPosition(1) = [[timeUs=1, position=0]] + getPosition(0) = [[timeUs=0, position=0]] + getPosition(0) = [[timeUs=0, position=0]] +numberOfTracks = 1 +track 189: + total output bytes = 1252 + sample count = 3 + format 0: + id = 189 + sampleMimeType = audio/ac3 + channelCount = 1 + sampleRate = 44100 + sample 0: + time = 0 + flags = 1 + data = length 416, hash 6B14E268 + sample 1: + time = 34829 + flags = 1 + data = length 418, hash BC27DF0B + sample 2: + time = 69658 + flags = 1 + data = length 418, hash BC27DF0B +tracksEnded = false diff --git a/testdata/src/test/assets/ts/sample_ac3.ps.3.dump b/testdata/src/test/assets/ts/sample_ac3.ps.3.dump new file mode 100644 index 0000000000..27d0c450fd --- /dev/null +++ b/testdata/src/test/assets/ts/sample_ac3.ps.3.dump @@ -0,0 +1,29 @@ +seekMap: + isSeekable = true + duration = 0 + getPosition(0) = [[timeUs=0, position=0]] + getPosition(1) = [[timeUs=1, position=0]] + getPosition(0) = [[timeUs=0, position=0]] + getPosition(0) = [[timeUs=0, position=0]] +numberOfTracks = 1 +track 189: + total output bytes = 1252 + sample count = 3 + format 0: + id = 189 + sampleMimeType = audio/ac3 + channelCount = 1 + sampleRate = 44100 + sample 0: + time = 0 + flags = 1 + data = length 416, hash 6B14E268 + sample 1: + time = 34829 + flags = 1 + data = length 418, hash BC27DF0B + sample 2: + time = 69658 + flags = 1 + data = length 418, hash BC27DF0B +tracksEnded = false diff --git a/testdata/src/test/assets/ts/sample_ac3.ps.unknown_length.dump b/testdata/src/test/assets/ts/sample_ac3.ps.unknown_length.dump new file mode 100644 index 0000000000..960882156b --- /dev/null +++ b/testdata/src/test/assets/ts/sample_ac3.ps.unknown_length.dump @@ -0,0 +1,26 @@ +seekMap: + isSeekable = false + duration = UNSET TIME + getPosition(0) = [[timeUs=0, position=0]] +numberOfTracks = 1 +track 189: + total output bytes = 1252 + sample count = 3 + format 0: + id = 189 + sampleMimeType = audio/ac3 + channelCount = 1 + sampleRate = 44100 + sample 0: + time = 0 + flags = 1 + data = length 416, hash 6B14E268 + sample 1: + time = 34829 + flags = 1 + data = length 418, hash BC27DF0B + sample 2: + time = 69658 + flags = 1 + data = length 418, hash BC27DF0B +tracksEnded = false diff --git a/testdata/src/test/assets/ts/sample_ac3.ts b/testdata/src/test/assets/ts/sample_ac3.ts new file mode 100644 index 0000000000..48dc17c215 Binary files /dev/null and b/testdata/src/test/assets/ts/sample_ac3.ts differ diff --git a/testdata/src/test/assets/ts/sample_ac3.ts.0.dump b/testdata/src/test/assets/ts/sample_ac3.ts.0.dump new file mode 100644 index 0000000000..561963e10c --- /dev/null +++ b/testdata/src/test/assets/ts/sample_ac3.ts.0.dump @@ -0,0 +1,49 @@ +seekMap: + isSeekable = true + duration = 252977 + getPosition(0) = [[timeUs=0, position=0]] + getPosition(1) = [[timeUs=1, position=0]] + getPosition(126488) = [[timeUs=126488, position=9099]] + getPosition(252977) = [[timeUs=252977, position=18386]] +numberOfTracks = 1 +track 1900: + total output bytes = 13281 + sample count = 8 + format 0: + id = 1/1900 + sampleMimeType = audio/ac3 + channelCount = 6 + sampleRate = 48000 + sample 0: + time = 32000 + flags = 1 + data = length 1536, hash 7108D5C2 + sample 1: + time = 64000 + flags = 1 + data = length 1536, hash 80BF3B34 + sample 2: + time = 96000 + flags = 1 + data = length 1536, hash 5D09685 + sample 3: + time = 128000 + flags = 1 + data = length 1536, hash A9A24E44 + sample 4: + time = 160000 + flags = 1 + data = length 1536, hash 6F856273 + sample 5: + time = 192000 + flags = 1 + data = length 1536, hash B1737D3C + sample 6: + time = 224000 + flags = 1 + data = length 1536, hash 98FDEB9D + sample 7: + time = 256000 + flags = 1 + data = length 1536, hash 99B9B943 +tracksEnded = true diff --git a/testdata/src/test/assets/ts/sample_ac3.ts.1.dump b/testdata/src/test/assets/ts/sample_ac3.ts.1.dump new file mode 100644 index 0000000000..d778af898d --- /dev/null +++ b/testdata/src/test/assets/ts/sample_ac3.ts.1.dump @@ -0,0 +1,41 @@ +seekMap: + isSeekable = true + duration = 252977 + getPosition(0) = [[timeUs=0, position=0]] + getPosition(1) = [[timeUs=1, position=0]] + getPosition(126488) = [[timeUs=126488, position=9099]] + getPosition(252977) = [[timeUs=252977, position=18386]] +numberOfTracks = 1 +track 1900: + total output bytes = 10209 + sample count = 6 + format 0: + id = 1/1900 + sampleMimeType = audio/ac3 + channelCount = 6 + sampleRate = 48000 + sample 0: + time = 96000 + flags = 1 + data = length 1536, hash 5D09685 + sample 1: + time = 128000 + flags = 1 + data = length 1536, hash A9A24E44 + sample 2: + time = 160000 + flags = 1 + data = length 1536, hash 6F856273 + sample 3: + time = 192000 + flags = 1 + data = length 1536, hash B1737D3C + sample 4: + time = 224000 + flags = 1 + data = length 1536, hash 98FDEB9D + sample 5: + time = 256000 + flags = 1 + data = length 1536, hash 99B9B943 +tracksEnded = true diff --git a/testdata/src/test/assets/ts/sample_ac3.ts.2.dump b/testdata/src/test/assets/ts/sample_ac3.ts.2.dump new file mode 100644 index 0000000000..f48ba43854 --- /dev/null +++ b/testdata/src/test/assets/ts/sample_ac3.ts.2.dump @@ -0,0 +1,33 @@ +seekMap: + isSeekable = true + duration = 252977 + getPosition(0) = [[timeUs=0, position=0]] + getPosition(1) = [[timeUs=1, position=0]] + getPosition(126488) = [[timeUs=126488, position=9099]] + getPosition(252977) = [[timeUs=252977, position=18386]] +numberOfTracks = 1 +track 1900: + total output bytes = 7137 + sample count = 4 + format 0: + id = 1/1900 + sampleMimeType = audio/ac3 + channelCount = 6 + sampleRate = 48000 + sample 0: + time = 160000 + flags = 1 + data = length 1536, hash 6F856273 + sample 1: + time = 192000 + flags = 1 + data = length 1536, hash B1737D3C + sample 2: + time = 224000 + flags = 1 + data = length 1536, hash 98FDEB9D + sample 3: + time = 256000 + flags = 1 + data = length 1536, hash 99B9B943 +tracksEnded = true diff --git a/testdata/src/test/assets/ts/sample_ac3.ts.3.dump b/testdata/src/test/assets/ts/sample_ac3.ts.3.dump new file mode 100644 index 0000000000..997d7a6b02 --- /dev/null +++ b/testdata/src/test/assets/ts/sample_ac3.ts.3.dump @@ -0,0 +1,17 @@ +seekMap: + isSeekable = true + duration = 252977 + getPosition(0) = [[timeUs=0, position=0]] + getPosition(1) = [[timeUs=1, position=0]] + getPosition(126488) = [[timeUs=126488, position=9099]] + getPosition(252977) = [[timeUs=252977, position=18386]] +numberOfTracks = 1 +track 1900: + total output bytes = 0 + sample count = 0 + format 0: + id = 1/1900 + sampleMimeType = audio/ac3 + channelCount = 6 + sampleRate = 48000 +tracksEnded = true diff --git a/testdata/src/test/assets/ts/sample_ac3.ts.unknown_length.dump b/testdata/src/test/assets/ts/sample_ac3.ts.unknown_length.dump new file mode 100644 index 0000000000..a98cb798cb --- /dev/null +++ b/testdata/src/test/assets/ts/sample_ac3.ts.unknown_length.dump @@ -0,0 +1,46 @@ +seekMap: + isSeekable = false + duration = UNSET TIME + getPosition(0) = [[timeUs=0, position=0]] +numberOfTracks = 1 +track 1900: + total output bytes = 13281 + sample count = 8 + format 0: + id = 1/1900 + sampleMimeType = audio/ac3 + channelCount = 6 + sampleRate = 48000 + sample 0: + time = 32000 + flags = 1 + data = length 1536, hash 7108D5C2 + sample 1: + time = 64000 + flags = 1 + data = length 1536, hash 80BF3B34 + sample 2: + time = 96000 + flags = 1 + data = length 1536, hash 5D09685 + sample 3: + time = 128000 + flags = 1 + data = length 1536, hash A9A24E44 + sample 4: + time = 160000 + flags = 1 + data = length 1536, hash 6F856273 + sample 5: + time = 192000 + flags = 1 + data = length 1536, hash B1737D3C + sample 6: + time = 224000 + flags = 1 + data = length 1536, hash 98FDEB9D + sample 7: + time = 256000 + flags = 1 + data = length 1536, hash 99B9B943 +tracksEnded = true diff --git a/testdata/src/test/assets/ts/sample_ac4.ts b/testdata/src/test/assets/ts/sample_ac4.ts new file mode 100644 index 0000000000..7be76c5463 Binary files /dev/null and b/testdata/src/test/assets/ts/sample_ac4.ts differ diff --git a/testdata/src/test/assets/ts/sample_ac4.ts.0.dump b/testdata/src/test/assets/ts/sample_ac4.ts.0.dump new file mode 100644 index 0000000000..80978a8483 --- /dev/null +++ b/testdata/src/test/assets/ts/sample_ac4.ts.0.dump @@ -0,0 +1,93 @@ +seekMap: + isSeekable = true + duration = 796888 + getPosition(0) = [[timeUs=0, position=0]] + getPosition(1) = [[timeUs=1, position=0]] + getPosition(398444) = [[timeUs=398444, position=13385]] + getPosition(796888) = [[timeUs=796888, position=26959]] +numberOfTracks = 1 +track 1900: + total output bytes = 7594 + sample count = 19 + format 0: + id = 1/1900 + sampleMimeType = audio/ac4 + channelCount = 2 + sampleRate = 48000 + sample 0: + time = 0 + flags = 1 + data = length 366, hash B4277F9E + sample 1: + time = 40000 + flags = 1 + data = length 366, hash E8E0A142 + sample 2: + time = 80000 + flags = 1 + data = length 366, hash 2E5073D0 + sample 3: + time = 120000 + flags = 1 + data = length 366, hash 850E71D8 + sample 4: + time = 160000 + flags = 1 + data = length 366, hash 69CD444E + sample 5: + time = 200000 + flags = 1 + data = length 366, hash BD24F36D + sample 6: + time = 240000 + flags = 1 + data = length 366, hash E24F2490 + sample 7: + time = 280000 + flags = 1 + data = length 366, hash EE6F1F06 + sample 8: + time = 320000 + flags = 1 + data = length 366, hash 2DAB000F + sample 9: + time = 360000 + flags = 1 + data = length 366, hash 8102B7EC + sample 10: + time = 400000 + flags = 1 + data = length 366, hash 55BF59AC + sample 11: + time = 440000 + flags = 1 + data = length 494, hash CBC2E09F + sample 12: + time = 480000 + flags = 1 + data = length 519, hash 9DAF56E9 + sample 13: + time = 520000 + flags = 1 + data = length 598, hash 8169EE2 + sample 14: + time = 560000 + flags = 1 + data = length 435, hash 28C21246 + sample 15: + time = 600000 + flags = 1 + data = length 365, hash FF14716D + sample 16: + time = 640000 + flags = 1 + data = length 392, hash 4CC96B29 + sample 17: + time = 680000 + flags = 1 + data = length 373, hash D7AC6D4E + sample 18: + time = 720000 + flags = 1 + data = length 392, hash 99F2511F +tracksEnded = true diff --git a/testdata/src/test/assets/ts/sample_ac4.ts.1.dump b/testdata/src/test/assets/ts/sample_ac4.ts.1.dump new file mode 100644 index 0000000000..f9bb28919c --- /dev/null +++ b/testdata/src/test/assets/ts/sample_ac4.ts.1.dump @@ -0,0 +1,65 @@ +seekMap: + isSeekable = true + duration = 796888 + getPosition(0) = [[timeUs=0, position=0]] + getPosition(1) = [[timeUs=1, position=0]] + getPosition(398444) = [[timeUs=398444, position=13385]] + getPosition(796888) = [[timeUs=796888, position=26959]] +numberOfTracks = 1 +track 1900: + total output bytes = 5032 + sample count = 12 + format 0: + id = 1/1900 + sampleMimeType = audio/ac4 + channelCount = 2 + sampleRate = 48000 + sample 0: + time = 280000 + flags = 1 + data = length 366, hash EE6F1F06 + sample 1: + time = 320000 + flags = 1 + data = length 366, hash 2DAB000F + sample 2: + time = 360000 + flags = 1 + data = length 366, hash 8102B7EC + sample 3: + time = 400000 + flags = 1 + data = length 366, hash 55BF59AC + sample 4: + time = 440000 + flags = 1 + data = length 494, hash CBC2E09F + sample 5: + time = 480000 + flags = 1 + data = length 519, hash 9DAF56E9 + sample 6: + time = 520000 + flags = 1 + data = length 598, hash 8169EE2 + sample 7: + time = 560000 + flags = 1 + data = length 435, hash 28C21246 + sample 8: + time = 600000 + flags = 1 + data = length 365, hash FF14716D + sample 9: + time = 640000 + flags = 1 + data = length 392, hash 4CC96B29 + sample 10: + time = 680000 + flags = 1 + data = length 373, hash D7AC6D4E + sample 11: + time = 720000 + flags = 1 + data = length 392, hash 99F2511F +tracksEnded = true diff --git a/testdata/src/test/assets/ts/sample_ac4.ts.2.dump b/testdata/src/test/assets/ts/sample_ac4.ts.2.dump new file mode 100644 index 0000000000..3c90f502a5 --- /dev/null +++ b/testdata/src/test/assets/ts/sample_ac4.ts.2.dump @@ -0,0 +1,41 @@ +seekMap: + isSeekable = true + duration = 796888 + getPosition(0) = [[timeUs=0, position=0]] + getPosition(1) = [[timeUs=1, position=0]] + getPosition(398444) = [[timeUs=398444, position=13385]] + getPosition(796888) = [[timeUs=796888, position=26959]] +numberOfTracks = 1 +track 1900: + total output bytes = 2555 + sample count = 6 + format 0: + id = 1/1900 + sampleMimeType = audio/ac4 + channelCount = 2 + sampleRate = 48000 + sample 0: + time = 520000 + flags = 1 + data = length 598, hash 8169EE2 + sample 1: + time = 560000 + flags = 1 + data = length 435, hash 28C21246 + sample 2: + time = 600000 + flags = 1 + data = length 365, hash FF14716D + sample 3: + time = 640000 + flags = 1 + data = length 392, hash 4CC96B29 + sample 4: + time = 680000 + flags = 1 + data = length 373, hash D7AC6D4E + sample 5: + time = 720000 + flags = 1 + data = length 392, hash 99F2511F +tracksEnded = true diff --git a/testdata/src/test/assets/ts/sample_ac4.ts.3.dump b/testdata/src/test/assets/ts/sample_ac4.ts.3.dump new file mode 100644 index 0000000000..87199300c7 --- /dev/null +++ b/testdata/src/test/assets/ts/sample_ac4.ts.3.dump @@ -0,0 +1,17 @@ +seekMap: + isSeekable = true + duration = 796888 + getPosition(0) = [[timeUs=0, position=0]] + getPosition(1) = [[timeUs=1, position=0]] + getPosition(398444) = [[timeUs=398444, position=13385]] + getPosition(796888) = [[timeUs=796888, position=26959]] +numberOfTracks = 1 +track 1900: + total output bytes = 0 + sample count = 0 + format 0: + id = 1/1900 + sampleMimeType = audio/ac4 + channelCount = 2 + sampleRate = 48000 +tracksEnded = true diff --git a/testdata/src/test/assets/ts/sample_ac4.ts.unknown_length.dump b/testdata/src/test/assets/ts/sample_ac4.ts.unknown_length.dump new file mode 100644 index 0000000000..3982b407bd --- /dev/null +++ b/testdata/src/test/assets/ts/sample_ac4.ts.unknown_length.dump @@ -0,0 +1,90 @@ +seekMap: + isSeekable = false + duration = UNSET TIME + getPosition(0) = [[timeUs=0, position=0]] +numberOfTracks = 1 +track 1900: + total output bytes = 7594 + sample count = 19 + format 0: + id = 1/1900 + sampleMimeType = audio/ac4 + channelCount = 2 + sampleRate = 48000 + sample 0: + time = 0 + flags = 1 + data = length 366, hash B4277F9E + sample 1: + time = 40000 + flags = 1 + data = length 366, hash E8E0A142 + sample 2: + time = 80000 + flags = 1 + data = length 366, hash 2E5073D0 + sample 3: + time = 120000 + flags = 1 + data = length 366, hash 850E71D8 + sample 4: + time = 160000 + flags = 1 + data = length 366, hash 69CD444E + sample 5: + time = 200000 + flags = 1 + data = length 366, hash BD24F36D + sample 6: + time = 240000 + flags = 1 + data = length 366, hash E24F2490 + sample 7: + time = 280000 + flags = 1 + data = length 366, hash EE6F1F06 + sample 8: + time = 320000 + flags = 1 + data = length 366, hash 2DAB000F + sample 9: + time = 360000 + flags = 1 + data = length 366, hash 8102B7EC + sample 10: + time = 400000 + flags = 1 + data = length 366, hash 55BF59AC + sample 11: + time = 440000 + flags = 1 + data = length 494, hash CBC2E09F + sample 12: + time = 480000 + flags = 1 + data = length 519, hash 9DAF56E9 + sample 13: + time = 520000 + flags = 1 + data = length 598, hash 8169EE2 + sample 14: + time = 560000 + flags = 1 + data = length 435, hash 28C21246 + sample 15: + time = 600000 + flags = 1 + data = length 365, hash FF14716D + sample 16: + time = 640000 + flags = 1 + data = length 392, hash 4CC96B29 + sample 17: + time = 680000 + flags = 1 + data = length 373, hash D7AC6D4E + sample 18: + time = 720000 + flags = 1 + data = length 392, hash 99F2511F +tracksEnded = true diff --git a/testdata/src/test/assets/ts/sample_ait.ts b/testdata/src/test/assets/ts/sample_ait.ts new file mode 100644 index 0000000000..eb3678e00e Binary files /dev/null and b/testdata/src/test/assets/ts/sample_ait.ts differ diff --git a/testdata/src/test/assets/ts/sample_ait.ts.0.dump b/testdata/src/test/assets/ts/sample_ait.ts.0.dump new file mode 100644 index 0000000000..355b403293 --- /dev/null +++ b/testdata/src/test/assets/ts/sample_ait.ts.0.dump @@ -0,0 +1,109 @@ +seekMap: + isSeekable = false + duration = UNSET TIME + getPosition(0) = [[timeUs=0, position=0]] +numberOfTracks = 2 +track 330: + total output bytes = 9928 + sample count = 19 + format 0: + id = 1031/330 + sampleMimeType = audio/eac3 + channelCount = 2 + sampleRate = 48000 + language = fr + sample 0: + time = 0 + flags = 1 + data = length 512, hash E47547D4 + sample 1: + time = 32000 + flags = 1 + data = length 512, hash F6A537AC + sample 2: + time = 64000 + flags = 1 + data = length 512, hash 97391682 + sample 3: + time = 96000 + flags = 1 + data = length 512, hash CFD3B665 + sample 4: + time = 128000 + flags = 1 + data = length 512, hash 2E79A3AF + sample 5: + time = 160000 + flags = 1 + data = length 512, hash 2C24E2A3 + sample 6: + time = 192000 + flags = 1 + data = length 512, hash 5BCB9661 + sample 7: + time = 224000 + flags = 1 + data = length 512, hash 943ACBF2 + sample 8: + time = 256000 + flags = 1 + data = length 512, hash B248E943 + sample 9: + time = 288000 + flags = 1 + data = length 512, hash EC2DD86F + sample 10: + time = 320000 + flags = 1 + data = length 512, hash A659332F + sample 11: + time = 352000 + flags = 1 + data = length 512, hash CB641607 + sample 12: + time = 384000 + flags = 1 + data = length 512, hash 157489A0 + sample 13: + time = 416000 + flags = 1 + data = length 512, hash A37CB66E + sample 14: + time = 448000 + flags = 1 + data = length 512, hash 932F07D4 + sample 15: + time = 480000 + flags = 1 + data = length 512, hash 91F50161 + sample 16: + time = 512000 + flags = 1 + data = length 512, hash 7F9D6CCB + sample 17: + time = 544000 + flags = 1 + data = length 512, hash 3955F015 + sample 18: + time = 576000 + flags = 1 + data = length 512, hash A8E5C938 +track 370: + total output bytes = 1413 + sample count = 3 + format 0: + sampleMimeType = application/vnd.dvb.ait + subsampleOffsetUs = -43622564033 + sample 0: + time = 0 + flags = 1 + data = length 471, hash B189052F + sample 1: + time = 192000 + flags = 1 + data = length 471, hash B189052F + sample 2: + time = 384000 + flags = 1 + data = length 471, hash B189052F +tracksEnded = true diff --git a/testdata/src/test/assets/ts/sample_ait.ts.unknown_length.dump b/testdata/src/test/assets/ts/sample_ait.ts.unknown_length.dump new file mode 100644 index 0000000000..355b403293 --- /dev/null +++ b/testdata/src/test/assets/ts/sample_ait.ts.unknown_length.dump @@ -0,0 +1,109 @@ +seekMap: + isSeekable = false + duration = UNSET TIME + getPosition(0) = [[timeUs=0, position=0]] +numberOfTracks = 2 +track 330: + total output bytes = 9928 + sample count = 19 + format 0: + id = 1031/330 + sampleMimeType = audio/eac3 + channelCount = 2 + sampleRate = 48000 + language = fr + sample 0: + time = 0 + flags = 1 + data = length 512, hash E47547D4 + sample 1: + time = 32000 + flags = 1 + data = length 512, hash F6A537AC + sample 2: + time = 64000 + flags = 1 + data = length 512, hash 97391682 + sample 3: + time = 96000 + flags = 1 + data = length 512, hash CFD3B665 + sample 4: + time = 128000 + flags = 1 + data = length 512, hash 2E79A3AF + sample 5: + time = 160000 + flags = 1 + data = length 512, hash 2C24E2A3 + sample 6: + time = 192000 + flags = 1 + data = length 512, hash 5BCB9661 + sample 7: + time = 224000 + flags = 1 + data = length 512, hash 943ACBF2 + sample 8: + time = 256000 + flags = 1 + data = length 512, hash B248E943 + sample 9: + time = 288000 + flags = 1 + data = length 512, hash EC2DD86F + sample 10: + time = 320000 + flags = 1 + data = length 512, hash A659332F + sample 11: + time = 352000 + flags = 1 + data = length 512, hash CB641607 + sample 12: + time = 384000 + flags = 1 + data = length 512, hash 157489A0 + sample 13: + time = 416000 + flags = 1 + data = length 512, hash A37CB66E + sample 14: + time = 448000 + flags = 1 + data = length 512, hash 932F07D4 + sample 15: + time = 480000 + flags = 1 + data = length 512, hash 91F50161 + sample 16: + time = 512000 + flags = 1 + data = length 512, hash 7F9D6CCB + sample 17: + time = 544000 + flags = 1 + data = length 512, hash 3955F015 + sample 18: + time = 576000 + flags = 1 + data = length 512, hash A8E5C938 +track 370: + total output bytes = 1413 + sample count = 3 + format 0: + sampleMimeType = application/vnd.dvb.ait + subsampleOffsetUs = -43622564033 + sample 0: + time = 0 + flags = 1 + data = length 471, hash B189052F + sample 1: + time = 192000 + flags = 1 + data = length 471, hash B189052F + sample 2: + time = 384000 + flags = 1 + data = length 471, hash B189052F +tracksEnded = true diff --git a/library/core/src/test/assets/ts/sample_cbs.adts b/testdata/src/test/assets/ts/sample_cbs.adts similarity index 100% rename from library/core/src/test/assets/ts/sample_cbs.adts rename to testdata/src/test/assets/ts/sample_cbs.adts diff --git a/library/core/src/test/assets/ts/sample_cbs.adts.0.dump b/testdata/src/test/assets/ts/sample_cbs.adts.0.dump similarity index 94% rename from library/core/src/test/assets/ts/sample_cbs.adts.0.dump rename to testdata/src/test/assets/ts/sample_cbs.adts.0.dump index e535aa8cd7..e3aaa10d42 100644 --- a/library/core/src/test/assets/ts/sample_cbs.adts.0.dump +++ b/testdata/src/test/assets/ts/sample_cbs.adts.0.dump @@ -2,32 +2,21 @@ seekMap: isSeekable = true duration = 3356772 getPosition(0) = [[timeUs=0, position=0]] + getPosition(1) = [[timeUs=0, position=0], [timeUs=23219, position=220]] + getPosition(1678386) = [[timeUs=1671789, position=15840], [timeUs=1695009, position=16060]] + getPosition(3356772) = [[timeUs=3333553, position=31585]] 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 + format 0: + id = 0 + sampleMimeType = audio/mp4a-latm + codecs = mp4a.40.2 + channelCount = 1 + sampleRate = 44100 + initializationData: + data = length 2, hash 5F7 sample 0: time = 0 flags = 1 @@ -605,27 +594,9 @@ track 0: 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 + format 0: + id = 1 + sampleMimeType = application/id3 tracksEnded = true diff --git a/library/core/src/test/assets/ts/sample_cbs.adts.1.dump b/testdata/src/test/assets/ts/sample_cbs.adts.1.dump similarity index 91% rename from library/core/src/test/assets/ts/sample_cbs.adts.1.dump rename to testdata/src/test/assets/ts/sample_cbs.adts.1.dump index 96d2fcfb39..ab3d2feefd 100644 --- a/library/core/src/test/assets/ts/sample_cbs.adts.1.dump +++ b/testdata/src/test/assets/ts/sample_cbs.adts.1.dump @@ -2,32 +2,21 @@ seekMap: isSeekable = true duration = 3356772 getPosition(0) = [[timeUs=0, position=0]] + getPosition(1) = [[timeUs=0, position=0], [timeUs=23219, position=220]] + getPosition(1678386) = [[timeUs=1671789, position=15840], [timeUs=1695009, position=16060]] + getPosition(3356772) = [[timeUs=3333553, position=31585]] 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 + format 0: + id = 0 + sampleMimeType = audio/mp4a-latm + codecs = mp4a.40.2 + channelCount = 1 + sampleRate = 44100 + initializationData: + data = length 2, hash 5F7 sample 0: time = 1118924 flags = 1 @@ -405,27 +394,9 @@ track 0: 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 + format 0: + id = 1 + sampleMimeType = application/id3 tracksEnded = true diff --git a/library/core/src/test/assets/ts/sample_cbs.adts.2.dump b/testdata/src/test/assets/ts/sample_cbs.adts.2.dump similarity index 85% rename from library/core/src/test/assets/ts/sample_cbs.adts.2.dump rename to testdata/src/test/assets/ts/sample_cbs.adts.2.dump index 2e581bca28..2330cdc3ce 100644 --- a/library/core/src/test/assets/ts/sample_cbs.adts.2.dump +++ b/testdata/src/test/assets/ts/sample_cbs.adts.2.dump @@ -2,32 +2,21 @@ seekMap: isSeekable = true duration = 3356772 getPosition(0) = [[timeUs=0, position=0]] + getPosition(1) = [[timeUs=0, position=0], [timeUs=23219, position=220]] + getPosition(1678386) = [[timeUs=1671789, position=15840], [timeUs=1695009, position=16060]] + getPosition(3356772) = [[timeUs=3333553, position=31585]] 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 + format 0: + id = 0 + sampleMimeType = audio/mp4a-latm + codecs = mp4a.40.2 + channelCount = 1 + sampleRate = 44100 + initializationData: + data = length 2, hash 5F7 sample 0: time = 2237848 flags = 1 @@ -225,27 +214,9 @@ track 0: 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 + format 0: + id = 1 + sampleMimeType = application/id3 tracksEnded = true diff --git a/testdata/src/test/assets/ts/sample_cbs.adts.3.dump b/testdata/src/test/assets/ts/sample_cbs.adts.3.dump new file mode 100644 index 0000000000..ae07524dad --- /dev/null +++ b/testdata/src/test/assets/ts/sample_cbs.adts.3.dump @@ -0,0 +1,30 @@ +seekMap: + isSeekable = true + duration = 3356772 + getPosition(0) = [[timeUs=0, position=0]] + getPosition(1) = [[timeUs=0, position=0], [timeUs=23219, position=220]] + getPosition(1678386) = [[timeUs=1671789, position=15840], [timeUs=1695009, position=16060]] + getPosition(3356772) = [[timeUs=3333553, position=31585]] +numberOfTracks = 2 +track 0: + total output bytes = 174 + sample count = 1 + format 0: + id = 0 + sampleMimeType = audio/mp4a-latm + codecs = mp4a.40.2 + channelCount = 1 + sampleRate = 44100 + initializationData: + data = length 2, hash 5F7 + sample 0: + time = 3356772 + flags = 1 + data = length 174, hash 2B69C34E +track 1: + total output bytes = 0 + sample count = 0 + format 0: + id = 1 + sampleMimeType = application/id3 +tracksEnded = true diff --git a/testdata/src/test/assets/ts/sample_cbs.adts.unknown_length.dump b/testdata/src/test/assets/ts/sample_cbs.adts.unknown_length.dump new file mode 100644 index 0000000000..ddfea3e99b --- /dev/null +++ b/testdata/src/test/assets/ts/sample_cbs.adts.unknown_length.dump @@ -0,0 +1,599 @@ +seekMap: + isSeekable = false + duration = UNSET TIME + getPosition(0) = [[timeUs=0, position=0]] +numberOfTracks = 2 +track 0: + total output bytes = 30797 + sample count = 144 + format 0: + id = 0 + sampleMimeType = audio/mp4a-latm + codecs = mp4a.40.2 + channelCount = 1 + sampleRate = 44100 + initializationData: + data = length 2, hash 5F7 + 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: + total output bytes = 0 + sample count = 0 + format 0: + id = 1 + sampleMimeType = application/id3 +tracksEnded = true diff --git a/library/core/src/test/assets/ts/sample_cbs_truncated.adts b/testdata/src/test/assets/ts/sample_cbs_truncated.adts similarity index 100% rename from library/core/src/test/assets/ts/sample_cbs_truncated.adts rename to testdata/src/test/assets/ts/sample_cbs_truncated.adts diff --git a/library/core/src/test/assets/ts/sample_cbs_truncated.adts.0.dump b/testdata/src/test/assets/ts/sample_cbs_truncated.adts.0.dump similarity index 94% rename from library/core/src/test/assets/ts/sample_cbs_truncated.adts.0.dump rename to testdata/src/test/assets/ts/sample_cbs_truncated.adts.0.dump index 3344e7ba59..b94049c6be 100644 --- a/library/core/src/test/assets/ts/sample_cbs_truncated.adts.0.dump +++ b/testdata/src/test/assets/ts/sample_cbs_truncated.adts.0.dump @@ -2,32 +2,21 @@ seekMap: isSeekable = true duration = 3355717 getPosition(0) = [[timeUs=0, position=0]] + getPosition(1) = [[timeUs=0, position=0], [timeUs=23219, position=220]] + getPosition(1677858) = [[timeUs=1671789, position=15840], [timeUs=1695009, position=16060]] + getPosition(3355717) = [[timeUs=3332497, position=31575]] 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 = 30787 sample count = 143 + format 0: + id = 0 + sampleMimeType = audio/mp4a-latm + codecs = mp4a.40.2 + channelCount = 1 + sampleRate = 44100 + initializationData: + data = length 2, hash 5F7 sample 0: time = 0 flags = 1 @@ -601,27 +590,9 @@ track 0: flags = 1 data = length 232, hash A330F188 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 + format 0: + id = 1 + sampleMimeType = application/id3 tracksEnded = true diff --git a/library/core/src/test/assets/ts/sample_cbs_truncated.adts.1.dump b/testdata/src/test/assets/ts/sample_cbs_truncated.adts.1.dump similarity index 91% rename from library/core/src/test/assets/ts/sample_cbs_truncated.adts.1.dump rename to testdata/src/test/assets/ts/sample_cbs_truncated.adts.1.dump index 53608df7ed..79805d78f7 100644 --- a/library/core/src/test/assets/ts/sample_cbs_truncated.adts.1.dump +++ b/testdata/src/test/assets/ts/sample_cbs_truncated.adts.1.dump @@ -2,32 +2,21 @@ seekMap: isSeekable = true duration = 3355717 getPosition(0) = [[timeUs=0, position=0]] + getPosition(1) = [[timeUs=0, position=0], [timeUs=23219, position=220]] + getPosition(1677858) = [[timeUs=1671789, position=15840], [timeUs=1695009, position=16060]] + getPosition(3355717) = [[timeUs=3332497, position=31575]] 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 = 20523 sample count = 93 + format 0: + id = 0 + sampleMimeType = audio/mp4a-latm + codecs = mp4a.40.2 + channelCount = 1 + sampleRate = 44100 + initializationData: + data = length 2, hash 5F7 sample 0: time = 1118572 flags = 1 @@ -401,27 +390,9 @@ track 0: flags = 1 data = length 232, hash A330F188 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 + format 0: + id = 1 + sampleMimeType = application/id3 tracksEnded = true diff --git a/library/core/src/test/assets/ts/sample_cbs_truncated.adts.2.dump b/testdata/src/test/assets/ts/sample_cbs_truncated.adts.2.dump similarity index 84% rename from library/core/src/test/assets/ts/sample_cbs_truncated.adts.2.dump rename to testdata/src/test/assets/ts/sample_cbs_truncated.adts.2.dump index af8e415412..7c6278eca1 100644 --- a/library/core/src/test/assets/ts/sample_cbs_truncated.adts.2.dump +++ b/testdata/src/test/assets/ts/sample_cbs_truncated.adts.2.dump @@ -2,32 +2,21 @@ seekMap: isSeekable = true duration = 3355717 getPosition(0) = [[timeUs=0, position=0]] + getPosition(1) = [[timeUs=0, position=0], [timeUs=23219, position=220]] + getPosition(1677858) = [[timeUs=1671789, position=15840], [timeUs=1695009, position=16060]] + getPosition(3355717) = [[timeUs=3332497, position=31575]] 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 = 10151 sample count = 48 + format 0: + id = 0 + sampleMimeType = audio/mp4a-latm + codecs = mp4a.40.2 + channelCount = 1 + sampleRate = 44100 + initializationData: + data = length 2, hash 5F7 sample 0: time = 2237144 flags = 1 @@ -221,27 +210,9 @@ track 0: flags = 1 data = length 232, hash A330F188 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 + format 0: + id = 1 + sampleMimeType = application/id3 tracksEnded = true diff --git a/testdata/src/test/assets/ts/sample_cbs_truncated.adts.3.dump b/testdata/src/test/assets/ts/sample_cbs_truncated.adts.3.dump new file mode 100644 index 0000000000..518bcf4a3b --- /dev/null +++ b/testdata/src/test/assets/ts/sample_cbs_truncated.adts.3.dump @@ -0,0 +1,26 @@ +seekMap: + isSeekable = true + duration = 3355717 + getPosition(0) = [[timeUs=0, position=0]] + getPosition(1) = [[timeUs=0, position=0], [timeUs=23219, position=220]] + getPosition(1677858) = [[timeUs=1671789, position=15840], [timeUs=1695009, position=16060]] + getPosition(3355717) = [[timeUs=3332497, position=31575]] +numberOfTracks = 2 +track 0: + total output bytes = 164 + sample count = 0 + format 0: + id = 0 + sampleMimeType = audio/mp4a-latm + codecs = mp4a.40.2 + channelCount = 1 + sampleRate = 44100 + initializationData: + data = length 2, hash 5F7 +track 1: + total output bytes = 0 + sample count = 0 + format 0: + id = 1 + sampleMimeType = application/id3 +tracksEnded = true diff --git a/library/core/src/test/assets/ts/sample_cbs_truncated.adts.unklen.dump b/testdata/src/test/assets/ts/sample_cbs_truncated.adts.unknown_length.dump similarity index 94% rename from library/core/src/test/assets/ts/sample_cbs_truncated.adts.unklen.dump rename to testdata/src/test/assets/ts/sample_cbs_truncated.adts.unknown_length.dump index d478f65cac..0af445910d 100644 --- a/library/core/src/test/assets/ts/sample_cbs_truncated.adts.unklen.dump +++ b/testdata/src/test/assets/ts/sample_cbs_truncated.adts.unknown_length.dump @@ -4,30 +4,16 @@ seekMap: 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 = 30787 sample count = 143 + format 0: + id = 0 + sampleMimeType = audio/mp4a-latm + codecs = mp4a.40.2 + channelCount = 1 + sampleRate = 44100 + initializationData: + data = length 2, hash 5F7 sample 0: time = 0 flags = 1 @@ -601,27 +587,9 @@ track 0: flags = 1 data = length 232, hash A330F188 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 + format 0: + id = 1 + sampleMimeType = application/id3 tracksEnded = true diff --git a/testdata/src/test/assets/ts/sample_eac3.ts b/testdata/src/test/assets/ts/sample_eac3.ts new file mode 100644 index 0000000000..51d90cec0a Binary files /dev/null and b/testdata/src/test/assets/ts/sample_eac3.ts differ diff --git a/testdata/src/test/assets/ts/sample_eac3.ts.0.dump b/testdata/src/test/assets/ts/sample_eac3.ts.0.dump new file mode 100644 index 0000000000..dfc89c5f19 --- /dev/null +++ b/testdata/src/test/assets/ts/sample_eac3.ts.0.dump @@ -0,0 +1,233 @@ +seekMap: + isSeekable = true + duration = 250077 + getPosition(0) = [[timeUs=0, position=0]] + getPosition(1) = [[timeUs=1, position=0]] + getPosition(125038) = [[timeUs=125038, position=109918]] + getPosition(250077) = [[timeUs=250077, position=220025]] +numberOfTracks = 1 +track 1900: + total output bytes = 216000 + sample count = 54 + format 0: + id = 1/1900 + sampleMimeType = audio/eac3 + channelCount = 6 + sampleRate = 48000 + sample 0: + time = 0 + flags = 1 + data = length 4000, hash BAEAFB2A + sample 1: + time = 5333 + flags = 1 + data = length 4000, hash E3C5EBF0 + sample 2: + time = 10666 + flags = 1 + data = length 4000, hash 32E0F957 + sample 3: + time = 15999 + flags = 1 + data = length 4000, hash 5354CC5D + sample 4: + time = 21332 + flags = 1 + data = length 4000, hash FF834906 + sample 5: + time = 26665 + flags = 1 + data = length 4000, hash 6F571E61 + sample 6: + time = 32000 + flags = 1 + data = length 4000, hash 5C931F6B + sample 7: + time = 37333 + flags = 1 + data = length 4000, hash B1FB2E57 + sample 8: + time = 42666 + flags = 1 + data = length 4000, hash C71240EB + sample 9: + time = 47999 + flags = 1 + data = length 4000, hash C3E302EE + sample 10: + time = 53332 + flags = 1 + data = length 4000, hash 7994C27B + sample 11: + time = 58665 + flags = 1 + data = length 4000, hash 1ED4E6F3 + sample 12: + time = 64000 + flags = 1 + data = length 4000, hash 1D5E6AAC + sample 13: + time = 69333 + flags = 1 + data = length 4000, hash 30058F51 + sample 14: + time = 74666 + flags = 1 + data = length 4000, hash 15DD0E4A + sample 15: + time = 79999 + flags = 1 + data = length 4000, hash 37BE7C15 + sample 16: + time = 85332 + flags = 1 + data = length 4000, hash 7CFDD34B + sample 17: + time = 90665 + flags = 1 + data = length 4000, hash 27F20D29 + sample 18: + time = 96000 + flags = 1 + data = length 4000, hash 6F565894 + sample 19: + time = 101333 + flags = 1 + data = length 4000, hash A6F07C4A + sample 20: + time = 106666 + flags = 1 + data = length 4000, hash 3A0CA15C + sample 21: + time = 111999 + flags = 1 + data = length 4000, hash DB365414 + sample 22: + time = 117332 + flags = 1 + data = length 4000, hash 31E08469 + sample 23: + time = 122665 + flags = 1 + data = length 4000, hash 315F5C28 + sample 24: + time = 128000 + flags = 1 + data = length 4000, hash CC65DF80 + sample 25: + time = 133333 + flags = 1 + data = length 4000, hash 503FB64C + sample 26: + time = 138666 + flags = 1 + data = length 4000, hash 817CF735 + sample 27: + time = 143999 + flags = 1 + data = length 4000, hash 37391ADA + sample 28: + time = 149332 + flags = 1 + data = length 4000, hash 37391ADA + sample 29: + time = 154665 + flags = 1 + data = length 4000, hash 64DBF751 + sample 30: + time = 160000 + flags = 1 + data = length 4000, hash 81AE828E + sample 31: + time = 165333 + flags = 1 + data = length 4000, hash 767D6C98 + sample 32: + time = 170666 + flags = 1 + data = length 4000, hash A5F6D4E + sample 33: + time = 175999 + flags = 1 + data = length 4000, hash EABC6B0D + sample 34: + time = 181332 + flags = 1 + data = length 4000, hash F47EF742 + sample 35: + time = 186665 + flags = 1 + data = length 4000, hash 9B2549DA + sample 36: + time = 192000 + flags = 1 + data = length 4000, hash A12733C9 + sample 37: + time = 197333 + flags = 1 + data = length 4000, hash 95F62E99 + sample 38: + time = 202666 + flags = 1 + data = length 4000, hash A4D858 + sample 39: + time = 207999 + flags = 1 + data = length 4000, hash A4D858 + sample 40: + time = 213332 + flags = 1 + data = length 4000, hash 22C1A129 + sample 41: + time = 218665 + flags = 1 + data = length 4000, hash 2C51E4A1 + sample 42: + time = 224000 + flags = 1 + data = length 4000, hash 3782E8BB + sample 43: + time = 229333 + flags = 1 + data = length 4000, hash 2C51E4A1 + sample 44: + time = 234666 + flags = 1 + data = length 4000, hash BDB3D129 + sample 45: + time = 239999 + flags = 1 + data = length 4000, hash F642A55 + sample 46: + time = 245332 + flags = 1 + data = length 4000, hash 32F259F4 + sample 47: + time = 250665 + flags = 1 + data = length 4000, hash 4C987B7C + sample 48: + time = 256000 + flags = 1 + data = length 4000, hash 57C98E1C + sample 49: + time = 261333 + flags = 1 + data = length 4000, hash 4C987B7C + sample 50: + time = 266666 + flags = 1 + data = length 4000, hash 4C987B7C + sample 51: + time = 271999 + flags = 1 + data = length 4000, hash 4C987B7C + sample 52: + time = 277332 + flags = 1 + data = length 4000, hash 4C987B7C + sample 53: + time = 282665 + flags = 1 + data = length 4000, hash 4C987B7C +tracksEnded = true diff --git a/testdata/src/test/assets/ts/sample_eac3.ts.1.dump b/testdata/src/test/assets/ts/sample_eac3.ts.1.dump new file mode 100644 index 0000000000..c06294df2c --- /dev/null +++ b/testdata/src/test/assets/ts/sample_eac3.ts.1.dump @@ -0,0 +1,185 @@ +seekMap: + isSeekable = true + duration = 250077 + getPosition(0) = [[timeUs=0, position=0]] + getPosition(1) = [[timeUs=1, position=0]] + getPosition(125038) = [[timeUs=125038, position=109918]] + getPosition(250077) = [[timeUs=250077, position=220025]] +numberOfTracks = 1 +track 1900: + total output bytes = 168000 + sample count = 42 + format 0: + id = 1/1900 + sampleMimeType = audio/eac3 + channelCount = 6 + sampleRate = 48000 + sample 0: + time = 64000 + flags = 1 + data = length 4000, hash 1D5E6AAC + sample 1: + time = 69333 + flags = 1 + data = length 4000, hash 30058F51 + sample 2: + time = 74666 + flags = 1 + data = length 4000, hash 15DD0E4A + sample 3: + time = 79999 + flags = 1 + data = length 4000, hash 37BE7C15 + sample 4: + time = 85332 + flags = 1 + data = length 4000, hash 7CFDD34B + sample 5: + time = 90665 + flags = 1 + data = length 4000, hash 27F20D29 + sample 6: + time = 96000 + flags = 1 + data = length 4000, hash 6F565894 + sample 7: + time = 101333 + flags = 1 + data = length 4000, hash A6F07C4A + sample 8: + time = 106666 + flags = 1 + data = length 4000, hash 3A0CA15C + sample 9: + time = 111999 + flags = 1 + data = length 4000, hash DB365414 + sample 10: + time = 117332 + flags = 1 + data = length 4000, hash 31E08469 + sample 11: + time = 122665 + flags = 1 + data = length 4000, hash 315F5C28 + sample 12: + time = 128000 + flags = 1 + data = length 4000, hash CC65DF80 + sample 13: + time = 133333 + flags = 1 + data = length 4000, hash 503FB64C + sample 14: + time = 138666 + flags = 1 + data = length 4000, hash 817CF735 + sample 15: + time = 143999 + flags = 1 + data = length 4000, hash 37391ADA + sample 16: + time = 149332 + flags = 1 + data = length 4000, hash 37391ADA + sample 17: + time = 154665 + flags = 1 + data = length 4000, hash 64DBF751 + sample 18: + time = 160000 + flags = 1 + data = length 4000, hash 81AE828E + sample 19: + time = 165333 + flags = 1 + data = length 4000, hash 767D6C98 + sample 20: + time = 170666 + flags = 1 + data = length 4000, hash A5F6D4E + sample 21: + time = 175999 + flags = 1 + data = length 4000, hash EABC6B0D + sample 22: + time = 181332 + flags = 1 + data = length 4000, hash F47EF742 + sample 23: + time = 186665 + flags = 1 + data = length 4000, hash 9B2549DA + sample 24: + time = 192000 + flags = 1 + data = length 4000, hash A12733C9 + sample 25: + time = 197333 + flags = 1 + data = length 4000, hash 95F62E99 + sample 26: + time = 202666 + flags = 1 + data = length 4000, hash A4D858 + sample 27: + time = 207999 + flags = 1 + data = length 4000, hash A4D858 + sample 28: + time = 213332 + flags = 1 + data = length 4000, hash 22C1A129 + sample 29: + time = 218665 + flags = 1 + data = length 4000, hash 2C51E4A1 + sample 30: + time = 224000 + flags = 1 + data = length 4000, hash 3782E8BB + sample 31: + time = 229333 + flags = 1 + data = length 4000, hash 2C51E4A1 + sample 32: + time = 234666 + flags = 1 + data = length 4000, hash BDB3D129 + sample 33: + time = 239999 + flags = 1 + data = length 4000, hash F642A55 + sample 34: + time = 245332 + flags = 1 + data = length 4000, hash 32F259F4 + sample 35: + time = 250665 + flags = 1 + data = length 4000, hash 4C987B7C + sample 36: + time = 256000 + flags = 1 + data = length 4000, hash 57C98E1C + sample 37: + time = 261333 + flags = 1 + data = length 4000, hash 4C987B7C + sample 38: + time = 266666 + flags = 1 + data = length 4000, hash 4C987B7C + sample 39: + time = 271999 + flags = 1 + data = length 4000, hash 4C987B7C + sample 40: + time = 277332 + flags = 1 + data = length 4000, hash 4C987B7C + sample 41: + time = 282665 + flags = 1 + data = length 4000, hash 4C987B7C +tracksEnded = true diff --git a/testdata/src/test/assets/ts/sample_eac3.ts.2.dump b/testdata/src/test/assets/ts/sample_eac3.ts.2.dump new file mode 100644 index 0000000000..9104607498 --- /dev/null +++ b/testdata/src/test/assets/ts/sample_eac3.ts.2.dump @@ -0,0 +1,113 @@ +seekMap: + isSeekable = true + duration = 250077 + getPosition(0) = [[timeUs=0, position=0]] + getPosition(1) = [[timeUs=1, position=0]] + getPosition(125038) = [[timeUs=125038, position=109918]] + getPosition(250077) = [[timeUs=250077, position=220025]] +numberOfTracks = 1 +track 1900: + total output bytes = 96000 + sample count = 24 + format 0: + id = 1/1900 + sampleMimeType = audio/eac3 + channelCount = 6 + sampleRate = 48000 + sample 0: + time = 160000 + flags = 1 + data = length 4000, hash 81AE828E + sample 1: + time = 165333 + flags = 1 + data = length 4000, hash 767D6C98 + sample 2: + time = 170666 + flags = 1 + data = length 4000, hash A5F6D4E + sample 3: + time = 175999 + flags = 1 + data = length 4000, hash EABC6B0D + sample 4: + time = 181332 + flags = 1 + data = length 4000, hash F47EF742 + sample 5: + time = 186665 + flags = 1 + data = length 4000, hash 9B2549DA + sample 6: + time = 192000 + flags = 1 + data = length 4000, hash A12733C9 + sample 7: + time = 197333 + flags = 1 + data = length 4000, hash 95F62E99 + sample 8: + time = 202666 + flags = 1 + data = length 4000, hash A4D858 + sample 9: + time = 207999 + flags = 1 + data = length 4000, hash A4D858 + sample 10: + time = 213332 + flags = 1 + data = length 4000, hash 22C1A129 + sample 11: + time = 218665 + flags = 1 + data = length 4000, hash 2C51E4A1 + sample 12: + time = 224000 + flags = 1 + data = length 4000, hash 3782E8BB + sample 13: + time = 229333 + flags = 1 + data = length 4000, hash 2C51E4A1 + sample 14: + time = 234666 + flags = 1 + data = length 4000, hash BDB3D129 + sample 15: + time = 239999 + flags = 1 + data = length 4000, hash F642A55 + sample 16: + time = 245332 + flags = 1 + data = length 4000, hash 32F259F4 + sample 17: + time = 250665 + flags = 1 + data = length 4000, hash 4C987B7C + sample 18: + time = 256000 + flags = 1 + data = length 4000, hash 57C98E1C + sample 19: + time = 261333 + flags = 1 + data = length 4000, hash 4C987B7C + sample 20: + time = 266666 + flags = 1 + data = length 4000, hash 4C987B7C + sample 21: + time = 271999 + flags = 1 + data = length 4000, hash 4C987B7C + sample 22: + time = 277332 + flags = 1 + data = length 4000, hash 4C987B7C + sample 23: + time = 282665 + flags = 1 + data = length 4000, hash 4C987B7C +tracksEnded = true diff --git a/testdata/src/test/assets/ts/sample_eac3.ts.3.dump b/testdata/src/test/assets/ts/sample_eac3.ts.3.dump new file mode 100644 index 0000000000..c490b7eca8 --- /dev/null +++ b/testdata/src/test/assets/ts/sample_eac3.ts.3.dump @@ -0,0 +1,17 @@ +seekMap: + isSeekable = true + duration = 250077 + getPosition(0) = [[timeUs=0, position=0]] + getPosition(1) = [[timeUs=1, position=0]] + getPosition(125038) = [[timeUs=125038, position=109918]] + getPosition(250077) = [[timeUs=250077, position=220025]] +numberOfTracks = 1 +track 1900: + total output bytes = 0 + sample count = 0 + format 0: + id = 1/1900 + sampleMimeType = audio/eac3 + channelCount = 6 + sampleRate = 48000 +tracksEnded = true diff --git a/testdata/src/test/assets/ts/sample_eac3.ts.unknown_length.dump b/testdata/src/test/assets/ts/sample_eac3.ts.unknown_length.dump new file mode 100644 index 0000000000..0aae4097a7 --- /dev/null +++ b/testdata/src/test/assets/ts/sample_eac3.ts.unknown_length.dump @@ -0,0 +1,230 @@ +seekMap: + isSeekable = false + duration = UNSET TIME + getPosition(0) = [[timeUs=0, position=0]] +numberOfTracks = 1 +track 1900: + total output bytes = 216000 + sample count = 54 + format 0: + id = 1/1900 + sampleMimeType = audio/eac3 + channelCount = 6 + sampleRate = 48000 + sample 0: + time = 0 + flags = 1 + data = length 4000, hash BAEAFB2A + sample 1: + time = 5333 + flags = 1 + data = length 4000, hash E3C5EBF0 + sample 2: + time = 10666 + flags = 1 + data = length 4000, hash 32E0F957 + sample 3: + time = 15999 + flags = 1 + data = length 4000, hash 5354CC5D + sample 4: + time = 21332 + flags = 1 + data = length 4000, hash FF834906 + sample 5: + time = 26665 + flags = 1 + data = length 4000, hash 6F571E61 + sample 6: + time = 32000 + flags = 1 + data = length 4000, hash 5C931F6B + sample 7: + time = 37333 + flags = 1 + data = length 4000, hash B1FB2E57 + sample 8: + time = 42666 + flags = 1 + data = length 4000, hash C71240EB + sample 9: + time = 47999 + flags = 1 + data = length 4000, hash C3E302EE + sample 10: + time = 53332 + flags = 1 + data = length 4000, hash 7994C27B + sample 11: + time = 58665 + flags = 1 + data = length 4000, hash 1ED4E6F3 + sample 12: + time = 64000 + flags = 1 + data = length 4000, hash 1D5E6AAC + sample 13: + time = 69333 + flags = 1 + data = length 4000, hash 30058F51 + sample 14: + time = 74666 + flags = 1 + data = length 4000, hash 15DD0E4A + sample 15: + time = 79999 + flags = 1 + data = length 4000, hash 37BE7C15 + sample 16: + time = 85332 + flags = 1 + data = length 4000, hash 7CFDD34B + sample 17: + time = 90665 + flags = 1 + data = length 4000, hash 27F20D29 + sample 18: + time = 96000 + flags = 1 + data = length 4000, hash 6F565894 + sample 19: + time = 101333 + flags = 1 + data = length 4000, hash A6F07C4A + sample 20: + time = 106666 + flags = 1 + data = length 4000, hash 3A0CA15C + sample 21: + time = 111999 + flags = 1 + data = length 4000, hash DB365414 + sample 22: + time = 117332 + flags = 1 + data = length 4000, hash 31E08469 + sample 23: + time = 122665 + flags = 1 + data = length 4000, hash 315F5C28 + sample 24: + time = 128000 + flags = 1 + data = length 4000, hash CC65DF80 + sample 25: + time = 133333 + flags = 1 + data = length 4000, hash 503FB64C + sample 26: + time = 138666 + flags = 1 + data = length 4000, hash 817CF735 + sample 27: + time = 143999 + flags = 1 + data = length 4000, hash 37391ADA + sample 28: + time = 149332 + flags = 1 + data = length 4000, hash 37391ADA + sample 29: + time = 154665 + flags = 1 + data = length 4000, hash 64DBF751 + sample 30: + time = 160000 + flags = 1 + data = length 4000, hash 81AE828E + sample 31: + time = 165333 + flags = 1 + data = length 4000, hash 767D6C98 + sample 32: + time = 170666 + flags = 1 + data = length 4000, hash A5F6D4E + sample 33: + time = 175999 + flags = 1 + data = length 4000, hash EABC6B0D + sample 34: + time = 181332 + flags = 1 + data = length 4000, hash F47EF742 + sample 35: + time = 186665 + flags = 1 + data = length 4000, hash 9B2549DA + sample 36: + time = 192000 + flags = 1 + data = length 4000, hash A12733C9 + sample 37: + time = 197333 + flags = 1 + data = length 4000, hash 95F62E99 + sample 38: + time = 202666 + flags = 1 + data = length 4000, hash A4D858 + sample 39: + time = 207999 + flags = 1 + data = length 4000, hash A4D858 + sample 40: + time = 213332 + flags = 1 + data = length 4000, hash 22C1A129 + sample 41: + time = 218665 + flags = 1 + data = length 4000, hash 2C51E4A1 + sample 42: + time = 224000 + flags = 1 + data = length 4000, hash 3782E8BB + sample 43: + time = 229333 + flags = 1 + data = length 4000, hash 2C51E4A1 + sample 44: + time = 234666 + flags = 1 + data = length 4000, hash BDB3D129 + sample 45: + time = 239999 + flags = 1 + data = length 4000, hash F642A55 + sample 46: + time = 245332 + flags = 1 + data = length 4000, hash 32F259F4 + sample 47: + time = 250665 + flags = 1 + data = length 4000, hash 4C987B7C + sample 48: + time = 256000 + flags = 1 + data = length 4000, hash 57C98E1C + sample 49: + time = 261333 + flags = 1 + data = length 4000, hash 4C987B7C + sample 50: + time = 266666 + flags = 1 + data = length 4000, hash 4C987B7C + sample 51: + time = 271999 + flags = 1 + data = length 4000, hash 4C987B7C + sample 52: + time = 277332 + flags = 1 + data = length 4000, hash 4C987B7C + sample 53: + time = 282665 + flags = 1 + data = length 4000, hash 4C987B7C +tracksEnded = true diff --git a/testdata/src/test/assets/ts/sample_eac3joc.ec3 b/testdata/src/test/assets/ts/sample_eac3joc.ec3 new file mode 100644 index 0000000000..26ba6b4421 Binary files /dev/null and b/testdata/src/test/assets/ts/sample_eac3joc.ec3 differ diff --git a/testdata/src/test/assets/ts/sample_eac3joc.ec3.0.dump b/testdata/src/test/assets/ts/sample_eac3joc.ec3.0.dump new file mode 100644 index 0000000000..f8888698bd --- /dev/null +++ b/testdata/src/test/assets/ts/sample_eac3joc.ec3.0.dump @@ -0,0 +1,270 @@ +seekMap: + isSeekable = false + duration = UNSET TIME + getPosition(0) = [[timeUs=0, position=0]] +numberOfTracks = 1 +track 0: + total output bytes = 163840 + sample count = 64 + format 0: + id = 0 + sampleMimeType = audio/eac3-joc + channelCount = 6 + sampleRate = 48000 + sample 0: + time = 0 + flags = 1 + data = length 2560, hash 882594AD + sample 1: + time = 32000 + flags = 1 + data = length 2560, hash 41EC8B22 + sample 2: + time = 64000 + flags = 1 + data = length 2560, hash 67E6EFD4 + sample 3: + time = 96000 + flags = 1 + data = length 2560, hash A7E66AFD + sample 4: + time = 128000 + flags = 1 + data = length 2560, hash 3924116 + sample 5: + time = 160000 + flags = 1 + data = length 2560, hash 64DCE40B + sample 6: + time = 192000 + flags = 1 + data = length 2560, hash F2E0DA64 + sample 7: + time = 224000 + flags = 1 + data = length 2560, hash C156258B + sample 8: + time = 256000 + flags = 1 + data = length 2560, hash D8DBDCDE + sample 9: + time = 288000 + flags = 1 + data = length 2560, hash C11B2F25 + sample 10: + time = 320000 + flags = 1 + data = length 2560, hash B3C5612 + sample 11: + time = 352000 + flags = 1 + data = length 2560, hash A94B15D0 + sample 12: + time = 384000 + flags = 1 + data = length 2560, hash 12E4E306 + sample 13: + time = 416000 + flags = 1 + data = length 2560, hash 11CB959F + sample 14: + time = 448000 + flags = 1 + data = length 2560, hash B6433844 + sample 15: + time = 480000 + flags = 1 + data = length 2560, hash EA6DEB89 + sample 16: + time = 512000 + flags = 1 + data = length 2560, hash 6D65CBD9 + sample 17: + time = 544000 + flags = 1 + data = length 2560, hash A5D635C5 + sample 18: + time = 576000 + flags = 1 + data = length 2560, hash 992E36AB + sample 19: + time = 608000 + flags = 1 + data = length 2560, hash 1EC4E5AF + sample 20: + time = 640000 + flags = 1 + data = length 2560, hash DCFEB7D2 + sample 21: + time = 672000 + flags = 1 + data = length 2560, hash 45EFC639 + sample 22: + time = 704000 + flags = 1 + data = length 2560, hash F598673 + sample 23: + time = 736000 + flags = 1 + data = length 2560, hash 89E4E5EC + sample 24: + time = 768000 + flags = 1 + data = length 2560, hash FBE2532B + sample 25: + time = 800000 + flags = 1 + data = length 2560, hash 9CE5F83B + sample 26: + time = 832000 + flags = 1 + data = length 2560, hash 6ED49E2C + sample 27: + time = 864000 + flags = 1 + data = length 2560, hash BC52F8F3 + sample 28: + time = 896000 + flags = 1 + data = length 2560, hash 759203E2 + sample 29: + time = 928000 + flags = 1 + data = length 2560, hash D5D31AE9 + sample 30: + time = 960000 + flags = 1 + data = length 2560, hash 640A24ED + sample 31: + time = 992000 + flags = 1 + data = length 2560, hash 19B52B8B + sample 32: + time = 1024000 + flags = 1 + data = length 2560, hash 5DA977C3 + sample 33: + time = 1056000 + flags = 1 + data = length 2560, hash 982879DD + sample 34: + time = 1088000 + flags = 1 + data = length 2560, hash A7656B9C + sample 35: + time = 1120000 + flags = 1 + data = length 2560, hash 445CCC67 + sample 36: + time = 1152000 + flags = 1 + data = length 2560, hash ACD5CB5C + sample 37: + time = 1184000 + flags = 1 + data = length 2560, hash 175BBF26 + sample 38: + time = 1216000 + flags = 1 + data = length 2560, hash DBCBEB0 + sample 39: + time = 1248000 + flags = 1 + data = length 2560, hash DA39D991 + sample 40: + time = 1280000 + flags = 1 + data = length 2560, hash F08CC8E2 + sample 41: + time = 1312000 + flags = 1 + data = length 2560, hash 6B0842D7 + sample 42: + time = 1344000 + flags = 1 + data = length 2560, hash 9FE87594 + sample 43: + time = 1376000 + flags = 1 + data = length 2560, hash 8E62CE19 + sample 44: + time = 1408000 + flags = 1 + data = length 2560, hash 5FDC4084 + sample 45: + time = 1440000 + flags = 1 + data = length 2560, hash C32DAEE1 + sample 46: + time = 1472000 + flags = 1 + data = length 2560, hash BBEFB568 + sample 47: + time = 1504000 + flags = 1 + data = length 2560, hash 20504279 + sample 48: + time = 1536000 + flags = 1 + data = length 2560, hash 3B8192D2 + sample 49: + time = 1568000 + flags = 1 + data = length 2560, hash 4206B48 + sample 50: + time = 1600000 + flags = 1 + data = length 2560, hash B195AB53 + sample 51: + time = 1632000 + flags = 1 + data = length 2560, hash 3AA8E25F + sample 52: + time = 1664000 + flags = 1 + data = length 2560, hash BC227D7B + sample 53: + time = 1696000 + flags = 1 + data = length 2560, hash 6A34F7EA + sample 54: + time = 1728000 + flags = 1 + data = length 2560, hash F1E731C4 + sample 55: + time = 1760000 + flags = 1 + data = length 2560, hash 9CC406 + sample 56: + time = 1792000 + flags = 1 + data = length 2560, hash A1532233 + sample 57: + time = 1824000 + flags = 1 + data = length 2560, hash 98E49039 + sample 58: + time = 1856000 + flags = 1 + data = length 2560, hash 3F8B6DC0 + sample 59: + time = 1888000 + flags = 1 + data = length 2560, hash 4E7BF79F + sample 60: + time = 1920000 + flags = 1 + data = length 2560, hash 6DD6F2D7 + sample 61: + time = 1952000 + flags = 1 + data = length 2560, hash A05C0EC2 + sample 62: + time = 1984000 + flags = 1 + data = length 2560, hash 10C62F30 + sample 63: + time = 2016000 + flags = 1 + data = length 2560, hash EE4F848A +tracksEnded = true diff --git a/testdata/src/test/assets/ts/sample_eac3joc.ec3.unknown_length.dump b/testdata/src/test/assets/ts/sample_eac3joc.ec3.unknown_length.dump new file mode 100644 index 0000000000..f8888698bd --- /dev/null +++ b/testdata/src/test/assets/ts/sample_eac3joc.ec3.unknown_length.dump @@ -0,0 +1,270 @@ +seekMap: + isSeekable = false + duration = UNSET TIME + getPosition(0) = [[timeUs=0, position=0]] +numberOfTracks = 1 +track 0: + total output bytes = 163840 + sample count = 64 + format 0: + id = 0 + sampleMimeType = audio/eac3-joc + channelCount = 6 + sampleRate = 48000 + sample 0: + time = 0 + flags = 1 + data = length 2560, hash 882594AD + sample 1: + time = 32000 + flags = 1 + data = length 2560, hash 41EC8B22 + sample 2: + time = 64000 + flags = 1 + data = length 2560, hash 67E6EFD4 + sample 3: + time = 96000 + flags = 1 + data = length 2560, hash A7E66AFD + sample 4: + time = 128000 + flags = 1 + data = length 2560, hash 3924116 + sample 5: + time = 160000 + flags = 1 + data = length 2560, hash 64DCE40B + sample 6: + time = 192000 + flags = 1 + data = length 2560, hash F2E0DA64 + sample 7: + time = 224000 + flags = 1 + data = length 2560, hash C156258B + sample 8: + time = 256000 + flags = 1 + data = length 2560, hash D8DBDCDE + sample 9: + time = 288000 + flags = 1 + data = length 2560, hash C11B2F25 + sample 10: + time = 320000 + flags = 1 + data = length 2560, hash B3C5612 + sample 11: + time = 352000 + flags = 1 + data = length 2560, hash A94B15D0 + sample 12: + time = 384000 + flags = 1 + data = length 2560, hash 12E4E306 + sample 13: + time = 416000 + flags = 1 + data = length 2560, hash 11CB959F + sample 14: + time = 448000 + flags = 1 + data = length 2560, hash B6433844 + sample 15: + time = 480000 + flags = 1 + data = length 2560, hash EA6DEB89 + sample 16: + time = 512000 + flags = 1 + data = length 2560, hash 6D65CBD9 + sample 17: + time = 544000 + flags = 1 + data = length 2560, hash A5D635C5 + sample 18: + time = 576000 + flags = 1 + data = length 2560, hash 992E36AB + sample 19: + time = 608000 + flags = 1 + data = length 2560, hash 1EC4E5AF + sample 20: + time = 640000 + flags = 1 + data = length 2560, hash DCFEB7D2 + sample 21: + time = 672000 + flags = 1 + data = length 2560, hash 45EFC639 + sample 22: + time = 704000 + flags = 1 + data = length 2560, hash F598673 + sample 23: + time = 736000 + flags = 1 + data = length 2560, hash 89E4E5EC + sample 24: + time = 768000 + flags = 1 + data = length 2560, hash FBE2532B + sample 25: + time = 800000 + flags = 1 + data = length 2560, hash 9CE5F83B + sample 26: + time = 832000 + flags = 1 + data = length 2560, hash 6ED49E2C + sample 27: + time = 864000 + flags = 1 + data = length 2560, hash BC52F8F3 + sample 28: + time = 896000 + flags = 1 + data = length 2560, hash 759203E2 + sample 29: + time = 928000 + flags = 1 + data = length 2560, hash D5D31AE9 + sample 30: + time = 960000 + flags = 1 + data = length 2560, hash 640A24ED + sample 31: + time = 992000 + flags = 1 + data = length 2560, hash 19B52B8B + sample 32: + time = 1024000 + flags = 1 + data = length 2560, hash 5DA977C3 + sample 33: + time = 1056000 + flags = 1 + data = length 2560, hash 982879DD + sample 34: + time = 1088000 + flags = 1 + data = length 2560, hash A7656B9C + sample 35: + time = 1120000 + flags = 1 + data = length 2560, hash 445CCC67 + sample 36: + time = 1152000 + flags = 1 + data = length 2560, hash ACD5CB5C + sample 37: + time = 1184000 + flags = 1 + data = length 2560, hash 175BBF26 + sample 38: + time = 1216000 + flags = 1 + data = length 2560, hash DBCBEB0 + sample 39: + time = 1248000 + flags = 1 + data = length 2560, hash DA39D991 + sample 40: + time = 1280000 + flags = 1 + data = length 2560, hash F08CC8E2 + sample 41: + time = 1312000 + flags = 1 + data = length 2560, hash 6B0842D7 + sample 42: + time = 1344000 + flags = 1 + data = length 2560, hash 9FE87594 + sample 43: + time = 1376000 + flags = 1 + data = length 2560, hash 8E62CE19 + sample 44: + time = 1408000 + flags = 1 + data = length 2560, hash 5FDC4084 + sample 45: + time = 1440000 + flags = 1 + data = length 2560, hash C32DAEE1 + sample 46: + time = 1472000 + flags = 1 + data = length 2560, hash BBEFB568 + sample 47: + time = 1504000 + flags = 1 + data = length 2560, hash 20504279 + sample 48: + time = 1536000 + flags = 1 + data = length 2560, hash 3B8192D2 + sample 49: + time = 1568000 + flags = 1 + data = length 2560, hash 4206B48 + sample 50: + time = 1600000 + flags = 1 + data = length 2560, hash B195AB53 + sample 51: + time = 1632000 + flags = 1 + data = length 2560, hash 3AA8E25F + sample 52: + time = 1664000 + flags = 1 + data = length 2560, hash BC227D7B + sample 53: + time = 1696000 + flags = 1 + data = length 2560, hash 6A34F7EA + sample 54: + time = 1728000 + flags = 1 + data = length 2560, hash F1E731C4 + sample 55: + time = 1760000 + flags = 1 + data = length 2560, hash 9CC406 + sample 56: + time = 1792000 + flags = 1 + data = length 2560, hash A1532233 + sample 57: + time = 1824000 + flags = 1 + data = length 2560, hash 98E49039 + sample 58: + time = 1856000 + flags = 1 + data = length 2560, hash 3F8B6DC0 + sample 59: + time = 1888000 + flags = 1 + data = length 2560, hash 4E7BF79F + sample 60: + time = 1920000 + flags = 1 + data = length 2560, hash 6DD6F2D7 + sample 61: + time = 1952000 + flags = 1 + data = length 2560, hash A05C0EC2 + sample 62: + time = 1984000 + flags = 1 + data = length 2560, hash 10C62F30 + sample 63: + time = 2016000 + flags = 1 + data = length 2560, hash EE4F848A +tracksEnded = true diff --git a/testdata/src/test/assets/ts/sample_eac3joc.ts b/testdata/src/test/assets/ts/sample_eac3joc.ts new file mode 100644 index 0000000000..04bcf59287 Binary files /dev/null and b/testdata/src/test/assets/ts/sample_eac3joc.ts differ diff --git a/testdata/src/test/assets/ts/sample_eac3joc.ts.0.dump b/testdata/src/test/assets/ts/sample_eac3joc.ts.0.dump new file mode 100644 index 0000000000..a3cf812691 --- /dev/null +++ b/testdata/src/test/assets/ts/sample_eac3joc.ts.0.dump @@ -0,0 +1,273 @@ +seekMap: + isSeekable = true + duration = 2013977 + getPosition(0) = [[timeUs=0, position=0]] + getPosition(1) = [[timeUs=1, position=0]] + getPosition(1006988) = [[timeUs=1006988, position=101524]] + getPosition(2013977) = [[timeUs=2013977, position=203237]] +numberOfTracks = 1 +track 1900: + total output bytes = 163840 + sample count = 64 + format 0: + id = 1/1900 + sampleMimeType = audio/eac3-joc + channelCount = 6 + sampleRate = 48000 + sample 0: + time = 0 + flags = 1 + data = length 2560, hash 882594AD + sample 1: + time = 32000 + flags = 1 + data = length 2560, hash 41EC8B22 + sample 2: + time = 64000 + flags = 1 + data = length 2560, hash 67E6EFD4 + sample 3: + time = 96000 + flags = 1 + data = length 2560, hash A7E66AFD + sample 4: + time = 128000 + flags = 1 + data = length 2560, hash 3924116 + sample 5: + time = 160000 + flags = 1 + data = length 2560, hash 64DCE40B + sample 6: + time = 192000 + flags = 1 + data = length 2560, hash F2E0DA64 + sample 7: + time = 224000 + flags = 1 + data = length 2560, hash C156258B + sample 8: + time = 256000 + flags = 1 + data = length 2560, hash D8DBDCDE + sample 9: + time = 288000 + flags = 1 + data = length 2560, hash C11B2F25 + sample 10: + time = 320000 + flags = 1 + data = length 2560, hash B3C5612 + sample 11: + time = 352000 + flags = 1 + data = length 2560, hash A94B15D0 + sample 12: + time = 384000 + flags = 1 + data = length 2560, hash 12E4E306 + sample 13: + time = 416000 + flags = 1 + data = length 2560, hash 11CB959F + sample 14: + time = 448000 + flags = 1 + data = length 2560, hash B6433844 + sample 15: + time = 480000 + flags = 1 + data = length 2560, hash EA6DEB89 + sample 16: + time = 512000 + flags = 1 + data = length 2560, hash 6D65CBD9 + sample 17: + time = 544000 + flags = 1 + data = length 2560, hash A5D635C5 + sample 18: + time = 576000 + flags = 1 + data = length 2560, hash 992E36AB + sample 19: + time = 608000 + flags = 1 + data = length 2560, hash 1EC4E5AF + sample 20: + time = 640000 + flags = 1 + data = length 2560, hash DCFEB7D2 + sample 21: + time = 672000 + flags = 1 + data = length 2560, hash 45EFC639 + sample 22: + time = 704000 + flags = 1 + data = length 2560, hash F598673 + sample 23: + time = 736000 + flags = 1 + data = length 2560, hash 89E4E5EC + sample 24: + time = 768000 + flags = 1 + data = length 2560, hash FBE2532B + sample 25: + time = 800000 + flags = 1 + data = length 2560, hash 9CE5F83B + sample 26: + time = 832000 + flags = 1 + data = length 2560, hash 6ED49E2C + sample 27: + time = 864000 + flags = 1 + data = length 2560, hash BC52F8F3 + sample 28: + time = 896000 + flags = 1 + data = length 2560, hash 759203E2 + sample 29: + time = 928000 + flags = 1 + data = length 2560, hash D5D31AE9 + sample 30: + time = 960000 + flags = 1 + data = length 2560, hash 640A24ED + sample 31: + time = 992000 + flags = 1 + data = length 2560, hash 19B52B8B + sample 32: + time = 1024000 + flags = 1 + data = length 2560, hash 5DA977C3 + sample 33: + time = 1056000 + flags = 1 + data = length 2560, hash 982879DD + sample 34: + time = 1088000 + flags = 1 + data = length 2560, hash A7656B9C + sample 35: + time = 1120000 + flags = 1 + data = length 2560, hash 445CCC67 + sample 36: + time = 1152000 + flags = 1 + data = length 2560, hash ACD5CB5C + sample 37: + time = 1184000 + flags = 1 + data = length 2560, hash 175BBF26 + sample 38: + time = 1216000 + flags = 1 + data = length 2560, hash DBCBEB0 + sample 39: + time = 1248000 + flags = 1 + data = length 2560, hash DA39D991 + sample 40: + time = 1280000 + flags = 1 + data = length 2560, hash F08CC8E2 + sample 41: + time = 1312000 + flags = 1 + data = length 2560, hash 6B0842D7 + sample 42: + time = 1344000 + flags = 1 + data = length 2560, hash 9FE87594 + sample 43: + time = 1376000 + flags = 1 + data = length 2560, hash 8E62CE19 + sample 44: + time = 1408000 + flags = 1 + data = length 2560, hash 5FDC4084 + sample 45: + time = 1440000 + flags = 1 + data = length 2560, hash C32DAEE1 + sample 46: + time = 1472000 + flags = 1 + data = length 2560, hash BBEFB568 + sample 47: + time = 1504000 + flags = 1 + data = length 2560, hash 20504279 + sample 48: + time = 1536000 + flags = 1 + data = length 2560, hash 3B8192D2 + sample 49: + time = 1568000 + flags = 1 + data = length 2560, hash 4206B48 + sample 50: + time = 1600000 + flags = 1 + data = length 2560, hash B195AB53 + sample 51: + time = 1632000 + flags = 1 + data = length 2560, hash 3AA8E25F + sample 52: + time = 1664000 + flags = 1 + data = length 2560, hash BC227D7B + sample 53: + time = 1696000 + flags = 1 + data = length 2560, hash 6A34F7EA + sample 54: + time = 1728000 + flags = 1 + data = length 2560, hash F1E731C4 + sample 55: + time = 1760000 + flags = 1 + data = length 2560, hash 9CC406 + sample 56: + time = 1792000 + flags = 1 + data = length 2560, hash A1532233 + sample 57: + time = 1824000 + flags = 1 + data = length 2560, hash 98E49039 + sample 58: + time = 1856000 + flags = 1 + data = length 2560, hash 3F8B6DC0 + sample 59: + time = 1888000 + flags = 1 + data = length 2560, hash 4E7BF79F + sample 60: + time = 1920000 + flags = 1 + data = length 2560, hash 6DD6F2D7 + sample 61: + time = 1952000 + flags = 1 + data = length 2560, hash A05C0EC2 + sample 62: + time = 1984000 + flags = 1 + data = length 2560, hash 10C62F30 + sample 63: + time = 2016000 + flags = 1 + data = length 2560, hash EE4F848A +tracksEnded = true diff --git a/testdata/src/test/assets/ts/sample_eac3joc.ts.1.dump b/testdata/src/test/assets/ts/sample_eac3joc.ts.1.dump new file mode 100644 index 0000000000..77951bd767 --- /dev/null +++ b/testdata/src/test/assets/ts/sample_eac3joc.ts.1.dump @@ -0,0 +1,193 @@ +seekMap: + isSeekable = true + duration = 2013977 + getPosition(0) = [[timeUs=0, position=0]] + getPosition(1) = [[timeUs=1, position=0]] + getPosition(1006988) = [[timeUs=1006988, position=101524]] + getPosition(2013977) = [[timeUs=2013977, position=203237]] +numberOfTracks = 1 +track 1900: + total output bytes = 112640 + sample count = 44 + format 0: + id = 1/1900 + sampleMimeType = audio/eac3-joc + channelCount = 6 + sampleRate = 48000 + sample 0: + time = 640000 + flags = 1 + data = length 2560, hash DCFEB7D2 + sample 1: + time = 672000 + flags = 1 + data = length 2560, hash 45EFC639 + sample 2: + time = 704000 + flags = 1 + data = length 2560, hash F598673 + sample 3: + time = 736000 + flags = 1 + data = length 2560, hash 89E4E5EC + sample 4: + time = 768000 + flags = 1 + data = length 2560, hash FBE2532B + sample 5: + time = 800000 + flags = 1 + data = length 2560, hash 9CE5F83B + sample 6: + time = 832000 + flags = 1 + data = length 2560, hash 6ED49E2C + sample 7: + time = 864000 + flags = 1 + data = length 2560, hash BC52F8F3 + sample 8: + time = 896000 + flags = 1 + data = length 2560, hash 759203E2 + sample 9: + time = 928000 + flags = 1 + data = length 2560, hash D5D31AE9 + sample 10: + time = 960000 + flags = 1 + data = length 2560, hash 640A24ED + sample 11: + time = 992000 + flags = 1 + data = length 2560, hash 19B52B8B + sample 12: + time = 1024000 + flags = 1 + data = length 2560, hash 5DA977C3 + sample 13: + time = 1056000 + flags = 1 + data = length 2560, hash 982879DD + sample 14: + time = 1088000 + flags = 1 + data = length 2560, hash A7656B9C + sample 15: + time = 1120000 + flags = 1 + data = length 2560, hash 445CCC67 + sample 16: + time = 1152000 + flags = 1 + data = length 2560, hash ACD5CB5C + sample 17: + time = 1184000 + flags = 1 + data = length 2560, hash 175BBF26 + sample 18: + time = 1216000 + flags = 1 + data = length 2560, hash DBCBEB0 + sample 19: + time = 1248000 + flags = 1 + data = length 2560, hash DA39D991 + sample 20: + time = 1280000 + flags = 1 + data = length 2560, hash F08CC8E2 + sample 21: + time = 1312000 + flags = 1 + data = length 2560, hash 6B0842D7 + sample 22: + time = 1344000 + flags = 1 + data = length 2560, hash 9FE87594 + sample 23: + time = 1376000 + flags = 1 + data = length 2560, hash 8E62CE19 + sample 24: + time = 1408000 + flags = 1 + data = length 2560, hash 5FDC4084 + sample 25: + time = 1440000 + flags = 1 + data = length 2560, hash C32DAEE1 + sample 26: + time = 1472000 + flags = 1 + data = length 2560, hash BBEFB568 + sample 27: + time = 1504000 + flags = 1 + data = length 2560, hash 20504279 + sample 28: + time = 1536000 + flags = 1 + data = length 2560, hash 3B8192D2 + sample 29: + time = 1568000 + flags = 1 + data = length 2560, hash 4206B48 + sample 30: + time = 1600000 + flags = 1 + data = length 2560, hash B195AB53 + sample 31: + time = 1632000 + flags = 1 + data = length 2560, hash 3AA8E25F + sample 32: + time = 1664000 + flags = 1 + data = length 2560, hash BC227D7B + sample 33: + time = 1696000 + flags = 1 + data = length 2560, hash 6A34F7EA + sample 34: + time = 1728000 + flags = 1 + data = length 2560, hash F1E731C4 + sample 35: + time = 1760000 + flags = 1 + data = length 2560, hash 9CC406 + sample 36: + time = 1792000 + flags = 1 + data = length 2560, hash A1532233 + sample 37: + time = 1824000 + flags = 1 + data = length 2560, hash 98E49039 + sample 38: + time = 1856000 + flags = 1 + data = length 2560, hash 3F8B6DC0 + sample 39: + time = 1888000 + flags = 1 + data = length 2560, hash 4E7BF79F + sample 40: + time = 1920000 + flags = 1 + data = length 2560, hash 6DD6F2D7 + sample 41: + time = 1952000 + flags = 1 + data = length 2560, hash A05C0EC2 + sample 42: + time = 1984000 + flags = 1 + data = length 2560, hash 10C62F30 + sample 43: + time = 2016000 + flags = 1 + data = length 2560, hash EE4F848A +tracksEnded = true diff --git a/testdata/src/test/assets/ts/sample_eac3joc.ts.2.dump b/testdata/src/test/assets/ts/sample_eac3joc.ts.2.dump new file mode 100644 index 0000000000..0354754df2 --- /dev/null +++ b/testdata/src/test/assets/ts/sample_eac3joc.ts.2.dump @@ -0,0 +1,105 @@ +seekMap: + isSeekable = true + duration = 2013977 + getPosition(0) = [[timeUs=0, position=0]] + getPosition(1) = [[timeUs=1, position=0]] + getPosition(1006988) = [[timeUs=1006988, position=101524]] + getPosition(2013977) = [[timeUs=2013977, position=203237]] +numberOfTracks = 1 +track 1900: + total output bytes = 56320 + sample count = 22 + format 0: + id = 1/1900 + sampleMimeType = audio/eac3-joc + channelCount = 6 + sampleRate = 48000 + sample 0: + time = 1344000 + flags = 1 + data = length 2560, hash 9FE87594 + sample 1: + time = 1376000 + flags = 1 + data = length 2560, hash 8E62CE19 + sample 2: + time = 1408000 + flags = 1 + data = length 2560, hash 5FDC4084 + sample 3: + time = 1440000 + flags = 1 + data = length 2560, hash C32DAEE1 + sample 4: + time = 1472000 + flags = 1 + data = length 2560, hash BBEFB568 + sample 5: + time = 1504000 + flags = 1 + data = length 2560, hash 20504279 + sample 6: + time = 1536000 + flags = 1 + data = length 2560, hash 3B8192D2 + sample 7: + time = 1568000 + flags = 1 + data = length 2560, hash 4206B48 + sample 8: + time = 1600000 + flags = 1 + data = length 2560, hash B195AB53 + sample 9: + time = 1632000 + flags = 1 + data = length 2560, hash 3AA8E25F + sample 10: + time = 1664000 + flags = 1 + data = length 2560, hash BC227D7B + sample 11: + time = 1696000 + flags = 1 + data = length 2560, hash 6A34F7EA + sample 12: + time = 1728000 + flags = 1 + data = length 2560, hash F1E731C4 + sample 13: + time = 1760000 + flags = 1 + data = length 2560, hash 9CC406 + sample 14: + time = 1792000 + flags = 1 + data = length 2560, hash A1532233 + sample 15: + time = 1824000 + flags = 1 + data = length 2560, hash 98E49039 + sample 16: + time = 1856000 + flags = 1 + data = length 2560, hash 3F8B6DC0 + sample 17: + time = 1888000 + flags = 1 + data = length 2560, hash 4E7BF79F + sample 18: + time = 1920000 + flags = 1 + data = length 2560, hash 6DD6F2D7 + sample 19: + time = 1952000 + flags = 1 + data = length 2560, hash A05C0EC2 + sample 20: + time = 1984000 + flags = 1 + data = length 2560, hash 10C62F30 + sample 21: + time = 2016000 + flags = 1 + data = length 2560, hash EE4F848A +tracksEnded = true diff --git a/testdata/src/test/assets/ts/sample_eac3joc.ts.3.dump b/testdata/src/test/assets/ts/sample_eac3joc.ts.3.dump new file mode 100644 index 0000000000..742d87e271 --- /dev/null +++ b/testdata/src/test/assets/ts/sample_eac3joc.ts.3.dump @@ -0,0 +1,25 @@ +seekMap: + isSeekable = true + duration = 2013977 + getPosition(0) = [[timeUs=0, position=0]] + getPosition(1) = [[timeUs=1, position=0]] + getPosition(1006988) = [[timeUs=1006988, position=101524]] + getPosition(2013977) = [[timeUs=2013977, position=203237]] +numberOfTracks = 1 +track 1900: + total output bytes = 5120 + sample count = 2 + format 0: + id = 1/1900 + sampleMimeType = audio/eac3-joc + channelCount = 6 + sampleRate = 48000 + sample 0: + time = 1984000 + flags = 1 + data = length 2560, hash 10C62F30 + sample 1: + time = 2016000 + flags = 1 + data = length 2560, hash EE4F848A +tracksEnded = true diff --git a/testdata/src/test/assets/ts/sample_eac3joc.ts.unknown_length.dump b/testdata/src/test/assets/ts/sample_eac3joc.ts.unknown_length.dump new file mode 100644 index 0000000000..269dd63593 --- /dev/null +++ b/testdata/src/test/assets/ts/sample_eac3joc.ts.unknown_length.dump @@ -0,0 +1,270 @@ +seekMap: + isSeekable = false + duration = UNSET TIME + getPosition(0) = [[timeUs=0, position=0]] +numberOfTracks = 1 +track 1900: + total output bytes = 163840 + sample count = 64 + format 0: + id = 1/1900 + sampleMimeType = audio/eac3-joc + channelCount = 6 + sampleRate = 48000 + sample 0: + time = 0 + flags = 1 + data = length 2560, hash 882594AD + sample 1: + time = 32000 + flags = 1 + data = length 2560, hash 41EC8B22 + sample 2: + time = 64000 + flags = 1 + data = length 2560, hash 67E6EFD4 + sample 3: + time = 96000 + flags = 1 + data = length 2560, hash A7E66AFD + sample 4: + time = 128000 + flags = 1 + data = length 2560, hash 3924116 + sample 5: + time = 160000 + flags = 1 + data = length 2560, hash 64DCE40B + sample 6: + time = 192000 + flags = 1 + data = length 2560, hash F2E0DA64 + sample 7: + time = 224000 + flags = 1 + data = length 2560, hash C156258B + sample 8: + time = 256000 + flags = 1 + data = length 2560, hash D8DBDCDE + sample 9: + time = 288000 + flags = 1 + data = length 2560, hash C11B2F25 + sample 10: + time = 320000 + flags = 1 + data = length 2560, hash B3C5612 + sample 11: + time = 352000 + flags = 1 + data = length 2560, hash A94B15D0 + sample 12: + time = 384000 + flags = 1 + data = length 2560, hash 12E4E306 + sample 13: + time = 416000 + flags = 1 + data = length 2560, hash 11CB959F + sample 14: + time = 448000 + flags = 1 + data = length 2560, hash B6433844 + sample 15: + time = 480000 + flags = 1 + data = length 2560, hash EA6DEB89 + sample 16: + time = 512000 + flags = 1 + data = length 2560, hash 6D65CBD9 + sample 17: + time = 544000 + flags = 1 + data = length 2560, hash A5D635C5 + sample 18: + time = 576000 + flags = 1 + data = length 2560, hash 992E36AB + sample 19: + time = 608000 + flags = 1 + data = length 2560, hash 1EC4E5AF + sample 20: + time = 640000 + flags = 1 + data = length 2560, hash DCFEB7D2 + sample 21: + time = 672000 + flags = 1 + data = length 2560, hash 45EFC639 + sample 22: + time = 704000 + flags = 1 + data = length 2560, hash F598673 + sample 23: + time = 736000 + flags = 1 + data = length 2560, hash 89E4E5EC + sample 24: + time = 768000 + flags = 1 + data = length 2560, hash FBE2532B + sample 25: + time = 800000 + flags = 1 + data = length 2560, hash 9CE5F83B + sample 26: + time = 832000 + flags = 1 + data = length 2560, hash 6ED49E2C + sample 27: + time = 864000 + flags = 1 + data = length 2560, hash BC52F8F3 + sample 28: + time = 896000 + flags = 1 + data = length 2560, hash 759203E2 + sample 29: + time = 928000 + flags = 1 + data = length 2560, hash D5D31AE9 + sample 30: + time = 960000 + flags = 1 + data = length 2560, hash 640A24ED + sample 31: + time = 992000 + flags = 1 + data = length 2560, hash 19B52B8B + sample 32: + time = 1024000 + flags = 1 + data = length 2560, hash 5DA977C3 + sample 33: + time = 1056000 + flags = 1 + data = length 2560, hash 982879DD + sample 34: + time = 1088000 + flags = 1 + data = length 2560, hash A7656B9C + sample 35: + time = 1120000 + flags = 1 + data = length 2560, hash 445CCC67 + sample 36: + time = 1152000 + flags = 1 + data = length 2560, hash ACD5CB5C + sample 37: + time = 1184000 + flags = 1 + data = length 2560, hash 175BBF26 + sample 38: + time = 1216000 + flags = 1 + data = length 2560, hash DBCBEB0 + sample 39: + time = 1248000 + flags = 1 + data = length 2560, hash DA39D991 + sample 40: + time = 1280000 + flags = 1 + data = length 2560, hash F08CC8E2 + sample 41: + time = 1312000 + flags = 1 + data = length 2560, hash 6B0842D7 + sample 42: + time = 1344000 + flags = 1 + data = length 2560, hash 9FE87594 + sample 43: + time = 1376000 + flags = 1 + data = length 2560, hash 8E62CE19 + sample 44: + time = 1408000 + flags = 1 + data = length 2560, hash 5FDC4084 + sample 45: + time = 1440000 + flags = 1 + data = length 2560, hash C32DAEE1 + sample 46: + time = 1472000 + flags = 1 + data = length 2560, hash BBEFB568 + sample 47: + time = 1504000 + flags = 1 + data = length 2560, hash 20504279 + sample 48: + time = 1536000 + flags = 1 + data = length 2560, hash 3B8192D2 + sample 49: + time = 1568000 + flags = 1 + data = length 2560, hash 4206B48 + sample 50: + time = 1600000 + flags = 1 + data = length 2560, hash B195AB53 + sample 51: + time = 1632000 + flags = 1 + data = length 2560, hash 3AA8E25F + sample 52: + time = 1664000 + flags = 1 + data = length 2560, hash BC227D7B + sample 53: + time = 1696000 + flags = 1 + data = length 2560, hash 6A34F7EA + sample 54: + time = 1728000 + flags = 1 + data = length 2560, hash F1E731C4 + sample 55: + time = 1760000 + flags = 1 + data = length 2560, hash 9CC406 + sample 56: + time = 1792000 + flags = 1 + data = length 2560, hash A1532233 + sample 57: + time = 1824000 + flags = 1 + data = length 2560, hash 98E49039 + sample 58: + time = 1856000 + flags = 1 + data = length 2560, hash 3F8B6DC0 + sample 59: + time = 1888000 + flags = 1 + data = length 2560, hash 4E7BF79F + sample 60: + time = 1920000 + flags = 1 + data = length 2560, hash 6DD6F2D7 + sample 61: + time = 1952000 + flags = 1 + data = length 2560, hash A05C0EC2 + sample 62: + time = 1984000 + flags = 1 + data = length 2560, hash 10C62F30 + sample 63: + time = 2016000 + flags = 1 + data = length 2560, hash EE4F848A +tracksEnded = true diff --git a/library/core/src/test/assets/ts/sample.ps b/testdata/src/test/assets/ts/sample_h262_mpeg_audio.ps similarity index 100% rename from library/core/src/test/assets/ts/sample.ps rename to testdata/src/test/assets/ts/sample_h262_mpeg_audio.ps diff --git a/library/core/src/test/assets/ts/sample.ps.0.dump b/testdata/src/test/assets/ts/sample_h262_mpeg_audio.ps.0.dump similarity index 57% rename from library/core/src/test/assets/ts/sample.ps.0.dump rename to testdata/src/test/assets/ts/sample_h262_mpeg_audio.ps.0.dump index 06ef48de7a..d62351650b 100644 --- a/library/core/src/test/assets/ts/sample.ps.0.dump +++ b/testdata/src/test/assets/ts/sample_h262_mpeg_audio.ps.0.dump @@ -2,31 +2,19 @@ seekMap: isSeekable = true duration = 766 getPosition(0) = [[timeUs=0, position=0]] + getPosition(1) = [[timeUs=1, position=0]] + getPosition(383) = [[timeUs=383, position=23128]] + getPosition(766) = [[timeUs=766, position=46445]] 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 + format 0: + id = 192 + sampleMimeType = audio/mpeg-L2 + maxInputSize = 4096 + channelCount = 1 + sampleRate = 44100 sample 0: time = 29088 flags = 1 @@ -44,30 +32,15 @@ track 192: 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 + format 0: + id = 224 + sampleMimeType = video/mpeg2 + width = 640 + height = 426 + initializationData: + data = length 22, hash 743CC6F8 sample 0: time = 40000 flags = 1 diff --git a/testdata/src/test/assets/ts/sample_h262_mpeg_audio.ps.1.dump b/testdata/src/test/assets/ts/sample_h262_mpeg_audio.ps.1.dump new file mode 100644 index 0000000000..9bdd929c42 --- /dev/null +++ b/testdata/src/test/assets/ts/sample_h262_mpeg_audio.ps.1.dump @@ -0,0 +1,32 @@ +seekMap: + isSeekable = true + duration = 766 + getPosition(0) = [[timeUs=0, position=0]] + getPosition(1) = [[timeUs=1, position=0]] + getPosition(383) = [[timeUs=383, position=23128]] + getPosition(766) = [[timeUs=766, position=46445]] +numberOfTracks = 2 +track 192: + total output bytes = 0 + sample count = 0 + format 0: + id = 192 + sampleMimeType = audio/mpeg-L2 + maxInputSize = 4096 + channelCount = 1 + sampleRate = 44100 +track 224: + total output bytes = 33949 + sample count = 1 + format 0: + id = 224 + sampleMimeType = video/mpeg2 + width = 640 + height = 426 + initializationData: + data = length 22, hash 743CC6F8 + sample 0: + time = 80000 + flags = 0 + data = length 17831, hash 5C5A57F5 +tracksEnded = true diff --git a/testdata/src/test/assets/ts/sample_h262_mpeg_audio.ps.2.dump b/testdata/src/test/assets/ts/sample_h262_mpeg_audio.ps.2.dump new file mode 100644 index 0000000000..ef5e2476d2 --- /dev/null +++ b/testdata/src/test/assets/ts/sample_h262_mpeg_audio.ps.2.dump @@ -0,0 +1,28 @@ +seekMap: + isSeekable = true + duration = 766 + getPosition(0) = [[timeUs=0, position=0]] + getPosition(1) = [[timeUs=1, position=0]] + getPosition(383) = [[timeUs=383, position=23128]] + getPosition(766) = [[timeUs=766, position=46445]] +numberOfTracks = 2 +track 192: + total output bytes = 0 + sample count = 0 + format 0: + id = 192 + sampleMimeType = audio/mpeg-L2 + maxInputSize = 4096 + channelCount = 1 + sampleRate = 44100 +track 224: + total output bytes = 19791 + sample count = 0 + format 0: + id = 224 + sampleMimeType = video/mpeg2 + width = 640 + height = 426 + initializationData: + data = length 22, hash 743CC6F8 +tracksEnded = true diff --git a/testdata/src/test/assets/ts/sample_h262_mpeg_audio.ps.3.dump b/testdata/src/test/assets/ts/sample_h262_mpeg_audio.ps.3.dump new file mode 100644 index 0000000000..59dabce629 --- /dev/null +++ b/testdata/src/test/assets/ts/sample_h262_mpeg_audio.ps.3.dump @@ -0,0 +1,28 @@ +seekMap: + isSeekable = true + duration = 766 + getPosition(0) = [[timeUs=0, position=0]] + getPosition(1) = [[timeUs=1, position=0]] + getPosition(383) = [[timeUs=383, position=23128]] + getPosition(766) = [[timeUs=766, position=46445]] +numberOfTracks = 2 +track 192: + total output bytes = 0 + sample count = 0 + format 0: + id = 192 + sampleMimeType = audio/mpeg-L2 + maxInputSize = 4096 + channelCount = 1 + sampleRate = 44100 +track 224: + total output bytes = 1585 + sample count = 0 + format 0: + id = 224 + sampleMimeType = video/mpeg2 + width = 640 + height = 426 + initializationData: + data = length 22, hash 743CC6F8 +tracksEnded = true diff --git a/library/core/src/test/assets/ts/sample.ps.unklen.dump b/testdata/src/test/assets/ts/sample_h262_mpeg_audio.ps.unknown_length.dump similarity index 57% rename from library/core/src/test/assets/ts/sample.ps.unklen.dump rename to testdata/src/test/assets/ts/sample_h262_mpeg_audio.ps.unknown_length.dump index dda6de8ab4..5edc0b708a 100644 --- a/library/core/src/test/assets/ts/sample.ps.unklen.dump +++ b/testdata/src/test/assets/ts/sample_h262_mpeg_audio.ps.unknown_length.dump @@ -4,29 +4,14 @@ seekMap: 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 + format 0: + id = 192 + sampleMimeType = audio/mpeg-L2 + maxInputSize = 4096 + channelCount = 1 + sampleRate = 44100 sample 0: time = 29088 flags = 1 @@ -44,30 +29,15 @@ track 192: 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 + format 0: + id = 224 + sampleMimeType = video/mpeg2 + width = 640 + height = 426 + initializationData: + data = length 22, hash 743CC6F8 sample 0: time = 40000 flags = 1 diff --git a/library/core/src/test/assets/ts/sample.ts b/testdata/src/test/assets/ts/sample_h262_mpeg_audio.ts similarity index 100% rename from library/core/src/test/assets/ts/sample.ts rename to testdata/src/test/assets/ts/sample_h262_mpeg_audio.ts diff --git a/testdata/src/test/assets/ts/sample_h262_mpeg_audio.ts.0.dump b/testdata/src/test/assets/ts/sample_h262_mpeg_audio.ts.0.dump new file mode 100644 index 0000000000..c7160e4b45 --- /dev/null +++ b/testdata/src/test/assets/ts/sample_h262_mpeg_audio.ts.0.dump @@ -0,0 +1,59 @@ +seekMap: + isSeekable = true + duration = 66733 + getPosition(0) = [[timeUs=0, position=0]] + getPosition(1) = [[timeUs=1, position=0]] + getPosition(33366) = [[timeUs=33366, position=25709]] + getPosition(66733) = [[timeUs=66733, position=51606]] +numberOfTracks = 3 +track 256: + total output bytes = 45026 + sample count = 2 + format 0: + id = 1/256 + sampleMimeType = video/mpeg2 + width = 640 + height = 426 + initializationData: + data = length 22, hash CE183139 + sample 0: + time = 33366 + flags = 1 + data = length 20711, hash 34341E8 + sample 1: + time = 66733 + flags = 0 + data = length 18112, hash EC44B35B +track 257: + total output bytes = 5015 + sample count = 4 + format 0: + id = 1/257 + sampleMimeType = audio/mpeg-L2 + maxInputSize = 4096 + channelCount = 1 + sampleRate = 44100 + language = und + 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: + total output bytes = 0 + sample count = 0 + format 0: + id = 1/8448 + sampleMimeType = application/cea-608 +tracksEnded = true diff --git a/testdata/src/test/assets/ts/sample_h262_mpeg_audio.ts.1.dump b/testdata/src/test/assets/ts/sample_h262_mpeg_audio.ts.1.dump new file mode 100644 index 0000000000..cf45b1f875 --- /dev/null +++ b/testdata/src/test/assets/ts/sample_h262_mpeg_audio.ts.1.dump @@ -0,0 +1,59 @@ +seekMap: + isSeekable = true + duration = 66733 + getPosition(0) = [[timeUs=0, position=0]] + getPosition(1) = [[timeUs=1, position=0]] + getPosition(33366) = [[timeUs=33366, position=25709]] + getPosition(66733) = [[timeUs=66733, position=51606]] +numberOfTracks = 3 +track 256: + total output bytes = 45026 + sample count = 2 + format 0: + id = 1/256 + sampleMimeType = video/mpeg2 + width = 640 + height = 426 + initializationData: + data = length 22, hash CE183139 + sample 0: + time = 55610 + flags = 1 + data = length 20711, hash 34341E8 + sample 1: + time = 88977 + flags = 0 + data = length 18112, hash EC44B35B +track 257: + total output bytes = 5015 + sample count = 4 + format 0: + id = 1/257 + sampleMimeType = audio/mpeg-L2 + maxInputSize = 4096 + channelCount = 1 + sampleRate = 44100 + language = und + sample 0: + time = 44699 + flags = 1 + data = length 1253, hash 727FD1C6 + sample 1: + time = 70821 + flags = 1 + data = length 1254, hash 73FB07B8 + sample 2: + time = 96944 + flags = 1 + data = length 1254, hash 73FB07B8 + sample 3: + time = 123066 + flags = 1 + data = length 1254, hash 73FB07B8 +track 8448: + total output bytes = 0 + sample count = 0 + format 0: + id = 1/8448 + sampleMimeType = application/cea-608 +tracksEnded = true diff --git a/testdata/src/test/assets/ts/sample_h262_mpeg_audio.ts.2.dump b/testdata/src/test/assets/ts/sample_h262_mpeg_audio.ts.2.dump new file mode 100644 index 0000000000..bc9786b5b9 --- /dev/null +++ b/testdata/src/test/assets/ts/sample_h262_mpeg_audio.ts.2.dump @@ -0,0 +1,59 @@ +seekMap: + isSeekable = true + duration = 66733 + getPosition(0) = [[timeUs=0, position=0]] + getPosition(1) = [[timeUs=1, position=0]] + getPosition(33366) = [[timeUs=33366, position=25709]] + getPosition(66733) = [[timeUs=66733, position=51606]] +numberOfTracks = 3 +track 256: + total output bytes = 45026 + sample count = 2 + format 0: + id = 1/256 + sampleMimeType = video/mpeg2 + width = 640 + height = 426 + initializationData: + data = length 22, hash CE183139 + sample 0: + time = 77854 + flags = 1 + data = length 20711, hash 34341E8 + sample 1: + time = 111221 + flags = 0 + data = length 18112, hash EC44B35B +track 257: + total output bytes = 5015 + sample count = 4 + format 0: + id = 1/257 + sampleMimeType = audio/mpeg-L2 + maxInputSize = 4096 + channelCount = 1 + sampleRate = 44100 + language = und + sample 0: + time = 66943 + flags = 1 + data = length 1253, hash 727FD1C6 + sample 1: + time = 93065 + flags = 1 + data = length 1254, hash 73FB07B8 + sample 2: + time = 119188 + flags = 1 + data = length 1254, hash 73FB07B8 + sample 3: + time = 145310 + flags = 1 + data = length 1254, hash 73FB07B8 +track 8448: + total output bytes = 0 + sample count = 0 + format 0: + id = 1/8448 + sampleMimeType = application/cea-608 +tracksEnded = true diff --git a/testdata/src/test/assets/ts/sample_h262_mpeg_audio.ts.3.dump b/testdata/src/test/assets/ts/sample_h262_mpeg_audio.ts.3.dump new file mode 100644 index 0000000000..727fa382bd --- /dev/null +++ b/testdata/src/test/assets/ts/sample_h262_mpeg_audio.ts.3.dump @@ -0,0 +1,43 @@ +seekMap: + isSeekable = true + duration = 66733 + getPosition(0) = [[timeUs=0, position=0]] + getPosition(1) = [[timeUs=1, position=0]] + getPosition(33366) = [[timeUs=33366, position=25709]] + getPosition(66733) = [[timeUs=66733, position=51606]] +numberOfTracks = 3 +track 256: + total output bytes = 0 + sample count = 0 + format 0: + id = 1/256 + sampleMimeType = video/mpeg2 + width = 640 + height = 426 + initializationData: + data = length 22, hash CE183139 +track 257: + total output bytes = 2508 + sample count = 2 + format 0: + id = 1/257 + sampleMimeType = audio/mpeg-L2 + maxInputSize = 4096 + channelCount = 1 + sampleRate = 44100 + language = und + sample 0: + time = 66733 + flags = 1 + data = length 1254, hash 73FB07B8 + sample 1: + time = 92855 + flags = 1 + data = length 1254, hash 73FB07B8 +track 8448: + total output bytes = 0 + sample count = 0 + format 0: + id = 1/8448 + sampleMimeType = application/cea-608 +tracksEnded = true diff --git a/library/core/src/test/assets/ts/sample.ts.unklen.dump b/testdata/src/test/assets/ts/sample_h262_mpeg_audio.ts.unknown_length.dump similarity index 50% rename from library/core/src/test/assets/ts/sample.ts.unklen.dump rename to testdata/src/test/assets/ts/sample_h262_mpeg_audio.ts.unknown_length.dump index 56f6b01a9c..e18961d73b 100644 --- a/library/core/src/test/assets/ts/sample.ts.unklen.dump +++ b/testdata/src/test/assets/ts/sample_h262_mpeg_audio.ts.unknown_length.dump @@ -4,30 +4,15 @@ seekMap: 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 + format 0: + id = 1/256 + sampleMimeType = video/mpeg2 + width = 640 + height = 426 + initializationData: + data = length 22, hash CE183139 sample 0: time = 33366 flags = 1 @@ -37,29 +22,15 @@ track 256: 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 + format 0: + id = 1/257 + sampleMimeType = audio/mpeg-L2 + maxInputSize = 4096 + channelCount = 1 + sampleRate = 44100 + language = und sample 0: time = 22455 flags = 1 @@ -77,27 +48,9 @@ track 257: 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 + format 0: + id = 1/8448 + sampleMimeType = application/cea-608 tracksEnded = true diff --git a/testdata/src/test/assets/ts/sample_h264_dts_audio.ts b/testdata/src/test/assets/ts/sample_h264_dts_audio.ts new file mode 100644 index 0000000000..e9aafd77c7 Binary files /dev/null and b/testdata/src/test/assets/ts/sample_h264_dts_audio.ts differ diff --git a/testdata/src/test/assets/ts/sample_h264_dts_audio.ts.0.dump b/testdata/src/test/assets/ts/sample_h264_dts_audio.ts.0.dump new file mode 100644 index 0000000000..86b6448eee --- /dev/null +++ b/testdata/src/test/assets/ts/sample_h264_dts_audio.ts.0.dump @@ -0,0 +1,81 @@ +seekMap: + isSeekable = true + duration = 0 + getPosition(0) = [[timeUs=0, position=0]] + getPosition(1) = [[timeUs=1, position=0]] + getPosition(0) = [[timeUs=0, position=0]] + getPosition(0) = [[timeUs=0, position=0]] +numberOfTracks = 3 +track 256: + total output bytes = 13650 + sample count = 2 + format 0: + id = 1/256 + sampleMimeType = video/avc + codecs = avc1.64001E + width = 640 + height = 426 + initializationData: + data = length 29, hash 4C2CAE9C + data = length 9, hash D971CD89 + sample 0: + time = 66733 + flags = 1 + data = length 12394, hash A39F5311 + sample 1: + time = 100100 + flags = 0 + data = length 813, hash 99F7B4FA +track 257: + total output bytes = 18432 + sample count = 9 + format 0: + averageBitrate = 1411500 + id = 1/257 + sampleMimeType = audio/vnd.dts + channelCount = 1 + sampleRate = 44100 + language = und + sample 0: + time = 66733 + flags = 1 + data = length 2048, hash 88866F81 + sample 1: + time = 78344 + flags = 1 + data = length 2048, hash 88866F81 + sample 2: + time = 89955 + flags = 1 + data = length 2048, hash 88866F81 + sample 3: + time = 101566 + flags = 1 + data = length 2048, hash 88866F81 + sample 4: + time = 113177 + flags = 1 + data = length 2048, hash 88866F81 + sample 5: + time = 124777 + flags = 1 + data = length 2048, hash 88866F81 + sample 6: + time = 136388 + flags = 1 + data = length 2048, hash 88866F81 + sample 7: + time = 148000 + flags = 1 + data = length 2048, hash 88866F81 + sample 8: + time = 159611 + flags = 1 + data = length 2048, hash 88866F81 +track 8448: + total output bytes = 0 + sample count = 0 + format 0: + id = 1/8448 + sampleMimeType = application/cea-608 +tracksEnded = true diff --git a/testdata/src/test/assets/ts/sample_h264_dts_audio.ts.1.dump b/testdata/src/test/assets/ts/sample_h264_dts_audio.ts.1.dump new file mode 100644 index 0000000000..86b6448eee --- /dev/null +++ b/testdata/src/test/assets/ts/sample_h264_dts_audio.ts.1.dump @@ -0,0 +1,81 @@ +seekMap: + isSeekable = true + duration = 0 + getPosition(0) = [[timeUs=0, position=0]] + getPosition(1) = [[timeUs=1, position=0]] + getPosition(0) = [[timeUs=0, position=0]] + getPosition(0) = [[timeUs=0, position=0]] +numberOfTracks = 3 +track 256: + total output bytes = 13650 + sample count = 2 + format 0: + id = 1/256 + sampleMimeType = video/avc + codecs = avc1.64001E + width = 640 + height = 426 + initializationData: + data = length 29, hash 4C2CAE9C + data = length 9, hash D971CD89 + sample 0: + time = 66733 + flags = 1 + data = length 12394, hash A39F5311 + sample 1: + time = 100100 + flags = 0 + data = length 813, hash 99F7B4FA +track 257: + total output bytes = 18432 + sample count = 9 + format 0: + averageBitrate = 1411500 + id = 1/257 + sampleMimeType = audio/vnd.dts + channelCount = 1 + sampleRate = 44100 + language = und + sample 0: + time = 66733 + flags = 1 + data = length 2048, hash 88866F81 + sample 1: + time = 78344 + flags = 1 + data = length 2048, hash 88866F81 + sample 2: + time = 89955 + flags = 1 + data = length 2048, hash 88866F81 + sample 3: + time = 101566 + flags = 1 + data = length 2048, hash 88866F81 + sample 4: + time = 113177 + flags = 1 + data = length 2048, hash 88866F81 + sample 5: + time = 124777 + flags = 1 + data = length 2048, hash 88866F81 + sample 6: + time = 136388 + flags = 1 + data = length 2048, hash 88866F81 + sample 7: + time = 148000 + flags = 1 + data = length 2048, hash 88866F81 + sample 8: + time = 159611 + flags = 1 + data = length 2048, hash 88866F81 +track 8448: + total output bytes = 0 + sample count = 0 + format 0: + id = 1/8448 + sampleMimeType = application/cea-608 +tracksEnded = true diff --git a/testdata/src/test/assets/ts/sample_h264_dts_audio.ts.2.dump b/testdata/src/test/assets/ts/sample_h264_dts_audio.ts.2.dump new file mode 100644 index 0000000000..86b6448eee --- /dev/null +++ b/testdata/src/test/assets/ts/sample_h264_dts_audio.ts.2.dump @@ -0,0 +1,81 @@ +seekMap: + isSeekable = true + duration = 0 + getPosition(0) = [[timeUs=0, position=0]] + getPosition(1) = [[timeUs=1, position=0]] + getPosition(0) = [[timeUs=0, position=0]] + getPosition(0) = [[timeUs=0, position=0]] +numberOfTracks = 3 +track 256: + total output bytes = 13650 + sample count = 2 + format 0: + id = 1/256 + sampleMimeType = video/avc + codecs = avc1.64001E + width = 640 + height = 426 + initializationData: + data = length 29, hash 4C2CAE9C + data = length 9, hash D971CD89 + sample 0: + time = 66733 + flags = 1 + data = length 12394, hash A39F5311 + sample 1: + time = 100100 + flags = 0 + data = length 813, hash 99F7B4FA +track 257: + total output bytes = 18432 + sample count = 9 + format 0: + averageBitrate = 1411500 + id = 1/257 + sampleMimeType = audio/vnd.dts + channelCount = 1 + sampleRate = 44100 + language = und + sample 0: + time = 66733 + flags = 1 + data = length 2048, hash 88866F81 + sample 1: + time = 78344 + flags = 1 + data = length 2048, hash 88866F81 + sample 2: + time = 89955 + flags = 1 + data = length 2048, hash 88866F81 + sample 3: + time = 101566 + flags = 1 + data = length 2048, hash 88866F81 + sample 4: + time = 113177 + flags = 1 + data = length 2048, hash 88866F81 + sample 5: + time = 124777 + flags = 1 + data = length 2048, hash 88866F81 + sample 6: + time = 136388 + flags = 1 + data = length 2048, hash 88866F81 + sample 7: + time = 148000 + flags = 1 + data = length 2048, hash 88866F81 + sample 8: + time = 159611 + flags = 1 + data = length 2048, hash 88866F81 +track 8448: + total output bytes = 0 + sample count = 0 + format 0: + id = 1/8448 + sampleMimeType = application/cea-608 +tracksEnded = true diff --git a/testdata/src/test/assets/ts/sample_h264_dts_audio.ts.3.dump b/testdata/src/test/assets/ts/sample_h264_dts_audio.ts.3.dump new file mode 100644 index 0000000000..86b6448eee --- /dev/null +++ b/testdata/src/test/assets/ts/sample_h264_dts_audio.ts.3.dump @@ -0,0 +1,81 @@ +seekMap: + isSeekable = true + duration = 0 + getPosition(0) = [[timeUs=0, position=0]] + getPosition(1) = [[timeUs=1, position=0]] + getPosition(0) = [[timeUs=0, position=0]] + getPosition(0) = [[timeUs=0, position=0]] +numberOfTracks = 3 +track 256: + total output bytes = 13650 + sample count = 2 + format 0: + id = 1/256 + sampleMimeType = video/avc + codecs = avc1.64001E + width = 640 + height = 426 + initializationData: + data = length 29, hash 4C2CAE9C + data = length 9, hash D971CD89 + sample 0: + time = 66733 + flags = 1 + data = length 12394, hash A39F5311 + sample 1: + time = 100100 + flags = 0 + data = length 813, hash 99F7B4FA +track 257: + total output bytes = 18432 + sample count = 9 + format 0: + averageBitrate = 1411500 + id = 1/257 + sampleMimeType = audio/vnd.dts + channelCount = 1 + sampleRate = 44100 + language = und + sample 0: + time = 66733 + flags = 1 + data = length 2048, hash 88866F81 + sample 1: + time = 78344 + flags = 1 + data = length 2048, hash 88866F81 + sample 2: + time = 89955 + flags = 1 + data = length 2048, hash 88866F81 + sample 3: + time = 101566 + flags = 1 + data = length 2048, hash 88866F81 + sample 4: + time = 113177 + flags = 1 + data = length 2048, hash 88866F81 + sample 5: + time = 124777 + flags = 1 + data = length 2048, hash 88866F81 + sample 6: + time = 136388 + flags = 1 + data = length 2048, hash 88866F81 + sample 7: + time = 148000 + flags = 1 + data = length 2048, hash 88866F81 + sample 8: + time = 159611 + flags = 1 + data = length 2048, hash 88866F81 +track 8448: + total output bytes = 0 + sample count = 0 + format 0: + id = 1/8448 + sampleMimeType = application/cea-608 +tracksEnded = true diff --git a/testdata/src/test/assets/ts/sample_h264_dts_audio.ts.unknown_length.dump b/testdata/src/test/assets/ts/sample_h264_dts_audio.ts.unknown_length.dump new file mode 100644 index 0000000000..0e87b909ea --- /dev/null +++ b/testdata/src/test/assets/ts/sample_h264_dts_audio.ts.unknown_length.dump @@ -0,0 +1,78 @@ +seekMap: + isSeekable = false + duration = UNSET TIME + getPosition(0) = [[timeUs=0, position=0]] +numberOfTracks = 3 +track 256: + total output bytes = 13650 + sample count = 2 + format 0: + id = 1/256 + sampleMimeType = video/avc + codecs = avc1.64001E + width = 640 + height = 426 + initializationData: + data = length 29, hash 4C2CAE9C + data = length 9, hash D971CD89 + sample 0: + time = 66733 + flags = 1 + data = length 12394, hash A39F5311 + sample 1: + time = 100100 + flags = 0 + data = length 813, hash 99F7B4FA +track 257: + total output bytes = 18432 + sample count = 9 + format 0: + averageBitrate = 1411500 + id = 1/257 + sampleMimeType = audio/vnd.dts + channelCount = 1 + sampleRate = 44100 + language = und + sample 0: + time = 66733 + flags = 1 + data = length 2048, hash 88866F81 + sample 1: + time = 78344 + flags = 1 + data = length 2048, hash 88866F81 + sample 2: + time = 89955 + flags = 1 + data = length 2048, hash 88866F81 + sample 3: + time = 101566 + flags = 1 + data = length 2048, hash 88866F81 + sample 4: + time = 113177 + flags = 1 + data = length 2048, hash 88866F81 + sample 5: + time = 124777 + flags = 1 + data = length 2048, hash 88866F81 + sample 6: + time = 136388 + flags = 1 + data = length 2048, hash 88866F81 + sample 7: + time = 148000 + flags = 1 + data = length 2048, hash 88866F81 + sample 8: + time = 159611 + flags = 1 + data = length 2048, hash 88866F81 +track 8448: + total output bytes = 0 + sample count = 0 + format 0: + id = 1/8448 + sampleMimeType = application/cea-608 +tracksEnded = true diff --git a/testdata/src/test/assets/ts/sample_h264_mpeg_audio.ts b/testdata/src/test/assets/ts/sample_h264_mpeg_audio.ts new file mode 100644 index 0000000000..dabf547e19 Binary files /dev/null and b/testdata/src/test/assets/ts/sample_h264_mpeg_audio.ts differ diff --git a/testdata/src/test/assets/ts/sample_h264_mpeg_audio.ts.0.dump b/testdata/src/test/assets/ts/sample_h264_mpeg_audio.ts.0.dump new file mode 100644 index 0000000000..49ec76ee00 --- /dev/null +++ b/testdata/src/test/assets/ts/sample_h264_mpeg_audio.ts.0.dump @@ -0,0 +1,61 @@ +seekMap: + isSeekable = true + duration = 66733 + getPosition(0) = [[timeUs=0, position=0]] + getPosition(1) = [[timeUs=1, position=0]] + getPosition(33366) = [[timeUs=33366, position=9545]] + getPosition(66733) = [[timeUs=66733, position=19279]] +numberOfTracks = 3 +track 256: + total output bytes = 13650 + sample count = 2 + format 0: + id = 1/256 + sampleMimeType = video/avc + codecs = avc1.64001E + width = 640 + height = 426 + initializationData: + data = length 29, hash 4C2CAE9C + data = length 9, hash D971CD89 + sample 0: + time = 66733 + flags = 1 + data = length 12394, hash A39F5311 + sample 1: + time = 100100 + flags = 0 + data = length 813, hash 99F7B4FA +track 257: + total output bytes = 5015 + sample count = 4 + format 0: + id = 1/257 + sampleMimeType = audio/mpeg-L2 + maxInputSize = 4096 + channelCount = 1 + sampleRate = 44100 + language = und + sample 0: + time = 66733 + flags = 1 + data = length 1253, hash 727FD1C6 + sample 1: + time = 92855 + flags = 1 + data = length 1254, hash 73FB07B8 + sample 2: + time = 118977 + flags = 1 + data = length 1254, hash 73FB07B8 + sample 3: + time = 145099 + flags = 1 + data = length 1254, hash 73FB07B8 +track 8448: + total output bytes = 0 + sample count = 0 + format 0: + id = 1/8448 + sampleMimeType = application/cea-608 +tracksEnded = true diff --git a/testdata/src/test/assets/ts/sample_h264_mpeg_audio.ts.1.dump b/testdata/src/test/assets/ts/sample_h264_mpeg_audio.ts.1.dump new file mode 100644 index 0000000000..ddd38b6597 --- /dev/null +++ b/testdata/src/test/assets/ts/sample_h264_mpeg_audio.ts.1.dump @@ -0,0 +1,61 @@ +seekMap: + isSeekable = true + duration = 66733 + getPosition(0) = [[timeUs=0, position=0]] + getPosition(1) = [[timeUs=1, position=0]] + getPosition(33366) = [[timeUs=33366, position=9545]] + getPosition(66733) = [[timeUs=66733, position=19279]] +numberOfTracks = 3 +track 256: + total output bytes = 13650 + sample count = 2 + format 0: + id = 1/256 + sampleMimeType = video/avc + codecs = avc1.64001E + width = 640 + height = 426 + initializationData: + data = length 29, hash 4C2CAE9C + data = length 9, hash D971CD89 + sample 0: + time = 88977 + flags = 1 + data = length 12394, hash A39F5311 + sample 1: + time = 122344 + flags = 0 + data = length 813, hash 99F7B4FA +track 257: + total output bytes = 5015 + sample count = 4 + format 0: + id = 1/257 + sampleMimeType = audio/mpeg-L2 + maxInputSize = 4096 + channelCount = 1 + sampleRate = 44100 + language = und + sample 0: + time = 88977 + flags = 1 + data = length 1253, hash 727FD1C6 + sample 1: + time = 115099 + flags = 1 + data = length 1254, hash 73FB07B8 + sample 2: + time = 141221 + flags = 1 + data = length 1254, hash 73FB07B8 + sample 3: + time = 167343 + flags = 1 + data = length 1254, hash 73FB07B8 +track 8448: + total output bytes = 0 + sample count = 0 + format 0: + id = 1/8448 + sampleMimeType = application/cea-608 +tracksEnded = true diff --git a/testdata/src/test/assets/ts/sample_h264_mpeg_audio.ts.2.dump b/testdata/src/test/assets/ts/sample_h264_mpeg_audio.ts.2.dump new file mode 100644 index 0000000000..53290c3fa3 --- /dev/null +++ b/testdata/src/test/assets/ts/sample_h264_mpeg_audio.ts.2.dump @@ -0,0 +1,61 @@ +seekMap: + isSeekable = true + duration = 66733 + getPosition(0) = [[timeUs=0, position=0]] + getPosition(1) = [[timeUs=1, position=0]] + getPosition(33366) = [[timeUs=33366, position=9545]] + getPosition(66733) = [[timeUs=66733, position=19279]] +numberOfTracks = 3 +track 256: + total output bytes = 13650 + sample count = 2 + format 0: + id = 1/256 + sampleMimeType = video/avc + codecs = avc1.64001E + width = 640 + height = 426 + initializationData: + data = length 29, hash 4C2CAE9C + data = length 9, hash D971CD89 + sample 0: + time = 111221 + flags = 1 + data = length 12394, hash A39F5311 + sample 1: + time = 144588 + flags = 0 + data = length 813, hash 99F7B4FA +track 257: + total output bytes = 5015 + sample count = 4 + format 0: + id = 1/257 + sampleMimeType = audio/mpeg-L2 + maxInputSize = 4096 + channelCount = 1 + sampleRate = 44100 + language = und + sample 0: + time = 111221 + flags = 1 + data = length 1253, hash 727FD1C6 + sample 1: + time = 137343 + flags = 1 + data = length 1254, hash 73FB07B8 + sample 2: + time = 163465 + flags = 1 + data = length 1254, hash 73FB07B8 + sample 3: + time = 189587 + flags = 1 + data = length 1254, hash 73FB07B8 +track 8448: + total output bytes = 0 + sample count = 0 + format 0: + id = 1/8448 + sampleMimeType = application/cea-608 +tracksEnded = true diff --git a/testdata/src/test/assets/ts/sample_h264_mpeg_audio.ts.3.dump b/testdata/src/test/assets/ts/sample_h264_mpeg_audio.ts.3.dump new file mode 100644 index 0000000000..2be5c14165 --- /dev/null +++ b/testdata/src/test/assets/ts/sample_h264_mpeg_audio.ts.3.dump @@ -0,0 +1,37 @@ +seekMap: + isSeekable = true + duration = 66733 + getPosition(0) = [[timeUs=0, position=0]] + getPosition(1) = [[timeUs=1, position=0]] + getPosition(33366) = [[timeUs=33366, position=9545]] + getPosition(66733) = [[timeUs=66733, position=19279]] +numberOfTracks = 3 +track 256: + total output bytes = 0 + sample count = 0 + format 0: + id = 1/256 + sampleMimeType = video/avc + codecs = avc1.64001E + width = 640 + height = 426 + initializationData: + data = length 29, hash 4C2CAE9C + data = length 9, hash D971CD89 +track 257: + total output bytes = 0 + sample count = 0 + format 0: + id = 1/257 + sampleMimeType = audio/mpeg-L2 + maxInputSize = 4096 + channelCount = 1 + sampleRate = 44100 + language = und +track 8448: + total output bytes = 0 + sample count = 0 + format 0: + id = 1/8448 + sampleMimeType = application/cea-608 +tracksEnded = true diff --git a/testdata/src/test/assets/ts/sample_h264_mpeg_audio.ts.unknown_length.dump b/testdata/src/test/assets/ts/sample_h264_mpeg_audio.ts.unknown_length.dump new file mode 100644 index 0000000000..86327e7fa8 --- /dev/null +++ b/testdata/src/test/assets/ts/sample_h264_mpeg_audio.ts.unknown_length.dump @@ -0,0 +1,58 @@ +seekMap: + isSeekable = false + duration = UNSET TIME + getPosition(0) = [[timeUs=0, position=0]] +numberOfTracks = 3 +track 256: + total output bytes = 13650 + sample count = 2 + format 0: + id = 1/256 + sampleMimeType = video/avc + codecs = avc1.64001E + width = 640 + height = 426 + initializationData: + data = length 29, hash 4C2CAE9C + data = length 9, hash D971CD89 + sample 0: + time = 66733 + flags = 1 + data = length 12394, hash A39F5311 + sample 1: + time = 100100 + flags = 0 + data = length 813, hash 99F7B4FA +track 257: + total output bytes = 5015 + sample count = 4 + format 0: + id = 1/257 + sampleMimeType = audio/mpeg-L2 + maxInputSize = 4096 + channelCount = 1 + sampleRate = 44100 + language = und + sample 0: + time = 66733 + flags = 1 + data = length 1253, hash 727FD1C6 + sample 1: + time = 92855 + flags = 1 + data = length 1254, hash 73FB07B8 + sample 2: + time = 118977 + flags = 1 + data = length 1254, hash 73FB07B8 + sample 3: + time = 145099 + flags = 1 + data = length 1254, hash 73FB07B8 +track 8448: + total output bytes = 0 + sample count = 0 + format 0: + id = 1/8448 + sampleMimeType = application/cea-608 +tracksEnded = true diff --git a/testdata/src/test/assets/ts/sample_h264_no_access_unit_delimiters.ts b/testdata/src/test/assets/ts/sample_h264_no_access_unit_delimiters.ts new file mode 100644 index 0000000000..a1cc40ba70 Binary files /dev/null and b/testdata/src/test/assets/ts/sample_h264_no_access_unit_delimiters.ts differ diff --git a/testdata/src/test/assets/ts/sample_h264_no_access_unit_delimiters.ts.0.dump b/testdata/src/test/assets/ts/sample_h264_no_access_unit_delimiters.ts.0.dump new file mode 100644 index 0000000000..f3f3d233df --- /dev/null +++ b/testdata/src/test/assets/ts/sample_h264_no_access_unit_delimiters.ts.0.dump @@ -0,0 +1,43 @@ +seekMap: + isSeekable = true + duration = 66733 + getPosition(0) = [[timeUs=0, position=0]] + getPosition(1) = [[timeUs=1, position=0]] + getPosition(33366) = [[timeUs=33366, position=6420]] + getPosition(66733) = [[timeUs=66733, position=13028]] +numberOfTracks = 2 +track 256: + total output bytes = 12451 + sample count = 4 + format 0: + id = 1/256 + sampleMimeType = video/avc + codecs = avc1.64001E + width = 640 + height = 426 + initializationData: + data = length 29, hash 4C2CAE9C + data = length 9, hash D971CD89 + sample 0: + time = 66733 + flags = 0 + data = length 734, hash AF0D9485 + sample 1: + time = 66733 + flags = 1 + data = length 10938, hash 68420875 + sample 2: + time = 133466 + flags = 0 + data = length 6, hash 34E6CF79 + sample 3: + time = 133466 + flags = 0 + data = length 518, hash 546C177 +track 8448: + total output bytes = 0 + sample count = 0 + format 0: + id = 1/8448 + sampleMimeType = application/cea-608 +tracksEnded = true diff --git a/testdata/src/test/assets/ts/sample_h264_no_access_unit_delimiters.ts.1.dump b/testdata/src/test/assets/ts/sample_h264_no_access_unit_delimiters.ts.1.dump new file mode 100644 index 0000000000..94d3d8d3b9 --- /dev/null +++ b/testdata/src/test/assets/ts/sample_h264_no_access_unit_delimiters.ts.1.dump @@ -0,0 +1,43 @@ +seekMap: + isSeekable = true + duration = 66733 + getPosition(0) = [[timeUs=0, position=0]] + getPosition(1) = [[timeUs=1, position=0]] + getPosition(33366) = [[timeUs=33366, position=6420]] + getPosition(66733) = [[timeUs=66733, position=13028]] +numberOfTracks = 2 +track 256: + total output bytes = 12451 + sample count = 4 + format 0: + id = 1/256 + sampleMimeType = video/avc + codecs = avc1.64001E + width = 640 + height = 426 + initializationData: + data = length 29, hash 4C2CAE9C + data = length 9, hash D971CD89 + sample 0: + time = 88977 + flags = 0 + data = length 734, hash AF0D9485 + sample 1: + time = 88977 + flags = 1 + data = length 10938, hash 68420875 + sample 2: + time = 155710 + flags = 0 + data = length 6, hash 34E6CF79 + sample 3: + time = 155710 + flags = 0 + data = length 518, hash 546C177 +track 8448: + total output bytes = 0 + sample count = 0 + format 0: + id = 1/8448 + sampleMimeType = application/cea-608 +tracksEnded = true diff --git a/testdata/src/test/assets/ts/sample_h264_no_access_unit_delimiters.ts.2.dump b/testdata/src/test/assets/ts/sample_h264_no_access_unit_delimiters.ts.2.dump new file mode 100644 index 0000000000..e6e8332462 --- /dev/null +++ b/testdata/src/test/assets/ts/sample_h264_no_access_unit_delimiters.ts.2.dump @@ -0,0 +1,43 @@ +seekMap: + isSeekable = true + duration = 66733 + getPosition(0) = [[timeUs=0, position=0]] + getPosition(1) = [[timeUs=1, position=0]] + getPosition(33366) = [[timeUs=33366, position=6420]] + getPosition(66733) = [[timeUs=66733, position=13028]] +numberOfTracks = 2 +track 256: + total output bytes = 12451 + sample count = 4 + format 0: + id = 1/256 + sampleMimeType = video/avc + codecs = avc1.64001E + width = 640 + height = 426 + initializationData: + data = length 29, hash 4C2CAE9C + data = length 9, hash D971CD89 + sample 0: + time = 111221 + flags = 0 + data = length 734, hash AF0D9485 + sample 1: + time = 111221 + flags = 1 + data = length 10938, hash 68420875 + sample 2: + time = 177954 + flags = 0 + data = length 6, hash 34E6CF79 + sample 3: + time = 177954 + flags = 0 + data = length 518, hash 546C177 +track 8448: + total output bytes = 0 + sample count = 0 + format 0: + id = 1/8448 + sampleMimeType = application/cea-608 +tracksEnded = true diff --git a/testdata/src/test/assets/ts/sample_h264_no_access_unit_delimiters.ts.3.dump b/testdata/src/test/assets/ts/sample_h264_no_access_unit_delimiters.ts.3.dump new file mode 100644 index 0000000000..8710d53411 --- /dev/null +++ b/testdata/src/test/assets/ts/sample_h264_no_access_unit_delimiters.ts.3.dump @@ -0,0 +1,27 @@ +seekMap: + isSeekable = true + duration = 66733 + getPosition(0) = [[timeUs=0, position=0]] + getPosition(1) = [[timeUs=1, position=0]] + getPosition(33366) = [[timeUs=33366, position=6420]] + getPosition(66733) = [[timeUs=66733, position=13028]] +numberOfTracks = 2 +track 256: + total output bytes = 255 + sample count = 0 + format 0: + id = 1/256 + sampleMimeType = video/avc + codecs = avc1.64001E + width = 640 + height = 426 + initializationData: + data = length 29, hash 4C2CAE9C + data = length 9, hash D971CD89 +track 8448: + total output bytes = 0 + sample count = 0 + format 0: + id = 1/8448 + sampleMimeType = application/cea-608 +tracksEnded = true diff --git a/testdata/src/test/assets/ts/sample_h264_no_access_unit_delimiters.ts.unknown_length.dump b/testdata/src/test/assets/ts/sample_h264_no_access_unit_delimiters.ts.unknown_length.dump new file mode 100644 index 0000000000..a23c081a59 --- /dev/null +++ b/testdata/src/test/assets/ts/sample_h264_no_access_unit_delimiters.ts.unknown_length.dump @@ -0,0 +1,40 @@ +seekMap: + isSeekable = false + duration = UNSET TIME + getPosition(0) = [[timeUs=0, position=0]] +numberOfTracks = 2 +track 256: + total output bytes = 12451 + sample count = 4 + format 0: + id = 1/256 + sampleMimeType = video/avc + codecs = avc1.64001E + width = 640 + height = 426 + initializationData: + data = length 29, hash 4C2CAE9C + data = length 9, hash D971CD89 + sample 0: + time = 66733 + flags = 0 + data = length 734, hash AF0D9485 + sample 1: + time = 66733 + flags = 1 + data = length 10938, hash 68420875 + sample 2: + time = 133466 + flags = 0 + data = length 6, hash 34E6CF79 + sample 3: + time = 133466 + flags = 0 + data = length 518, hash 546C177 +track 8448: + total output bytes = 0 + sample count = 0 + format 0: + id = 1/8448 + sampleMimeType = application/cea-608 +tracksEnded = true diff --git a/testdata/src/test/assets/ts/sample_h265.ts b/testdata/src/test/assets/ts/sample_h265.ts new file mode 100644 index 0000000000..483010fd86 Binary files /dev/null and b/testdata/src/test/assets/ts/sample_h265.ts differ diff --git a/testdata/src/test/assets/ts/sample_h265.ts.0.dump b/testdata/src/test/assets/ts/sample_h265.ts.0.dump new file mode 100644 index 0000000000..cee71dd25a --- /dev/null +++ b/testdata/src/test/assets/ts/sample_h265.ts.0.dump @@ -0,0 +1,141 @@ +seekMap: + isSeekable = true + duration = 900000 + getPosition(0) = [[timeUs=0, position=0]] + getPosition(1) = [[timeUs=1, position=0]] + getPosition(450000) = [[timeUs=450000, position=11421]] + getPosition(900000) = [[timeUs=900000, position=23030]] +numberOfTracks = 2 +track 256: + total output bytes = 19364 + sample count = 29 + format 0: + id = 1/256 + sampleMimeType = video/hevc + width = 854 + height = 480 + initializationData: + data = length 83, hash 7F428 + sample 0: + time = 66666 + flags = 1 + data = length 2517, hash 85352308 + sample 1: + time = 100000 + flags = 0 + data = length 1226, hash 11D564DA + sample 2: + time = 266666 + flags = 0 + data = length 7817, hash 50D15703 + sample 3: + time = 200000 + flags = 0 + data = length 2313, hash ECA5AEE6 + sample 4: + time = 133333 + flags = 0 + data = length 1065, hash 8720A939 + sample 5: + time = 166666 + flags = 0 + data = length 105, hash 3A3A582D + sample 6: + time = 233333 + flags = 0 + data = length 68, hash FC241239 + sample 7: + time = 433333 + flags = 0 + data = length 303, hash 41B28452 + sample 8: + time = 366666 + flags = 0 + data = length 144, hash 60BBCD4C + sample 9: + time = 300000 + flags = 0 + data = length 225, hash E0FAD7E9 + sample 10: + time = 333333 + flags = 0 + data = length 184, hash A3A6E036 + sample 11: + time = 400000 + flags = 0 + data = length 89, hash 43B0E322 + sample 12: + time = 533333 + flags = 0 + data = length 297, hash 6D9FEEDA + sample 13: + time = 500000 + flags = 0 + data = length 275, hash 27430DB + sample 14: + time = 466666 + flags = 0 + data = length 185, hash 97389E88 + sample 15: + time = 566666 + flags = 0 + data = length 278, hash 5819FEBB + sample 16: + time = 733333 + flags = 0 + data = length 264, hash 8545F36A + sample 17: + time = 666666 + flags = 0 + data = length 213, hash 52C7574A + sample 18: + time = 600000 + flags = 0 + data = length 137, hash D4F0BCD7 + sample 19: + time = 633333 + flags = 0 + data = length 121, hash BE52EEB8 + sample 20: + time = 700000 + flags = 0 + data = length 102, hash 6AA3C84F + sample 21: + time = 900000 + flags = 0 + data = length 240, hash 8E3CA414 + sample 22: + time = 833333 + flags = 0 + data = length 210, hash 5D050FE8 + sample 23: + time = 766666 + flags = 0 + data = length 102, hash ED3BD5C9 + sample 24: + time = 800000 + flags = 0 + data = length 110, hash CF65ED37 + sample 25: + time = 866666 + flags = 0 + data = length 118, hash BA0156BF + sample 26: + time = 1033333 + flags = 0 + data = length 260, hash ED6ABC1D + sample 27: + time = 966666 + flags = 0 + data = length 141, hash 9787F33A + sample 28: + time = 933333 + flags = 0 + data = length 87, hash EEC4D98C +track 8448: + total output bytes = 0 + sample count = 0 + format 0: + id = 1/8448 + sampleMimeType = application/cea-608 +tracksEnded = true diff --git a/testdata/src/test/assets/ts/sample_h265.ts.1.dump b/testdata/src/test/assets/ts/sample_h265.ts.1.dump new file mode 100644 index 0000000000..a9db2afd14 --- /dev/null +++ b/testdata/src/test/assets/ts/sample_h265.ts.1.dump @@ -0,0 +1,105 @@ +seekMap: + isSeekable = true + duration = 900000 + getPosition(0) = [[timeUs=0, position=0]] + getPosition(1) = [[timeUs=1, position=0]] + getPosition(450000) = [[timeUs=450000, position=11421]] + getPosition(900000) = [[timeUs=900000, position=23030]] +numberOfTracks = 2 +track 256: + total output bytes = 3806 + sample count = 20 + format 0: + id = 1/256 + sampleMimeType = video/hevc + width = 854 + height = 480 + initializationData: + data = length 83, hash 7F428 + sample 0: + time = 300000 + flags = 0 + data = length 225, hash E0FAD7E9 + sample 1: + time = 333333 + flags = 0 + data = length 184, hash A3A6E036 + sample 2: + time = 400000 + flags = 0 + data = length 89, hash 43B0E322 + sample 3: + time = 533333 + flags = 0 + data = length 297, hash 6D9FEEDA + sample 4: + time = 500000 + flags = 0 + data = length 275, hash 27430DB + sample 5: + time = 466666 + flags = 0 + data = length 185, hash 97389E88 + sample 6: + time = 566666 + flags = 0 + data = length 278, hash 5819FEBB + sample 7: + time = 733333 + flags = 0 + data = length 264, hash 8545F36A + sample 8: + time = 666666 + flags = 0 + data = length 213, hash 52C7574A + sample 9: + time = 600000 + flags = 0 + data = length 137, hash D4F0BCD7 + sample 10: + time = 633333 + flags = 0 + data = length 121, hash BE52EEB8 + sample 11: + time = 700000 + flags = 0 + data = length 102, hash 6AA3C84F + sample 12: + time = 900000 + flags = 0 + data = length 240, hash 8E3CA414 + sample 13: + time = 833333 + flags = 0 + data = length 210, hash 5D050FE8 + sample 14: + time = 766666 + flags = 0 + data = length 102, hash ED3BD5C9 + sample 15: + time = 800000 + flags = 0 + data = length 110, hash CF65ED37 + sample 16: + time = 866666 + flags = 0 + data = length 118, hash BA0156BF + sample 17: + time = 1033333 + flags = 0 + data = length 260, hash ED6ABC1D + sample 18: + time = 966666 + flags = 0 + data = length 141, hash 9787F33A + sample 19: + time = 933333 + flags = 0 + data = length 87, hash EEC4D98C +track 8448: + total output bytes = 0 + sample count = 0 + format 0: + id = 1/8448 + sampleMimeType = application/cea-608 +tracksEnded = true diff --git a/testdata/src/test/assets/ts/sample_h265.ts.2.dump b/testdata/src/test/assets/ts/sample_h265.ts.2.dump new file mode 100644 index 0000000000..7f694a374c --- /dev/null +++ b/testdata/src/test/assets/ts/sample_h265.ts.2.dump @@ -0,0 +1,69 @@ +seekMap: + isSeekable = true + duration = 900000 + getPosition(0) = [[timeUs=0, position=0]] + getPosition(1) = [[timeUs=1, position=0]] + getPosition(450000) = [[timeUs=450000, position=11421]] + getPosition(900000) = [[timeUs=900000, position=23030]] +numberOfTracks = 2 +track 256: + total output bytes = 1796 + sample count = 11 + format 0: + id = 1/256 + sampleMimeType = video/hevc + width = 854 + height = 480 + initializationData: + data = length 83, hash 7F428 + sample 0: + time = 600000 + flags = 0 + data = length 137, hash D4F0BCD7 + sample 1: + time = 633333 + flags = 0 + data = length 121, hash BE52EEB8 + sample 2: + time = 700000 + flags = 0 + data = length 102, hash 6AA3C84F + sample 3: + time = 900000 + flags = 0 + data = length 240, hash 8E3CA414 + sample 4: + time = 833333 + flags = 0 + data = length 210, hash 5D050FE8 + sample 5: + time = 766666 + flags = 0 + data = length 102, hash ED3BD5C9 + sample 6: + time = 800000 + flags = 0 + data = length 110, hash CF65ED37 + sample 7: + time = 866666 + flags = 0 + data = length 118, hash BA0156BF + sample 8: + time = 1033333 + flags = 0 + data = length 260, hash ED6ABC1D + sample 9: + time = 966666 + flags = 0 + data = length 141, hash 9787F33A + sample 10: + time = 933333 + flags = 0 + data = length 87, hash EEC4D98C +track 8448: + total output bytes = 0 + sample count = 0 + format 0: + id = 1/8448 + sampleMimeType = application/cea-608 +tracksEnded = true diff --git a/testdata/src/test/assets/ts/sample_h265.ts.3.dump b/testdata/src/test/assets/ts/sample_h265.ts.3.dump new file mode 100644 index 0000000000..7926850807 --- /dev/null +++ b/testdata/src/test/assets/ts/sample_h265.ts.3.dump @@ -0,0 +1,33 @@ +seekMap: + isSeekable = true + duration = 900000 + getPosition(0) = [[timeUs=0, position=0]] + getPosition(1) = [[timeUs=1, position=0]] + getPosition(450000) = [[timeUs=450000, position=11421]] + getPosition(900000) = [[timeUs=900000, position=23030]] +numberOfTracks = 2 +track 256: + total output bytes = 396 + sample count = 2 + format 0: + id = 1/256 + sampleMimeType = video/hevc + width = 854 + height = 480 + initializationData: + data = length 83, hash 7F428 + sample 0: + time = 966666 + flags = 0 + data = length 141, hash 9787F33A + sample 1: + time = 933333 + flags = 0 + data = length 87, hash EEC4D98C +track 8448: + total output bytes = 0 + sample count = 0 + format 0: + id = 1/8448 + sampleMimeType = application/cea-608 +tracksEnded = true diff --git a/testdata/src/test/assets/ts/sample_h265.ts.unknown_length.dump b/testdata/src/test/assets/ts/sample_h265.ts.unknown_length.dump new file mode 100644 index 0000000000..2df0a73c3a --- /dev/null +++ b/testdata/src/test/assets/ts/sample_h265.ts.unknown_length.dump @@ -0,0 +1,138 @@ +seekMap: + isSeekable = false + duration = UNSET TIME + getPosition(0) = [[timeUs=0, position=0]] +numberOfTracks = 2 +track 256: + total output bytes = 19364 + sample count = 29 + format 0: + id = 1/256 + sampleMimeType = video/hevc + width = 854 + height = 480 + initializationData: + data = length 83, hash 7F428 + sample 0: + time = 66666 + flags = 1 + data = length 2517, hash 85352308 + sample 1: + time = 100000 + flags = 0 + data = length 1226, hash 11D564DA + sample 2: + time = 266666 + flags = 0 + data = length 7817, hash 50D15703 + sample 3: + time = 200000 + flags = 0 + data = length 2313, hash ECA5AEE6 + sample 4: + time = 133333 + flags = 0 + data = length 1065, hash 8720A939 + sample 5: + time = 166666 + flags = 0 + data = length 105, hash 3A3A582D + sample 6: + time = 233333 + flags = 0 + data = length 68, hash FC241239 + sample 7: + time = 433333 + flags = 0 + data = length 303, hash 41B28452 + sample 8: + time = 366666 + flags = 0 + data = length 144, hash 60BBCD4C + sample 9: + time = 300000 + flags = 0 + data = length 225, hash E0FAD7E9 + sample 10: + time = 333333 + flags = 0 + data = length 184, hash A3A6E036 + sample 11: + time = 400000 + flags = 0 + data = length 89, hash 43B0E322 + sample 12: + time = 533333 + flags = 0 + data = length 297, hash 6D9FEEDA + sample 13: + time = 500000 + flags = 0 + data = length 275, hash 27430DB + sample 14: + time = 466666 + flags = 0 + data = length 185, hash 97389E88 + sample 15: + time = 566666 + flags = 0 + data = length 278, hash 5819FEBB + sample 16: + time = 733333 + flags = 0 + data = length 264, hash 8545F36A + sample 17: + time = 666666 + flags = 0 + data = length 213, hash 52C7574A + sample 18: + time = 600000 + flags = 0 + data = length 137, hash D4F0BCD7 + sample 19: + time = 633333 + flags = 0 + data = length 121, hash BE52EEB8 + sample 20: + time = 700000 + flags = 0 + data = length 102, hash 6AA3C84F + sample 21: + time = 900000 + flags = 0 + data = length 240, hash 8E3CA414 + sample 22: + time = 833333 + flags = 0 + data = length 210, hash 5D050FE8 + sample 23: + time = 766666 + flags = 0 + data = length 102, hash ED3BD5C9 + sample 24: + time = 800000 + flags = 0 + data = length 110, hash CF65ED37 + sample 25: + time = 866666 + flags = 0 + data = length 118, hash BA0156BF + sample 26: + time = 1033333 + flags = 0 + data = length 260, hash ED6ABC1D + sample 27: + time = 966666 + flags = 0 + data = length 141, hash 9787F33A + sample 28: + time = 933333 + flags = 0 + data = length 87, hash EEC4D98C +track 8448: + total output bytes = 0 + sample count = 0 + format 0: + id = 1/8448 + sampleMimeType = application/cea-608 +tracksEnded = true diff --git a/testdata/src/test/assets/ts/sample_latm.ts b/testdata/src/test/assets/ts/sample_latm.ts new file mode 100644 index 0000000000..a6a6e0b5e3 Binary files /dev/null and b/testdata/src/test/assets/ts/sample_latm.ts differ diff --git a/testdata/src/test/assets/ts/sample_latm.ts.0.dump b/testdata/src/test/assets/ts/sample_latm.ts.0.dump new file mode 100644 index 0000000000..51e3ce4c71 --- /dev/null +++ b/testdata/src/test/assets/ts/sample_latm.ts.0.dump @@ -0,0 +1,41 @@ +seekMap: + isSeekable = true + duration = 0 + getPosition(0) = [[timeUs=0, position=0]] + getPosition(1) = [[timeUs=1, position=0]] + getPosition(0) = [[timeUs=0, position=0]] + getPosition(0) = [[timeUs=0, position=0]] +numberOfTracks = 1 +track 256: + total output bytes = 1396 + sample count = 5 + format 0: + id = 1/256 + sampleMimeType = audio/mp4a-latm + codecs = mp4a.40.5 + channelCount = 1 + sampleRate = 44100 + language = und + initializationData: + data = length 4, hash 1FE978 + sample 0: + time = 0 + flags = 1 + data = length 279, hash 79BF9F9B + sample 1: + time = 23219 + flags = 1 + data = length 279, hash C96F4684 + sample 2: + time = 46438 + flags = 1 + data = length 279, hash 65670B86 + sample 3: + time = 69657 + flags = 1 + data = length 280, hash 1AF29BCE + sample 4: + time = 92876 + flags = 1 + data = length 279, hash C96F4684 +tracksEnded = true diff --git a/testdata/src/test/assets/ts/sample_latm.ts.1.dump b/testdata/src/test/assets/ts/sample_latm.ts.1.dump new file mode 100644 index 0000000000..51e3ce4c71 --- /dev/null +++ b/testdata/src/test/assets/ts/sample_latm.ts.1.dump @@ -0,0 +1,41 @@ +seekMap: + isSeekable = true + duration = 0 + getPosition(0) = [[timeUs=0, position=0]] + getPosition(1) = [[timeUs=1, position=0]] + getPosition(0) = [[timeUs=0, position=0]] + getPosition(0) = [[timeUs=0, position=0]] +numberOfTracks = 1 +track 256: + total output bytes = 1396 + sample count = 5 + format 0: + id = 1/256 + sampleMimeType = audio/mp4a-latm + codecs = mp4a.40.5 + channelCount = 1 + sampleRate = 44100 + language = und + initializationData: + data = length 4, hash 1FE978 + sample 0: + time = 0 + flags = 1 + data = length 279, hash 79BF9F9B + sample 1: + time = 23219 + flags = 1 + data = length 279, hash C96F4684 + sample 2: + time = 46438 + flags = 1 + data = length 279, hash 65670B86 + sample 3: + time = 69657 + flags = 1 + data = length 280, hash 1AF29BCE + sample 4: + time = 92876 + flags = 1 + data = length 279, hash C96F4684 +tracksEnded = true diff --git a/testdata/src/test/assets/ts/sample_latm.ts.2.dump b/testdata/src/test/assets/ts/sample_latm.ts.2.dump new file mode 100644 index 0000000000..51e3ce4c71 --- /dev/null +++ b/testdata/src/test/assets/ts/sample_latm.ts.2.dump @@ -0,0 +1,41 @@ +seekMap: + isSeekable = true + duration = 0 + getPosition(0) = [[timeUs=0, position=0]] + getPosition(1) = [[timeUs=1, position=0]] + getPosition(0) = [[timeUs=0, position=0]] + getPosition(0) = [[timeUs=0, position=0]] +numberOfTracks = 1 +track 256: + total output bytes = 1396 + sample count = 5 + format 0: + id = 1/256 + sampleMimeType = audio/mp4a-latm + codecs = mp4a.40.5 + channelCount = 1 + sampleRate = 44100 + language = und + initializationData: + data = length 4, hash 1FE978 + sample 0: + time = 0 + flags = 1 + data = length 279, hash 79BF9F9B + sample 1: + time = 23219 + flags = 1 + data = length 279, hash C96F4684 + sample 2: + time = 46438 + flags = 1 + data = length 279, hash 65670B86 + sample 3: + time = 69657 + flags = 1 + data = length 280, hash 1AF29BCE + sample 4: + time = 92876 + flags = 1 + data = length 279, hash C96F4684 +tracksEnded = true diff --git a/testdata/src/test/assets/ts/sample_latm.ts.3.dump b/testdata/src/test/assets/ts/sample_latm.ts.3.dump new file mode 100644 index 0000000000..51e3ce4c71 --- /dev/null +++ b/testdata/src/test/assets/ts/sample_latm.ts.3.dump @@ -0,0 +1,41 @@ +seekMap: + isSeekable = true + duration = 0 + getPosition(0) = [[timeUs=0, position=0]] + getPosition(1) = [[timeUs=1, position=0]] + getPosition(0) = [[timeUs=0, position=0]] + getPosition(0) = [[timeUs=0, position=0]] +numberOfTracks = 1 +track 256: + total output bytes = 1396 + sample count = 5 + format 0: + id = 1/256 + sampleMimeType = audio/mp4a-latm + codecs = mp4a.40.5 + channelCount = 1 + sampleRate = 44100 + language = und + initializationData: + data = length 4, hash 1FE978 + sample 0: + time = 0 + flags = 1 + data = length 279, hash 79BF9F9B + sample 1: + time = 23219 + flags = 1 + data = length 279, hash C96F4684 + sample 2: + time = 46438 + flags = 1 + data = length 279, hash 65670B86 + sample 3: + time = 69657 + flags = 1 + data = length 280, hash 1AF29BCE + sample 4: + time = 92876 + flags = 1 + data = length 279, hash C96F4684 +tracksEnded = true diff --git a/testdata/src/test/assets/ts/sample_latm.ts.unknown_length.dump b/testdata/src/test/assets/ts/sample_latm.ts.unknown_length.dump new file mode 100644 index 0000000000..e97431e323 --- /dev/null +++ b/testdata/src/test/assets/ts/sample_latm.ts.unknown_length.dump @@ -0,0 +1,38 @@ +seekMap: + isSeekable = false + duration = UNSET TIME + getPosition(0) = [[timeUs=0, position=0]] +numberOfTracks = 1 +track 256: + total output bytes = 1396 + sample count = 5 + format 0: + id = 1/256 + sampleMimeType = audio/mp4a-latm + codecs = mp4a.40.5 + channelCount = 1 + sampleRate = 44100 + language = und + initializationData: + data = length 4, hash 1FE978 + sample 0: + time = 0 + flags = 1 + data = length 279, hash 79BF9F9B + sample 1: + time = 23219 + flags = 1 + data = length 279, hash C96F4684 + sample 2: + time = 46438 + flags = 1 + data = length 279, hash 65670B86 + sample 3: + time = 69657 + flags = 1 + data = length 280, hash 1AF29BCE + sample 4: + time = 92876 + flags = 1 + data = length 279, hash C96F4684 +tracksEnded = true diff --git a/testdata/src/test/assets/ts/sample_scte35.ts b/testdata/src/test/assets/ts/sample_scte35.ts new file mode 100644 index 0000000000..81f9ce9c21 Binary files /dev/null and b/testdata/src/test/assets/ts/sample_scte35.ts differ diff --git a/testdata/src/test/assets/ts/sample_scte35.ts.0.dump b/testdata/src/test/assets/ts/sample_scte35.ts.0.dump new file mode 100644 index 0000000000..b545d6069a --- /dev/null +++ b/testdata/src/test/assets/ts/sample_scte35.ts.0.dump @@ -0,0 +1,77 @@ +seekMap: + isSeekable = true + duration = 66733 + getPosition(0) = [[timeUs=0, position=0]] + getPosition(1) = [[timeUs=1, position=0]] + getPosition(33366) = [[timeUs=33366, position=25887]] + getPosition(66733) = [[timeUs=66733, position=51963]] +numberOfTracks = 4 +track 256: + total output bytes = 45026 + sample count = 2 + format 0: + id = 1/256 + sampleMimeType = video/mpeg2 + width = 640 + height = 426 + initializationData: + data = length 22, hash CE183139 + sample 0: + time = 33366 + flags = 1 + data = length 20711, hash 34341E8 + sample 1: + time = 66733 + flags = 0 + data = length 18112, hash EC44B35B +track 257: + total output bytes = 5015 + sample count = 4 + format 0: + id = 1/257 + sampleMimeType = audio/mpeg-L2 + maxInputSize = 4096 + channelCount = 1 + sampleRate = 44100 + language = und + 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 600: + total output bytes = 105 + sample count = 3 + format 0: + sampleMimeType = application/x-scte35 + subsampleOffsetUs = -1400000 + sample 0: + time = 33366 + flags = 1 + data = length 35, hash A892AAAF + sample 1: + time = 33366 + flags = 1 + data = length 35, hash A892AAAF + sample 2: + time = 33366 + flags = 1 + data = length 35, hash DFA3EF74 +track 8448: + total output bytes = 0 + sample count = 0 + format 0: + id = 1/8448 + sampleMimeType = application/cea-608 +tracksEnded = true diff --git a/testdata/src/test/assets/ts/sample_scte35.ts.1.dump b/testdata/src/test/assets/ts/sample_scte35.ts.1.dump new file mode 100644 index 0000000000..643642c3a7 --- /dev/null +++ b/testdata/src/test/assets/ts/sample_scte35.ts.1.dump @@ -0,0 +1,77 @@ +seekMap: + isSeekable = true + duration = 66733 + getPosition(0) = [[timeUs=0, position=0]] + getPosition(1) = [[timeUs=1, position=0]] + getPosition(33366) = [[timeUs=33366, position=25887]] + getPosition(66733) = [[timeUs=66733, position=51963]] +numberOfTracks = 4 +track 256: + total output bytes = 45026 + sample count = 2 + format 0: + id = 1/256 + sampleMimeType = video/mpeg2 + width = 640 + height = 426 + initializationData: + data = length 22, hash CE183139 + sample 0: + time = 55610 + flags = 1 + data = length 20711, hash 34341E8 + sample 1: + time = 88977 + flags = 0 + data = length 18112, hash EC44B35B +track 257: + total output bytes = 5015 + sample count = 4 + format 0: + id = 1/257 + sampleMimeType = audio/mpeg-L2 + maxInputSize = 4096 + channelCount = 1 + sampleRate = 44100 + language = und + sample 0: + time = 44699 + flags = 1 + data = length 1253, hash 727FD1C6 + sample 1: + time = 70821 + flags = 1 + data = length 1254, hash 73FB07B8 + sample 2: + time = 96944 + flags = 1 + data = length 1254, hash 73FB07B8 + sample 3: + time = 123066 + flags = 1 + data = length 1254, hash 73FB07B8 +track 600: + total output bytes = 105 + sample count = 3 + format 0: + sampleMimeType = application/x-scte35 + subsampleOffsetUs = -1377756 + sample 0: + time = 55610 + flags = 1 + data = length 35, hash A892AAAF + sample 1: + time = 55610 + flags = 1 + data = length 35, hash A892AAAF + sample 2: + time = 55610 + flags = 1 + data = length 35, hash DFA3EF74 +track 8448: + total output bytes = 0 + sample count = 0 + format 0: + id = 1/8448 + sampleMimeType = application/cea-608 +tracksEnded = true diff --git a/testdata/src/test/assets/ts/sample_scte35.ts.2.dump b/testdata/src/test/assets/ts/sample_scte35.ts.2.dump new file mode 100644 index 0000000000..40737cbeda --- /dev/null +++ b/testdata/src/test/assets/ts/sample_scte35.ts.2.dump @@ -0,0 +1,77 @@ +seekMap: + isSeekable = true + duration = 66733 + getPosition(0) = [[timeUs=0, position=0]] + getPosition(1) = [[timeUs=1, position=0]] + getPosition(33366) = [[timeUs=33366, position=25887]] + getPosition(66733) = [[timeUs=66733, position=51963]] +numberOfTracks = 4 +track 256: + total output bytes = 45026 + sample count = 2 + format 0: + id = 1/256 + sampleMimeType = video/mpeg2 + width = 640 + height = 426 + initializationData: + data = length 22, hash CE183139 + sample 0: + time = 77854 + flags = 1 + data = length 20711, hash 34341E8 + sample 1: + time = 111221 + flags = 0 + data = length 18112, hash EC44B35B +track 257: + total output bytes = 5015 + sample count = 4 + format 0: + id = 1/257 + sampleMimeType = audio/mpeg-L2 + maxInputSize = 4096 + channelCount = 1 + sampleRate = 44100 + language = und + sample 0: + time = 66943 + flags = 1 + data = length 1253, hash 727FD1C6 + sample 1: + time = 93065 + flags = 1 + data = length 1254, hash 73FB07B8 + sample 2: + time = 119188 + flags = 1 + data = length 1254, hash 73FB07B8 + sample 3: + time = 145310 + flags = 1 + data = length 1254, hash 73FB07B8 +track 600: + total output bytes = 105 + sample count = 3 + format 0: + sampleMimeType = application/x-scte35 + subsampleOffsetUs = -1355512 + sample 0: + time = 77854 + flags = 1 + data = length 35, hash A892AAAF + sample 1: + time = 77854 + flags = 1 + data = length 35, hash A892AAAF + sample 2: + time = 77854 + flags = 1 + data = length 35, hash DFA3EF74 +track 8448: + total output bytes = 0 + sample count = 0 + format 0: + id = 1/8448 + sampleMimeType = application/cea-608 +tracksEnded = true diff --git a/testdata/src/test/assets/ts/sample_scte35.ts.3.dump b/testdata/src/test/assets/ts/sample_scte35.ts.3.dump new file mode 100644 index 0000000000..f38391ea22 --- /dev/null +++ b/testdata/src/test/assets/ts/sample_scte35.ts.3.dump @@ -0,0 +1,49 @@ +seekMap: + isSeekable = true + duration = 66733 + getPosition(0) = [[timeUs=0, position=0]] + getPosition(1) = [[timeUs=1, position=0]] + getPosition(33366) = [[timeUs=33366, position=25887]] + getPosition(66733) = [[timeUs=66733, position=51963]] +numberOfTracks = 4 +track 256: + total output bytes = 0 + sample count = 0 + format 0: + id = 1/256 + sampleMimeType = video/mpeg2 + width = 640 + height = 426 + initializationData: + data = length 22, hash CE183139 +track 257: + total output bytes = 2508 + sample count = 2 + format 0: + id = 1/257 + sampleMimeType = audio/mpeg-L2 + maxInputSize = 4096 + channelCount = 1 + sampleRate = 44100 + language = und + sample 0: + time = 66733 + flags = 1 + data = length 1254, hash 73FB07B8 + sample 1: + time = 92855 + flags = 1 + data = length 1254, hash 73FB07B8 +track 600: + total output bytes = 0 + sample count = 0 + format 0: + sampleMimeType = application/x-scte35 + subsampleOffsetUs = -1355512 +track 8448: + total output bytes = 0 + sample count = 0 + format 0: + id = 1/8448 + sampleMimeType = application/cea-608 +tracksEnded = true diff --git a/testdata/src/test/assets/ts/sample_scte35.ts.unknown_length.dump b/testdata/src/test/assets/ts/sample_scte35.ts.unknown_length.dump new file mode 100644 index 0000000000..d3e8c326aa --- /dev/null +++ b/testdata/src/test/assets/ts/sample_scte35.ts.unknown_length.dump @@ -0,0 +1,74 @@ +seekMap: + isSeekable = false + duration = UNSET TIME + getPosition(0) = [[timeUs=0, position=0]] +numberOfTracks = 4 +track 256: + total output bytes = 45026 + sample count = 2 + format 0: + id = 1/256 + sampleMimeType = video/mpeg2 + width = 640 + height = 426 + initializationData: + data = length 22, hash CE183139 + sample 0: + time = 33366 + flags = 1 + data = length 20711, hash 34341E8 + sample 1: + time = 66733 + flags = 0 + data = length 18112, hash EC44B35B +track 257: + total output bytes = 5015 + sample count = 4 + format 0: + id = 1/257 + sampleMimeType = audio/mpeg-L2 + maxInputSize = 4096 + channelCount = 1 + sampleRate = 44100 + language = und + 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 600: + total output bytes = 105 + sample count = 3 + format 0: + sampleMimeType = application/x-scte35 + subsampleOffsetUs = -1400000 + sample 0: + time = 33366 + flags = 1 + data = length 35, hash A892AAAF + sample 1: + time = 33366 + flags = 1 + data = length 35, hash A892AAAF + sample 2: + time = 33366 + flags = 1 + data = length 35, hash DFA3EF74 +track 8448: + total output bytes = 0 + sample count = 0 + format 0: + id = 1/8448 + sampleMimeType = application/cea-608 +tracksEnded = true diff --git a/testdata/src/test/assets/ts/sample_with_id3.adts b/testdata/src/test/assets/ts/sample_with_id3.adts new file mode 100644 index 0000000000..690fe90bd0 Binary files /dev/null and b/testdata/src/test/assets/ts/sample_with_id3.adts differ diff --git a/testdata/src/test/assets/ts/sample_with_id3.adts.0.dump b/testdata/src/test/assets/ts/sample_with_id3.adts.0.dump new file mode 100644 index 0000000000..1f5d510b2e --- /dev/null +++ b/testdata/src/test/assets/ts/sample_with_id3.adts.0.dump @@ -0,0 +1,607 @@ +seekMap: + isSeekable = false + duration = UNSET TIME + getPosition(0) = [[timeUs=0, position=0]] +numberOfTracks = 2 +track 0: + total output bytes = 30797 + sample count = 144 + format 0: + id = 0 + sampleMimeType = audio/mp4a-latm + codecs = mp4a.40.2 + channelCount = 1 + sampleRate = 44100 + initializationData: + data = length 2, hash 5F7 + 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: + total output bytes = 141 + sample count = 2 + format 0: + id = 1 + sampleMimeType = application/id3 + sample 0: + time = 0 + flags = 1 + data = length 55, hash A7EB51A0 + sample 1: + time = 23219 + flags = 1 + data = length 86, hash 3FA72D40 +tracksEnded = true diff --git a/testdata/src/test/assets/ts/sample_with_id3.adts.unknown_length.dump b/testdata/src/test/assets/ts/sample_with_id3.adts.unknown_length.dump new file mode 100644 index 0000000000..1f5d510b2e --- /dev/null +++ b/testdata/src/test/assets/ts/sample_with_id3.adts.unknown_length.dump @@ -0,0 +1,607 @@ +seekMap: + isSeekable = false + duration = UNSET TIME + getPosition(0) = [[timeUs=0, position=0]] +numberOfTracks = 2 +track 0: + total output bytes = 30797 + sample count = 144 + format 0: + id = 0 + sampleMimeType = audio/mp4a-latm + codecs = mp4a.40.2 + channelCount = 1 + sampleRate = 44100 + initializationData: + data = length 2, hash 5F7 + 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: + total output bytes = 141 + sample count = 2 + format 0: + id = 1 + sampleMimeType = application/id3 + sample 0: + time = 0 + flags = 1 + data = length 55, hash A7EB51A0 + sample 1: + time = 23219 + flags = 1 + data = length 86, hash 3FA72D40 +tracksEnded = true diff --git a/testdata/src/test/assets/ts/sample_with_junk b/testdata/src/test/assets/ts/sample_with_junk new file mode 100644 index 0000000000..a83777f9fb Binary files /dev/null and b/testdata/src/test/assets/ts/sample_with_junk differ diff --git a/testdata/src/test/assets/ts/sample_with_junk.0.dump b/testdata/src/test/assets/ts/sample_with_junk.0.dump new file mode 100644 index 0000000000..ae6d60aeb5 --- /dev/null +++ b/testdata/src/test/assets/ts/sample_with_junk.0.dump @@ -0,0 +1,59 @@ +seekMap: + isSeekable = true + duration = 66733 + getPosition(0) = [[timeUs=0, position=0]] + getPosition(1) = [[timeUs=1, position=0]] + getPosition(33366) = [[timeUs=33366, position=28281]] + getPosition(66733) = [[timeUs=66733, position=56751]] +numberOfTracks = 3 +track 256: + total output bytes = 45026 + sample count = 2 + format 0: + id = 1/256 + sampleMimeType = video/mpeg2 + width = 640 + height = 426 + initializationData: + data = length 22, hash CE183139 + sample 0: + time = 33366 + flags = 1 + data = length 20711, hash 34341E8 + sample 1: + time = 66733 + flags = 0 + data = length 18112, hash EC44B35B +track 257: + total output bytes = 5015 + sample count = 4 + format 0: + id = 1/257 + sampleMimeType = audio/mpeg-L2 + maxInputSize = 4096 + channelCount = 1 + sampleRate = 44100 + language = und + 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: + total output bytes = 0 + sample count = 0 + format 0: + id = 1/8448 + sampleMimeType = application/cea-608 +tracksEnded = true diff --git a/testdata/src/test/assets/ts/sample_with_junk.1.dump b/testdata/src/test/assets/ts/sample_with_junk.1.dump new file mode 100644 index 0000000000..4a03d4953e --- /dev/null +++ b/testdata/src/test/assets/ts/sample_with_junk.1.dump @@ -0,0 +1,59 @@ +seekMap: + isSeekable = true + duration = 66733 + getPosition(0) = [[timeUs=0, position=0]] + getPosition(1) = [[timeUs=1, position=0]] + getPosition(33366) = [[timeUs=33366, position=28281]] + getPosition(66733) = [[timeUs=66733, position=56751]] +numberOfTracks = 3 +track 256: + total output bytes = 45026 + sample count = 2 + format 0: + id = 1/256 + sampleMimeType = video/mpeg2 + width = 640 + height = 426 + initializationData: + data = length 22, hash CE183139 + sample 0: + time = 55610 + flags = 1 + data = length 20711, hash 34341E8 + sample 1: + time = 88977 + flags = 0 + data = length 18112, hash EC44B35B +track 257: + total output bytes = 5015 + sample count = 4 + format 0: + id = 1/257 + sampleMimeType = audio/mpeg-L2 + maxInputSize = 4096 + channelCount = 1 + sampleRate = 44100 + language = und + sample 0: + time = 44699 + flags = 1 + data = length 1253, hash 727FD1C6 + sample 1: + time = 70821 + flags = 1 + data = length 1254, hash 73FB07B8 + sample 2: + time = 96944 + flags = 1 + data = length 1254, hash 73FB07B8 + sample 3: + time = 123066 + flags = 1 + data = length 1254, hash 73FB07B8 +track 8448: + total output bytes = 0 + sample count = 0 + format 0: + id = 1/8448 + sampleMimeType = application/cea-608 +tracksEnded = true diff --git a/testdata/src/test/assets/ts/sample_with_junk.2.dump b/testdata/src/test/assets/ts/sample_with_junk.2.dump new file mode 100644 index 0000000000..7ed12dbb45 --- /dev/null +++ b/testdata/src/test/assets/ts/sample_with_junk.2.dump @@ -0,0 +1,59 @@ +seekMap: + isSeekable = true + duration = 66733 + getPosition(0) = [[timeUs=0, position=0]] + getPosition(1) = [[timeUs=1, position=0]] + getPosition(33366) = [[timeUs=33366, position=28281]] + getPosition(66733) = [[timeUs=66733, position=56751]] +numberOfTracks = 3 +track 256: + total output bytes = 45026 + sample count = 2 + format 0: + id = 1/256 + sampleMimeType = video/mpeg2 + width = 640 + height = 426 + initializationData: + data = length 22, hash CE183139 + sample 0: + time = 77854 + flags = 1 + data = length 20711, hash 34341E8 + sample 1: + time = 111221 + flags = 0 + data = length 18112, hash EC44B35B +track 257: + total output bytes = 5015 + sample count = 4 + format 0: + id = 1/257 + sampleMimeType = audio/mpeg-L2 + maxInputSize = 4096 + channelCount = 1 + sampleRate = 44100 + language = und + sample 0: + time = 66943 + flags = 1 + data = length 1253, hash 727FD1C6 + sample 1: + time = 93065 + flags = 1 + data = length 1254, hash 73FB07B8 + sample 2: + time = 119188 + flags = 1 + data = length 1254, hash 73FB07B8 + sample 3: + time = 145310 + flags = 1 + data = length 1254, hash 73FB07B8 +track 8448: + total output bytes = 0 + sample count = 0 + format 0: + id = 1/8448 + sampleMimeType = application/cea-608 +tracksEnded = true diff --git a/testdata/src/test/assets/ts/sample_with_junk.3.dump b/testdata/src/test/assets/ts/sample_with_junk.3.dump new file mode 100644 index 0000000000..603d1518f8 --- /dev/null +++ b/testdata/src/test/assets/ts/sample_with_junk.3.dump @@ -0,0 +1,43 @@ +seekMap: + isSeekable = true + duration = 66733 + getPosition(0) = [[timeUs=0, position=0]] + getPosition(1) = [[timeUs=1, position=0]] + getPosition(33366) = [[timeUs=33366, position=28281]] + getPosition(66733) = [[timeUs=66733, position=56751]] +numberOfTracks = 3 +track 256: + total output bytes = 0 + sample count = 0 + format 0: + id = 1/256 + sampleMimeType = video/mpeg2 + width = 640 + height = 426 + initializationData: + data = length 22, hash CE183139 +track 257: + total output bytes = 2508 + sample count = 2 + format 0: + id = 1/257 + sampleMimeType = audio/mpeg-L2 + maxInputSize = 4096 + channelCount = 1 + sampleRate = 44100 + language = und + sample 0: + time = 66733 + flags = 1 + data = length 1254, hash 73FB07B8 + sample 1: + time = 92855 + flags = 1 + data = length 1254, hash 73FB07B8 +track 8448: + total output bytes = 0 + sample count = 0 + format 0: + id = 1/8448 + sampleMimeType = application/cea-608 +tracksEnded = true diff --git a/testdata/src/test/assets/ts/sample_with_junk.unknown_length.dump b/testdata/src/test/assets/ts/sample_with_junk.unknown_length.dump new file mode 100644 index 0000000000..e18961d73b --- /dev/null +++ b/testdata/src/test/assets/ts/sample_with_junk.unknown_length.dump @@ -0,0 +1,56 @@ +seekMap: + isSeekable = false + duration = UNSET TIME + getPosition(0) = [[timeUs=0, position=0]] +numberOfTracks = 3 +track 256: + total output bytes = 45026 + sample count = 2 + format 0: + id = 1/256 + sampleMimeType = video/mpeg2 + width = 640 + height = 426 + initializationData: + data = length 22, hash CE183139 + sample 0: + time = 33366 + flags = 1 + data = length 20711, hash 34341E8 + sample 1: + time = 66733 + flags = 0 + data = length 18112, hash EC44B35B +track 257: + total output bytes = 5015 + sample count = 4 + format 0: + id = 1/257 + sampleMimeType = audio/mpeg-L2 + maxInputSize = 4096 + channelCount = 1 + sampleRate = 44100 + language = und + 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: + total output bytes = 0 + sample count = 0 + format 0: + id = 1/8448 + sampleMimeType = application/cea-608 +tracksEnded = true diff --git a/library/core/src/test/assets/ts/sample_with_sdt.ts b/testdata/src/test/assets/ts/sample_with_sdt.ts similarity index 100% rename from library/core/src/test/assets/ts/sample_with_sdt.ts rename to testdata/src/test/assets/ts/sample_with_sdt.ts diff --git a/library/core/src/test/assets/ttml/bitmap_percentage_region.xml b/testdata/src/test/assets/ttml/bitmap_percentage_region.xml similarity index 100% rename from library/core/src/test/assets/ttml/bitmap_percentage_region.xml rename to testdata/src/test/assets/ttml/bitmap_percentage_region.xml diff --git a/library/core/src/test/assets/ttml/bitmap_pixel_region.xml b/testdata/src/test/assets/ttml/bitmap_pixel_region.xml similarity index 100% rename from library/core/src/test/assets/ttml/bitmap_pixel_region.xml rename to testdata/src/test/assets/ttml/bitmap_pixel_region.xml diff --git a/library/core/src/test/assets/ttml/bitmap_unsupported_region.xml b/testdata/src/test/assets/ttml/bitmap_unsupported_region.xml similarity index 100% rename from library/core/src/test/assets/ttml/bitmap_unsupported_region.xml rename to testdata/src/test/assets/ttml/bitmap_unsupported_region.xml diff --git a/library/core/src/test/assets/ttml/chain_multiple_styles.xml b/testdata/src/test/assets/ttml/chain_multiple_styles.xml similarity index 100% rename from library/core/src/test/assets/ttml/chain_multiple_styles.xml rename to testdata/src/test/assets/ttml/chain_multiple_styles.xml diff --git a/library/core/src/test/assets/ttml/font_size.xml b/testdata/src/test/assets/ttml/font_size.xml similarity index 80% rename from library/core/src/test/assets/ttml/font_size.xml rename to testdata/src/test/assets/ttml/font_size.xml index a25fff1cf9..986931e547 100644 --- a/library/core/src/test/assets/ttml/font_size.xml +++ b/testdata/src/test/assets/ttml/font_size.xml @@ -3,14 +3,6 @@ xmlns:tts="http://www.w3.org/2006/10/ttaf1#style" xmlns="http://www.w3.org/ns/ttml" xmlns="http://www.w3.org/2006/10/ttaf1"> - - -