mirror of
https://github.com/samsonjs/media.git
synced 2026-03-29 10:05:48 +00:00
commit
6bfdf8f8f6
121 changed files with 4558 additions and 2356 deletions
|
|
@ -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`
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
153
extensions/av1/src/main/jni/cpu_info.cc
Normal file
153
extensions/av1/src/main/jni/cpu_info.cc
Normal 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
|
||||
13
extensions/av1/src/main/jni/cpu_info.h
Normal file
13
extensions/av1/src/main/jni/cpu_info.h
Normal 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_
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.*`
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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. */
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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).");
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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}.
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
BIN
library/core/src/test/assets/mp4/sample_android_slow_motion.mp4
Normal file
BIN
library/core/src/test/assets/mp4/sample_android_slow_motion.mp4
Normal file
Binary file not shown.
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()));
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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. */
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Reference in a new issue