Merge pull request #7437 from google/dev-v2-r2.11.5

r2.11.5
This commit is contained in:
Oliver Woodman 2020-06-05 16:06:36 +01:00 committed by GitHub
commit 6bfdf8f8f6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
121 changed files with 4558 additions and 2356 deletions

View file

@ -1,5 +1,84 @@
# Release notes #
### 2.11.5 (2020-06-05) ###
* Improve the smoothness of video playback immediately after starting, seeking
or resuming a playback
([#6901](https://github.com/google/ExoPlayer/issues/6901)).
* Add `SilenceMediaSource.Factory` to support tags.
* Enable the configuration of `SilenceSkippingAudioProcessor`
([#6705](https://github.com/google/ExoPlayer/issues/6705)).
* Fix bug where `PlayerMessages` throw an exception after `MediaSources`
are removed from the playlist
([#7278](https://github.com/google/ExoPlayer/issues/7278)).
* Fix "Not allowed to start service" `IllegalStateException` in
`DownloadService`
([#7306](https://github.com/google/ExoPlayer/issues/7306)).
* Fix issue in `AudioTrackPositionTracker` that could cause negative positions
to be reported at the start of playback and immediately after seeking
([#7456](https://github.com/google/ExoPlayer/issues/7456).
* Fix further cases where downloads would sometimes not resume after their
network requirements are met
([#7453](https://github.com/google/ExoPlayer/issues/7453).
* 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)).
* MP4: Store the Android capture frame rate only in `Format.metadata`.
`Format.frameRate` now stores the calculated frame rate.
* FMP4: Avoid throwing an exception while parsing default sample values whose
most significant bits are set
([#7207](https://github.com/google/ExoPlayer/issues/7207)).
* MP3: Fix issue parsing the XING headers belonging to files larger than 2GB
([#7337](https://github.com/google/ExoPlayer/issues/7337)).
* MPEG-TS: Fix issue where SEI NAL units were incorrectly dropped from H.265
samples ([#7113](https://github.com/google/ExoPlayer/issues/7113)).
* UI:
* Fix `DefaultTimeBar` to respect touch transformations
([#7303](https://github.com/google/ExoPlayer/issues/7303)).
* Add `showScrubber` and `hideScrubber` methods to `DefaultTimeBar`.
* Text:
* Use anti-aliasing and bitmap filtering when displaying bitmap
subtitles.
* Fix `SubtitlePainter` to render `EDGE_TYPE_OUTLINE` using the correct
color.
* IMA extension:
* Upgrade to IMA SDK version 3.19.0, and migrate to new
preloading APIs
([#6429](https://github.com/google/ExoPlayer/issues/6429)). This fixes
several issues involving preloading and handling of ad loading error
cases: ([#4140](https://github.com/google/ExoPlayer/issues/4140),
[#5006](https://github.com/google/ExoPlayer/issues/5006),
[#6030](https://github.com/google/ExoPlayer/issues/6030),
[#6097](https://github.com/google/ExoPlayer/issues/6097),
[#6425](https://github.com/google/ExoPlayer/issues/6425),
[#6967](https://github.com/google/ExoPlayer/issues/6967),
[#7041](https://github.com/google/ExoPlayer/issues/7041),
[#7161](https://github.com/google/ExoPlayer/issues/7161),
[#7212](https://github.com/google/ExoPlayer/issues/7212),
[#7340](https://github.com/google/ExoPlayer/issues/7340)).
* Add support for timing out ad preloading, to avoid playback getting
stuck if an ad group unexpectedly fails to load
([#5444](https://github.com/google/ExoPlayer/issues/5444),
[#5966](https://github.com/google/ExoPlayer/issues/5966),
[#7002](https://github.com/google/ExoPlayer/issues/7002)).
* Fix `AdsMediaSource` child `MediaSource`s not being released.
* Cronet extension: Default to using the Cronet implementation in Google Play
Services rather than Cronet Embedded. This allows Cronet to be used with a
negligible increase in application size, compared to approximately 8MB when
embedding the library.
* OkHttp extension: Upgrade OkHttp dependency to 3.12.11.
* MediaSession extension:
* Only set the playback state to `BUFFERING` if `playWhenReady` is true
([#7206](https://github.com/google/ExoPlayer/issues/7206)).
* 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.
### 2.11.4 (2020-04-08) ###
* Add `SimpleExoPlayer.setWakeMode` to allow automatic `WifiLock` and `WakeLock`

View file

@ -13,8 +13,8 @@
// limitations under the License.
project.ext {
// ExoPlayer version and version code.
releaseVersion = '2.11.4'
releaseVersionCode = 2011004
releaseVersion = '2.11.5'
releaseVersionCode = 2011005
minSdkVersion = 16
appTargetSdkVersion = 29
targetSdkVersion = 28 // TODO: Bump once b/143232359 is resolved

View file

@ -2,3 +2,24 @@
This directory contains applications that demonstrate how to use ExoPlayer.
Browse the individual demos and their READMEs to learn more.
## Running a demo ##
### From Android Studio ###
* File -> New -> Import Project -> Specify the root ExoPlayer folder.
* Choose the demo from the run configuration dropdown list.
* Click Run.
### Using gradle from the command line: ###
* Open a Terminal window at the root ExoPlayer folder.
* Run `./gradlew projects` to show all projects. Demo projects start with `demo`.
* Run `./gradlew :<demo name>:tasks` to view the list of available tasks for
the demo project. Choose an install option from the `Install tasks` section.
* Run `./gradlew :<demo name>:<install task>`.
**Example**:
`./gradlew :demo:installNoExtensionsDebug` installs the main ExoPlayer demo app
in debug mode with no extensions.

View file

@ -2,3 +2,6 @@
This folder contains a demo application that showcases ExoPlayer integration
with Google Cast.
Please see the [demos README](../README.md) for instructions on how to build and
run this demo.

View file

@ -8,4 +8,7 @@ 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.
Please see the [demos README](../README.md) for instructions on how to build and
run this demo.
[GLSurfaceView]: https://developer.android.com/reference/android/opengl/GLSurfaceView

View file

@ -3,3 +3,6 @@
This is the main ExoPlayer demo application. It uses ExoPlayer to play a number
of test streams. It can be used as a starting point or reference project when
developing other applications that make use of the ExoPlayer library.
Please see the [demos README](../README.md) for instructions on how to build and
run this demo.

View file

@ -3,208 +3,147 @@
"name": "YouTube DASH",
"samples": [
{
"name": "Google Glass (MP4,H264)",
"name": "Google Glass H264 (MP4)",
"uri": "https://www.youtube.com/api/manifest/dash/id/bf5bb2419360daf1/source/youtube?as=fmp4_audio_clear,fmp4_sd_hd_clear&sparams=ip,ipbits,expire,source,id,as&ip=0.0.0.0&ipbits=0&expire=19000000000&signature=51AF5F39AB0CEC3E5497CD9C900EBFEAECCCB5C7.8506521BFC350652163895D4C26DEE124209AA9E&key=ik0",
"extension": "mpd"
},
{
"name": "Google Play (MP4,H264)",
"name": "Google Play H264 (MP4)",
"uri": "https://www.youtube.com/api/manifest/dash/id/3aa39fa2cc27967f/source/youtube?as=fmp4_audio_clear,fmp4_sd_hd_clear&sparams=ip,ipbits,expire,source,id,as&ip=0.0.0.0&ipbits=0&expire=19000000000&signature=A2716F75795F5D2AF0E88962FFCD10DB79384F29.84308FF04844498CE6FBCE4731507882B8307798&key=ik0",
"extension": "mpd"
},
{
"name": "Google Glass (WebM,VP9)",
"name": "Google Glass VP9 (WebM)",
"uri": "https://www.youtube.com/api/manifest/dash/id/bf5bb2419360daf1/source/youtube?as=fmp4_audio_clear,webm2_sd_hd_clear&sparams=ip,ipbits,expire,source,id,as&ip=0.0.0.0&ipbits=0&expire=19000000000&signature=249B04F79E984D7F86B4D8DB48AE6FAF41C17AB3.7B9F0EC0505E1566E59B8E488E9419F253DDF413&key=ik0",
"extension": "mpd"
},
{
"name": "Google Play (WebM,VP9)",
"name": "Google Play VP9 (WebM)",
"uri": "https://www.youtube.com/api/manifest/dash/id/3aa39fa2cc27967f/source/youtube?as=fmp4_audio_clear,webm2_sd_hd_clear&sparams=ip,ipbits,expire,source,id,as&ip=0.0.0.0&ipbits=0&expire=19000000000&signature=B1C2A74783AC1CC4865EB312D7DD2D48230CC9FD.BD153B9882175F1F94BFE5141A5482313EA38E8D&key=ik0",
"extension": "mpd"
}
]
},
{
"name": "Widevine DASH Policy Tests (GTS)",
"name": "Widevine GTS policy tests",
"samples": [
{
"name": "WV: HDCP not specified",
"name": "SW secure crypto (L3)",
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd",
"drm_scheme": "widevine",
"drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=d286538032258a1c&provider=widevine_test"
"drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=GTS_SW_SECURE_CRYPTO&provider=widevine_test"
},
{
"name": "WV: HDCP not required",
"name": "SW secure decode",
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd",
"drm_scheme": "widevine",
"drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=48fcc369939ac96c&provider=widevine_test"
"drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=GTS_SW_SECURE_DECODE&provider=widevine_test"
},
{
"name": "WV: HDCP required",
"name": "HW secure crypto",
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd",
"drm_scheme": "widevine",
"drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=e06c39f1151da3df&provider=widevine_test"
"drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=GTS_HW_SECURE_CRYPTO&provider=widevine_test"
},
{
"name": "WV: Secure video path required (MP4,H264)",
"name": "HW secure decode",
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd",
"drm_scheme": "widevine",
"drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=0894c7c8719b28a0&provider=widevine_test"
"drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=GTS_HW_SECURE_DECODE&provider=widevine_test"
},
{
"name": "WV: Secure video path required (WebM,VP9)",
"uri": "https://storage.googleapis.com/wvmedia/cenc/vp9/tears/tears.mpd",
"drm_scheme": "widevine",
"drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=0894c7c8719b28a0&provider=widevine_test"
},
{
"name": "WV: Secure video path required (MP4,H265)",
"uri": "https://storage.googleapis.com/wvmedia/cenc/hevc/tears/tears.mpd",
"drm_scheme": "widevine",
"drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=0894c7c8719b28a0&provider=widevine_test"
},
{
"name": "WV: HDCP + secure video path required",
"name": "HW secure all (L1)",
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd",
"drm_scheme": "widevine",
"drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=efd045b1eb61888a&provider=widevine_test"
"drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=GTS_HW_SECURE_ALL&provider=widevine_test"
},
{
"name": "WV: 30s license duration (fails at ~30s)",
"name": "30s license (fails at ~30s)",
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd",
"drm_scheme": "widevine",
"drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=f9a34cab7b05881a&provider=widevine_test"
"drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=GTS_CAN_RENEW_FALSE_LICENSE_30S_PLAYBACK_30S&provider=widevine_test"
},
{
"name": "HDCP not required",
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd",
"drm_scheme": "widevine",
"drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=GTS_SW_SECURE_CRYPTO_HDCP_NONE&provider=widevine_test"
},
{
"name": "HDCP 1.0 required",
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd",
"drm_scheme": "widevine",
"drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=GTS_SW_SECURE_CRYPTO_HDCP_V1&provider=widevine_test"
},
{
"name": "HDCP 2.0 required",
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd",
"drm_scheme": "widevine",
"drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=GTS_SW_SECURE_CRYPTO_HDCP_V2&provider=widevine_test"
},
{
"name": "HDCP 2.1 required",
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd",
"drm_scheme": "widevine",
"drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=GTS_SW_SECURE_CRYPTO_HDCP_V2_1&provider=widevine_test"
},
{
"name": "HDCP 2.2 required",
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd",
"drm_scheme": "widevine",
"drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=GTS_SW_SECURE_CRYPTO_HDCP_V2_2&provider=widevine_test"
},
{
"name": "HDCP no digital output",
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd",
"drm_scheme": "widevine",
"drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=GTS_SW_SECURE_CRYPTO_HDCP_NO_DIGITAL_OUTPUT&provider=widevine_test"
}
]
},
{
"name": "Widevine HDCP Capabilities Tests",
"name": "Widevine DASH H264 (MP4)",
"samples": [
{
"name": "WV: HDCP: None (not required)",
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd",
"drm_scheme": "widevine",
"drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=HDCP_None&provider=widevine_test"
},
{
"name": "WV: HDCP: 1.0 required",
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd",
"drm_scheme": "widevine",
"drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=HDCP_V1&provider=widevine_test"
},
{
"name": "WV: HDCP: 2.0 required",
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd",
"drm_scheme": "widevine",
"drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=HDCP_V2&provider=widevine_test"
},
{
"name": "WV: HDCP: 2.1 required",
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd",
"drm_scheme": "widevine",
"drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=HDCP_V2_1&provider=widevine_test"
},
{
"name": "WV: HDCP: 2.2 required",
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd",
"drm_scheme": "widevine",
"drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=HDCP_V2_2&provider=widevine_test"
},
{
"name": "WV: HDCP: No digital output",
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd",
"drm_scheme": "widevine",
"drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=HDCP_NO_DIGTAL_OUTPUT&provider=widevine_test"
}
]
},
{
"name": "Widevine DASH: MP4,H264",
"samples": [
{
"name": "WV: Clear SD & HD (MP4,H264)",
"name": "Clear",
"uri": "https://storage.googleapis.com/wvmedia/clear/h264/tears/tears.mpd"
},
{
"name": "WV: Clear SD (MP4,H264)",
"uri": "https://storage.googleapis.com/wvmedia/clear/h264/tears/tears_sd.mpd"
},
{
"name": "WV: Clear HD (MP4,H264)",
"uri": "https://storage.googleapis.com/wvmedia/clear/h264/tears/tears_hd.mpd"
},
{
"name": "WV: Clear UHD (MP4,H264)",
"name": "Clear UHD",
"uri": "https://storage.googleapis.com/wvmedia/clear/h264/tears/tears_uhd.mpd"
},
{
"name": "WV: Secure SD & HD (cenc,MP4,H264)",
"name": "Secure (cenc)",
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd",
"drm_scheme": "widevine",
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
},
{
"name": "WV: Secure SD (cenc,MP4,H264)",
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_sd.mpd",
"drm_scheme": "widevine",
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
},
{
"name": "WV: Secure HD (cenc,MP4,H264)",
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_hd.mpd",
"drm_scheme": "widevine",
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
},
{
"name": "WV: Secure UHD (cenc,MP4,H264)",
"name": "Secure UHD (cenc)",
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_uhd.mpd",
"drm_scheme": "widevine",
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
},
{
"name": "WV: Secure SD & HD (cbc1,MP4,H264)",
"name": "Secure (cbc1)",
"uri": "https://storage.googleapis.com/wvmedia/cbc1/h264/tears/tears_aes_cbc1.mpd",
"drm_scheme": "widevine",
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
},
{
"name": "WV: Secure SD (cbc1,MP4,H264)",
"uri": "https://storage.googleapis.com/wvmedia/cbc1/h264/tears/tears_aes_cbc1_sd.mpd",
"drm_scheme": "widevine",
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
},
{
"name": "WV: Secure HD (cbc1,MP4,H264)",
"uri": "https://storage.googleapis.com/wvmedia/cbc1/h264/tears/tears_aes_cbc1_hd.mpd",
"drm_scheme": "widevine",
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
},
{
"name": "WV: Secure UHD (cbc1,MP4,H264)",
"name": "Secure UHD (cbc1)",
"uri": "https://storage.googleapis.com/wvmedia/cbc1/h264/tears/tears_aes_cbc1_uhd.mpd",
"drm_scheme": "widevine",
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
},
{
"name": "WV: Secure SD & HD (cbcs,MP4,H264)",
"name": "Secure (cbcs)",
"uri": "https://storage.googleapis.com/wvmedia/cbcs/h264/tears/tears_aes_cbcs.mpd",
"drm_scheme": "widevine",
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
},
{
"name": "WV: Secure SD (cbcs,MP4,H264)",
"uri": "https://storage.googleapis.com/wvmedia/cbcs/h264/tears/tears_aes_cbcs_sd.mpd",
"drm_scheme": "widevine",
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
},
{
"name": "WV: Secure HD (cbcs,MP4,H264)",
"uri": "https://storage.googleapis.com/wvmedia/cbcs/h264/tears/tears_aes_cbcs_hd.mpd",
"drm_scheme": "widevine",
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
},
{
"name": "WV: Secure UHD (cbcs,MP4,H264)",
"name": "Secure UHD (cbcs)",
"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"
@ -212,68 +151,36 @@
]
},
{
"name": "Widevine DASH: WebM,VP9",
"name": "Widevine DASH VP9 (WebM)",
"samples": [
{
"name": "WV: Clear SD & HD (WebM,VP9)",
"name": "Clear",
"uri": "https://storage.googleapis.com/wvmedia/clear/vp9/tears/tears.mpd"
},
{
"name": "WV: Clear SD (WebM,VP9)",
"uri": "https://storage.googleapis.com/wvmedia/clear/vp9/tears/tears_sd.mpd"
},
{
"name": "WV: Clear HD (WebM,VP9)",
"uri": "https://storage.googleapis.com/wvmedia/clear/vp9/tears/tears_hd.mpd"
},
{
"name": "WV: Clear UHD (WebM,VP9)",
"name": "Clear UHD",
"uri": "https://storage.googleapis.com/wvmedia/clear/vp9/tears/tears_uhd.mpd"
},
{
"name": "WV: Secure Fullsample SD & HD (WebM,VP9)",
"name": "Secure (full-sample)",
"uri": "https://storage.googleapis.com/wvmedia/cenc/vp9/tears/tears.mpd",
"drm_scheme": "widevine",
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
},
{
"name": "WV: Secure Fullsample SD (WebM,VP9)",
"uri": "https://storage.googleapis.com/wvmedia/cenc/vp9/tears/tears_sd.mpd",
"drm_scheme": "widevine",
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
},
{
"name": "WV: Secure Fullsample HD (WebM,VP9)",
"uri": "https://storage.googleapis.com/wvmedia/cenc/vp9/tears/tears_hd.mpd",
"drm_scheme": "widevine",
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
},
{
"name": "WV: Secure Fullsample UHD (WebM,VP9)",
"name": "Secure UHD (full-sample)",
"uri": "https://storage.googleapis.com/wvmedia/cenc/vp9/tears/tears_uhd.mpd",
"drm_scheme": "widevine",
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
},
{
"name": "WV: Secure Subsample SD & HD (WebM,VP9)",
"name": "Secure (sub-sample)",
"uri": "https://storage.googleapis.com/wvmedia/cenc/vp9/subsample/24fps/tears/tears.mpd",
"drm_scheme": "widevine",
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
},
{
"name": "WV: Secure Subsample SD (WebM,VP9)",
"uri": "https://storage.googleapis.com/wvmedia/cenc/vp9/subsample/24fps/tears/tears_sd.mpd",
"drm_scheme": "widevine",
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
},
{
"name": "WV: Secure Subsample HD (WebM,VP9)",
"uri": "https://storage.googleapis.com/wvmedia/cenc/vp9/subsample/24fps/tears/tears_hd.mpd",
"drm_scheme": "widevine",
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
},
{
"name": "WV: Secure Subsample UHD (WebM,VP9)",
"name": "Secure UHD (sub-sample)",
"uri": "https://storage.googleapis.com/wvmedia/cenc/vp9/subsample/24fps/tears/tears_uhd.mpd",
"drm_scheme": "widevine",
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
@ -281,50 +188,51 @@
]
},
{
"name": "Widevine DASH: MP4,H265",
"name": "Widevine DASH H265 (MP4)",
"samples": [
{
"name": "WV: Clear SD & HD (MP4,H265)",
"name": "Clear",
"uri": "https://storage.googleapis.com/wvmedia/clear/hevc/tears/tears.mpd"
},
{
"name": "WV: Clear SD (MP4,H265)",
"uri": "https://storage.googleapis.com/wvmedia/clear/hevc/tears/tears_sd.mpd"
},
{
"name": "WV: Clear HD (MP4,H265)",
"uri": "https://storage.googleapis.com/wvmedia/clear/hevc/tears/tears_hd.mpd"
},
{
"name": "WV: Clear UHD (MP4,H265)",
"name": "Clear UHD",
"uri": "https://storage.googleapis.com/wvmedia/clear/hevc/tears/tears_uhd.mpd"
},
{
"name": "WV: Secure SD & HD (MP4,H265)",
"name": "Secure",
"uri": "https://storage.googleapis.com/wvmedia/cenc/hevc/tears/tears.mpd",
"drm_scheme": "widevine",
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
},
{
"name": "WV: Secure SD (MP4,H265)",
"uri": "https://storage.googleapis.com/wvmedia/cenc/hevc/tears/tears_sd.mpd",
"drm_scheme": "widevine",
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
},
{
"name": "WV: Secure HD (MP4,H265)",
"uri": "https://storage.googleapis.com/wvmedia/cenc/hevc/tears/tears_hd.mpd",
"drm_scheme": "widevine",
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
},
{
"name": "WV: Secure UHD (MP4,H265)",
"name": "Secure UHD",
"uri": "https://storage.googleapis.com/wvmedia/cenc/hevc/tears/tears_uhd.mpd",
"drm_scheme": "widevine",
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
}
]
},
{
"name": "Widevine AV1 (WebM)",
"samples": [
{
"name": "Clear",
"uri": "https://storage.googleapis.com/wvmedia/2019/clear/av1/24/webm/llama_av1_480p_400.webm"
},
{
"name": "Secure L3",
"uri": "https://storage.googleapis.com/wvmedia/2019/cenc/av1/24/webm/llama_av1_480p_400.webm",
"drm_scheme": "widevine",
"drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=GTS_SW_SECURE_CRYPTO&provider=widevine_test"
},
{
"name": "Secure L1",
"uri": "https://storage.googleapis.com/wvmedia/2019/cenc/av1/24/webm/llama_av1_480p_400.webm",
"drm_scheme": "widevine",
"drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=GTS_HW_SECURE_ALL&provider=widevine_test"
}
]
},
{
"name": "SmoothStreaming",
"samples": [
@ -355,7 +263,7 @@
"uri": "https://devstreaming-cdn.apple.com/videos/streaming/examples/img_bipbop_adv_example_ts/master.m3u8"
},
{
"name": "Apple master playlist advanced (fMP4)",
"name": "Apple master playlist advanced (FMP4)",
"uri": "https://devstreaming-cdn.apple.com/videos/streaming/examples/img_bipbop_adv_example_fmp4/master.m3u8"
},
{

View file

@ -628,7 +628,10 @@ public class PlayerActivity extends AppCompatActivity
@Override
public MediaSourceFactory setDrmSessionManager(DrmSessionManager<?> drmSessionManager) {
this.drmSessionManager = drmSessionManager;
this.drmSessionManager =
drmSessionManager != null
? drmSessionManager
: DrmSessionManager.getDummyDrmSessionManager();
return this;
}

View file

@ -18,4 +18,7 @@ called, and because you can move output off-screen easily (`setOutputSurface`
can't take a `null` surface, so the player has to use a `DummySurface`, which
doesn't handle protected output on all devices).
Please see the [demos README](../README.md) for instructions on how to build and
run this demo.
[SurfaceControl]: https://developer.android.com/reference/android/view/SurfaceControl

View file

@ -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(
@ -231,4 +245,11 @@ import java.nio.ByteBuffer;
* @return {@link #GAV1_OK} if there was no error, {@link #GAV1_ERROR} if an error occured.
*/
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();
}

View file

@ -15,8 +15,6 @@
*/
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;
@ -55,6 +53,13 @@ import com.google.android.exoplayer2.video.VideoRendererEventListener;
*/
public class Libgav1VideoRenderer extends SimpleDecoderVideoRenderer {
/**
* 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 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. */
@ -94,7 +99,7 @@ public class Libgav1VideoRenderer extends SimpleDecoderVideoRenderer {
eventHandler,
eventListener,
maxDroppedFramesToNotify,
/* threads= */ getRuntime().availableProcessors(),
THREAD_COUNT_AUTODETECT,
DEFAULT_NUM_OF_INPUT_BUFFERS,
DEFAULT_NUM_OF_OUTPUT_BUFFERS);
}
@ -109,7 +114,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
* auto-detected based on CPU capabilities.
* @param numInputBuffers Number of input buffers.
* @param numOutputBuffers Number of output buffers.
*/

View file

@ -44,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)

View file

@ -0,0 +1,153 @@
#include "cpu_info.h" // NOLINT
#include <unistd.h>
#include <cerrno>
#include <climits>
#include <cstdio>
#include <cstdlib>
#include <cstring>
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<int>(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<int>(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

View file

@ -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_

View file

@ -32,6 +32,7 @@
#include <mutex> // NOLINT
#include <new>
#include "cpu_info.h" // NOLINT
#include "gav1/decoder.h"
#define LOG_TAG "gav1_jni"
@ -774,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.

View file

@ -20,6 +20,10 @@ Alternatively, you can clone the ExoPlayer repository and depend on the module
locally. Instructions for doing this can be found in ExoPlayer's
[top level README][].
Note that by default, the extension will use the Cronet implementation in
Google Play Services. If you prefer, it's also possible to embed the Cronet
implementation directly into your application. See below for more details.
[top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md
## Using the extension ##
@ -47,6 +51,46 @@ new DefaultDataSourceFactory(
```
respectively.
## Choosing between Google Play Services Cronet and Cronet Embedded ##
The underlying Cronet implementation is available both via a [Google Play
Services](https://developers.google.com/android/guides/overview) API, and as a
library that can be embedded directly into your application. When you depend on
`com.google.android.exoplayer:extension-cronet:2.X.X`, the library will _not_ be
embedded into your application by default. The extension will attempt to use the
Cronet implementation in Google Play Services. The benefits of this approach
are:
* A negligible increase in the size of your application.
* The Cronet implementation is updated automatically by Google Play Services.
If Google Play Services is not available on a device, `CronetDataSourceFactory`
will fall back to creating `DefaultHttpDataSource` instances, or
`HttpDataSource` instances created by a `fallbackFactory` that you can specify.
It's also possible to embed the Cronet implementation directly into your
application. To do this, add an additional gradle dependency to the Cronet
Embedded library:
```gradle
implementation 'com.google.android.exoplayer:extension-cronet:2.X.X'
implementation 'org.chromium.net:cronet-embedded:XX.XXXX.XXX'
```
where `XX.XXXX.XXX` is the version of the library that you wish to use. The
extension will automatically detect and use the library. Embedding will add
approximately 8MB to your application, however it may be suitable if:
* Your application is likely to be used in markets where Google Play Services is
not widely available.
* You want to control the exact version of the Cronet implementation being used.
If you do embed the library, you can specify which implementation should
be preferred if the Google Play Services implementation is also available. This
is controlled by a `preferGMSCoreCronet` parameter, which can be passed to the
`CronetEngineWrapper` constructor (GMS Core is another name for Google Play
Services).
## Links ##
* [Javadoc][]: Classes matching `com.google.android.exoplayer2.ext.cronet.*`

View file

@ -31,7 +31,7 @@ android {
}
dependencies {
api 'org.chromium.net:cronet-embedded:76.3809.111'
api "com.google.android.gms:play-services-cronet:17.0.0"
implementation project(modulePrefix + 'library-core')
implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion

View file

@ -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) {

View file

@ -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;
@ -47,6 +48,7 @@ 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.concurrent.CountDownLatch;
import java.util.concurrent.Executor;
@ -56,7 +58,6 @@ import org.chromium.net.CronetEngine;
import org.chromium.net.NetworkException;
import org.chromium.net.UrlRequest;
import org.chromium.net.UrlResponseInfo;
import org.chromium.net.impl.UrlResponseInfoImpl;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@ -139,15 +140,62 @@ public final class CronetDataSourceTest {
private UrlResponseInfo createUrlResponseInfoWithUrl(String url, int statusCode) {
ArrayList<Map.Entry<String, String>> responseHeaderList = new ArrayList<>();
responseHeaderList.addAll(testResponseHeader.entrySet());
return new UrlResponseInfoImpl(
Collections.singletonList(url),
statusCode,
null, // httpStatusText
responseHeaderList,
false, // wasCached
null, // negotiatedProtocol
null); // proxyServer
Map<String, List<String>> responseHeaderMap = new HashMap<>();
for (Map.Entry<String, String> entry : testResponseHeader.entrySet()) {
responseHeaderList.add(entry);
responseHeaderMap.put(entry.getKey(), Collections.singletonList(entry.getValue()));
}
return new UrlResponseInfo() {
@Override
public String getUrl() {
return url;
}
@Override
public List<String> getUrlChain() {
return Collections.singletonList(url);
}
@Override
public int getHttpStatusCode() {
return statusCode;
}
@Override
public String getHttpStatusText() {
return null;
}
@Override
public List<Map.Entry<String, String>> getAllHeadersAsList() {
return responseHeaderList;
}
@Override
public Map<String, List<String>> getAllHeaders() {
return responseHeaderMap;
}
@Override
public boolean wasCached() {
return false;
}
@Override
public String getNegotiatedProtocol() {
return null;
}
@Override
public String getProxyServer() {
return null;
}
@Override
public long getReceivedByteCount() {
return 0;
}
};
}
@Test
@ -282,7 +330,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);
@ -320,7 +368,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);
@ -336,7 +384,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())
@ -359,7 +407,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);
@ -890,8 +938,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();
@ -928,8 +976,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();
@ -999,8 +1047,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();
}
@ -1224,7 +1272,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();
}
}
@ -1255,7 +1303,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();
}
}

View file

@ -32,11 +32,12 @@ android {
}
dependencies {
api 'com.google.ads.interactivemedia.v3:interactivemedia:3.11.3'
api 'com.google.ads.interactivemedia.v3:interactivemedia:3.19.0'
implementation project(modulePrefix + 'library-core')
implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
implementation 'com.google.android.gms:play-services-ads-identifier:17.0.0'
testImplementation project(modulePrefix + 'testutils')
testImplementation 'com.google.guava:guava:' + guavaVersion
testImplementation 'org.robolectric:robolectric:' + robolectricVersion
}

View file

@ -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<UiElement> getUiElements() {
throw new UnsupportedOperationException();
}
@Override
public List<CompanionAd> getCompanionAds() {
throw new UnsupportedOperationException();
}
}

View file

@ -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<AdsLoadedListener> adsLoadedListeners;
private final ArrayList<AdErrorListener> 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);
}
}

View file

@ -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<String, String> 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<String> 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();
}
}

View file

@ -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,79 @@ 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 com.google.common.collect.ImmutableMap;
import java.io.IOException;
import java.time.Duration;
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;
import org.robolectric.shadows.ShadowSystemClock;
/** 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 long[][] 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;
@Mock private AdEvent mockPostrollFetchErrorAdEvent;
private ViewGroup adViewGroup;
private View adOverlayView;
private AdsLoader.AdViewProvider adViewProvider;
@ -86,16 +117,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,44 +142,54 @@ public class ImaAdsLoaderTest {
}
@Test
public void testBuilder_overridesPlayerType() {
when(imaSdkSettings.getPlayerType()).thenReturn("test player type");
setupPlayback(CONTENT_TIMELINE, PREROLL_ADS_DURATIONS_US, PREROLL_CUE_POINTS_SECONDS);
public void builder_overridesPlayerType() {
when(mockImaSdkSettings.getPlayerType()).thenReturn("test player type");
setupPlayback(CONTENT_TIMELINE, 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() {
setupPlayback(CONTENT_TIMELINE, PREROLL_ADS_DURATIONS_US, PREROLL_CUE_POINTS_SECONDS);
public void start_setsAdUiViewGroup() {
setupPlayback(CONTENT_TIMELINE, 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() {
setupPlayback(CONTENT_TIMELINE, PREROLL_ADS_DURATIONS_US, PREROLL_CUE_POINTS_SECONDS);
public void start_withPlaceholderContent_initializedAdsLoader() {
Timeline placeholderTimeline = new DummyTimeline(/* tag= */ null);
setupPlayback(placeholderTimeline, 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, ADS_DURATIONS_US, PREROLL_CUE_POINTS_SECONDS);
imaAdsLoader.start(adsLoaderListener, adViewProvider);
assertThat(adsLoaderListener.adPlaybackState)
.isEqualTo(
new AdPlaybackState(/* adGroupTimesUs= */ 0)
.withAdDurationsUs(PREROLL_ADS_DURATIONS_US)
.withContentDurationUs(CONTENT_DURATION_US));
new AdPlaybackState(/* adGroupTimesUs...= */ 0)
.withAdDurationsUs(ADS_DURATIONS_US)
.withContentDurationUs(CONTENT_PERIOD_DURATION_US));
}
@Test
public void testStartAfterRelease() {
setupPlayback(CONTENT_TIMELINE, PREROLL_ADS_DURATIONS_US, PREROLL_CUE_POINTS_SECONDS);
public void startAfterRelease() {
setupPlayback(CONTENT_TIMELINE, ADS_DURATIONS_US, PREROLL_CUE_POINTS_SECONDS);
imaAdsLoader.release();
imaAdsLoader.start(adsLoaderListener, adViewProvider);
}
@Test
public void testStartAndCallbacksAfterRelease() {
setupPlayback(CONTENT_TIMELINE, PREROLL_ADS_DURATIONS_US, PREROLL_CUE_POINTS_SECONDS);
public void startAndCallbacksAfterRelease() {
setupPlayback(CONTENT_TIMELINE, ADS_DURATIONS_US, PREROLL_CUE_POINTS_SECONDS);
imaAdsLoader.release();
imaAdsLoader.start(adsLoaderListener, adViewProvider);
fakeExoPlayer.setPlayingContentPosition(/* position= */ 0);
@ -168,13 +200,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,69 +215,188 @@ public class ImaAdsLoaderTest {
}
@Test
public void testPlayback_withPrerollAd_marksAdAsPlayed() {
setupPlayback(CONTENT_TIMELINE, PREROLL_ADS_DURATIONS_US, PREROLL_CUE_POINTS_SECONDS);
public void playback_withPrerollAd_marksAdAsPlayed() {
setupPlayback(CONTENT_TIMELINE, 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)
.withAdDurationsUs(ADS_DURATIONS_US)
.withPlayedAd(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0)
.withAdResumePositionUs(/* adResumePositionUs= */ 0));
}
@Test
public void testStop_unregistersAllVideoControlOverlays() {
setupPlayback(CONTENT_TIMELINE, PREROLL_ADS_DURATIONS_US, PREROLL_CUE_POINTS_SECONDS);
public void playback_withPostrollFetchError_marksAdAsInErrorState() {
setupPlayback(CONTENT_TIMELINE, ADS_DURATIONS_US, new Float[] {-1f});
// Simulate loading an empty postroll ad.
imaAdsLoader.start(adsLoaderListener, adViewProvider);
imaAdsLoader.onAdEvent(mockPostrollFetchErrorAdEvent);
assertThat(adsLoaderListener.adPlaybackState)
.isEqualTo(
new AdPlaybackState(/* adGroupTimesUs...= */ C.TIME_END_OF_SOURCE)
.withContentDurationUs(CONTENT_PERIOD_DURATION_US)
.withAdDurationsUs(ADS_DURATIONS_US)
.withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 1)
.withAdLoadError(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0));
}
@Test
public void playback_withAdNotPreloadingBeforeTimeout_hasNoError() {
// Simulate an ad at 2 seconds.
long adGroupPositionInWindowUs = 2 * C.MICROS_PER_SECOND;
setupPlayback(
CONTENT_TIMELINE,
ADS_DURATIONS_US,
new Float[] {(float) adGroupPositionInWindowUs / C.MICROS_PER_SECOND});
// Advance playback to just before the midroll and simulate buffering.
imaAdsLoader.start(adsLoaderListener, adViewProvider);
fakeExoPlayer.setPlayingContentPosition(C.usToMs(adGroupPositionInWindowUs));
fakeExoPlayer.setState(Player.STATE_BUFFERING, /* playWhenReady= */ true);
// Advance before the timeout and simulating polling content progress.
ShadowSystemClock.advanceBy(Duration.ofSeconds(1));
imaAdsLoader.getContentProgress();
assertThat(adsLoaderListener.adPlaybackState)
.isEqualTo(
new AdPlaybackState(/* adGroupTimesUs...= */ adGroupPositionInWindowUs)
.withContentDurationUs(CONTENT_PERIOD_DURATION_US)
.withAdDurationsUs(ADS_DURATIONS_US));
}
@Test
public void playback_withAdNotPreloadingAfterTimeout_hasErrorAdGroup() {
// Simulate an ad at 2 seconds.
long adGroupPositionInWindowUs = 2 * C.MICROS_PER_SECOND;
setupPlayback(
CONTENT_TIMELINE,
ADS_DURATIONS_US,
new Float[] {(float) adGroupPositionInWindowUs / C.MICROS_PER_SECOND});
// Advance playback to just before the midroll and simulate buffering.
imaAdsLoader.start(adsLoaderListener, adViewProvider);
fakeExoPlayer.setPlayingContentPosition(C.usToMs(adGroupPositionInWindowUs));
fakeExoPlayer.setState(Player.STATE_BUFFERING, /* playWhenReady= */ true);
// Advance past the timeout and simulate polling content progress.
ShadowSystemClock.advanceBy(Duration.ofSeconds(5));
imaAdsLoader.getContentProgress();
assertThat(adsLoaderListener.adPlaybackState)
.isEqualTo(
new AdPlaybackState(/* adGroupTimesUs...= */ adGroupPositionInWindowUs)
.withContentDurationUs(CONTENT_PERIOD_DURATION_US)
.withAdDurationsUs(ADS_DURATIONS_US)
.withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 1)
.withAdLoadError(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0));
}
@Test
public void stop_unregistersAllVideoControlOverlays() {
setupPlayback(CONTENT_TIMELINE, 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<Object> userRequestContextCaptor = ArgumentCaptor.forClass(Object.class);
doNothing().when(mockAdsRequest).setUserRequestContext(userRequestContextCaptor.capture());
when(mockAdsRequest.getUserRequestContext())
.thenAnswer((Answer<Object>) invocation -> userRequestContextCaptor.getValue());
List<com.google.ads.interactivemedia.v3.api.AdsLoader.AdsLoadedListener> 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<Object>)
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);
when(mockPostrollFetchErrorAdEvent.getType()).thenReturn(AdEventType.AD_BREAK_FETCH_ERROR);
when(mockPostrollFetchErrorAdEvent.getAdData())
.thenReturn(ImmutableMap.of("adBreakTime", "-1"));
}
private static AdEvent getAdEvent(AdEventType adEventType, @Nullable Ad ad) {
return new AdEvent() {
@Override
@ -286,7 +437,8 @@ 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));
}
@Override

View file

@ -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;
}
}

View file

@ -218,25 +218,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 +336,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 +381,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);
@ -987,7 +987,9 @@ public final class MediaSessionConnector {
@Player.State int exoPlayerPlaybackState, boolean playWhenReady) {
switch (exoPlayerPlaybackState) {
case Player.STATE_BUFFERING:
return PlaybackStateCompat.STATE_BUFFERING;
return playWhenReady
? PlaybackStateCompat.STATE_BUFFERING
: PlaybackStateCompat.STATE_PAUSED;
case Player.STATE_READY:
return playWhenReady ? PlaybackStateCompat.STATE_PLAYING : PlaybackStateCompat.STATE_PAUSED;
case Player.STATE_ENDED:
@ -1319,42 +1321,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);
}
@ -1368,7 +1370,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);
}

View file

@ -41,7 +41,7 @@ dependencies {
// 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 won't
// stop us from making this mistake!
api 'com.squareup.okhttp3:okhttp:3.12.8'
api 'com.squareup.okhttp3:okhttp:3.12.11'
}
ext {

View file

@ -1056,7 +1056,8 @@ public final class C {
* #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)
@ -1076,7 +1077,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. */
@ -1122,6 +1124,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

View file

@ -120,7 +120,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
private int pendingPrepareCount;
private SeekPosition pendingInitialSeekPosition;
private long rendererPositionUs;
private int nextPendingMessageIndex;
private int nextPendingMessageIndexHint;
private boolean deliverPendingMessageAtStartPositionRequired;
public ExoPlayerImplInternal(
@ -928,7 +928,6 @@ import java.util.concurrent.atomic.AtomicBoolean;
pendingMessageInfo.message.markAsProcessed(/* isDelivered= */ false);
}
pendingMessages.clear();
nextPendingMessageIndex = 0;
}
MediaPeriodId mediaPeriodId =
resetPosition
@ -954,7 +953,12 @@ import java.util.concurrent.atomic.AtomicBoolean;
startPositionUs);
if (releaseMediaSource) {
if (mediaSource != null) {
mediaSource.releaseSource(/* caller= */ this);
try {
mediaSource.releaseSource(/* caller= */ this);
} catch (RuntimeException e) {
// There's nothing we can do.
Log.e(TAG, "Failed to release child source.", e);
}
mediaSource = null;
}
}
@ -1077,6 +1081,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
// 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
@ -1122,6 +1127,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
? pendingMessages.get(nextPendingMessageIndex)
: null;
}
nextPendingMessageIndexHint = nextPendingMessageIndex;
}
private void ensureStopped(Renderer renderer) throws ExoPlaybackException {

View file

@ -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.4";
public static final String VERSION = "2.11.5";
/** 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.4";
public static final String VERSION_SLASHY = "ExoPlayerLib/2.11.5";
/**
* 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 = 2011004;
public static final int VERSION_INT = 2011005;
/**
* Whether the library was compiled with {@link com.google.android.exoplayer2.util.Assertions}

View file

@ -19,6 +19,7 @@ import android.util.Pair;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.source.ads.AdPlaybackState;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Util;
/**
* A flexible representation of the structure of media. A timeline is able to represent the
@ -278,6 +279,48 @@ public abstract class Timeline {
return positionInFirstPeriodUs;
}
@Override
public boolean equals(@Nullable Object obj) {
if (this == obj) {
return true;
}
if (obj == null || !getClass().equals(obj.getClass())) {
return false;
}
Window that = (Window) obj;
return Util.areEqual(uid, that.uid)
&& Util.areEqual(tag, that.tag)
&& Util.areEqual(manifest, that.manifest)
&& presentationStartTimeMs == that.presentationStartTimeMs
&& windowStartTimeMs == that.windowStartTimeMs
&& isSeekable == that.isSeekable
&& isDynamic == that.isDynamic
&& isLive == that.isLive
&& defaultPositionUs == that.defaultPositionUs
&& durationUs == that.durationUs
&& firstPeriodIndex == that.firstPeriodIndex
&& lastPeriodIndex == that.lastPeriodIndex
&& positionInFirstPeriodUs == that.positionInFirstPeriodUs;
}
@Override
public int hashCode() {
int result = 7;
result = 31 * result + uid.hashCode();
result = 31 * result + (tag == null ? 0 : tag.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 + (isSeekable ? 1 : 0);
result = 31 * result + (isDynamic ? 1 : 0);
result = 31 * result + (isLive ? 1 : 0);
result = 31 * result + (int) (defaultPositionUs ^ (defaultPositionUs >>> 32));
result = 31 * result + (int) (durationUs ^ (durationUs >>> 32));
result = 31 * result + firstPeriodIndex;
result = 31 * result + lastPeriodIndex;
result = 31 * result + (int) (positionInFirstPeriodUs ^ (positionInFirstPeriodUs >>> 32));
return result;
}
}
/**
@ -423,8 +466,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];
@ -467,22 +510,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) {
@ -534,6 +578,34 @@ public abstract class Timeline {
return adPlaybackState.adResumePositionUs;
}
@Override
public boolean equals(@Nullable Object obj) {
if (this == obj) {
return true;
}
if (obj == null || !getClass().equals(obj.getClass())) {
return false;
}
Period that = (Period) obj;
return Util.areEqual(id, that.id)
&& Util.areEqual(uid, that.uid)
&& windowIndex == that.windowIndex
&& durationUs == that.durationUs
&& positionInWindowUs == that.positionInWindowUs
&& Util.areEqual(adPlaybackState, that.adPlaybackState);
}
@Override
public int hashCode() {
int result = 7;
result = 31 * result + (id == null ? 0 : id.hashCode());
result = 31 * result + (uid == null ? 0 : uid.hashCode());
result = 31 * result + windowIndex;
result = 31 * result + (int) (durationUs ^ (durationUs >>> 32));
result = 31 * result + (int) (positionInWindowUs ^ (positionInWindowUs >>> 32));
result = 31 * result + (adPlaybackState == null ? 0 : adPlaybackState.hashCode());
return result;
}
}
/** An empty timeline. */
@ -834,4 +906,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;
}
}

View file

@ -29,7 +29,6 @@ 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
@ -48,8 +47,7 @@ public final class DefaultPlaybackSessionManager implements PlaybackSessionManag
private @MonotonicNonNull Listener listener;
private Timeline currentTimeline;
@Nullable private MediaPeriodId currentMediaPeriodId;
@Nullable private String activeSessionId;
@Nullable private String currentSessionId;
/** Creates session manager. */
public DefaultPlaybackSessionManager() {
@ -83,22 +81,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 +122,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 +146,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<SessionDescriptor> 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(
@ -199,18 +228,6 @@ public final class DefaultPlaybackSessionManager implements PlaybackSessionManag
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() {
byte[] randomBytes = new byte[SESSION_ID_LENGTH];
RANDOM.nextBytes(randomBytes);
@ -284,8 +301,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;
}

View file

@ -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);
}

View file

@ -148,7 +148,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<String, PlaybackStatsTracker> trackerCopy = new HashMap<>(playbackStatsTrackers);
EventTime dummyEventTime =
new EventTime(
SystemClock.elapsedRealtime(),
@ -158,9 +157,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.
@ -189,11 +186,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 +204,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))
@ -239,7 +240,7 @@ public final class PlaybackStatsListener
EventTime eventTime, boolean playWhenReady, @Player.State int playbackState) {
this.playWhenReady = playWhenReady;
this.playbackState = playbackState;
sessionManager.updateSessions(eventTime);
maybeAddSession(eventTime);
for (String session : playbackStatsTrackers.keySet()) {
boolean belongsToPlayback = sessionManager.belongsToSession(eventTime, session);
playbackStatsTrackers
@ -252,7 +253,7 @@ public final class PlaybackStatsListener
public void onPlaybackSuppressionReasonChanged(
EventTime eventTime, 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
@ -264,7 +265,7 @@ public final class PlaybackStatsListener
@Override
public void onTimelineChanged(EventTime eventTime, 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);
@ -275,7 +276,7 @@ public final class PlaybackStatsListener
@Override
public void onPositionDiscontinuity(EventTime eventTime, int reason) {
sessionManager.handlePositionDiscontinuity(eventTime, reason);
sessionManager.updateSessions(eventTime);
maybeAddSession(eventTime);
for (String session : playbackStatsTrackers.keySet()) {
if (sessionManager.belongsToSession(eventTime, session)) {
playbackStatsTrackers.get(session).onPositionDiscontinuity(eventTime);
@ -285,7 +286,7 @@ public final class PlaybackStatsListener
@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);
@ -295,7 +296,7 @@ public final class PlaybackStatsListener
@Override
public void onSeekProcessed(EventTime eventTime) {
sessionManager.updateSessions(eventTime);
maybeAddSession(eventTime);
for (String session : playbackStatsTrackers.keySet()) {
if (sessionManager.belongsToSession(eventTime, session)) {
playbackStatsTrackers.get(session).onSeekProcessed(eventTime);
@ -305,7 +306,7 @@ public final class PlaybackStatsListener
@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);
@ -317,7 +318,7 @@ public final class PlaybackStatsListener
public void onPlaybackParametersChanged(
EventTime eventTime, PlaybackParameters playbackParameters) {
playbackSpeed = playbackParameters.speed;
sessionManager.updateSessions(eventTime);
maybeAddSession(eventTime);
for (PlaybackStatsTracker tracker : playbackStatsTrackers.values()) {
tracker.onPlaybackSpeedChanged(eventTime, playbackSpeed);
}
@ -326,7 +327,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 +338,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 +348,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 +363,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 +374,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 +385,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 +395,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 +410,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 +420,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,6 +428,13 @@ 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 {

View file

@ -37,7 +37,7 @@ import java.lang.annotation.RetentionPolicy;
*
* <p>If {@link #hasTimestamp()} returns {@code true}, call {@link #getTimestampSystemTimeUs()} to
* get the system time at which the latest timestamp was sampled and {@link
* #getTimestampPositionFrames()} to get its position in frames. If {@link #isTimestampAdvancing()}
* #getTimestampPositionFrames()} to get its position in frames. If {@link #hasAdvancingTimestamp()}
* returns {@code true}, the caller should assume that the timestamp has been increasing in real
* time since it was sampled. Otherwise, it may be stationary.
*
@ -68,7 +68,7 @@ import java.lang.annotation.RetentionPolicy;
private static final int STATE_ERROR = 4;
/** The polling interval for {@link #STATE_INITIALIZING} and {@link #STATE_TIMESTAMP}. */
private static final int FAST_POLL_INTERVAL_US = 5_000;
private static final int FAST_POLL_INTERVAL_US = 10_000;
/**
* The polling interval for {@link #STATE_TIMESTAMP_ADVANCING} and {@link #STATE_NO_TIMESTAMP}.
*/
@ -110,7 +110,7 @@ import java.lang.annotation.RetentionPolicy;
* timestamp is available via {@link #getTimestampSystemTimeUs()} and {@link
* #getTimestampPositionFrames()}, and the caller should call {@link #acceptTimestamp()} if the
* timestamp was valid, or {@link #rejectTimestamp()} otherwise. The values returned by {@link
* #hasTimestamp()} and {@link #isTimestampAdvancing()} may be updated.
* #hasTimestamp()} and {@link #hasAdvancingTimestamp()} may be updated.
*
* @param systemTimeUs The current system time, in microseconds.
* @return Whether the timestamp was updated.
@ -200,12 +200,12 @@ import java.lang.annotation.RetentionPolicy;
}
/**
* Returns whether the timestamp appears to be advancing. If {@code true}, call {@link
* Returns whether this instance has an advancing timestamp. If {@code true}, call {@link
* #getTimestampSystemTimeUs()} and {@link #getTimestampSystemTimeUs()} to access the timestamp. A
* current position for the track can be extrapolated based on elapsed real time since the system
* time at which the timestamp was sampled.
*/
public boolean isTimestampAdvancing() {
public boolean hasAdvancingTimestamp() {
return state == STATE_TIMESTAMP_ADVANCING;
}

View file

@ -123,6 +123,8 @@ import java.lang.reflect.Method;
* <p>This is a fail safe that should not be required on correctly functioning devices.
*/
private static final long MAX_LATENCY_US = 5 * C.MICROS_PER_SECOND;
/** The duration of time used to smooth over an adjustment between position sampling modes. */
private static final long MODE_SWITCH_SMOOTHING_DURATION_US = C.MICROS_PER_SECOND;
private static final long FORCE_RESET_WORKAROUND_TIMEOUT_MS = 200;
@ -160,6 +162,15 @@ import java.lang.reflect.Method;
private long stopPlaybackHeadPosition;
private long endPlaybackHeadPosition;
// Results from the previous call to getCurrentPositionUs.
private long lastPositionUs;
private long lastSystemTimeUs;
private boolean lastSampleUsedGetTimestampMode;
// Results from the last call to getCurrentPositionUs that used a different sample mode.
private long previousModePositionUs;
private long previousModeSystemTimeUs;
/**
* Creates a new audio track position tracker.
*
@ -206,6 +217,7 @@ import java.lang.reflect.Method;
hasData = false;
stopTimestampUs = C.TIME_UNSET;
forceResetWorkaroundTimeMs = C.TIME_UNSET;
lastLatencySampleTimeUs = 0;
latencyUs = 0;
}
@ -217,18 +229,16 @@ import java.lang.reflect.Method;
// If the device supports it, use the playback timestamp from AudioTrack.getTimestamp.
// Otherwise, derive a smoothed position by sampling the track's frame position.
long systemTimeUs = System.nanoTime() / 1000;
long positionUs;
AudioTimestampPoller audioTimestampPoller = Assertions.checkNotNull(this.audioTimestampPoller);
if (audioTimestampPoller.hasTimestamp()) {
boolean useGetTimestampMode = audioTimestampPoller.hasAdvancingTimestamp();
if (useGetTimestampMode) {
// Calculate the speed-adjusted position using the timestamp (which may be in the future).
long timestampPositionFrames = audioTimestampPoller.getTimestampPositionFrames();
long timestampPositionUs = framesToDurationUs(timestampPositionFrames);
if (!audioTimestampPoller.isTimestampAdvancing()) {
return timestampPositionUs;
}
long elapsedSinceTimestampUs = systemTimeUs - audioTimestampPoller.getTimestampSystemTimeUs();
return timestampPositionUs + elapsedSinceTimestampUs;
positionUs = timestampPositionUs + elapsedSinceTimestampUs;
} else {
long positionUs;
if (playheadOffsetCount == 0) {
// The AudioTrack has started, but we don't have any samples to compute a smoothed position.
positionUs = getPlaybackHeadPositionUs();
@ -239,10 +249,31 @@ import java.lang.reflect.Method;
positionUs = systemTimeUs + smoothedPlayheadOffsetUs;
}
if (!sourceEnded) {
positionUs -= latencyUs;
positionUs = Math.max(0, positionUs - latencyUs);
}
return positionUs;
}
if (lastSampleUsedGetTimestampMode != useGetTimestampMode) {
// We've switched sampling mode.
previousModeSystemTimeUs = lastSystemTimeUs;
previousModePositionUs = lastPositionUs;
}
long elapsedSincePreviousModeUs = systemTimeUs - previousModeSystemTimeUs;
if (elapsedSincePreviousModeUs < MODE_SWITCH_SMOOTHING_DURATION_US) {
// Use a ramp to smooth between the old mode and the new one to avoid introducing a sudden
// jump if the two modes disagree.
long previousModeProjectedPositionUs = previousModePositionUs + elapsedSincePreviousModeUs;
// A ramp consisting of 1000 points distributed over MODE_SWITCH_SMOOTHING_DURATION_US.
long rampPoint = (elapsedSincePreviousModeUs * 1000) / MODE_SWITCH_SMOOTHING_DURATION_US;
positionUs *= rampPoint;
positionUs += (1000 - rampPoint) * previousModeProjectedPositionUs;
positionUs /= 1000;
}
lastSystemTimeUs = systemTimeUs;
lastPositionUs = positionUs;
lastSampleUsedGetTimestampMode = useGetTimestampMode;
return positionUs;
}
/** Starts position tracking. Must be called immediately before {@link AudioTrack#play()}. */
@ -353,7 +384,7 @@ import java.lang.reflect.Method;
}
/**
* Resets the position tracker. Should be called when the audio track previous passed to {@link
* Resets the position tracker. Should be called when the audio track previously passed to {@link
* #setAudioTrack(AudioTrack, int, int, int)} is no longer in use.
*/
public void reset() {
@ -457,6 +488,8 @@ import java.lang.reflect.Method;
playheadOffsetCount = 0;
nextPlayheadOffsetIndex = 0;
lastPlayheadSampleTimeUs = 0;
lastSystemTimeUs = 0;
previousModeSystemTimeUs = 0;
}
/**

View file

@ -120,9 +120,20 @@ public final class DefaultAudioSink implements AudioSink {
/**
* Creates a new default chain of audio processors, with the user-defined {@code
* audioProcessors} applied before silence skipping and playback parameters.
* audioProcessors} applied before silence skipping and speed adjustment processors.
*/
public DefaultAudioProcessorChain(AudioProcessor... audioProcessors) {
this(audioProcessors, new SilenceSkippingAudioProcessor(), new SonicAudioProcessor());
}
/**
* Creates a new default chain of audio processors, with the user-defined {@code
* audioProcessors} applied before silence skipping and speed adjustment processors.
*/
public DefaultAudioProcessorChain(
AudioProcessor[] audioProcessors,
SilenceSkippingAudioProcessor silenceSkippingAudioProcessor,
SonicAudioProcessor sonicAudioProcessor) {
// The passed-in type may be more specialized than AudioProcessor[], so allocate a new array
// rather than using Arrays.copyOf.
this.audioProcessors = new AudioProcessor[audioProcessors.length + 2];
@ -132,8 +143,8 @@ public final class DefaultAudioSink implements AudioSink {
/* dest= */ this.audioProcessors,
/* destPos= */ 0,
/* length= */ audioProcessors.length);
silenceSkippingAudioProcessor = new SilenceSkippingAudioProcessor();
sonicAudioProcessor = new SonicAudioProcessor();
this.silenceSkippingAudioProcessor = silenceSkippingAudioProcessor;
this.sonicAudioProcessor = sonicAudioProcessor;
this.audioProcessors[audioProcessors.length] = silenceSkippingAudioProcessor;
this.audioProcessors[audioProcessors.length + 1] = sonicAudioProcessor;
}

View file

@ -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
@ -30,27 +32,20 @@ import java.nio.ByteBuffer;
public final class SilenceSkippingAudioProcessor extends BaseAudioProcessor {
/**
* The minimum duration of audio that must be below {@link #SILENCE_THRESHOLD_LEVEL} to classify
* that part of audio as silent, in microseconds.
* The default value for {@link #SilenceSkippingAudioProcessor(long, long, short)
* minimumSilenceDurationUs}.
*/
private static final long MINIMUM_SILENCE_DURATION_US = 150_000;
public static final long DEFAULT_MINIMUM_SILENCE_DURATION_US = 150_000;
/**
* The duration of silence by which to extend non-silent sections, in microseconds. The value must
* not exceed {@link #MINIMUM_SILENCE_DURATION_US}.
* The default value for {@link #SilenceSkippingAudioProcessor(long, long, short)
* paddingSilenceUs}.
*/
private static final long PADDING_SILENCE_US = 20_000;
public static final long DEFAULT_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 default value for {@link #SilenceSkippingAudioProcessor(long, long, short)
* silenceThresholdLevel}.
*/
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;
public static final short DEFAULT_SILENCE_THRESHOLD_LEVEL = 1024;
/** Trimming states. */
@Documented
@ -68,8 +63,10 @@ public final class SilenceSkippingAudioProcessor extends BaseAudioProcessor {
/** State when the input is silent. */
private static final int STATE_SILENT = 2;
private final long minimumSilenceDurationUs;
private final long paddingSilenceUs;
private final short silenceThresholdLevel;
private int bytesPerFrame;
private boolean enabled;
/**
@ -91,8 +88,31 @@ public final class SilenceSkippingAudioProcessor extends BaseAudioProcessor {
private boolean hasOutputNoise;
private long skippedFrames;
/** Creates a new silence trimming audio processor. */
/** Creates a new silence skipping audio processor. */
public SilenceSkippingAudioProcessor() {
this(
DEFAULT_MINIMUM_SILENCE_DURATION_US,
DEFAULT_PADDING_SILENCE_US,
DEFAULT_SILENCE_THRESHOLD_LEVEL);
}
/**
* Creates a new silence skipping audio processor.
*
* @param minimumSilenceDurationUs The minimum duration of audio that must be below {@code
* silenceThresholdLevel} to classify that part of audio as silent, in microseconds.
* @param paddingSilenceUs The duration of silence by which to extend non-silent sections, in
* microseconds. The value must not exceed {@code minimumSilenceDurationUs}.
* @param silenceThresholdLevel The absolute level below which an individual PCM sample is
* classified as silent.
*/
public SilenceSkippingAudioProcessor(
long minimumSilenceDurationUs, long paddingSilenceUs, short silenceThresholdLevel) {
Assertions.checkArgument(paddingSilenceUs <= minimumSilenceDurationUs);
this.minimumSilenceDurationUs = minimumSilenceDurationUs;
this.paddingSilenceUs = paddingSilenceUs;
this.silenceThresholdLevel = silenceThresholdLevel;
maybeSilenceBuffer = Util.EMPTY_BYTE_ARRAY;
paddingBuffer = Util.EMPTY_BYTE_ARRAY;
}
@ -166,11 +186,11 @@ public final class SilenceSkippingAudioProcessor extends BaseAudioProcessor {
protected void onFlush() {
if (enabled) {
bytesPerFrame = inputAudioFormat.bytesPerFrame;
int maybeSilenceBufferSize = durationUsToFrames(MINIMUM_SILENCE_DURATION_US) * bytesPerFrame;
int maybeSilenceBufferSize = durationUsToFrames(minimumSilenceDurationUs) * bytesPerFrame;
if (maybeSilenceBuffer.length != maybeSilenceBufferSize) {
maybeSilenceBuffer = new byte[maybeSilenceBufferSize];
}
paddingSize = durationUsToFrames(PADDING_SILENCE_US) * bytesPerFrame;
paddingSize = durationUsToFrames(paddingSilenceUs) * bytesPerFrame;
if (paddingBuffer.length != paddingSize) {
paddingBuffer = new byte[paddingSize];
}
@ -325,9 +345,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)) > silenceThresholdLevel) {
// Round to the start of the frame.
return bytesPerFrame * (i / bytesPerFrame);
}
@ -340,9 +361,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)) > silenceThresholdLevel) {
// Return the start of the next frame.
return bytesPerFrame * (i / bytesPerFrame) + bytesPerFrame;
}

View file

@ -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();
@ -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;
}

View file

@ -155,18 +155,20 @@ import java.nio.ByteBuffer;
@Override
protected void onFlush() {
if (reconfigurationPending) {
// This is the initial flush after reconfiguration. Prepare to trim bytes from the start/end.
// 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 {
// This is a flush during playback (after the initial flush). We assume this was caused by a
// seek to a non-zero position and clear pending start bytes. This assumption may be wrong (we
// may be seeking to zero), but playing data that should have been trimmed shouldn't be
// noticeable after a seek. Ideally we would 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;
}

View file

@ -60,7 +60,7 @@ import com.google.android.exoplayer2.util.Util;
return new XingSeeker(position, mpegAudioHeader.frameSize, durationUs);
}
long dataSize = frame.readUnsignedIntToInt();
long dataSize = frame.readUnsignedInt();
long[] tableOfContents = new long[100];
for (int i = 0; i < 100; i++) {
tableOfContents[i] = frame.readUnsignedByte();

View file

@ -664,9 +664,9 @@ public class FragmentedMp4Extractor implements Extractor {
private static Pair<Integer, DefaultSampleValues> 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,
@ -751,8 +751,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<LeafAtom> leafChildren = traf.leafChildren;
@ -871,13 +872,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;
@ -910,16 +918,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);
@ -937,7 +951,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;
@ -948,20 +962,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
@ -972,9 +986,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) {
@ -984,13 +999,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);
@ -1000,6 +1015,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);
@ -1269,7 +1291,7 @@ public class FragmentedMp4Extractor implements Extractor {
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);
}
@ -1513,10 +1535,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;
}

View file

@ -28,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 {
@ -282,7 +281,6 @@ 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() {}
@ -312,15 +310,8 @@ 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)) {
format = format.copyWithMetadata(new Metadata(mdtaMetadataEntry));
}
}
}

View file

@ -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.
*/
@ -139,8 +135,8 @@ import java.io.IOException;
// 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];
}
@ -186,8 +182,14 @@ import java.io.IOException;
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. */

View file

@ -41,6 +41,7 @@ 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;
@ -59,7 +60,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.
@ -161,9 +162,8 @@ public final class H265Reader implements ElementaryStreamReader {
}
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);
@ -173,9 +173,8 @@ public final class H265Reader implements ElementaryStreamReader {
}
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);
@ -185,9 +184,8 @@ public final class H265Reader implements ElementaryStreamReader {
}
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);
@ -400,17 +398,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 +420,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 +463,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;
}
}
}

View file

@ -460,10 +460,15 @@ public final class TsExtractor implements Extractor {
// See ISO/IEC 13818-1, section 2.4.4.4 for more information on table id assignment.
return;
}
// section_syntax_indicator(1), '0'(1), reserved(2), section_length(12),
// transport_stream_id (16), reserved (2), version_number (5), current_next_indicator (1),
// section_number (8), last_section_number (8)
sectionData.skipBytes(7);
// section_syntax_indicator(1), '0'(1), reserved(2), section_length(4)
int secondHeaderByte = sectionData.readUnsignedByte();
if ((secondHeaderByte & 0x80) == 0) {
// section_syntax_indicator must be 1. See ISO/IEC 13818-1, section 2.4.4.5.
return;
}
// section_length(8), transport_stream_id (16), reserved (2), version_number (5),
// current_next_indicator (1), section_number (8), last_section_number (8)
sectionData.skipBytes(6);
int programCount = sectionData.bytesLeft() / 4;
for (int i = 0; i < programCount; i++) {
@ -535,8 +540,14 @@ public final class TsExtractor implements Extractor {
timestampAdjusters.add(timestampAdjuster);
}
// section_syntax_indicator(1), '0'(1), reserved(2), section_length(12)
sectionData.skipBytes(2);
// section_syntax_indicator(1), '0'(1), reserved(2), section_length(4)
int secondHeaderByte = sectionData.readUnsignedByte();
if ((secondHeaderByte & 0x80) == 0) {
// section_syntax_indicator must be 1. See ISO/IEC 13818-1, section 2.4.4.9.
return;
}
// section_length(8)
sectionData.skipBytes(1);
int programNumber = sectionData.readUnsignedShort();
// Skip 3 bytes (24 bits), including:

View file

@ -573,7 +573,9 @@ public final class MediaCodecInfo {
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

View file

@ -1022,7 +1022,7 @@ public abstract class DownloadService extends Service {
try {
Intent intent = getIntent(context, serviceClass, DownloadService.ACTION_INIT);
context.startService(intent);
} catch (IllegalArgumentException e) {
} catch (IllegalStateException e) {
// 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).");

View file

@ -129,8 +129,9 @@ public final class Requirements implements Parcelable {
}
ConnectivityManager connectivityManager =
(ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo networkInfo = Assertions.checkNotNull(connectivityManager).getActiveNetworkInfo();
(ConnectivityManager)
Assertions.checkNotNull(context.getSystemService(Context.CONNECTIVITY_SERVICE));
@Nullable NetworkInfo networkInfo = connectivityManager.getActiveNetworkInfo();
if (networkInfo == null
|| !networkInfo.isConnected()
|| !isInternetConnectivityValidated(connectivityManager)) {
@ -156,23 +157,27 @@ public final class Requirements implements Parcelable {
}
private boolean isDeviceIdle(Context context) {
PowerManager powerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
PowerManager powerManager =
(PowerManager) Assertions.checkNotNull(context.getSystemService(Context.POWER_SERVICE));
return Util.SDK_INT >= 23
? powerManager.isDeviceIdleMode()
: Util.SDK_INT >= 20 ? !powerManager.isInteractive() : !powerManager.isScreenOn();
}
private static boolean isInternetConnectivityValidated(ConnectivityManager connectivityManager) {
// 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.
// It's possible to check NetworkCapabilities.NET_CAPABILITY_VALIDATED from API level 23, but
// RequirementsWatcher only fires an event to re-check the requirements when NetworkCapabilities
// change from API level 24. We assume that network capability is validated for API level 23 to
// keep in sync.
if (Util.SDK_INT < 24) {
return true;
}
Network activeNetwork = connectivityManager.getActiveNetwork();
@Nullable Network activeNetwork = connectivityManager.getActiveNetwork();
if (activeNetwork == null) {
return false;
}
@Nullable
NetworkCapabilities networkCapabilities =
connectivityManager.getNetworkCapabilities(activeNetwork);
return networkCapabilities != null

View file

@ -150,6 +150,23 @@ public final class RequirementsWatcher {
}
}
/**
* Re-checks the requirements if there are network requirements that are currently not met.
*
* <p>When we receive an event that implies newly established network connectivity, we re-check
* the requirements by calling {@link #checkRequirements()}. This check sometimes sees that there
* is still no active network, meaning that any network requirements will remain not met. By
* calling this method when we receive other events that imply continued network connectivity, we
* can detect that the requirements are met once an active network does exist.
*/
private void recheckNotMetNetworkRequirements() {
if ((notMetRequirements & (Requirements.NETWORK | Requirements.NETWORK_UNMETERED)) == 0) {
// No unmet network requirements to recheck.
return;
}
checkRequirements();
}
private class DeviceStatusChangeReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
@ -161,17 +178,25 @@ public final class RequirementsWatcher {
@RequiresApi(24)
private final class NetworkCallback extends ConnectivityManager.NetworkCallback {
boolean receivedCapabilitiesChange;
boolean networkValidated;
private boolean receivedCapabilitiesChange;
private boolean networkValidated;
@Override
public void onAvailable(Network network) {
onNetworkCallback();
postCheckRequirements();
}
@Override
public void onLost(Network network) {
onNetworkCallback();
postCheckRequirements();
}
@Override
public void onBlockedStatusChanged(Network network, boolean blocked) {
if (!blocked) {
postRecheckNotMetNetworkRequirements();
}
}
@Override
@ -181,11 +206,13 @@ public final class RequirementsWatcher {
if (!receivedCapabilitiesChange || this.networkValidated != networkValidated) {
receivedCapabilitiesChange = true;
this.networkValidated = networkValidated;
onNetworkCallback();
postCheckRequirements();
} else if (networkValidated) {
postRecheckNotMetNetworkRequirements();
}
}
private void onNetworkCallback() {
private void postCheckRequirements() {
handler.post(
() -> {
if (networkCallback != null) {
@ -193,5 +220,14 @@ public final class RequirementsWatcher {
}
});
}
private void postRecheckNotMetNetworkRequirements() {
handler.post(
() -> {
if (networkCallback != null) {
recheckNotMetNetworkRequirements();
}
});
}
}
}

View file

@ -293,7 +293,7 @@ public final class MaskingMediaSource extends CompositeMediaSource<Void> {
}
/** Dummy placeholder timeline with one dynamic window with a period of indeterminate duration. */
private static final class DummyTimeline extends Timeline {
public static final class DummyTimeline extends Timeline {
@Nullable private final Object tag;

View file

@ -679,7 +679,8 @@ 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);
trackOutput.setUpstreamFormatChangeListener(this);
@NullableType
TrackId[] sampleQueueTrackIds = Arrays.copyOf(this.sampleQueueTrackIds, trackCount + 1);
@ -729,6 +730,11 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
trackFormat = trackFormat.copyWithBitrate(icyHeaders.bitrate);
}
}
if (trackFormat.drmInitData != null) {
trackFormat =
trackFormat.copyWithExoMediaCryptoType(
drmSessionManager.getExoMediaCryptoType(trackFormat.drmInitData));
}
trackArray[i] = new TrackGroup(trackFormat);
}
isLive = length == C.LENGTH_UNSET && seekMap.getDurationUs() == C.TIME_UNSET;

View file

@ -174,7 +174,10 @@ public final class ProgressiveMediaSource extends BaseMediaSource
@Override
public Factory setDrmSessionManager(DrmSessionManager<?> drmSessionManager) {
Assertions.checkState(!isCreateCalled);
this.drmSessionManager = drmSessionManager;
this.drmSessionManager =
drmSessionManager != null
? drmSessionManager
: DrmSessionManager.getDummyDrmSessionManager();
return this;
}

View file

@ -55,6 +55,7 @@ public class SampleQueue implements TrackOutput {
private final SampleExtrasHolder extrasHolder;
private final DrmSessionManager<?> drmSessionManager;
private UpstreamFormatChangedListener upstreamFormatChangeListener;
private final Looper playbackLooper;
@Nullable private Format downstreamFormat;
@Nullable private DrmSession<?> currentDrmSession;
@ -91,11 +92,13 @@ public class SampleQueue implements TrackOutput {
* 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}.
*/
public SampleQueue(Allocator allocator, DrmSessionManager<?> drmSessionManager) {
public SampleQueue(Allocator allocator, Looper playbackLooper, DrmSessionManager<?> drmSessionManager) {
sampleDataQueue = new SampleDataQueue(allocator);
this.playbackLooper = playbackLooper;
this.drmSessionManager = drmSessionManager;
extrasHolder = new SampleExtrasHolder();
capacity = SAMPLE_CAPACITY_INCREMENT;
@ -789,8 +792,7 @@ public class SampleQueue implements TrackOutput {
}
// 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());
@Nullable DrmSession previousSession = currentDrmSession;
currentDrmSession =
newDrmInitData != null
? drmSessionManager.acquireSession(playbackLooper, newDrmInitData)

View file

@ -33,6 +33,42 @@ 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;
private static final int CHANNEL_COUNT = 2;
@ -54,6 +90,7 @@ public final class SilenceMediaSource extends BaseMediaSource {
new byte[Util.getPcmFrameSize(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 +98,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

View file

@ -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.
*
* <p>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) {
@ -357,6 +360,18 @@ public final class AdPlaybackState {
return index < adGroupTimesUs.length ? index : C.INDEX_UNSET;
}
/** Returns whether the specified ad has been marked as in {@link #AD_STATE_ERROR}. */
public boolean isAdInErrorState(int adGroupIndex, int adIndexInAdGroup) {
if (adGroupIndex >= adGroups.length) {
return false;
}
AdGroup adGroup = adGroups[adGroupIndex];
if (adGroup.count == C.LENGTH_UNSET || adIndexInAdGroup >= adGroup.count) {
return false;
}
return adGroup.states[adIndexInAdGroup] == AdPlaybackState.AD_STATE_ERROR;
}
/**
* Returns an instance with the number of ads in {@code adGroupIndex} resolved to {@code adCount}.
* The ad count must be greater than zero.
@ -425,7 +440,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 +489,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;
}

View file

@ -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,10 +45,9 @@ 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
@ -128,15 +128,13 @@ public final class AdsMediaSource extends CompositeMediaSource<MediaPeriodId> {
private final AdsLoader adsLoader;
private final AdsLoader.AdViewProvider adViewProvider;
private final Handler mainHandler;
private final Map<MediaSource, List<MaskingMediaPeriod>> 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
@ -178,10 +176,8 @@ public final class AdsMediaSource extends CompositeMediaSource<MediaPeriodId> {
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());
}
@ -208,36 +204,21 @@ public final class AdsMediaSource extends CompositeMediaSource<MediaPeriodId> {
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<MaskingMediaPeriod> 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);
@ -249,12 +230,18 @@ public final class AdsMediaSource extends CompositeMediaSource<MediaPeriodId> {
@Override
public void releasePeriod(MediaPeriod mediaPeriod) {
MaskingMediaPeriod maskingMediaPeriod = (MaskingMediaPeriod) mediaPeriod;
List<MaskingMediaPeriod> 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
@ -262,11 +249,9 @@ public final class AdsMediaSource extends CompositeMediaSource<MediaPeriodId> {
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);
}
@ -276,14 +261,17 @@ public final class AdsMediaSource extends CompositeMediaSource<MediaPeriodId> {
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.
@ -294,42 +282,17 @@ public final class AdsMediaSource extends CompositeMediaSource<MediaPeriodId> {
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<MaskingMediaPeriod> 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
@ -338,19 +301,16 @@ public final class AdsMediaSource extends CompositeMediaSource<MediaPeriodId> {
}
}
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. */
@ -399,7 +359,7 @@ public final class AdsMediaSource extends CompositeMediaSource<MediaPeriodId> {
dataSpec.uri,
/* responseHeaders= */ Collections.emptyMap(),
C.DATA_TYPE_AD,
C.TRACK_TYPE_UNKNOWN,
/* elapsedRealtimeMs= */ SystemClock.elapsedRealtime(),
/* loadDurationMs= */ 0,
/* bytesLoaded= */ 0,
error,
@ -436,4 +396,61 @@ public final class AdsMediaSource extends CompositeMediaSource<MediaPeriodId> {
() -> adsLoader.handlePrepareError(adGroupIndex, adIndexInAdGroup, exception));
}
}
private final class AdMediaSourceHolder {
private final MediaSource adMediaSource;
private final List<MaskingMediaPeriod> activeMediaPeriods;
@MonotonicNonNull private 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();
}
}
}

View file

@ -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;
@ -130,13 +131,19 @@ public class ChunkSampleStream<T extends ChunkSource> implements SampleStream, S
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);
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());
embeddedSampleQueues[i] = sampleQueue;
sampleQueues[i + 1] = sampleQueue;
trackTypes[i + 1] = embeddedTrackTypes[i];

View file

@ -1990,6 +1990,10 @@ public class DefaultTrackSelector extends MappingTrackSelector {
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))
@ -2013,9 +2017,13 @@ public class DefaultTrackSelector extends MappingTrackSelector {
params.viewportWidth, params.viewportHeight, params.viewportOrientationMayChange);
@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)

View file

@ -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,10 +203,11 @@ 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 and 5G to prevent using the slower fallback.
// Assume default Wifi and 4G bitrate for Ethernet and 5G, respectively, 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]]);
result.append(C.NETWORK_TYPE_5G, DEFAULT_INITIAL_BITRATE_ESTIMATES_4G[groupIndices[3]]);
return result;
}
@ -488,244 +489,245 @@ public final class DefaultBandwidthMeter implements BandwidthMeter, TransferList
private static Map<String, int[]> createInitialBitrateCountryGroupAssignment() {
HashMap<String, int[]> 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);
}
}

View file

@ -32,6 +32,7 @@ import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* Manages the background loading of {@link Loadable}s.
@ -56,6 +57,21 @@ public final class Loader implements LoaderErrorThrower {
/**
* Cancels the load.
*
* <p>Loadable implementations should ensure that a currently executing {@link #load()} call
* will exit reasonably quickly after this method is called. The {@link #load()} call may exit
* either by returning or by throwing an {@link IOException}.
*
* <p>If there is a currently executing {@link #load()} call, then the thread on which that call
* is being made will be interrupted immediately after the call to this method. Hence
* implementations do not need to (and should not attempt to) interrupt the loading thread
* themselves.
*
* <p>Although the loading thread will be interrupted, Loadable implementations should not use
* the interrupted status of the loading thread in {@link #load()} to determine whether the load
* has been canceled. This approach is not robust [Internal ref: b/79223737]. Instead,
* implementations should use their own flag to signal cancelation (for example, using {@link
* AtomicBoolean}).
*/
void cancelLoad();
@ -309,10 +325,9 @@ public final class Loader implements LoaderErrorThrower {
private static final String TAG = "LoadTask";
private static final int MSG_START = 0;
private static final int MSG_CANCEL = 1;
private static final int MSG_END_OF_SOURCE = 2;
private static final int MSG_IO_EXCEPTION = 3;
private static final int MSG_FATAL_ERROR = 4;
private static final int MSG_FINISH = 1;
private static final int MSG_IO_EXCEPTION = 2;
private static final int MSG_FATAL_ERROR = 3;
public final int defaultMinRetryCount;
@ -323,8 +338,8 @@ public final class Loader implements LoaderErrorThrower {
@Nullable private IOException currentError;
private int errorCount;
@Nullable private volatile Thread executorThread;
private volatile boolean canceled;
@Nullable private Thread executorThread;
private boolean canceled;
private volatile boolean released;
public LoadTask(Looper looper, T loadable, Loader.Callback<T> callback,
@ -356,16 +371,21 @@ public final class Loader implements LoaderErrorThrower {
this.released = released;
currentError = null;
if (hasMessages(MSG_START)) {
// The task has not been given to the executor yet.
canceled = true;
removeMessages(MSG_START);
if (!released) {
sendEmptyMessage(MSG_CANCEL);
sendEmptyMessage(MSG_FINISH);
}
} else {
canceled = true;
loadable.cancelLoad();
Thread executorThread = this.executorThread;
if (executorThread != null) {
executorThread.interrupt();
// The task has been given to the executor.
synchronized (this) {
canceled = true;
loadable.cancelLoad();
@Nullable Thread executorThread = this.executorThread;
if (executorThread != null) {
executorThread.interrupt();
}
}
}
if (released) {
@ -384,8 +404,12 @@ public final class Loader implements LoaderErrorThrower {
@Override
public void run() {
try {
executorThread = Thread.currentThread();
if (!canceled) {
boolean shouldLoad;
synchronized (this) {
shouldLoad = !canceled;
executorThread = Thread.currentThread();
}
if (shouldLoad) {
TraceUtil.beginSection("load:" + loadable.getClass().getSimpleName());
try {
loadable.load();
@ -393,8 +417,13 @@ public final class Loader implements LoaderErrorThrower {
TraceUtil.endSection();
}
}
synchronized (this) {
executorThread = null;
// Clear the interrupted flag if set, to avoid it leaking into a subsequent task.
Thread.interrupted();
}
if (!released) {
sendEmptyMessage(MSG_END_OF_SOURCE);
sendEmptyMessage(MSG_FINISH);
}
} catch (IOException e) {
if (!released) {
@ -404,7 +433,7 @@ public final class Loader implements LoaderErrorThrower {
// The load was canceled.
Assertions.checkState(canceled);
if (!released) {
sendEmptyMessage(MSG_END_OF_SOURCE);
sendEmptyMessage(MSG_FINISH);
}
} catch (Exception e) {
// This should never happen, but handle it anyway.
@ -453,10 +482,7 @@ public final class Loader implements LoaderErrorThrower {
return;
}
switch (msg.what) {
case MSG_CANCEL:
callback.onLoadCanceled(loadable, nowMs, durationMs, false);
break;
case MSG_END_OF_SOURCE:
case MSG_FINISH:
try {
callback.onLoadCompleted(loadable, nowMs, durationMs);
} catch (RuntimeException e) {

View file

@ -109,7 +109,6 @@ public final class CacheUtil {
*
* @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 progressListener A listener to receive progress updates, or {@code null}.
* @param isCanceled An optional flag that will interrupt caching if set to true.
@ -120,7 +119,6 @@ public final class CacheUtil {
public static void cache(
DataSpec dataSpec,
Cache cache,
@Nullable CacheKeyFactory cacheKeyFactory,
DataSource upstream,
@Nullable ProgressListener progressListener,
@Nullable AtomicBoolean isCanceled)
@ -128,7 +126,7 @@ public final class CacheUtil {
cache(
dataSpec,
cache,
cacheKeyFactory,
/* cacheKeyFactory= */ null,
new CacheDataSource(cache, upstream),
new byte[DEFAULT_BUFFER_SIZE_BYTES],
/* priorityTaskManager= */ null,
@ -139,14 +137,14 @@ public final class CacheUtil {
}
/**
* 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.
*
* <p>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.
* <p>If a {@link PriorityTaskManager} is provided, 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.
*
* <p>This method may be slow and shouldn't normally be called on the main thread.
*

View file

@ -15,8 +15,10 @@
*/
package com.google.android.exoplayer2.upstream.cache;
import static com.google.android.exoplayer2.util.Assertions.checkArgument;
import static com.google.android.exoplayer2.util.Assertions.checkState;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Log;
import java.io.File;
import java.util.TreeSet;
@ -115,12 +117,18 @@ import java.util.TreeSet;
* @return the length of the cached or not cached data block length.
*/
public long getCachedBytesLength(long position, long length) {
checkArgument(position >= 0);
checkArgument(length >= 0);
SimpleCacheSpan span = getSpan(position);
if (span.isHoleSpan()) {
// We don't have a span covering the start of the queried region.
return -Math.min(span.isOpenEnded() ? Long.MAX_VALUE : span.length, length);
}
long queryEndPosition = position + length;
if (queryEndPosition < 0) {
// The calculation rolled over (length is probably Long.MAX_VALUE).
queryEndPosition = Long.MAX_VALUE;
}
long currentEndPosition = span.position + span.length;
if (currentEndPosition < queryEndPosition) {
for (SimpleCacheSpan next : cachedSpans.tailSet(span, false)) {
@ -151,7 +159,7 @@ import java.util.TreeSet;
*/
public SimpleCacheSpan setLastTouchTimestamp(
SimpleCacheSpan cacheSpan, long lastTouchTimestamp, boolean updateFile) {
Assertions.checkState(cachedSpans.remove(cacheSpan));
checkState(cachedSpans.remove(cacheSpan));
File file = cacheSpan.file;
if (updateFile) {
File directory = file.getParentFile();

View file

@ -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();

View file

@ -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}:
*
* <ul>
* <li>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.
* <li>Support for injecting a custom {@link Clock}.
* <li>The ability to query the variable's current state, by calling {@link #isOpen()}.
* <li>{@link #open()} and {@link #close()} return whether they changed the variable's state.
* </ul>
*/
public final 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;
}

View file

@ -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() {

View file

@ -726,6 +726,24 @@ public final class Util {
return C.INDEX_UNSET;
}
/**
* Returns the index of the first occurrence of {@code value} in {@code array}, or {@link
* C#INDEX_UNSET} if {@code value} is not contained in {@code array}.
*
* @param array The array to search.
* @param value The value to search for.
* @return The index of the first occurrence of value in {@code array}, or {@link C#INDEX_UNSET}
* if {@code value} is not contained in {@code array}.
*/
public static int linearSearch(long[] array, long value) {
for (int i = 0; i < array.length; i++) {
if (array[i] == value) {
return i;
}
}
return C.INDEX_UNSET;
}
/**
* Returns the index of the largest element in {@code array} that is less than (or optionally
* equal to) a specified {@code value}.

View file

@ -81,7 +81,7 @@ track 0:
flags = 1
data = length 520, hash FEE56928
sample 13:
time = 520000
time = 519999
flags = 1
data = length 599, hash 41F496C5
sample 14:

View file

@ -57,7 +57,7 @@ track 0:
flags = 1
data = length 520, hash FEE56928
sample 7:
time = 520000
time = 519999
flags = 1
data = length 599, hash 41F496C5
sample 8:

View file

@ -33,7 +33,7 @@ track 0:
flags = 1
data = length 520, hash FEE56928
sample 1:
time = 520000
time = 519999
flags = 1
data = length 599, hash 41F496C5
sample 2:

View file

@ -107,7 +107,7 @@ track 0:
crypto mode = 1
encryption key = length 16, hash 9FDDEA52
sample 13:
time = 520000
time = 519999
flags = 1073741825
data = length 616, hash 3F657E23
crypto mode = 1

View file

@ -71,7 +71,7 @@ track 0:
crypto mode = 1
encryption key = length 16, hash 9FDDEA52
sample 7:
time = 520000
time = 519999
flags = 1073741825
data = length 616, hash 3F657E23
crypto mode = 1

View file

@ -35,7 +35,7 @@ track 0:
crypto mode = 1
encryption key = length 16, hash 9FDDEA52
sample 1:
time = 520000
time = 519999
flags = 1073741825
data = length 616, hash 3F657E23
crypto mode = 1

View file

@ -0,0 +1,61 @@
seekMap:
isSeekable = true
duration = 526000
getPosition(0) = [[timeUs=0, position=1161]]
numberOfTracks = 1
track 0:
format:
bitrate = -1
id = 1
containerMimeType = null
sampleMimeType = video/avc
maxInputSize = 34686
width = 1280
height = 720
frameRate = 13.307984
rotationDegrees = 0
pixelWidthHeightRatio = 1.0
channelCount = -1
sampleRate = -1
pcmEncoding = -1
encoderDelay = 0
encoderPadding = 0
subsampleOffsetUs = 9223372036854775807
selectionFlags = 0
language = null
drmInitData = -
metadata = entries=[mdta: key=com.android.capture.fps]
initializationData:
data = length 22, hash 4CF81805
data = length 9, hash FBAFBA1C
total output bytes = 42320
sample count = 7
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

View file

@ -0,0 +1,61 @@
seekMap:
isSeekable = true
duration = 526000
getPosition(0) = [[timeUs=0, position=1161]]
numberOfTracks = 1
track 0:
format:
bitrate = -1
id = 1
containerMimeType = null
sampleMimeType = video/avc
maxInputSize = 34686
width = 1280
height = 720
frameRate = 13.307984
rotationDegrees = 0
pixelWidthHeightRatio = 1.0
channelCount = -1
sampleRate = -1
pcmEncoding = -1
encoderDelay = 0
encoderPadding = 0
subsampleOffsetUs = 9223372036854775807
selectionFlags = 0
language = null
drmInitData = -
metadata = entries=[mdta: key=com.android.capture.fps]
initializationData:
data = length 22, hash 4CF81805
data = length 9, hash FBAFBA1C
total output bytes = 42320
sample count = 7
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

View file

@ -0,0 +1,61 @@
seekMap:
isSeekable = true
duration = 526000
getPosition(0) = [[timeUs=0, position=1161]]
numberOfTracks = 1
track 0:
format:
bitrate = -1
id = 1
containerMimeType = null
sampleMimeType = video/avc
maxInputSize = 34686
width = 1280
height = 720
frameRate = 13.307984
rotationDegrees = 0
pixelWidthHeightRatio = 1.0
channelCount = -1
sampleRate = -1
pcmEncoding = -1
encoderDelay = 0
encoderPadding = 0
subsampleOffsetUs = 9223372036854775807
selectionFlags = 0
language = null
drmInitData = -
metadata = entries=[mdta: key=com.android.capture.fps]
initializationData:
data = length 22, hash 4CF81805
data = length 9, hash FBAFBA1C
total output bytes = 42320
sample count = 7
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

View file

@ -0,0 +1,61 @@
seekMap:
isSeekable = true
duration = 526000
getPosition(0) = [[timeUs=0, position=1161]]
numberOfTracks = 1
track 0:
format:
bitrate = -1
id = 1
containerMimeType = null
sampleMimeType = video/avc
maxInputSize = 34686
width = 1280
height = 720
frameRate = 13.307984
rotationDegrees = 0
pixelWidthHeightRatio = 1.0
channelCount = -1
sampleRate = -1
pcmEncoding = -1
encoderDelay = 0
encoderPadding = 0
subsampleOffsetUs = 9223372036854775807
selectionFlags = 0
language = null
drmInitData = -
metadata = entries=[mdta: key=com.android.capture.fps]
initializationData:
data = length 22, hash 4CF81805
data = length 9, hash FBAFBA1C
total output bytes = 42320
sample count = 7
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

View file

@ -31,123 +31,123 @@ track 0:
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:
@ -181,183 +181,183 @@ track 1:
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

View file

@ -31,123 +31,123 @@ track 0:
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:
@ -181,183 +181,183 @@ track 1:
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

View file

@ -31,123 +31,123 @@ track 0:
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:
@ -177,127 +177,127 @@ track 1:
total output bytes = 13359
sample count = 31
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

View file

@ -31,123 +31,123 @@ track 0:
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:
@ -177,67 +177,67 @@ track 1:
total output bytes = 6804
sample count = 16
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

View file

@ -31,123 +31,123 @@ track 0:
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:
@ -177,7 +177,7 @@ track 1:
total output bytes = 10
sample count = 1
sample 0:
time = 1044000
time = 1044897
flags = 1
data = length 10, hash A453EEBE
tracksEnded = true

View file

@ -31,123 +31,123 @@ track 0:
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:
@ -181,183 +181,183 @@ track 1:
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:

View file

@ -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<String> sessionId1 = ArgumentCaptor.forClass(String.class);
ArgumentCaptor<String> sessionId2 = ArgumentCaptor.forClass(String.class);
@ -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);
@ -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);
}
@ -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());
}
@ -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());
@ -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(

View file

@ -16,11 +16,20 @@
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;
@ -28,7 +37,7 @@ import org.junit.runner.RunWith;
@RunWith(AndroidJUnit4.class)
public final class PlaybackStatsListenerTest {
private static final AnalyticsListener.EventTime TEST_EVENT_TIME =
private static final AnalyticsListener.EventTime EMPTY_TIMELINE_EVENT_TIME =
new AnalyticsListener.EventTime(
/* realtimeMs= */ 500,
Timeline.EMPTY,
@ -37,6 +46,41 @@ public final class PlaybackStatsListenerTest {
/* 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 stateChangeEvent_toNonIdle_createsInitialPlaybackStats() {
PlaybackStatsListener playbackStatsListener =
new PlaybackStatsListener(/* keepHistory= */ true, /* callback= */ null);
playbackStatsListener.onPlayerStateChanged(
EMPTY_TIMELINE_EVENT_TIME, /* playWhenReady= */ false, 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_DYNAMIC);
assertThat(playbackStatsListener.getPlaybackStats()).isNotNull();
}
@Test
public void playback_withKeepHistory_updatesStats() {
@ -71,4 +115,72 @@ public final class PlaybackStatsListenerTest {
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.onPlayerStateChanged(
TEST_EVENT_TIME, /* playWhenReady= */ false, Player.STATE_BUFFERING);
verify(callback, never()).onPlaybackStatsReady(any(), any());
playbackStatsListener.onTimelineChanged(
EMPTY_TIMELINE_EVENT_TIME, Player.TIMELINE_CHANGE_REASON_DYNAMIC);
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.onPlayerStateChanged(
eventTimeWindow0, /* playWhenReady= */ false, Player.STATE_BUFFERING);
playbackStatsListener.onPlayerStateChanged(
eventTimeWindow1, /* playWhenReady= */ false, 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.onPlayerStateChanged(
TEST_EVENT_TIME, /* playWhenReady= */ false, 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_DYNAMIC);
verify(callback).onPlaybackStatsReady(any(), any());
}
}

View file

@ -204,7 +204,34 @@ public final class SilenceSkippingAudioProcessorTest {
}
@Test
public void testSkipThenFlush_resetsSkippedFrameCount() throws Exception {
public void customPaddingValue_hasCorrectOutputAndSkippedFrameCounts() throws Exception {
// Given a signal that alternates between silence and noise.
InputBufferProvider inputBufferProvider =
getInputBufferProviderForAlternatingSilenceAndNoise(
TEST_SIGNAL_SILENCE_DURATION_MS,
TEST_SIGNAL_NOISE_DURATION_MS,
TEST_SIGNAL_FRAME_COUNT);
// When processing the entire signal with a larger than normal padding silence.
SilenceSkippingAudioProcessor silenceSkippingAudioProcessor =
new SilenceSkippingAudioProcessor(
SilenceSkippingAudioProcessor.DEFAULT_MINIMUM_SILENCE_DURATION_US,
/* paddingSilenceUs= */ 21_000,
SilenceSkippingAudioProcessor.DEFAULT_SILENCE_THRESHOLD_LEVEL);
silenceSkippingAudioProcessor.setEnabled(true);
silenceSkippingAudioProcessor.configure(AUDIO_FORMAT);
silenceSkippingAudioProcessor.flush();
assertThat(silenceSkippingAudioProcessor.isActive()).isTrue();
long totalOutputFrames =
process(silenceSkippingAudioProcessor, inputBufferProvider, /* inputBufferSize= */ 120);
// The right number of frames are skipped/output.
assertThat(totalOutputFrames).isEqualTo(58379);
assertThat(silenceSkippingAudioProcessor.getSkippedFrames()).isEqualTo(41621);
}
@Test
public void skipThenFlush_resetsSkippedFrameCount() throws Exception {
// Given a signal that alternates between silence and noise.
InputBufferProvider inputBufferProvider =
getInputBufferProviderForAlternatingSilenceAndNoise(

View file

@ -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);
}
}

View file

@ -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;
}
}

View file

@ -42,4 +42,9 @@ public final class Mp4ExtractorTest {
public void testMp4SampleWithAc4Track() throws Exception {
ExtractorAsserts.assertBehavior(Mp4Extractor::new, "mp4/sample_ac4.mp4");
}
@Test
public void testMp4SampleWithSlowMotionMetadata() throws Exception {
ExtractorAsserts.assertBehavior(Mp4Extractor::new, "mp4/sample_android_slow_motion.mp4");
}
}

View file

@ -26,6 +26,7 @@ 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;
@ -40,6 +41,7 @@ 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.ParsableByteArray;
import java.io.IOException;
import java.util.Arrays;
@ -143,7 +145,10 @@ public final class SampleQueueTest {
mockDrmSession = (DrmSession<ExoMediaCrypto>) Mockito.mock(DrmSession.class);
when(mockDrmSessionManager.acquireSession(ArgumentMatchers.any(), ArgumentMatchers.any()))
.thenReturn(mockDrmSession);
sampleQueue = new SampleQueue(allocator, mockDrmSessionManager);
sampleQueue = new SampleQueue(
allocator,
/* playbackLooper= */ Assertions.checkNotNull(Looper.myLooper()),
mockDrmSessionManager);
formatHolder = new FormatHolder();
inputBuffer = new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_NORMAL);
}
@ -360,7 +365,10 @@ public final class SampleQueueTest {
public void testIsReadyReturnsTrueForClearSampleAndPlayClearSamplesWithoutKeysIsTrue() {
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);
writeTestDataWithEncryptedSections();
assertThat(sampleQueue.isReady(/* loadingFinished= */ false)).isTrue();
}
@ -542,7 +550,10 @@ public final class SampleQueueTest {
public void testAllowPlayClearSamplesWithoutKeysReadsClearSamples() {
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);
when(mockDrmSession.getState()).thenReturn(DrmSession.STATE_OPENED);
writeTestDataWithEncryptedSections();
@ -931,7 +942,10 @@ public final class SampleQueueTest {
public void testAdjustUpstreamFormat() {
String label = "label";
sampleQueue =
new SampleQueue(allocator, mockDrmSessionManager) {
new SampleQueue(
allocator,
/* playbackLooper= */ Assertions.checkNotNull(Looper.myLooper()),
mockDrmSessionManager) {
@Override
public Format getAdjustedUpstreamFormat(Format format) {
return super.getAdjustedUpstreamFormat(format.copyWithLabel(label));
@ -947,7 +961,10 @@ public final class SampleQueueTest {
public void testInvalidateUpstreamFormatAdjustment() {
AtomicReference<String> label = new AtomicReference<>("label1");
sampleQueue =
new SampleQueue(allocator, mockDrmSessionManager) {
new SampleQueue(
allocator,
/* playbackLooper= */ Assertions.checkNotNull(Looper.myLooper()),
mockDrmSessionManager) {
@Override
public Format getAdjustedUpstreamFormat(Format format) {
return super.getAdjustedUpstreamFormat(format.copyWithLabel(label.get()));

View file

@ -64,7 +64,9 @@ public final class AdPlaybackStateTest {
assertThat(state.adGroups[0].uris[0]).isNull();
assertThat(state.adGroups[0].states[0]).isEqualTo(AdPlaybackState.AD_STATE_ERROR);
assertThat(state.isAdInErrorState(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0)).isTrue();
assertThat(state.adGroups[0].states[1]).isEqualTo(AdPlaybackState.AD_STATE_UNAVAILABLE);
assertThat(state.isAdInErrorState(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 1)).isFalse();
}
@Test

View file

@ -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<EventListener> 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, null);
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, null);
adsMediaSource.createPeriod(
new MediaPeriodId(
CONTENT_PERIOD_UID,
/* adGroupIndex= */ 0,
/* adIndexInAdGroup= */ 0,
/* windowSequenceNumber= */ 0),
mock(Allocator.class),
/* startPositionUs= */ 0);
prerollAdMediaSource.setNewSourceInfo(PREROLL_AD_TIMELINE, null);
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, null);
adsMediaSource.createPeriod(
new MediaPeriodId(
CONTENT_PERIOD_UID,
/* adGroupIndex= */ 0,
/* adIndexInAdGroup= */ 0,
/* windowSequenceNumber= */ 0),
mock(Allocator.class),
/* startPositionUs= */ 0);
prerollAdMediaSource.setNewSourceInfo(PREROLL_AD_TIMELINE, null);
shadowOf(Looper.getMainLooper()).idle();
prerollAdMediaSource.assertMediaPeriodCreated(
new MediaPeriodId(PREROLL_AD_PERIOD_UID, /* windowSequenceNumber= */ 0));
}
@Test
public void createPeriod_createsChildContentMediaPeriod() {
contentMediaSource.setNewSourceInfo(CONTENT_TIMELINE, null);
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, null);
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, null);
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();
}
}

View file

@ -365,7 +365,6 @@ public final class CacheDataSourceTest {
CacheUtil.cache(
unboundedDataSpec,
cache,
/* cacheKeyFactory= */ null,
upstream2,
/* progressListener= */ null,
/* isCanceled= */ null);
@ -414,7 +413,6 @@ public final class CacheDataSourceTest {
CacheUtil.cache(
unboundedDataSpec,
cache,
/* cacheKeyFactory= */ null,
upstream2,
/* progressListener= */ null,
/* isCanceled= */ null);
@ -438,7 +436,6 @@ public final class CacheDataSourceTest {
CacheUtil.cache(
dataSpec,
cache,
/* cacheKeyFactory= */ null,
upstream,
/* progressListener= */ null,
/* isCanceled= */ null);
@ -474,7 +471,6 @@ public final class CacheDataSourceTest {
CacheUtil.cache(
dataSpec,
cache,
/* cacheKeyFactory= */ null,
upstream,
/* progressListener= */ null,
/* isCanceled= */ null);

View file

@ -207,7 +207,6 @@ public final class CacheUtilTest {
CacheUtil.cache(
new DataSpec(Uri.parse("test_data")),
cache,
/* cacheKeyFactory= */ null,
dataSource,
counters,
/* isCanceled= */ null);
@ -224,8 +223,7 @@ public final class CacheUtilTest {
Uri testUri = Uri.parse("test_data");
DataSpec dataSpec = new DataSpec(testUri, 10, 20, null);
CachingCounters counters = new CachingCounters();
CacheUtil.cache(
dataSpec, cache, /* cacheKeyFactory= */ null, dataSource, counters, /* isCanceled= */ null);
CacheUtil.cache(dataSpec, cache, dataSource, counters, /* isCanceled= */ null);
counters.assertValues(0, 20, 20);
counters.reset();
@ -233,7 +231,6 @@ public final class CacheUtilTest {
CacheUtil.cache(
new DataSpec(testUri),
cache,
/* cacheKeyFactory= */ null,
dataSource,
counters,
/* isCanceled= */ null);
@ -251,8 +248,7 @@ 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(dataSpec, cache, dataSource, counters, /* isCanceled= */ null);
counters.assertValues(0, 100, 100);
assertCachedData(cache, fakeDataSet);
@ -268,8 +264,7 @@ public final class CacheUtilTest {
Uri testUri = Uri.parse("test_data");
DataSpec dataSpec = new DataSpec(testUri, 10, 20, null);
CachingCounters counters = new CachingCounters();
CacheUtil.cache(
dataSpec, cache, /* cacheKeyFactory= */ null, dataSource, counters, /* isCanceled= */ null);
CacheUtil.cache(dataSpec, cache, dataSource, counters, /* isCanceled= */ null);
counters.assertValues(0, 20, 20);
counters.reset();
@ -277,7 +272,6 @@ public final class CacheUtilTest {
CacheUtil.cache(
new DataSpec(testUri),
cache,
/* cacheKeyFactory= */ null,
dataSource,
counters,
/* isCanceled= */ null);
@ -294,8 +288,7 @@ public final class CacheUtilTest {
Uri testUri = Uri.parse("test_data");
DataSpec dataSpec = new DataSpec(testUri, 0, 1000, null);
CachingCounters counters = new CachingCounters();
CacheUtil.cache(
dataSpec, cache, /* cacheKeyFactory= */ null, dataSource, counters, /* isCanceled= */ null);
CacheUtil.cache(dataSpec, cache, dataSource, counters, /* isCanceled= */ null);
counters.assertValues(0, 100, 1000);
assertCachedData(cache, fakeDataSet);
@ -344,7 +337,6 @@ public final class CacheUtilTest {
CacheUtil.cache(
new DataSpec(Uri.parse("test_data")),
cache,
/* cacheKeyFactory= */ null,
dataSource,
counters,
/* isCanceled= */ null);

View file

@ -284,38 +284,93 @@ public class SimpleCacheTest {
}
@Test
public void testGetCachedLength() throws Exception {
public void getCachedLength_noCachedContent_returnsNegativeMaxHoleLength() {
SimpleCache simpleCache = getSimpleCache();
CacheSpan cacheSpan = simpleCache.startReadWrite(KEY_1, 0);
// No cached bytes, returns -'length'
assertThat(simpleCache.getCachedLength(KEY_1, 0, 100)).isEqualTo(-100);
// Position value doesn't affect the return value
assertThat(simpleCache.getCachedLength(KEY_1, 20, 100)).isEqualTo(-100);
addCache(simpleCache, KEY_1, 0, 15);
// Returns the length of a single span
assertThat(simpleCache.getCachedLength(KEY_1, 0, 100)).isEqualTo(15);
// Value is capped by the 'length'
assertThat(simpleCache.getCachedLength(KEY_1, 0, 10)).isEqualTo(10);
addCache(simpleCache, KEY_1, 15, 35);
// Returns the length of two adjacent spans
assertThat(simpleCache.getCachedLength(KEY_1, 0, 100)).isEqualTo(50);
addCache(simpleCache, KEY_1, 60, 10);
// Not adjacent span doesn't affect return value
assertThat(simpleCache.getCachedLength(KEY_1, 0, 100)).isEqualTo(50);
// Returns length of hole up to the next cached span
assertThat(simpleCache.getCachedLength(KEY_1, 55, 100)).isEqualTo(-5);
assertThat(simpleCache.getCachedLength(KEY_1, /* position= */ 0, /* length= */ 100))
.isEqualTo(-100);
assertThat(simpleCache.getCachedLength(KEY_1, /* position= */ 0, /* length= */ Long.MAX_VALUE))
.isEqualTo(-Long.MAX_VALUE);
assertThat(simpleCache.getCachedLength(KEY_1, /* position= */ 20, /* length= */ 100))
.isEqualTo(-100);
assertThat(simpleCache.getCachedLength(KEY_1, /* position= */ 20, /* length= */ Long.MAX_VALUE))
.isEqualTo(-Long.MAX_VALUE);
}
@Test
public void getCachedLength_returnsNegativeHoleLength() throws Exception {
SimpleCache simpleCache = getSimpleCache();
CacheSpan cacheSpan = simpleCache.startReadWrite(KEY_1, /* position= */ 0);
addCache(simpleCache, KEY_1, /* position= */ 50, /* length= */ 50);
simpleCache.releaseHoleSpan(cacheSpan);
assertThat(simpleCache.getCachedLength(KEY_1, /* position= */ 0, /* length= */ 100))
.isEqualTo(-50);
assertThat(simpleCache.getCachedLength(KEY_1, /* position= */ 0, /* length= */ Long.MAX_VALUE))
.isEqualTo(-50);
assertThat(simpleCache.getCachedLength(KEY_1, /* position= */ 20, /* length= */ 100))
.isEqualTo(-30);
assertThat(simpleCache.getCachedLength(KEY_1, /* position= */ 20, /* length= */ Long.MAX_VALUE))
.isEqualTo(-30);
}
@Test
public void getCachedLength_returnsCachedLength() throws Exception {
SimpleCache simpleCache = getSimpleCache();
CacheSpan cacheSpan = simpleCache.startReadWrite(KEY_1, /* position= */ 0);
addCache(simpleCache, KEY_1, /* position= */ 0, /* length= */ 50);
simpleCache.releaseHoleSpan(cacheSpan);
assertThat(simpleCache.getCachedLength(KEY_1, /* position= */ 0, /* length= */ 100))
.isEqualTo(50);
assertThat(simpleCache.getCachedLength(KEY_1, /* position= */ 0, /* length= */ Long.MAX_VALUE))
.isEqualTo(50);
assertThat(simpleCache.getCachedLength(KEY_1, /* position= */ 20, /* length= */ 100))
.isEqualTo(30);
assertThat(simpleCache.getCachedLength(KEY_1, /* position= */ 20, /* length= */ Long.MAX_VALUE))
.isEqualTo(30);
assertThat(simpleCache.getCachedLength(KEY_1, /* position= */ 20, /* length= */ 15))
.isEqualTo(15);
}
@Test
public void getCachedLength_withMultipleAdjacentSpans_returnsCachedLength() throws Exception {
SimpleCache simpleCache = getSimpleCache();
CacheSpan cacheSpan = simpleCache.startReadWrite(KEY_1, /* position= */ 0);
addCache(simpleCache, KEY_1, /* position= */ 0, /* length= */ 25);
addCache(simpleCache, KEY_1, /* position= */ 25, /* length= */ 25);
simpleCache.releaseHoleSpan(cacheSpan);
assertThat(simpleCache.getCachedLength(KEY_1, /* position= */ 0, /* length= */ 100))
.isEqualTo(50);
assertThat(simpleCache.getCachedLength(KEY_1, /* position= */ 0, /* length= */ Long.MAX_VALUE))
.isEqualTo(50);
assertThat(simpleCache.getCachedLength(KEY_1, /* position= */ 20, /* length= */ 100))
.isEqualTo(30);
assertThat(simpleCache.getCachedLength(KEY_1, /* position= */ 20, /* length= */ Long.MAX_VALUE))
.isEqualTo(30);
assertThat(simpleCache.getCachedLength(KEY_1, /* position= */ 20, /* length= */ 15))
.isEqualTo(15);
}
@Test
public void getCachedLength_withMultipleNonAdjacentSpans_returnsCachedLength() throws Exception {
SimpleCache simpleCache = getSimpleCache();
CacheSpan cacheSpan = simpleCache.startReadWrite(KEY_1, /* position= */ 0);
addCache(simpleCache, KEY_1, /* position= */ 0, /* length= */ 10);
addCache(simpleCache, KEY_1, /* position= */ 15, /* length= */ 35);
simpleCache.releaseHoleSpan(cacheSpan);
assertThat(simpleCache.getCachedLength(KEY_1, /* position= */ 0, /* length= */ 100))
.isEqualTo(10);
assertThat(simpleCache.getCachedLength(KEY_1, /* position= */ 0, /* length= */ Long.MAX_VALUE))
.isEqualTo(10);
assertThat(simpleCache.getCachedLength(KEY_1, /* position= */ 20, /* length= */ 100))
.isEqualTo(30);
assertThat(simpleCache.getCachedLength(KEY_1, /* position= */ 20, /* length= */ Long.MAX_VALUE))
.isEqualTo(30);
assertThat(simpleCache.getCachedLength(KEY_1, /* position= */ 20, /* length= */ 15))
.isEqualTo(15);
}
/* Tests https://github.com/google/ExoPlayer/issues/3260 case. */

View file

@ -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(500L);
}
@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();
}
});
}
}

View file

@ -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;
@ -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:
*
* <ul>
* <li>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.
* <li>The two adaptation sets are marked as safe for switching using {@code
* urn:mpeg:dash:adaptation-set-switching:2016} supplemental properties.
* </ul>
*
* @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<AdaptationSet> adaptationSets) {
int adaptationSetCount = adaptationSets.size();
SparseIntArray idToIndexMap = new SparseIntArray(adaptationSetCount);
SparseIntArray adaptationSetIdToIndex = new SparseIntArray(adaptationSetCount);
List<List<Integer>> adaptationSetGroupedIndices = new ArrayList<>(adaptationSetCount);
SparseArray<List<Integer>> 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<Integer> 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<Integer> thisGroup = adaptationSetIndexToGroupedIndices.get(i);
List<Integer> 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;
}
/**
@ -739,9 +784,19 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
}
private static Descriptor findAdaptationSetSwitchingProperty(List<Descriptor> descriptors) {
return findDescriptor(descriptors, "urn:mpeg:dash:adaptation-set-switching:2016");
}
@Nullable
private static Descriptor findTrickPlayProperty(List<Descriptor> descriptors) {
return findDescriptor(descriptors, "http://dashif.org/guidelines/trickmode");
}
@Nullable
private static Descriptor findDescriptor(List<Descriptor> 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;
}
}

View file

@ -307,7 +307,10 @@ public final class DashMediaSource extends BaseMediaSource {
@Override
public Factory setDrmSessionManager(DrmSessionManager<?> drmSessionManager) {
Assertions.checkState(!isCreateCalled);
this.drmSessionManager = drmSessionManager;
this.drmSessionManager =
drmSessionManager != null
? drmSessionManager
: DrmSessionManager.getDummyDrmSessionManager();
return this;
}

Some files were not shown because too many files have changed in this diff Show more