From 56425ade097a52b9d566bf687ac9b559dc4dbbfc Mon Sep 17 00:00:00 2001 From: Emily Dixon Date: Tue, 19 Sep 2023 15:14:48 -0700 Subject: [PATCH] Releases/v3.4.5 (#329) ## Improvements * Add support for ExoPlayer 2.19 (#327) * Clean out code for old ExoPlayer support (#328) Co-authored-by: Emily Dixon Co-authored-by: GitHub --- .github/workflows/saucelabs-tests-push.yml | 17 +- .github/workflows/saucelabs-tests-release.yml | 8 +- ExoPlayerAdapter/build.gradle | 24 +- .../sdk/muxstats/AnalyticsListenerMetrics.kt | 0 .../stats/sdk/muxstats/SessionDataBindings.kt | 97 + .../sdk/muxstats/AnalyticsListenerMetrics.kt | 38 + .../stats/sdk/muxstats/SessionDataBindings.kt | 0 .../muxstats/internal/ExoAnalyticsListener.kt | 6 - .../stats/sdk/muxstats/MuxStateCollector.kt | 19 - .../sdk/muxstats/ExoErrorMetricsUpTo214.kt | 49 - .../sdk/muxstats/BasicExoMetricsUpTo213.kt | 77 - .../mux/stats/sdk/muxstats/ExoPlayerExt.kt | 13 - MuxExoPlayer/build.gradle | 8 +- automatedtests/build.gradle | 24 +- .../exoplayer2/ext/ima/ImaAdsLoader.java | 1509 ------------- .../ui/SimplePlayerTestActivity.java | 204 -- .../exoplayer2/ext/ima/ImaAdsLoader.java | 1513 ------------- .../ui/SimplePlayerTestActivity.java | 214 -- .../exoplayer2/ext/ima/ImaAdsLoader.java | 1990 ----------------- .../android/exoplayer2/ext/ima/ImaUtil.java | 239 -- .../ui/SimplePlayerTestActivity.java | 228 -- .../exoplayer2/ext/ima/AdTagLoader.java | 1522 ------------- .../exoplayer2/ext/ima/ImaAdsLoader.java | 770 ------- .../android/exoplayer2/ext/ima/ImaUtil.java | 268 --- .../exoplayer2/ext/ima/AdTagLoader.java | 1582 ------------- .../exoplayer2/ext/ima/ImaAdsLoader.java | 759 ------- .../android/exoplayer2/ext/ima/ImaUtil.java | 255 --- .../ui/SimplePlayerTestActivity.java | 1 - .../ui/SimplePlayerTestActivity.java | 42 +- demo/build.gradle | 14 +- demo/src/r2_10_6/AndroidManifest.xml | 113 - demo/src/r2_10_6/assets/media.exolist.json | 582 ----- .../exoplayer2/demo/DemoApplication.java | 179 -- .../exoplayer2/demo/DemoDownloadService.java | 93 - .../exoplayer2/demo/DownloadTracker.java | 276 --- .../exoplayer2/demo/PlayerActivity.java | 766 ------- .../demo/SampleChooserActivity.java | 616 ----- .../exoplayer2/demo/TrackSelectionDialog.java | 373 --- .../exoplayer2/ext/ima/ImaAdsLoader.java | 1509 ------------- .../r2_10_6/res/layout/player_activity.xml | 60 - .../res/layout/sample_chooser_activity.xml | 25 - .../r2_10_6/res/layout/sample_list_item.xml | 39 - .../res/layout/track_selection_dialog.xml | 58 - .../r2_10_6/res/menu/sample_chooser_menu.xml | 27 - demo/src/r2_10_6/res/values/strings.xml | 69 - demo/src/r2_10_6/res/values/styles.xml | 29 - demo/src/r2_11_1/AndroidManifest.xml | 112 - demo/src/r2_11_1/assets/media.exolist.json | 602 ----- .../exoplayer2/demo/DemoApplication.java | 179 -- .../exoplayer2/demo/DemoDownloadService.java | 93 - .../exoplayer2/demo/DownloadTracker.java | 281 --- .../exoplayer2/demo/PlayerActivity.java | 800 ------- .../android/exoplayer2/demo/Sample.java | 240 -- .../demo/SampleChooserActivity.java | 544 ----- .../exoplayer2/demo/TrackSelectionDialog.java | 373 --- .../exoplayer2/ext/ima/ImaAdsLoader.java | 1513 ------------- .../r2_11_1/res/drawable-hdpi/ic_download.png | Bin 199 -> 0 bytes .../res/drawable-hdpi/ic_download_done.png | Bin 218 -> 0 bytes .../r2_11_1/res/drawable-mdpi/ic_download.png | Bin 163 -> 0 bytes .../res/drawable-mdpi/ic_download_done.png | Bin 182 -> 0 bytes .../r2_11_1/res/drawable-xhdpi/ic_banner.png | Bin 4299 -> 0 bytes .../res/drawable-xhdpi/ic_download.png | Bin 187 -> 0 bytes .../res/drawable-xhdpi/ic_download_done.png | Bin 304 -> 0 bytes .../res/drawable-xxhdpi/ic_download.png | Bin 261 -> 0 bytes .../res/drawable-xxhdpi/ic_download_done.png | Bin 450 -> 0 bytes .../res/drawable-xxxhdpi/ic_download.png | Bin 263 -> 0 bytes .../res/drawable-xxxhdpi/ic_download_done.png | Bin 575 -> 0 bytes .../r2_11_1/res/layout/player_activity.xml | 60 - .../res/layout/sample_chooser_activity.xml | 25 - .../r2_11_1/res/layout/sample_list_item.xml | 39 - .../res/layout/track_selection_dialog.xml | 58 - .../r2_11_1/res/menu/sample_chooser_menu.xml | 32 - .../r2_11_1/res/mipmap-hdpi/ic_launcher.png | Bin 3394 -> 0 bytes .../r2_11_1/res/mipmap-mdpi/ic_launcher.png | Bin 2184 -> 0 bytes .../r2_11_1/res/mipmap-xhdpi/ic_launcher.png | Bin 4886 -> 0 bytes .../r2_11_1/res/mipmap-xxhdpi/ic_launcher.png | Bin 7492 -> 0 bytes .../res/mipmap-xxxhdpi/ic_launcher.png | Bin 10801 -> 0 bytes demo/src/r2_11_1/res/values/strings.xml | 75 - demo/src/r2_11_1/res/values/styles.xml | 29 - demo/src/r2_12_1/AndroidManifest.xml | 112 - demo/src/r2_12_1/assets/media.exolist.json | 572 ----- .../android/exoplayer2/demo/DemoUtil.java | 210 -- .../exoplayer2/demo/DownloadTracker.java | 435 ---- .../android/exoplayer2/demo/IntentUtil.java | 236 -- .../exoplayer2/demo/PlayerActivity.java | 622 ------ .../demo/SampleChooserActivity.java | 592 ----- .../exoplayer2/demo/TrackSelectionDialog.java | 378 ---- .../cronet/ByteArrayUploadDataProvider.java | 57 - .../ext/cronet/CronetDataSource.java | 1047 --------- .../ext/cronet/CronetDataSourceFactory.java | 377 ---- .../ext/cronet/CronetEngineWrapper.java | 256 --- .../exoplayer2/ext/cronet/package-info.java | 19 - .../exoplayer2/ext/ima/ImaAdsLoader.java | 1990 ----------------- .../android/exoplayer2/ext/ima/ImaUtil.java | 239 -- .../exoplayer2/ext/ima/package-info.java | 19 - .../r2_12_1/res/drawable-hdpi/ic_download.png | Bin 199 -> 0 bytes .../res/drawable-hdpi/ic_download_done.png | Bin 218 -> 0 bytes .../r2_12_1/res/drawable-mdpi/ic_download.png | Bin 163 -> 0 bytes .../res/drawable-mdpi/ic_download_done.png | Bin 182 -> 0 bytes .../r2_12_1/res/drawable-xhdpi/ic_banner.png | Bin 4299 -> 0 bytes .../res/drawable-xhdpi/ic_download.png | Bin 187 -> 0 bytes .../res/drawable-xhdpi/ic_download_done.png | Bin 304 -> 0 bytes .../res/drawable-xxhdpi/ic_download.png | Bin 261 -> 0 bytes .../res/drawable-xxhdpi/ic_download_done.png | Bin 450 -> 0 bytes .../res/drawable-xxxhdpi/ic_download.png | Bin 263 -> 0 bytes .../res/drawable-xxxhdpi/ic_download_done.png | Bin 575 -> 0 bytes .../r2_12_1/res/layout/player_activity.xml | 63 - .../res/layout/sample_chooser_activity.xml | 25 - .../r2_12_1/res/layout/sample_list_item.xml | 39 - .../res/layout/track_selection_dialog.xml | 58 - .../r2_12_1/res/mipmap-hdpi/ic_launcher.png | Bin 3394 -> 0 bytes .../r2_12_1/res/mipmap-mdpi/ic_launcher.png | Bin 2184 -> 0 bytes .../r2_12_1/res/mipmap-xhdpi/ic_launcher.png | Bin 4886 -> 0 bytes .../r2_12_1/res/mipmap-xxhdpi/ic_launcher.png | Bin 7492 -> 0 bytes .../res/mipmap-xxxhdpi/ic_launcher.png | Bin 10801 -> 0 bytes demo/src/r2_13_1/AndroidManifest.xml | 112 - .../exoplayer2/demo/DemoDownloadService.java | 125 -- .../android/exoplayer2/demo/package-info.java | 19 - .../cronet/ByteArrayUploadDataProvider.java | 57 - .../ext/cronet/CronetDataSource.java | 1206 ---------- .../ext/cronet/CronetDataSourceFactory.java | 375 ---- .../ext/cronet/CronetEngineWrapper.java | 264 --- .../exoplayer2/ext/cronet/package-info.java | 19 - .../exoplayer2/ext/ima/AdTagLoader.java | 1522 ------------- .../exoplayer2/ext/ima/ImaAdsLoader.java | 770 ------- .../android/exoplayer2/ext/ima/ImaUtil.java | 268 --- .../exoplayer2/ext/ima/package-info.java | 19 - .../r2_13_1/res/drawable-hdpi/ic_download.png | Bin 199 -> 0 bytes .../res/drawable-hdpi/ic_download_done.png | Bin 218 -> 0 bytes .../r2_13_1/res/drawable-mdpi/ic_download.png | Bin 163 -> 0 bytes .../res/drawable-mdpi/ic_download_done.png | Bin 182 -> 0 bytes .../r2_13_1/res/drawable-xhdpi/ic_banner.png | Bin 4299 -> 0 bytes .../res/drawable-xhdpi/ic_download.png | Bin 187 -> 0 bytes .../res/drawable-xhdpi/ic_download_done.png | Bin 304 -> 0 bytes .../res/drawable-xxhdpi/ic_download.png | Bin 261 -> 0 bytes .../res/drawable-xxhdpi/ic_download_done.png | Bin 450 -> 0 bytes .../res/drawable-xxxhdpi/ic_download.png | Bin 263 -> 0 bytes .../res/drawable-xxxhdpi/ic_download_done.png | Bin 575 -> 0 bytes .../r2_13_1/res/layout/player_activity.xml | 63 - .../r2_13_1/res/layout/sample_list_item.xml | 39 - .../res/layout/track_selection_dialog.xml | 58 - .../r2_13_1/res/menu/sample_chooser_menu.xml | 22 - .../r2_13_1/res/mipmap-hdpi/ic_launcher.png | Bin 3394 -> 0 bytes .../r2_13_1/res/mipmap-mdpi/ic_launcher.png | Bin 2184 -> 0 bytes .../r2_13_1/res/mipmap-xhdpi/ic_launcher.png | Bin 4886 -> 0 bytes .../r2_13_1/res/mipmap-xxhdpi/ic_launcher.png | Bin 7492 -> 0 bytes .../res/mipmap-xxxhdpi/ic_launcher.png | Bin 10801 -> 0 bytes demo/src/r2_13_1/res/values/strings.xml | 63 - demo/src/r2_13_1/res/values/styles.xml | 25 - demo/src/r2_19_1/AndroidManifest.xml | 99 + .../assets/media.exolist.json | 557 ++--- .../exoplayer2/demo/DemoDownloadService.java | 17 +- .../android/exoplayer2/demo/DemoUtil.java | 108 +- .../exoplayer2/demo/DownloadTracker.java | 118 +- .../android/exoplayer2/demo/IntentUtil.java | 174 +- .../exoplayer2/demo/PlayerActivity.java | 298 ++- .../demo/SampleChooserActivity.java | 101 +- .../exoplayer2/demo/TrackSelectionDialog.java | 259 +-- .../android/exoplayer2/demo/package-info.java | 0 .../res/drawable-hdpi/ic_download.png | Bin .../res/drawable-hdpi/ic_download_done.png | Bin .../res/drawable-mdpi/ic_download.png | Bin .../res/drawable-mdpi/ic_download_done.png | Bin .../res/drawable-xhdpi/ic_banner.png | Bin .../res/drawable-xhdpi/ic_download.png | Bin .../res/drawable-xhdpi/ic_download_done.png | Bin .../res/drawable-xxhdpi/ic_download.png | Bin .../res/drawable-xxhdpi/ic_download_done.png | Bin .../res/drawable-xxxhdpi/ic_download.png | Bin .../res/drawable-xxxhdpi/ic_download_done.png | Bin .../r2_19_1/res/layout/player_activity.xml | 60 + .../res/layout/sample_chooser_activity.xml | 16 +- .../r2_19_1/res/layout/sample_list_item.xml | 38 + .../res/layout/track_selection_dialog.xml | 59 + .../res/menu/sample_chooser_menu.xml | 14 +- .../res/mipmap-hdpi/ic_launcher.png | Bin .../res/mipmap-mdpi/ic_launcher.png | Bin .../res/mipmap-xhdpi/ic_launcher.png | Bin .../res/mipmap-xxhdpi/ic_launcher.png | Bin .../res/mipmap-xxxhdpi/ic_launcher.png | Bin .../res/values/strings.xml | 7 +- .../res/values/styles.xml | 3 +- 182 files changed, 1308 insertions(+), 34351 deletions(-) rename ExoPlayerAdapter/src/{exo-analytics-2_16-now => exo-analytics-2_19-now}/java/com/mux/stats/sdk/muxstats/AnalyticsListenerMetrics.kt (100%) create mode 100644 ExoPlayerAdapter/src/exo-analytics-2_19-now/java/com/mux/stats/sdk/muxstats/SessionDataBindings.kt create mode 100644 ExoPlayerAdapter/src/exo-analytics-just2_16/java/com/mux/stats/sdk/muxstats/AnalyticsListenerMetrics.kt rename ExoPlayerAdapter/src/{exo-analytics-2_16-now => exo-analytics-just2_16}/java/com/mux/stats/sdk/muxstats/SessionDataBindings.kt (100%) delete mode 100644 ExoPlayerAdapter/src/exo-collector-upto-2_10/java/com/mux/stats/sdk/muxstats/MuxStateCollector.kt delete mode 100644 ExoPlayerAdapter/src/exo-error-upto-2_13/java/com/mux/stats/sdk/muxstats/ExoErrorMetricsUpTo214.kt delete mode 100644 ExoPlayerAdapter/src/exo-event-upto-2_13/java/com/mux/stats/sdk/muxstats/BasicExoMetricsUpTo213.kt delete mode 100644 ExoPlayerAdapter/src/exo-mediaitem-upto-2_13/java/com/mux/stats/sdk/muxstats/ExoPlayerExt.kt delete mode 100644 automatedtests/src/androidTestR2_10_6/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java delete mode 100644 automatedtests/src/androidTestR2_10_6/java/com/mux/stats/sdk/muxstats/automatedtests/ui/SimplePlayerTestActivity.java delete mode 100644 automatedtests/src/androidTestR2_11_1/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java delete mode 100644 automatedtests/src/androidTestR2_11_1/java/com/mux/stats/sdk/muxstats/automatedtests/ui/SimplePlayerTestActivity.java delete mode 100644 automatedtests/src/androidTestR2_12_1/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java delete mode 100644 automatedtests/src/androidTestR2_12_1/java/com/google/android/exoplayer2/ext/ima/ImaUtil.java delete mode 100644 automatedtests/src/androidTestR2_12_1/java/com/mux/stats/sdk/muxstats/automatedtests/ui/SimplePlayerTestActivity.java delete mode 100644 automatedtests/src/androidTestR2_13_1/java/com/google/android/exoplayer2/ext/ima/AdTagLoader.java delete mode 100644 automatedtests/src/androidTestR2_13_1/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java delete mode 100644 automatedtests/src/androidTestR2_13_1/java/com/google/android/exoplayer2/ext/ima/ImaUtil.java delete mode 100644 automatedtests/src/androidTestR2_18_1/java/com/google/android/exoplayer2/ext/ima/AdTagLoader.java delete mode 100644 automatedtests/src/androidTestR2_18_1/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java delete mode 100644 automatedtests/src/androidTestR2_18_1/java/com/google/android/exoplayer2/ext/ima/ImaUtil.java rename automatedtests/src/{androidTestR2_13_1 => androidTestR2_19_1}/java/com/mux/stats/sdk/muxstats/automatedtests/ui/SimplePlayerTestActivity.java (89%) delete mode 100644 demo/src/r2_10_6/AndroidManifest.xml delete mode 100644 demo/src/r2_10_6/assets/media.exolist.json delete mode 100644 demo/src/r2_10_6/java/com/google/android/exoplayer2/demo/DemoApplication.java delete mode 100644 demo/src/r2_10_6/java/com/google/android/exoplayer2/demo/DemoDownloadService.java delete mode 100644 demo/src/r2_10_6/java/com/google/android/exoplayer2/demo/DownloadTracker.java delete mode 100644 demo/src/r2_10_6/java/com/google/android/exoplayer2/demo/PlayerActivity.java delete mode 100644 demo/src/r2_10_6/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java delete mode 100644 demo/src/r2_10_6/java/com/google/android/exoplayer2/demo/TrackSelectionDialog.java delete mode 100644 demo/src/r2_10_6/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java delete mode 100644 demo/src/r2_10_6/res/layout/player_activity.xml delete mode 100644 demo/src/r2_10_6/res/layout/sample_chooser_activity.xml delete mode 100644 demo/src/r2_10_6/res/layout/sample_list_item.xml delete mode 100644 demo/src/r2_10_6/res/layout/track_selection_dialog.xml delete mode 100644 demo/src/r2_10_6/res/menu/sample_chooser_menu.xml delete mode 100644 demo/src/r2_10_6/res/values/strings.xml delete mode 100644 demo/src/r2_10_6/res/values/styles.xml delete mode 100644 demo/src/r2_11_1/AndroidManifest.xml delete mode 100644 demo/src/r2_11_1/assets/media.exolist.json delete mode 100644 demo/src/r2_11_1/java/com/google/android/exoplayer2/demo/DemoApplication.java delete mode 100644 demo/src/r2_11_1/java/com/google/android/exoplayer2/demo/DemoDownloadService.java delete mode 100644 demo/src/r2_11_1/java/com/google/android/exoplayer2/demo/DownloadTracker.java delete mode 100644 demo/src/r2_11_1/java/com/google/android/exoplayer2/demo/PlayerActivity.java delete mode 100644 demo/src/r2_11_1/java/com/google/android/exoplayer2/demo/Sample.java delete mode 100644 demo/src/r2_11_1/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java delete mode 100644 demo/src/r2_11_1/java/com/google/android/exoplayer2/demo/TrackSelectionDialog.java delete mode 100644 demo/src/r2_11_1/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java delete mode 100644 demo/src/r2_11_1/res/drawable-hdpi/ic_download.png delete mode 100644 demo/src/r2_11_1/res/drawable-hdpi/ic_download_done.png delete mode 100644 demo/src/r2_11_1/res/drawable-mdpi/ic_download.png delete mode 100644 demo/src/r2_11_1/res/drawable-mdpi/ic_download_done.png delete mode 100644 demo/src/r2_11_1/res/drawable-xhdpi/ic_banner.png delete mode 100644 demo/src/r2_11_1/res/drawable-xhdpi/ic_download.png delete mode 100644 demo/src/r2_11_1/res/drawable-xhdpi/ic_download_done.png delete mode 100644 demo/src/r2_11_1/res/drawable-xxhdpi/ic_download.png delete mode 100644 demo/src/r2_11_1/res/drawable-xxhdpi/ic_download_done.png delete mode 100644 demo/src/r2_11_1/res/drawable-xxxhdpi/ic_download.png delete mode 100644 demo/src/r2_11_1/res/drawable-xxxhdpi/ic_download_done.png delete mode 100644 demo/src/r2_11_1/res/layout/player_activity.xml delete mode 100644 demo/src/r2_11_1/res/layout/sample_chooser_activity.xml delete mode 100644 demo/src/r2_11_1/res/layout/sample_list_item.xml delete mode 100644 demo/src/r2_11_1/res/layout/track_selection_dialog.xml delete mode 100644 demo/src/r2_11_1/res/menu/sample_chooser_menu.xml delete mode 100644 demo/src/r2_11_1/res/mipmap-hdpi/ic_launcher.png delete mode 100644 demo/src/r2_11_1/res/mipmap-mdpi/ic_launcher.png delete mode 100644 demo/src/r2_11_1/res/mipmap-xhdpi/ic_launcher.png delete mode 100644 demo/src/r2_11_1/res/mipmap-xxhdpi/ic_launcher.png delete mode 100644 demo/src/r2_11_1/res/mipmap-xxxhdpi/ic_launcher.png delete mode 100644 demo/src/r2_11_1/res/values/strings.xml delete mode 100644 demo/src/r2_11_1/res/values/styles.xml delete mode 100644 demo/src/r2_12_1/AndroidManifest.xml delete mode 100644 demo/src/r2_12_1/assets/media.exolist.json delete mode 100644 demo/src/r2_12_1/java/com/google/android/exoplayer2/demo/DemoUtil.java delete mode 100644 demo/src/r2_12_1/java/com/google/android/exoplayer2/demo/DownloadTracker.java delete mode 100644 demo/src/r2_12_1/java/com/google/android/exoplayer2/demo/IntentUtil.java delete mode 100644 demo/src/r2_12_1/java/com/google/android/exoplayer2/demo/PlayerActivity.java delete mode 100644 demo/src/r2_12_1/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java delete mode 100644 demo/src/r2_12_1/java/com/google/android/exoplayer2/demo/TrackSelectionDialog.java delete mode 100644 demo/src/r2_12_1/java/com/google/android/exoplayer2/ext/cronet/ByteArrayUploadDataProvider.java delete mode 100644 demo/src/r2_12_1/java/com/google/android/exoplayer2/ext/cronet/CronetDataSource.java delete mode 100644 demo/src/r2_12_1/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceFactory.java delete mode 100644 demo/src/r2_12_1/java/com/google/android/exoplayer2/ext/cronet/CronetEngineWrapper.java delete mode 100644 demo/src/r2_12_1/java/com/google/android/exoplayer2/ext/cronet/package-info.java delete mode 100644 demo/src/r2_12_1/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java delete mode 100644 demo/src/r2_12_1/java/com/google/android/exoplayer2/ext/ima/ImaUtil.java delete mode 100644 demo/src/r2_12_1/java/com/google/android/exoplayer2/ext/ima/package-info.java delete mode 100644 demo/src/r2_12_1/res/drawable-hdpi/ic_download.png delete mode 100644 demo/src/r2_12_1/res/drawable-hdpi/ic_download_done.png delete mode 100644 demo/src/r2_12_1/res/drawable-mdpi/ic_download.png delete mode 100644 demo/src/r2_12_1/res/drawable-mdpi/ic_download_done.png delete mode 100644 demo/src/r2_12_1/res/drawable-xhdpi/ic_banner.png delete mode 100644 demo/src/r2_12_1/res/drawable-xhdpi/ic_download.png delete mode 100644 demo/src/r2_12_1/res/drawable-xhdpi/ic_download_done.png delete mode 100644 demo/src/r2_12_1/res/drawable-xxhdpi/ic_download.png delete mode 100644 demo/src/r2_12_1/res/drawable-xxhdpi/ic_download_done.png delete mode 100644 demo/src/r2_12_1/res/drawable-xxxhdpi/ic_download.png delete mode 100644 demo/src/r2_12_1/res/drawable-xxxhdpi/ic_download_done.png delete mode 100644 demo/src/r2_12_1/res/layout/player_activity.xml delete mode 100644 demo/src/r2_12_1/res/layout/sample_chooser_activity.xml delete mode 100644 demo/src/r2_12_1/res/layout/sample_list_item.xml delete mode 100644 demo/src/r2_12_1/res/layout/track_selection_dialog.xml delete mode 100644 demo/src/r2_12_1/res/mipmap-hdpi/ic_launcher.png delete mode 100644 demo/src/r2_12_1/res/mipmap-mdpi/ic_launcher.png delete mode 100644 demo/src/r2_12_1/res/mipmap-xhdpi/ic_launcher.png delete mode 100644 demo/src/r2_12_1/res/mipmap-xxhdpi/ic_launcher.png delete mode 100644 demo/src/r2_12_1/res/mipmap-xxxhdpi/ic_launcher.png delete mode 100644 demo/src/r2_13_1/AndroidManifest.xml delete mode 100644 demo/src/r2_13_1/java/com/google/android/exoplayer2/demo/DemoDownloadService.java delete mode 100644 demo/src/r2_13_1/java/com/google/android/exoplayer2/demo/package-info.java delete mode 100644 demo/src/r2_13_1/java/com/google/android/exoplayer2/ext/cronet/ByteArrayUploadDataProvider.java delete mode 100644 demo/src/r2_13_1/java/com/google/android/exoplayer2/ext/cronet/CronetDataSource.java delete mode 100644 demo/src/r2_13_1/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceFactory.java delete mode 100644 demo/src/r2_13_1/java/com/google/android/exoplayer2/ext/cronet/CronetEngineWrapper.java delete mode 100644 demo/src/r2_13_1/java/com/google/android/exoplayer2/ext/cronet/package-info.java delete mode 100644 demo/src/r2_13_1/java/com/google/android/exoplayer2/ext/ima/AdTagLoader.java delete mode 100644 demo/src/r2_13_1/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java delete mode 100644 demo/src/r2_13_1/java/com/google/android/exoplayer2/ext/ima/ImaUtil.java delete mode 100644 demo/src/r2_13_1/java/com/google/android/exoplayer2/ext/ima/package-info.java delete mode 100644 demo/src/r2_13_1/res/drawable-hdpi/ic_download.png delete mode 100644 demo/src/r2_13_1/res/drawable-hdpi/ic_download_done.png delete mode 100644 demo/src/r2_13_1/res/drawable-mdpi/ic_download.png delete mode 100644 demo/src/r2_13_1/res/drawable-mdpi/ic_download_done.png delete mode 100644 demo/src/r2_13_1/res/drawable-xhdpi/ic_banner.png delete mode 100644 demo/src/r2_13_1/res/drawable-xhdpi/ic_download.png delete mode 100644 demo/src/r2_13_1/res/drawable-xhdpi/ic_download_done.png delete mode 100644 demo/src/r2_13_1/res/drawable-xxhdpi/ic_download.png delete mode 100644 demo/src/r2_13_1/res/drawable-xxhdpi/ic_download_done.png delete mode 100644 demo/src/r2_13_1/res/drawable-xxxhdpi/ic_download.png delete mode 100644 demo/src/r2_13_1/res/drawable-xxxhdpi/ic_download_done.png delete mode 100644 demo/src/r2_13_1/res/layout/player_activity.xml delete mode 100644 demo/src/r2_13_1/res/layout/sample_list_item.xml delete mode 100644 demo/src/r2_13_1/res/layout/track_selection_dialog.xml delete mode 100644 demo/src/r2_13_1/res/menu/sample_chooser_menu.xml delete mode 100644 demo/src/r2_13_1/res/mipmap-hdpi/ic_launcher.png delete mode 100644 demo/src/r2_13_1/res/mipmap-mdpi/ic_launcher.png delete mode 100644 demo/src/r2_13_1/res/mipmap-xhdpi/ic_launcher.png delete mode 100644 demo/src/r2_13_1/res/mipmap-xxhdpi/ic_launcher.png delete mode 100644 demo/src/r2_13_1/res/mipmap-xxxhdpi/ic_launcher.png delete mode 100644 demo/src/r2_13_1/res/values/strings.xml delete mode 100644 demo/src/r2_13_1/res/values/styles.xml create mode 100644 demo/src/r2_19_1/AndroidManifest.xml rename demo/src/{r2_13_1 => r2_19_1}/assets/media.exolist.json (83%) rename demo/src/{r2_12_1 => r2_19_1}/java/com/google/android/exoplayer2/demo/DemoDownloadService.java (91%) rename demo/src/{r2_13_1 => r2_19_1}/java/com/google/android/exoplayer2/demo/DemoUtil.java (67%) rename demo/src/{r2_13_1 => r2_19_1}/java/com/google/android/exoplayer2/demo/DownloadTracker.java (79%) rename demo/src/{r2_13_1 => r2_19_1}/java/com/google/android/exoplayer2/demo/IntentUtil.java (55%) rename demo/src/{r2_13_1 => r2_19_1}/java/com/google/android/exoplayer2/demo/PlayerActivity.java (64%) rename demo/src/{r2_13_1 => r2_19_1}/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java (84%) rename demo/src/{r2_13_1 => r2_19_1}/java/com/google/android/exoplayer2/demo/TrackSelectionDialog.java (54%) rename demo/src/{r2_12_1 => r2_19_1}/java/com/google/android/exoplayer2/demo/package-info.java (100%) rename demo/src/{r2_10_6 => r2_19_1}/res/drawable-hdpi/ic_download.png (100%) rename demo/src/{r2_10_6 => r2_19_1}/res/drawable-hdpi/ic_download_done.png (100%) rename demo/src/{r2_10_6 => r2_19_1}/res/drawable-mdpi/ic_download.png (100%) rename demo/src/{r2_10_6 => r2_19_1}/res/drawable-mdpi/ic_download_done.png (100%) rename demo/src/{r2_10_6 => r2_19_1}/res/drawable-xhdpi/ic_banner.png (100%) rename demo/src/{r2_10_6 => r2_19_1}/res/drawable-xhdpi/ic_download.png (100%) rename demo/src/{r2_10_6 => r2_19_1}/res/drawable-xhdpi/ic_download_done.png (100%) rename demo/src/{r2_10_6 => r2_19_1}/res/drawable-xxhdpi/ic_download.png (100%) rename demo/src/{r2_10_6 => r2_19_1}/res/drawable-xxhdpi/ic_download_done.png (100%) rename demo/src/{r2_10_6 => r2_19_1}/res/drawable-xxxhdpi/ic_download.png (100%) rename demo/src/{r2_10_6 => r2_19_1}/res/drawable-xxxhdpi/ic_download_done.png (100%) create mode 100644 demo/src/r2_19_1/res/layout/player_activity.xml rename demo/src/{r2_13_1 => r2_19_1}/res/layout/sample_chooser_activity.xml (69%) create mode 100644 demo/src/r2_19_1/res/layout/sample_list_item.xml create mode 100644 demo/src/r2_19_1/res/layout/track_selection_dialog.xml rename demo/src/{r2_12_1 => r2_19_1}/res/menu/sample_chooser_menu.xml (66%) rename demo/src/{r2_10_6 => r2_19_1}/res/mipmap-hdpi/ic_launcher.png (100%) rename demo/src/{r2_10_6 => r2_19_1}/res/mipmap-mdpi/ic_launcher.png (100%) rename demo/src/{r2_10_6 => r2_19_1}/res/mipmap-xhdpi/ic_launcher.png (100%) rename demo/src/{r2_10_6 => r2_19_1}/res/mipmap-xxhdpi/ic_launcher.png (100%) rename demo/src/{r2_10_6 => r2_19_1}/res/mipmap-xxxhdpi/ic_launcher.png (100%) rename demo/src/{r2_12_1 => r2_19_1}/res/values/strings.xml (90%) rename demo/src/{r2_12_1 => r2_19_1}/res/values/styles.xml (90%) diff --git a/.github/workflows/saucelabs-tests-push.yml b/.github/workflows/saucelabs-tests-push.yml index b024d336..4aaf2e8e 100644 --- a/.github/workflows/saucelabs-tests-push.yml +++ b/.github/workflows/saucelabs-tests-push.yml @@ -2,17 +2,14 @@ name: Test with Sauce Labs (Only Latest) on: push: - branches: - - '!releases/**' - - '!master' env: SAUCE_USERNAME: ${{secrets.SAUCE_USERNAME}} SAUCE_ACCESS_KEY: ${{secrets.SAUCE_ACCESS_KEY}} -concurrency: - group: sauce-labs - cancel-in-progress: true +#concurrency: +# group: sauce-labs +# cancel-in-progress: true jobs: build: @@ -29,7 +26,7 @@ jobs: - name: Build automated tests uses: gradle/gradle-build-action@v2.3.3 with: - arguments: automatedtests:assembleR2_18_1Debug :automatedtests:assembleR2_18_1DebugAndroidTest + arguments: automatedtests:assembleR2_19_1Debug :automatedtests:assembleR2_19_1DebugAndroidTest - name: Upload Test APKs uses: actions/upload-artifact@v3 with: @@ -38,7 +35,7 @@ jobs: automatedtests/buildout/outputs/apk/androidTest/**/*.apk automatedtests/buildout/outputs/apk/*/debug/automatedtests-*-debug.apk - test-18: + test: name: Run Sauce Labs Tests runs-on: ubuntu-latest needs: build @@ -46,7 +43,7 @@ jobs: strategy: max-parallel: 2 matrix: - exo: ['r2_18_1'] + exo: ['r2_19_1'] env: app_artifact: automatedtests\/buildout\/outputs\/apk\/${{ matrix.exo }}\/debug\/automatedtests-${{ matrix.exo }}-debug.apk @@ -69,7 +66,7 @@ jobs: .sauce/template.yml > .sauce/conf.yml - name: Dump Generated Sauce Conf run: cat .sauce/conf.yml - - name: Run Saucelabs Test (2.18.1) + - name: Run Saucelabs Test uses: saucelabs/saucectl-run-action@v3 env: GITHUB_TOKEN: ${{ github.token }} diff --git a/.github/workflows/saucelabs-tests-release.yml b/.github/workflows/saucelabs-tests-release.yml index 12ecf850..16e91c76 100644 --- a/.github/workflows/saucelabs-tests-release.yml +++ b/.github/workflows/saucelabs-tests-release.yml @@ -10,9 +10,9 @@ env: SAUCE_USERNAME: ${{secrets.SAUCE_USERNAME}} SAUCE_ACCESS_KEY: ${{secrets.SAUCE_ACCESS_KEY}} -concurrency: - group: sauce-labs - cancel-in-progress: true +#concurrency: +# group: sauce-labs +# cancel-in-progress: true jobs: build: @@ -47,7 +47,7 @@ jobs: max-parallel: 1 #max-parallel: 2 matrix: - exo: ['r2_18_1','r2_17_1','r2_16_1','r2_15_1','r2_14_1',] + exo: ['r2_18_1','r2_17_1','r2_16_1','r2_15_1','r2_14_1'] env: app_artifact: automatedtests\/buildout\/outputs\/apk\/${{ matrix.exo }}\/debug\/automatedtests-${{ matrix.exo }}-debug.apk diff --git a/ExoPlayerAdapter/build.gradle b/ExoPlayerAdapter/build.gradle index 72cf6191..0d845501 100644 --- a/ExoPlayerAdapter/build.gradle +++ b/ExoPlayerAdapter/build.gradle @@ -33,7 +33,8 @@ android { just2_15 { dimension 'source' } just2_16 { dimension 'source' } just2_17 { dimension 'source' } - from2_18toNow { dimension 'source' } + just2_18 { dimension 'source' } + just2_19 { dimension 'source' } // Exoplayer version for testing/variant matching r2_14_1 { dimension 'exoplayer' } r2_15_1 { dimension 'exoplayer' } @@ -41,6 +42,7 @@ android { amznPort { dimension 'exoplayer' } r2_17_1 { dimension 'exoplayer' } r2_18_1 { dimension 'exoplayer' } + r2_19_1 { dimension 'exoplayer' } } // We only want the variants where the sourceSet and exoplayer version agree def realVariants = [ @@ -48,7 +50,8 @@ android { "just2_15" : ["r2_15_1"], "just2_16" : ["r2_16_1", "amznPort"], "just2_17" : ["r2_17_1"], - "from2_18toNow" : ["r2_18_1"] + "just2_18" : ["r2_18_1"], + "just2_19" : ["r2_19_1"], ] variantFilter { variant -> def exoPlayerFlavor = variant.flavors.findAll { it.dimension == "exoplayer" }[0] @@ -77,7 +80,7 @@ android { just2_16 { java.srcDirs += 'src/exo-analytics-listener-2_13-2_16/java' java.srcDirs += 'src/exo-collector-2_10-now/java' - java.srcDirs += 'src/exo-analytics-2_16-now/java' + java.srcDirs += 'src/exo-analytics-just2_16/java' java.srcDirs += 'src/exo-error-2_15-now/java' java.srcDirs += 'src/exo-mediaitem-2_14-now/java' java.srcDirs += 'src/exo-player-2_16-now/java' @@ -86,21 +89,29 @@ android { just2_17 { java.srcDirs += 'src/exo-analytics-listener-just-2_17/java' java.srcDirs += 'src/exo-collector-2_10-now/java' - java.srcDirs += 'src/exo-analytics-2_16-now/java' + java.srcDirs += 'src/exo-analytics-just2_16/java' java.srcDirs += 'src/exo-error-2_15-now/java' java.srcDirs += 'src/exo-mediaitem-2_14-now/java' java.srcDirs += 'src/exo-player-2_16-now/java' // From 2.16 onward, we don't need exo-event } - from2_18toNow { + just2_18 { java.srcDirs += 'src/exo-analytics-listener-2_18-now/java' java.srcDirs += 'src/exo-collector-2_10-now/java' - java.srcDirs += 'src/exo-analytics-2_16-now/java' + java.srcDirs += 'src/exo-analytics-just2_16/java' java.srcDirs += 'src/exo-error-2_15-now/java' java.srcDirs += 'src/exo-mediaitem-2_14-now/java' java.srcDirs += 'src/exo-player-2_16-now/java' // From 2.16 onward, we don't need exo-event } + just2_19 { + java.srcDirs += 'src/exo-analytics-listener-2_18-now/java' + java.srcDirs += 'src/exo-collector-2_10-now/java' + java.srcDirs += 'src/exo-analytics-2_19-now/java' + java.srcDirs += 'src/exo-error-2_15-now/java' + java.srcDirs += 'src/exo-mediaitem-2_14-now/java' + java.srcDirs += 'src/exo-player-2_16-now/java' + } } compileOptions { @@ -144,6 +155,7 @@ dependencies { r2_17_1Api 'com.google.android.exoplayer:exoplayer:2.17.1' //noinspection GradleDynamicVersion,GradleDependency r2_18_1Api 'com.google.android.exoplayer:exoplayer:2.18.1' + r2_19_1Api 'com.google.android.exoplayer:exoplayer:2.19.1' implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:${project.ext.kotlinxCoroutinesVersion}" diff --git a/ExoPlayerAdapter/src/exo-analytics-2_16-now/java/com/mux/stats/sdk/muxstats/AnalyticsListenerMetrics.kt b/ExoPlayerAdapter/src/exo-analytics-2_19-now/java/com/mux/stats/sdk/muxstats/AnalyticsListenerMetrics.kt similarity index 100% rename from ExoPlayerAdapter/src/exo-analytics-2_16-now/java/com/mux/stats/sdk/muxstats/AnalyticsListenerMetrics.kt rename to ExoPlayerAdapter/src/exo-analytics-2_19-now/java/com/mux/stats/sdk/muxstats/AnalyticsListenerMetrics.kt diff --git a/ExoPlayerAdapter/src/exo-analytics-2_19-now/java/com/mux/stats/sdk/muxstats/SessionDataBindings.kt b/ExoPlayerAdapter/src/exo-analytics-2_19-now/java/com/mux/stats/sdk/muxstats/SessionDataBindings.kt new file mode 100644 index 00000000..533fb1a9 --- /dev/null +++ b/ExoPlayerAdapter/src/exo-analytics-2_19-now/java/com/mux/stats/sdk/muxstats/SessionDataBindings.kt @@ -0,0 +1,97 @@ +package com.mux.stats.sdk.muxstats + +import com.google.android.exoplayer2.ExoPlayer +import com.google.android.exoplayer2.analytics.AnalyticsListener +import com.google.android.exoplayer2.source.hls.HlsManifest +import com.mux.stats.sdk.core.model.SessionTag +import com.mux.stats.sdk.core.util.MuxLogger +import com.mux.stats.sdk.muxstats.internal.isHlsExtensionAvailable +import com.mux.stats.sdk.muxstats.internal.weak +import java.util.regex.Matcher +import java.util.regex.Pattern + +private class SessionDataPlayerBinding : MuxPlayerAdapter.PlayerBinding { + + private var listener: AnalyticsListener? by weak(null) + + override fun bindPlayer(player: ExoPlayer, collector: MuxStateCollector) { + if (isHlsExtensionAvailable()) { + listener = SessionDataListener(player, collector).also { player.addAnalyticsListener(it) } + } + } + + override fun unbindPlayer(player: ExoPlayer, collector: MuxStateCollector) { + listener?.let { player.removeAnalyticsListener(it) } + } + + /** + * Listens for timeline changes and updates HLS session data if we're on an HLS stream. + * This class should only be instantiated if ExoPlayer's HLS extension is available at runtime + * @see [.isHlsExtensionAvailable] + */ + private class SessionDataListener(player: ExoPlayer, val collector: MuxStateCollector) : + AnalyticsListener { + + private val player by weak(player) + + companion object { + val RX_SESSION_TAG_DATA_ID by lazy { Pattern.compile("DATA-ID=\"(.*)\",") } + val RX_SESSION_TAG_VALUES by lazy { Pattern.compile("VALUE=\"(.*)\"") } + + /** HLS session data tags with this Data ID will be sent to Mux Data */ + const val HLS_SESSION_LITIX_PREFIX = "io.litix.data." + const val LOG_TAG = "SessionDataListener" + } + + override fun onTimelineChanged(eventTime: AnalyticsListener.EventTime, reason: Int) { + player?.let { safePlayer -> + val manifest = safePlayer.currentManifest + if (manifest is HlsManifest) { + collector.onMainPlaylistTags(parseHlsSessionData(manifest.multivariantPlaylist.tags)) + } + } + } + + private fun parseHlsSessionData(hlsTags: List): List { + val data: MutableList = ArrayList() + for (tag in filterHlsSessionTags(hlsTags)) { + val st: SessionTag = parseHlsSessionTag(tag) + if (st.key != null && st.key.contains(HLS_SESSION_LITIX_PREFIX)) { + data.add(parseHlsSessionTag(tag)) + } + } + return data + } + + private fun filterHlsSessionTags(rawTags: List) = + rawTags.filter { it.substring(1).startsWith("EXT-X-SESSION-DATA") } + + private fun parseHlsSessionTag(line: String): SessionTag { + val dataId: Matcher = RX_SESSION_TAG_DATA_ID.matcher(line) + val value: Matcher = RX_SESSION_TAG_VALUES.matcher(line) + var parsedDataId: String? = "" + var parsedValue: String? = "" + if (dataId.find()) { + parsedDataId = dataId.group(1)?.replace(HLS_SESSION_LITIX_PREFIX, "") + } else { + MuxLogger.d(LOG_TAG, "Data-ID not found in session data: $line") + } + if (value.find()) { + parsedValue = value.group(1) + } else { + MuxLogger.d(LOG_TAG, "Value not found in session data: $line") + } + return SessionTag(parsedDataId, parsedValue) + } + } +} + +/** + * Creates a listener that listens for timeline changes and updates HLS session data if we're on an + * HLS stream. + * This class should only be instantiated if ExoPlayer's HLS extension is available at runtime + * @see [.isHlsExtensionAvailable] + */ +@Suppress("unused") // using the receiver to avoid polluting customers' namespace +fun MuxStateCollector.createExoSessionDataBinding(): MuxPlayerAdapter.PlayerBinding = + SessionDataPlayerBinding() diff --git a/ExoPlayerAdapter/src/exo-analytics-just2_16/java/com/mux/stats/sdk/muxstats/AnalyticsListenerMetrics.kt b/ExoPlayerAdapter/src/exo-analytics-just2_16/java/com/mux/stats/sdk/muxstats/AnalyticsListenerMetrics.kt new file mode 100644 index 00000000..2472141b --- /dev/null +++ b/ExoPlayerAdapter/src/exo-analytics-just2_16/java/com/mux/stats/sdk/muxstats/AnalyticsListenerMetrics.kt @@ -0,0 +1,38 @@ +package com.mux.stats.sdk.muxstats + +import com.google.android.exoplayer2.ExoPlayer +import com.google.android.exoplayer2.analytics.AnalyticsListener +import com.mux.stats.sdk.core.util.MuxLogger +import com.mux.stats.sdk.muxstats.internal.exoAnalyticsListener +import com.mux.stats.sdk.muxstats.internal.logTag +import com.mux.stats.sdk.muxstats.internal.watchContentPosition + +/** + * Binding to an ExoPlayer using AnalyticsListener + */ +private class AnalyticsListenerBinding216ToNow : MuxPlayerAdapter.PlayerBinding { + + private var listener: AnalyticsListener? = null//by weak(null) + + init { + MuxLogger.d(logTag(), "created"); + } + + override fun bindPlayer(player: ExoPlayer, collector: MuxStateCollector) { + listener = exoAnalyticsListener(player, collector).also { + player.addAnalyticsListener(it) + player.watchContentPosition(collector) + } + } + + override fun unbindPlayer(player: ExoPlayer, collector: MuxStateCollector) { + listener?.let { player.removeAnalyticsListener(it) } + collector.positionWatcher?.stop("unbound") + } + +} + + +@JvmSynthetic // Hides from java callers outside the module +internal fun analyticsListenerMetrics() + : MuxPlayerAdapter.PlayerBinding = AnalyticsListenerBinding216ToNow() diff --git a/ExoPlayerAdapter/src/exo-analytics-2_16-now/java/com/mux/stats/sdk/muxstats/SessionDataBindings.kt b/ExoPlayerAdapter/src/exo-analytics-just2_16/java/com/mux/stats/sdk/muxstats/SessionDataBindings.kt similarity index 100% rename from ExoPlayerAdapter/src/exo-analytics-2_16-now/java/com/mux/stats/sdk/muxstats/SessionDataBindings.kt rename to ExoPlayerAdapter/src/exo-analytics-just2_16/java/com/mux/stats/sdk/muxstats/SessionDataBindings.kt diff --git a/ExoPlayerAdapter/src/exo-analytics-listener-2_18-now/java/com/mux/stats/sdk/muxstats/internal/ExoAnalyticsListener.kt b/ExoPlayerAdapter/src/exo-analytics-listener-2_18-now/java/com/mux/stats/sdk/muxstats/internal/ExoAnalyticsListener.kt index 19d3255e..e4a4d2c8 100644 --- a/ExoPlayerAdapter/src/exo-analytics-listener-2_18-now/java/com/mux/stats/sdk/muxstats/internal/ExoAnalyticsListener.kt +++ b/ExoPlayerAdapter/src/exo-analytics-listener-2_18-now/java/com/mux/stats/sdk/muxstats/internal/ExoAnalyticsListener.kt @@ -73,12 +73,6 @@ private class ExoAnalyticsListener(player: ExoPlayer, val collector: MuxStateCol collector.seeking() } - @Suppress("OVERRIDE_DEPRECATION") // Not worth making a new variant over (deprecated 2.12) - override fun onSeekProcessed(eventTime: EventTime) { - // TODO: This the new way (over position discontinuity or guessing) so figure out how to use it - //collector.seeked(false) - } - override fun onTimelineChanged(eventTime: EventTime, reason: Int) { val player = player // strong reference during the listener call if (player != null && eventTime.timeline.windowCount > 0) { diff --git a/ExoPlayerAdapter/src/exo-collector-upto-2_10/java/com/mux/stats/sdk/muxstats/MuxStateCollector.kt b/ExoPlayerAdapter/src/exo-collector-upto-2_10/java/com/mux/stats/sdk/muxstats/MuxStateCollector.kt deleted file mode 100644 index e8ae5fe4..00000000 --- a/ExoPlayerAdapter/src/exo-collector-upto-2_10/java/com/mux/stats/sdk/muxstats/MuxStateCollector.kt +++ /dev/null @@ -1,19 +0,0 @@ -package com.mux.stats.sdk.muxstats - -import com.mux.stats.sdk.core.events.IEventDispatcher -import com.mux.stats.sdk.muxstats.MuxStateCollectorBase -import com.mux.stats.sdk.muxstats.MuxStats - -open class MuxStateCollector( - private val _muxStats: () -> MuxStats, - private val _dispatcher: IEventDispatcher, - private val _trackFirstFrameRendered: Boolean = true, -): MuxStateCollectorBase(_muxStats, _dispatcher, _trackFirstFrameRendered) { - override fun isLivePlayback(): Boolean { - return false; - } - - override fun parseManifestTag(tagName: String): String { - return "-1" - } -} \ No newline at end of file diff --git a/ExoPlayerAdapter/src/exo-error-upto-2_13/java/com/mux/stats/sdk/muxstats/ExoErrorMetricsUpTo214.kt b/ExoPlayerAdapter/src/exo-error-upto-2_13/java/com/mux/stats/sdk/muxstats/ExoErrorMetricsUpTo214.kt deleted file mode 100644 index 4b69cf56..00000000 --- a/ExoPlayerAdapter/src/exo-error-upto-2_13/java/com/mux/stats/sdk/muxstats/ExoErrorMetricsUpTo214.kt +++ /dev/null @@ -1,49 +0,0 @@ -package com.mux.stats.sdk.muxstats - -import com.google.android.exoplayer2.ExoPlaybackException -import com.google.android.exoplayer2.ExoPlayer -import com.google.android.exoplayer2.Player -import com.mux.stats.sdk.core.util.MuxLogger -import com.mux.stats.sdk.muxstats.internal.handleExoPlaybackException -import com.mux.stats.sdk.muxstats.internal.logTag -import com.mux.stats.sdk.muxstats.internal.weak - -/** - * Player binding for exoplayer android metrics - * This implementation works up until 2.14.1 - * - * NOTE: This is only used on ExoPlayer versions 2.15 and below. AnalyticsListener is preferred, - * and its presence is guaranteed on our player object in exo 2.16 and higher - */ -private class ExoErrorMetricsByListenerUpTo213 : MuxPlayerAdapter.PlayerBinding { - private var playerListener: Player.EventListener? by weak(null) - - init { - MuxLogger.d(logTag(), "created"); - } - - override fun bindPlayer(player: ExoPlayer, collector: MuxStateCollector) { - playerListener = newListener(collector).also { player.addListener(it) } - } - - override fun unbindPlayer(player: ExoPlayer, collector: MuxStateCollector) { - collector.positionWatcher?.stop("player unbound") - collector.positionWatcher = null - playerListener?.let { player.removeListener(it) } - } - - private fun newListener(collector: MuxStateCollector) = ErrorPlayerListenerUpTo214(collector) -} // class ErrorPlayerBuListenerUpTo214 - -private class ErrorPlayerListenerUpTo214(val collector: MuxStateCollector) : Player.EventListener { - override fun onPlayerError(error: ExoPlaybackException) { - collector.handleExoPlaybackException(error.type, error) - } -} - -/** - * Generates a player binding for exoplayer error metrics. - */ -@JvmSynthetic -internal fun playerErrorMetrics(): MuxPlayerAdapter.PlayerBinding = - ExoErrorMetricsByListenerUpTo213(); diff --git a/ExoPlayerAdapter/src/exo-event-upto-2_13/java/com/mux/stats/sdk/muxstats/BasicExoMetricsUpTo213.kt b/ExoPlayerAdapter/src/exo-event-upto-2_13/java/com/mux/stats/sdk/muxstats/BasicExoMetricsUpTo213.kt deleted file mode 100644 index 51553f5e..00000000 --- a/ExoPlayerAdapter/src/exo-event-upto-2_13/java/com/mux/stats/sdk/muxstats/BasicExoMetricsUpTo213.kt +++ /dev/null @@ -1,77 +0,0 @@ -package com.mux.stats.sdk.muxstats - -import com.google.android.exoplayer2.ExoPlayer -import com.google.android.exoplayer2.Player -import com.google.android.exoplayer2.Timeline -import com.google.android.exoplayer2.source.TrackGroupArray -import com.google.android.exoplayer2.trackselection.TrackSelectionArray -import com.mux.stats.sdk.core.util.MuxLogger -import com.mux.stats.sdk.muxstats.internal.* - -/** - * Player binding for basic ExoPlayer metrics. - * This implementation works on ExoPlayer up to 2.13.0 (as of 4/22/22) - */ -private class BasicExoMetricsUpTo213 : MuxPlayerAdapter.PlayerBinding { - - private var playerListener: Player.EventListener? by weak(null) - - init { - MuxLogger.d(logTag(), "created"); - } - - override fun bindPlayer(player: ExoPlayer, collector: MuxStateCollector) { - playerListener = newListener(player, collector).also { - player.addListener(it) - } - } - - override fun unbindPlayer(player: ExoPlayer, collector: MuxStateCollector) { - collector.positionWatcher?.stop("player unbound") - collector.positionWatcher = null - playerListener?.let { player.removeListener(it) } - } - - private fun newListener(player: ExoPlayer, collector: MuxStateCollector) = - PlayerListener(player, collector) -} // class BaseExoMetrics - -private class PlayerListener(player: ExoPlayer, val collector: MuxStateCollector) : - Player.EventListener { - val player by weak(player) // player should be weakly reachable in case user doesn't clean up - -// override fun onPlaybackStateChanged(playbackState: Int) { -// // We rely on the player's playWhenReady because the order of this callback and its callback -// // is not well-defined -// player?.let { collector.handleExoPlaybackState(playbackState, it.playWhenReady) } -// } - - override fun onPlayerStateChanged(playWhenReady: Boolean, playbackState: Int) { - player?.let { collector.handleExoPlaybackState(playbackState, it.playWhenReady) } - } - - override fun onPositionDiscontinuity(reason: Int) { - collector.handlePositionDiscontinuity(reason) - } - - override fun onTimelineChanged(timeline: Timeline, manifest: Any?, reason: Int) { - timeline.takeIf { it.windowCount > 0 }?.let { tl -> - val window = Timeline.Window().apply { tl.getWindow(0, this) } - collector.sourceDurationMs = window.durationMs - } - } - - override fun onTracksChanged( - trackGroups: TrackGroupArray, - trackSelections: TrackSelectionArray - ) { - player?.watchContentPosition(collector) - } -} // class PlayerListener - -/** - * Generates a player binding for exoplayer error metrics. - */ -@JvmSynthetic -internal fun basicExoEvents(): MuxPlayerAdapter.PlayerBinding = - BasicExoMetricsUpTo213(); diff --git a/ExoPlayerAdapter/src/exo-mediaitem-upto-2_13/java/com/mux/stats/sdk/muxstats/ExoPlayerExt.kt b/ExoPlayerAdapter/src/exo-mediaitem-upto-2_13/java/com/mux/stats/sdk/muxstats/ExoPlayerExt.kt deleted file mode 100644 index 5c9a7516..00000000 --- a/ExoPlayerAdapter/src/exo-mediaitem-upto-2_13/java/com/mux/stats/sdk/muxstats/ExoPlayerExt.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.mux.stats.sdk.muxstats - -import com.google.android.exoplayer2.ExoPlayer - -/* - * # ExoPlayerExt.kt: Useful extensions on [ExoPlayer]. - */ - -/** - * This method is not supported for this version of ExoPlayer, and it always returns null - */ -@Suppress("unused") -fun ExoPlayer.getAdTagUrl(): String? = null diff --git a/MuxExoPlayer/build.gradle b/MuxExoPlayer/build.gradle index 3003fb87..2da80d5b 100644 --- a/MuxExoPlayer/build.gradle +++ b/MuxExoPlayer/build.gradle @@ -78,7 +78,11 @@ android { } r2_18_1 { dimension 'exoplayer' - matchingFallbacks = ['from2_18toNow'] + matchingFallbacks = ['just2_18'] + } + r2_19_1 { + dimension 'exoplayer' + matchingFallbacks = ['just2_19'] } } //productFlavors @@ -125,7 +129,9 @@ dependencies { amznPortApi "com.amazon.android:exoplayer:2.16.1" //noinspection GradleDynamicVersion,GradleDependency r2_17_1Api 'com.google.android.exoplayer:exoplayer:2.17.1' + //noinspection GradleDynamicVersion,GradleDependency r2_18_1Api 'com.google.android.exoplayer:exoplayer:2.18.1' + r2_19_1Api 'com.google.android.exoplayer:exoplayer:2.19.1' //noinspection GradleDynamicVersion,GradleDependency compileOnly 'com.google.ads.interactivemedia.v3:interactivemedia:3.29.0' diff --git a/automatedtests/build.gradle b/automatedtests/build.gradle index 0e2ab29d..e324bb98 100644 --- a/automatedtests/build.gradle +++ b/automatedtests/build.gradle @@ -68,7 +68,11 @@ android { } r2_18_1 { dimension 'exoplayer' - matchingFallbacks = ['from2_18toNow'] + matchingFallbacks = ['just2_18'] + } + r2_19_1 { + dimension 'exoplayer' + matchingFallbacks = ['just2_19'] } } variantFilter { // this app doesn't need a release build, so save some resources @@ -104,11 +108,14 @@ dependencies { // Optional -- UI testing with UI Automator androidTestImplementation 'androidx.test.uiautomator:uiautomator:2.2.0' androidTestImplementation 'androidx.test.ext:junit:1.1.2' - androidTestR2_14_1Implementation 'com.google.ads.interactivemedia.v3:interactivemedia:3.23.0' - androidTestR2_15_1Implementation 'com.google.ads.interactivemedia.v3:interactivemedia:3.23.0' - androidTestR2_16_1Implementation 'com.google.ads.interactivemedia.v3:interactivemedia:3.23.0' - androidTestR2_17_1Implementation 'com.google.ads.interactivemedia.v3:interactivemedia:3.23.0' - androidTestR2_18_1Implementation 'com.google.ads.interactivemedia.v3:interactivemedia:3.23.0' + + androidTestR2_14_1Implementation 'com.google.ads.interactivemedia.v3:interactivemedia:3.29.0' + androidTestR2_15_1Implementation 'com.google.ads.interactivemedia.v3:interactivemedia:3.29.0' + androidTestR2_16_1Implementation 'com.google.ads.interactivemedia.v3:interactivemedia:3.29.0' + androidTestR2_17_1Implementation 'com.google.ads.interactivemedia.v3:interactivemedia:3.29.0' + +// androidTestR2_18_1Implementation 'com.google.ads.interactivemedia.v3:interactivemedia:3.29.0' +// androidTestR2_19_1Implementation 'com.google.ads.interactivemedia.v3:interactivemedia:3.29.0' api 'org.checkerframework:checker-qual:3.33.0' @@ -126,6 +133,11 @@ dependencies { r2_18_1Api 'com.google.android.exoplayer:exoplayer:2.18.1' r2_18_1Api 'com.google.android.exoplayer:extension-mediasession:2.18.1' + r2_18_1Api 'com.google.android.exoplayer:extension-ima:2.18.1' + + r2_19_1Api 'com.google.android.exoplayer:exoplayer:2.19.1' + r2_19_1Api 'com.google.android.exoplayer:extension-mediasession:2.19.1' + r2_19_1Api 'com.google.android.exoplayer:extension-ima:2.19.1' // Automated tests should always test the local module and not the maven dependency. implementation project(":MuxExoPlayer") diff --git a/automatedtests/src/androidTestR2_10_6/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java b/automatedtests/src/androidTestR2_10_6/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java deleted file mode 100644 index e2b463c8..00000000 --- a/automatedtests/src/androidTestR2_10_6/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java +++ /dev/null @@ -1,1509 +0,0 @@ -/* - * Copyright (C) 2017 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.google.android.exoplayer2.ext.ima; - -import android.content.Context; -import android.net.Uri; -import android.os.Looper; -import android.os.SystemClock; -import android.view.View; -import android.view.ViewGroup; -import androidx.annotation.IntDef; -import androidx.annotation.Nullable; -import androidx.annotation.VisibleForTesting; -import com.google.ads.interactivemedia.v3.api.Ad; -import com.google.ads.interactivemedia.v3.api.AdDisplayContainer; -import com.google.ads.interactivemedia.v3.api.AdError; -import com.google.ads.interactivemedia.v3.api.AdError.AdErrorCode; -import com.google.ads.interactivemedia.v3.api.AdErrorEvent; -import com.google.ads.interactivemedia.v3.api.AdErrorEvent.AdErrorListener; -import com.google.ads.interactivemedia.v3.api.AdEvent; -import com.google.ads.interactivemedia.v3.api.AdEvent.AdEventListener; -import com.google.ads.interactivemedia.v3.api.AdEvent.AdEventType; -import com.google.ads.interactivemedia.v3.api.AdPodInfo; -import com.google.ads.interactivemedia.v3.api.AdsLoader.AdsLoadedListener; -import com.google.ads.interactivemedia.v3.api.AdsManager; -import com.google.ads.interactivemedia.v3.api.AdsManagerLoadedEvent; -import com.google.ads.interactivemedia.v3.api.AdsRenderingSettings; -import com.google.ads.interactivemedia.v3.api.AdsRequest; -import com.google.ads.interactivemedia.v3.api.CompanionAdSlot; -import com.google.ads.interactivemedia.v3.api.ImaSdkFactory; -import com.google.ads.interactivemedia.v3.api.ImaSdkSettings; -import com.google.ads.interactivemedia.v3.api.UiElement; -import com.google.ads.interactivemedia.v3.api.player.ContentProgressProvider; -import com.google.ads.interactivemedia.v3.api.player.VideoAdPlayer; -import com.google.ads.interactivemedia.v3.api.player.VideoProgressUpdate; -import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.ExoPlaybackException; -import com.google.android.exoplayer2.ExoPlayerLibraryInfo; -import com.google.android.exoplayer2.Player; -import com.google.android.exoplayer2.Timeline; -import com.google.android.exoplayer2.source.ads.AdPlaybackState; -import com.google.android.exoplayer2.source.ads.AdPlaybackState.AdState; -import com.google.android.exoplayer2.source.ads.AdsLoader; -import com.google.android.exoplayer2.source.ads.AdsMediaSource.AdLoadException; -import com.google.android.exoplayer2.trackselection.TrackSelectionArray; -import com.google.android.exoplayer2.upstream.DataSpec; -import com.google.android.exoplayer2.util.Assertions; -import com.google.android.exoplayer2.util.Log; -import com.google.android.exoplayer2.util.MimeTypes; -import com.google.android.exoplayer2.util.Util; -import java.io.IOException; -import java.lang.annotation.Documented; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; - -/** - * {@link AdsLoader} using the IMA SDK. All methods must be called on the main thread. - * - *

The player instance that will play the loaded ads must be set before playback using {@link - * #setPlayer(Player)}. If the ads loader is no longer required, it must be released by calling - * {@link #release()}. - * - *

The IMA SDK can take into account video control overlay views when calculating ad - * viewability. For more details see {@link AdDisplayContainer#registerVideoControlsOverlay(View)} - * and {@link AdViewProvider#getAdOverlayViews()}. - */ -public final class ImaAdsLoader - implements Player.EventListener, - AdsLoader, - VideoAdPlayer, - ContentProgressProvider, - AdErrorListener, - AdsLoadedListener, - AdEventListener { - - static { - ExoPlayerLibraryInfo.registerModule("goog.exo.ima"); - } - - /** - * Builder for {@link ImaAdsLoader}. - */ - public static final class Builder { - - private final Context context; - - @Nullable - private ImaSdkSettings imaSdkSettings; - @Nullable - private AdEventListener adEventListener; - @Nullable - private Set adUiElements; - private int vastLoadTimeoutMs; - private int mediaLoadTimeoutMs; - private int mediaBitrate; - private boolean focusSkipButtonWhenAvailable; - private ImaFactory imaFactory; - - /** - * Creates a new builder for {@link ImaAdsLoader}. - * - * @param context The context; - */ - public Builder(Context context) { - this.context = Assertions.checkNotNull(context); - vastLoadTimeoutMs = TIMEOUT_UNSET; - mediaLoadTimeoutMs = TIMEOUT_UNSET; - mediaBitrate = BITRATE_UNSET; - focusSkipButtonWhenAvailable = true; - imaFactory = new DefaultImaFactory(); - } - - /** - * Sets the IMA SDK settings. The provided settings instance's player type and version fields - * may be overwritten. - * - *

If this method is not called the default settings will be used. - * - * @param imaSdkSettings The {@link ImaSdkSettings}. - * @return This builder, for convenience. - */ - public Builder setImaSdkSettings(ImaSdkSettings imaSdkSettings) { - this.imaSdkSettings = Assertions.checkNotNull(imaSdkSettings); - return this; - } - - /** - * Sets a listener for ad events that will be passed to {@link AdsManager#addAdEventListener(AdEventListener)}. - * - * @param adEventListener The ad event listener. - * @return This builder, for convenience. - */ - public Builder setAdEventListener(AdEventListener adEventListener) { - this.adEventListener = Assertions.checkNotNull(adEventListener); - return this; - } - - /** - * Sets the ad UI elements to be rendered by the IMA SDK. - * - * @param adUiElements The ad UI elements to be rendered by the IMA SDK. - * @return This builder, for convenience. - * @see AdsRenderingSettings#setUiElements(Set) - */ - public Builder setAdUiElements(Set adUiElements) { - this.adUiElements = new HashSet<>(Assertions.checkNotNull(adUiElements)); - return this; - } - - /** - * Sets the VAST load timeout, in milliseconds. - * - * @param vastLoadTimeoutMs The VAST load timeout, in milliseconds. - * @return This builder, for convenience. - * @see AdsRequest#setVastLoadTimeout(float) - */ - public Builder setVastLoadTimeoutMs(int vastLoadTimeoutMs) { - Assertions.checkArgument(vastLoadTimeoutMs > 0); - this.vastLoadTimeoutMs = vastLoadTimeoutMs; - return this; - } - - /** - * Sets the ad media load timeout, in milliseconds. - * - * @param mediaLoadTimeoutMs The ad media load timeout, in milliseconds. - * @return This builder, for convenience. - * @see AdsRenderingSettings#setLoadVideoTimeout(int) - */ - public Builder setMediaLoadTimeoutMs(int mediaLoadTimeoutMs) { - Assertions.checkArgument(mediaLoadTimeoutMs > 0); - this.mediaLoadTimeoutMs = mediaLoadTimeoutMs; - return this; - } - - /** - * Sets the media maximum recommended bitrate for ads, in bps. - * - * @param bitrate The media maximum recommended bitrate for ads, in bps. - * @return This builder, for convenience. - * @see AdsRenderingSettings#setBitrateKbps(int) - */ - public Builder setMaxMediaBitrate(int bitrate) { - Assertions.checkArgument(bitrate > 0); - this.mediaBitrate = bitrate; - return this; - } - - /** - * Sets whether to focus the skip button (when available) on Android TV devices. The default - * setting is {@code true}. - * - * @param focusSkipButtonWhenAvailable Whether to focus the skip button (when available) on - * Android TV devices. - * @return This builder, for convenience. - * @see AdsRenderingSettings#setFocusSkipButtonWhenAvailable(boolean) - */ - public Builder setFocusSkipButtonWhenAvailable(boolean focusSkipButtonWhenAvailable) { - this.focusSkipButtonWhenAvailable = focusSkipButtonWhenAvailable; - return this; - } - - @VisibleForTesting - /* package */ Builder setImaFactory(ImaFactory imaFactory) { - this.imaFactory = Assertions.checkNotNull(imaFactory); - return this; - } - - /** - * Returns a new {@link ImaAdsLoader} for the specified ad tag. - * - * @param adTagUri The URI of a compatible ad tag to load. See https://developers.google.com/interactive-media-ads/docs/sdks/android/compatibility - * for information on compatible ad tags. - * @return The new {@link ImaAdsLoader}. - */ - public ImaAdsLoader buildForAdTag(Uri adTagUri) { - return new ImaAdsLoader( - context, - adTagUri, - imaSdkSettings, - null, - vastLoadTimeoutMs, - mediaLoadTimeoutMs, - mediaBitrate, - focusSkipButtonWhenAvailable, - adUiElements, - adEventListener, - imaFactory); - } - - /** - * Returns a new {@link ImaAdsLoader} with the specified sideloaded ads response. - * - * @param adsResponse The sideloaded VAST, VMAP, or ad rules response to be used instead of - * making a request via an ad tag URL. - * @return The new {@link ImaAdsLoader}. - */ - public ImaAdsLoader buildForAdsResponse(String adsResponse) { - return new ImaAdsLoader( - context, - null, - imaSdkSettings, - adsResponse, - vastLoadTimeoutMs, - mediaLoadTimeoutMs, - mediaBitrate, - focusSkipButtonWhenAvailable, - adUiElements, - adEventListener, - imaFactory); - } - } - - private static final boolean DEBUG = false; - private static final String TAG = "ImaAdsLoader"; - - /** - * Whether to enable preloading of ads in {@link AdsRenderingSettings}. - */ - private static final boolean ENABLE_PRELOADING = true; - - private static final String IMA_SDK_SETTINGS_PLAYER_TYPE = "google/exo.ext.ima"; - private static final String IMA_SDK_SETTINGS_PLAYER_VERSION = ExoPlayerLibraryInfo.VERSION; - - /** - * The value used in {@link VideoProgressUpdate}s to indicate an unset duration. - */ - private static final long IMA_DURATION_UNSET = -1L; - - /** - * Threshold before the end of content at which IMA is notified that content is complete if the - * player buffers, in milliseconds. - */ - private static final long END_OF_CONTENT_POSITION_THRESHOLD_MS = 5000; - - /** - * The maximum duration before an ad break that IMA may start preloading the next ad. - */ - private static final long MAXIMUM_PRELOAD_DURATION_MS = 8000; - - private static final int TIMEOUT_UNSET = -1; - private static final int BITRATE_UNSET = -1; - - /** - * The state of ad playback. - */ - @Documented - @Retention(RetentionPolicy.SOURCE) - @IntDef({IMA_AD_STATE_NONE, IMA_AD_STATE_PLAYING, IMA_AD_STATE_PAUSED}) - private @interface ImaAdState { - - } - - /** - * The ad playback state when IMA is not playing an ad. - */ - private static final int IMA_AD_STATE_NONE = 0; - /** - * The ad playback state when IMA has called {@link #playAd()} and not {@link #pauseAd()}. - */ - private static final int IMA_AD_STATE_PLAYING = 1; - /** - * The ad playback state when IMA has called {@link #pauseAd()} while playing an ad. - */ - private static final int IMA_AD_STATE_PAUSED = 2; - - private final @Nullable - Uri adTagUri; - private final @Nullable - String adsResponse; - private final int vastLoadTimeoutMs; - private final int mediaLoadTimeoutMs; - private final boolean focusSkipButtonWhenAvailable; - private final int mediaBitrate; - private final @Nullable - Set adUiElements; - private final @Nullable - AdEventListener adEventListener; - private final ImaFactory imaFactory; - private final Timeline.Period period; - private final List adCallbacks; - private final AdDisplayContainer adDisplayContainer; - private final com.google.ads.interactivemedia.v3.api.AdsLoader adsLoader; - - private boolean wasSetPlayerCalled; - @Nullable - private Player nextPlayer; - private Object pendingAdRequestContext; - private List supportedMimeTypes; - @Nullable - private EventListener eventListener; - @Nullable - private Player player; - private VideoProgressUpdate lastContentProgress; - private VideoProgressUpdate lastAdProgress; - private int lastVolumePercentage; - - private AdsManager adsManager; - private boolean initializedAdsManager; - private AdLoadException pendingAdLoadError; - private Timeline timeline; - private long contentDurationMs; - private int podIndexOffset; - private AdPlaybackState adPlaybackState; - - // Fields tracking IMA's state. - - /** - * The expected ad group index that IMA should load next. - */ - private int expectedAdGroupIndex; - /** - * The index of the current ad group that IMA is loading. - */ - private int adGroupIndex; - /** - * Whether IMA has sent an ad event to pause content since the last resume content event. - */ - private boolean imaPausedContent; - /** - * The current ad playback state. - */ - private @ImaAdState - int imaAdState; - /** - * Whether {@link com.google.ads.interactivemedia.v3.api.AdsLoader#contentComplete()} has been - * called since starting ad playback. - */ - private boolean sentContentComplete; - - // Fields tracking the player/loader state. - - /** - * Whether the player is playing an ad. - */ - private boolean playingAd; - /** - * If the player is playing an ad, stores the ad index in its ad group. {@link C#INDEX_UNSET} - * otherwise. - */ - private int playingAdIndexInAdGroup; - /** - * Whether there's a pending ad preparation error which IMA needs to be notified of when it - * transitions from playing content to playing the ad. - */ - private boolean shouldNotifyAdPrepareError; - /** - * If a content period has finished but IMA has not yet called {@link #playAd()}, stores the value - * of {@link SystemClock#elapsedRealtime()} when the content stopped playing. This can be used to - * determine a fake, increasing content position. {@link C#TIME_UNSET} otherwise. - */ - private long fakeContentProgressElapsedRealtimeMs; - /** - * If {@link #fakeContentProgressElapsedRealtimeMs} is set, stores the offset from which the - * content progress should increase. {@link C#TIME_UNSET} otherwise. - */ - private long fakeContentProgressOffsetMs; - /** - * Stores the pending content position when a seek operation was intercepted to play an ad. - */ - private long pendingContentPositionMs; - /** - * Whether {@link #getContentProgress()} has sent {@link #pendingContentPositionMs} to IMA. - */ - private boolean sentPendingContentPositionMs; - - /** - * Creates a new IMA ads loader. - * - *

If you need to customize the ad request, use {@link ImaAdsLoader.Builder} instead. - * - * @param context The context. - * @param adTagUri The {@link Uri} of an ad tag compatible with the Android IMA SDK. See - * https://developers.google.com/interactive-media-ads/docs/sdks/android/compatibility - * for more information. - */ - public ImaAdsLoader(Context context, Uri adTagUri) { - this( - context, - adTagUri, - /* imaSdkSettings= */ null, - /* adsResponse= */ null, - /* vastLoadTimeoutMs= */ TIMEOUT_UNSET, - /* mediaLoadTimeoutMs= */ TIMEOUT_UNSET, - /* mediaBitrate= */ BITRATE_UNSET, - /* focusSkipButtonWhenAvailable= */ true, - /* adUiElements= */ null, - /* adEventListener= */ null, - /* imaFactory= */ new DefaultImaFactory()); - } - - /** - * Creates a new IMA ads loader. - * - * @param context The context. - * @param adTagUri The {@link Uri} of an ad tag compatible with the Android IMA SDK. See - * https://developers.google.com/interactive-media-ads/docs/sdks/android/compatibility - * for more information. - * @param imaSdkSettings {@link ImaSdkSettings} used to configure the IMA SDK, or {@code null} to - * use the default settings. If set, the player type and version fields may - * be overwritten. - * @deprecated Use {@link ImaAdsLoader.Builder}. - */ - @Deprecated - public ImaAdsLoader(Context context, Uri adTagUri, ImaSdkSettings imaSdkSettings) { - this( - context, - adTagUri, - imaSdkSettings, - /* adsResponse= */ null, - /* vastLoadTimeoutMs= */ TIMEOUT_UNSET, - /* mediaLoadTimeoutMs= */ TIMEOUT_UNSET, - /* mediaBitrate= */ BITRATE_UNSET, - /* focusSkipButtonWhenAvailable= */ true, - /* adUiElements= */ null, - /* adEventListener= */ null, - /* imaFactory= */ new DefaultImaFactory()); - } - - private ImaAdsLoader( - Context context, - @Nullable Uri adTagUri, - @Nullable ImaSdkSettings imaSdkSettings, - @Nullable String adsResponse, - int vastLoadTimeoutMs, - int mediaLoadTimeoutMs, - int mediaBitrate, - boolean focusSkipButtonWhenAvailable, - @Nullable Set adUiElements, - @Nullable AdEventListener adEventListener, - ImaFactory imaFactory) { - Assertions.checkArgument(adTagUri != null || adsResponse != null); - this.adTagUri = adTagUri; - this.adsResponse = adsResponse; - this.vastLoadTimeoutMs = vastLoadTimeoutMs; - this.mediaLoadTimeoutMs = mediaLoadTimeoutMs; - this.mediaBitrate = mediaBitrate; - this.focusSkipButtonWhenAvailable = focusSkipButtonWhenAvailable; - this.adUiElements = adUiElements; - this.adEventListener = adEventListener; - this.imaFactory = imaFactory; - if (imaSdkSettings == null) { - imaSdkSettings = imaFactory.createImaSdkSettings(); - if (DEBUG) { - imaSdkSettings.setDebugMode(true); - } - } - imaSdkSettings.setPlayerType(IMA_SDK_SETTINGS_PLAYER_TYPE); - imaSdkSettings.setPlayerVersion(IMA_SDK_SETTINGS_PLAYER_VERSION); - period = new Timeline.Period(); - adCallbacks = new ArrayList<>(/* initialCapacity= */ 1); - adDisplayContainer = imaFactory.createAdDisplayContainer(); - adDisplayContainer.setPlayer(/* videoAdPlayer= */ this); - adsLoader = imaFactory.createAdsLoader(context, imaSdkSettings, adDisplayContainer); - adsLoader.addAdErrorListener(/* adErrorListener= */ this); - adsLoader.addAdsLoadedListener(/* adsLoadedListener= */ this); - fakeContentProgressElapsedRealtimeMs = C.TIME_UNSET; - fakeContentProgressOffsetMs = C.TIME_UNSET; - pendingContentPositionMs = C.TIME_UNSET; - adGroupIndex = C.INDEX_UNSET; - contentDurationMs = C.TIME_UNSET; - timeline = Timeline.EMPTY; - } - - /** - * Returns the underlying {@code com.google.ads.interactivemedia.v3.api.AdsLoader} wrapped by this - * instance. - */ - public com.google.ads.interactivemedia.v3.api.AdsLoader getAdsLoader() { - return adsLoader; - } - - /** - * Returns the {@link AdDisplayContainer} used by this loader. - * - *

Note: any video controls overlays registered via {@link - * AdDisplayContainer#registerVideoControlsOverlay(View)} will be unregistered automatically when - * the media source detaches from this instance. It is therefore necessary to re-register views - * each time the ads loader is reused. Alternatively, provide overlay views via the {@link - * AdsLoader.AdViewProvider} when creating the media source to benefit from automatic - * registration. - */ - public AdDisplayContainer getAdDisplayContainer() { - return adDisplayContainer; - } - - /** - * Sets the slots for displaying companion ads. Individual slots can be created using {@link - * ImaSdkFactory#createCompanionAdSlot()}. - * - * @param companionSlots Slots for displaying companion ads. - * @see AdDisplayContainer#setCompanionSlots(Collection) - * @deprecated Use {@code getAdDisplayContainer().setCompanionSlots(...)}. - */ - @Deprecated - public void setCompanionSlots(Collection companionSlots) { - adDisplayContainer.setCompanionSlots(companionSlots); - } - - /** - * Requests ads, if they have not already been requested. Must be called on the main thread. - * - *

Ads will be requested automatically when the player is prepared if this method has not been - * called, so it is only necessary to call this method if you want to request ads before preparing - * the player. - * - * @param adViewGroup A {@link ViewGroup} on top of the player that will show any ad UI. - */ - public void requestAds(ViewGroup adViewGroup) { - if (adPlaybackState != null || adsManager != null || pendingAdRequestContext != null) { - // Ads have already been requested. - return; - } - adDisplayContainer.setAdContainer(adViewGroup); - pendingAdRequestContext = new Object(); - AdsRequest request = imaFactory.createAdsRequest(); - if (adTagUri != null) { - request.setAdTagUrl(adTagUri.toString()); - } else /* adsResponse != null */ { - request.setAdsResponse(adsResponse); - } - if (vastLoadTimeoutMs != TIMEOUT_UNSET) { - request.setVastLoadTimeout(vastLoadTimeoutMs); - } - request.setContentProgressProvider(this); - request.setUserRequestContext(pendingAdRequestContext); - adsLoader.requestAds(request); - } - - // AdsLoader implementation. - - @Override - public void setPlayer(@Nullable Player player) { - Assertions.checkState(Looper.getMainLooper() == Looper.myLooper()); - Assertions.checkState( - player == null || player.getApplicationLooper() == Looper.getMainLooper()); - nextPlayer = player; - wasSetPlayerCalled = true; - } - - @Override - public void setSupportedContentTypes(@C.ContentType int... contentTypes) { - List supportedMimeTypes = new ArrayList<>(); - for (@C.ContentType int contentType : contentTypes) { - if (contentType == C.TYPE_DASH) { - supportedMimeTypes.add(MimeTypes.APPLICATION_MPD); - } else if (contentType == C.TYPE_HLS) { - supportedMimeTypes.add(MimeTypes.APPLICATION_M3U8); - } else if (contentType == C.TYPE_OTHER) { - supportedMimeTypes.addAll( - Arrays.asList( - MimeTypes.VIDEO_MP4, - MimeTypes.VIDEO_WEBM, - MimeTypes.VIDEO_H263, - MimeTypes.AUDIO_MP4, - MimeTypes.AUDIO_MPEG)); - } else if (contentType == C.TYPE_SS) { - // IMA does not support Smooth Streaming ad media. - } - } - this.supportedMimeTypes = Collections.unmodifiableList(supportedMimeTypes); - } - - @Override - public void start(EventListener eventListener, AdViewProvider adViewProvider) { - Assertions.checkState( - wasSetPlayerCalled, "Set player using adsLoader.setPlayer before preparing the player."); - player = nextPlayer; - if (player == null) { - return; - } - this.eventListener = eventListener; - lastVolumePercentage = 0; - lastAdProgress = null; - lastContentProgress = null; - ViewGroup adViewGroup = adViewProvider.getAdViewGroup(); - adDisplayContainer.setAdContainer(adViewGroup); - View[] adOverlayViews = adViewProvider.getAdOverlayViews(); - for (View view : adOverlayViews) { - adDisplayContainer.registerVideoControlsOverlay(view); - } - player.addListener(this); - maybeNotifyPendingAdLoadError(); - if (adPlaybackState != null) { - // Pass the ad playback state to the player, and resume ads if necessary. - eventListener.onAdPlaybackState(adPlaybackState); - if (imaPausedContent && player.getPlayWhenReady()) { - adsManager.resume(); - } - } else if (adsManager != null) { - adPlaybackState = new AdPlaybackState(getAdGroupTimesUs(adsManager.getAdCuePoints())); - updateAdPlaybackState(); - } else { - // Ads haven't loaded yet, so request them. - requestAds(adViewGroup); - } - } - - @Override - public void stop() { - if (player == null) { - return; - } - if (adsManager != null && imaPausedContent) { - adPlaybackState = - adPlaybackState.withAdResumePositionUs( - playingAd ? C.msToUs(player.getCurrentPosition()) : 0); - adsManager.pause(); - } - lastVolumePercentage = getVolume(); - lastAdProgress = getAdProgress(); - lastContentProgress = getContentProgress(); - adDisplayContainer.unregisterAllVideoControlsOverlays(); - player.removeListener(this); - player = null; - eventListener = null; - } - - @Override - public void release() { - pendingAdRequestContext = null; - if (adsManager != null) { - adsManager.destroy(); - adsManager = null; - } - adsLoader.removeAdsLoadedListener(/* adsLoadedListener= */ this); - adsLoader.removeAdErrorListener(/* adErrorListener= */ this); - imaPausedContent = false; - imaAdState = IMA_AD_STATE_NONE; - pendingAdLoadError = null; - adPlaybackState = AdPlaybackState.NONE; - updateAdPlaybackState(); - } - - @Override - public void handlePrepareError(int adGroupIndex, int adIndexInAdGroup, IOException exception) { - if (player == null) { - return; - } - try { - handleAdPrepareError(adGroupIndex, adIndexInAdGroup, exception); - } catch (Exception e) { - maybeNotifyInternalError("handlePrepareError", e); - } - } - - // com.google.ads.interactivemedia.v3.api.AdsLoader.AdsLoadedListener implementation. - - @Override - public void onAdsManagerLoaded(AdsManagerLoadedEvent adsManagerLoadedEvent) { - AdsManager adsManager = adsManagerLoadedEvent.getAdsManager(); - if (!Util.areEqual(pendingAdRequestContext, adsManagerLoadedEvent.getUserRequestContext())) { - adsManager.destroy(); - return; - } - pendingAdRequestContext = null; - this.adsManager = adsManager; - adsManager.addAdErrorListener(this); - adsManager.addAdEventListener(this); - if (adEventListener != null) { - adsManager.addAdEventListener(adEventListener); - } - if (player != null) { - // If a player is attached already, start playback immediately. - try { - adPlaybackState = new AdPlaybackState(getAdGroupTimesUs(adsManager.getAdCuePoints())); - updateAdPlaybackState(); - } catch (Exception e) { - maybeNotifyInternalError("onAdsManagerLoaded", e); - } - } - } - - // AdEvent.AdEventListener implementation. - - @Override - public void onAdEvent(AdEvent adEvent) { - AdEventType adEventType = adEvent.getType(); - if (DEBUG) { - Log.d(TAG, "onAdEvent: " + adEventType); - } - if (adsManager == null) { - Log.w(TAG, "Ignoring AdEvent after release: " + adEvent); - return; - } - try { - handleAdEvent(adEvent); - } catch (Exception e) { - maybeNotifyInternalError("onAdEvent", e); - } - } - - // AdErrorEvent.AdErrorListener implementation. - - @Override - public void onAdError(AdErrorEvent adErrorEvent) { - AdError error = adErrorEvent.getError(); - if (DEBUG) { - Log.d(TAG, "onAdError", error); - } - if (adsManager == null) { - // No ads were loaded, so allow playback to start without any ads. - pendingAdRequestContext = null; - adPlaybackState = new AdPlaybackState(); - updateAdPlaybackState(); - } else if (isAdGroupLoadError(error)) { - try { - handleAdGroupLoadError(error); - } catch (Exception e) { - maybeNotifyInternalError("onAdError", e); - } - } - if (pendingAdLoadError == null) { - pendingAdLoadError = AdLoadException.createForAllAds(error); - } - maybeNotifyPendingAdLoadError(); - } - - // ContentProgressProvider implementation. - - @Override - public VideoProgressUpdate getContentProgress() { - if (player == null) { - return lastContentProgress; - } - boolean hasContentDuration = contentDurationMs != C.TIME_UNSET; - long contentPositionMs; - if (pendingContentPositionMs != C.TIME_UNSET) { - sentPendingContentPositionMs = true; - contentPositionMs = pendingContentPositionMs; - expectedAdGroupIndex = - adPlaybackState.getAdGroupIndexForPositionUs(C.msToUs(contentPositionMs)); - } else if (fakeContentProgressElapsedRealtimeMs != C.TIME_UNSET) { - long elapsedSinceEndMs = SystemClock.elapsedRealtime() - fakeContentProgressElapsedRealtimeMs; - contentPositionMs = fakeContentProgressOffsetMs + elapsedSinceEndMs; - expectedAdGroupIndex = - adPlaybackState.getAdGroupIndexForPositionUs(C.msToUs(contentPositionMs)); - } else if (imaAdState == IMA_AD_STATE_NONE && !playingAd && hasContentDuration) { - contentPositionMs = player.getCurrentPosition(); - // Update the expected ad group index for the current content position. The update is delayed - // until MAXIMUM_PRELOAD_DURATION_MS before the ad so that an ad group load error delivered - // just after an ad group isn't incorrectly attributed to the next ad group. - int nextAdGroupIndex = - adPlaybackState.getAdGroupIndexAfterPositionUs( - C.msToUs(contentPositionMs), C.msToUs(contentDurationMs)); - if (nextAdGroupIndex != expectedAdGroupIndex && nextAdGroupIndex != C.INDEX_UNSET) { - long nextAdGroupTimeMs = C.usToMs(adPlaybackState.adGroupTimesUs[nextAdGroupIndex]); - if (nextAdGroupTimeMs == C.TIME_END_OF_SOURCE) { - nextAdGroupTimeMs = contentDurationMs; - } - if (nextAdGroupTimeMs - contentPositionMs < MAXIMUM_PRELOAD_DURATION_MS) { - expectedAdGroupIndex = nextAdGroupIndex; - } - } - } else { - return VideoProgressUpdate.VIDEO_TIME_NOT_READY; - } - long contentDurationMs = hasContentDuration ? this.contentDurationMs : IMA_DURATION_UNSET; - return new VideoProgressUpdate(contentPositionMs, contentDurationMs); - } - - // VideoAdPlayer implementation. - - @Override - public VideoProgressUpdate getAdProgress() { - if (player == null) { - return lastAdProgress; - } else if (imaAdState != IMA_AD_STATE_NONE && playingAd) { - long adDuration = player.getDuration(); - return adDuration == C.TIME_UNSET ? VideoProgressUpdate.VIDEO_TIME_NOT_READY - : new VideoProgressUpdate(player.getCurrentPosition(), adDuration); - } else { - return VideoProgressUpdate.VIDEO_TIME_NOT_READY; - } - } - - @Override - public int getVolume() { - if (player == null) { - return lastVolumePercentage; - } - - Player.AudioComponent audioComponent = player.getAudioComponent(); - if (audioComponent != null) { - return (int) (audioComponent.getVolume() * 100); - } - - // Check for a selected track using an audio renderer. - TrackSelectionArray trackSelections = player.getCurrentTrackSelections(); - for (int i = 0; i < player.getRendererCount() && i < trackSelections.length; i++) { - if (player.getRendererType(i) == C.TRACK_TYPE_AUDIO && trackSelections.get(i) != null) { - return 100; - } - } - return 0; - } - - @Override - public void loadAd(String adUriString) { - try { - if (DEBUG) { - Log.d(TAG, "loadAd in ad group " + adGroupIndex); - } - if (adsManager == null) { - Log.w(TAG, "Ignoring loadAd after release"); - return; - } - if (adGroupIndex == C.INDEX_UNSET) { - Log.w( - TAG, - "Unexpected loadAd without LOADED event; assuming ad group index is actually " - + expectedAdGroupIndex); - adGroupIndex = expectedAdGroupIndex; - adsManager.start(); - } - int adIndexInAdGroup = getAdIndexInAdGroupToLoad(adGroupIndex); - if (adIndexInAdGroup == C.INDEX_UNSET) { - Log.w(TAG, "Unexpected loadAd in an ad group with no remaining unavailable ads"); - return; - } - adPlaybackState = - adPlaybackState.withAdUri(adGroupIndex, adIndexInAdGroup, Uri.parse(adUriString)); - updateAdPlaybackState(); - } catch (Exception e) { - maybeNotifyInternalError("loadAd", e); - } - } - - @Override - public void addCallback(VideoAdPlayerCallback videoAdPlayerCallback) { - adCallbacks.add(videoAdPlayerCallback); - } - - @Override - public void removeCallback(VideoAdPlayerCallback videoAdPlayerCallback) { - adCallbacks.remove(videoAdPlayerCallback); - } - - @Override - public void playAd() { - if (DEBUG) { - Log.d(TAG, "playAd"); - } - if (adsManager == null) { - Log.w(TAG, "Ignoring playAd after release"); - return; - } - switch (imaAdState) { - case IMA_AD_STATE_PLAYING: - // IMA does not always call stopAd before resuming content. - // See [Internal: b/38354028, b/63320878]. - Log.w(TAG, "Unexpected playAd without stopAd"); - break; - case IMA_AD_STATE_NONE: - // IMA is requesting to play the ad, so stop faking the content position. - fakeContentProgressElapsedRealtimeMs = C.TIME_UNSET; - fakeContentProgressOffsetMs = C.TIME_UNSET; - imaAdState = IMA_AD_STATE_PLAYING; - for (int i = 0; i < adCallbacks.size(); i++) { - adCallbacks.get(i).onPlay(); - } - if (shouldNotifyAdPrepareError) { - shouldNotifyAdPrepareError = false; - for (int i = 0; i < adCallbacks.size(); i++) { - adCallbacks.get(i).onError(); - } - } - break; - case IMA_AD_STATE_PAUSED: - imaAdState = IMA_AD_STATE_PLAYING; - for (int i = 0; i < adCallbacks.size(); i++) { - adCallbacks.get(i).onResume(); - } - break; - default: - throw new IllegalStateException(); - } - if (player == null) { - // Sometimes messages from IMA arrive after detaching the player. See [Internal: b/63801642]. - Log.w(TAG, "Unexpected playAd while detached"); - } else if (!player.getPlayWhenReady()) { - adsManager.pause(); - } - } - - @Override - public void stopAd() { - if (DEBUG) { - Log.d(TAG, "stopAd"); - } - if (adsManager == null) { - Log.w(TAG, "Ignoring stopAd after release"); - return; - } - if (player == null) { - // Sometimes messages from IMA arrive after detaching the player. See [Internal: b/63801642]. - Log.w(TAG, "Unexpected stopAd while detached"); - } - if (imaAdState == IMA_AD_STATE_NONE) { - Log.w(TAG, "Unexpected stopAd"); - return; - } - try { - stopAdInternal(); - } catch (Exception e) { - maybeNotifyInternalError("stopAd", e); - } - } - - @Override - public void pauseAd() { - if (DEBUG) { - Log.d(TAG, "pauseAd"); - } - if (imaAdState == IMA_AD_STATE_NONE) { - // This method is called after content is resumed. - return; - } - imaAdState = IMA_AD_STATE_PAUSED; - for (int i = 0; i < adCallbacks.size(); i++) { - adCallbacks.get(i).onPause(); - } - } - - @Override - public void resumeAd() { - // This method is never called. See [Internal: b/18931719]. - maybeNotifyInternalError("resumeAd", new IllegalStateException("Unexpected call to resumeAd")); - } - - // Player.EventListener implementation. - - @Override - public void onTimelineChanged( - Timeline timeline, @Nullable Object manifest, @Player.TimelineChangeReason int reason) { - if (timeline.isEmpty()) { - // The player is being reset or contains no media. - return; - } - Assertions.checkArgument(timeline.getPeriodCount() == 1); - this.timeline = timeline; - long contentDurationUs = timeline.getPeriod(0, period).durationUs; - contentDurationMs = C.usToMs(contentDurationUs); - if (contentDurationUs != C.TIME_UNSET) { - adPlaybackState = adPlaybackState.withContentDurationUs(contentDurationUs); - } - if (!initializedAdsManager && adsManager != null) { - initializedAdsManager = true; - initializeAdsManager(); - } - onPositionDiscontinuity(Player.DISCONTINUITY_REASON_INTERNAL); - } - - @Override - public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { - if (adsManager == null) { - return; - } - - if (imaAdState == IMA_AD_STATE_PLAYING && !playWhenReady) { - adsManager.pause(); - return; - } - - if (imaAdState == IMA_AD_STATE_PAUSED && playWhenReady) { - adsManager.resume(); - return; - } - - if (imaAdState == IMA_AD_STATE_NONE && playbackState == Player.STATE_BUFFERING - && playWhenReady) { - checkForContentComplete(); - } else if (imaAdState != IMA_AD_STATE_NONE && playbackState == Player.STATE_ENDED) { - for (int i = 0; i < adCallbacks.size(); i++) { - adCallbacks.get(i).onEnded(); - } - if (DEBUG) { - Log.d(TAG, "VideoAdPlayerCallback.onEnded in onPlayerStateChanged"); - } - } - } - - @Override - public void onPlayerError(ExoPlaybackException error) { - if (imaAdState != IMA_AD_STATE_NONE) { - for (int i = 0; i < adCallbacks.size(); i++) { - adCallbacks.get(i).onError(); - } - } - } - - @Override - public void onPositionDiscontinuity(@Player.DiscontinuityReason int reason) { - if (adsManager == null) { - return; - } - if (!playingAd && !player.isPlayingAd()) { - checkForContentComplete(); - if (sentContentComplete) { - for (int i = 0; i < adPlaybackState.adGroupCount; i++) { - if (adPlaybackState.adGroupTimesUs[i] != C.TIME_END_OF_SOURCE) { - adPlaybackState = adPlaybackState.withSkippedAdGroup(i); - } - } - updateAdPlaybackState(); - } else if (!timeline.isEmpty()) { - long positionMs = player.getCurrentPosition(); - timeline.getPeriod(0, period); - int newAdGroupIndex = period.getAdGroupIndexForPositionUs(C.msToUs(positionMs)); - if (newAdGroupIndex != C.INDEX_UNSET) { - sentPendingContentPositionMs = false; - pendingContentPositionMs = positionMs; - if (newAdGroupIndex != adGroupIndex) { - shouldNotifyAdPrepareError = false; - } - } - } - } - updateImaStateForPlayerState(); - } - - // Internal methods. - - private void initializeAdsManager() { - AdsRenderingSettings adsRenderingSettings = imaFactory.createAdsRenderingSettings(); - adsRenderingSettings.setEnablePreloading(ENABLE_PRELOADING); - adsRenderingSettings.setMimeTypes(supportedMimeTypes); - if (mediaLoadTimeoutMs != TIMEOUT_UNSET) { - adsRenderingSettings.setLoadVideoTimeout(mediaLoadTimeoutMs); - } - if (mediaBitrate != BITRATE_UNSET) { - adsRenderingSettings.setBitrateKbps(mediaBitrate / 1000); - } - adsRenderingSettings.setFocusSkipButtonWhenAvailable(focusSkipButtonWhenAvailable); - if (adUiElements != null) { - adsRenderingSettings.setUiElements(adUiElements); - } - - // Skip ads based on the start position as required. - long[] adGroupTimesUs = getAdGroupTimesUs(adsManager.getAdCuePoints()); - long contentPositionMs = player.getContentPosition(); - int adGroupIndexForPosition = - adPlaybackState.getAdGroupIndexForPositionUs(C.msToUs(contentPositionMs)); - if (adGroupIndexForPosition > 0 && adGroupIndexForPosition != C.INDEX_UNSET) { - // Skip any ad groups before the one at or immediately before the playback position. - for (int i = 0; i < adGroupIndexForPosition; i++) { - adPlaybackState = adPlaybackState.withSkippedAdGroup(i); - } - // Play ads after the midpoint between the ad to play and the one before it, to avoid issues - // with rounding one of the two ad times. - long adGroupForPositionTimeUs = adGroupTimesUs[adGroupIndexForPosition]; - long adGroupBeforeTimeUs = adGroupTimesUs[adGroupIndexForPosition - 1]; - double midpointTimeUs = (adGroupForPositionTimeUs + adGroupBeforeTimeUs) / 2d; - adsRenderingSettings.setPlayAdsAfterTime(midpointTimeUs / C.MICROS_PER_SECOND); - } - - // IMA indexes any remaining midroll ad pods from 1. A preroll (if present) has index 0. - // Store an index offset as we want to index all ads (including skipped ones) from 0. - if (adGroupIndexForPosition == 0 && adGroupTimesUs[0] == 0) { - // We are playing a preroll. - podIndexOffset = 0; - } else if (adGroupIndexForPosition == C.INDEX_UNSET) { - // There's no ad to play which means there's no preroll. - podIndexOffset = -1; - } else { - // We are playing a midroll and any ads before it were skipped. - podIndexOffset = adGroupIndexForPosition - 1; - } - - if (adGroupIndexForPosition != C.INDEX_UNSET && hasMidrollAdGroups(adGroupTimesUs)) { - // Provide the player's initial position to trigger loading and playing the ad. - pendingContentPositionMs = contentPositionMs; - } - - adsManager.init(adsRenderingSettings); - updateAdPlaybackState(); - if (DEBUG) { - Log.d(TAG, "Initialized with ads rendering settings: " + adsRenderingSettings); - } - } - - private void handleAdEvent(AdEvent adEvent) { - Ad ad = adEvent.getAd(); - switch (adEvent.getType()) { - case LOADED: - // The ad position is not always accurate when using preloading. See [Internal: b/62613240]. - AdPodInfo adPodInfo = ad.getAdPodInfo(); - int podIndex = adPodInfo.getPodIndex(); - adGroupIndex = - podIndex == -1 ? (adPlaybackState.adGroupCount - 1) : (podIndex + podIndexOffset); - int adPosition = adPodInfo.getAdPosition(); - int adCount = adPodInfo.getTotalAds(); - adsManager.start(); - if (DEBUG) { - Log.d(TAG, "Loaded ad " + adPosition + " of " + adCount + " in group " + adGroupIndex); - } - int oldAdCount = adPlaybackState.adGroups[adGroupIndex].count; - if (adCount != oldAdCount) { - if (oldAdCount == C.LENGTH_UNSET) { - adPlaybackState = adPlaybackState.withAdCount(adGroupIndex, adCount); - updateAdPlaybackState(); - } else { - // IMA sometimes unexpectedly decreases the ad count in an ad group. - Log.w(TAG, "Unexpected ad count in LOADED, " + adCount + ", expected " + oldAdCount); - } - } - if (adGroupIndex != expectedAdGroupIndex) { - Log.w( - TAG, - "Expected ad group index " - + expectedAdGroupIndex - + ", actual ad group index " - + adGroupIndex); - expectedAdGroupIndex = adGroupIndex; - } - break; - case CONTENT_PAUSE_REQUESTED: - // After CONTENT_PAUSE_REQUESTED, IMA will playAd/pauseAd/stopAd to show one or more ads - // before sending CONTENT_RESUME_REQUESTED. - imaPausedContent = true; - pauseContentInternal(); - break; - case TAPPED: - if (eventListener != null) { - eventListener.onAdTapped(); - } - break; - case CLICKED: - if (eventListener != null) { - eventListener.onAdClicked(); - } - break; - case CONTENT_RESUME_REQUESTED: - imaPausedContent = false; - resumeContentInternal(); - break; - case LOG: - Map adData = adEvent.getAdData(); - String message = "AdEvent: " + adData; - Log.i(TAG, message); - if ("adLoadError".equals(adData.get("type"))) { - handleAdGroupLoadError(new IOException(message)); - } - break; - case STARTED: - case ALL_ADS_COMPLETED: - default: - break; - } - } - - private void updateImaStateForPlayerState() { - boolean wasPlayingAd = playingAd; - int oldPlayingAdIndexInAdGroup = playingAdIndexInAdGroup; - playingAd = player.isPlayingAd(); - playingAdIndexInAdGroup = playingAd ? player.getCurrentAdIndexInAdGroup() : C.INDEX_UNSET; - boolean adFinished = wasPlayingAd && playingAdIndexInAdGroup != oldPlayingAdIndexInAdGroup; - if (adFinished) { - // IMA is waiting for the ad playback to finish so invoke the callback now. - // Either CONTENT_RESUME_REQUESTED will be passed next, or playAd will be called again. - for (int i = 0; i < adCallbacks.size(); i++) { - adCallbacks.get(i).onEnded(); - } - if (DEBUG) { - Log.d(TAG, "VideoAdPlayerCallback.onEnded in onTimelineChanged/onPositionDiscontinuity"); - } - } - if (!sentContentComplete && !wasPlayingAd && playingAd && imaAdState == IMA_AD_STATE_NONE) { - int adGroupIndex = player.getCurrentAdGroupIndex(); - // IMA hasn't called playAd yet, so fake the content position. - fakeContentProgressElapsedRealtimeMs = SystemClock.elapsedRealtime(); - fakeContentProgressOffsetMs = C.usToMs(adPlaybackState.adGroupTimesUs[adGroupIndex]); - if (fakeContentProgressOffsetMs == C.TIME_END_OF_SOURCE) { - fakeContentProgressOffsetMs = contentDurationMs; - } - } - } - - private void resumeContentInternal() { - if (imaAdState != IMA_AD_STATE_NONE) { - imaAdState = IMA_AD_STATE_NONE; - if (DEBUG) { - Log.d(TAG, "Unexpected CONTENT_RESUME_REQUESTED without stopAd"); - } - } - if (adGroupIndex != C.INDEX_UNSET) { - adPlaybackState = adPlaybackState.withSkippedAdGroup(adGroupIndex); - adGroupIndex = C.INDEX_UNSET; - updateAdPlaybackState(); - } - } - - private void pauseContentInternal() { - imaAdState = IMA_AD_STATE_NONE; - if (sentPendingContentPositionMs) { - pendingContentPositionMs = C.TIME_UNSET; - sentPendingContentPositionMs = false; - } - } - - private void stopAdInternal() { - imaAdState = IMA_AD_STATE_NONE; - int adIndexInAdGroup = adPlaybackState.adGroups[adGroupIndex].getFirstAdIndexToPlay(); - // TODO: Handle the skipped event so the ad can be marked as skipped rather than played. - adPlaybackState = - adPlaybackState.withPlayedAd(adGroupIndex, adIndexInAdGroup).withAdResumePositionUs(0); - updateAdPlaybackState(); - if (!playingAd) { - adGroupIndex = C.INDEX_UNSET; - } - } - - private void handleAdGroupLoadError(Exception error) { - int adGroupIndex = - this.adGroupIndex == C.INDEX_UNSET ? expectedAdGroupIndex : this.adGroupIndex; - if (adGroupIndex == C.INDEX_UNSET) { - // Drop the error, as we don't know which ad group it relates to. - return; - } - AdPlaybackState.AdGroup adGroup = adPlaybackState.adGroups[adGroupIndex]; - if (adGroup.count == C.LENGTH_UNSET) { - adPlaybackState = - adPlaybackState.withAdCount(adGroupIndex, Math.max(1, adGroup.states.length)); - adGroup = adPlaybackState.adGroups[adGroupIndex]; - } - for (int i = 0; i < adGroup.count; i++) { - if (adGroup.states[i] == AdPlaybackState.AD_STATE_UNAVAILABLE) { - if (DEBUG) { - Log.d(TAG, "Removing ad " + i + " in ad group " + adGroupIndex); - } - adPlaybackState = adPlaybackState.withAdLoadError(adGroupIndex, i); - } - } - updateAdPlaybackState(); - if (pendingAdLoadError == null) { - pendingAdLoadError = AdLoadException.createForAdGroup(error, adGroupIndex); - } - pendingContentPositionMs = C.TIME_UNSET; - fakeContentProgressElapsedRealtimeMs = C.TIME_UNSET; - } - - private void handleAdPrepareError(int adGroupIndex, int adIndexInAdGroup, Exception exception) { - if (DEBUG) { - Log.d( - TAG, "Prepare error for ad " + adIndexInAdGroup + " in group " + adGroupIndex, exception); - } - if (adsManager == null) { - Log.w(TAG, "Ignoring ad prepare error after release"); - return; - } - if (imaAdState == IMA_AD_STATE_NONE) { - // Send IMA a content position at the ad group so that it will try to play it, at which point - // we can notify that it failed to load. - fakeContentProgressElapsedRealtimeMs = SystemClock.elapsedRealtime(); - fakeContentProgressOffsetMs = C.usToMs(adPlaybackState.adGroupTimesUs[adGroupIndex]); - if (fakeContentProgressOffsetMs == C.TIME_END_OF_SOURCE) { - fakeContentProgressOffsetMs = contentDurationMs; - } - shouldNotifyAdPrepareError = true; - } else { - // We're already playing an ad. - if (adIndexInAdGroup > playingAdIndexInAdGroup) { - // Mark the playing ad as ended so we can notify the error on the next ad and remove it, - // which means that the ad after will load (if any). - for (int i = 0; i < adCallbacks.size(); i++) { - adCallbacks.get(i).onEnded(); - } - } - playingAdIndexInAdGroup = adPlaybackState.adGroups[adGroupIndex].getFirstAdIndexToPlay(); - for (int i = 0; i < adCallbacks.size(); i++) { - adCallbacks.get(i).onError(); - } - } - adPlaybackState = adPlaybackState.withAdLoadError(adGroupIndex, adIndexInAdGroup); - updateAdPlaybackState(); - } - - private void checkForContentComplete() { - if (contentDurationMs != C.TIME_UNSET && pendingContentPositionMs == C.TIME_UNSET - && player.getContentPosition() + END_OF_CONTENT_POSITION_THRESHOLD_MS >= contentDurationMs - && !sentContentComplete) { - adsLoader.contentComplete(); - if (DEBUG) { - Log.d(TAG, "adsLoader.contentComplete"); - } - sentContentComplete = true; - // After sending content complete IMA will not poll the content position, so set the expected - // ad group index. - expectedAdGroupIndex = - adPlaybackState.getAdGroupIndexForPositionUs(C.msToUs(contentDurationMs)); - } - } - - private void updateAdPlaybackState() { - // Ignore updates while detached. When a player is attached it will receive the latest state. - if (eventListener != null) { - eventListener.onAdPlaybackState(adPlaybackState); - } - } - - /** - * Returns the next ad index in the specified ad group to load, or {@link C#INDEX_UNSET} if all - * ads in the ad group have loaded. - */ - private int getAdIndexInAdGroupToLoad(int adGroupIndex) { - @AdState int[] states = adPlaybackState.adGroups[adGroupIndex].states; - int adIndexInAdGroup = 0; - // IMA loads ads in order. - while (adIndexInAdGroup < states.length - && states[adIndexInAdGroup] != AdPlaybackState.AD_STATE_UNAVAILABLE) { - adIndexInAdGroup++; - } - return adIndexInAdGroup == states.length ? C.INDEX_UNSET : adIndexInAdGroup; - } - - private void maybeNotifyPendingAdLoadError() { - if (pendingAdLoadError != null && eventListener != null) { - eventListener.onAdLoadError(pendingAdLoadError, new DataSpec(adTagUri)); - pendingAdLoadError = null; - } - } - - private void maybeNotifyInternalError(String name, Exception cause) { - String message = "Internal error in " + name; - Log.e(TAG, message, cause); - // We can't recover from an unexpected error in general, so skip all remaining ads. - if (adPlaybackState == null) { - adPlaybackState = AdPlaybackState.NONE; - } else { - for (int i = 0; i < adPlaybackState.adGroupCount; i++) { - adPlaybackState = adPlaybackState.withSkippedAdGroup(i); - } - } - updateAdPlaybackState(); - if (eventListener != null) { - eventListener.onAdLoadError( - AdLoadException.createForUnexpected(new RuntimeException(message, cause)), - new DataSpec(adTagUri)); - } - } - - private static long[] getAdGroupTimesUs(List cuePoints) { - if (cuePoints.isEmpty()) { - // If no cue points are specified, there is a preroll ad. - return new long[]{0}; - } - - int count = cuePoints.size(); - long[] adGroupTimesUs = new long[count]; - int adGroupIndex = 0; - for (int i = 0; i < count; i++) { - double cuePoint = cuePoints.get(i); - if (cuePoint == -1.0) { - adGroupTimesUs[count - 1] = C.TIME_END_OF_SOURCE; - } else { - adGroupTimesUs[adGroupIndex++] = (long) (C.MICROS_PER_SECOND * cuePoint); - } - } - // Cue points may be out of order, so sort them. - Arrays.sort(adGroupTimesUs, 0, adGroupIndex); - return adGroupTimesUs; - } - - private static boolean isAdGroupLoadError(AdError adError) { - // TODO: Find out what other errors need to be handled (if any), and whether each one relates to - // a single ad, ad group or the whole timeline. - return adError.getErrorCode() == AdErrorCode.VAST_LINEAR_ASSET_MISMATCH - || adError.getErrorCode() == AdErrorCode.UNKNOWN_ERROR; - } - - private static boolean hasMidrollAdGroups(long[] adGroupTimesUs) { - int count = adGroupTimesUs.length; - if (count == 1) { - return adGroupTimesUs[0] != 0 && adGroupTimesUs[0] != C.TIME_END_OF_SOURCE; - } else if (count == 2) { - return adGroupTimesUs[0] != 0 || adGroupTimesUs[1] != C.TIME_END_OF_SOURCE; - } else { - // There's at least one midroll ad group, as adGroupTimesUs is never empty. - return true; - } - } - - /** - * Factory for objects provided by the IMA SDK. - */ - @VisibleForTesting - /* package */ interface ImaFactory { - - /** - * @see ImaSdkSettings - */ - ImaSdkSettings createImaSdkSettings(); - - /** - * @see com.google.ads.interactivemedia.v3.api.ImaSdkFactory#createAdsRenderingSettings() - */ - AdsRenderingSettings createAdsRenderingSettings(); - - /** - * @see com.google.ads.interactivemedia.v3.api.ImaSdkFactory#createAdDisplayContainer() - */ - AdDisplayContainer createAdDisplayContainer(); - - /** - * @see com.google.ads.interactivemedia.v3.api.ImaSdkFactory#createAdsRequest() - */ - AdsRequest createAdsRequest(); - - /** - * @see ImaSdkFactory#createAdsLoader(Context, ImaSdkSettings, AdDisplayContainer) - */ - com.google.ads.interactivemedia.v3.api.AdsLoader createAdsLoader( - Context context, ImaSdkSettings imaSdkSettings, AdDisplayContainer adDisplayContainer); - } - - /** - * Default {@link ImaFactory} for non-test usage, which delegates to {@link ImaSdkFactory}. - */ - private static final class DefaultImaFactory implements ImaFactory { - - @Override - public ImaSdkSettings createImaSdkSettings() { - return ImaSdkFactory.getInstance().createImaSdkSettings(); - } - - @Override - public AdsRenderingSettings createAdsRenderingSettings() { - return ImaSdkFactory.getInstance().createAdsRenderingSettings(); - } - - @Override - public AdDisplayContainer createAdDisplayContainer() { - return ImaSdkFactory.getInstance().createAdDisplayContainer(); - } - - @Override - public AdsRequest createAdsRequest() { - return ImaSdkFactory.getInstance().createAdsRequest(); - } - - @Override - public com.google.ads.interactivemedia.v3.api.AdsLoader createAdsLoader( - Context context, ImaSdkSettings imaSdkSettings, AdDisplayContainer adDisplayContainer) { - return ImaSdkFactory.getInstance() - .createAdsLoader(context, imaSdkSettings, adDisplayContainer); - } - } -} diff --git a/automatedtests/src/androidTestR2_10_6/java/com/mux/stats/sdk/muxstats/automatedtests/ui/SimplePlayerTestActivity.java b/automatedtests/src/androidTestR2_10_6/java/com/mux/stats/sdk/muxstats/automatedtests/ui/SimplePlayerTestActivity.java deleted file mode 100644 index f56a5b54..00000000 --- a/automatedtests/src/androidTestR2_10_6/java/com/mux/stats/sdk/muxstats/automatedtests/ui/SimplePlayerTestActivity.java +++ /dev/null @@ -1,204 +0,0 @@ -package com.mux.stats.sdk.muxstats.automatedtests.ui; - - -import android.app.Notification; -import android.net.Uri; -import android.support.v4.media.session.MediaSessionCompat; -import android.util.Log; -import androidx.annotation.Nullable; -import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.DefaultRenderersFactory; -import com.google.android.exoplayer2.ExoPlayerFactory; -import com.google.android.exoplayer2.Player; -import com.google.android.exoplayer2.RenderersFactory; -import com.google.android.exoplayer2.SimpleExoPlayer; -import com.google.android.exoplayer2.ext.ima.ImaAdsLoader; -import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector; -import com.google.android.exoplayer2.source.MediaSource; -import com.google.android.exoplayer2.source.ProgressiveMediaSource; -import com.google.android.exoplayer2.source.ads.AdsLoader; -import com.google.android.exoplayer2.source.ads.AdsMediaSource; -import com.google.android.exoplayer2.source.dash.DashMediaSource; -import com.google.android.exoplayer2.source.hls.HlsMediaSource; -import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource; -import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection; -import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; -import com.google.android.exoplayer2.trackselection.TrackSelection; -import com.google.android.exoplayer2.ui.PlayerNotificationManager; -import com.google.android.exoplayer2.upstream.DataSource; -import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; -import com.google.android.exoplayer2.util.Util; -import com.mux.stats.sdk.muxstats.SimplePlayerBaseActivity; -import com.mux.stats.sdk.muxstats.automatedtests.R; -import java.lang.reflect.Constructor; - -public class SimplePlayerTestActivity extends SimplePlayerBaseActivity - implements Player.EventListener{ - - private boolean isShowingTrackSelectionDialog; - - public void initExoPlayer() { - // Hopfully this will not channge the track selection set programmatically - TrackSelection.Factory trackSelectionFactory = new AdaptiveTrackSelection.Factory( - AdaptiveTrackSelection.DEFAULT_MIN_DURATION_FOR_QUALITY_INCREASE_MS * 10, - AdaptiveTrackSelection.DEFAULT_MAX_DURATION_FOR_QUALITY_DECREASE_MS * 10, - AdaptiveTrackSelection.DEFAULT_MIN_DURATION_TO_RETAIN_AFTER_DISCARD_MS, - AdaptiveTrackSelection.DEFAULT_BANDWIDTH_FRACTION - ); - DefaultTrackSelector.Parameters trackSelectorParameters - = new DefaultTrackSelector.ParametersBuilder().build(); - trackSelector = new DefaultTrackSelector(trackSelectionFactory); - trackSelector.setParameters(trackSelectorParameters); - RenderersFactory renderersFactory = new DefaultRenderersFactory(/* context= */ this); - player = ExoPlayerFactory.newSimpleInstance(this, renderersFactory, trackSelector); - player.addListener(this); - } - - // This is for background playback, set appropriate notification and etc - public void initAudioSession() { - notificationManager = PlayerNotificationManager.createWithNotificationChannel( - getApplicationContext(), - PLAYBACK_CHANNEL_ID, - R.string.channel_name, - R.string.channel_description, - PLAYBACK_NOTIFICATION_ID, - new MDAdapter(), - new CustomNotificationListener() - ); - notificationManager.setUseNavigationActions(false); - notificationManager.setUseStopAction(true); - notificationManager.setPlayer(player); - - mediaSessionCompat = new MediaSessionCompat(this, "hello_world_media"); - notificationManager.setMediaSessionToken(mediaSessionCompat.getSessionToken()); - mediaSessionConnector = new MediaSessionConnector(mediaSessionCompat); - mediaSessionConnector.setPlayer(player); - } - - public DataSource.Factory buildDataSourceFactory() { - return new DefaultDataSourceFactory(this, "Android-automated_tests"); - } - - public MediaSource buildMediaSource(Uri uri, @Nullable String overrideExtension) { - DataSource.Factory dataSourceFactory = buildDataSourceFactory(); - @C.ContentType int type = Util.inferContentType(uri, overrideExtension); - switch (type) { - case C.TYPE_DASH: - return new DashMediaSource.Factory(dataSourceFactory).createMediaSource(uri); - case C.TYPE_SS: - return new SsMediaSource.Factory(dataSourceFactory).createMediaSource(uri); - case C.TYPE_HLS: - return new HlsMediaSource.Factory(dataSourceFactory).createMediaSource(uri); - case C.TYPE_OTHER: - return new ProgressiveMediaSource.Factory(dataSourceFactory).createMediaSource(uri); - default: - throw new IllegalStateException("Unsupported type: " + type); - } - } - - private MediaSource buildMediaSource(Uri uri) { - return buildMediaSource(uri, null); - } - - /** - * Returns an ads media source, reusing the ads loader if one exists. - */ - public MediaSource createAdsMediaSource(MediaSource mediaSource, Uri adTagUri) { - // Load the extension source using reflection so the demo app doesn't have to depend on it. - // The ads loader is reused for multiple playbacks, so that ad playback can resume. - try { - Class loaderClass = Class.forName("com.google.android.exoplayer2.ext.ima.ImaAdsLoader"); - if (adsLoader == null) { - // Full class names used so the LINT.IfChange rule triggers should any of the classes move. - // LINT.IfChange - Constructor loaderConstructor = - loaderClass - .asSubclass(AdsLoader.class) - .getConstructor(android.content.Context.class, android.net.Uri.class); - // LINT.ThenChange(../../../../../../../../proguard-rules.txt) - adsLoader = loaderConstructor.newInstance(this, adTagUri); - } - adsLoader.setPlayer(player); - AdsMediaSource.MediaSourceFactory adMediaSourceFactory = - new AdsMediaSource.MediaSourceFactory() { - @Override - public MediaSource createMediaSource(Uri uri) { - return SimplePlayerTestActivity.this.buildMediaSource(uri); - } - - @Override - public int[] getSupportedTypes() { - return new int[]{C.TYPE_DASH, C.TYPE_SS, C.TYPE_HLS, C.TYPE_OTHER}; - } - }; - - // Because of how this is loaded via reflection, we know that this will be - // a ImaAdsLoader, so cast it over so that we can get a reference to the - // real IMA AdsLoader instance. - muxStats.monitorImaAdsLoader(((ImaAdsLoader) adsLoader).getAdsLoader()); - return new AdsMediaSource(mediaSource, adMediaSourceFactory, adsLoader, playerView); - } catch (ClassNotFoundException e) { - // IMA extension not loaded. - return null; - } catch (Exception e) { - throw new RuntimeException(e); - } - } - - @Override - public void startPlayback() { - Uri testUri = Uri.parse(urlToPlay); - testMediaSource = buildMediaSource(testUri, null); - if (loadedAdTagUri != null) { - testMediaSource = createAdsMediaSource(testMediaSource, loadedAdTagUri); - } - - player.setPlayWhenReady(playWhenReady); - player.seekTo(playbackStartPosition); - ((SimpleExoPlayer)player).prepare(testMediaSource, false, true); - } - - // For background audio playback - class CustomNotificationListener implements PlayerNotificationManager.NotificationListener { - - @Override - public void onNotificationPosted(int notificationId, Notification notification, - boolean ongoing) { - // TODO implement this - Log.e(TAG, "onNotificationPosted"); - } - } - - ////////////////////////////////////////////////////////////////////// - ////// Player.EventListener ////////////////////////////////////////// - - @Override - public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { - switch (playbackState) { - case Player.STATE_BUFFERING: - signalPlaybackBuffering(); - break; - case Player.STATE_ENDED: - signalPlaybackEnded(); - break; - case Player.STATE_READY: - // By the time we get here, it depends on playWhenReady to know if we're playing - if (playWhenReady) { - signalPlaybackStarted(); - } else { - // TODO implement this -// signalPlaybackPaused(); - } - case Player.STATE_IDLE: - signalPlaybackStopped(); - break; - } - } - - @Override - public void onRepeatModeChanged(int repeatMode) { - activityLock.lock(); - activityInitialized.signalAll(); - activityLock.unlock(); - } -} diff --git a/automatedtests/src/androidTestR2_11_1/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java b/automatedtests/src/androidTestR2_11_1/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java deleted file mode 100644 index ac3cb4a4..00000000 --- a/automatedtests/src/androidTestR2_11_1/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java +++ /dev/null @@ -1,1513 +0,0 @@ -/* - * Copyright (C) 2017 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.google.android.exoplayer2.ext.ima; - -import android.content.Context; -import android.net.Uri; -import android.os.Looper; -import android.os.SystemClock; -import android.view.View; -import android.view.ViewGroup; -import androidx.annotation.IntDef; -import androidx.annotation.Nullable; -import androidx.annotation.VisibleForTesting; -import com.google.ads.interactivemedia.v3.api.Ad; -import com.google.ads.interactivemedia.v3.api.AdDisplayContainer; -import com.google.ads.interactivemedia.v3.api.AdError; -import com.google.ads.interactivemedia.v3.api.AdError.AdErrorCode; -import com.google.ads.interactivemedia.v3.api.AdErrorEvent; -import com.google.ads.interactivemedia.v3.api.AdErrorEvent.AdErrorListener; -import com.google.ads.interactivemedia.v3.api.AdEvent; -import com.google.ads.interactivemedia.v3.api.AdEvent.AdEventListener; -import com.google.ads.interactivemedia.v3.api.AdEvent.AdEventType; -import com.google.ads.interactivemedia.v3.api.AdPodInfo; -import com.google.ads.interactivemedia.v3.api.AdsLoader.AdsLoadedListener; -import com.google.ads.interactivemedia.v3.api.AdsManager; -import com.google.ads.interactivemedia.v3.api.AdsManagerLoadedEvent; -import com.google.ads.interactivemedia.v3.api.AdsRenderingSettings; -import com.google.ads.interactivemedia.v3.api.AdsRequest; -import com.google.ads.interactivemedia.v3.api.CompanionAdSlot; -import com.google.ads.interactivemedia.v3.api.ImaSdkFactory; -import com.google.ads.interactivemedia.v3.api.ImaSdkSettings; -import com.google.ads.interactivemedia.v3.api.UiElement; -import com.google.ads.interactivemedia.v3.api.player.ContentProgressProvider; -import com.google.ads.interactivemedia.v3.api.player.VideoAdPlayer; -import com.google.ads.interactivemedia.v3.api.player.VideoProgressUpdate; -import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.ExoPlaybackException; -import com.google.android.exoplayer2.ExoPlayerLibraryInfo; -import com.google.android.exoplayer2.Player; -import com.google.android.exoplayer2.Timeline; -import com.google.android.exoplayer2.source.ads.AdPlaybackState; -import com.google.android.exoplayer2.source.ads.AdPlaybackState.AdState; -import com.google.android.exoplayer2.source.ads.AdsLoader; -import com.google.android.exoplayer2.source.ads.AdsMediaSource.AdLoadException; -import com.google.android.exoplayer2.trackselection.TrackSelectionArray; -import com.google.android.exoplayer2.upstream.DataSpec; -import com.google.android.exoplayer2.util.Assertions; -import com.google.android.exoplayer2.util.Log; -import com.google.android.exoplayer2.util.MimeTypes; -import com.google.android.exoplayer2.util.Util; -import java.io.IOException; -import java.lang.annotation.Documented; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; - -/** - * {@link AdsLoader} using the IMA SDK. All methods must be called on the main thread. - * - *

The player instance that will play the loaded ads must be set before playback using {@link - * #setPlayer(Player)}. If the ads loader is no longer required, it must be released by calling - * {@link #release()}. - * - *

The IMA SDK can take into account video control overlay views when calculating ad - * viewability. For more details see {@link AdDisplayContainer#registerVideoControlsOverlay(View)} - * and {@link AdViewProvider#getAdOverlayViews()}. - */ -public final class ImaAdsLoader - implements Player.EventListener, - AdsLoader, - VideoAdPlayer, - ContentProgressProvider, - AdErrorListener, - AdsLoadedListener, - AdEventListener { - - static { - ExoPlayerLibraryInfo.registerModule("goog.exo.ima"); - } - - /** - * Builder for {@link ImaAdsLoader}. - */ - public static final class Builder { - - private final Context context; - - @Nullable - private ImaSdkSettings imaSdkSettings; - @Nullable - private AdEventListener adEventListener; - @Nullable - private Set adUiElements; - private int vastLoadTimeoutMs; - private int mediaLoadTimeoutMs; - private int mediaBitrate; - private boolean focusSkipButtonWhenAvailable; - private ImaFactory imaFactory; - - /** - * Creates a new builder for {@link ImaAdsLoader}. - * - * @param context The context; - */ - public Builder(Context context) { - this.context = Assertions.checkNotNull(context); - vastLoadTimeoutMs = TIMEOUT_UNSET; - mediaLoadTimeoutMs = TIMEOUT_UNSET; - mediaBitrate = BITRATE_UNSET; - focusSkipButtonWhenAvailable = true; - imaFactory = new DefaultImaFactory(); - } - - /** - * Sets the IMA SDK settings. The provided settings instance's player type and version fields - * may be overwritten. - * - *

If this method is not called the default settings will be used. - * - * @param imaSdkSettings The {@link ImaSdkSettings}. - * @return This builder, for convenience. - */ - public Builder setImaSdkSettings(ImaSdkSettings imaSdkSettings) { - this.imaSdkSettings = Assertions.checkNotNull(imaSdkSettings); - return this; - } - - /** - * Sets a listener for ad events that will be passed to {@link AdsManager#addAdEventListener(AdEventListener)}. - * - * @param adEventListener The ad event listener. - * @return This builder, for convenience. - */ - public Builder setAdEventListener(AdEventListener adEventListener) { - this.adEventListener = Assertions.checkNotNull(adEventListener); - return this; - } - - /** - * Sets the ad UI elements to be rendered by the IMA SDK. - * - * @param adUiElements The ad UI elements to be rendered by the IMA SDK. - * @return This builder, for convenience. - * @see AdsRenderingSettings#setUiElements(Set) - */ - public Builder setAdUiElements(Set adUiElements) { - this.adUiElements = new HashSet<>(Assertions.checkNotNull(adUiElements)); - return this; - } - - /** - * Sets the VAST load timeout, in milliseconds. - * - * @param vastLoadTimeoutMs The VAST load timeout, in milliseconds. - * @return This builder, for convenience. - * @see AdsRequest#setVastLoadTimeout(float) - */ - public Builder setVastLoadTimeoutMs(int vastLoadTimeoutMs) { - Assertions.checkArgument(vastLoadTimeoutMs > 0); - this.vastLoadTimeoutMs = vastLoadTimeoutMs; - return this; - } - - /** - * Sets the ad media load timeout, in milliseconds. - * - * @param mediaLoadTimeoutMs The ad media load timeout, in milliseconds. - * @return This builder, for convenience. - * @see AdsRenderingSettings#setLoadVideoTimeout(int) - */ - public Builder setMediaLoadTimeoutMs(int mediaLoadTimeoutMs) { - Assertions.checkArgument(mediaLoadTimeoutMs > 0); - this.mediaLoadTimeoutMs = mediaLoadTimeoutMs; - return this; - } - - /** - * Sets the media maximum recommended bitrate for ads, in bps. - * - * @param bitrate The media maximum recommended bitrate for ads, in bps. - * @return This builder, for convenience. - * @see AdsRenderingSettings#setBitrateKbps(int) - */ - public Builder setMaxMediaBitrate(int bitrate) { - Assertions.checkArgument(bitrate > 0); - this.mediaBitrate = bitrate; - return this; - } - - /** - * Sets whether to focus the skip button (when available) on Android TV devices. The default - * setting is {@code true}. - * - * @param focusSkipButtonWhenAvailable Whether to focus the skip button (when available) on - * Android TV devices. - * @return This builder, for convenience. - * @see AdsRenderingSettings#setFocusSkipButtonWhenAvailable(boolean) - */ - public Builder setFocusSkipButtonWhenAvailable(boolean focusSkipButtonWhenAvailable) { - this.focusSkipButtonWhenAvailable = focusSkipButtonWhenAvailable; - return this; - } - - @VisibleForTesting - /* package */ Builder setImaFactory(ImaFactory imaFactory) { - this.imaFactory = Assertions.checkNotNull(imaFactory); - return this; - } - - /** - * Returns a new {@link ImaAdsLoader} for the specified ad tag. - * - * @param adTagUri The URI of a compatible ad tag to load. See https://developers.google.com/interactive-media-ads/docs/sdks/android/compatibility - * for information on compatible ad tags. - * @return The new {@link ImaAdsLoader}. - */ - public ImaAdsLoader buildForAdTag(Uri adTagUri) { - return new ImaAdsLoader( - context, - adTagUri, - imaSdkSettings, - null, - vastLoadTimeoutMs, - mediaLoadTimeoutMs, - mediaBitrate, - focusSkipButtonWhenAvailable, - adUiElements, - adEventListener, - imaFactory); - } - - /** - * Returns a new {@link ImaAdsLoader} with the specified sideloaded ads response. - * - * @param adsResponse The sideloaded VAST, VMAP, or ad rules response to be used instead of - * making a request via an ad tag URL. - * @return The new {@link ImaAdsLoader}. - */ - public ImaAdsLoader buildForAdsResponse(String adsResponse) { - return new ImaAdsLoader( - context, - null, - imaSdkSettings, - adsResponse, - vastLoadTimeoutMs, - mediaLoadTimeoutMs, - mediaBitrate, - focusSkipButtonWhenAvailable, - adUiElements, - adEventListener, - imaFactory); - } - } - - private static final boolean DEBUG = false; - private static final String TAG = "ImaAdsLoader"; - - /** - * Whether to enable preloading of ads in {@link AdsRenderingSettings}. - */ - private static final boolean ENABLE_PRELOADING = true; - - private static final String IMA_SDK_SETTINGS_PLAYER_TYPE = "google/exo.ext.ima"; - private static final String IMA_SDK_SETTINGS_PLAYER_VERSION = ExoPlayerLibraryInfo.VERSION; - - /** - * The value used in {@link VideoProgressUpdate}s to indicate an unset duration. - */ - private static final long IMA_DURATION_UNSET = -1L; - - /** - * Threshold before the end of content at which IMA is notified that content is complete if the - * player buffers, in milliseconds. - */ - private static final long END_OF_CONTENT_POSITION_THRESHOLD_MS = 5000; - - /** - * The maximum duration before an ad break that IMA may start preloading the next ad. - */ - private static final long MAXIMUM_PRELOAD_DURATION_MS = 8000; - - private static final int TIMEOUT_UNSET = -1; - private static final int BITRATE_UNSET = -1; - - /** - * The state of ad playback. - */ - @Documented - @Retention(RetentionPolicy.SOURCE) - @IntDef({IMA_AD_STATE_NONE, IMA_AD_STATE_PLAYING, IMA_AD_STATE_PAUSED}) - private @interface ImaAdState { - - } - - /** - * The ad playback state when IMA is not playing an ad. - */ - private static final int IMA_AD_STATE_NONE = 0; - /** - * The ad playback state when IMA has called {@link #playAd()} and not {@link #pauseAd()}. - */ - private static final int IMA_AD_STATE_PLAYING = 1; - /** - * The ad playback state when IMA has called {@link #pauseAd()} while playing an ad. - */ - private static final int IMA_AD_STATE_PAUSED = 2; - - @Nullable - private final Uri adTagUri; - @Nullable - private final String adsResponse; - private final int vastLoadTimeoutMs; - private final int mediaLoadTimeoutMs; - private final boolean focusSkipButtonWhenAvailable; - private final int mediaBitrate; - @Nullable - private final Set adUiElements; - @Nullable - private final AdEventListener adEventListener; - private final ImaFactory imaFactory; - private final Timeline.Period period; - private final List adCallbacks; - private final AdDisplayContainer adDisplayContainer; - private final com.google.ads.interactivemedia.v3.api.AdsLoader adsLoader; - - private boolean wasSetPlayerCalled; - @Nullable - private Player nextPlayer; - private Object pendingAdRequestContext; - private List supportedMimeTypes; - @Nullable - private EventListener eventListener; - @Nullable - private Player player; - private VideoProgressUpdate lastContentProgress; - private VideoProgressUpdate lastAdProgress; - private int lastVolumePercentage; - - private AdsManager adsManager; - private boolean initializedAdsManager; - private AdLoadException pendingAdLoadError; - private Timeline timeline; - private long contentDurationMs; - private int podIndexOffset; - private AdPlaybackState adPlaybackState; - - // Fields tracking IMA's state. - - /** - * The expected ad group index that IMA should load next. - */ - private int expectedAdGroupIndex; - /** - * The index of the current ad group that IMA is loading. - */ - private int adGroupIndex; - /** - * Whether IMA has sent an ad event to pause content since the last resume content event. - */ - private boolean imaPausedContent; - /** - * The current ad playback state. - */ - private @ImaAdState - int imaAdState; - /** - * Whether {@link com.google.ads.interactivemedia.v3.api.AdsLoader#contentComplete()} has been - * called since starting ad playback. - */ - private boolean sentContentComplete; - - // Fields tracking the player/loader state. - - /** - * Whether the player is playing an ad. - */ - private boolean playingAd; - /** - * If the player is playing an ad, stores the ad index in its ad group. {@link C#INDEX_UNSET} - * otherwise. - */ - private int playingAdIndexInAdGroup; - /** - * Whether there's a pending ad preparation error which IMA needs to be notified of when it - * transitions from playing content to playing the ad. - */ - private boolean shouldNotifyAdPrepareError; - /** - * If a content period has finished but IMA has not yet called {@link #playAd()}, stores the value - * of {@link SystemClock#elapsedRealtime()} when the content stopped playing. This can be used to - * determine a fake, increasing content position. {@link C#TIME_UNSET} otherwise. - */ - private long fakeContentProgressElapsedRealtimeMs; - /** - * If {@link #fakeContentProgressElapsedRealtimeMs} is set, stores the offset from which the - * content progress should increase. {@link C#TIME_UNSET} otherwise. - */ - private long fakeContentProgressOffsetMs; - /** - * Stores the pending content position when a seek operation was intercepted to play an ad. - */ - private long pendingContentPositionMs; - /** - * Whether {@link #getContentProgress()} has sent {@link #pendingContentPositionMs} to IMA. - */ - private boolean sentPendingContentPositionMs; - - /** - * Creates a new IMA ads loader. - * - *

If you need to customize the ad request, use {@link ImaAdsLoader.Builder} instead. - * - * @param context The context. - * @param adTagUri The {@link Uri} of an ad tag compatible with the Android IMA SDK. See - * https://developers.google.com/interactive-media-ads/docs/sdks/android/compatibility - * for more information. - */ - public ImaAdsLoader(Context context, Uri adTagUri) { - this( - context, - adTagUri, - /* imaSdkSettings= */ null, - /* adsResponse= */ null, - /* vastLoadTimeoutMs= */ TIMEOUT_UNSET, - /* mediaLoadTimeoutMs= */ TIMEOUT_UNSET, - /* mediaBitrate= */ BITRATE_UNSET, - /* focusSkipButtonWhenAvailable= */ true, - /* adUiElements= */ null, - /* adEventListener= */ null, - /* imaFactory= */ new DefaultImaFactory()); - } - - /** - * Creates a new IMA ads loader. - * - * @param context The context. - * @param adTagUri The {@link Uri} of an ad tag compatible with the Android IMA SDK. See - * https://developers.google.com/interactive-media-ads/docs/sdks/android/compatibility - * for more information. - * @param imaSdkSettings {@link ImaSdkSettings} used to configure the IMA SDK, or {@code null} to - * use the default settings. If set, the player type and version fields may - * be overwritten. - * @deprecated Use {@link ImaAdsLoader.Builder}. - */ - @Deprecated - public ImaAdsLoader(Context context, Uri adTagUri, @Nullable ImaSdkSettings imaSdkSettings) { - this( - context, - adTagUri, - imaSdkSettings, - /* adsResponse= */ null, - /* vastLoadTimeoutMs= */ TIMEOUT_UNSET, - /* mediaLoadTimeoutMs= */ TIMEOUT_UNSET, - /* mediaBitrate= */ BITRATE_UNSET, - /* focusSkipButtonWhenAvailable= */ true, - /* adUiElements= */ null, - /* adEventListener= */ null, - /* imaFactory= */ new DefaultImaFactory()); - } - - private ImaAdsLoader( - Context context, - @Nullable Uri adTagUri, - @Nullable ImaSdkSettings imaSdkSettings, - @Nullable String adsResponse, - int vastLoadTimeoutMs, - int mediaLoadTimeoutMs, - int mediaBitrate, - boolean focusSkipButtonWhenAvailable, - @Nullable Set adUiElements, - @Nullable AdEventListener adEventListener, - ImaFactory imaFactory) { - Assertions.checkArgument(adTagUri != null || adsResponse != null); - this.adTagUri = adTagUri; - this.adsResponse = adsResponse; - this.vastLoadTimeoutMs = vastLoadTimeoutMs; - this.mediaLoadTimeoutMs = mediaLoadTimeoutMs; - this.mediaBitrate = mediaBitrate; - this.focusSkipButtonWhenAvailable = focusSkipButtonWhenAvailable; - this.adUiElements = adUiElements; - this.adEventListener = adEventListener; - this.imaFactory = imaFactory; - if (imaSdkSettings == null) { - imaSdkSettings = imaFactory.createImaSdkSettings(); - if (DEBUG) { - imaSdkSettings.setDebugMode(true); - } - } - imaSdkSettings.setPlayerType(IMA_SDK_SETTINGS_PLAYER_TYPE); - imaSdkSettings.setPlayerVersion(IMA_SDK_SETTINGS_PLAYER_VERSION); - period = new Timeline.Period(); - adCallbacks = new ArrayList<>(/* initialCapacity= */ 1); - adDisplayContainer = imaFactory.createAdDisplayContainer(); - adDisplayContainer.setPlayer(/* videoAdPlayer= */ this); - adsLoader = imaFactory.createAdsLoader(context, imaSdkSettings, adDisplayContainer); - adsLoader.addAdErrorListener(/* adErrorListener= */ this); - adsLoader.addAdsLoadedListener(/* adsLoadedListener= */ this); - fakeContentProgressElapsedRealtimeMs = C.TIME_UNSET; - fakeContentProgressOffsetMs = C.TIME_UNSET; - pendingContentPositionMs = C.TIME_UNSET; - adGroupIndex = C.INDEX_UNSET; - contentDurationMs = C.TIME_UNSET; - timeline = Timeline.EMPTY; - } - - /** - * Returns the underlying {@code com.google.ads.interactivemedia.v3.api.AdsLoader} wrapped by this - * instance. - */ - public com.google.ads.interactivemedia.v3.api.AdsLoader getAdsLoader() { - return adsLoader; - } - - /** - * Returns the {@link AdDisplayContainer} used by this loader. - * - *

Note: any video controls overlays registered via {@link - * AdDisplayContainer#registerVideoControlsOverlay(View)} will be unregistered automatically when - * the media source detaches from this instance. It is therefore necessary to re-register views - * each time the ads loader is reused. Alternatively, provide overlay views via the {@link - * AdsLoader.AdViewProvider} when creating the media source to benefit from automatic - * registration. - */ - public AdDisplayContainer getAdDisplayContainer() { - return adDisplayContainer; - } - - /** - * Sets the slots for displaying companion ads. Individual slots can be created using {@link - * ImaSdkFactory#createCompanionAdSlot()}. - * - * @param companionSlots Slots for displaying companion ads. - * @see AdDisplayContainer#setCompanionSlots(Collection) - * @deprecated Use {@code getAdDisplayContainer().setCompanionSlots(...)}. - */ - @Deprecated - public void setCompanionSlots(Collection companionSlots) { - adDisplayContainer.setCompanionSlots(companionSlots); - } - - /** - * Requests ads, if they have not already been requested. Must be called on the main thread. - * - *

Ads will be requested automatically when the player is prepared if this method has not been - * called, so it is only necessary to call this method if you want to request ads before preparing - * the player. - * - * @param adViewGroup A {@link ViewGroup} on top of the player that will show any ad UI. - */ - public void requestAds(ViewGroup adViewGroup) { - if (adPlaybackState != null || adsManager != null || pendingAdRequestContext != null) { - // Ads have already been requested. - return; - } - adDisplayContainer.setAdContainer(adViewGroup); - pendingAdRequestContext = new Object(); - AdsRequest request = imaFactory.createAdsRequest(); - if (adTagUri != null) { - request.setAdTagUrl(adTagUri.toString()); - } else /* adsResponse != null */ { - request.setAdsResponse(adsResponse); - } - if (vastLoadTimeoutMs != TIMEOUT_UNSET) { - request.setVastLoadTimeout(vastLoadTimeoutMs); - } - request.setContentProgressProvider(this); - request.setUserRequestContext(pendingAdRequestContext); - adsLoader.requestAds(request); - } - - // AdsLoader implementation. - - @Override - public void setPlayer(@Nullable Player player) { - Assertions.checkState(Looper.getMainLooper() == Looper.myLooper()); - Assertions.checkState( - player == null || player.getApplicationLooper() == Looper.getMainLooper()); - nextPlayer = player; - wasSetPlayerCalled = true; - } - - @Override - public void setSupportedContentTypes(@C.ContentType int... contentTypes) { - List supportedMimeTypes = new ArrayList<>(); - for (@C.ContentType int contentType : contentTypes) { - if (contentType == C.TYPE_DASH) { - supportedMimeTypes.add(MimeTypes.APPLICATION_MPD); - } else if (contentType == C.TYPE_HLS) { - supportedMimeTypes.add(MimeTypes.APPLICATION_M3U8); - } else if (contentType == C.TYPE_OTHER) { - supportedMimeTypes.addAll( - Arrays.asList( - MimeTypes.VIDEO_MP4, - MimeTypes.VIDEO_WEBM, - MimeTypes.VIDEO_H263, - MimeTypes.AUDIO_MP4, - MimeTypes.AUDIO_MPEG)); - } else if (contentType == C.TYPE_SS) { - // IMA does not support Smooth Streaming ad media. - } - } - this.supportedMimeTypes = Collections.unmodifiableList(supportedMimeTypes); - } - - @Override - public void start(EventListener eventListener, AdViewProvider adViewProvider) { - Assertions.checkState( - wasSetPlayerCalled, "Set player using adsLoader.setPlayer before preparing the player."); - player = nextPlayer; - if (player == null) { - return; - } - this.eventListener = eventListener; - lastVolumePercentage = 0; - lastAdProgress = null; - lastContentProgress = null; - ViewGroup adViewGroup = adViewProvider.getAdViewGroup(); - adDisplayContainer.setAdContainer(adViewGroup); - View[] adOverlayViews = adViewProvider.getAdOverlayViews(); - for (View view : adOverlayViews) { - adDisplayContainer.registerVideoControlsOverlay(view); - } - player.addListener(this); - maybeNotifyPendingAdLoadError(); - if (adPlaybackState != null) { - // Pass the ad playback state to the player, and resume ads if necessary. - eventListener.onAdPlaybackState(adPlaybackState); - if (imaPausedContent && player.getPlayWhenReady()) { - adsManager.resume(); - } - } else if (adsManager != null) { - adPlaybackState = new AdPlaybackState(getAdGroupTimesUs(adsManager.getAdCuePoints())); - updateAdPlaybackState(); - } else { - // Ads haven't loaded yet, so request them. - requestAds(adViewGroup); - } - } - - @Override - public void stop() { - if (player == null) { - return; - } - if (adsManager != null && imaPausedContent) { - adPlaybackState = - adPlaybackState.withAdResumePositionUs( - playingAd ? C.msToUs(player.getCurrentPosition()) : 0); - adsManager.pause(); - } - lastVolumePercentage = getVolume(); - lastAdProgress = getAdProgress(); - lastContentProgress = getContentProgress(); - adDisplayContainer.unregisterAllVideoControlsOverlays(); - player.removeListener(this); - player = null; - eventListener = null; - } - - @Override - public void release() { - pendingAdRequestContext = null; - if (adsManager != null) { - adsManager.removeAdErrorListener(this); - adsManager.removeAdEventListener(this); - if (adEventListener != null) { - adsManager.removeAdEventListener(adEventListener); - } - adsManager.destroy(); - adsManager = null; - } - adsLoader.removeAdsLoadedListener(/* adsLoadedListener= */ this); - adsLoader.removeAdErrorListener(/* adErrorListener= */ this); - imaPausedContent = false; - imaAdState = IMA_AD_STATE_NONE; - pendingAdLoadError = null; - adPlaybackState = AdPlaybackState.NONE; - updateAdPlaybackState(); - } - - @Override - public void handlePrepareError(int adGroupIndex, int adIndexInAdGroup, IOException exception) { - if (player == null) { - return; - } - try { - handleAdPrepareError(adGroupIndex, adIndexInAdGroup, exception); - } catch (Exception e) { - maybeNotifyInternalError("handlePrepareError", e); - } - } - - // com.google.ads.interactivemedia.v3.api.AdsLoader.AdsLoadedListener implementation. - - @Override - public void onAdsManagerLoaded(AdsManagerLoadedEvent adsManagerLoadedEvent) { - AdsManager adsManager = adsManagerLoadedEvent.getAdsManager(); - if (!Util.areEqual(pendingAdRequestContext, adsManagerLoadedEvent.getUserRequestContext())) { - adsManager.destroy(); - return; - } - pendingAdRequestContext = null; - this.adsManager = adsManager; - adsManager.addAdErrorListener(this); - adsManager.addAdEventListener(this); - if (adEventListener != null) { - adsManager.addAdEventListener(adEventListener); - } - if (player != null) { - // If a player is attached already, start playback immediately. - try { - adPlaybackState = new AdPlaybackState(getAdGroupTimesUs(adsManager.getAdCuePoints())); - updateAdPlaybackState(); - } catch (Exception e) { - maybeNotifyInternalError("onAdsManagerLoaded", e); - } - } - } - - // AdEvent.AdEventListener implementation. - - @Override - public void onAdEvent(AdEvent adEvent) { - AdEventType adEventType = adEvent.getType(); - if (DEBUG) { - Log.d(TAG, "onAdEvent: " + adEventType); - } - if (adsManager == null) { - Log.w(TAG, "Ignoring AdEvent after release: " + adEvent); - return; - } - try { - handleAdEvent(adEvent); - } catch (Exception e) { - maybeNotifyInternalError("onAdEvent", e); - } - } - - // AdErrorEvent.AdErrorListener implementation. - - @Override - public void onAdError(AdErrorEvent adErrorEvent) { - AdError error = adErrorEvent.getError(); - if (DEBUG) { - Log.d(TAG, "onAdError", error); - } - if (adsManager == null) { - // No ads were loaded, so allow playback to start without any ads. - pendingAdRequestContext = null; - adPlaybackState = new AdPlaybackState(); - updateAdPlaybackState(); - } else if (isAdGroupLoadError(error)) { - try { - handleAdGroupLoadError(error); - } catch (Exception e) { - maybeNotifyInternalError("onAdError", e); - } - } - if (pendingAdLoadError == null) { - pendingAdLoadError = AdLoadException.createForAllAds(error); - } - maybeNotifyPendingAdLoadError(); - } - - // ContentProgressProvider implementation. - - @Override - public VideoProgressUpdate getContentProgress() { - if (player == null) { - return lastContentProgress; - } - boolean hasContentDuration = contentDurationMs != C.TIME_UNSET; - long contentPositionMs; - if (pendingContentPositionMs != C.TIME_UNSET) { - sentPendingContentPositionMs = true; - contentPositionMs = pendingContentPositionMs; - expectedAdGroupIndex = - adPlaybackState.getAdGroupIndexForPositionUs(C.msToUs(contentPositionMs)); - } else if (fakeContentProgressElapsedRealtimeMs != C.TIME_UNSET) { - long elapsedSinceEndMs = SystemClock.elapsedRealtime() - fakeContentProgressElapsedRealtimeMs; - contentPositionMs = fakeContentProgressOffsetMs + elapsedSinceEndMs; - expectedAdGroupIndex = - adPlaybackState.getAdGroupIndexForPositionUs(C.msToUs(contentPositionMs)); - } else if (imaAdState == IMA_AD_STATE_NONE && !playingAd && hasContentDuration) { - contentPositionMs = player.getCurrentPosition(); - // Update the expected ad group index for the current content position. The update is delayed - // until MAXIMUM_PRELOAD_DURATION_MS before the ad so that an ad group load error delivered - // just after an ad group isn't incorrectly attributed to the next ad group. - int nextAdGroupIndex = - adPlaybackState.getAdGroupIndexAfterPositionUs( - C.msToUs(contentPositionMs), C.msToUs(contentDurationMs)); - if (nextAdGroupIndex != expectedAdGroupIndex && nextAdGroupIndex != C.INDEX_UNSET) { - long nextAdGroupTimeMs = C.usToMs(adPlaybackState.adGroupTimesUs[nextAdGroupIndex]); - if (nextAdGroupTimeMs == C.TIME_END_OF_SOURCE) { - nextAdGroupTimeMs = contentDurationMs; - } - if (nextAdGroupTimeMs - contentPositionMs < MAXIMUM_PRELOAD_DURATION_MS) { - expectedAdGroupIndex = nextAdGroupIndex; - } - } - } else { - return VideoProgressUpdate.VIDEO_TIME_NOT_READY; - } - long contentDurationMs = hasContentDuration ? this.contentDurationMs : IMA_DURATION_UNSET; - return new VideoProgressUpdate(contentPositionMs, contentDurationMs); - } - - // VideoAdPlayer implementation. - - @Override - public VideoProgressUpdate getAdProgress() { - if (player == null) { - return lastAdProgress; - } else if (imaAdState != IMA_AD_STATE_NONE && playingAd) { - long adDuration = player.getDuration(); - return adDuration == C.TIME_UNSET ? VideoProgressUpdate.VIDEO_TIME_NOT_READY - : new VideoProgressUpdate(player.getCurrentPosition(), adDuration); - } else { - return VideoProgressUpdate.VIDEO_TIME_NOT_READY; - } - } - - @Override - public int getVolume() { - if (player == null) { - return lastVolumePercentage; - } - - Player.AudioComponent audioComponent = player.getAudioComponent(); - if (audioComponent != null) { - return (int) (audioComponent.getVolume() * 100); - } - - // Check for a selected track using an audio renderer. - TrackSelectionArray trackSelections = player.getCurrentTrackSelections(); - for (int i = 0; i < player.getRendererCount() && i < trackSelections.length; i++) { - if (player.getRendererType(i) == C.TRACK_TYPE_AUDIO && trackSelections.get(i) != null) { - return 100; - } - } - return 0; - } - - @Override - public void loadAd(String adUriString) { - try { - if (DEBUG) { - Log.d(TAG, "loadAd in ad group " + adGroupIndex); - } - if (adsManager == null) { - Log.w(TAG, "Ignoring loadAd after release"); - return; - } - if (adGroupIndex == C.INDEX_UNSET) { - Log.w( - TAG, - "Unexpected loadAd without LOADED event; assuming ad group index is actually " - + expectedAdGroupIndex); - adGroupIndex = expectedAdGroupIndex; - adsManager.start(); - } - int adIndexInAdGroup = getAdIndexInAdGroupToLoad(adGroupIndex); - if (adIndexInAdGroup == C.INDEX_UNSET) { - Log.w(TAG, "Unexpected loadAd in an ad group with no remaining unavailable ads"); - return; - } - adPlaybackState = - adPlaybackState.withAdUri(adGroupIndex, adIndexInAdGroup, Uri.parse(adUriString)); - updateAdPlaybackState(); - } catch (Exception e) { - maybeNotifyInternalError("loadAd", e); - } - } - - @Override - public void addCallback(VideoAdPlayerCallback videoAdPlayerCallback) { - adCallbacks.add(videoAdPlayerCallback); - } - - @Override - public void removeCallback(VideoAdPlayerCallback videoAdPlayerCallback) { - adCallbacks.remove(videoAdPlayerCallback); - } - - @Override - public void playAd() { - if (DEBUG) { - Log.d(TAG, "playAd"); - } - if (adsManager == null) { - Log.w(TAG, "Ignoring playAd after release"); - return; - } - switch (imaAdState) { - case IMA_AD_STATE_PLAYING: - // IMA does not always call stopAd before resuming content. - // See [Internal: b/38354028, b/63320878]. - Log.w(TAG, "Unexpected playAd without stopAd"); - break; - case IMA_AD_STATE_NONE: - // IMA is requesting to play the ad, so stop faking the content position. - fakeContentProgressElapsedRealtimeMs = C.TIME_UNSET; - fakeContentProgressOffsetMs = C.TIME_UNSET; - imaAdState = IMA_AD_STATE_PLAYING; - for (int i = 0; i < adCallbacks.size(); i++) { - adCallbacks.get(i).onPlay(); - } - if (shouldNotifyAdPrepareError) { - shouldNotifyAdPrepareError = false; - for (int i = 0; i < adCallbacks.size(); i++) { - adCallbacks.get(i).onError(); - } - } - break; - case IMA_AD_STATE_PAUSED: - imaAdState = IMA_AD_STATE_PLAYING; - for (int i = 0; i < adCallbacks.size(); i++) { - adCallbacks.get(i).onResume(); - } - break; - default: - throw new IllegalStateException(); - } - if (player == null) { - // Sometimes messages from IMA arrive after detaching the player. See [Internal: b/63801642]. - Log.w(TAG, "Unexpected playAd while detached"); - } else if (!player.getPlayWhenReady()) { - adsManager.pause(); - } - } - - @Override - public void stopAd() { - if (DEBUG) { - Log.d(TAG, "stopAd"); - } - if (adsManager == null) { - Log.w(TAG, "Ignoring stopAd after release"); - return; - } - if (player == null) { - // Sometimes messages from IMA arrive after detaching the player. See [Internal: b/63801642]. - Log.w(TAG, "Unexpected stopAd while detached"); - } - if (imaAdState == IMA_AD_STATE_NONE) { - Log.w(TAG, "Unexpected stopAd"); - return; - } - try { - stopAdInternal(); - } catch (Exception e) { - maybeNotifyInternalError("stopAd", e); - } - } - - @Override - public void pauseAd() { - if (DEBUG) { - Log.d(TAG, "pauseAd"); - } - if (imaAdState == IMA_AD_STATE_NONE) { - // This method is called after content is resumed. - return; - } - imaAdState = IMA_AD_STATE_PAUSED; - for (int i = 0; i < adCallbacks.size(); i++) { - adCallbacks.get(i).onPause(); - } - } - - @Override - public void resumeAd() { - // This method is never called. See [Internal: b/18931719]. - maybeNotifyInternalError("resumeAd", new IllegalStateException("Unexpected call to resumeAd")); - } - - // Player.EventListener implementation. - - @Override - public void onTimelineChanged(Timeline timeline, @Player.TimelineChangeReason int reason) { - if (timeline.isEmpty()) { - // The player is being reset or contains no media. - return; - } - Assertions.checkArgument(timeline.getPeriodCount() == 1); - this.timeline = timeline; - long contentDurationUs = timeline.getPeriod(0, period).durationUs; - contentDurationMs = C.usToMs(contentDurationUs); - if (contentDurationUs != C.TIME_UNSET) { - adPlaybackState = adPlaybackState.withContentDurationUs(contentDurationUs); - } - if (!initializedAdsManager && adsManager != null) { - initializedAdsManager = true; - initializeAdsManager(); - } - onPositionDiscontinuity(Player.DISCONTINUITY_REASON_INTERNAL); - } - - @Override - public void onPlayerStateChanged(boolean playWhenReady, @Player.State int playbackState) { - if (adsManager == null) { - return; - } - - if (imaAdState == IMA_AD_STATE_PLAYING && !playWhenReady) { - adsManager.pause(); - return; - } - - if (imaAdState == IMA_AD_STATE_PAUSED && playWhenReady) { - adsManager.resume(); - return; - } - - if (imaAdState == IMA_AD_STATE_NONE && playbackState == Player.STATE_BUFFERING - && playWhenReady) { - checkForContentComplete(); - } else if (imaAdState != IMA_AD_STATE_NONE && playbackState == Player.STATE_ENDED) { - for (int i = 0; i < adCallbacks.size(); i++) { - adCallbacks.get(i).onEnded(); - } - if (DEBUG) { - Log.d(TAG, "VideoAdPlayerCallback.onEnded in onPlayerStateChanged"); - } - } - } - - @Override - public void onPlayerError(ExoPlaybackException error) { - if (imaAdState != IMA_AD_STATE_NONE) { - for (int i = 0; i < adCallbacks.size(); i++) { - adCallbacks.get(i).onError(); - } - } - } - - @Override - public void onPositionDiscontinuity(@Player.DiscontinuityReason int reason) { - if (adsManager == null) { - return; - } - if (!playingAd && !player.isPlayingAd()) { - checkForContentComplete(); - if (sentContentComplete) { - for (int i = 0; i < adPlaybackState.adGroupCount; i++) { - if (adPlaybackState.adGroupTimesUs[i] != C.TIME_END_OF_SOURCE) { - adPlaybackState = adPlaybackState.withSkippedAdGroup(i); - } - } - updateAdPlaybackState(); - } else if (!timeline.isEmpty()) { - long positionMs = player.getCurrentPosition(); - timeline.getPeriod(0, period); - int newAdGroupIndex = period.getAdGroupIndexForPositionUs(C.msToUs(positionMs)); - if (newAdGroupIndex != C.INDEX_UNSET) { - sentPendingContentPositionMs = false; - pendingContentPositionMs = positionMs; - if (newAdGroupIndex != adGroupIndex) { - shouldNotifyAdPrepareError = false; - } - } - } - } - updateImaStateForPlayerState(); - } - - // Internal methods. - - private void initializeAdsManager() { - AdsRenderingSettings adsRenderingSettings = imaFactory.createAdsRenderingSettings(); - adsRenderingSettings.setEnablePreloading(ENABLE_PRELOADING); - adsRenderingSettings.setMimeTypes(supportedMimeTypes); - if (mediaLoadTimeoutMs != TIMEOUT_UNSET) { - adsRenderingSettings.setLoadVideoTimeout(mediaLoadTimeoutMs); - } - if (mediaBitrate != BITRATE_UNSET) { - adsRenderingSettings.setBitrateKbps(mediaBitrate / 1000); - } - adsRenderingSettings.setFocusSkipButtonWhenAvailable(focusSkipButtonWhenAvailable); - if (adUiElements != null) { - adsRenderingSettings.setUiElements(adUiElements); - } - - // Skip ads based on the start position as required. - long[] adGroupTimesUs = getAdGroupTimesUs(adsManager.getAdCuePoints()); - long contentPositionMs = player.getContentPosition(); - int adGroupIndexForPosition = - adPlaybackState.getAdGroupIndexForPositionUs(C.msToUs(contentPositionMs)); - if (adGroupIndexForPosition > 0 && adGroupIndexForPosition != C.INDEX_UNSET) { - // Skip any ad groups before the one at or immediately before the playback position. - for (int i = 0; i < adGroupIndexForPosition; i++) { - adPlaybackState = adPlaybackState.withSkippedAdGroup(i); - } - // Play ads after the midpoint between the ad to play and the one before it, to avoid issues - // with rounding one of the two ad times. - long adGroupForPositionTimeUs = adGroupTimesUs[adGroupIndexForPosition]; - long adGroupBeforeTimeUs = adGroupTimesUs[adGroupIndexForPosition - 1]; - double midpointTimeUs = (adGroupForPositionTimeUs + adGroupBeforeTimeUs) / 2d; - adsRenderingSettings.setPlayAdsAfterTime(midpointTimeUs / C.MICROS_PER_SECOND); - } - - // IMA indexes any remaining midroll ad pods from 1. A preroll (if present) has index 0. - // Store an index offset as we want to index all ads (including skipped ones) from 0. - if (adGroupIndexForPosition == 0 && adGroupTimesUs[0] == 0) { - // We are playing a preroll. - podIndexOffset = 0; - } else if (adGroupIndexForPosition == C.INDEX_UNSET) { - // There's no ad to play which means there's no preroll. - podIndexOffset = -1; - } else { - // We are playing a midroll and any ads before it were skipped. - podIndexOffset = adGroupIndexForPosition - 1; - } - - if (adGroupIndexForPosition != C.INDEX_UNSET && hasMidrollAdGroups(adGroupTimesUs)) { - // Provide the player's initial position to trigger loading and playing the ad. - pendingContentPositionMs = contentPositionMs; - } - - adsManager.init(adsRenderingSettings); - updateAdPlaybackState(); - if (DEBUG) { - Log.d(TAG, "Initialized with ads rendering settings: " + adsRenderingSettings); - } - } - - private void handleAdEvent(AdEvent adEvent) { - Ad ad = adEvent.getAd(); - switch (adEvent.getType()) { - case LOADED: - // The ad position is not always accurate when using preloading. See [Internal: b/62613240]. - AdPodInfo adPodInfo = ad.getAdPodInfo(); - int podIndex = adPodInfo.getPodIndex(); - adGroupIndex = - podIndex == -1 ? (adPlaybackState.adGroupCount - 1) : (podIndex + podIndexOffset); - int adPosition = adPodInfo.getAdPosition(); - int adCount = adPodInfo.getTotalAds(); - adsManager.start(); - if (DEBUG) { - Log.d(TAG, "Loaded ad " + adPosition + " of " + adCount + " in group " + adGroupIndex); - } - int oldAdCount = adPlaybackState.adGroups[adGroupIndex].count; - if (adCount != oldAdCount) { - if (oldAdCount == C.LENGTH_UNSET) { - adPlaybackState = adPlaybackState.withAdCount(adGroupIndex, adCount); - updateAdPlaybackState(); - } else { - // IMA sometimes unexpectedly decreases the ad count in an ad group. - Log.w(TAG, "Unexpected ad count in LOADED, " + adCount + ", expected " + oldAdCount); - } - } - if (adGroupIndex != expectedAdGroupIndex) { - Log.w( - TAG, - "Expected ad group index " - + expectedAdGroupIndex - + ", actual ad group index " - + adGroupIndex); - expectedAdGroupIndex = adGroupIndex; - } - break; - case CONTENT_PAUSE_REQUESTED: - // After CONTENT_PAUSE_REQUESTED, IMA will playAd/pauseAd/stopAd to show one or more ads - // before sending CONTENT_RESUME_REQUESTED. - imaPausedContent = true; - pauseContentInternal(); - break; - case TAPPED: - if (eventListener != null) { - eventListener.onAdTapped(); - } - break; - case CLICKED: - if (eventListener != null) { - eventListener.onAdClicked(); - } - break; - case CONTENT_RESUME_REQUESTED: - imaPausedContent = false; - resumeContentInternal(); - break; - case LOG: - Map adData = adEvent.getAdData(); - String message = "AdEvent: " + adData; - Log.i(TAG, message); - if ("adLoadError".equals(adData.get("type"))) { - handleAdGroupLoadError(new IOException(message)); - } - break; - case STARTED: - case ALL_ADS_COMPLETED: - default: - break; - } - } - - private void updateImaStateForPlayerState() { - boolean wasPlayingAd = playingAd; - int oldPlayingAdIndexInAdGroup = playingAdIndexInAdGroup; - playingAd = player.isPlayingAd(); - playingAdIndexInAdGroup = playingAd ? player.getCurrentAdIndexInAdGroup() : C.INDEX_UNSET; - boolean adFinished = wasPlayingAd && playingAdIndexInAdGroup != oldPlayingAdIndexInAdGroup; - if (adFinished) { - // IMA is waiting for the ad playback to finish so invoke the callback now. - // Either CONTENT_RESUME_REQUESTED will be passed next, or playAd will be called again. - for (int i = 0; i < adCallbacks.size(); i++) { - adCallbacks.get(i).onEnded(); - } - if (DEBUG) { - Log.d(TAG, "VideoAdPlayerCallback.onEnded in onTimelineChanged/onPositionDiscontinuity"); - } - } - if (!sentContentComplete && !wasPlayingAd && playingAd && imaAdState == IMA_AD_STATE_NONE) { - int adGroupIndex = player.getCurrentAdGroupIndex(); - // IMA hasn't called playAd yet, so fake the content position. - fakeContentProgressElapsedRealtimeMs = SystemClock.elapsedRealtime(); - fakeContentProgressOffsetMs = C.usToMs(adPlaybackState.adGroupTimesUs[adGroupIndex]); - if (fakeContentProgressOffsetMs == C.TIME_END_OF_SOURCE) { - fakeContentProgressOffsetMs = contentDurationMs; - } - } - } - - private void resumeContentInternal() { - if (imaAdState != IMA_AD_STATE_NONE) { - imaAdState = IMA_AD_STATE_NONE; - if (DEBUG) { - Log.d(TAG, "Unexpected CONTENT_RESUME_REQUESTED without stopAd"); - } - } - if (adGroupIndex != C.INDEX_UNSET) { - adPlaybackState = adPlaybackState.withSkippedAdGroup(adGroupIndex); - adGroupIndex = C.INDEX_UNSET; - updateAdPlaybackState(); - } - } - - private void pauseContentInternal() { - imaAdState = IMA_AD_STATE_NONE; - if (sentPendingContentPositionMs) { - pendingContentPositionMs = C.TIME_UNSET; - sentPendingContentPositionMs = false; - } - } - - private void stopAdInternal() { - imaAdState = IMA_AD_STATE_NONE; - int adIndexInAdGroup = adPlaybackState.adGroups[adGroupIndex].getFirstAdIndexToPlay(); - // TODO: Handle the skipped event so the ad can be marked as skipped rather than played. - adPlaybackState = - adPlaybackState.withPlayedAd(adGroupIndex, adIndexInAdGroup).withAdResumePositionUs(0); - updateAdPlaybackState(); - if (!playingAd) { - adGroupIndex = C.INDEX_UNSET; - } - } - - private void handleAdGroupLoadError(Exception error) { - int adGroupIndex = - this.adGroupIndex == C.INDEX_UNSET ? expectedAdGroupIndex : this.adGroupIndex; - if (adGroupIndex == C.INDEX_UNSET) { - // Drop the error, as we don't know which ad group it relates to. - return; - } - AdPlaybackState.AdGroup adGroup = adPlaybackState.adGroups[adGroupIndex]; - if (adGroup.count == C.LENGTH_UNSET) { - adPlaybackState = - adPlaybackState.withAdCount(adGroupIndex, Math.max(1, adGroup.states.length)); - adGroup = adPlaybackState.adGroups[adGroupIndex]; - } - for (int i = 0; i < adGroup.count; i++) { - if (adGroup.states[i] == AdPlaybackState.AD_STATE_UNAVAILABLE) { - if (DEBUG) { - Log.d(TAG, "Removing ad " + i + " in ad group " + adGroupIndex); - } - adPlaybackState = adPlaybackState.withAdLoadError(adGroupIndex, i); - } - } - updateAdPlaybackState(); - if (pendingAdLoadError == null) { - pendingAdLoadError = AdLoadException.createForAdGroup(error, adGroupIndex); - } - pendingContentPositionMs = C.TIME_UNSET; - fakeContentProgressElapsedRealtimeMs = C.TIME_UNSET; - } - - private void handleAdPrepareError(int adGroupIndex, int adIndexInAdGroup, Exception exception) { - if (DEBUG) { - Log.d( - TAG, "Prepare error for ad " + adIndexInAdGroup + " in group " + adGroupIndex, exception); - } - if (adsManager == null) { - Log.w(TAG, "Ignoring ad prepare error after release"); - return; - } - if (imaAdState == IMA_AD_STATE_NONE) { - // Send IMA a content position at the ad group so that it will try to play it, at which point - // we can notify that it failed to load. - fakeContentProgressElapsedRealtimeMs = SystemClock.elapsedRealtime(); - fakeContentProgressOffsetMs = C.usToMs(adPlaybackState.adGroupTimesUs[adGroupIndex]); - if (fakeContentProgressOffsetMs == C.TIME_END_OF_SOURCE) { - fakeContentProgressOffsetMs = contentDurationMs; - } - shouldNotifyAdPrepareError = true; - } else { - // We're already playing an ad. - if (adIndexInAdGroup > playingAdIndexInAdGroup) { - // Mark the playing ad as ended so we can notify the error on the next ad and remove it, - // which means that the ad after will load (if any). - for (int i = 0; i < adCallbacks.size(); i++) { - adCallbacks.get(i).onEnded(); - } - } - playingAdIndexInAdGroup = adPlaybackState.adGroups[adGroupIndex].getFirstAdIndexToPlay(); - for (int i = 0; i < adCallbacks.size(); i++) { - adCallbacks.get(i).onError(); - } - } - adPlaybackState = adPlaybackState.withAdLoadError(adGroupIndex, adIndexInAdGroup); - updateAdPlaybackState(); - } - - private void checkForContentComplete() { - if (contentDurationMs != C.TIME_UNSET && pendingContentPositionMs == C.TIME_UNSET - && player.getContentPosition() + END_OF_CONTENT_POSITION_THRESHOLD_MS >= contentDurationMs - && !sentContentComplete) { - adsLoader.contentComplete(); - if (DEBUG) { - Log.d(TAG, "adsLoader.contentComplete"); - } - sentContentComplete = true; - // After sending content complete IMA will not poll the content position, so set the expected - // ad group index. - expectedAdGroupIndex = - adPlaybackState.getAdGroupIndexForPositionUs(C.msToUs(contentDurationMs)); - } - } - - private void updateAdPlaybackState() { - // Ignore updates while detached. When a player is attached it will receive the latest state. - if (eventListener != null) { - eventListener.onAdPlaybackState(adPlaybackState); - } - } - - /** - * Returns the next ad index in the specified ad group to load, or {@link C#INDEX_UNSET} if all - * ads in the ad group have loaded. - */ - private int getAdIndexInAdGroupToLoad(int adGroupIndex) { - @AdState int[] states = adPlaybackState.adGroups[adGroupIndex].states; - int adIndexInAdGroup = 0; - // IMA loads ads in order. - while (adIndexInAdGroup < states.length - && states[adIndexInAdGroup] != AdPlaybackState.AD_STATE_UNAVAILABLE) { - adIndexInAdGroup++; - } - return adIndexInAdGroup == states.length ? C.INDEX_UNSET : adIndexInAdGroup; - } - - private void maybeNotifyPendingAdLoadError() { - if (pendingAdLoadError != null && eventListener != null) { - eventListener.onAdLoadError(pendingAdLoadError, new DataSpec(adTagUri)); - pendingAdLoadError = null; - } - } - - private void maybeNotifyInternalError(String name, Exception cause) { - String message = "Internal error in " + name; - Log.e(TAG, message, cause); - // We can't recover from an unexpected error in general, so skip all remaining ads. - if (adPlaybackState == null) { - adPlaybackState = AdPlaybackState.NONE; - } else { - for (int i = 0; i < adPlaybackState.adGroupCount; i++) { - adPlaybackState = adPlaybackState.withSkippedAdGroup(i); - } - } - updateAdPlaybackState(); - if (eventListener != null) { - eventListener.onAdLoadError( - AdLoadException.createForUnexpected(new RuntimeException(message, cause)), - new DataSpec(adTagUri)); - } - } - - private static long[] getAdGroupTimesUs(List cuePoints) { - if (cuePoints.isEmpty()) { - // If no cue points are specified, there is a preroll ad. - return new long[]{0}; - } - - int count = cuePoints.size(); - long[] adGroupTimesUs = new long[count]; - int adGroupIndex = 0; - for (int i = 0; i < count; i++) { - double cuePoint = cuePoints.get(i); - if (cuePoint == -1.0) { - adGroupTimesUs[count - 1] = C.TIME_END_OF_SOURCE; - } else { - adGroupTimesUs[adGroupIndex++] = (long) (C.MICROS_PER_SECOND * cuePoint); - } - } - // Cue points may be out of order, so sort them. - Arrays.sort(adGroupTimesUs, 0, adGroupIndex); - return adGroupTimesUs; - } - - private static boolean isAdGroupLoadError(AdError adError) { - // TODO: Find out what other errors need to be handled (if any), and whether each one relates to - // a single ad, ad group or the whole timeline. - return adError.getErrorCode() == AdErrorCode.VAST_LINEAR_ASSET_MISMATCH - || adError.getErrorCode() == AdErrorCode.UNKNOWN_ERROR; - } - - private static boolean hasMidrollAdGroups(long[] adGroupTimesUs) { - int count = adGroupTimesUs.length; - if (count == 1) { - return adGroupTimesUs[0] != 0 && adGroupTimesUs[0] != C.TIME_END_OF_SOURCE; - } else if (count == 2) { - return adGroupTimesUs[0] != 0 || adGroupTimesUs[1] != C.TIME_END_OF_SOURCE; - } else { - // There's at least one midroll ad group, as adGroupTimesUs is never empty. - return true; - } - } - - /** - * Factory for objects provided by the IMA SDK. - */ - @VisibleForTesting - /* package */ interface ImaFactory { - - /** - * @see ImaSdkSettings - */ - ImaSdkSettings createImaSdkSettings(); - - /** - * @see com.google.ads.interactivemedia.v3.api.ImaSdkFactory#createAdsRenderingSettings() - */ - AdsRenderingSettings createAdsRenderingSettings(); - - /** - * @see com.google.ads.interactivemedia.v3.api.ImaSdkFactory#createAdDisplayContainer() - */ - AdDisplayContainer createAdDisplayContainer(); - - /** - * @see com.google.ads.interactivemedia.v3.api.ImaSdkFactory#createAdsRequest() - */ - AdsRequest createAdsRequest(); - - /** - * @see ImaSdkFactory#createAdsLoader(Context, ImaSdkSettings, AdDisplayContainer) - */ - com.google.ads.interactivemedia.v3.api.AdsLoader createAdsLoader( - Context context, ImaSdkSettings imaSdkSettings, AdDisplayContainer adDisplayContainer); - } - - /** - * Default {@link ImaFactory} for non-test usage, which delegates to {@link ImaSdkFactory}. - */ - private static final class DefaultImaFactory implements ImaFactory { - - @Override - public ImaSdkSettings createImaSdkSettings() { - return ImaSdkFactory.getInstance().createImaSdkSettings(); - } - - @Override - public AdsRenderingSettings createAdsRenderingSettings() { - return ImaSdkFactory.getInstance().createAdsRenderingSettings(); - } - - @Override - public AdDisplayContainer createAdDisplayContainer() { - return ImaSdkFactory.getInstance().createAdDisplayContainer(); - } - - @Override - public AdsRequest createAdsRequest() { - return ImaSdkFactory.getInstance().createAdsRequest(); - } - - @Override - public com.google.ads.interactivemedia.v3.api.AdsLoader createAdsLoader( - Context context, ImaSdkSettings imaSdkSettings, AdDisplayContainer adDisplayContainer) { - return ImaSdkFactory.getInstance() - .createAdsLoader(context, imaSdkSettings, adDisplayContainer); - } - } -} diff --git a/automatedtests/src/androidTestR2_11_1/java/com/mux/stats/sdk/muxstats/automatedtests/ui/SimplePlayerTestActivity.java b/automatedtests/src/androidTestR2_11_1/java/com/mux/stats/sdk/muxstats/automatedtests/ui/SimplePlayerTestActivity.java deleted file mode 100644 index deefe358..00000000 --- a/automatedtests/src/androidTestR2_11_1/java/com/mux/stats/sdk/muxstats/automatedtests/ui/SimplePlayerTestActivity.java +++ /dev/null @@ -1,214 +0,0 @@ -package com.mux.stats.sdk.muxstats.automatedtests.ui; - - -import android.app.Notification; -import android.net.Uri; -import android.support.v4.media.session.MediaSessionCompat; -import android.util.Log; -import androidx.annotation.Nullable; -import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.DefaultRenderersFactory; -import com.google.android.exoplayer2.ExoPlayerFactory; -import com.google.android.exoplayer2.Player; -import com.google.android.exoplayer2.RenderersFactory; -import com.google.android.exoplayer2.SimpleExoPlayer; -import com.google.android.exoplayer2.drm.DrmSessionManager; -import com.google.android.exoplayer2.drm.ExoMediaCrypto; -import com.google.android.exoplayer2.ext.ima.ImaAdsLoader; -import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector; -import com.google.android.exoplayer2.source.MediaSource; -import com.google.android.exoplayer2.source.MediaSourceFactory; -import com.google.android.exoplayer2.source.ProgressiveMediaSource; -import com.google.android.exoplayer2.source.ads.AdsLoader; -import com.google.android.exoplayer2.source.ads.AdsMediaSource; -import com.google.android.exoplayer2.source.dash.DashMediaSource; -import com.google.android.exoplayer2.source.hls.HlsMediaSource; -import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource; -import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection; -import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; -import com.google.android.exoplayer2.trackselection.TrackSelection; -import com.google.android.exoplayer2.ui.PlayerNotificationManager; -import com.google.android.exoplayer2.upstream.DataSource; -import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; -import com.google.android.exoplayer2.util.Util; -import com.mux.stats.sdk.muxstats.automatedtests.R; -import com.mux.stats.sdk.muxstats.SimplePlayerBaseActivity; -import java.lang.reflect.Constructor; - -public class SimplePlayerTestActivity extends SimplePlayerBaseActivity - implements Player.EventListener { - - public void initExoPlayer() { - // Hopfully this will not channge the track selection set programmatically - TrackSelection.Factory trackSelectionFactory = new AdaptiveTrackSelection.Factory( - AdaptiveTrackSelection.DEFAULT_MIN_DURATION_FOR_QUALITY_INCREASE_MS * 10, - AdaptiveTrackSelection.DEFAULT_MAX_DURATION_FOR_QUALITY_DECREASE_MS * 10, - AdaptiveTrackSelection.DEFAULT_MIN_DURATION_TO_RETAIN_AFTER_DISCARD_MS, - AdaptiveTrackSelection.DEFAULT_BANDWIDTH_FRACTION - ); - DefaultTrackSelector.Parameters trackSelectorParameters - = new DefaultTrackSelector.ParametersBuilder(this).build(); - trackSelector = new DefaultTrackSelector(this, trackSelectionFactory); - trackSelector.setParameters(trackSelectorParameters); - RenderersFactory renderersFactory = new DefaultRenderersFactory(/* context= */ this); - player = ExoPlayerFactory.newSimpleInstance(this, renderersFactory, trackSelector); - player.addListener(this); - } - - // This is for background playback, set appropriate notification and etc - public void initAudioSession() { - notificationManager = PlayerNotificationManager.createWithNotificationChannel( - getApplicationContext(), - PLAYBACK_CHANNEL_ID, - R.string.channel_name, - R.string.channel_description, - PLAYBACK_NOTIFICATION_ID, - new MDAdapter(), - new CustomNotificationListener() - ); - notificationManager.setUseNavigationActions(false); - notificationManager.setUseStopAction(true); - notificationManager.setPlayer(player); - - mediaSessionCompat = new MediaSessionCompat(this, "hello_world_media"); - notificationManager.setMediaSessionToken(mediaSessionCompat.getSessionToken()); - mediaSessionConnector = new MediaSessionConnector(mediaSessionCompat); - mediaSessionConnector.setPlayer(player); - } - - public DataSource.Factory buildDataSourceFactory() { - return new DefaultDataSourceFactory(this, "Android-automated_tests"); - } - - private MediaSource createLeafMediaSource( - Uri uri, String extension, DrmSessionManager drmSessionManager) { - @C.ContentType int type = Util.inferContentType(uri, extension); - DataSource.Factory dataSourceFactory = buildDataSourceFactory(); - switch (type) { - case C.TYPE_DASH: - return new DashMediaSource.Factory(dataSourceFactory) - .setDrmSessionManager(drmSessionManager) - .createMediaSource(uri); - case C.TYPE_SS: - return new SsMediaSource.Factory(dataSourceFactory) - .setDrmSessionManager(drmSessionManager) - .createMediaSource(uri); - case C.TYPE_HLS: - return new HlsMediaSource.Factory(dataSourceFactory) - .setDrmSessionManager(drmSessionManager) - .createMediaSource(uri); - case C.TYPE_OTHER: - return new ProgressiveMediaSource.Factory(dataSourceFactory) - .setDrmSessionManager(drmSessionManager) - .createMediaSource(uri); - default: - throw new IllegalStateException("Unsupported type: " + type); - } - } - - public MediaSource buildMediaSource(Uri uri, @Nullable String overrideExtension) { - return createLeafMediaSource(uri, overrideExtension, - DrmSessionManager.getDummyDrmSessionManager()); - } - - /** - * Returns an ads media source, reusing the ads loader if one exists. - */ - public MediaSource createAdsMediaSource(MediaSource aMediaSource, Uri adTagUri) { - // Load the extension source using reflection so the demo app doesn't have to depend on it. - // The ads loader is reused for multiple playbacks, so that ad playback can resume. - try { - Class loaderClass = Class.forName("com.google.android.exoplayer2.ext.ima.ImaAdsLoader"); - if (adsLoader == null) { - // Full class names used so the LINT.IfChange rule triggers should any of the classes move. - // LINT.IfChange - Constructor loaderConstructor = - loaderClass - .asSubclass(AdsLoader.class) - .getConstructor(android.content.Context.class, android.net.Uri.class); - // LINT.ThenChange(../../../../../../../../proguard-rules.txt) - adsLoader = loaderConstructor.newInstance(this, adTagUri); - } - MediaSourceFactory adMediaSourceFactory = - new MediaSourceFactory() { - @Override - public MediaSource createMediaSource(Uri uri) { - return SimplePlayerTestActivity.this.createLeafMediaSource( - uri, /* extension=*/ null, DrmSessionManager.getDummyDrmSessionManager()); - } - - @Override - public int[] getSupportedTypes() { - return new int[]{C.TYPE_DASH, C.TYPE_SS, C.TYPE_HLS, C.TYPE_OTHER}; - } - }; - // Because of how this is loaded via reflection, we know that this will be - // a ImaAdsLoader, so cast it over so that we can get a reference to the - // real IMA AdsLoader instance. - ((ImaAdsLoader) adsLoader).setPlayer(player); - muxStats.monitorImaAdsLoader(((ImaAdsLoader) adsLoader).getAdsLoader()); - return new AdsMediaSource(aMediaSource, adMediaSourceFactory, adsLoader, playerView); - } catch (ClassNotFoundException e) { - // IMA extension not loaded. - return null; - } catch (Exception e) { - throw new RuntimeException(e); - } - } - - @Override - public void startPlayback() { - Uri testUri = Uri.parse(urlToPlay); - testMediaSource = buildMediaSource(testUri, null); - if (loadedAdTagUri != null) { - testMediaSource = createAdsMediaSource(testMediaSource, loadedAdTagUri); - } - - player.setPlayWhenReady(playWhenReady); - player.seekTo(playbackStartPosition); - ((SimpleExoPlayer)player).prepare(testMediaSource, false, true); - } - - class CustomNotificationListener implements PlayerNotificationManager.NotificationListener { - - @Override - public void onNotificationPosted(int notificationId, Notification notification, - boolean ongoing) { - // TODO implement this - Log.e(TAG, "onNotificationPosted"); - } - } - - ////////////////////////////////////////////////////////////////////// - ////// Player.EventListener ////////////////////////////////////////// - - @Override - public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { - switch (playbackState) { - case Player.STATE_BUFFERING: - signalPlaybackBuffering(); - break; - case Player.STATE_ENDED: - signalPlaybackEnded(); - break; - case Player.STATE_READY: - // By the time we get here, it depends on playWhenReady to know if we're playing - if (playWhenReady) { - signalPlaybackStarted(); - } else { - // TODO implement this -// signalPlaybackPaused(); - } - case Player.STATE_IDLE: - signalPlaybackStopped(); - break; - } - } - - @Override - public void onRepeatModeChanged(int repeatMode) { - activityLock.lock(); - activityInitialized.signalAll(); - activityLock.unlock(); - } -} diff --git a/automatedtests/src/androidTestR2_12_1/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java b/automatedtests/src/androidTestR2_12_1/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java deleted file mode 100644 index d79153d8..00000000 --- a/automatedtests/src/androidTestR2_12_1/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java +++ /dev/null @@ -1,1990 +0,0 @@ -/* - * Copyright (C) 2017 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.google.android.exoplayer2.ext.ima; - -import static com.google.android.exoplayer2.util.Assertions.checkArgument; -import static com.google.android.exoplayer2.util.Assertions.checkNotNull; -import static com.google.android.exoplayer2.util.Assertions.checkState; -import static java.lang.Math.max; - -import android.content.Context; -import android.net.Uri; -import android.os.Handler; -import android.os.Looper; -import android.os.SystemClock; -import android.view.View; -import android.view.ViewGroup; -import androidx.annotation.IntDef; -import androidx.annotation.Nullable; -import androidx.annotation.VisibleForTesting; -import com.google.ads.interactivemedia.v3.api.AdDisplayContainer; -import com.google.ads.interactivemedia.v3.api.AdError; -import com.google.ads.interactivemedia.v3.api.AdErrorEvent; -import com.google.ads.interactivemedia.v3.api.AdErrorEvent.AdErrorListener; -import com.google.ads.interactivemedia.v3.api.AdEvent; -import com.google.ads.interactivemedia.v3.api.AdEvent.AdEventListener; -import com.google.ads.interactivemedia.v3.api.AdEvent.AdEventType; -import com.google.ads.interactivemedia.v3.api.AdPodInfo; -import com.google.ads.interactivemedia.v3.api.AdsLoader; -import com.google.ads.interactivemedia.v3.api.AdsLoader.AdsLoadedListener; -import com.google.ads.interactivemedia.v3.api.AdsManager; -import com.google.ads.interactivemedia.v3.api.AdsManagerLoadedEvent; -import com.google.ads.interactivemedia.v3.api.AdsRenderingSettings; -import com.google.ads.interactivemedia.v3.api.AdsRequest; -import com.google.ads.interactivemedia.v3.api.CompanionAdSlot; -import com.google.ads.interactivemedia.v3.api.FriendlyObstruction; -import com.google.ads.interactivemedia.v3.api.FriendlyObstructionPurpose; -import com.google.ads.interactivemedia.v3.api.ImaSdkFactory; -import com.google.ads.interactivemedia.v3.api.ImaSdkSettings; -import com.google.ads.interactivemedia.v3.api.UiElement; -import com.google.ads.interactivemedia.v3.api.player.AdMediaInfo; -import com.google.ads.interactivemedia.v3.api.player.ContentProgressProvider; -import com.google.ads.interactivemedia.v3.api.player.VideoAdPlayer; -import com.google.ads.interactivemedia.v3.api.player.VideoProgressUpdate; -import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.ExoPlaybackException; -import com.google.android.exoplayer2.ExoPlayerLibraryInfo; -import com.google.android.exoplayer2.Player; -import com.google.android.exoplayer2.Timeline; -import com.google.android.exoplayer2.source.MediaSourceFactory; -import com.google.android.exoplayer2.source.ads.AdPlaybackState; -import com.google.android.exoplayer2.source.ads.AdsMediaSource; -import com.google.android.exoplayer2.source.ads.AdsMediaSource.AdLoadException; -import com.google.android.exoplayer2.trackselection.TrackSelectionArray; -import com.google.android.exoplayer2.upstream.DataSpec; -import com.google.android.exoplayer2.util.Log; -import com.google.android.exoplayer2.util.MimeTypes; -import com.google.android.exoplayer2.util.Util; -import com.google.common.collect.BiMap; -import com.google.common.collect.HashBiMap; -import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableSet; -import java.io.IOException; -import java.lang.annotation.Documented; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.Set; -import org.checkerframework.checker.nullness.qual.MonotonicNonNull; - -/** - * {@link com.google.android.exoplayer2.source.ads.AdsLoader} using the IMA SDK. All methods must be - * called on the main thread. - * - *

The player instance that will play the loaded ads must be set before playback using {@link - * #setPlayer(Player)}. If the ads loader is no longer required, it must be released by calling - * {@link #release()}. - * - *

See https://developers.google.com/interactive-media-ads/docs/sdks/android/compatibility for - * information on compatible ad tag formats. Pass the ad tag URI when setting media item playback - * properties (if using the media item API) or as a {@link DataSpec} when constructing the {@link - * AdsMediaSource} (if using media sources directly). For the latter case, please note that this - * implementation delegates loading of the data spec to the IMA SDK, so range and headers - * specifications will be ignored in ad tag URIs. Literal ads responses can be encoded as data - * scheme data specs, for example, by constructing the data spec using a URI generated via {@link - * Util#getDataUriForString(String, String)}. - * - *

The IMA SDK can report obstructions to the ad view for accurate viewability measurement. This - * means that any overlay views that obstruct the ad overlay but are essential for playback need to - * be registered via the {@link AdViewProvider} passed to the {@link AdsMediaSource}. See the IMA - * SDK Open Measurement documentation for more information. - */ -public final class ImaAdsLoader - implements Player.EventListener, com.google.android.exoplayer2.source.ads.AdsLoader { - - static { - ExoPlayerLibraryInfo.registerModule("goog.exo.ima"); - } - - /** - * Builder for {@link ImaAdsLoader}. - */ - public static final class Builder { - - /** - * The default duration in milliseconds for which the player must buffer while preloading an ad - * group before that ad group is skipped and marked as having failed to load. - * - *

This value should be large enough not to trigger discarding the ad when it actually might - * load soon, but small enough so that user is not waiting for too long. - * - * @see #setAdPreloadTimeoutMs(long) - */ - public static final long DEFAULT_AD_PRELOAD_TIMEOUT_MS = 10 * C.MILLIS_PER_SECOND; - - private final Context context; - - @Nullable - private ImaSdkSettings imaSdkSettings; - @Nullable - private AdErrorListener adErrorListener; - @Nullable - private AdEventListener adEventListener; - @Nullable - private VideoAdPlayer.VideoAdPlayerCallback videoAdPlayerCallback; - @Nullable - private List adMediaMimeTypes; - @Nullable - private Set adUiElements; - @Nullable - private Collection companionAdSlots; - private long adPreloadTimeoutMs; - private int vastLoadTimeoutMs; - private int mediaLoadTimeoutMs; - private int mediaBitrate; - private boolean focusSkipButtonWhenAvailable; - private boolean playAdBeforeStartPosition; - private boolean debugModeEnabled; - private ImaUtil.ImaFactory imaFactory; - - /** - * Creates a new builder for {@link ImaAdsLoader}. - * - * @param context The context; - */ - public Builder(Context context) { - this.context = checkNotNull(context).getApplicationContext(); - adPreloadTimeoutMs = DEFAULT_AD_PRELOAD_TIMEOUT_MS; - vastLoadTimeoutMs = TIMEOUT_UNSET; - mediaLoadTimeoutMs = TIMEOUT_UNSET; - mediaBitrate = BITRATE_UNSET; - focusSkipButtonWhenAvailable = true; - playAdBeforeStartPosition = true; - imaFactory = new DefaultImaFactory(); - } - - /** - * Sets the IMA SDK settings. The provided settings instance's player type and version fields - * may be overwritten. - * - *

If this method is not called the default settings will be used. - * - * @param imaSdkSettings The {@link ImaSdkSettings}. - * @return This builder, for convenience. - */ - public Builder setImaSdkSettings(ImaSdkSettings imaSdkSettings) { - this.imaSdkSettings = checkNotNull(imaSdkSettings); - return this; - } - - /** - * Sets a listener for ad errors that will be passed to {@link AdsLoader#addAdErrorListener(AdErrorListener)} - * and {@link AdsManager#addAdErrorListener(AdErrorListener)}. - * - * @param adErrorListener The ad error listener. - * @return This builder, for convenience. - */ - public Builder setAdErrorListener(AdErrorListener adErrorListener) { - this.adErrorListener = checkNotNull(adErrorListener); - return this; - } - - /** - * Sets a listener for ad events that will be passed to {@link AdsManager#addAdEventListener(AdEventListener)}. - * - * @param adEventListener The ad event listener. - * @return This builder, for convenience. - */ - public Builder setAdEventListener(AdEventListener adEventListener) { - this.adEventListener = checkNotNull(adEventListener); - return this; - } - - /** - * Sets a callback to receive video ad player events. Note that these events are handled - * internally by the IMA SDK and this ads loader. For analytics and diagnostics, new - * implementations should generally use events from the top-level {@link Player} listeners - * instead of setting a callback via this method. - * - * @param videoAdPlayerCallback The callback to receive video ad player events. - * @return This builder, for convenience. - * @see com.google.ads.interactivemedia.v3.api.player.VideoAdPlayer.VideoAdPlayerCallback - */ - public Builder setVideoAdPlayerCallback( - VideoAdPlayer.VideoAdPlayerCallback videoAdPlayerCallback) { - this.videoAdPlayerCallback = checkNotNull(videoAdPlayerCallback); - return this; - } - - /** - * Sets the ad UI elements to be rendered by the IMA SDK. - * - * @param adUiElements The ad UI elements to be rendered by the IMA SDK. - * @return This builder, for convenience. - * @see AdsRenderingSettings#setUiElements(Set) - */ - public Builder setAdUiElements(Set adUiElements) { - this.adUiElements = ImmutableSet.copyOf(checkNotNull(adUiElements)); - return this; - } - - /** - * Sets the slots to use for companion ads, if they are present in the loaded ad. - * - * @param companionAdSlots The slots to use for companion ads. - * @return This builder, for convenience. - * @see AdDisplayContainer#setCompanionSlots(Collection) - */ - public Builder setCompanionAdSlots(Collection companionAdSlots) { - this.companionAdSlots = ImmutableList.copyOf(checkNotNull(companionAdSlots)); - return this; - } - - /** - * Sets the MIME types to prioritize for linear ad media. If not specified, MIME types supported - * by the {@link MediaSourceFactory adMediaSourceFactory} used to construct the {@link - * AdsMediaSource} will be used. - * - * @param adMediaMimeTypes The MIME types to prioritize for linear ad media. May contain {@link - * MimeTypes#APPLICATION_MPD}, {@link MimeTypes#APPLICATION_M3U8}, - * {@link MimeTypes#VIDEO_MP4}, {@link MimeTypes#VIDEO_WEBM}, {@link - * MimeTypes#VIDEO_H263}, {@link MimeTypes#AUDIO_MP4} and {@link - * MimeTypes#AUDIO_MPEG}. - * @return This builder, for convenience. - * @see AdsRenderingSettings#setMimeTypes(List) - */ - public Builder setAdMediaMimeTypes(List adMediaMimeTypes) { - this.adMediaMimeTypes = ImmutableList.copyOf(checkNotNull(adMediaMimeTypes)); - return this; - } - - /** - * Sets the duration in milliseconds for which the player must buffer while preloading an ad - * group before that ad group is skipped and marked as having failed to load. Pass {@link - * C#TIME_UNSET} if there should be no such timeout. The default value is {@value - * #DEFAULT_AD_PRELOAD_TIMEOUT_MS} ms. - * - *

The purpose of this timeout is to avoid playback getting stuck in the unexpected case - * that the IMA SDK does not load an ad break based on the player's reported content position. - * - * @param adPreloadTimeoutMs The timeout buffering duration in milliseconds, or {@link - * C#TIME_UNSET} for no timeout. - * @return This builder, for convenience. - */ - public Builder setAdPreloadTimeoutMs(long adPreloadTimeoutMs) { - checkArgument(adPreloadTimeoutMs == C.TIME_UNSET || adPreloadTimeoutMs > 0); - this.adPreloadTimeoutMs = adPreloadTimeoutMs; - return this; - } - - /** - * Sets the VAST load timeout, in milliseconds. - * - * @param vastLoadTimeoutMs The VAST load timeout, in milliseconds. - * @return This builder, for convenience. - * @see AdsRequest#setVastLoadTimeout(float) - */ - public Builder setVastLoadTimeoutMs(int vastLoadTimeoutMs) { - checkArgument(vastLoadTimeoutMs > 0); - this.vastLoadTimeoutMs = vastLoadTimeoutMs; - return this; - } - - /** - * Sets the ad media load timeout, in milliseconds. - * - * @param mediaLoadTimeoutMs The ad media load timeout, in milliseconds. - * @return This builder, for convenience. - * @see AdsRenderingSettings#setLoadVideoTimeout(int) - */ - public Builder setMediaLoadTimeoutMs(int mediaLoadTimeoutMs) { - checkArgument(mediaLoadTimeoutMs > 0); - this.mediaLoadTimeoutMs = mediaLoadTimeoutMs; - return this; - } - - /** - * Sets the media maximum recommended bitrate for ads, in bps. - * - * @param bitrate The media maximum recommended bitrate for ads, in bps. - * @return This builder, for convenience. - * @see AdsRenderingSettings#setBitrateKbps(int) - */ - public Builder setMaxMediaBitrate(int bitrate) { - checkArgument(bitrate > 0); - this.mediaBitrate = bitrate; - return this; - } - - /** - * Sets whether to focus the skip button (when available) on Android TV devices. The default - * setting is {@code true}. - * - * @param focusSkipButtonWhenAvailable Whether to focus the skip button (when available) on - * Android TV devices. - * @return This builder, for convenience. - * @see AdsRenderingSettings#setFocusSkipButtonWhenAvailable(boolean) - */ - public Builder setFocusSkipButtonWhenAvailable(boolean focusSkipButtonWhenAvailable) { - this.focusSkipButtonWhenAvailable = focusSkipButtonWhenAvailable; - return this; - } - - /** - * Sets whether to play an ad before the start position when beginning playback. If {@code - * true}, an ad will be played if there is one at or before the start position. If {@code - * false}, an ad will be played only if there is one exactly at the start position. The default - * setting is {@code true}. - * - * @param playAdBeforeStartPosition Whether to play an ad before the start position when - * beginning playback. - * @return This builder, for convenience. - */ - public Builder setPlayAdBeforeStartPosition(boolean playAdBeforeStartPosition) { - this.playAdBeforeStartPosition = playAdBeforeStartPosition; - return this; - } - - /** - * Sets whether to enable outputting verbose logs for the IMA extension and IMA SDK. The default - * value is {@code false}. This setting is intended for debugging only, and should not be - * enabled in production applications. - * - * @param debugModeEnabled Whether to enable outputting verbose logs for the IMA extension and - * IMA SDK. - * @return This builder, for convenience. - * @see ImaSdkSettings#setDebugMode(boolean) - */ - public Builder setDebugModeEnabled(boolean debugModeEnabled) { - this.debugModeEnabled = debugModeEnabled; - return this; - } - - @VisibleForTesting - /* package */ Builder setImaFactory(ImaUtil.ImaFactory imaFactory) { - this.imaFactory = checkNotNull(imaFactory); - return this; - } - - /** - * Returns a new {@link ImaAdsLoader} for the specified ad tag. - * - * @param adTagUri The URI of a compatible ad tag to load. See https://developers.google.com/interactive-media-ads/docs/sdks/android/compatibility - * for information on compatible ad tags. - * @return The new {@link ImaAdsLoader}. - * @deprecated Pass the ad tag URI when setting media item playback properties (if using the - * media item API) or as a {@link DataSpec} when constructing the {@link AdsMediaSource} (if - * using media sources directly). - */ - @Deprecated - public ImaAdsLoader buildForAdTag(Uri adTagUri) { - return new ImaAdsLoader( - context, - getConfiguration(), - imaFactory, - /* adTagUri= */ adTagUri, - /* adsResponse= */ null); - } - - /** - * Returns a new {@link ImaAdsLoader} with the specified sideloaded ads response. - * - * @param adsResponse The sideloaded VAST, VMAP, or ad rules response to be used instead of - * making a request via an ad tag URL. - * @return The new {@link ImaAdsLoader}. - * @deprecated Pass the ads response as a data URI when setting media item playback properties - * (if using the media item API) or as a {@link DataSpec} when constructing the {@link - * AdsMediaSource} (if using media sources directly). {@link Util#getDataUriForString(String, - * String)} can be used to construct a data URI from literal string ads response (with MIME type - * text/xml). - */ - @Deprecated - public ImaAdsLoader buildForAdsResponse(String adsResponse) { - return new ImaAdsLoader( - context, getConfiguration(), imaFactory, /* adTagUri= */ null, adsResponse); - } - - /** - * Returns a new {@link ImaAdsLoader}. - */ - public ImaAdsLoader build() { - return new ImaAdsLoader( - context, getConfiguration(), imaFactory, /* adTagUri= */ null, /* adsResponse= */ null); - } - - // TODO(internal: b/169646419): Remove/hide once the deprecated constructor has been removed. - /* package */ ImaUtil.Configuration getConfiguration() { - return new ImaUtil.Configuration( - adPreloadTimeoutMs, - vastLoadTimeoutMs, - mediaLoadTimeoutMs, - focusSkipButtonWhenAvailable, - playAdBeforeStartPosition, - mediaBitrate, - adMediaMimeTypes, - adUiElements, - companionAdSlots, - adErrorListener, - adEventListener, - videoAdPlayerCallback, - imaSdkSettings, - debugModeEnabled); - } - } - - private static final String TAG = "ImaAdsLoader"; - - private static final String IMA_SDK_SETTINGS_PLAYER_TYPE = "google/exo.ext.ima"; - private static final String IMA_SDK_SETTINGS_PLAYER_VERSION = ExoPlayerLibraryInfo.VERSION; - - /** - * Interval at which ad progress updates are provided to the IMA SDK, in milliseconds. 100 ms is - * the interval recommended by the IMA documentation. - * - * @see VideoAdPlayer.VideoAdPlayerCallback - */ - private static final int AD_PROGRESS_UPDATE_INTERVAL_MS = 100; - - /** - * The value used in {@link VideoProgressUpdate}s to indicate an unset duration. - */ - private static final long IMA_DURATION_UNSET = -1L; - - /** - * Threshold before the end of content at which IMA is notified that content is complete if the - * player buffers, in milliseconds. - */ - private static final long THRESHOLD_END_OF_CONTENT_MS = 5000; - /** - * Threshold before the start of an ad at which IMA is expected to be able to preload the ad, in - * milliseconds. - */ - private static final long THRESHOLD_AD_PRELOAD_MS = 4000; - /** - * The threshold below which ad cue points are treated as matching, in microseconds. - */ - private static final long THRESHOLD_AD_MATCH_US = 1000; - - private static final int TIMEOUT_UNSET = -1; - private static final int BITRATE_UNSET = -1; - - /** - * The state of ad playback. - */ - @Documented - @Retention(RetentionPolicy.SOURCE) - @IntDef({IMA_AD_STATE_NONE, IMA_AD_STATE_PLAYING, IMA_AD_STATE_PAUSED}) - private @interface ImaAdState { - - } - - /** - * The ad playback state when IMA is not playing an ad. - */ - private static final int IMA_AD_STATE_NONE = 0; - /** - * The ad playback state when IMA has called {@link ComponentListener#playAd(AdMediaInfo)} and not - * {@link ComponentListener##pauseAd(AdMediaInfo)}. - */ - private static final int IMA_AD_STATE_PLAYING = 1; - /** - * The ad playback state when IMA has called {@link ComponentListener#pauseAd(AdMediaInfo)} while - * playing an ad. - */ - private static final int IMA_AD_STATE_PAUSED = 2; - - private static final DataSpec EMPTY_AD_TAG_DATA_SPEC = new DataSpec(Uri.EMPTY); - - private final ImaUtil.Configuration configuration; - private final Context context; - private final ImaUtil.ImaFactory imaFactory; - @Nullable - private final Uri adTagUri; - @Nullable - private final String adsResponse; - private final ImaSdkSettings imaSdkSettings; - private final Timeline.Period period; - private final Handler handler; - private final ComponentListener componentListener; - private final List adCallbacks; - private final Runnable updateAdProgressRunnable; - private final BiMap adInfoByAdMediaInfo; - - private @MonotonicNonNull - AdDisplayContainer adDisplayContainer; - private @MonotonicNonNull - AdsLoader adsLoader; - private boolean wasSetPlayerCalled; - @Nullable - private Player nextPlayer; - @Nullable - private Object pendingAdRequestContext; - private List supportedMimeTypes; - @Nullable - private EventListener eventListener; - @Nullable - private Player player; - private DataSpec adTagDataSpec; - private VideoProgressUpdate lastContentProgress; - private VideoProgressUpdate lastAdProgress; - private int lastVolumePercent; - - @Nullable - private AdsManager adsManager; - private boolean isAdsManagerInitialized; - private boolean hasAdPlaybackState; - @Nullable - private AdLoadException pendingAdLoadError; - private Timeline timeline; - private long contentDurationMs; - private AdPlaybackState adPlaybackState; - - // Fields tracking IMA's state. - - /** - * Whether IMA has sent an ad event to pause content since the last resume content event. - */ - private boolean imaPausedContent; - /** - * The current ad playback state. - */ - private @ImaAdState - int imaAdState; - /** - * The current ad media info, or {@code null} if in state {@link #IMA_AD_STATE_NONE}. - */ - @Nullable - private AdMediaInfo imaAdMediaInfo; - /** - * The current ad info, or {@code null} if in state {@link #IMA_AD_STATE_NONE}. - */ - @Nullable - private AdInfo imaAdInfo; - /** - * Whether IMA has been notified that playback of content has finished. - */ - private boolean sentContentComplete; - - // Fields tracking the player/loader state. - - /** - * Whether the player is playing an ad. - */ - private boolean playingAd; - /** - * Whether the player is buffering an ad. - */ - private boolean bufferingAd; - /** - * If the player is playing an ad, stores the ad index in its ad group. {@link C#INDEX_UNSET} - * otherwise. - */ - private int playingAdIndexInAdGroup; - /** - * The ad info for a pending ad for which the media failed preparation, or {@code null} if no - * pending ads have failed to prepare. - */ - @Nullable - private AdInfo pendingAdPrepareErrorAdInfo; - /** - * If a content period has finished but IMA has not yet called {@link - * ComponentListener#playAd(AdMediaInfo)}, stores the value of {@link - * SystemClock#elapsedRealtime()} when the content stopped playing. This can be used to determine - * a fake, increasing content position. {@link C#TIME_UNSET} otherwise. - */ - private long fakeContentProgressElapsedRealtimeMs; - /** - * If {@link #fakeContentProgressElapsedRealtimeMs} is set, stores the offset from which the - * content progress should increase. {@link C#TIME_UNSET} otherwise. - */ - private long fakeContentProgressOffsetMs; - /** - * Stores the pending content position when a seek operation was intercepted to play an ad. - */ - private long pendingContentPositionMs; - /** - * Whether {@link ComponentListener#getContentProgress()} has sent {@link - * #pendingContentPositionMs} to IMA. - */ - private boolean sentPendingContentPositionMs; - /** - * Stores the real time in milliseconds at which the player started buffering, possibly due to not - * having preloaded an ad, or {@link C#TIME_UNSET} if not applicable. - */ - private long waitingForPreloadElapsedRealtimeMs; - - /** - * Creates a new IMA ads loader. - * - *

If you need to customize the ad request, use {@link ImaAdsLoader.Builder} instead. - * - * @param context The context. - * @param adTagUri The {@link Uri} of an ad tag compatible with the Android IMA SDK. See - * https://developers.google.com/interactive-media-ads/docs/sdks/android/compatibility - * for more information. - * @deprecated Use {@link Builder} to create an instance. Pass the ad tag URI when setting media - * item playback properties (if using the media item API) or as a {@link DataSpec} when - * constructing the {@link AdsMediaSource} (if using media sources directly). - */ - @Deprecated - public ImaAdsLoader(Context context, Uri adTagUri) { - this( - context, - new Builder(context).getConfiguration(), - new DefaultImaFactory(), - adTagUri, - /* adsResponse= */ null); - } - - @SuppressWarnings({"nullness:argument.type.incompatible", "methodref.receiver.bound.invalid"}) - private ImaAdsLoader( - Context context, - ImaUtil.Configuration configuration, - ImaUtil.ImaFactory imaFactory, - @Nullable Uri adTagUri, - @Nullable String adsResponse) { - this.context = context.getApplicationContext(); - this.configuration = configuration; - this.imaFactory = imaFactory; - this.adTagUri = adTagUri; - this.adsResponse = adsResponse; - @Nullable ImaSdkSettings imaSdkSettings = configuration.imaSdkSettings; - if (imaSdkSettings == null) { - imaSdkSettings = imaFactory.createImaSdkSettings(); - if (configuration.debugModeEnabled) { - imaSdkSettings.setDebugMode(true); - } - } - imaSdkSettings.setPlayerType(IMA_SDK_SETTINGS_PLAYER_TYPE); - imaSdkSettings.setPlayerVersion(IMA_SDK_SETTINGS_PLAYER_VERSION); - this.imaSdkSettings = imaSdkSettings; - period = new Timeline.Period(); - handler = Util.createHandler(getImaLooper(), /* callback= */ null); - componentListener = new ComponentListener(); - adCallbacks = new ArrayList<>(/* initialCapacity= */ 1); - if (configuration.applicationVideoAdPlayerCallback != null) { - adCallbacks.add(configuration.applicationVideoAdPlayerCallback); - } - updateAdProgressRunnable = this::updateAdProgress; - adInfoByAdMediaInfo = HashBiMap.create(); - supportedMimeTypes = Collections.emptyList(); - adTagDataSpec = EMPTY_AD_TAG_DATA_SPEC; - lastContentProgress = VideoProgressUpdate.VIDEO_TIME_NOT_READY; - lastAdProgress = VideoProgressUpdate.VIDEO_TIME_NOT_READY; - fakeContentProgressElapsedRealtimeMs = C.TIME_UNSET; - fakeContentProgressOffsetMs = C.TIME_UNSET; - pendingContentPositionMs = C.TIME_UNSET; - waitingForPreloadElapsedRealtimeMs = C.TIME_UNSET; - contentDurationMs = C.TIME_UNSET; - timeline = Timeline.EMPTY; - adPlaybackState = AdPlaybackState.NONE; - } - - /** - * Returns the underlying {@link AdsLoader} wrapped by this instance, or {@code null} if ads have - * not been requested yet. - */ - @Nullable - public AdsLoader getAdsLoader() { - return adsLoader; - } - - /** - * Returns the {@link AdDisplayContainer} used by this loader, or {@code null} if ads have not - * been requested yet. - * - *

Note: any video controls overlays registered via {@link - * AdDisplayContainer#registerFriendlyObstruction(FriendlyObstruction)} will be unregistered - * automatically when the media source detaches from this instance. It is therefore necessary to - * re-register views each time the ads loader is reused. Alternatively, provide overlay views via - * the {@link com.google.android.exoplayer2.source.ads.AdsLoader.AdViewProvider} when creating the - * media source to benefit from automatic registration. - */ - @Nullable - public AdDisplayContainer getAdDisplayContainer() { - return adDisplayContainer; - } - - /** - * Requests ads, if they have not already been requested. Must be called on the main thread. - * - *

Ads will be requested automatically when the player is prepared if this method has not been - * called, so it is only necessary to call this method if you want to request ads before preparing - * the player. - * - * @param adViewGroup A {@link ViewGroup} on top of the player that will show any ad UI, or {@code - * null} if playing audio-only ads. - * @deprecated Use {@link #requestAds(DataSpec, ViewGroup)}, specifying the ad tag data spec to - * request, and migrate off deprecated builder methods/constructor that require an ad tag or ads - * response. - */ - @Deprecated - public void requestAds(@Nullable ViewGroup adViewGroup) { - requestAds(adTagDataSpec, adViewGroup); - } - - /** - * Requests ads, if they have not already been requested. Must be called on the main thread. - * - *

Ads will be requested automatically when the player is prepared if this method has not been - * called, so it is only necessary to call this method if you want to request ads before preparing - * the player. - * - * @param adTagDataSpec The data specification of the ad tag to load. See class javadoc for - * information about compatible ad tag formats. - * @param adViewGroup A {@link ViewGroup} on top of the player that will show any ad UI, or - * {@code null} if playing audio-only ads. - */ - public void requestAds(DataSpec adTagDataSpec, @Nullable ViewGroup adViewGroup) { - if (hasAdPlaybackState || adsManager != null || pendingAdRequestContext != null) { - // Ads have already been requested. - return; - } - - if (EMPTY_AD_TAG_DATA_SPEC.equals(adTagDataSpec)) { - // Handle deprecated ways of specifying the ad tag. - if (adTagUri != null) { - adTagDataSpec = new DataSpec(adTagUri); - } else if (adsResponse != null) { - adTagDataSpec = new DataSpec(Util.getDataUriForString(adsResponse, "text/xml")); - } else { - throw new IllegalStateException(); - } - } - - AdsRequest request; - try { - request = ImaUtil.getAdsRequestForAdTagDataSpec(imaFactory, adTagDataSpec); - } catch (IOException e) { - hasAdPlaybackState = true; - updateAdPlaybackState(); - pendingAdLoadError = AdLoadException.createForAllAds(e); - maybeNotifyPendingAdLoadError(); - return; - } - this.adTagDataSpec = adTagDataSpec; - pendingAdRequestContext = new Object(); - request.setUserRequestContext(pendingAdRequestContext); - if (configuration.vastLoadTimeoutMs != TIMEOUT_UNSET) { - request.setVastLoadTimeout(configuration.vastLoadTimeoutMs); - } - request.setContentProgressProvider(componentListener); - - if (adViewGroup != null) { - adDisplayContainer = - imaFactory.createAdDisplayContainer(adViewGroup, /* player= */ componentListener); - } else { - adDisplayContainer = - imaFactory.createAudioAdDisplayContainer(context, /* player= */ componentListener); - } - if (configuration.companionAdSlots != null) { - adDisplayContainer.setCompanionSlots(configuration.companionAdSlots); - } - - adsLoader = imaFactory.createAdsLoader(context, imaSdkSettings, adDisplayContainer); - adsLoader.addAdErrorListener(componentListener); - if (configuration.applicationAdErrorListener != null) { - adsLoader.addAdErrorListener(configuration.applicationAdErrorListener); - } - adsLoader.addAdsLoadedListener(componentListener); - adsLoader.requestAds(request); - } - - /** - * Skips the current ad. - * - *

This method is intended for apps that play audio-only ads and so need to provide their own - * UI for users to skip skippable ads. Apps showing video ads should not call this method, as the - * IMA SDK provides the UI to skip ads in the ad view group passed via {@link AdViewProvider}. - */ - public void skipAd() { - if (adsManager != null) { - adsManager.skip(); - } - } - - // com.google.android.exoplayer2.source.ads.AdsLoader implementation. - - @Override - public void setPlayer(@Nullable Player player) { - checkState(Looper.myLooper() == getImaLooper()); - checkState(player == null || player.getApplicationLooper() == getImaLooper()); - nextPlayer = player; - wasSetPlayerCalled = true; - } - - @Override - public void setSupportedContentTypes(@C.ContentType int... contentTypes) { - List supportedMimeTypes = new ArrayList<>(); - for (@C.ContentType int contentType : contentTypes) { - // IMA does not support Smooth Streaming ad media. - if (contentType == C.TYPE_DASH) { - supportedMimeTypes.add(MimeTypes.APPLICATION_MPD); - } else if (contentType == C.TYPE_HLS) { - supportedMimeTypes.add(MimeTypes.APPLICATION_M3U8); - } else if (contentType == C.TYPE_OTHER) { - supportedMimeTypes.addAll( - Arrays.asList( - MimeTypes.VIDEO_MP4, - MimeTypes.VIDEO_WEBM, - MimeTypes.VIDEO_H263, - MimeTypes.AUDIO_MP4, - MimeTypes.AUDIO_MPEG)); - } - } - this.supportedMimeTypes = Collections.unmodifiableList(supportedMimeTypes); - } - - @Override - public void setAdTagDataSpec(DataSpec adTagDataSpec) { - this.adTagDataSpec = adTagDataSpec; - } - - @Override - public void start(EventListener eventListener, AdViewProvider adViewProvider) { - checkState( - wasSetPlayerCalled, "Set player using adsLoader.setPlayer before preparing the player."); - player = nextPlayer; - if (player == null) { - return; - } - player.addListener(this); - boolean playWhenReady = player.getPlayWhenReady(); - this.eventListener = eventListener; - lastVolumePercent = 0; - lastAdProgress = VideoProgressUpdate.VIDEO_TIME_NOT_READY; - lastContentProgress = VideoProgressUpdate.VIDEO_TIME_NOT_READY; - maybeNotifyPendingAdLoadError(); - if (hasAdPlaybackState) { - // Pass the ad playback state to the player, and resume ads if necessary. - eventListener.onAdPlaybackState(adPlaybackState); - if (adsManager != null && imaPausedContent && playWhenReady) { - adsManager.resume(); - } - } else if (adsManager != null) { - adPlaybackState = ImaUtil.getInitialAdPlaybackStateForCuePoints(adsManager.getAdCuePoints()); - updateAdPlaybackState(); - } else { - // Ads haven't loaded yet, so request them. - requestAds(adTagDataSpec, adViewProvider.getAdViewGroup()); - } - if (adDisplayContainer != null) { - for (OverlayInfo overlayInfo : adViewProvider.getAdOverlayInfos()) { - adDisplayContainer.registerFriendlyObstruction( - imaFactory.createFriendlyObstruction( - overlayInfo.view, - ImaUtil.getFriendlyObstructionPurpose(overlayInfo.purpose), - overlayInfo.reasonDetail)); - } - } - } - - @Override - public void stop() { - @Nullable Player player = this.player; - if (player == null) { - return; - } - if (adsManager != null && imaPausedContent) { - adsManager.pause(); - adPlaybackState = - adPlaybackState.withAdResumePositionUs( - playingAd ? C.msToUs(player.getCurrentPosition()) : 0); - } - lastVolumePercent = getPlayerVolumePercent(); - lastAdProgress = getAdVideoProgressUpdate(); - lastContentProgress = getContentVideoProgressUpdate(); - if (adDisplayContainer != null) { - adDisplayContainer.unregisterAllFriendlyObstructions(); - } - player.removeListener(this); - this.player = null; - eventListener = null; - } - - @Override - public void release() { - pendingAdRequestContext = null; - destroyAdsManager(); - if (adsLoader != null) { - adsLoader.removeAdsLoadedListener(componentListener); - adsLoader.removeAdErrorListener(componentListener); - if (configuration.applicationAdErrorListener != null) { - adsLoader.removeAdErrorListener(configuration.applicationAdErrorListener); - } - } - imaPausedContent = false; - imaAdState = IMA_AD_STATE_NONE; - imaAdMediaInfo = null; - stopUpdatingAdProgress(); - imaAdInfo = null; - pendingAdLoadError = null; - adPlaybackState = AdPlaybackState.NONE; - hasAdPlaybackState = true; - updateAdPlaybackState(); - } - - @Override - public void handlePrepareComplete(int adGroupIndex, int adIndexInAdGroup) { - AdInfo adInfo = new AdInfo(adGroupIndex, adIndexInAdGroup); - if (configuration.debugModeEnabled) { - Log.d(TAG, "Prepared ad " + adInfo); - } - @Nullable AdMediaInfo adMediaInfo = adInfoByAdMediaInfo.inverse().get(adInfo); - if (adMediaInfo != null) { - for (int i = 0; i < adCallbacks.size(); i++) { - adCallbacks.get(i).onLoaded(adMediaInfo); - } - } else { - Log.w(TAG, "Unexpected prepared ad " + adInfo); - } - } - - @Override - public void handlePrepareError(int adGroupIndex, int adIndexInAdGroup, IOException exception) { - if (player == null) { - return; - } - try { - handleAdPrepareError(adGroupIndex, adIndexInAdGroup, exception); - } catch (RuntimeException e) { - maybeNotifyInternalError("handlePrepareError", e); - } - } - - // Player.EventListener implementation. - - @Override - public void onTimelineChanged(Timeline timeline, @Player.TimelineChangeReason int reason) { - if (timeline.isEmpty()) { - // The player is being reset or contains no media. - return; - } - checkArgument(timeline.getPeriodCount() == 1); - this.timeline = timeline; - long contentDurationUs = timeline.getPeriod(/* periodIndex= */ 0, period).durationUs; - contentDurationMs = C.usToMs(contentDurationUs); - if (contentDurationUs != C.TIME_UNSET) { - adPlaybackState = adPlaybackState.withContentDurationUs(contentDurationUs); - } - @Nullable AdsManager adsManager = this.adsManager; - if (!isAdsManagerInitialized && adsManager != null) { - isAdsManagerInitialized = true; - @Nullable AdsRenderingSettings adsRenderingSettings = setupAdsRendering(); - if (adsRenderingSettings == null) { - // There are no ads to play. - destroyAdsManager(); - } else { - adsManager.init(adsRenderingSettings); - adsManager.start(); - if (configuration.debugModeEnabled) { - Log.d(TAG, "Initialized with ads rendering settings: " + adsRenderingSettings); - } - } - updateAdPlaybackState(); - } - handleTimelineOrPositionChanged(); - } - - @Override - public void onPositionDiscontinuity(@Player.DiscontinuityReason int reason) { - handleTimelineOrPositionChanged(); - } - - @Override - public void onPlaybackStateChanged(@Player.State int playbackState) { - @Nullable Player player = this.player; - if (adsManager == null || player == null) { - return; - } - - if (playbackState == Player.STATE_BUFFERING && !player.isPlayingAd()) { - // Check whether we are waiting for an ad to preload. - int adGroupIndex = getLoadingAdGroupIndex(); - if (adGroupIndex == C.INDEX_UNSET) { - return; - } - AdPlaybackState.AdGroup adGroup = adPlaybackState.adGroups[adGroupIndex]; - if (adGroup.count != C.LENGTH_UNSET - && adGroup.count != 0 - && adGroup.states[0] != AdPlaybackState.AD_STATE_UNAVAILABLE) { - // An ad is available already so we must be buffering for some other reason. - return; - } - long adGroupTimeMs = C.usToMs(adPlaybackState.adGroupTimesUs[adGroupIndex]); - long contentPositionMs = getContentPeriodPositionMs(player, timeline, period); - long timeUntilAdMs = adGroupTimeMs - contentPositionMs; - if (timeUntilAdMs < configuration.adPreloadTimeoutMs) { - waitingForPreloadElapsedRealtimeMs = SystemClock.elapsedRealtime(); - } - } else if (playbackState == Player.STATE_READY) { - waitingForPreloadElapsedRealtimeMs = C.TIME_UNSET; - } - - handlePlayerStateChanged(player.getPlayWhenReady(), playbackState); - } - - @Override - public void onPlayWhenReadyChanged( - boolean playWhenReady, @Player.PlayWhenReadyChangeReason int reason) { - if (adsManager == null || player == null) { - return; - } - - if (imaAdState == IMA_AD_STATE_PLAYING && !playWhenReady) { - adsManager.pause(); - return; - } - - if (imaAdState == IMA_AD_STATE_PAUSED && playWhenReady) { - adsManager.resume(); - return; - } - handlePlayerStateChanged(playWhenReady, player.getPlaybackState()); - } - - @Override - public void onPlayerError(ExoPlaybackException error) { - if (imaAdState != IMA_AD_STATE_NONE) { - AdMediaInfo adMediaInfo = checkNotNull(imaAdMediaInfo); - for (int i = 0; i < adCallbacks.size(); i++) { - adCallbacks.get(i).onError(adMediaInfo); - } - } - } - - // Internal methods. - - /** - * Configures ads rendering for starting playback, returning the settings for the IMA SDK or - * {@code null} if no ads should play. - */ - @Nullable - private AdsRenderingSettings setupAdsRendering() { - AdsRenderingSettings adsRenderingSettings = imaFactory.createAdsRenderingSettings(); - adsRenderingSettings.setEnablePreloading(true); - adsRenderingSettings.setMimeTypes( - configuration.adMediaMimeTypes != null - ? configuration.adMediaMimeTypes - : supportedMimeTypes); - if (configuration.mediaLoadTimeoutMs != TIMEOUT_UNSET) { - adsRenderingSettings.setLoadVideoTimeout(configuration.mediaLoadTimeoutMs); - } - if (configuration.mediaBitrate != BITRATE_UNSET) { - adsRenderingSettings.setBitrateKbps(configuration.mediaBitrate / 1000); - } - adsRenderingSettings.setFocusSkipButtonWhenAvailable( - configuration.focusSkipButtonWhenAvailable); - if (configuration.adUiElements != null) { - adsRenderingSettings.setUiElements(configuration.adUiElements); - } - - // Skip ads based on the start position as required. - long[] adGroupTimesUs = adPlaybackState.adGroupTimesUs; - long contentPositionMs = getContentPeriodPositionMs(checkNotNull(player), timeline, period); - int adGroupForPositionIndex = - adPlaybackState.getAdGroupIndexForPositionUs( - C.msToUs(contentPositionMs), C.msToUs(contentDurationMs)); - if (adGroupForPositionIndex != C.INDEX_UNSET) { - boolean playAdWhenStartingPlayback = - configuration.playAdBeforeStartPosition - || adGroupTimesUs[adGroupForPositionIndex] == C.msToUs(contentPositionMs); - if (!playAdWhenStartingPlayback) { - adGroupForPositionIndex++; - } else if (hasMidrollAdGroups(adGroupTimesUs)) { - // Provide the player's initial position to trigger loading and playing the ad. If there are - // no midrolls, we are playing a preroll and any pending content position wouldn't be - // cleared. - pendingContentPositionMs = contentPositionMs; - } - if (adGroupForPositionIndex > 0) { - for (int i = 0; i < adGroupForPositionIndex; i++) { - adPlaybackState = adPlaybackState.withSkippedAdGroup(i); - } - if (adGroupForPositionIndex == adGroupTimesUs.length) { - // We don't need to play any ads. Because setPlayAdsAfterTime does not discard non-VMAP - // ads, we signal that no ads will render so the caller can destroy the ads manager. - return null; - } - long adGroupForPositionTimeUs = adGroupTimesUs[adGroupForPositionIndex]; - long adGroupBeforePositionTimeUs = adGroupTimesUs[adGroupForPositionIndex - 1]; - if (adGroupForPositionTimeUs == C.TIME_END_OF_SOURCE) { - // Play the postroll by offsetting the start position just past the last non-postroll ad. - adsRenderingSettings.setPlayAdsAfterTime( - (double) adGroupBeforePositionTimeUs / C.MICROS_PER_SECOND + 1d); - } else { - // Play ads after the midpoint between the ad to play and the one before it, to avoid - // issues with rounding one of the two ad times. - double midpointTimeUs = (adGroupForPositionTimeUs + adGroupBeforePositionTimeUs) / 2d; - adsRenderingSettings.setPlayAdsAfterTime(midpointTimeUs / C.MICROS_PER_SECOND); - } - } - } - return adsRenderingSettings; - } - - private VideoProgressUpdate getContentVideoProgressUpdate() { - if (player == null) { - return lastContentProgress; - } - boolean hasContentDuration = contentDurationMs != C.TIME_UNSET; - long contentPositionMs; - if (pendingContentPositionMs != C.TIME_UNSET) { - sentPendingContentPositionMs = true; - contentPositionMs = pendingContentPositionMs; - } else if (fakeContentProgressElapsedRealtimeMs != C.TIME_UNSET) { - long elapsedSinceEndMs = SystemClock.elapsedRealtime() - fakeContentProgressElapsedRealtimeMs; - contentPositionMs = fakeContentProgressOffsetMs + elapsedSinceEndMs; - } else if (imaAdState == IMA_AD_STATE_NONE && !playingAd && hasContentDuration) { - contentPositionMs = getContentPeriodPositionMs(player, timeline, period); - } else { - return VideoProgressUpdate.VIDEO_TIME_NOT_READY; - } - long contentDurationMs = hasContentDuration ? this.contentDurationMs : IMA_DURATION_UNSET; - return new VideoProgressUpdate(contentPositionMs, contentDurationMs); - } - - private VideoProgressUpdate getAdVideoProgressUpdate() { - if (player == null) { - return lastAdProgress; - } else if (imaAdState != IMA_AD_STATE_NONE && playingAd) { - long adDuration = player.getDuration(); - return adDuration == C.TIME_UNSET - ? VideoProgressUpdate.VIDEO_TIME_NOT_READY - : new VideoProgressUpdate(player.getCurrentPosition(), adDuration); - } else { - return VideoProgressUpdate.VIDEO_TIME_NOT_READY; - } - } - - private void updateAdProgress() { - VideoProgressUpdate videoProgressUpdate = getAdVideoProgressUpdate(); - AdMediaInfo adMediaInfo = checkNotNull(imaAdMediaInfo); - for (int i = 0; i < adCallbacks.size(); i++) { - adCallbacks.get(i).onAdProgress(adMediaInfo, videoProgressUpdate); - } - handler.removeCallbacks(updateAdProgressRunnable); - handler.postDelayed(updateAdProgressRunnable, AD_PROGRESS_UPDATE_INTERVAL_MS); - } - - private void stopUpdatingAdProgress() { - handler.removeCallbacks(updateAdProgressRunnable); - } - - private int getPlayerVolumePercent() { - @Nullable Player player = this.player; - if (player == null) { - return lastVolumePercent; - } - - @Nullable Player.AudioComponent audioComponent = player.getAudioComponent(); - if (audioComponent != null) { - return (int) (audioComponent.getVolume() * 100); - } - - // Check for a selected track using an audio renderer. - TrackSelectionArray trackSelections = player.getCurrentTrackSelections(); - for (int i = 0; i < player.getRendererCount() && i < trackSelections.length; i++) { - if (player.getRendererType(i) == C.TRACK_TYPE_AUDIO && trackSelections.get(i) != null) { - return 100; - } - } - return 0; - } - - private void handleAdEvent(AdEvent adEvent) { - if (adsManager == null) { - // Drop events after release. - return; - } - switch (adEvent.getType()) { - case AD_BREAK_FETCH_ERROR: - String adGroupTimeSecondsString = checkNotNull(adEvent.getAdData().get("adBreakTime")); - if (configuration.debugModeEnabled) { - Log.d(TAG, "Fetch error for ad at " + adGroupTimeSecondsString + " seconds"); - } - double adGroupTimeSeconds = Double.parseDouble(adGroupTimeSecondsString); - int adGroupIndex = - adGroupTimeSeconds == -1.0 - ? adPlaybackState.adGroupCount - 1 - : getAdGroupIndexForCuePointTimeSeconds(adGroupTimeSeconds); - markAdGroupInErrorStateAndClearPendingContentPosition(adGroupIndex); - break; - case CONTENT_PAUSE_REQUESTED: - // After CONTENT_PAUSE_REQUESTED, IMA will playAd/pauseAd/stopAd to show one or more ads - // before sending CONTENT_RESUME_REQUESTED. - imaPausedContent = true; - pauseContentInternal(); - break; - case TAPPED: - if (eventListener != null) { - eventListener.onAdTapped(); - } - break; - case CLICKED: - if (eventListener != null) { - eventListener.onAdClicked(); - } - break; - case CONTENT_RESUME_REQUESTED: - imaPausedContent = false; - resumeContentInternal(); - break; - case LOG: - Map adData = adEvent.getAdData(); - String message = "AdEvent: " + adData; - Log.i(TAG, message); - break; - default: - break; - } - } - - private void pauseContentInternal() { - imaAdState = IMA_AD_STATE_NONE; - if (sentPendingContentPositionMs) { - pendingContentPositionMs = C.TIME_UNSET; - sentPendingContentPositionMs = false; - } - } - - private void resumeContentInternal() { - if (imaAdInfo != null) { - adPlaybackState = adPlaybackState.withSkippedAdGroup(imaAdInfo.adGroupIndex); - updateAdPlaybackState(); - } else if (adPlaybackState.adGroupCount == 1 && adPlaybackState.adGroupTimesUs[0] == 0) { - // For incompatible VPAID ads with one preroll, content is resumed immediately. In this case - // we haven't received ad info (the ad never loaded), but there is only one ad group to skip. - adPlaybackState = adPlaybackState.withSkippedAdGroup(/* adGroupIndex= */ 0); - updateAdPlaybackState(); - } - } - - private void handlePlayerStateChanged(boolean playWhenReady, @Player.State int playbackState) { - if (playingAd && imaAdState == IMA_AD_STATE_PLAYING) { - if (!bufferingAd && playbackState == Player.STATE_BUFFERING) { - AdMediaInfo adMediaInfo = checkNotNull(imaAdMediaInfo); - for (int i = 0; i < adCallbacks.size(); i++) { - adCallbacks.get(i).onBuffering(adMediaInfo); - } - stopUpdatingAdProgress(); - } else if (bufferingAd && playbackState == Player.STATE_READY) { - bufferingAd = false; - updateAdProgress(); - } - } - - if (imaAdState == IMA_AD_STATE_NONE - && playbackState == Player.STATE_BUFFERING - && playWhenReady) { - ensureSentContentCompleteIfAtEndOfStream(); - } else if (imaAdState != IMA_AD_STATE_NONE && playbackState == Player.STATE_ENDED) { - AdMediaInfo adMediaInfo = checkNotNull(imaAdMediaInfo); - if (adMediaInfo == null) { - Log.w(TAG, "onEnded without ad media info"); - } else { - for (int i = 0; i < adCallbacks.size(); i++) { - adCallbacks.get(i).onEnded(adMediaInfo); - } - } - if (configuration.debugModeEnabled) { - Log.d(TAG, "VideoAdPlayerCallback.onEnded in onPlaybackStateChanged"); - } - } - } - - private void handleTimelineOrPositionChanged() { - @Nullable Player player = this.player; - if (adsManager == null || player == null) { - return; - } - if (!playingAd && !player.isPlayingAd()) { - ensureSentContentCompleteIfAtEndOfStream(); - if (!sentContentComplete && !timeline.isEmpty()) { - long positionMs = getContentPeriodPositionMs(player, timeline, period); - timeline.getPeriod(/* periodIndex= */ 0, period); - int newAdGroupIndex = period.getAdGroupIndexForPositionUs(C.msToUs(positionMs)); - if (newAdGroupIndex != C.INDEX_UNSET) { - sentPendingContentPositionMs = false; - pendingContentPositionMs = positionMs; - } - } - } - - boolean wasPlayingAd = playingAd; - int oldPlayingAdIndexInAdGroup = playingAdIndexInAdGroup; - playingAd = player.isPlayingAd(); - playingAdIndexInAdGroup = playingAd ? player.getCurrentAdIndexInAdGroup() : C.INDEX_UNSET; - boolean adFinished = wasPlayingAd && playingAdIndexInAdGroup != oldPlayingAdIndexInAdGroup; - if (adFinished) { - // IMA is waiting for the ad playback to finish so invoke the callback now. - // Either CONTENT_RESUME_REQUESTED will be passed next, or playAd will be called again. - @Nullable AdMediaInfo adMediaInfo = imaAdMediaInfo; - if (adMediaInfo == null) { - Log.w(TAG, "onEnded without ad media info"); - } else { - for (int i = 0; i < adCallbacks.size(); i++) { - adCallbacks.get(i).onEnded(adMediaInfo); - } - } - if (configuration.debugModeEnabled) { - Log.d(TAG, "VideoAdPlayerCallback.onEnded in onTimelineChanged/onPositionDiscontinuity"); - } - } - if (!sentContentComplete && !wasPlayingAd && playingAd && imaAdState == IMA_AD_STATE_NONE) { - int adGroupIndex = player.getCurrentAdGroupIndex(); - if (adPlaybackState.adGroupTimesUs[adGroupIndex] == C.TIME_END_OF_SOURCE) { - sendContentComplete(); - } else { - // IMA hasn't called playAd yet, so fake the content position. - fakeContentProgressElapsedRealtimeMs = SystemClock.elapsedRealtime(); - fakeContentProgressOffsetMs = C.usToMs(adPlaybackState.adGroupTimesUs[adGroupIndex]); - if (fakeContentProgressOffsetMs == C.TIME_END_OF_SOURCE) { - fakeContentProgressOffsetMs = contentDurationMs; - } - } - } - } - - private void loadAdInternal(AdMediaInfo adMediaInfo, AdPodInfo adPodInfo) { - if (adsManager == null) { - // Drop events after release. - if (configuration.debugModeEnabled) { - Log.d( - TAG, - "loadAd after release " + getAdMediaInfoString(adMediaInfo) + ", ad pod " + adPodInfo); - } - return; - } - - int adGroupIndex = getAdGroupIndexForAdPod(adPodInfo); - int adIndexInAdGroup = adPodInfo.getAdPosition() - 1; - AdInfo adInfo = new AdInfo(adGroupIndex, adIndexInAdGroup); - adInfoByAdMediaInfo.put(adMediaInfo, adInfo); - if (configuration.debugModeEnabled) { - Log.d(TAG, "loadAd " + getAdMediaInfoString(adMediaInfo)); - } - if (adPlaybackState.isAdInErrorState(adGroupIndex, adIndexInAdGroup)) { - // We have already marked this ad as having failed to load, so ignore the request. IMA will - // timeout after its media load timeout. - return; - } - - // The ad count may increase on successive loads of ads in the same ad pod, for example, due to - // separate requests for ad tags with multiple ads within the ad pod completing after an earlier - // ad has loaded. See also https://github.com/google/ExoPlayer/issues/7477. - AdPlaybackState.AdGroup adGroup = adPlaybackState.adGroups[adInfo.adGroupIndex]; - adPlaybackState = - adPlaybackState.withAdCount( - adInfo.adGroupIndex, max(adPodInfo.getTotalAds(), adGroup.states.length)); - adGroup = adPlaybackState.adGroups[adInfo.adGroupIndex]; - for (int i = 0; i < adIndexInAdGroup; i++) { - // Any preceding ads that haven't loaded are not going to load. - if (adGroup.states[i] == AdPlaybackState.AD_STATE_UNAVAILABLE) { - adPlaybackState = adPlaybackState.withAdLoadError(adGroupIndex, /* adIndexInAdGroup= */ i); - } - } - - Uri adUri = Uri.parse(adMediaInfo.getUrl()); - adPlaybackState = - adPlaybackState.withAdUri(adInfo.adGroupIndex, adInfo.adIndexInAdGroup, adUri); - updateAdPlaybackState(); - } - - private void playAdInternal(AdMediaInfo adMediaInfo) { - if (configuration.debugModeEnabled) { - Log.d(TAG, "playAd " + getAdMediaInfoString(adMediaInfo)); - } - if (adsManager == null) { - // Drop events after release. - return; - } - - if (imaAdState == IMA_AD_STATE_PLAYING) { - // IMA does not always call stopAd before resuming content. - // See [Internal: b/38354028]. - Log.w(TAG, "Unexpected playAd without stopAd"); - } - - if (imaAdState == IMA_AD_STATE_NONE) { - // IMA is requesting to play the ad, so stop faking the content position. - fakeContentProgressElapsedRealtimeMs = C.TIME_UNSET; - fakeContentProgressOffsetMs = C.TIME_UNSET; - imaAdState = IMA_AD_STATE_PLAYING; - imaAdMediaInfo = adMediaInfo; - imaAdInfo = checkNotNull(adInfoByAdMediaInfo.get(adMediaInfo)); - for (int i = 0; i < adCallbacks.size(); i++) { - adCallbacks.get(i).onPlay(adMediaInfo); - } - if (pendingAdPrepareErrorAdInfo != null && pendingAdPrepareErrorAdInfo.equals(imaAdInfo)) { - pendingAdPrepareErrorAdInfo = null; - for (int i = 0; i < adCallbacks.size(); i++) { - adCallbacks.get(i).onError(adMediaInfo); - } - } - updateAdProgress(); - } else { - imaAdState = IMA_AD_STATE_PLAYING; - checkState(adMediaInfo.equals(imaAdMediaInfo)); - for (int i = 0; i < adCallbacks.size(); i++) { - adCallbacks.get(i).onResume(adMediaInfo); - } - } - if (!checkNotNull(player).getPlayWhenReady()) { - checkNotNull(adsManager).pause(); - } - } - - private void pauseAdInternal(AdMediaInfo adMediaInfo) { - if (configuration.debugModeEnabled) { - Log.d(TAG, "pauseAd " + getAdMediaInfoString(adMediaInfo)); - } - if (adsManager == null) { - // Drop event after release. - return; - } - if (imaAdState == IMA_AD_STATE_NONE) { - // This method is called if loadAd has been called but the loaded ad won't play due to a seek - // to a different position, so drop the event. See also [Internal: b/159111848]. - return; - } - checkState(adMediaInfo.equals(imaAdMediaInfo)); - imaAdState = IMA_AD_STATE_PAUSED; - for (int i = 0; i < adCallbacks.size(); i++) { - adCallbacks.get(i).onPause(adMediaInfo); - } - } - - private void stopAdInternal(AdMediaInfo adMediaInfo) { - if (configuration.debugModeEnabled) { - Log.d(TAG, "stopAd " + getAdMediaInfoString(adMediaInfo)); - } - if (adsManager == null) { - // Drop event after release. - return; - } - if (imaAdState == IMA_AD_STATE_NONE) { - // This method is called if loadAd has been called but the preloaded ad won't play due to a - // seek to a different position, so drop the event and discard the ad. See also [Internal: - // b/159111848]. - @Nullable AdInfo adInfo = adInfoByAdMediaInfo.get(adMediaInfo); - if (adInfo != null) { - adPlaybackState = - adPlaybackState.withSkippedAd(adInfo.adGroupIndex, adInfo.adIndexInAdGroup); - updateAdPlaybackState(); - } - return; - } - checkNotNull(player); - imaAdState = IMA_AD_STATE_NONE; - stopUpdatingAdProgress(); - // TODO: Handle the skipped event so the ad can be marked as skipped rather than played. - checkNotNull(imaAdInfo); - int adGroupIndex = imaAdInfo.adGroupIndex; - int adIndexInAdGroup = imaAdInfo.adIndexInAdGroup; - if (adPlaybackState.isAdInErrorState(adGroupIndex, adIndexInAdGroup)) { - // We have already marked this ad as having failed to load, so ignore the request. - return; - } - adPlaybackState = - adPlaybackState.withPlayedAd(adGroupIndex, adIndexInAdGroup).withAdResumePositionUs(0); - updateAdPlaybackState(); - if (!playingAd) { - imaAdMediaInfo = null; - imaAdInfo = null; - } - } - - private void handleAdGroupLoadError(Exception error) { - int adGroupIndex = getLoadingAdGroupIndex(); - if (adGroupIndex == C.INDEX_UNSET) { - Log.w(TAG, "Unable to determine ad group index for ad group load error", error); - return; - } - markAdGroupInErrorStateAndClearPendingContentPosition(adGroupIndex); - if (pendingAdLoadError == null) { - pendingAdLoadError = AdLoadException.createForAdGroup(error, adGroupIndex); - } - } - - private void markAdGroupInErrorStateAndClearPendingContentPosition(int adGroupIndex) { - // Update the ad playback state so all ads in the ad group are in the error state. - AdPlaybackState.AdGroup adGroup = adPlaybackState.adGroups[adGroupIndex]; - if (adGroup.count == C.LENGTH_UNSET) { - adPlaybackState = adPlaybackState.withAdCount(adGroupIndex, max(1, adGroup.states.length)); - adGroup = adPlaybackState.adGroups[adGroupIndex]; - } - for (int i = 0; i < adGroup.count; i++) { - if (adGroup.states[i] == AdPlaybackState.AD_STATE_UNAVAILABLE) { - if (configuration.debugModeEnabled) { - Log.d(TAG, "Removing ad " + i + " in ad group " + adGroupIndex); - } - adPlaybackState = adPlaybackState.withAdLoadError(adGroupIndex, i); - } - } - updateAdPlaybackState(); - // Clear any pending content position that triggered attempting to load the ad group. - pendingContentPositionMs = C.TIME_UNSET; - fakeContentProgressElapsedRealtimeMs = C.TIME_UNSET; - } - - private void handleAdPrepareError(int adGroupIndex, int adIndexInAdGroup, Exception exception) { - if (configuration.debugModeEnabled) { - Log.d( - TAG, "Prepare error for ad " + adIndexInAdGroup + " in group " + adGroupIndex, exception); - } - if (adsManager == null) { - Log.w(TAG, "Ignoring ad prepare error after release"); - return; - } - if (imaAdState == IMA_AD_STATE_NONE) { - // Send IMA a content position at the ad group so that it will try to play it, at which point - // we can notify that it failed to load. - fakeContentProgressElapsedRealtimeMs = SystemClock.elapsedRealtime(); - fakeContentProgressOffsetMs = C.usToMs(adPlaybackState.adGroupTimesUs[adGroupIndex]); - if (fakeContentProgressOffsetMs == C.TIME_END_OF_SOURCE) { - fakeContentProgressOffsetMs = contentDurationMs; - } - pendingAdPrepareErrorAdInfo = new AdInfo(adGroupIndex, adIndexInAdGroup); - } else { - AdMediaInfo adMediaInfo = checkNotNull(imaAdMediaInfo); - // We're already playing an ad. - if (adIndexInAdGroup > playingAdIndexInAdGroup) { - // Mark the playing ad as ended so we can notify the error on the next ad and remove it, - // which means that the ad after will load (if any). - for (int i = 0; i < adCallbacks.size(); i++) { - adCallbacks.get(i).onEnded(adMediaInfo); - } - } - playingAdIndexInAdGroup = adPlaybackState.adGroups[adGroupIndex].getFirstAdIndexToPlay(); - for (int i = 0; i < adCallbacks.size(); i++) { - adCallbacks.get(i).onError(checkNotNull(adMediaInfo)); - } - } - adPlaybackState = adPlaybackState.withAdLoadError(adGroupIndex, adIndexInAdGroup); - updateAdPlaybackState(); - } - - private void ensureSentContentCompleteIfAtEndOfStream() { - if (!sentContentComplete - && contentDurationMs != C.TIME_UNSET - && pendingContentPositionMs == C.TIME_UNSET - && getContentPeriodPositionMs(checkNotNull(player), timeline, period) - + THRESHOLD_END_OF_CONTENT_MS - >= contentDurationMs) { - sendContentComplete(); - } - } - - private void sendContentComplete() { - for (int i = 0; i < adCallbacks.size(); i++) { - adCallbacks.get(i).onContentComplete(); - } - sentContentComplete = true; - if (configuration.debugModeEnabled) { - Log.d(TAG, "adsLoader.contentComplete"); - } - for (int i = 0; i < adPlaybackState.adGroupCount; i++) { - if (adPlaybackState.adGroupTimesUs[i] != C.TIME_END_OF_SOURCE) { - adPlaybackState = adPlaybackState.withSkippedAdGroup(/* adGroupIndex= */ i); - } - } - updateAdPlaybackState(); - } - - private void updateAdPlaybackState() { - // Ignore updates while detached. When a player is attached it will receive the latest state. - if (eventListener != null) { - eventListener.onAdPlaybackState(adPlaybackState); - } - } - - private void maybeNotifyPendingAdLoadError() { - if (pendingAdLoadError != null && eventListener != null) { - eventListener.onAdLoadError(pendingAdLoadError, adTagDataSpec); - pendingAdLoadError = null; - } - } - - private void maybeNotifyInternalError(String name, Exception cause) { - String message = "Internal error in " + name; - Log.e(TAG, message, cause); - // We can't recover from an unexpected error in general, so skip all remaining ads. - for (int i = 0; i < adPlaybackState.adGroupCount; i++) { - adPlaybackState = adPlaybackState.withSkippedAdGroup(i); - } - updateAdPlaybackState(); - if (eventListener != null) { - eventListener.onAdLoadError( - AdLoadException.createForUnexpected(new RuntimeException(message, cause)), adTagDataSpec); - } - } - - private int getAdGroupIndexForAdPod(AdPodInfo adPodInfo) { - if (adPodInfo.getPodIndex() == -1) { - // This is a postroll ad. - return adPlaybackState.adGroupCount - 1; - } - - // adPodInfo.podIndex may be 0-based or 1-based, so for now look up the cue point instead. - return getAdGroupIndexForCuePointTimeSeconds(adPodInfo.getTimeOffset()); - } - - /** - * Returns the index of the ad group that will preload next, or {@link C#INDEX_UNSET} if there is - * no such ad group. - */ - private int getLoadingAdGroupIndex() { - if (player == null) { - return C.INDEX_UNSET; - } - long playerPositionUs = C.msToUs(getContentPeriodPositionMs(player, timeline, period)); - int adGroupIndex = - adPlaybackState.getAdGroupIndexForPositionUs(playerPositionUs, C.msToUs(contentDurationMs)); - if (adGroupIndex == C.INDEX_UNSET) { - adGroupIndex = - adPlaybackState.getAdGroupIndexAfterPositionUs( - playerPositionUs, C.msToUs(contentDurationMs)); - } - return adGroupIndex; - } - - private int getAdGroupIndexForCuePointTimeSeconds(double cuePointTimeSeconds) { - // We receive initial cue points from IMA SDK as floats. This code replicates the same - // calculation used to populate adGroupTimesUs (having truncated input back to float, to avoid - // failures if the behavior of the IMA SDK changes to provide greater precision). - float cuePointTimeSecondsFloat = (float) cuePointTimeSeconds; - long adPodTimeUs = Math.round((double) cuePointTimeSecondsFloat * C.MICROS_PER_SECOND); - for (int adGroupIndex = 0; adGroupIndex < adPlaybackState.adGroupCount; adGroupIndex++) { - long adGroupTimeUs = adPlaybackState.adGroupTimesUs[adGroupIndex]; - if (adGroupTimeUs != C.TIME_END_OF_SOURCE - && Math.abs(adGroupTimeUs - adPodTimeUs) < THRESHOLD_AD_MATCH_US) { - return adGroupIndex; - } - } - throw new IllegalStateException("Failed to find cue point"); - } - - private String getAdMediaInfoString(AdMediaInfo adMediaInfo) { - @Nullable AdInfo adInfo = adInfoByAdMediaInfo.get(adMediaInfo); - return "AdMediaInfo[" + adMediaInfo.getUrl() + (adInfo != null ? ", " + adInfo : "") + "]"; - } - - private static long getContentPeriodPositionMs( - Player player, Timeline timeline, Timeline.Period period) { - long contentWindowPositionMs = player.getContentPosition(); - return contentWindowPositionMs - - (timeline.isEmpty() - ? 0 - : timeline.getPeriod(/* periodIndex= */ 0, period).getPositionInWindowMs()); - } - - private static Looper getImaLooper() { - // IMA SDK callbacks occur on the main thread. This method can be used to check that the player - // is using the same looper, to ensure all interaction with this class is on the main thread. - return Looper.getMainLooper(); - } - - private static boolean hasMidrollAdGroups(long[] adGroupTimesUs) { - int count = adGroupTimesUs.length; - if (count == 1) { - return adGroupTimesUs[0] != 0 && adGroupTimesUs[0] != C.TIME_END_OF_SOURCE; - } else if (count == 2) { - return adGroupTimesUs[0] != 0 || adGroupTimesUs[1] != C.TIME_END_OF_SOURCE; - } else { - // There's at least one midroll ad group, as adGroupTimesUs is never empty. - return true; - } - } - - private void destroyAdsManager() { - if (adsManager != null) { - adsManager.removeAdErrorListener(componentListener); - if (configuration.applicationAdErrorListener != null) { - adsManager.removeAdErrorListener(configuration.applicationAdErrorListener); - } - adsManager.removeAdEventListener(componentListener); - if (configuration.applicationAdEventListener != null) { - adsManager.removeAdEventListener(configuration.applicationAdEventListener); - } - adsManager.destroy(); - adsManager = null; - } - } - - private final class ComponentListener - implements AdsLoadedListener, - ContentProgressProvider, - AdEventListener, - AdErrorListener, - VideoAdPlayer { - - // AdsLoader.AdsLoadedListener implementation. - - @Override - public void onAdsManagerLoaded(AdsManagerLoadedEvent adsManagerLoadedEvent) { - AdsManager adsManager = adsManagerLoadedEvent.getAdsManager(); - if (!Util.areEqual(pendingAdRequestContext, adsManagerLoadedEvent.getUserRequestContext())) { - adsManager.destroy(); - return; - } - pendingAdRequestContext = null; - ImaAdsLoader.this.adsManager = adsManager; - adsManager.addAdErrorListener(this); - if (configuration.applicationAdErrorListener != null) { - adsManager.addAdErrorListener(configuration.applicationAdErrorListener); - } - adsManager.addAdEventListener(this); - if (configuration.applicationAdEventListener != null) { - adsManager.addAdEventListener(configuration.applicationAdEventListener); - } - if (player != null) { - // If a player is attached already, start playback immediately. - try { - adPlaybackState = - ImaUtil.getInitialAdPlaybackStateForCuePoints(adsManager.getAdCuePoints()); - hasAdPlaybackState = true; - updateAdPlaybackState(); - } catch (RuntimeException e) { - maybeNotifyInternalError("onAdsManagerLoaded", e); - } - } - } - - // ContentProgressProvider implementation. - - @Override - public VideoProgressUpdate getContentProgress() { - VideoProgressUpdate videoProgressUpdate = getContentVideoProgressUpdate(); - if (configuration.debugModeEnabled) { - if (VideoProgressUpdate.VIDEO_TIME_NOT_READY.equals(videoProgressUpdate)) { - Log.d(TAG, "Content progress: not ready"); - } else { - Log.d( - TAG, - Util.formatInvariant( - "Content progress: %.1f of %.1f s", - videoProgressUpdate.getCurrentTime(), videoProgressUpdate.getDuration())); - } - } - - if (waitingForPreloadElapsedRealtimeMs != C.TIME_UNSET) { - // IMA is polling the player position but we are buffering for an ad to preload, so playback - // may be stuck. Detect this case and signal an error if applicable. - long stuckElapsedRealtimeMs = - SystemClock.elapsedRealtime() - waitingForPreloadElapsedRealtimeMs; - if (stuckElapsedRealtimeMs >= THRESHOLD_AD_PRELOAD_MS) { - waitingForPreloadElapsedRealtimeMs = C.TIME_UNSET; - handleAdGroupLoadError(new IOException("Ad preloading timed out")); - maybeNotifyPendingAdLoadError(); - } - } - - return videoProgressUpdate; - } - - // AdEvent.AdEventListener implementation. - - @Override - public void onAdEvent(AdEvent adEvent) { - AdEventType adEventType = adEvent.getType(); - if (configuration.debugModeEnabled && adEventType != AdEventType.AD_PROGRESS) { - Log.d(TAG, "onAdEvent: " + adEventType); - } - try { - handleAdEvent(adEvent); - } catch (RuntimeException e) { - maybeNotifyInternalError("onAdEvent", e); - } - } - - // AdErrorEvent.AdErrorListener implementation. - - @Override - public void onAdError(AdErrorEvent adErrorEvent) { - AdError error = adErrorEvent.getError(); - if (configuration.debugModeEnabled) { - Log.d(TAG, "onAdError", error); - } - if (adsManager == null) { - // No ads were loaded, so allow playback to start without any ads. - pendingAdRequestContext = null; - adPlaybackState = AdPlaybackState.NONE; - hasAdPlaybackState = true; - updateAdPlaybackState(); - } else if (ImaUtil.isAdGroupLoadError(error)) { - try { - handleAdGroupLoadError(error); - } catch (RuntimeException e) { - maybeNotifyInternalError("onAdError", e); - } - } - if (pendingAdLoadError == null) { - pendingAdLoadError = AdLoadException.createForAllAds(error); - } - maybeNotifyPendingAdLoadError(); - } - - // VideoAdPlayer implementation. - - @Override - public void addCallback(VideoAdPlayerCallback videoAdPlayerCallback) { - adCallbacks.add(videoAdPlayerCallback); - } - - @Override - public void removeCallback(VideoAdPlayerCallback videoAdPlayerCallback) { - adCallbacks.remove(videoAdPlayerCallback); - } - - @Override - public VideoProgressUpdate getAdProgress() { - throw new IllegalStateException("Unexpected call to getAdProgress when using preloading"); - } - - @Override - public int getVolume() { - return getPlayerVolumePercent(); - } - - @Override - public void loadAd(AdMediaInfo adMediaInfo, AdPodInfo adPodInfo) { - try { - loadAdInternal(adMediaInfo, adPodInfo); - } catch (RuntimeException e) { - maybeNotifyInternalError("loadAd", e); - } - } - - @Override - public void playAd(AdMediaInfo adMediaInfo) { - try { - playAdInternal(adMediaInfo); - } catch (RuntimeException e) { - maybeNotifyInternalError("playAd", e); - } - } - - @Override - public void pauseAd(AdMediaInfo adMediaInfo) { - try { - pauseAdInternal(adMediaInfo); - } catch (RuntimeException e) { - maybeNotifyInternalError("pauseAd", e); - } - } - - @Override - public void stopAd(AdMediaInfo adMediaInfo) { - try { - stopAdInternal(adMediaInfo); - } catch (RuntimeException e) { - maybeNotifyInternalError("stopAd", e); - } - } - - @Override - public void release() { - // Do nothing. - } - } - - // TODO: Consider moving this into AdPlaybackState. - private static final class AdInfo { - - public final int adGroupIndex; - public final int adIndexInAdGroup; - - public AdInfo(int adGroupIndex, int adIndexInAdGroup) { - this.adGroupIndex = adGroupIndex; - this.adIndexInAdGroup = adIndexInAdGroup; - } - - @Override - public boolean equals(@Nullable Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - AdInfo adInfo = (AdInfo) o; - if (adGroupIndex != adInfo.adGroupIndex) { - return false; - } - return adIndexInAdGroup == adInfo.adIndexInAdGroup; - } - - @Override - public int hashCode() { - int result = adGroupIndex; - result = 31 * result + adIndexInAdGroup; - return result; - } - - @Override - public String toString() { - return "(" + adGroupIndex + ", " + adIndexInAdGroup + ')'; - } - } - - /** - * Default {@link ImaUtil.ImaFactory} for non-test usage, which delegates to {@link - * ImaSdkFactory}. - */ - private static final class DefaultImaFactory implements ImaUtil.ImaFactory { - - @Override - public ImaSdkSettings createImaSdkSettings() { - return ImaSdkFactory.getInstance().createImaSdkSettings(); - } - - @Override - public AdsRenderingSettings createAdsRenderingSettings() { - return ImaSdkFactory.getInstance().createAdsRenderingSettings(); - } - - @Override - public AdDisplayContainer createAdDisplayContainer(ViewGroup container, VideoAdPlayer player) { - return ImaSdkFactory.createAdDisplayContainer(container, player); - } - - @Override - public AdDisplayContainer createAudioAdDisplayContainer(Context context, VideoAdPlayer player) { - return ImaSdkFactory.createAudioAdDisplayContainer(context, player); - } - - // The reasonDetail parameter to createFriendlyObstruction is annotated @Nullable but the - // annotation is not kept in the obfuscated dependency. - @SuppressWarnings("nullness:argument.type.incompatible") - @Override - public FriendlyObstruction createFriendlyObstruction( - View view, - FriendlyObstructionPurpose friendlyObstructionPurpose, - @Nullable String reasonDetail) { - return ImaSdkFactory.getInstance() - .createFriendlyObstruction(view, friendlyObstructionPurpose, reasonDetail); - } - - @Override - public AdsRequest createAdsRequest() { - return ImaSdkFactory.getInstance().createAdsRequest(); - } - - @Override - public AdsLoader createAdsLoader( - Context context, ImaSdkSettings imaSdkSettings, AdDisplayContainer adDisplayContainer) { - return ImaSdkFactory.getInstance() - .createAdsLoader(context, imaSdkSettings, adDisplayContainer); - } - } -} diff --git a/automatedtests/src/androidTestR2_12_1/java/com/google/android/exoplayer2/ext/ima/ImaUtil.java b/automatedtests/src/androidTestR2_12_1/java/com/google/android/exoplayer2/ext/ima/ImaUtil.java deleted file mode 100644 index cf073e95..00000000 --- a/automatedtests/src/androidTestR2_12_1/java/com/google/android/exoplayer2/ext/ima/ImaUtil.java +++ /dev/null @@ -1,239 +0,0 @@ -/* - * Copyright (C) 2020 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.google.android.exoplayer2.ext.ima; - -import android.content.Context; -import android.view.View; -import android.view.ViewGroup; -import androidx.annotation.Nullable; -import com.google.ads.interactivemedia.v3.api.AdDisplayContainer; -import com.google.ads.interactivemedia.v3.api.AdError; -import com.google.ads.interactivemedia.v3.api.AdErrorEvent; -import com.google.ads.interactivemedia.v3.api.AdEvent; -import com.google.ads.interactivemedia.v3.api.AdsLoader; -import com.google.ads.interactivemedia.v3.api.AdsManager; -import com.google.ads.interactivemedia.v3.api.AdsRenderingSettings; -import com.google.ads.interactivemedia.v3.api.AdsRequest; -import com.google.ads.interactivemedia.v3.api.CompanionAdSlot; -import com.google.ads.interactivemedia.v3.api.FriendlyObstruction; -import com.google.ads.interactivemedia.v3.api.FriendlyObstructionPurpose; -import com.google.ads.interactivemedia.v3.api.ImaSdkSettings; -import com.google.ads.interactivemedia.v3.api.UiElement; -import com.google.ads.interactivemedia.v3.api.player.VideoAdPlayer; -import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.source.ads.AdPlaybackState; -import com.google.android.exoplayer2.source.ads.AdsLoader.OverlayInfo; -import com.google.android.exoplayer2.upstream.DataSchemeDataSource; -import com.google.android.exoplayer2.upstream.DataSpec; -import com.google.android.exoplayer2.util.Util; -import java.io.IOException; -import java.util.Arrays; -import java.util.Collection; -import java.util.List; -import java.util.Set; - -/** - * Utilities for working with IMA SDK and IMA extension data types. - */ -/* package */ final class ImaUtil { - - /** - * Factory for objects provided by the IMA SDK. - */ - public interface ImaFactory { - - /** - * Creates {@link ImaSdkSettings} for configuring the IMA SDK. - */ - ImaSdkSettings createImaSdkSettings(); - - /** - * Creates {@link AdsRenderingSettings} for giving the {@link AdsManager} parameters that - * control rendering of ads. - */ - AdsRenderingSettings createAdsRenderingSettings(); - - /** - * Creates an {@link AdDisplayContainer} to hold the player for video ads, a container for - * non-linear ads, and slots for companion ads. - */ - AdDisplayContainer createAdDisplayContainer(ViewGroup container, VideoAdPlayer player); - - /** - * Creates an {@link AdDisplayContainer} to hold the player for audio ads. - */ - AdDisplayContainer createAudioAdDisplayContainer(Context context, VideoAdPlayer player); - - /** - * Creates a {@link FriendlyObstruction} to describe an obstruction considered "friendly" for - * viewability measurement purposes. - */ - FriendlyObstruction createFriendlyObstruction( - View view, - FriendlyObstructionPurpose friendlyObstructionPurpose, - @Nullable String reasonDetail); - - /** - * Creates an {@link AdsRequest} to contain the data used to request ads. - */ - AdsRequest createAdsRequest(); - - /** - * Creates an {@link AdsLoader} for requesting ads using the specified settings. - */ - AdsLoader createAdsLoader( - Context context, ImaSdkSettings imaSdkSettings, AdDisplayContainer adDisplayContainer); - } - - /** - * Stores configuration for ad loading and playback. - */ - public static final class Configuration { - - public final long adPreloadTimeoutMs; - public final int vastLoadTimeoutMs; - public final int mediaLoadTimeoutMs; - public final boolean focusSkipButtonWhenAvailable; - public final boolean playAdBeforeStartPosition; - public final int mediaBitrate; - @Nullable - public final List adMediaMimeTypes; - @Nullable - public final Set adUiElements; - @Nullable - public final Collection companionAdSlots; - @Nullable - public final AdErrorEvent.AdErrorListener applicationAdErrorListener; - @Nullable - public final AdEvent.AdEventListener applicationAdEventListener; - @Nullable - public final VideoAdPlayer.VideoAdPlayerCallback applicationVideoAdPlayerCallback; - @Nullable - public final ImaSdkSettings imaSdkSettings; - public final boolean debugModeEnabled; - - public Configuration( - long adPreloadTimeoutMs, - int vastLoadTimeoutMs, - int mediaLoadTimeoutMs, - boolean focusSkipButtonWhenAvailable, - boolean playAdBeforeStartPosition, - int mediaBitrate, - @Nullable List adMediaMimeTypes, - @Nullable Set adUiElements, - @Nullable Collection companionAdSlots, - @Nullable AdErrorEvent.AdErrorListener applicationAdErrorListener, - @Nullable AdEvent.AdEventListener applicationAdEventListener, - @Nullable VideoAdPlayer.VideoAdPlayerCallback applicationVideoAdPlayerCallback, - @Nullable ImaSdkSettings imaSdkSettings, - boolean debugModeEnabled) { - this.adPreloadTimeoutMs = adPreloadTimeoutMs; - this.vastLoadTimeoutMs = vastLoadTimeoutMs; - this.mediaLoadTimeoutMs = mediaLoadTimeoutMs; - this.focusSkipButtonWhenAvailable = focusSkipButtonWhenAvailable; - this.playAdBeforeStartPosition = playAdBeforeStartPosition; - this.mediaBitrate = mediaBitrate; - this.adMediaMimeTypes = adMediaMimeTypes; - this.adUiElements = adUiElements; - this.companionAdSlots = companionAdSlots; - this.applicationAdErrorListener = applicationAdErrorListener; - this.applicationAdEventListener = applicationAdEventListener; - this.applicationVideoAdPlayerCallback = applicationVideoAdPlayerCallback; - this.imaSdkSettings = imaSdkSettings; - this.debugModeEnabled = debugModeEnabled; - } - } - - /** - * Returns the IMA {@link FriendlyObstructionPurpose} corresponding to the given {@link - * OverlayInfo#purpose}. - */ - public static FriendlyObstructionPurpose getFriendlyObstructionPurpose( - @OverlayInfo.Purpose int purpose) { - switch (purpose) { - case OverlayInfo.PURPOSE_CONTROLS: - return FriendlyObstructionPurpose.VIDEO_CONTROLS; - case OverlayInfo.PURPOSE_CLOSE_AD: - return FriendlyObstructionPurpose.CLOSE_AD; - case OverlayInfo.PURPOSE_NOT_VISIBLE: - return FriendlyObstructionPurpose.NOT_VISIBLE; - case OverlayInfo.PURPOSE_OTHER: - default: - return FriendlyObstructionPurpose.OTHER; - } - } - - /** - * Returns an initial {@link AdPlaybackState} with ad groups at the provided {@code cuePoints}. - * - * @param cuePoints The cue points of the ads in seconds. - * @return The {@link AdPlaybackState}. - */ - public static AdPlaybackState getInitialAdPlaybackStateForCuePoints(List cuePoints) { - if (cuePoints.isEmpty()) { - // If no cue points are specified, there is a preroll ad. - return new AdPlaybackState(/* adGroupTimesUs...= */ 0); - } - - int count = cuePoints.size(); - long[] adGroupTimesUs = new long[count]; - int adGroupIndex = 0; - for (int i = 0; i < count; i++) { - double cuePoint = cuePoints.get(i); - if (cuePoint == -1.0) { - adGroupTimesUs[count - 1] = C.TIME_END_OF_SOURCE; - } else { - adGroupTimesUs[adGroupIndex++] = Math.round(C.MICROS_PER_SECOND * cuePoint); - } - } - // Cue points may be out of order, so sort them. - Arrays.sort(adGroupTimesUs, 0, adGroupIndex); - return new AdPlaybackState(adGroupTimesUs); - } - - /** - * Returns an {@link AdsRequest} based on the specified ad tag {@link DataSpec}. - */ - public static AdsRequest getAdsRequestForAdTagDataSpec( - ImaFactory imaFactory, DataSpec adTagDataSpec) throws IOException { - AdsRequest request = imaFactory.createAdsRequest(); - if (DataSchemeDataSource.SCHEME_DATA.equals(adTagDataSpec.uri.getScheme())) { - DataSchemeDataSource dataSchemeDataSource = new DataSchemeDataSource(); - try { - dataSchemeDataSource.open(adTagDataSpec); - request.setAdsResponse(Util.fromUtf8Bytes(Util.readToEnd(dataSchemeDataSource))); - } finally { - dataSchemeDataSource.close(); - } - } else { - request.setAdTagUrl(adTagDataSpec.uri.toString()); - } - return request; - } - - /** - * Returns whether the ad error indicates that an entire ad group failed to load. - */ - public static boolean isAdGroupLoadError(AdError adError) { - // TODO: Find out what other errors need to be handled (if any), and whether each one relates to - // a single ad, ad group or the whole timeline. - return adError.getErrorCode() == AdError.AdErrorCode.VAST_LINEAR_ASSET_MISMATCH - || adError.getErrorCode() == AdError.AdErrorCode.UNKNOWN_ERROR; - } - - private ImaUtil() { - } -} diff --git a/automatedtests/src/androidTestR2_12_1/java/com/mux/stats/sdk/muxstats/automatedtests/ui/SimplePlayerTestActivity.java b/automatedtests/src/androidTestR2_12_1/java/com/mux/stats/sdk/muxstats/automatedtests/ui/SimplePlayerTestActivity.java deleted file mode 100644 index 9c3b2a51..00000000 --- a/automatedtests/src/androidTestR2_12_1/java/com/mux/stats/sdk/muxstats/automatedtests/ui/SimplePlayerTestActivity.java +++ /dev/null @@ -1,228 +0,0 @@ -package com.mux.stats.sdk.muxstats.automatedtests.ui; - - -import android.app.Notification; -import android.net.Uri; -import android.support.v4.media.session.MediaSessionCompat; -import android.util.Log; -import androidx.annotation.Nullable; -import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.DefaultRenderersFactory; -import com.google.android.exoplayer2.MediaItem; -import com.google.android.exoplayer2.PlaybackPreparer; -import com.google.android.exoplayer2.Player; -import com.google.android.exoplayer2.RenderersFactory; -import com.google.android.exoplayer2.SimpleExoPlayer; -import com.google.android.exoplayer2.ext.ima.ImaAdsLoader; -import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector; -import com.google.android.exoplayer2.source.DefaultMediaSourceFactory; -import com.google.android.exoplayer2.source.MediaSource; -import com.google.android.exoplayer2.source.MediaSourceFactory; -import com.google.android.exoplayer2.source.ProgressiveMediaSource; -import com.google.android.exoplayer2.source.ads.AdsLoader; -import com.google.android.exoplayer2.source.ads.AdsMediaSource; -import com.google.android.exoplayer2.source.dash.DashMediaSource; -import com.google.android.exoplayer2.source.hls.HlsMediaSource; -import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource; -import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection; -import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; -import com.google.android.exoplayer2.trackselection.TrackSelection; -import com.google.android.exoplayer2.ui.PlayerNotificationManager; -import com.google.android.exoplayer2.upstream.DataSource; -import com.google.android.exoplayer2.upstream.DataSpec; -import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; -import com.google.android.exoplayer2.util.Util; -import com.mux.stats.sdk.muxstats.automatedtests.R; -import com.mux.stats.sdk.muxstats.SimplePlayerBaseActivity; - - -public class SimplePlayerTestActivity extends SimplePlayerBaseActivity implements PlaybackPreparer, - Player.EventListener { - - MediaSourceFactory mediaSourceFactory; - - public void initExoPlayer() { - // Hopfully this will not channge the track selection set programmatically - TrackSelection.Factory trackSelectionFactory = new AdaptiveTrackSelection.Factory( - AdaptiveTrackSelection.DEFAULT_MIN_DURATION_FOR_QUALITY_INCREASE_MS * 10, - AdaptiveTrackSelection.DEFAULT_MAX_DURATION_FOR_QUALITY_DECREASE_MS * 10, - AdaptiveTrackSelection.DEFAULT_MIN_DURATION_TO_RETAIN_AFTER_DISCARD_MS, - AdaptiveTrackSelection.DEFAULT_BANDWIDTH_FRACTION - ); - DefaultTrackSelector.ParametersBuilder builder = - new DefaultTrackSelector.ParametersBuilder(/* context= */ this); - DefaultTrackSelector.Parameters trackSelectorParameters = builder - .build(); - - mediaSourceFactory = - new DefaultMediaSourceFactory(buildDataSourceFactory()) - .setAdsLoaderProvider(this::getAdsLoader) - .setAdViewProvider(playerView); - - trackSelector = new DefaultTrackSelector(/* context= */ this, trackSelectionFactory); - trackSelector.setParameters(trackSelectorParameters); - RenderersFactory renderersFactory = new DefaultRenderersFactory(/* context= */ this); - player = - new SimpleExoPlayer.Builder(/* context= */ this, renderersFactory) - .setMediaSourceFactory(mediaSourceFactory) - .setTrackSelector(trackSelector) - .build(); - playerView.setPlaybackPreparer(this); - player.addListener(this); - } - - @Override - public void preparePlayback() { - player.prepare(); - } - - // This is for background playback, set appropriate notification and etc - public void initAudioSession() { - notificationManager = PlayerNotificationManager.createWithNotificationChannel( - getApplicationContext(), - PLAYBACK_CHANNEL_ID, - R.string.channel_name, - R.string.channel_description, - PLAYBACK_NOTIFICATION_ID, - new MDAdapter(), - new CustomNotificationListener() - ); - notificationManager.setUseNavigationActions(false); - notificationManager.setUseStopAction(true); - notificationManager.setPlayer(player); - - mediaSessionCompat = new MediaSessionCompat(this, "hello_world_media"); - notificationManager.setMediaSessionToken(mediaSessionCompat.getSessionToken()); - mediaSessionConnector = new MediaSessionConnector(mediaSessionCompat); - mediaSessionConnector.setPlayer(player); - } - - public DataSource.Factory buildDataSourceFactory() { - return new DefaultDataSourceFactory(this, "Android-automated_tests"); - } - - private MediaSource createMediaSource(Uri uri, String extension) { - MediaItem lMEdiaItem = createMediaItem(uri); - @C.ContentType int type = Util.inferContentType(uri, extension); - DataSource.Factory dataSourceFactory = buildDataSourceFactory(); - switch (type) { - case C.TYPE_DASH: - return new DashMediaSource.Factory(dataSourceFactory) - .createMediaSource(lMEdiaItem); - case C.TYPE_SS: - return new SsMediaSource.Factory(dataSourceFactory) - .createMediaSource(lMEdiaItem); - case C.TYPE_HLS: - return new HlsMediaSource.Factory(dataSourceFactory) - .createMediaSource(lMEdiaItem); - case C.TYPE_OTHER: - return new ProgressiveMediaSource.Factory(buildDataSourceFactory()) - .createMediaSource(lMEdiaItem); - default: - throw new IllegalStateException("Unsupported type: " + type); - } - } - - public MediaSource createAdsMediaSource(MediaSource aMediaSource, Uri adTagUri) { - return new AdsMediaSource( - aMediaSource, - new DataSpec(loadedAdTagUri), - mediaSourceFactory, - getAdsLoader(loadedAdTagUri), - playerView); - } - - public MediaSource buildMediaSource(Uri uri, @Nullable String overrideExtension) { - return createMediaSource(uri, overrideExtension); - } - - private MediaItem createMediaItem( - Uri uri) { - MediaItem.Builder lBuilder = - new MediaItem.Builder() - .setUri(uri); - return lBuilder.build(); - } - - - private AdsLoader getAdsLoader(Uri adTagUri) { - if (!adTagUri.equals(loadedAdTagUri)) { - releaseAdsLoader(); - loadedAdTagUri = adTagUri; - } - // The ads loader is reused for multiple playbacks, so that ad playback can resume. - if (adsLoader == null) { - adsLoader = new ImaAdsLoader.Builder(/* context= */ this) - .setAdErrorListener(muxStats.getAdsImaSdkListener()) - .setAdEventListener(muxStats.getAdsImaSdkListener()) - .build(); - } - adsLoader.setPlayer(player); - return adsLoader; - } - - private void releaseAdsLoader() { - if (adsLoader != null) { - adsLoader.release(); - adsLoader = null; - loadedAdTagUri = null; - } - } - - @Override - public void startPlayback() { - Uri testUri = Uri.parse(urlToPlay); - testMediaSource = buildMediaSource(testUri, null); - if (loadedAdTagUri != null) { - testMediaSource = createAdsMediaSource(testMediaSource, loadedAdTagUri); - } - - player.setPlayWhenReady(playWhenReady); - ((SimpleExoPlayer)player).setMediaSource(testMediaSource); - player.seekTo(playbackStartPosition); - player.prepare(); - } - - class CustomNotificationListener implements PlayerNotificationManager.NotificationListener { - - @Override - public void onNotificationPosted(int notificationId, Notification notification, - boolean ongoing) { - // TODO implement this - Log.e(TAG, "onNotificationPosted"); - } - } - - ////////////////////////////////////////////////////////////////////// - ////// Player.EventListener ////////////////////////////////////////// - - @Override - public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { - switch (playbackState) { - case Player.STATE_BUFFERING: - signalPlaybackBuffering(); - break; - case Player.STATE_ENDED: - signalPlaybackEnded(); - break; - case Player.STATE_READY: - // By the time we get here, it depends on playWhenReady to know if we're playing - if (playWhenReady) { - signalPlaybackStarted(); - } else { - // TODO implement this -// signalPlaybackPaused(); - } - case Player.STATE_IDLE: - signalPlaybackStopped(); - break; - } - } - - @Override - public void onRepeatModeChanged(int repeatMode) { - activityLock.lock(); - activityInitialized.signalAll(); - activityLock.unlock(); - } -} diff --git a/automatedtests/src/androidTestR2_13_1/java/com/google/android/exoplayer2/ext/ima/AdTagLoader.java b/automatedtests/src/androidTestR2_13_1/java/com/google/android/exoplayer2/ext/ima/AdTagLoader.java deleted file mode 100644 index 8d6594df..00000000 --- a/automatedtests/src/androidTestR2_13_1/java/com/google/android/exoplayer2/ext/ima/AdTagLoader.java +++ /dev/null @@ -1,1522 +0,0 @@ -/* - * Copyright 2020 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.google.android.exoplayer2.ext.ima; - -import static com.google.android.exoplayer2.ext.ima.ImaUtil.BITRATE_UNSET; -import static com.google.android.exoplayer2.ext.ima.ImaUtil.TIMEOUT_UNSET; -import static com.google.android.exoplayer2.ext.ima.ImaUtil.getAdGroupTimesUsForCuePoints; -import static com.google.android.exoplayer2.ext.ima.ImaUtil.getImaLooper; -import static com.google.android.exoplayer2.util.Assertions.checkNotNull; -import static com.google.android.exoplayer2.util.Assertions.checkState; -import static java.lang.Math.max; - -import android.content.Context; -import android.net.Uri; -import android.os.Handler; -import android.os.SystemClock; -import android.view.ViewGroup; -import androidx.annotation.IntDef; -import androidx.annotation.Nullable; -import com.google.ads.interactivemedia.v3.api.AdDisplayContainer; -import com.google.ads.interactivemedia.v3.api.AdError; -import com.google.ads.interactivemedia.v3.api.AdErrorEvent; -import com.google.ads.interactivemedia.v3.api.AdErrorEvent.AdErrorListener; -import com.google.ads.interactivemedia.v3.api.AdEvent; -import com.google.ads.interactivemedia.v3.api.AdEvent.AdEventListener; -import com.google.ads.interactivemedia.v3.api.AdEvent.AdEventType; -import com.google.ads.interactivemedia.v3.api.AdPodInfo; -import com.google.ads.interactivemedia.v3.api.AdsLoader; -import com.google.ads.interactivemedia.v3.api.AdsLoader.AdsLoadedListener; -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.ads.interactivemedia.v3.api.player.ContentProgressProvider; -import com.google.ads.interactivemedia.v3.api.player.VideoAdPlayer; -import com.google.ads.interactivemedia.v3.api.player.VideoProgressUpdate; -import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.ExoPlaybackException; -import com.google.android.exoplayer2.ExoPlayerLibraryInfo; -import com.google.android.exoplayer2.Player; -import com.google.android.exoplayer2.Timeline; -import com.google.android.exoplayer2.source.ads.AdPlaybackState; -import com.google.android.exoplayer2.source.ads.AdsLoader.AdViewProvider; -import com.google.android.exoplayer2.source.ads.AdsLoader.EventListener; -import com.google.android.exoplayer2.source.ads.AdsLoader.OverlayInfo; -import com.google.android.exoplayer2.source.ads.AdsMediaSource.AdLoadException; -import com.google.android.exoplayer2.trackselection.TrackSelectionArray; -import com.google.android.exoplayer2.upstream.DataSpec; -import com.google.android.exoplayer2.util.Log; -import com.google.android.exoplayer2.util.Util; -import com.google.common.collect.BiMap; -import com.google.common.collect.HashBiMap; -import java.io.IOException; -import java.lang.annotation.Documented; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -/** - * Handles loading and playback of a single ad tag. - */ -/* package */ final class AdTagLoader implements Player.EventListener { - - private static final String TAG = "AdTagLoader"; - - private static final String IMA_SDK_SETTINGS_PLAYER_TYPE = "google/exo.ext.ima"; - private static final String IMA_SDK_SETTINGS_PLAYER_VERSION = ExoPlayerLibraryInfo.VERSION; - - /** - * Interval at which ad progress updates are provided to the IMA SDK, in milliseconds. 100 ms is - * the interval recommended by the IMA documentation. - * - * @see VideoAdPlayer.VideoAdPlayerCallback - */ - private static final int AD_PROGRESS_UPDATE_INTERVAL_MS = 100; - - /** - * The value used in {@link VideoProgressUpdate}s to indicate an unset duration. - */ - private static final long IMA_DURATION_UNSET = -1L; - - /** - * Threshold before the end of content at which IMA is notified that content is complete if the - * player buffers, in milliseconds. - */ - private static final long THRESHOLD_END_OF_CONTENT_MS = 5000; - /** - * Threshold before the start of an ad at which IMA is expected to be able to preload the ad, in - * milliseconds. - */ - private static final long THRESHOLD_AD_PRELOAD_MS = 4000; - /** - * The threshold below which ad cue points are treated as matching, in microseconds. - */ - private static final long THRESHOLD_AD_MATCH_US = 1000; - - /** - * The state of ad playback. - */ - @Documented - @Retention(RetentionPolicy.SOURCE) - @IntDef({IMA_AD_STATE_NONE, IMA_AD_STATE_PLAYING, IMA_AD_STATE_PAUSED}) - private @interface ImaAdState { - - } - - /** - * The ad playback state when IMA is not playing an ad. - */ - private static final int IMA_AD_STATE_NONE = 0; - /** - * The ad playback state when IMA has called {@link ComponentListener#playAd(AdMediaInfo)} and not - * {@link ComponentListener##pauseAd(AdMediaInfo)}. - */ - private static final int IMA_AD_STATE_PLAYING = 1; - /** - * The ad playback state when IMA has called {@link ComponentListener#pauseAd(AdMediaInfo)} while - * playing an ad. - */ - private static final int IMA_AD_STATE_PAUSED = 2; - - private final ImaUtil.Configuration configuration; - private final ImaUtil.ImaFactory imaFactory; - private final List supportedMimeTypes; - private final DataSpec adTagDataSpec; - private final Object adsId; - private final Timeline.Period period; - private final Handler handler; - private final ComponentListener componentListener; - private final List eventListeners; - private final List adCallbacks; - private final Runnable updateAdProgressRunnable; - private final BiMap adInfoByAdMediaInfo; - private final AdDisplayContainer adDisplayContainer; - private final AdsLoader adsLoader; - - @Nullable - private Object pendingAdRequestContext; - @Nullable - private Player player; - private VideoProgressUpdate lastContentProgress; - private VideoProgressUpdate lastAdProgress; - private int lastVolumePercent; - - @Nullable - private AdsManager adsManager; - private boolean isAdsManagerInitialized; - @Nullable - private AdLoadException pendingAdLoadError; - private Timeline timeline; - private long contentDurationMs; - private AdPlaybackState adPlaybackState; - - private boolean released; - - // Fields tracking IMA's state. - - /** - * Whether IMA has sent an ad event to pause content since the last resume content event. - */ - private boolean imaPausedContent; - /** - * The current ad playback state. - */ - private @ImaAdState - int imaAdState; - /** - * The current ad media info, or {@code null} if in state {@link #IMA_AD_STATE_NONE}. - */ - @Nullable - private AdMediaInfo imaAdMediaInfo; - /** - * The current ad info, or {@code null} if in state {@link #IMA_AD_STATE_NONE}. - */ - @Nullable - private AdInfo imaAdInfo; - /** - * Whether IMA has been notified that playback of content has finished. - */ - private boolean sentContentComplete; - - // Fields tracking the player/loader state. - - /** - * Whether the player is playing an ad. - */ - private boolean playingAd; - /** - * Whether the player is buffering an ad. - */ - private boolean bufferingAd; - /** - * If the player is playing an ad, stores the ad index in its ad group. {@link C#INDEX_UNSET} - * otherwise. - */ - private int playingAdIndexInAdGroup; - /** - * The ad info for a pending ad for which the media failed preparation, or {@code null} if no - * pending ads have failed to prepare. - */ - @Nullable - private AdInfo pendingAdPrepareErrorAdInfo; - /** - * If a content period has finished but IMA has not yet called {@link - * ComponentListener#playAd(AdMediaInfo)}, stores the value of {@link - * SystemClock#elapsedRealtime()} when the content stopped playing. This can be used to determine - * a fake, increasing content position. {@link C#TIME_UNSET} otherwise. - */ - private long fakeContentProgressElapsedRealtimeMs; - /** - * If {@link #fakeContentProgressElapsedRealtimeMs} is set, stores the offset from which the - * content progress should increase. {@link C#TIME_UNSET} otherwise. - */ - private long fakeContentProgressOffsetMs; - /** - * Stores the pending content position when a seek operation was intercepted to play an ad. - */ - private long pendingContentPositionMs; - /** - * Whether {@link ComponentListener#getContentProgress()} has sent {@link - * #pendingContentPositionMs} to IMA. - */ - private boolean sentPendingContentPositionMs; - /** - * Stores the real time in milliseconds at which the player started buffering, possibly due to not - * having preloaded an ad, or {@link C#TIME_UNSET} if not applicable. - */ - private long waitingForPreloadElapsedRealtimeMs; - - /** - * Creates a new ad tag loader, starting the ad request if the ad tag is valid. - */ - @SuppressWarnings({"methodref.receiver.bound.invalid", "method.invocation.invalid"}) - public AdTagLoader( - Context context, - ImaUtil.Configuration configuration, - ImaUtil.ImaFactory imaFactory, - List supportedMimeTypes, - DataSpec adTagDataSpec, - Object adsId, - @Nullable ViewGroup adViewGroup) { - this.configuration = configuration; - this.imaFactory = imaFactory; - @Nullable ImaSdkSettings imaSdkSettings = configuration.imaSdkSettings; - if (imaSdkSettings == null) { - imaSdkSettings = imaFactory.createImaSdkSettings(); - if (configuration.debugModeEnabled) { - imaSdkSettings.setDebugMode(true); - } - } - imaSdkSettings.setPlayerType(IMA_SDK_SETTINGS_PLAYER_TYPE); - imaSdkSettings.setPlayerVersion(IMA_SDK_SETTINGS_PLAYER_VERSION); - this.supportedMimeTypes = supportedMimeTypes; - this.adTagDataSpec = adTagDataSpec; - this.adsId = adsId; - period = new Timeline.Period(); - handler = Util.createHandler(getImaLooper(), /* callback= */ null); - componentListener = new ComponentListener(); - eventListeners = new ArrayList<>(); - adCallbacks = new ArrayList<>(/* initialCapacity= */ 1); - if (configuration.applicationVideoAdPlayerCallback != null) { - adCallbacks.add(configuration.applicationVideoAdPlayerCallback); - } - updateAdProgressRunnable = this::updateAdProgress; - adInfoByAdMediaInfo = HashBiMap.create(); - lastContentProgress = VideoProgressUpdate.VIDEO_TIME_NOT_READY; - lastAdProgress = VideoProgressUpdate.VIDEO_TIME_NOT_READY; - fakeContentProgressElapsedRealtimeMs = C.TIME_UNSET; - fakeContentProgressOffsetMs = C.TIME_UNSET; - pendingContentPositionMs = C.TIME_UNSET; - waitingForPreloadElapsedRealtimeMs = C.TIME_UNSET; - contentDurationMs = C.TIME_UNSET; - timeline = Timeline.EMPTY; - adPlaybackState = AdPlaybackState.NONE; - if (adViewGroup != null) { - adDisplayContainer = - imaFactory.createAdDisplayContainer(adViewGroup, /* player= */ componentListener); - } else { - adDisplayContainer = - imaFactory.createAudioAdDisplayContainer(context, /* player= */ componentListener); - } - if (configuration.companionAdSlots != null) { - adDisplayContainer.setCompanionSlots(configuration.companionAdSlots); - } - adsLoader = requestAds(context, imaSdkSettings, adDisplayContainer); - } - - /** - * Returns the underlying IMA SDK ads loader. - */ - public AdsLoader getAdsLoader() { - return adsLoader; - } - - /** - * Returns the IMA SDK ad display container. - */ - public AdDisplayContainer getAdDisplayContainer() { - return adDisplayContainer; - } - - /** - * Skips the current skippable ad, if there is one. - */ - public void skipAd() { - if (adsManager != null) { - adsManager.skip(); - } - } - - /** - * Moves UI focus to the skip button (or other interactive elements), if currently shown. See - * {@link AdsManager#focus()}. - */ - public void focusSkipButton() { - if (adsManager != null) { - adsManager.focus(); - } - } - - /** - * Starts passing events from this instance (including any pending ad playback state) and - * registers obstructions. - */ - public void addListenerWithAdView(EventListener eventListener, AdViewProvider adViewProvider) { - boolean isStarted = !eventListeners.isEmpty(); - eventListeners.add(eventListener); - if (isStarted) { - if (!AdPlaybackState.NONE.equals(adPlaybackState)) { - // Pass the existing ad playback state to the new listener. - eventListener.onAdPlaybackState(adPlaybackState); - } - return; - } - lastVolumePercent = 0; - lastAdProgress = VideoProgressUpdate.VIDEO_TIME_NOT_READY; - lastContentProgress = VideoProgressUpdate.VIDEO_TIME_NOT_READY; - maybeNotifyPendingAdLoadError(); - if (!AdPlaybackState.NONE.equals(adPlaybackState)) { - // Pass the ad playback state to the player, and resume ads if necessary. - eventListener.onAdPlaybackState(adPlaybackState); - } else if (adsManager != null) { - adPlaybackState = - new AdPlaybackState(adsId, getAdGroupTimesUsForCuePoints(adsManager.getAdCuePoints())); - updateAdPlaybackState(); - } - for (OverlayInfo overlayInfo : adViewProvider.getAdOverlayInfos()) { - adDisplayContainer.registerFriendlyObstruction( - imaFactory.createFriendlyObstruction( - overlayInfo.view, - ImaUtil.getFriendlyObstructionPurpose(overlayInfo.purpose), - overlayInfo.reasonDetail)); - } - } - - /** - * Populates the ad playback state with loaded cue points, if available. Any preroll will be - * paused immediately while waiting for this instance to be {@link #activate(Player) activated}. - */ - public void maybePreloadAds(long contentPositionMs, long contentDurationMs) { - maybeInitializeAdsManager(contentPositionMs, contentDurationMs); - } - - /** - * Activates playback. - */ - public void activate(Player player) { - this.player = player; - player.addListener(this); - - boolean playWhenReady = player.getPlayWhenReady(); - onTimelineChanged(player.getCurrentTimeline(), Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE); - if (!AdPlaybackState.NONE.equals(adPlaybackState) - && adsManager != null - && imaPausedContent - && playWhenReady) { - adsManager.resume(); - } - } - - /** - * Deactivates playback. - */ - public void deactivate() { - Player player = checkNotNull(this.player); - if (!AdPlaybackState.NONE.equals(adPlaybackState) && imaPausedContent) { - if (adsManager != null) { - adsManager.pause(); - } - adPlaybackState = - adPlaybackState.withAdResumePositionUs( - playingAd ? C.msToUs(player.getCurrentPosition()) : 0); - } - lastVolumePercent = getPlayerVolumePercent(); - lastAdProgress = getAdVideoProgressUpdate(); - lastContentProgress = getContentVideoProgressUpdate(); - - player.removeListener(this); - this.player = null; - } - - /** - * Stops passing of events from this instance and unregisters obstructions. - */ - public void removeListener(EventListener eventListener) { - eventListeners.remove(eventListener); - if (eventListeners.isEmpty()) { - adDisplayContainer.unregisterAllFriendlyObstructions(); - } - } - - /** - * Releases all resources used by the ad tag loader. - */ - public void release() { - if (released) { - return; - } - released = true; - pendingAdRequestContext = null; - destroyAdsManager(); - adsLoader.removeAdsLoadedListener(componentListener); - adsLoader.removeAdErrorListener(componentListener); - if (configuration.applicationAdErrorListener != null) { - adsLoader.removeAdErrorListener(configuration.applicationAdErrorListener); - } - adsLoader.release(); - imaPausedContent = false; - imaAdState = IMA_AD_STATE_NONE; - imaAdMediaInfo = null; - stopUpdatingAdProgress(); - imaAdInfo = null; - pendingAdLoadError = null; - adPlaybackState = new AdPlaybackState(adsId); - updateAdPlaybackState(); - } - - /** - * Notifies the IMA SDK that the specified ad has been prepared for playback. - */ - public void handlePrepareComplete(int adGroupIndex, int adIndexInAdGroup) { - AdInfo adInfo = new AdInfo(adGroupIndex, adIndexInAdGroup); - if (configuration.debugModeEnabled) { - Log.d(TAG, "Prepared ad " + adInfo); - } - @Nullable AdMediaInfo adMediaInfo = adInfoByAdMediaInfo.inverse().get(adInfo); - if (adMediaInfo != null) { - for (int i = 0; i < adCallbacks.size(); i++) { - adCallbacks.get(i).onLoaded(adMediaInfo); - } - } else { - Log.w(TAG, "Unexpected prepared ad " + adInfo); - } - } - - /** - * Notifies the IMA SDK that the specified ad has failed to prepare for playback. - */ - public void handlePrepareError(int adGroupIndex, int adIndexInAdGroup, IOException exception) { - if (player == null) { - return; - } - try { - handleAdPrepareError(adGroupIndex, adIndexInAdGroup, exception); - } catch (RuntimeException e) { - maybeNotifyInternalError("handlePrepareError", e); - } - } - - // Player.EventListener implementation. - - @Override - public void onTimelineChanged(Timeline timeline, @Player.TimelineChangeReason int reason) { - if (timeline.isEmpty()) { - // The player is being reset or contains no media. - return; - } - this.timeline = timeline; - Player player = checkNotNull(this.player); - long contentDurationUs = timeline.getPeriod(player.getCurrentPeriodIndex(), period).durationUs; - contentDurationMs = C.usToMs(contentDurationUs); - if (contentDurationUs != adPlaybackState.contentDurationUs) { - adPlaybackState = adPlaybackState.withContentDurationUs(contentDurationUs); - updateAdPlaybackState(); - } - long contentPositionMs = getContentPeriodPositionMs(player, timeline, period); - maybeInitializeAdsManager(contentPositionMs, contentDurationMs); - handleTimelineOrPositionChanged(); - } - - @Override - public void onPositionDiscontinuity(@Player.DiscontinuityReason int reason) { - handleTimelineOrPositionChanged(); - } - - @Override - public void onPlaybackStateChanged(@Player.State int playbackState) { - @Nullable Player player = this.player; - if (adsManager == null || player == null) { - return; - } - - if (playbackState == Player.STATE_BUFFERING - && !player.isPlayingAd() - && isWaitingForAdToLoad()) { - waitingForPreloadElapsedRealtimeMs = SystemClock.elapsedRealtime(); - } else if (playbackState == Player.STATE_READY) { - waitingForPreloadElapsedRealtimeMs = C.TIME_UNSET; - } - - handlePlayerStateChanged(player.getPlayWhenReady(), playbackState); - } - - @Override - public void onPlayWhenReadyChanged( - boolean playWhenReady, @Player.PlayWhenReadyChangeReason int reason) { - if (adsManager == null || player == null) { - return; - } - - if (imaAdState == IMA_AD_STATE_PLAYING && !playWhenReady) { - adsManager.pause(); - return; - } - - if (imaAdState == IMA_AD_STATE_PAUSED && playWhenReady) { - adsManager.resume(); - return; - } - handlePlayerStateChanged(playWhenReady, player.getPlaybackState()); - } - - @Override - public void onPlayerError(ExoPlaybackException error) { - if (imaAdState != IMA_AD_STATE_NONE) { - AdMediaInfo adMediaInfo = checkNotNull(imaAdMediaInfo); - for (int i = 0; i < adCallbacks.size(); i++) { - adCallbacks.get(i).onError(adMediaInfo); - } - } - } - - // Internal methods. - - private AdsLoader requestAds( - Context context, ImaSdkSettings imaSdkSettings, AdDisplayContainer adDisplayContainer) { - AdsLoader adsLoader = imaFactory.createAdsLoader(context, imaSdkSettings, adDisplayContainer); - adsLoader.addAdErrorListener(componentListener); - if (configuration.applicationAdErrorListener != null) { - adsLoader.addAdErrorListener(configuration.applicationAdErrorListener); - } - adsLoader.addAdsLoadedListener(componentListener); - AdsRequest request; - try { - request = ImaUtil.getAdsRequestForAdTagDataSpec(imaFactory, adTagDataSpec); - } catch (IOException e) { - adPlaybackState = new AdPlaybackState(adsId); - updateAdPlaybackState(); - pendingAdLoadError = AdLoadException.createForAllAds(e); - maybeNotifyPendingAdLoadError(); - return adsLoader; - } - pendingAdRequestContext = new Object(); - request.setUserRequestContext(pendingAdRequestContext); - if (configuration.enableContinuousPlayback != null) { - request.setContinuousPlayback(configuration.enableContinuousPlayback); - } - if (configuration.vastLoadTimeoutMs != TIMEOUT_UNSET) { - request.setVastLoadTimeout(configuration.vastLoadTimeoutMs); - } - request.setContentProgressProvider(componentListener); - adsLoader.requestAds(request); - return adsLoader; - } - - private void maybeInitializeAdsManager(long contentPositionMs, long contentDurationMs) { - @Nullable AdsManager adsManager = this.adsManager; - if (!isAdsManagerInitialized && adsManager != null) { - isAdsManagerInitialized = true; - @Nullable - AdsRenderingSettings adsRenderingSettings = - setupAdsRendering(contentPositionMs, contentDurationMs); - if (adsRenderingSettings == null) { - // There are no ads to play. - destroyAdsManager(); - } else { - adsManager.init(adsRenderingSettings); - adsManager.start(); - if (configuration.debugModeEnabled) { - Log.d(TAG, "Initialized with ads rendering settings: " + adsRenderingSettings); - } - } - updateAdPlaybackState(); - } - } - - /** - * Configures ads rendering for starting playback, returning the settings for the IMA SDK or - * {@code null} if no ads should play. - */ - @Nullable - private AdsRenderingSettings setupAdsRendering(long contentPositionMs, long contentDurationMs) { - AdsRenderingSettings adsRenderingSettings = imaFactory.createAdsRenderingSettings(); - adsRenderingSettings.setEnablePreloading(true); - adsRenderingSettings.setMimeTypes( - configuration.adMediaMimeTypes != null - ? configuration.adMediaMimeTypes - : supportedMimeTypes); - if (configuration.mediaLoadTimeoutMs != TIMEOUT_UNSET) { - adsRenderingSettings.setLoadVideoTimeout(configuration.mediaLoadTimeoutMs); - } - if (configuration.mediaBitrate != BITRATE_UNSET) { - adsRenderingSettings.setBitrateKbps(configuration.mediaBitrate / 1000); - } - adsRenderingSettings.setFocusSkipButtonWhenAvailable( - configuration.focusSkipButtonWhenAvailable); - if (configuration.adUiElements != null) { - adsRenderingSettings.setUiElements(configuration.adUiElements); - } - - // Skip ads based on the start position as required. - long[] adGroupTimesUs = adPlaybackState.adGroupTimesUs; - int adGroupForPositionIndex = - adPlaybackState.getAdGroupIndexForPositionUs( - C.msToUs(contentPositionMs), C.msToUs(contentDurationMs)); - if (adGroupForPositionIndex != C.INDEX_UNSET) { - boolean playAdWhenStartingPlayback = - configuration.playAdBeforeStartPosition - || adGroupTimesUs[adGroupForPositionIndex] == C.msToUs(contentPositionMs); - if (!playAdWhenStartingPlayback) { - adGroupForPositionIndex++; - } else if (hasMidrollAdGroups(adGroupTimesUs)) { - // Provide the player's initial position to trigger loading and playing the ad. If there are - // no midrolls, we are playing a preroll and any pending content position wouldn't be - // cleared. - pendingContentPositionMs = contentPositionMs; - } - if (adGroupForPositionIndex > 0) { - for (int i = 0; i < adGroupForPositionIndex; i++) { - adPlaybackState = adPlaybackState.withSkippedAdGroup(i); - } - if (adGroupForPositionIndex == adGroupTimesUs.length) { - // We don't need to play any ads. Because setPlayAdsAfterTime does not discard non-VMAP - // ads, we signal that no ads will render so the caller can destroy the ads manager. - return null; - } - long adGroupForPositionTimeUs = adGroupTimesUs[adGroupForPositionIndex]; - long adGroupBeforePositionTimeUs = adGroupTimesUs[adGroupForPositionIndex - 1]; - if (adGroupForPositionTimeUs == C.TIME_END_OF_SOURCE) { - // Play the postroll by offsetting the start position just past the last non-postroll ad. - adsRenderingSettings.setPlayAdsAfterTime( - (double) adGroupBeforePositionTimeUs / C.MICROS_PER_SECOND + 1d); - } else { - // Play ads after the midpoint between the ad to play and the one before it, to avoid - // issues with rounding one of the two ad times. - double midpointTimeUs = (adGroupForPositionTimeUs + adGroupBeforePositionTimeUs) / 2d; - adsRenderingSettings.setPlayAdsAfterTime(midpointTimeUs / C.MICROS_PER_SECOND); - } - } - } - return adsRenderingSettings; - } - - private VideoProgressUpdate getContentVideoProgressUpdate() { - boolean hasContentDuration = contentDurationMs != C.TIME_UNSET; - long contentPositionMs; - if (pendingContentPositionMs != C.TIME_UNSET) { - sentPendingContentPositionMs = true; - contentPositionMs = pendingContentPositionMs; - } else if (player == null) { - return lastContentProgress; - } else if (fakeContentProgressElapsedRealtimeMs != C.TIME_UNSET) { - long elapsedSinceEndMs = SystemClock.elapsedRealtime() - fakeContentProgressElapsedRealtimeMs; - contentPositionMs = fakeContentProgressOffsetMs + elapsedSinceEndMs; - } else if (imaAdState == IMA_AD_STATE_NONE && !playingAd && hasContentDuration) { - contentPositionMs = getContentPeriodPositionMs(player, timeline, period); - } else { - return VideoProgressUpdate.VIDEO_TIME_NOT_READY; - } - long contentDurationMs = hasContentDuration ? this.contentDurationMs : IMA_DURATION_UNSET; - return new VideoProgressUpdate(contentPositionMs, contentDurationMs); - } - - private VideoProgressUpdate getAdVideoProgressUpdate() { - if (player == null) { - return lastAdProgress; - } else if (imaAdState != IMA_AD_STATE_NONE && playingAd) { - long adDuration = player.getDuration(); - return adDuration == C.TIME_UNSET - ? VideoProgressUpdate.VIDEO_TIME_NOT_READY - : new VideoProgressUpdate(player.getCurrentPosition(), adDuration); - } else { - return VideoProgressUpdate.VIDEO_TIME_NOT_READY; - } - } - - private void updateAdProgress() { - VideoProgressUpdate videoProgressUpdate = getAdVideoProgressUpdate(); - if (configuration.debugModeEnabled) { - Log.d(TAG, "Ad progress: " + ImaUtil.getStringForVideoProgressUpdate(videoProgressUpdate)); - } - - AdMediaInfo adMediaInfo = checkNotNull(imaAdMediaInfo); - for (int i = 0; i < adCallbacks.size(); i++) { - adCallbacks.get(i).onAdProgress(adMediaInfo, videoProgressUpdate); - } - handler.removeCallbacks(updateAdProgressRunnable); - handler.postDelayed(updateAdProgressRunnable, AD_PROGRESS_UPDATE_INTERVAL_MS); - } - - private void stopUpdatingAdProgress() { - handler.removeCallbacks(updateAdProgressRunnable); - } - - private int getPlayerVolumePercent() { - @Nullable Player player = this.player; - if (player == null) { - return lastVolumePercent; - } - - @Nullable Player.AudioComponent audioComponent = player.getAudioComponent(); - if (audioComponent != null) { - return (int) (audioComponent.getVolume() * 100); - } - - // Check for a selected track using an audio renderer. - TrackSelectionArray trackSelections = player.getCurrentTrackSelections(); - for (int i = 0; i < player.getRendererCount() && i < trackSelections.length; i++) { - if (player.getRendererType(i) == C.TRACK_TYPE_AUDIO && trackSelections.get(i) != null) { - return 100; - } - } - return 0; - } - - private void handleAdEvent(AdEvent adEvent) { - if (adsManager == null) { - // Drop events after release. - return; - } - switch (adEvent.getType()) { - case AD_BREAK_FETCH_ERROR: - String adGroupTimeSecondsString = checkNotNull(adEvent.getAdData().get("adBreakTime")); - if (configuration.debugModeEnabled) { - Log.d(TAG, "Fetch error for ad at " + adGroupTimeSecondsString + " seconds"); - } - double adGroupTimeSeconds = Double.parseDouble(adGroupTimeSecondsString); - int adGroupIndex = - adGroupTimeSeconds == -1.0 - ? adPlaybackState.adGroupCount - 1 - : getAdGroupIndexForCuePointTimeSeconds(adGroupTimeSeconds); - markAdGroupInErrorStateAndClearPendingContentPosition(adGroupIndex); - break; - case CONTENT_PAUSE_REQUESTED: - // After CONTENT_PAUSE_REQUESTED, IMA will playAd/pauseAd/stopAd to show one or more ads - // before sending CONTENT_RESUME_REQUESTED. - imaPausedContent = true; - pauseContentInternal(); - break; - case TAPPED: - for (int i = 0; i < eventListeners.size(); i++) { - eventListeners.get(i).onAdTapped(); - } - break; - case CLICKED: - for (int i = 0; i < eventListeners.size(); i++) { - eventListeners.get(i).onAdClicked(); - } - break; - case CONTENT_RESUME_REQUESTED: - imaPausedContent = false; - resumeContentInternal(); - break; - case LOG: - Map adData = adEvent.getAdData(); - String message = "AdEvent: " + adData; - Log.i(TAG, message); - break; - default: - break; - } - } - - private void pauseContentInternal() { - imaAdState = IMA_AD_STATE_NONE; - if (sentPendingContentPositionMs) { - pendingContentPositionMs = C.TIME_UNSET; - sentPendingContentPositionMs = false; - } - } - - private void resumeContentInternal() { - if (imaAdInfo != null) { - adPlaybackState = adPlaybackState.withSkippedAdGroup(imaAdInfo.adGroupIndex); - updateAdPlaybackState(); - } - } - - /** - * Returns whether this instance is expecting the first ad in an the upcoming ad group to load - * within the {@link ImaUtil.Configuration#adPreloadTimeoutMs preload timeout}. - */ - private boolean isWaitingForAdToLoad() { - @Nullable Player player = this.player; - if (player == null) { - return false; - } - int adGroupIndex = getLoadingAdGroupIndex(); - if (adGroupIndex == C.INDEX_UNSET) { - return false; - } - AdPlaybackState.AdGroup adGroup = adPlaybackState.adGroups[adGroupIndex]; - if (adGroup.count != C.LENGTH_UNSET - && adGroup.count != 0 - && adGroup.states[0] != AdPlaybackState.AD_STATE_UNAVAILABLE) { - // An ad is available already. - return false; - } - long adGroupTimeMs = C.usToMs(adPlaybackState.adGroupTimesUs[adGroupIndex]); - long contentPositionMs = getContentPeriodPositionMs(player, timeline, period); - long timeUntilAdMs = adGroupTimeMs - contentPositionMs; - return timeUntilAdMs < configuration.adPreloadTimeoutMs; - } - - private void handlePlayerStateChanged(boolean playWhenReady, @Player.State int playbackState) { - if (playingAd && imaAdState == IMA_AD_STATE_PLAYING) { - if (!bufferingAd && playbackState == Player.STATE_BUFFERING) { - bufferingAd = true; - AdMediaInfo adMediaInfo = checkNotNull(imaAdMediaInfo); - for (int i = 0; i < adCallbacks.size(); i++) { - adCallbacks.get(i).onBuffering(adMediaInfo); - } - stopUpdatingAdProgress(); - } else if (bufferingAd && playbackState == Player.STATE_READY) { - bufferingAd = false; - updateAdProgress(); - } - } - - if (imaAdState == IMA_AD_STATE_NONE - && playbackState == Player.STATE_BUFFERING - && playWhenReady) { - ensureSentContentCompleteIfAtEndOfStream(); - } else if (imaAdState != IMA_AD_STATE_NONE && playbackState == Player.STATE_ENDED) { - @Nullable AdMediaInfo adMediaInfo = imaAdMediaInfo; - if (adMediaInfo == null) { - Log.w(TAG, "onEnded without ad media info"); - } else { - for (int i = 0; i < adCallbacks.size(); i++) { - adCallbacks.get(i).onEnded(adMediaInfo); - } - } - if (configuration.debugModeEnabled) { - Log.d(TAG, "VideoAdPlayerCallback.onEnded in onPlaybackStateChanged"); - } - } - } - - private void handleTimelineOrPositionChanged() { - @Nullable Player player = this.player; - if (adsManager == null || player == null) { - return; - } - if (!playingAd && !player.isPlayingAd()) { - ensureSentContentCompleteIfAtEndOfStream(); - if (!sentContentComplete && !timeline.isEmpty()) { - long positionMs = getContentPeriodPositionMs(player, timeline, period); - timeline.getPeriod(/* periodIndex= */ 0, period); - int newAdGroupIndex = period.getAdGroupIndexForPositionUs(C.msToUs(positionMs)); - if (newAdGroupIndex != C.INDEX_UNSET) { - sentPendingContentPositionMs = false; - pendingContentPositionMs = positionMs; - } - } - } - - boolean wasPlayingAd = playingAd; - int oldPlayingAdIndexInAdGroup = playingAdIndexInAdGroup; - playingAd = player.isPlayingAd(); - playingAdIndexInAdGroup = playingAd ? player.getCurrentAdIndexInAdGroup() : C.INDEX_UNSET; - boolean adFinished = wasPlayingAd && playingAdIndexInAdGroup != oldPlayingAdIndexInAdGroup; - if (adFinished) { - // IMA is waiting for the ad playback to finish so invoke the callback now. - // Either CONTENT_RESUME_REQUESTED will be passed next, or playAd will be called again. - @Nullable AdMediaInfo adMediaInfo = imaAdMediaInfo; - if (adMediaInfo == null) { - Log.w(TAG, "onEnded without ad media info"); - } else { - @Nullable AdInfo adInfo = adInfoByAdMediaInfo.get(adMediaInfo); - if (playingAdIndexInAdGroup == C.INDEX_UNSET - || (adInfo != null && adInfo.adIndexInAdGroup < playingAdIndexInAdGroup)) { - for (int i = 0; i < adCallbacks.size(); i++) { - adCallbacks.get(i).onEnded(adMediaInfo); - } - if (configuration.debugModeEnabled) { - Log.d( - TAG, "VideoAdPlayerCallback.onEnded in onTimelineChanged/onPositionDiscontinuity"); - } - } - } - } - if (!sentContentComplete && !wasPlayingAd && playingAd && imaAdState == IMA_AD_STATE_NONE) { - int adGroupIndex = player.getCurrentAdGroupIndex(); - if (adPlaybackState.adGroupTimesUs[adGroupIndex] == C.TIME_END_OF_SOURCE) { - sendContentComplete(); - } else { - // IMA hasn't called playAd yet, so fake the content position. - fakeContentProgressElapsedRealtimeMs = SystemClock.elapsedRealtime(); - fakeContentProgressOffsetMs = C.usToMs(adPlaybackState.adGroupTimesUs[adGroupIndex]); - if (fakeContentProgressOffsetMs == C.TIME_END_OF_SOURCE) { - fakeContentProgressOffsetMs = contentDurationMs; - } - } - } - } - - private void loadAdInternal(AdMediaInfo adMediaInfo, AdPodInfo adPodInfo) { - if (adsManager == null) { - // Drop events after release. - if (configuration.debugModeEnabled) { - Log.d( - TAG, - "loadAd after release " + getAdMediaInfoString(adMediaInfo) + ", ad pod " + adPodInfo); - } - return; - } - - int adGroupIndex = getAdGroupIndexForAdPod(adPodInfo); - int adIndexInAdGroup = adPodInfo.getAdPosition() - 1; - AdInfo adInfo = new AdInfo(adGroupIndex, adIndexInAdGroup); - // The ad URI may already be known, so force put to update it if needed. - adInfoByAdMediaInfo.forcePut(adMediaInfo, adInfo); - if (configuration.debugModeEnabled) { - Log.d(TAG, "loadAd " + getAdMediaInfoString(adMediaInfo)); - } - if (adPlaybackState.isAdInErrorState(adGroupIndex, adIndexInAdGroup)) { - // We have already marked this ad as having failed to load, so ignore the request. IMA will - // timeout after its media load timeout. - return; - } - - // The ad count may increase on successive loads of ads in the same ad pod, for example, due to - // separate requests for ad tags with multiple ads within the ad pod completing after an earlier - // ad has loaded. See also https://github.com/google/ExoPlayer/issues/7477. - AdPlaybackState.AdGroup adGroup = adPlaybackState.adGroups[adInfo.adGroupIndex]; - adPlaybackState = - adPlaybackState.withAdCount( - adInfo.adGroupIndex, max(adPodInfo.getTotalAds(), adGroup.states.length)); - adGroup = adPlaybackState.adGroups[adInfo.adGroupIndex]; - for (int i = 0; i < adIndexInAdGroup; i++) { - // Any preceding ads that haven't loaded are not going to load. - if (adGroup.states[i] == AdPlaybackState.AD_STATE_UNAVAILABLE) { - adPlaybackState = adPlaybackState.withAdLoadError(adGroupIndex, /* adIndexInAdGroup= */ i); - } - } - - Uri adUri = Uri.parse(adMediaInfo.getUrl()); - adPlaybackState = - adPlaybackState.withAdUri(adInfo.adGroupIndex, adInfo.adIndexInAdGroup, adUri); - updateAdPlaybackState(); - } - - private void playAdInternal(AdMediaInfo adMediaInfo) { - if (configuration.debugModeEnabled) { - Log.d(TAG, "playAd " + getAdMediaInfoString(adMediaInfo)); - } - if (adsManager == null) { - // Drop events after release. - return; - } - - if (imaAdState == IMA_AD_STATE_PLAYING) { - // IMA does not always call stopAd before resuming content. - // See [Internal: b/38354028]. - Log.w(TAG, "Unexpected playAd without stopAd"); - } - - if (imaAdState == IMA_AD_STATE_NONE) { - // IMA is requesting to play the ad, so stop faking the content position. - fakeContentProgressElapsedRealtimeMs = C.TIME_UNSET; - fakeContentProgressOffsetMs = C.TIME_UNSET; - imaAdState = IMA_AD_STATE_PLAYING; - imaAdMediaInfo = adMediaInfo; - imaAdInfo = checkNotNull(adInfoByAdMediaInfo.get(adMediaInfo)); - for (int i = 0; i < adCallbacks.size(); i++) { - adCallbacks.get(i).onPlay(adMediaInfo); - } - if (pendingAdPrepareErrorAdInfo != null && pendingAdPrepareErrorAdInfo.equals(imaAdInfo)) { - pendingAdPrepareErrorAdInfo = null; - for (int i = 0; i < adCallbacks.size(); i++) { - adCallbacks.get(i).onError(adMediaInfo); - } - } - updateAdProgress(); - } else { - imaAdState = IMA_AD_STATE_PLAYING; - checkState(adMediaInfo.equals(imaAdMediaInfo)); - for (int i = 0; i < adCallbacks.size(); i++) { - adCallbacks.get(i).onResume(adMediaInfo); - } - } - if (player == null || !player.getPlayWhenReady()) { - // Either this loader hasn't been activated yet, or the player is paused now. - checkNotNull(adsManager).pause(); - } - } - - private void pauseAdInternal(AdMediaInfo adMediaInfo) { - if (configuration.debugModeEnabled) { - Log.d(TAG, "pauseAd " + getAdMediaInfoString(adMediaInfo)); - } - if (adsManager == null) { - // Drop event after release. - return; - } - if (imaAdState == IMA_AD_STATE_NONE) { - // This method is called if loadAd has been called but the loaded ad won't play due to a seek - // to a different position, so drop the event. See also [Internal: b/159111848]. - return; - } - if (configuration.debugModeEnabled && !adMediaInfo.equals(imaAdMediaInfo)) { - Log.w( - TAG, - "Unexpected pauseAd for " - + getAdMediaInfoString(adMediaInfo) - + ", expected " - + getAdMediaInfoString(imaAdMediaInfo)); - } - imaAdState = IMA_AD_STATE_PAUSED; - for (int i = 0; i < adCallbacks.size(); i++) { - adCallbacks.get(i).onPause(adMediaInfo); - } - } - - private void stopAdInternal(AdMediaInfo adMediaInfo) { - if (configuration.debugModeEnabled) { - Log.d(TAG, "stopAd " + getAdMediaInfoString(adMediaInfo)); - } - if (adsManager == null) { - // Drop event after release. - return; - } - if (imaAdState == IMA_AD_STATE_NONE) { - // This method is called if loadAd has been called but the preloaded ad won't play due to a - // seek to a different position, so drop the event and discard the ad. See also [Internal: - // b/159111848]. - @Nullable AdInfo adInfo = adInfoByAdMediaInfo.get(adMediaInfo); - if (adInfo != null) { - adPlaybackState = - adPlaybackState.withSkippedAd(adInfo.adGroupIndex, adInfo.adIndexInAdGroup); - updateAdPlaybackState(); - } - return; - } - imaAdState = IMA_AD_STATE_NONE; - stopUpdatingAdProgress(); - // TODO: Handle the skipped event so the ad can be marked as skipped rather than played. - checkNotNull(imaAdInfo); - int adGroupIndex = imaAdInfo.adGroupIndex; - int adIndexInAdGroup = imaAdInfo.adIndexInAdGroup; - if (adPlaybackState.isAdInErrorState(adGroupIndex, adIndexInAdGroup)) { - // We have already marked this ad as having failed to load, so ignore the request. - return; - } - adPlaybackState = - adPlaybackState.withPlayedAd(adGroupIndex, adIndexInAdGroup).withAdResumePositionUs(0); - updateAdPlaybackState(); - if (!playingAd) { - imaAdMediaInfo = null; - imaAdInfo = null; - } - } - - private void handleAdGroupLoadError(Exception error) { - int adGroupIndex = getLoadingAdGroupIndex(); - if (adGroupIndex == C.INDEX_UNSET) { - Log.w(TAG, "Unable to determine ad group index for ad group load error", error); - return; - } - markAdGroupInErrorStateAndClearPendingContentPosition(adGroupIndex); - if (pendingAdLoadError == null) { - pendingAdLoadError = AdLoadException.createForAdGroup(error, adGroupIndex); - } - } - - private void markAdGroupInErrorStateAndClearPendingContentPosition(int adGroupIndex) { - // Update the ad playback state so all ads in the ad group are in the error state. - AdPlaybackState.AdGroup adGroup = adPlaybackState.adGroups[adGroupIndex]; - if (adGroup.count == C.LENGTH_UNSET) { - adPlaybackState = adPlaybackState.withAdCount(adGroupIndex, max(1, adGroup.states.length)); - adGroup = adPlaybackState.adGroups[adGroupIndex]; - } - for (int i = 0; i < adGroup.count; i++) { - if (adGroup.states[i] == AdPlaybackState.AD_STATE_UNAVAILABLE) { - if (configuration.debugModeEnabled) { - Log.d(TAG, "Removing ad " + i + " in ad group " + adGroupIndex); - } - adPlaybackState = adPlaybackState.withAdLoadError(adGroupIndex, i); - } - } - updateAdPlaybackState(); - // Clear any pending content position that triggered attempting to load the ad group. - pendingContentPositionMs = C.TIME_UNSET; - fakeContentProgressElapsedRealtimeMs = C.TIME_UNSET; - } - - private void handleAdPrepareError(int adGroupIndex, int adIndexInAdGroup, Exception exception) { - if (configuration.debugModeEnabled) { - Log.d( - TAG, "Prepare error for ad " + adIndexInAdGroup + " in group " + adGroupIndex, exception); - } - if (adsManager == null) { - Log.w(TAG, "Ignoring ad prepare error after release"); - return; - } - if (imaAdState == IMA_AD_STATE_NONE) { - // Send IMA a content position at the ad group so that it will try to play it, at which point - // we can notify that it failed to load. - fakeContentProgressElapsedRealtimeMs = SystemClock.elapsedRealtime(); - fakeContentProgressOffsetMs = C.usToMs(adPlaybackState.adGroupTimesUs[adGroupIndex]); - if (fakeContentProgressOffsetMs == C.TIME_END_OF_SOURCE) { - fakeContentProgressOffsetMs = contentDurationMs; - } - pendingAdPrepareErrorAdInfo = new AdInfo(adGroupIndex, adIndexInAdGroup); - } else { - AdMediaInfo adMediaInfo = checkNotNull(imaAdMediaInfo); - // We're already playing an ad. - if (adIndexInAdGroup > playingAdIndexInAdGroup) { - // Mark the playing ad as ended so we can notify the error on the next ad and remove it, - // which means that the ad after will load (if any). - for (int i = 0; i < adCallbacks.size(); i++) { - adCallbacks.get(i).onEnded(adMediaInfo); - } - } - playingAdIndexInAdGroup = adPlaybackState.adGroups[adGroupIndex].getFirstAdIndexToPlay(); - for (int i = 0; i < adCallbacks.size(); i++) { - adCallbacks.get(i).onError(checkNotNull(adMediaInfo)); - } - } - adPlaybackState = adPlaybackState.withAdLoadError(adGroupIndex, adIndexInAdGroup); - updateAdPlaybackState(); - } - - private void ensureSentContentCompleteIfAtEndOfStream() { - if (!sentContentComplete - && contentDurationMs != C.TIME_UNSET - && pendingContentPositionMs == C.TIME_UNSET - && getContentPeriodPositionMs(checkNotNull(player), timeline, period) - + THRESHOLD_END_OF_CONTENT_MS - >= contentDurationMs) { - sendContentComplete(); - } - } - - private void sendContentComplete() { - for (int i = 0; i < adCallbacks.size(); i++) { - adCallbacks.get(i).onContentComplete(); - } - sentContentComplete = true; - if (configuration.debugModeEnabled) { - Log.d(TAG, "adsLoader.contentComplete"); - } - for (int i = 0; i < adPlaybackState.adGroupCount; i++) { - if (adPlaybackState.adGroupTimesUs[i] != C.TIME_END_OF_SOURCE) { - adPlaybackState = adPlaybackState.withSkippedAdGroup(/* adGroupIndex= */ i); - } - } - updateAdPlaybackState(); - } - - private void updateAdPlaybackState() { - for (int i = 0; i < eventListeners.size(); i++) { - eventListeners.get(i).onAdPlaybackState(adPlaybackState); - } - } - - private void maybeNotifyPendingAdLoadError() { - if (pendingAdLoadError != null) { - for (int i = 0; i < eventListeners.size(); i++) { - eventListeners.get(i).onAdLoadError(pendingAdLoadError, adTagDataSpec); - } - pendingAdLoadError = null; - } - } - - private void maybeNotifyInternalError(String name, Exception cause) { - String message = "Internal error in " + name; - Log.e(TAG, message, cause); - // We can't recover from an unexpected error in general, so skip all remaining ads. - for (int i = 0; i < adPlaybackState.adGroupCount; i++) { - adPlaybackState = adPlaybackState.withSkippedAdGroup(i); - } - updateAdPlaybackState(); - for (int i = 0; i < eventListeners.size(); i++) { - eventListeners - .get(i) - .onAdLoadError( - AdLoadException.createForUnexpected(new RuntimeException(message, cause)), - adTagDataSpec); - } - } - - private int getAdGroupIndexForAdPod(AdPodInfo adPodInfo) { - if (adPodInfo.getPodIndex() == -1) { - // This is a postroll ad. - return adPlaybackState.adGroupCount - 1; - } - - // adPodInfo.podIndex may be 0-based or 1-based, so for now look up the cue point instead. - return getAdGroupIndexForCuePointTimeSeconds(adPodInfo.getTimeOffset()); - } - - /** - * Returns the index of the ad group that will preload next, or {@link C#INDEX_UNSET} if there is - * no such ad group. - */ - private int getLoadingAdGroupIndex() { - if (player == null) { - return C.INDEX_UNSET; - } - long playerPositionUs = C.msToUs(getContentPeriodPositionMs(player, timeline, period)); - int adGroupIndex = - adPlaybackState.getAdGroupIndexForPositionUs(playerPositionUs, C.msToUs(contentDurationMs)); - if (adGroupIndex == C.INDEX_UNSET) { - adGroupIndex = - adPlaybackState.getAdGroupIndexAfterPositionUs( - playerPositionUs, C.msToUs(contentDurationMs)); - } - return adGroupIndex; - } - - private int getAdGroupIndexForCuePointTimeSeconds(double cuePointTimeSeconds) { - // We receive initial cue points from IMA SDK as floats. This code replicates the same - // calculation used to populate adGroupTimesUs (having truncated input back to float, to avoid - // failures if the behavior of the IMA SDK changes to provide greater precision). - float cuePointTimeSecondsFloat = (float) cuePointTimeSeconds; - long adPodTimeUs = Math.round((double) cuePointTimeSecondsFloat * C.MICROS_PER_SECOND); - for (int adGroupIndex = 0; adGroupIndex < adPlaybackState.adGroupCount; adGroupIndex++) { - long adGroupTimeUs = adPlaybackState.adGroupTimesUs[adGroupIndex]; - if (adGroupTimeUs != C.TIME_END_OF_SOURCE - && Math.abs(adGroupTimeUs - adPodTimeUs) < THRESHOLD_AD_MATCH_US) { - return adGroupIndex; - } - } - throw new IllegalStateException("Failed to find cue point"); - } - - private String getAdMediaInfoString(@Nullable AdMediaInfo adMediaInfo) { - @Nullable AdInfo adInfo = adInfoByAdMediaInfo.get(adMediaInfo); - return "AdMediaInfo[" - + (adMediaInfo == null ? "null" : adMediaInfo.getUrl()) - + ", " - + adInfo - + "]"; - } - - private static long getContentPeriodPositionMs( - Player player, Timeline timeline, Timeline.Period period) { - long contentWindowPositionMs = player.getContentPosition(); - if (timeline.isEmpty()) { - return contentWindowPositionMs; - } else { - return contentWindowPositionMs - - timeline.getPeriod(player.getCurrentPeriodIndex(), period).getPositionInWindowMs(); - } - } - - private static boolean hasMidrollAdGroups(long[] adGroupTimesUs) { - int count = adGroupTimesUs.length; - if (count == 1) { - return adGroupTimesUs[0] != 0 && adGroupTimesUs[0] != C.TIME_END_OF_SOURCE; - } else if (count == 2) { - return adGroupTimesUs[0] != 0 || adGroupTimesUs[1] != C.TIME_END_OF_SOURCE; - } else { - // There's at least one midroll ad group, as adGroupTimesUs is never empty. - return true; - } - } - - private void destroyAdsManager() { - if (adsManager != null) { - adsManager.removeAdErrorListener(componentListener); - if (configuration.applicationAdErrorListener != null) { - adsManager.removeAdErrorListener(configuration.applicationAdErrorListener); - } - adsManager.removeAdEventListener(componentListener); - if (configuration.applicationAdEventListener != null) { - adsManager.removeAdEventListener(configuration.applicationAdEventListener); - } - adsManager.destroy(); - adsManager = null; - } - } - - private final class ComponentListener - implements AdsLoadedListener, - ContentProgressProvider, - AdEventListener, - AdErrorListener, - VideoAdPlayer { - - // AdsLoader.AdsLoadedListener implementation. - - @Override - public void onAdsManagerLoaded(AdsManagerLoadedEvent adsManagerLoadedEvent) { - AdsManager adsManager = adsManagerLoadedEvent.getAdsManager(); - if (!Util.areEqual(pendingAdRequestContext, adsManagerLoadedEvent.getUserRequestContext())) { - adsManager.destroy(); - return; - } - pendingAdRequestContext = null; - AdTagLoader.this.adsManager = adsManager; - adsManager.addAdErrorListener(this); - if (configuration.applicationAdErrorListener != null) { - adsManager.addAdErrorListener(configuration.applicationAdErrorListener); - } - adsManager.addAdEventListener(this); - if (configuration.applicationAdEventListener != null) { - adsManager.addAdEventListener(configuration.applicationAdEventListener); - } - try { - adPlaybackState = - new AdPlaybackState(adsId, getAdGroupTimesUsForCuePoints(adsManager.getAdCuePoints())); - updateAdPlaybackState(); - } catch (RuntimeException e) { - maybeNotifyInternalError("onAdsManagerLoaded", e); - } - } - - // ContentProgressProvider implementation. - - @Override - public VideoProgressUpdate getContentProgress() { - VideoProgressUpdate videoProgressUpdate = getContentVideoProgressUpdate(); - if (configuration.debugModeEnabled) { - Log.d( - TAG, - "Content progress: " + ImaUtil.getStringForVideoProgressUpdate(videoProgressUpdate)); - } - - if (waitingForPreloadElapsedRealtimeMs != C.TIME_UNSET) { - // IMA is polling the player position but we are buffering for an ad to preload, so playback - // may be stuck. Detect this case and signal an error if applicable. - long stuckElapsedRealtimeMs = - SystemClock.elapsedRealtime() - waitingForPreloadElapsedRealtimeMs; - if (stuckElapsedRealtimeMs >= THRESHOLD_AD_PRELOAD_MS) { - waitingForPreloadElapsedRealtimeMs = C.TIME_UNSET; - handleAdGroupLoadError(new IOException("Ad preloading timed out")); - maybeNotifyPendingAdLoadError(); - } - } else if (pendingContentPositionMs != C.TIME_UNSET - && player != null - && player.getPlaybackState() == Player.STATE_BUFFERING - && isWaitingForAdToLoad()) { - // Prepare to timeout the load of an ad for the pending seek operation. - waitingForPreloadElapsedRealtimeMs = SystemClock.elapsedRealtime(); - } - - return videoProgressUpdate; - } - - // AdEvent.AdEventListener implementation. - - @Override - public void onAdEvent(AdEvent adEvent) { - AdEventType adEventType = adEvent.getType(); - if (configuration.debugModeEnabled && adEventType != AdEventType.AD_PROGRESS) { - Log.d(TAG, "onAdEvent: " + adEventType); - } - try { - handleAdEvent(adEvent); - } catch (RuntimeException e) { - maybeNotifyInternalError("onAdEvent", e); - } - } - - // AdErrorEvent.AdErrorListener implementation. - - @Override - public void onAdError(AdErrorEvent adErrorEvent) { - AdError error = adErrorEvent.getError(); - if (configuration.debugModeEnabled) { - Log.d(TAG, "onAdError", error); - } - if (adsManager == null) { - // No ads were loaded, so allow playback to start without any ads. - pendingAdRequestContext = null; - adPlaybackState = new AdPlaybackState(adsId); - updateAdPlaybackState(); - } else if (ImaUtil.isAdGroupLoadError(error)) { - try { - handleAdGroupLoadError(error); - } catch (RuntimeException e) { - maybeNotifyInternalError("onAdError", e); - } - } - if (pendingAdLoadError == null) { - pendingAdLoadError = AdLoadException.createForAllAds(error); - } - maybeNotifyPendingAdLoadError(); - } - - // VideoAdPlayer implementation. - - @Override - public void addCallback(VideoAdPlayerCallback videoAdPlayerCallback) { - adCallbacks.add(videoAdPlayerCallback); - } - - @Override - public void removeCallback(VideoAdPlayerCallback videoAdPlayerCallback) { - adCallbacks.remove(videoAdPlayerCallback); - } - - @Override - public VideoProgressUpdate getAdProgress() { - throw new IllegalStateException("Unexpected call to getAdProgress when using preloading"); - } - - @Override - public int getVolume() { - return getPlayerVolumePercent(); - } - - @Override - public void loadAd(AdMediaInfo adMediaInfo, AdPodInfo adPodInfo) { - try { - loadAdInternal(adMediaInfo, adPodInfo); - } catch (RuntimeException e) { - maybeNotifyInternalError("loadAd", e); - } - } - - @Override - public void playAd(AdMediaInfo adMediaInfo) { - try { - playAdInternal(adMediaInfo); - } catch (RuntimeException e) { - maybeNotifyInternalError("playAd", e); - } - } - - @Override - public void pauseAd(AdMediaInfo adMediaInfo) { - try { - pauseAdInternal(adMediaInfo); - } catch (RuntimeException e) { - maybeNotifyInternalError("pauseAd", e); - } - } - - @Override - public void stopAd(AdMediaInfo adMediaInfo) { - try { - stopAdInternal(adMediaInfo); - } catch (RuntimeException e) { - maybeNotifyInternalError("stopAd", e); - } - } - - @Override - public void release() { - // Do nothing. - } - } - - // TODO: Consider moving this into AdPlaybackState. - private static final class AdInfo { - - public final int adGroupIndex; - public final int adIndexInAdGroup; - - public AdInfo(int adGroupIndex, int adIndexInAdGroup) { - this.adGroupIndex = adGroupIndex; - this.adIndexInAdGroup = adIndexInAdGroup; - } - - @Override - public boolean equals(@Nullable Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - AdInfo adInfo = (AdInfo) o; - if (adGroupIndex != adInfo.adGroupIndex) { - return false; - } - return adIndexInAdGroup == adInfo.adIndexInAdGroup; - } - - @Override - public int hashCode() { - int result = adGroupIndex; - result = 31 * result + adIndexInAdGroup; - return result; - } - - @Override - public String toString() { - return "(" + adGroupIndex + ", " + adIndexInAdGroup + ')'; - } - } -} diff --git a/automatedtests/src/androidTestR2_13_1/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java b/automatedtests/src/androidTestR2_13_1/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java deleted file mode 100644 index c7cd6636..00000000 --- a/automatedtests/src/androidTestR2_13_1/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java +++ /dev/null @@ -1,770 +0,0 @@ -/* - * Copyright (C) 2017 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.google.android.exoplayer2.ext.ima; - -import static com.google.android.exoplayer2.ext.ima.ImaUtil.BITRATE_UNSET; -import static com.google.android.exoplayer2.ext.ima.ImaUtil.TIMEOUT_UNSET; -import static com.google.android.exoplayer2.ext.ima.ImaUtil.getImaLooper; -import static com.google.android.exoplayer2.util.Assertions.checkArgument; -import static com.google.android.exoplayer2.util.Assertions.checkNotNull; -import static com.google.android.exoplayer2.util.Assertions.checkState; - -import android.content.Context; -import android.os.Looper; -import android.view.View; -import android.view.ViewGroup; -import androidx.annotation.Nullable; -import androidx.annotation.VisibleForTesting; -import com.google.ads.interactivemedia.v3.api.AdDisplayContainer; -import com.google.ads.interactivemedia.v3.api.AdErrorEvent.AdErrorListener; -import com.google.ads.interactivemedia.v3.api.AdEvent.AdEventListener; -import com.google.ads.interactivemedia.v3.api.AdsManager; -import com.google.ads.interactivemedia.v3.api.AdsRenderingSettings; -import com.google.ads.interactivemedia.v3.api.AdsRequest; -import com.google.ads.interactivemedia.v3.api.CompanionAdSlot; -import com.google.ads.interactivemedia.v3.api.FriendlyObstruction; -import com.google.ads.interactivemedia.v3.api.FriendlyObstructionPurpose; -import com.google.ads.interactivemedia.v3.api.ImaSdkFactory; -import com.google.ads.interactivemedia.v3.api.ImaSdkSettings; -import com.google.ads.interactivemedia.v3.api.UiElement; -import com.google.ads.interactivemedia.v3.api.player.VideoAdPlayer; -import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.ExoPlayerLibraryInfo; -import com.google.android.exoplayer2.Player; -import com.google.android.exoplayer2.Timeline; -import com.google.android.exoplayer2.source.MediaSourceFactory; -import com.google.android.exoplayer2.source.ads.AdsLoader; -import com.google.android.exoplayer2.source.ads.AdsMediaSource; -import com.google.android.exoplayer2.upstream.DataSpec; -import com.google.android.exoplayer2.util.MimeTypes; -import com.google.android.exoplayer2.util.Util; -import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableSet; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Set; - -/** - * {@link AdsLoader} using the IMA SDK. All methods must be called on the main thread. - * - *

The player instance that will play the loaded ads must be set before playback using {@link - * #setPlayer(Player)}. If the ads loader is no longer required, it must be released by calling - * {@link #release()}. - * - *

See https://developers.google.com/interactive-media-ads/docs/sdks/android/compatibility for - * information on compatible ad tag formats. Pass the ad tag URI when setting media item playback - * properties (if using the media item API) or as a {@link DataSpec} when constructing the {@link - * AdsMediaSource} (if using media sources directly). For the latter case, please note that this - * implementation delegates loading of the data spec to the IMA SDK, so range and headers - * specifications will be ignored in ad tag URIs. Literal ads responses can be encoded as data - * scheme data specs, for example, by constructing the data spec using a URI generated via {@link - * Util#getDataUriForString(String, String)}. - * - *

The IMA SDK can report obstructions to the ad view for accurate viewability measurement. This - * means that any overlay views that obstruct the ad overlay but are essential for playback need to - * be registered via the {@link AdViewProvider} passed to the {@link AdsMediaSource}. See the IMA - * SDK Open Measurement documentation for more information. - */ -public final class ImaAdsLoader implements Player.EventListener, AdsLoader { - - static { - ExoPlayerLibraryInfo.registerModule("goog.exo.ima"); - } - - /** - * Builder for {@link ImaAdsLoader}. - */ - public static final class Builder { - - /** - * The default duration in milliseconds for which the player must buffer while preloading an ad - * group before that ad group is skipped and marked as having failed to load. - * - *

This value should be large enough not to trigger discarding the ad when it actually might - * load soon, but small enough so that user is not waiting for too long. - * - * @see #setAdPreloadTimeoutMs(long) - */ - public static final long DEFAULT_AD_PRELOAD_TIMEOUT_MS = 10 * C.MILLIS_PER_SECOND; - - private final Context context; - - @Nullable - private ImaSdkSettings imaSdkSettings; - @Nullable - private AdErrorListener adErrorListener; - @Nullable - private AdEventListener adEventListener; - @Nullable - private VideoAdPlayer.VideoAdPlayerCallback videoAdPlayerCallback; - @Nullable - private List adMediaMimeTypes; - @Nullable - private Set adUiElements; - @Nullable - private Collection companionAdSlots; - @Nullable - private Boolean enableContinuousPlayback; - private long adPreloadTimeoutMs; - private int vastLoadTimeoutMs; - private int mediaLoadTimeoutMs; - private int mediaBitrate; - private boolean focusSkipButtonWhenAvailable; - private boolean playAdBeforeStartPosition; - private boolean debugModeEnabled; - private ImaUtil.ImaFactory imaFactory; - - /** - * Creates a new builder for {@link ImaAdsLoader}. - * - * @param context The context; - */ - public Builder(Context context) { - this.context = checkNotNull(context).getApplicationContext(); - adPreloadTimeoutMs = DEFAULT_AD_PRELOAD_TIMEOUT_MS; - vastLoadTimeoutMs = TIMEOUT_UNSET; - mediaLoadTimeoutMs = TIMEOUT_UNSET; - mediaBitrate = BITRATE_UNSET; - focusSkipButtonWhenAvailable = true; - playAdBeforeStartPosition = true; - imaFactory = new DefaultImaFactory(); - } - - /** - * Sets the IMA SDK settings. The provided settings instance's player type and version fields - * may be overwritten. - * - *

If this method is not called the default settings will be used. - * - * @param imaSdkSettings The {@link ImaSdkSettings}. - * @return This builder, for convenience. - */ - public Builder setImaSdkSettings(ImaSdkSettings imaSdkSettings) { - this.imaSdkSettings = checkNotNull(imaSdkSettings); - return this; - } - - /** - * Sets a listener for ad errors that will be passed to {@link com.google.ads.interactivemedia.v3.api.AdsLoader#addAdErrorListener(AdErrorListener)} - * and {@link AdsManager#addAdErrorListener(AdErrorListener)}. - * - * @param adErrorListener The ad error listener. - * @return This builder, for convenience. - */ - public Builder setAdErrorListener(AdErrorListener adErrorListener) { - this.adErrorListener = checkNotNull(adErrorListener); - return this; - } - - /** - * Sets a listener for ad events that will be passed to {@link AdsManager#addAdEventListener(AdEventListener)}. - * - * @param adEventListener The ad event listener. - * @return This builder, for convenience. - */ - public Builder setAdEventListener(AdEventListener adEventListener) { - this.adEventListener = checkNotNull(adEventListener); - return this; - } - - /** - * Sets a callback to receive video ad player events. Note that these events are handled - * internally by the IMA SDK and this ads loader. For analytics and diagnostics, new - * implementations should generally use events from the top-level {@link Player} listeners - * instead of setting a callback via this method. - * - * @param videoAdPlayerCallback The callback to receive video ad player events. - * @return This builder, for convenience. - * @see VideoAdPlayer.VideoAdPlayerCallback - */ - public Builder setVideoAdPlayerCallback( - VideoAdPlayer.VideoAdPlayerCallback videoAdPlayerCallback) { - this.videoAdPlayerCallback = checkNotNull(videoAdPlayerCallback); - return this; - } - - /** - * Sets the ad UI elements to be rendered by the IMA SDK. - * - * @param adUiElements The ad UI elements to be rendered by the IMA SDK. - * @return This builder, for convenience. - * @see AdsRenderingSettings#setUiElements(Set) - */ - public Builder setAdUiElements(Set adUiElements) { - this.adUiElements = ImmutableSet.copyOf(checkNotNull(adUiElements)); - return this; - } - - /** - * Sets the slots to use for companion ads, if they are present in the loaded ad. - * - * @param companionAdSlots The slots to use for companion ads. - * @return This builder, for convenience. - * @see AdDisplayContainer#setCompanionSlots(Collection) - */ - public Builder setCompanionAdSlots(Collection companionAdSlots) { - this.companionAdSlots = ImmutableList.copyOf(checkNotNull(companionAdSlots)); - return this; - } - - /** - * Sets the MIME types to prioritize for linear ad media. If not specified, MIME types supported - * by the {@link MediaSourceFactory adMediaSourceFactory} used to construct the {@link - * AdsMediaSource} will be used. - * - * @param adMediaMimeTypes The MIME types to prioritize for linear ad media. May contain {@link - * MimeTypes#APPLICATION_MPD}, {@link MimeTypes#APPLICATION_M3U8}, - * {@link MimeTypes#VIDEO_MP4}, {@link MimeTypes#VIDEO_WEBM}, {@link - * MimeTypes#VIDEO_H263}, {@link MimeTypes#AUDIO_MP4} and {@link - * MimeTypes#AUDIO_MPEG}. - * @return This builder, for convenience. - * @see AdsRenderingSettings#setMimeTypes(List) - */ - public Builder setAdMediaMimeTypes(List adMediaMimeTypes) { - this.adMediaMimeTypes = ImmutableList.copyOf(checkNotNull(adMediaMimeTypes)); - return this; - } - - /** - * Sets whether to enable continuous playback. Pass {@code true} if content videos will be - * played continuously, similar to a TV broadcast. This setting may modify the ads request but - * does not affect ad playback behavior. The requested value is unknown by default. - * - * @param enableContinuousPlayback Whether to enable continuous playback. - * @return This builder, for convenience. - * @see AdsRequest#setContinuousPlayback(boolean) - */ - public Builder setEnableContinuousPlayback(boolean enableContinuousPlayback) { - this.enableContinuousPlayback = enableContinuousPlayback; - return this; - } - - /** - * Sets the duration in milliseconds for which the player must buffer while preloading an ad - * group before that ad group is skipped and marked as having failed to load. Pass {@link - * C#TIME_UNSET} if there should be no such timeout. The default value is {@value - * #DEFAULT_AD_PRELOAD_TIMEOUT_MS} ms. - * - *

The purpose of this timeout is to avoid playback getting stuck in the unexpected case - * that the IMA SDK does not load an ad break based on the player's reported content position. - * - * @param adPreloadTimeoutMs The timeout buffering duration in milliseconds, or {@link - * C#TIME_UNSET} for no timeout. - * @return This builder, for convenience. - */ - public Builder setAdPreloadTimeoutMs(long adPreloadTimeoutMs) { - checkArgument(adPreloadTimeoutMs == C.TIME_UNSET || adPreloadTimeoutMs > 0); - this.adPreloadTimeoutMs = adPreloadTimeoutMs; - return this; - } - - /** - * Sets the VAST load timeout, in milliseconds. - * - * @param vastLoadTimeoutMs The VAST load timeout, in milliseconds. - * @return This builder, for convenience. - * @see AdsRequest#setVastLoadTimeout(float) - */ - public Builder setVastLoadTimeoutMs(int vastLoadTimeoutMs) { - checkArgument(vastLoadTimeoutMs > 0); - this.vastLoadTimeoutMs = vastLoadTimeoutMs; - return this; - } - - /** - * Sets the ad media load timeout, in milliseconds. - * - * @param mediaLoadTimeoutMs The ad media load timeout, in milliseconds. - * @return This builder, for convenience. - * @see AdsRenderingSettings#setLoadVideoTimeout(int) - */ - public Builder setMediaLoadTimeoutMs(int mediaLoadTimeoutMs) { - checkArgument(mediaLoadTimeoutMs > 0); - this.mediaLoadTimeoutMs = mediaLoadTimeoutMs; - return this; - } - - /** - * Sets the media maximum recommended bitrate for ads, in bps. - * - * @param bitrate The media maximum recommended bitrate for ads, in bps. - * @return This builder, for convenience. - * @see AdsRenderingSettings#setBitrateKbps(int) - */ - public Builder setMaxMediaBitrate(int bitrate) { - checkArgument(bitrate > 0); - this.mediaBitrate = bitrate; - return this; - } - - /** - * Sets whether to focus the skip button (when available) on Android TV devices. The default - * setting is {@code true}. - * - * @param focusSkipButtonWhenAvailable Whether to focus the skip button (when available) on - * Android TV devices. - * @return This builder, for convenience. - * @see AdsRenderingSettings#setFocusSkipButtonWhenAvailable(boolean) - */ - public Builder setFocusSkipButtonWhenAvailable(boolean focusSkipButtonWhenAvailable) { - this.focusSkipButtonWhenAvailable = focusSkipButtonWhenAvailable; - return this; - } - - /** - * Sets whether to play an ad before the start position when beginning playback. If {@code - * true}, an ad will be played if there is one at or before the start position. If {@code - * false}, an ad will be played only if there is one exactly at the start position. The default - * setting is {@code true}. - * - * @param playAdBeforeStartPosition Whether to play an ad before the start position when - * beginning playback. - * @return This builder, for convenience. - */ - public Builder setPlayAdBeforeStartPosition(boolean playAdBeforeStartPosition) { - this.playAdBeforeStartPosition = playAdBeforeStartPosition; - return this; - } - - /** - * Sets whether to enable outputting verbose logs for the IMA extension and IMA SDK. The default - * value is {@code false}. This setting is intended for debugging only, and should not be - * enabled in production applications. - * - * @param debugModeEnabled Whether to enable outputting verbose logs for the IMA extension and - * IMA SDK. - * @return This builder, for convenience. - * @see ImaSdkSettings#setDebugMode(boolean) - */ - public Builder setDebugModeEnabled(boolean debugModeEnabled) { - this.debugModeEnabled = debugModeEnabled; - return this; - } - - @VisibleForTesting - /* package */ Builder setImaFactory(ImaUtil.ImaFactory imaFactory) { - this.imaFactory = checkNotNull(imaFactory); - return this; - } - - /** - * Returns a new {@link ImaAdsLoader}. - */ - public ImaAdsLoader build() { - return new ImaAdsLoader( - context, - new ImaUtil.Configuration( - adPreloadTimeoutMs, - vastLoadTimeoutMs, - mediaLoadTimeoutMs, - focusSkipButtonWhenAvailable, - playAdBeforeStartPosition, - mediaBitrate, - enableContinuousPlayback, - adMediaMimeTypes, - adUiElements, - companionAdSlots, - adErrorListener, - adEventListener, - videoAdPlayerCallback, - imaSdkSettings, - debugModeEnabled), - imaFactory); - } - } - - private final ImaUtil.Configuration configuration; - private final Context context; - private final ImaUtil.ImaFactory imaFactory; - private final HashMap adTagLoaderByAdsId; - private final HashMap adTagLoaderByAdsMediaSource; - private final Timeline.Period period; - private final Timeline.Window window; - - private boolean wasSetPlayerCalled; - @Nullable - private Player nextPlayer; - private List supportedMimeTypes; - @Nullable - private Player player; - @Nullable - private AdTagLoader currentAdTagLoader; - - private ImaAdsLoader( - Context context, ImaUtil.Configuration configuration, ImaUtil.ImaFactory imaFactory) { - this.context = context.getApplicationContext(); - this.configuration = configuration; - this.imaFactory = imaFactory; - supportedMimeTypes = ImmutableList.of(); - adTagLoaderByAdsId = new HashMap<>(); - adTagLoaderByAdsMediaSource = new HashMap<>(); - period = new Timeline.Period(); - window = new Timeline.Window(); - } - - /** - * Returns the underlying {@link com.google.ads.interactivemedia.v3.api.AdsLoader} wrapped by this - * instance, or {@code null} if ads have not been requested yet. - */ - @Nullable - public com.google.ads.interactivemedia.v3.api.AdsLoader getAdsLoader() { - return currentAdTagLoader != null ? currentAdTagLoader.getAdsLoader() : null; - } - - /** - * Returns the {@link AdDisplayContainer} used by this loader, or {@code null} if ads have not - * been requested yet. - * - *

Note: any video controls overlays registered via {@link - * AdDisplayContainer#registerFriendlyObstruction(FriendlyObstruction)} will be unregistered - * automatically when the media source detaches from this instance. It is therefore necessary to - * re-register views each time the ads loader is reused. Alternatively, provide overlay views via - * the {@link AdViewProvider} when creating the media source to benefit from automatic - * registration. - */ - @Nullable - public AdDisplayContainer getAdDisplayContainer() { - return currentAdTagLoader != null ? currentAdTagLoader.getAdDisplayContainer() : null; - } - - /** - * Requests ads, if they have not already been requested. Must be called on the main thread. - * - *

Ads will be requested automatically when the player is prepared if this method has not been - * called, so it is only necessary to call this method if you want to request ads before preparing - * the player. - * - * @param adTagDataSpec The data specification of the ad tag to load. See class javadoc for - * information about compatible ad tag formats. - * @param adsId A opaque identifier for the ad playback state across start/stop calls. - * @param adViewGroup A {@link ViewGroup} on top of the player that will show any ad UI, or - * {@code null} if playing audio-only ads. - */ - public void requestAds(DataSpec adTagDataSpec, Object adsId, @Nullable ViewGroup adViewGroup) { - if (!adTagLoaderByAdsId.containsKey(adsId)) { - AdTagLoader adTagLoader = - new AdTagLoader( - context, - configuration, - imaFactory, - supportedMimeTypes, - adTagDataSpec, - adsId, - adViewGroup); - adTagLoaderByAdsId.put(adsId, adTagLoader); - } - } - - /** - * Skips the current ad. - * - *

This method is intended for apps that play audio-only ads and so need to provide their own - * UI for users to skip skippable ads. Apps showing video ads should not call this method, as the - * IMA SDK provides the UI to skip ads in the ad view group passed via {@link AdViewProvider}. - */ - public void skipAd() { - if (currentAdTagLoader != null) { - currentAdTagLoader.skipAd(); - } - } - - /** - * Moves UI focus to the skip button (or other interactive elements), if currently shown. See - * {@link AdsManager#focus()}. - */ - public void focusSkipButton() { - if (currentAdTagLoader != null) { - currentAdTagLoader.focusSkipButton(); - } - } - - // AdsLoader implementation. - - @Override - public void setPlayer(@Nullable Player player) { - checkState(Looper.myLooper() == getImaLooper()); - checkState(player == null || player.getApplicationLooper() == getImaLooper()); - nextPlayer = player; - wasSetPlayerCalled = true; - } - - @Override - public void setSupportedContentTypes(@C.ContentType int... contentTypes) { - List supportedMimeTypes = new ArrayList<>(); - for (@C.ContentType int contentType : contentTypes) { - // IMA does not support Smooth Streaming ad media. - if (contentType == C.TYPE_DASH) { - supportedMimeTypes.add(MimeTypes.APPLICATION_MPD); - } else if (contentType == C.TYPE_HLS) { - supportedMimeTypes.add(MimeTypes.APPLICATION_M3U8); - } else if (contentType == C.TYPE_OTHER) { - supportedMimeTypes.addAll( - Arrays.asList( - MimeTypes.VIDEO_MP4, - MimeTypes.VIDEO_WEBM, - MimeTypes.VIDEO_H263, - MimeTypes.AUDIO_MP4, - MimeTypes.AUDIO_MPEG)); - } - } - this.supportedMimeTypes = Collections.unmodifiableList(supportedMimeTypes); - } - - @Override - public void start( - AdsMediaSource adsMediaSource, - DataSpec adTagDataSpec, - Object adsId, - AdViewProvider adViewProvider, - EventListener eventListener) { - checkState( - wasSetPlayerCalled, "Set player using adsLoader.setPlayer before preparing the player."); - if (adTagLoaderByAdsMediaSource.isEmpty()) { - player = nextPlayer; - @Nullable Player player = this.player; - if (player == null) { - return; - } - player.addListener(this); - } - - @Nullable AdTagLoader adTagLoader = adTagLoaderByAdsId.get(adsId); - if (adTagLoader == null) { - requestAds(adTagDataSpec, adsId, adViewProvider.getAdViewGroup()); - adTagLoader = adTagLoaderByAdsId.get(adsId); - } - adTagLoaderByAdsMediaSource.put(adsMediaSource, checkNotNull(adTagLoader)); - adTagLoader.addListenerWithAdView(eventListener, adViewProvider); - maybeUpdateCurrentAdTagLoader(); - } - - @Override - public void stop(AdsMediaSource adsMediaSource, EventListener eventListener) { - @Nullable AdTagLoader removedAdTagLoader = adTagLoaderByAdsMediaSource.remove(adsMediaSource); - maybeUpdateCurrentAdTagLoader(); - if (removedAdTagLoader != null) { - removedAdTagLoader.removeListener(eventListener); - } - - if (player != null && adTagLoaderByAdsMediaSource.isEmpty()) { - player.removeListener(this); - player = null; - } - } - - @Override - public void release() { - if (player != null) { - player.removeListener(this); - player = null; - maybeUpdateCurrentAdTagLoader(); - } - nextPlayer = null; - - for (AdTagLoader adTagLoader : adTagLoaderByAdsMediaSource.values()) { - adTagLoader.release(); - } - adTagLoaderByAdsMediaSource.clear(); - - for (AdTagLoader adTagLoader : adTagLoaderByAdsId.values()) { - adTagLoader.release(); - } - adTagLoaderByAdsId.clear(); - } - - @Override - public void handlePrepareComplete( - AdsMediaSource adsMediaSource, int adGroupIndex, int adIndexInAdGroup) { - if (player == null) { - return; - } - checkNotNull(adTagLoaderByAdsMediaSource.get(adsMediaSource)) - .handlePrepareComplete(adGroupIndex, adIndexInAdGroup); - } - - @Override - public void handlePrepareError( - AdsMediaSource adsMediaSource, - int adGroupIndex, - int adIndexInAdGroup, - IOException exception) { - if (player == null) { - return; - } - checkNotNull(adTagLoaderByAdsMediaSource.get(adsMediaSource)) - .handlePrepareError(adGroupIndex, adIndexInAdGroup, exception); - } - - // Player.EventListener implementation. - - @Override - public void onTimelineChanged(Timeline timeline, @Player.TimelineChangeReason int reason) { - if (timeline.isEmpty()) { - // The player is being reset or contains no media. - return; - } - maybeUpdateCurrentAdTagLoader(); - maybePreloadNextPeriodAds(); - } - - @Override - public void onPositionDiscontinuity(@Player.DiscontinuityReason int reason) { - maybeUpdateCurrentAdTagLoader(); - maybePreloadNextPeriodAds(); - } - - @Override - public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) { - maybePreloadNextPeriodAds(); - } - - @Override - public void onRepeatModeChanged(@Player.RepeatMode int repeatMode) { - maybePreloadNextPeriodAds(); - } - - // Internal methods. - - private void maybeUpdateCurrentAdTagLoader() { - @Nullable AdTagLoader oldAdTagLoader = currentAdTagLoader; - @Nullable AdTagLoader newAdTagLoader = getCurrentAdTagLoader(); - if (!Util.areEqual(oldAdTagLoader, newAdTagLoader)) { - if (oldAdTagLoader != null) { - oldAdTagLoader.deactivate(); - } - currentAdTagLoader = newAdTagLoader; - if (newAdTagLoader != null) { - newAdTagLoader.activate(checkNotNull(player)); - } - } - } - - @Nullable - private AdTagLoader getCurrentAdTagLoader() { - @Nullable Player player = this.player; - if (player == null) { - return null; - } - Timeline timeline = player.getCurrentTimeline(); - if (timeline.isEmpty()) { - return null; - } - int periodIndex = player.getCurrentPeriodIndex(); - @Nullable Object adsId = timeline.getPeriod(periodIndex, period).getAdsId(); - if (adsId == null) { - return null; - } - @Nullable AdTagLoader adTagLoader = adTagLoaderByAdsId.get(adsId); - if (adTagLoader == null || !adTagLoaderByAdsMediaSource.containsValue(adTagLoader)) { - return null; - } - return adTagLoader; - } - - private void maybePreloadNextPeriodAds() { - @Nullable Player player = this.player; - if (player == null) { - return; - } - Timeline timeline = player.getCurrentTimeline(); - if (timeline.isEmpty()) { - return; - } - int nextPeriodIndex = - timeline.getNextPeriodIndex( - player.getCurrentPeriodIndex(), - period, - window, - player.getRepeatMode(), - player.getShuffleModeEnabled()); - if (nextPeriodIndex == C.INDEX_UNSET) { - return; - } - timeline.getPeriod(nextPeriodIndex, period); - @Nullable Object nextAdsId = period.getAdsId(); - if (nextAdsId == null) { - return; - } - @Nullable AdTagLoader nextAdTagLoader = adTagLoaderByAdsId.get(nextAdsId); - if (nextAdTagLoader == null || nextAdTagLoader == currentAdTagLoader) { - return; - } - long periodPositionUs = - timeline.getPeriodPosition( - window, period, period.windowIndex, /* windowPositionUs= */ C.TIME_UNSET) - .second; - nextAdTagLoader.maybePreloadAds(C.usToMs(periodPositionUs), C.usToMs(period.durationUs)); - } - - /** - * Default {@link ImaUtil.ImaFactory} for non-test usage, which delegates to {@link - * ImaSdkFactory}. - */ - private static final class DefaultImaFactory implements ImaUtil.ImaFactory { - - @Override - public ImaSdkSettings createImaSdkSettings() { - ImaSdkSettings settings = ImaSdkFactory.getInstance().createImaSdkSettings(); - settings.setLanguage(Util.getSystemLanguageCodes()[0]); - return settings; - } - - @Override - public AdsRenderingSettings createAdsRenderingSettings() { - return ImaSdkFactory.getInstance().createAdsRenderingSettings(); - } - - @Override - public AdDisplayContainer createAdDisplayContainer(ViewGroup container, VideoAdPlayer player) { - return ImaSdkFactory.createAdDisplayContainer(container, player); - } - - @Override - public AdDisplayContainer createAudioAdDisplayContainer(Context context, VideoAdPlayer player) { - return ImaSdkFactory.createAudioAdDisplayContainer(context, player); - } - - // The reasonDetail parameter to createFriendlyObstruction is annotated @Nullable but the - // annotation is not kept in the obfuscated dependency. - @SuppressWarnings("nullness:argument.type.incompatible") - @Override - public FriendlyObstruction createFriendlyObstruction( - View view, - FriendlyObstructionPurpose friendlyObstructionPurpose, - @Nullable String reasonDetail) { - return ImaSdkFactory.getInstance() - .createFriendlyObstruction(view, friendlyObstructionPurpose, reasonDetail); - } - - @Override - public AdsRequest createAdsRequest() { - return ImaSdkFactory.getInstance().createAdsRequest(); - } - - @Override - public com.google.ads.interactivemedia.v3.api.AdsLoader createAdsLoader( - Context context, ImaSdkSettings imaSdkSettings, AdDisplayContainer adDisplayContainer) { - return ImaSdkFactory.getInstance() - .createAdsLoader(context, imaSdkSettings, adDisplayContainer); - } - } -} diff --git a/automatedtests/src/androidTestR2_13_1/java/com/google/android/exoplayer2/ext/ima/ImaUtil.java b/automatedtests/src/androidTestR2_13_1/java/com/google/android/exoplayer2/ext/ima/ImaUtil.java deleted file mode 100644 index 443d98d2..00000000 --- a/automatedtests/src/androidTestR2_13_1/java/com/google/android/exoplayer2/ext/ima/ImaUtil.java +++ /dev/null @@ -1,268 +0,0 @@ -/* - * Copyright (C) 2020 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.google.android.exoplayer2.ext.ima; - -import android.content.Context; -import android.os.Looper; -import android.view.View; -import android.view.ViewGroup; -import androidx.annotation.Nullable; -import com.google.ads.interactivemedia.v3.api.AdDisplayContainer; -import com.google.ads.interactivemedia.v3.api.AdError; -import com.google.ads.interactivemedia.v3.api.AdErrorEvent; -import com.google.ads.interactivemedia.v3.api.AdEvent; -import com.google.ads.interactivemedia.v3.api.AdsLoader; -import com.google.ads.interactivemedia.v3.api.AdsManager; -import com.google.ads.interactivemedia.v3.api.AdsRenderingSettings; -import com.google.ads.interactivemedia.v3.api.AdsRequest; -import com.google.ads.interactivemedia.v3.api.CompanionAdSlot; -import com.google.ads.interactivemedia.v3.api.FriendlyObstruction; -import com.google.ads.interactivemedia.v3.api.FriendlyObstructionPurpose; -import com.google.ads.interactivemedia.v3.api.ImaSdkSettings; -import com.google.ads.interactivemedia.v3.api.UiElement; -import com.google.ads.interactivemedia.v3.api.player.VideoAdPlayer; -import com.google.ads.interactivemedia.v3.api.player.VideoProgressUpdate; -import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.source.ads.AdsLoader.OverlayInfo; -import com.google.android.exoplayer2.upstream.DataSchemeDataSource; -import com.google.android.exoplayer2.upstream.DataSpec; -import com.google.android.exoplayer2.util.Util; -import java.io.IOException; -import java.util.Arrays; -import java.util.Collection; -import java.util.List; -import java.util.Set; - -/** - * Utilities for working with IMA SDK and IMA extension data types. - */ -/* package */ final class ImaUtil { - - /** - * Factory for objects provided by the IMA SDK. - */ - public interface ImaFactory { - - /** - * Creates {@link ImaSdkSettings} for configuring the IMA SDK. - */ - ImaSdkSettings createImaSdkSettings(); - - /** - * Creates {@link AdsRenderingSettings} for giving the {@link AdsManager} parameters that - * control rendering of ads. - */ - AdsRenderingSettings createAdsRenderingSettings(); - - /** - * Creates an {@link AdDisplayContainer} to hold the player for video ads, a container for - * non-linear ads, and slots for companion ads. - */ - AdDisplayContainer createAdDisplayContainer(ViewGroup container, VideoAdPlayer player); - - /** - * Creates an {@link AdDisplayContainer} to hold the player for audio ads. - */ - AdDisplayContainer createAudioAdDisplayContainer(Context context, VideoAdPlayer player); - - /** - * Creates a {@link FriendlyObstruction} to describe an obstruction considered "friendly" for - * viewability measurement purposes. - */ - FriendlyObstruction createFriendlyObstruction( - View view, - FriendlyObstructionPurpose friendlyObstructionPurpose, - @Nullable String reasonDetail); - - /** - * Creates an {@link AdsRequest} to contain the data used to request ads. - */ - AdsRequest createAdsRequest(); - - /** - * Creates an {@link AdsLoader} for requesting ads using the specified settings. - */ - AdsLoader createAdsLoader( - Context context, ImaSdkSettings imaSdkSettings, AdDisplayContainer adDisplayContainer); - } - - /** - * Stores configuration for ad loading and playback. - */ - public static final class Configuration { - - public final long adPreloadTimeoutMs; - public final int vastLoadTimeoutMs; - public final int mediaLoadTimeoutMs; - public final boolean focusSkipButtonWhenAvailable; - public final boolean playAdBeforeStartPosition; - public final int mediaBitrate; - @Nullable - public final Boolean enableContinuousPlayback; - @Nullable - public final List adMediaMimeTypes; - @Nullable - public final Set adUiElements; - @Nullable - public final Collection companionAdSlots; - @Nullable - public final AdErrorEvent.AdErrorListener applicationAdErrorListener; - @Nullable - public final AdEvent.AdEventListener applicationAdEventListener; - @Nullable - public final VideoAdPlayer.VideoAdPlayerCallback applicationVideoAdPlayerCallback; - @Nullable - public final ImaSdkSettings imaSdkSettings; - public final boolean debugModeEnabled; - - public Configuration( - long adPreloadTimeoutMs, - int vastLoadTimeoutMs, - int mediaLoadTimeoutMs, - boolean focusSkipButtonWhenAvailable, - boolean playAdBeforeStartPosition, - int mediaBitrate, - @Nullable Boolean enableContinuousPlayback, - @Nullable List adMediaMimeTypes, - @Nullable Set adUiElements, - @Nullable Collection companionAdSlots, - @Nullable AdErrorEvent.AdErrorListener applicationAdErrorListener, - @Nullable AdEvent.AdEventListener applicationAdEventListener, - @Nullable VideoAdPlayer.VideoAdPlayerCallback applicationVideoAdPlayerCallback, - @Nullable ImaSdkSettings imaSdkSettings, - boolean debugModeEnabled) { - this.adPreloadTimeoutMs = adPreloadTimeoutMs; - this.vastLoadTimeoutMs = vastLoadTimeoutMs; - this.mediaLoadTimeoutMs = mediaLoadTimeoutMs; - this.focusSkipButtonWhenAvailable = focusSkipButtonWhenAvailable; - this.playAdBeforeStartPosition = playAdBeforeStartPosition; - this.mediaBitrate = mediaBitrate; - this.enableContinuousPlayback = enableContinuousPlayback; - this.adMediaMimeTypes = adMediaMimeTypes; - this.adUiElements = adUiElements; - this.companionAdSlots = companionAdSlots; - this.applicationAdErrorListener = applicationAdErrorListener; - this.applicationAdEventListener = applicationAdEventListener; - this.applicationVideoAdPlayerCallback = applicationVideoAdPlayerCallback; - this.imaSdkSettings = imaSdkSettings; - this.debugModeEnabled = debugModeEnabled; - } - } - - public static final int TIMEOUT_UNSET = -1; - public static final int BITRATE_UNSET = -1; - - /** - * Returns the IMA {@link FriendlyObstructionPurpose} corresponding to the given {@link - * OverlayInfo#purpose}. - */ - public static FriendlyObstructionPurpose getFriendlyObstructionPurpose( - @OverlayInfo.Purpose int purpose) { - switch (purpose) { - case OverlayInfo.PURPOSE_CONTROLS: - return FriendlyObstructionPurpose.VIDEO_CONTROLS; - case OverlayInfo.PURPOSE_CLOSE_AD: - return FriendlyObstructionPurpose.CLOSE_AD; - case OverlayInfo.PURPOSE_NOT_VISIBLE: - return FriendlyObstructionPurpose.NOT_VISIBLE; - case OverlayInfo.PURPOSE_OTHER: - default: - return FriendlyObstructionPurpose.OTHER; - } - } - - /** - * Returns the microsecond ad group timestamps corresponding to the specified cue points. - * - * @param cuePoints The cue points of the ads in seconds, provided by the IMA SDK. - * @return The corresponding microsecond ad group timestamps. - */ - public static long[] getAdGroupTimesUsForCuePoints(List cuePoints) { - if (cuePoints.isEmpty()) { - return new long[]{0L}; - } - - int count = cuePoints.size(); - long[] adGroupTimesUs = new long[count]; - int adGroupIndex = 0; - for (int i = 0; i < count; i++) { - double cuePoint = cuePoints.get(i); - if (cuePoint == -1.0) { - adGroupTimesUs[count - 1] = C.TIME_END_OF_SOURCE; - } else { - adGroupTimesUs[adGroupIndex++] = Math.round(C.MICROS_PER_SECOND * cuePoint); - } - } - // Cue points may be out of order, so sort them. - Arrays.sort(adGroupTimesUs, 0, adGroupIndex); - return adGroupTimesUs; - } - - /** - * Returns an {@link AdsRequest} based on the specified ad tag {@link DataSpec}. - */ - public static AdsRequest getAdsRequestForAdTagDataSpec( - ImaFactory imaFactory, DataSpec adTagDataSpec) throws IOException { - AdsRequest request = imaFactory.createAdsRequest(); - if (DataSchemeDataSource.SCHEME_DATA.equals(adTagDataSpec.uri.getScheme())) { - DataSchemeDataSource dataSchemeDataSource = new DataSchemeDataSource(); - try { - dataSchemeDataSource.open(adTagDataSpec); - request.setAdsResponse(Util.fromUtf8Bytes(Util.readToEnd(dataSchemeDataSource))); - } finally { - dataSchemeDataSource.close(); - } - } else { - request.setAdTagUrl(adTagDataSpec.uri.toString()); - } - return request; - } - - /** - * Returns whether the ad error indicates that an entire ad group failed to load. - */ - public static boolean isAdGroupLoadError(AdError adError) { - // TODO: Find out what other errors need to be handled (if any), and whether each one relates to - // a single ad, ad group or the whole timeline. - return adError.getErrorCode() == AdError.AdErrorCode.VAST_LINEAR_ASSET_MISMATCH - || adError.getErrorCode() == AdError.AdErrorCode.UNKNOWN_ERROR; - } - - /** - * Returns the looper on which all IMA SDK interaction must occur. - */ - public static Looper getImaLooper() { - // IMA SDK callbacks occur on the main thread. This method can be used to check that the player - // is using the same looper, to ensure all interaction with this class is on the main thread. - return Looper.getMainLooper(); - } - - /** - * Returns a human-readable representation of a video progress update. - */ - public static String getStringForVideoProgressUpdate(VideoProgressUpdate videoProgressUpdate) { - if (VideoProgressUpdate.VIDEO_TIME_NOT_READY.equals(videoProgressUpdate)) { - return "not ready"; - } else { - return Util.formatInvariant( - "%d ms of %d ms", - videoProgressUpdate.getCurrentTimeMs(), videoProgressUpdate.getDurationMs()); - } - } - - private ImaUtil() { - } -} diff --git a/automatedtests/src/androidTestR2_18_1/java/com/google/android/exoplayer2/ext/ima/AdTagLoader.java b/automatedtests/src/androidTestR2_18_1/java/com/google/android/exoplayer2/ext/ima/AdTagLoader.java deleted file mode 100644 index 0aa0ad26..00000000 --- a/automatedtests/src/androidTestR2_18_1/java/com/google/android/exoplayer2/ext/ima/AdTagLoader.java +++ /dev/null @@ -1,1582 +0,0 @@ -/* - * Copyright 2020 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.google.android.exoplayer2.ext.ima; - -import static com.google.android.exoplayer2.Player.COMMAND_GET_VOLUME; -import static com.google.android.exoplayer2.ext.ima.ImaUtil.BITRATE_UNSET; -import static com.google.android.exoplayer2.ext.ima.ImaUtil.TIMEOUT_UNSET; -import static com.google.android.exoplayer2.ext.ima.ImaUtil.getAdGroupTimesUsForCuePoints; -import static com.google.android.exoplayer2.ext.ima.ImaUtil.getImaLooper; -import static com.google.android.exoplayer2.util.Assertions.checkNotNull; -import static com.google.android.exoplayer2.util.Assertions.checkState; -import static java.lang.Math.max; - -import android.content.Context; -import android.net.Uri; -import android.os.Handler; -import android.os.SystemClock; -import android.view.ViewGroup; - -import androidx.annotation.IntDef; -import androidx.annotation.Nullable; - -import com.google.ads.interactivemedia.v3.api.AdDisplayContainer; -import com.google.ads.interactivemedia.v3.api.AdError; -import com.google.ads.interactivemedia.v3.api.AdErrorEvent; -import com.google.ads.interactivemedia.v3.api.AdErrorEvent.AdErrorListener; -import com.google.ads.interactivemedia.v3.api.AdEvent; -import com.google.ads.interactivemedia.v3.api.AdEvent.AdEventListener; -import com.google.ads.interactivemedia.v3.api.AdEvent.AdEventType; -import com.google.ads.interactivemedia.v3.api.AdPodInfo; -import com.google.ads.interactivemedia.v3.api.AdsLoader; -import com.google.ads.interactivemedia.v3.api.AdsLoader.AdsLoadedListener; -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.ads.interactivemedia.v3.api.player.ContentProgressProvider; -import com.google.ads.interactivemedia.v3.api.player.VideoAdPlayer; -import com.google.ads.interactivemedia.v3.api.player.VideoProgressUpdate; -import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.ExoPlayerLibraryInfo; -import com.google.android.exoplayer2.PlaybackException; -import com.google.android.exoplayer2.Player; -import com.google.android.exoplayer2.Timeline; -import com.google.android.exoplayer2.Tracks; -import com.google.android.exoplayer2.source.ads.AdPlaybackState; -import com.google.android.exoplayer2.source.ads.AdsLoader.EventListener; -import com.google.android.exoplayer2.source.ads.AdsMediaSource.AdLoadException; -import com.google.android.exoplayer2.trackselection.TrackSelection; -import com.google.android.exoplayer2.trackselection.TrackSelectionArray; -import com.google.android.exoplayer2.ui.AdOverlayInfo; -import com.google.android.exoplayer2.ui.AdViewProvider; -import com.google.android.exoplayer2.upstream.DataSpec; -import com.google.android.exoplayer2.util.Log; -import com.google.android.exoplayer2.util.MimeTypes; -import com.google.android.exoplayer2.util.Util; -import com.google.common.collect.BiMap; -import com.google.common.collect.HashBiMap; -import com.google.common.collect.ImmutableList; - -import java.io.IOException; -import java.lang.annotation.Documented; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -/** - * Handles loading and playback of a single ad tag. - */ -/* package */ final class AdTagLoader implements Player.Listener { - - private static final String TAG = "AdTagLoader"; - - private static final String IMA_SDK_SETTINGS_PLAYER_TYPE = "google/exo.ext.ima"; - private static final String IMA_SDK_SETTINGS_PLAYER_VERSION = ExoPlayerLibraryInfo.VERSION; - - /** - * Interval at which ad progress updates are provided to the IMA SDK, in milliseconds. 100 ms is - * the interval recommended by the IMA documentation. - * - * @see VideoAdPlayer.VideoAdPlayerCallback - */ - private static final int AD_PROGRESS_UPDATE_INTERVAL_MS = 100; - - /** - * The value used in {@link VideoProgressUpdate}s to indicate an unset duration. - */ - private static final long IMA_DURATION_UNSET = -1L; - - /** - * Threshold before the end of content at which IMA is notified that content is complete if the - * player buffers, in milliseconds. - */ - private static final long THRESHOLD_END_OF_CONTENT_MS = 5000; - /** - * Threshold before the start of an ad at which IMA is expected to be able to preload the ad, in - * milliseconds. - */ - private static final long THRESHOLD_AD_PRELOAD_MS = 4000; - /** - * The threshold below which ad cue points are treated as matching, in microseconds. - */ - private static final long THRESHOLD_AD_MATCH_US = 1000; - - /** - * The state of ad playback. - */ - @Documented - @Retention(RetentionPolicy.SOURCE) - @IntDef({IMA_AD_STATE_NONE, IMA_AD_STATE_PLAYING, IMA_AD_STATE_PAUSED}) - private @interface ImaAdState { - } - - /** - * The ad playback state when IMA is not playing an ad. - */ - private static final int IMA_AD_STATE_NONE = 0; - /** - * The ad playback state when IMA has called {@link ComponentListener#playAd(AdMediaInfo)} and not - * {@link ComponentListener##pauseAd(AdMediaInfo)}. - */ - private static final int IMA_AD_STATE_PLAYING = 1; - /** - * The ad playback state when IMA has called {@link ComponentListener#pauseAd(AdMediaInfo)} while - * playing an ad. - */ - private static final int IMA_AD_STATE_PAUSED = 2; - - private final ImaUtil.Configuration configuration; - private final ImaUtil.ImaFactory imaFactory; - private final List supportedMimeTypes; - private final DataSpec adTagDataSpec; - private final Object adsId; - private final Timeline.Period period; - private final Handler handler; - private final ComponentListener componentListener; - private final List eventListeners; - private final List adCallbacks; - private final Runnable updateAdProgressRunnable; - private final BiMap adInfoByAdMediaInfo; - private final AdDisplayContainer adDisplayContainer; - private final AdsLoader adsLoader; - - @Nullable - private Object pendingAdRequestContext; - @Nullable - private Player player; - private VideoProgressUpdate lastContentProgress; - private VideoProgressUpdate lastAdProgress; - private int lastVolumePercent; - - @Nullable - private AdsManager adsManager; - private boolean isAdsManagerInitialized; - @Nullable - private AdLoadException pendingAdLoadError; - private Timeline timeline; - private long contentDurationMs; - private AdPlaybackState adPlaybackState; - - private boolean released; - - // Fields tracking IMA's state. - - /** - * Whether IMA has sent an ad event to pause content since the last resume content event. - */ - private boolean imaPausedContent; - /** - * The current ad playback state. - */ - private @ImaAdState - int imaAdState; - /** - * The current ad media info, or {@code null} if in state {@link #IMA_AD_STATE_NONE}. - */ - @Nullable - private AdMediaInfo imaAdMediaInfo; - /** - * The current ad info, or {@code null} if in state {@link #IMA_AD_STATE_NONE}. - */ - @Nullable - private AdInfo imaAdInfo; - /** - * Whether IMA has been notified that playback of content has finished. - */ - private boolean sentContentComplete; - - // Fields tracking the player/loader state. - - /** - * Whether the player is playing an ad. - */ - private boolean playingAd; - /** - * Whether the player is buffering an ad. - */ - private boolean bufferingAd; - /** - * If the player is playing an ad, stores the ad index in its ad group. {@link C#INDEX_UNSET} - * otherwise. - */ - private int playingAdIndexInAdGroup; - /** - * The ad info for a pending ad for which the media failed preparation, or {@code null} if no - * pending ads have failed to prepare. - */ - @Nullable - private AdInfo pendingAdPrepareErrorAdInfo; - /** - * If a content period has finished but IMA has not yet called {@link - * ComponentListener#playAd(AdMediaInfo)}, stores the value of {@link - * SystemClock#elapsedRealtime()} when the content stopped playing. This can be used to determine - * a fake, increasing content position. {@link C#TIME_UNSET} otherwise. - */ - private long fakeContentProgressElapsedRealtimeMs; - /** - * If {@link #fakeContentProgressElapsedRealtimeMs} is set, stores the offset from which the - * content progress should increase. {@link C#TIME_UNSET} otherwise. - */ - private long fakeContentProgressOffsetMs; - /** - * Stores the pending content position when a seek operation was intercepted to play an ad. - */ - private long pendingContentPositionMs; - /** - * Whether {@link ComponentListener#getContentProgress()} has sent {@link - * #pendingContentPositionMs} to IMA. - */ - private boolean sentPendingContentPositionMs; - /** - * Stores the real time in milliseconds at which the player started buffering, possibly due to not - * having preloaded an ad, or {@link C#TIME_UNSET} if not applicable. - */ - private long waitingForPreloadElapsedRealtimeMs; - - /** - * Creates a new ad tag loader, starting the ad request if the ad tag is valid. - */ - @SuppressWarnings({"methodref.receiver.bound.invalid", "method.invocation.invalid"}) - public AdTagLoader( - Context context, - ImaUtil.Configuration configuration, - ImaUtil.ImaFactory imaFactory, - List supportedMimeTypes, - DataSpec adTagDataSpec, - Object adsId, - @Nullable ViewGroup adViewGroup) { - this.configuration = configuration; - this.imaFactory = imaFactory; - @Nullable ImaSdkSettings imaSdkSettings = configuration.imaSdkSettings; - if (imaSdkSettings == null) { - imaSdkSettings = imaFactory.createImaSdkSettings(); - if (configuration.debugModeEnabled) { - imaSdkSettings.setDebugMode(true); - } - } - imaSdkSettings.setPlayerType(IMA_SDK_SETTINGS_PLAYER_TYPE); - imaSdkSettings.setPlayerVersion(IMA_SDK_SETTINGS_PLAYER_VERSION); - this.supportedMimeTypes = supportedMimeTypes; - this.adTagDataSpec = adTagDataSpec; - this.adsId = adsId; - period = new Timeline.Period(); - handler = Util.createHandler(getImaLooper(), /* callback= */ null); - componentListener = new ComponentListener(); - eventListeners = new ArrayList<>(); - adCallbacks = new ArrayList<>(/* initialCapacity= */ 1); - if (configuration.applicationVideoAdPlayerCallback != null) { - adCallbacks.add(configuration.applicationVideoAdPlayerCallback); - } - updateAdProgressRunnable = this::updateAdProgress; - adInfoByAdMediaInfo = HashBiMap.create(); - lastContentProgress = VideoProgressUpdate.VIDEO_TIME_NOT_READY; - lastAdProgress = VideoProgressUpdate.VIDEO_TIME_NOT_READY; - fakeContentProgressElapsedRealtimeMs = C.TIME_UNSET; - fakeContentProgressOffsetMs = C.TIME_UNSET; - pendingContentPositionMs = C.TIME_UNSET; - waitingForPreloadElapsedRealtimeMs = C.TIME_UNSET; - contentDurationMs = C.TIME_UNSET; - timeline = Timeline.EMPTY; - adPlaybackState = AdPlaybackState.NONE; - if (adViewGroup != null) { - adDisplayContainer = - imaFactory.createAdDisplayContainer(adViewGroup, /* player= */ componentListener); - } else { - adDisplayContainer = - imaFactory.createAudioAdDisplayContainer(context, /* player= */ componentListener); - } - if (configuration.companionAdSlots != null) { - adDisplayContainer.setCompanionSlots(configuration.companionAdSlots); - } - adsLoader = requestAds(context, imaSdkSettings, adDisplayContainer); - } - - /** - * Returns the underlying IMA SDK ads loader. - */ - public AdsLoader getAdsLoader() { - return adsLoader; - } - - /** - * Returns the IMA SDK ad display container. - */ - public AdDisplayContainer getAdDisplayContainer() { - return adDisplayContainer; - } - - /** - * Skips the current skippable ad, if there is one. - */ - public void skipAd() { - if (adsManager != null) { - adsManager.skip(); - } - } - - /** - * Moves UI focus to the skip button (or other interactive elements), if currently shown. See - * {@link AdsManager#focus()}. - */ - public void focusSkipButton() { - if (adsManager != null) { - adsManager.focus(); - } - } - - /** - * Starts passing events from this instance (including any pending ad playback state) and - * registers obstructions. - */ - public void addListenerWithAdView(EventListener eventListener, AdViewProvider adViewProvider) { - boolean isStarted = !eventListeners.isEmpty(); - eventListeners.add(eventListener); - if (isStarted) { - if (!AdPlaybackState.NONE.equals(adPlaybackState)) { - // Pass the existing ad playback state to the new listener. - eventListener.onAdPlaybackState(adPlaybackState); - } - return; - } - lastVolumePercent = 0; - lastAdProgress = VideoProgressUpdate.VIDEO_TIME_NOT_READY; - lastContentProgress = VideoProgressUpdate.VIDEO_TIME_NOT_READY; - maybeNotifyPendingAdLoadError(); - if (!AdPlaybackState.NONE.equals(adPlaybackState)) { - // Pass the ad playback state to the player, and resume ads if necessary. - eventListener.onAdPlaybackState(adPlaybackState); - } else if (adsManager != null) { - adPlaybackState = - new AdPlaybackState(adsId, getAdGroupTimesUsForCuePoints(adsManager.getAdCuePoints())); - updateAdPlaybackState(); - } - for (AdOverlayInfo overlayInfo : adViewProvider.getAdOverlayInfos()) { - adDisplayContainer.registerFriendlyObstruction( - imaFactory.createFriendlyObstruction( - overlayInfo.view, - ImaUtil.getFriendlyObstructionPurpose(overlayInfo.purpose), - overlayInfo.reasonDetail)); - } - } - - /** - * Populates the ad playback state with loaded cue points, if available. Any preroll will be - * paused immediately while waiting for this instance to be {@link #activate(Player) activated}. - */ - public void maybePreloadAds(long contentPositionMs, long contentDurationMs) { - maybeInitializeAdsManager(contentPositionMs, contentDurationMs); - } - - /** - * Activates playback. - */ - public void activate(Player player) { - this.player = player; - player.addListener(this); - - boolean playWhenReady = player.getPlayWhenReady(); - onTimelineChanged(player.getCurrentTimeline(), Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE); - @Nullable AdsManager adsManager = this.adsManager; - if (!AdPlaybackState.NONE.equals(adPlaybackState) && adsManager != null && imaPausedContent) { - // Check whether the current ad break matches the expected ad break based on the current - // position. If not, discard the current ad break so that the correct ad break can load. - long contentPositionMs = getContentPeriodPositionMs(player, timeline, period); - int adGroupForPositionIndex = - adPlaybackState.getAdGroupIndexForPositionUs( - C.msToUs(contentPositionMs), C.msToUs(contentDurationMs)); - if (adGroupForPositionIndex != C.INDEX_UNSET - && imaAdInfo != null - && imaAdInfo.adGroupIndex != adGroupForPositionIndex) { - if (configuration.debugModeEnabled) { - Log.d(TAG, "Discarding preloaded ad " + imaAdInfo); - } - adsManager.discardAdBreak(); - } - if (playWhenReady) { - adsManager.resume(); - } - } - } - - /** - * Deactivates playback. - */ - public void deactivate() { - Player player = checkNotNull(this.player); - if (!AdPlaybackState.NONE.equals(adPlaybackState) && imaPausedContent) { - if (adsManager != null) { - adsManager.pause(); - } - adPlaybackState = - adPlaybackState.withAdResumePositionUs( - playingAd ? C.msToUs(player.getCurrentPosition()) : 0); - } - lastVolumePercent = getPlayerVolumePercent(); - lastAdProgress = getAdVideoProgressUpdate(); - lastContentProgress = getContentVideoProgressUpdate(); - - player.removeListener(this); - this.player = null; - } - - /** - * Stops passing of events from this instance and unregisters obstructions. - */ - public void removeListener(EventListener eventListener) { - eventListeners.remove(eventListener); - if (eventListeners.isEmpty()) { - adDisplayContainer.unregisterAllFriendlyObstructions(); - } - } - - /** - * Releases all resources used by the ad tag loader. - */ - public void release() { - if (released) { - return; - } - released = true; - pendingAdRequestContext = null; - destroyAdsManager(); - adsLoader.removeAdsLoadedListener(componentListener); - adsLoader.removeAdErrorListener(componentListener); - if (configuration.applicationAdErrorListener != null) { - adsLoader.removeAdErrorListener(configuration.applicationAdErrorListener); - } - adsLoader.release(); - imaPausedContent = false; - imaAdState = IMA_AD_STATE_NONE; - imaAdMediaInfo = null; - stopUpdatingAdProgress(); - imaAdInfo = null; - pendingAdLoadError = null; - // No more ads will play once the loader is released, so mark all ad groups as skipped. - for (int i = 0; i < adPlaybackState.adGroupCount; i++) { - adPlaybackState = adPlaybackState.withSkippedAdGroup(i); - } - updateAdPlaybackState(); - } - - /** - * Notifies the IMA SDK that the specified ad has been prepared for playback. - */ - public void handlePrepareComplete(int adGroupIndex, int adIndexInAdGroup) { - AdInfo adInfo = new AdInfo(adGroupIndex, adIndexInAdGroup); - if (configuration.debugModeEnabled) { - Log.d(TAG, "Prepared ad " + adInfo); - } - @Nullable AdMediaInfo adMediaInfo = adInfoByAdMediaInfo.inverse().get(adInfo); - if (adMediaInfo != null) { - for (int i = 0; i < adCallbacks.size(); i++) { - adCallbacks.get(i).onLoaded(adMediaInfo); - } - } else { - Log.w(TAG, "Unexpected prepared ad " + adInfo); - } - } - - /** - * Notifies the IMA SDK that the specified ad has failed to prepare for playback. - */ - public void handlePrepareError(int adGroupIndex, int adIndexInAdGroup, IOException exception) { - if (player == null) { - return; - } - try { - handleAdPrepareError(adGroupIndex, adIndexInAdGroup, exception); - } catch (RuntimeException e) { - maybeNotifyInternalError("handlePrepareError", e); - } - } - - // Player.EventListener implementation. - - @Override - public void onTimelineChanged(Timeline timeline, @Player.TimelineChangeReason int reason) { - if (timeline.isEmpty()) { - // The player is being reset or contains no media. - return; - } - this.timeline = timeline; - Player player = checkNotNull(this.player); - long contentDurationUs = timeline.getPeriod(player.getCurrentPeriodIndex(), period).durationUs; - contentDurationMs = C.usToMs(contentDurationUs); - if (contentDurationUs != adPlaybackState.contentDurationUs) { - adPlaybackState = adPlaybackState.withContentDurationUs(contentDurationUs); - updateAdPlaybackState(); - } - long contentPositionMs = getContentPeriodPositionMs(player, timeline, period); - maybeInitializeAdsManager(contentPositionMs, contentDurationMs); - handleTimelineOrPositionChanged(); - } - - @Override - public void onPositionDiscontinuity( - Player.PositionInfo oldPosition, - Player.PositionInfo newPosition, - @Player.DiscontinuityReason int reason) { - handleTimelineOrPositionChanged(); - } - - @Override - public void onPlaybackStateChanged(@Player.State int playbackState) { - @Nullable Player player = this.player; - if (adsManager == null || player == null) { - return; - } - - if (playbackState == Player.STATE_BUFFERING - && !player.isPlayingAd() - && isWaitingForAdToLoad()) { - waitingForPreloadElapsedRealtimeMs = SystemClock.elapsedRealtime(); - } else if (playbackState == Player.STATE_READY) { - waitingForPreloadElapsedRealtimeMs = C.TIME_UNSET; - } - - handlePlayerStateChanged(player.getPlayWhenReady(), playbackState); - } - - @Override - public void onPlayWhenReadyChanged( - boolean playWhenReady, @Player.PlayWhenReadyChangeReason int reason) { - if (adsManager == null || player == null) { - return; - } - - if (imaAdState == IMA_AD_STATE_PLAYING && !playWhenReady) { - adsManager.pause(); - return; - } - - if (imaAdState == IMA_AD_STATE_PAUSED && playWhenReady) { - adsManager.resume(); - return; - } - handlePlayerStateChanged(playWhenReady, player.getPlaybackState()); - } - - @Override - public void onPlayerError(PlaybackException error) { - if (imaAdState != IMA_AD_STATE_NONE) { - AdMediaInfo adMediaInfo = checkNotNull(imaAdMediaInfo); - for (int i = 0; i < adCallbacks.size(); i++) { - adCallbacks.get(i).onError(adMediaInfo); - } - } - } - - // Internal methods. - - private AdsLoader requestAds( - Context context, ImaSdkSettings imaSdkSettings, AdDisplayContainer adDisplayContainer) { - AdsLoader adsLoader = imaFactory.createAdsLoader(context, imaSdkSettings, adDisplayContainer); - adsLoader.addAdErrorListener(componentListener); - if (configuration.applicationAdErrorListener != null) { - adsLoader.addAdErrorListener(configuration.applicationAdErrorListener); - } - adsLoader.addAdsLoadedListener(componentListener); - AdsRequest request; - try { - request = ImaUtil.getAdsRequestForAdTagDataSpec(imaFactory, adTagDataSpec); - } catch (IOException e) { - adPlaybackState = new AdPlaybackState(adsId); - updateAdPlaybackState(); - pendingAdLoadError = AdLoadException.createForAllAds(e); - maybeNotifyPendingAdLoadError(); - return adsLoader; - } - pendingAdRequestContext = new Object(); - request.setUserRequestContext(pendingAdRequestContext); - if (configuration.enableContinuousPlayback != null) { - request.setContinuousPlayback(configuration.enableContinuousPlayback); - } - if (configuration.vastLoadTimeoutMs != TIMEOUT_UNSET) { - request.setVastLoadTimeout(configuration.vastLoadTimeoutMs); - } - request.setContentProgressProvider(componentListener); - adsLoader.requestAds(request); - return adsLoader; - } - - private void maybeInitializeAdsManager(long contentPositionMs, long contentDurationMs) { - @Nullable AdsManager adsManager = this.adsManager; - if (!isAdsManagerInitialized && adsManager != null) { - isAdsManagerInitialized = true; - @Nullable - AdsRenderingSettings adsRenderingSettings = - setupAdsRendering(contentPositionMs, contentDurationMs); - if (adsRenderingSettings == null) { - // There are no ads to play. - destroyAdsManager(); - } else { - adsManager.init(adsRenderingSettings); - adsManager.start(); - if (configuration.debugModeEnabled) { - Log.d(TAG, "Initialized with ads rendering settings: " + adsRenderingSettings); - } - } - updateAdPlaybackState(); - } - } - - /** - * Configures ads rendering for starting playback, returning the settings for the IMA SDK or - * {@code null} if no ads should play. - */ - @Nullable - private AdsRenderingSettings setupAdsRendering(long contentPositionMs, long contentDurationMs) { - AdsRenderingSettings adsRenderingSettings = imaFactory.createAdsRenderingSettings(); - adsRenderingSettings.setEnablePreloading(true); - adsRenderingSettings.setMimeTypes( - configuration.adMediaMimeTypes != null - ? configuration.adMediaMimeTypes - : supportedMimeTypes); - if (configuration.mediaLoadTimeoutMs != TIMEOUT_UNSET) { - adsRenderingSettings.setLoadVideoTimeout(configuration.mediaLoadTimeoutMs); - } - if (configuration.mediaBitrate != BITRATE_UNSET) { - adsRenderingSettings.setBitrateKbps(configuration.mediaBitrate / 1000); - } - adsRenderingSettings.setFocusSkipButtonWhenAvailable( - configuration.focusSkipButtonWhenAvailable); - if (configuration.adUiElements != null) { - adsRenderingSettings.setUiElements(configuration.adUiElements); - } - - // Skip ads based on the start position as required. - long[] adGroupTimesUs = getGroupTimesUs(adPlaybackState); - int adGroupForPositionIndex = - adPlaybackState.getAdGroupIndexForPositionUs( - C.msToUs(contentPositionMs), C.msToUs(contentDurationMs)); - if (adGroupForPositionIndex != C.INDEX_UNSET) { - boolean playAdWhenStartingPlayback = - configuration.playAdBeforeStartPosition - || adGroupTimesUs[adGroupForPositionIndex] == C.msToUs(contentPositionMs); - if (!playAdWhenStartingPlayback) { - adGroupForPositionIndex++; - } else if (hasMidrollAdGroups(adGroupTimesUs)) { - // Provide the player's initial position to trigger loading and playing the ad. If there are - // no midrolls, we are playing a preroll and any pending content position wouldn't be - // cleared. - pendingContentPositionMs = contentPositionMs; - } - if (adGroupForPositionIndex > 0) { - for (int i = 0; i < adGroupForPositionIndex; i++) { - adPlaybackState = adPlaybackState.withSkippedAdGroup(i); - } - if (adGroupForPositionIndex == adGroupTimesUs.length) { - // We don't need to play any ads. Because setPlayAdsAfterTime does not discard non-VMAP - // ads, we signal that no ads will render so the caller can destroy the ads manager. - return null; - } - long adGroupForPositionTimeUs = adGroupTimesUs[adGroupForPositionIndex]; - long adGroupBeforePositionTimeUs = adGroupTimesUs[adGroupForPositionIndex - 1]; - if (adGroupForPositionTimeUs == C.TIME_END_OF_SOURCE) { - // Play the postroll by offsetting the start position just past the last non-postroll ad. - adsRenderingSettings.setPlayAdsAfterTime( - (double) adGroupBeforePositionTimeUs / C.MICROS_PER_SECOND + 1d); - } else { - // Play ads after the midpoint between the ad to play and the one before it, to avoid - // issues with rounding one of the two ad times. - double midpointTimeUs = (adGroupForPositionTimeUs + adGroupBeforePositionTimeUs) / 2d; - adsRenderingSettings.setPlayAdsAfterTime(midpointTimeUs / C.MICROS_PER_SECOND); - } - } - } - return adsRenderingSettings; - } - - private long[] getGroupTimesUs(AdPlaybackState adPlaybackState) { - long[] groupTimes = new long[adPlaybackState.adGroupCount]; - - for (int i = 0; i < adPlaybackState.adGroupCount; i++) { - groupTimes[i] = adPlaybackState.getAdGroup(i).timeUs; - } - - return groupTimes; - } - - private VideoProgressUpdate getContentVideoProgressUpdate() { - boolean hasContentDuration = contentDurationMs != C.TIME_UNSET; - long contentPositionMs; - if (pendingContentPositionMs != C.TIME_UNSET) { - sentPendingContentPositionMs = true; - contentPositionMs = pendingContentPositionMs; - } else if (player == null) { - return lastContentProgress; - } else if (fakeContentProgressElapsedRealtimeMs != C.TIME_UNSET) { - long elapsedSinceEndMs = SystemClock.elapsedRealtime() - fakeContentProgressElapsedRealtimeMs; - contentPositionMs = fakeContentProgressOffsetMs + elapsedSinceEndMs; - } else if (imaAdState == IMA_AD_STATE_NONE && !playingAd && hasContentDuration) { - contentPositionMs = getContentPeriodPositionMs(player, timeline, period); - } else { - return VideoProgressUpdate.VIDEO_TIME_NOT_READY; - } - long contentDurationMs = hasContentDuration ? this.contentDurationMs : IMA_DURATION_UNSET; - return new VideoProgressUpdate(contentPositionMs, contentDurationMs); - } - - private VideoProgressUpdate getAdVideoProgressUpdate() { - if (player == null) { - return lastAdProgress; - } else if (imaAdState != IMA_AD_STATE_NONE && playingAd) { - long adDuration = player.getDuration(); - return adDuration == C.TIME_UNSET - ? VideoProgressUpdate.VIDEO_TIME_NOT_READY - : new VideoProgressUpdate(player.getCurrentPosition(), adDuration); - } else { - return VideoProgressUpdate.VIDEO_TIME_NOT_READY; - } - } - - private void updateAdProgress() { - VideoProgressUpdate videoProgressUpdate = getAdVideoProgressUpdate(); - if (configuration.debugModeEnabled) { - Log.d(TAG, "Ad progress: " + ImaUtil.getStringForVideoProgressUpdate(videoProgressUpdate)); - } - - AdMediaInfo adMediaInfo = checkNotNull(imaAdMediaInfo); - for (int i = 0; i < adCallbacks.size(); i++) { - adCallbacks.get(i).onAdProgress(adMediaInfo, videoProgressUpdate); - } - handler.removeCallbacks(updateAdProgressRunnable); - handler.postDelayed(updateAdProgressRunnable, AD_PROGRESS_UPDATE_INTERVAL_MS); - } - - private void stopUpdatingAdProgress() { - handler.removeCallbacks(updateAdProgressRunnable); - } - - private int getPlayerVolumePercent() { - @Nullable Player player = this.player; - if (player == null) { - return lastVolumePercent; - } - - if (player.isCommandAvailable(COMMAND_GET_VOLUME)) { - return (int) (player.getVolume() * 100); - } - - // Check for a selected track using an audio renderer. - return hasSelectedTrackOfType(player.getCurrentTracks(), C.TRACK_TYPE_AUDIO) ? 100 : 0; - } - - private static boolean hasSelectedTrackOfType(Tracks tracks, int trackType) { - ImmutableList trackGroups = tracks.getGroups(); - for (Tracks.Group g : trackGroups) { - if(g.isSelected()) { - for (int i = 0; i < g.length; i++) { - if (MimeTypes.getTrackType(g.getTrackFormat(i).sampleMimeType) == trackType) { - return true; - } - } - } - } - - return false; - } - - private static boolean hasSelectedTrackOfType(TrackSelectionArray trackSelections, int trackType) { - for (int i = 0; i < trackSelections.length; i++) { - @Nullable TrackSelection trackSelection = trackSelections.get(i); - if (trackSelection == null) { - continue; - } - for (int j = 0; j < trackSelection.length(); j++) { - if (MimeTypes.getTrackType(trackSelection.getFormat(j).sampleMimeType) == trackType) { - return true; - } - } - } - return false; - } - - private void handleAdEvent(AdEvent adEvent) { - if (adsManager == null) { - // Drop events after release. - return; - } - switch (adEvent.getType()) { - case AD_BREAK_FETCH_ERROR: - String adGroupTimeSecondsString = checkNotNull(adEvent.getAdData().get("adBreakTime")); - if (configuration.debugModeEnabled) { - Log.d(TAG, "Fetch error for ad at " + adGroupTimeSecondsString + " seconds"); - } - double adGroupTimeSeconds = Double.parseDouble(adGroupTimeSecondsString); - int adGroupIndex = - adGroupTimeSeconds == -1.0 - ? adPlaybackState.adGroupCount - 1 - : getAdGroupIndexForCuePointTimeSeconds(adGroupTimeSeconds); - markAdGroupInErrorStateAndClearPendingContentPosition(adGroupIndex); - break; - case CONTENT_PAUSE_REQUESTED: - // After CONTENT_PAUSE_REQUESTED, IMA will playAd/pauseAd/stopAd to show one or more ads - // before sending CONTENT_RESUME_REQUESTED. - imaPausedContent = true; - pauseContentInternal(); - break; - case TAPPED: - for (int i = 0; i < eventListeners.size(); i++) { - eventListeners.get(i).onAdTapped(); - } - break; - case CLICKED: - for (int i = 0; i < eventListeners.size(); i++) { - eventListeners.get(i).onAdClicked(); - } - break; - case CONTENT_RESUME_REQUESTED: - imaPausedContent = false; - resumeContentInternal(); - break; - case LOG: - Map adData = adEvent.getAdData(); - String message = "AdEvent: " + adData; - Log.i(TAG, message); - break; - default: - break; - } - } - - private void pauseContentInternal() { - imaAdState = IMA_AD_STATE_NONE; - if (sentPendingContentPositionMs) { - pendingContentPositionMs = C.TIME_UNSET; - sentPendingContentPositionMs = false; - } - } - - private void resumeContentInternal() { - if (imaAdInfo != null) { - adPlaybackState = adPlaybackState.withSkippedAdGroup(imaAdInfo.adGroupIndex); - updateAdPlaybackState(); - } - } - - /** - * Returns whether this instance is expecting the first ad in an the upcoming ad group to load - * within the {@link ImaUtil.Configuration#adPreloadTimeoutMs preload timeout}. - */ - private boolean isWaitingForAdToLoad() { - @Nullable Player player = this.player; - if (player == null) { - return false; - } - int adGroupIndex = getLoadingAdGroupIndex(); - if (adGroupIndex == C.INDEX_UNSET) { - return false; - } - AdPlaybackState.AdGroup adGroup = adPlaybackState.getAdGroup(adGroupIndex); - if (adGroup.count != C.LENGTH_UNSET - && adGroup.count != 0 - && adGroup.states[0] != AdPlaybackState.AD_STATE_UNAVAILABLE) { - // An ad is available already. - return false; - } - long adGroupTimeMs = C.usToMs(adGroup.timeUs); - long contentPositionMs = getContentPeriodPositionMs(player, timeline, period); - long timeUntilAdMs = adGroupTimeMs - contentPositionMs; - return timeUntilAdMs < configuration.adPreloadTimeoutMs; - } - - private void handlePlayerStateChanged(boolean playWhenReady, @Player.State int playbackState) { - if (playingAd && imaAdState == IMA_AD_STATE_PLAYING) { - if (!bufferingAd && playbackState == Player.STATE_BUFFERING) { - bufferingAd = true; - AdMediaInfo adMediaInfo = checkNotNull(imaAdMediaInfo); - for (int i = 0; i < adCallbacks.size(); i++) { - adCallbacks.get(i).onBuffering(adMediaInfo); - } - stopUpdatingAdProgress(); - } else if (bufferingAd && playbackState == Player.STATE_READY) { - bufferingAd = false; - updateAdProgress(); - } - } - - if (imaAdState == IMA_AD_STATE_NONE - && playbackState == Player.STATE_BUFFERING - && playWhenReady) { - ensureSentContentCompleteIfAtEndOfStream(); - } else if (imaAdState != IMA_AD_STATE_NONE && playbackState == Player.STATE_ENDED) { - @Nullable AdMediaInfo adMediaInfo = imaAdMediaInfo; - if (adMediaInfo == null) { - Log.w(TAG, "onEnded without ad media info"); - } else { - for (int i = 0; i < adCallbacks.size(); i++) { - adCallbacks.get(i).onEnded(adMediaInfo); - } - } - if (configuration.debugModeEnabled) { - Log.d(TAG, "VideoAdPlayerCallback.onEnded in onPlaybackStateChanged"); - } - } - } - - private void handleTimelineOrPositionChanged() { - @Nullable Player player = this.player; - if (adsManager == null || player == null) { - return; - } - if (!playingAd && !player.isPlayingAd()) { - ensureSentContentCompleteIfAtEndOfStream(); - if (!sentContentComplete && !timeline.isEmpty()) { - long positionMs = getContentPeriodPositionMs(player, timeline, period); - timeline.getPeriod(player.getCurrentPeriodIndex(), period); - int newAdGroupIndex = period.getAdGroupIndexForPositionUs(C.msToUs(positionMs)); - if (newAdGroupIndex != C.INDEX_UNSET) { - sentPendingContentPositionMs = false; - pendingContentPositionMs = positionMs; - } - } - } - - boolean wasPlayingAd = playingAd; - int oldPlayingAdIndexInAdGroup = playingAdIndexInAdGroup; - playingAd = player.isPlayingAd(); - playingAdIndexInAdGroup = playingAd ? player.getCurrentAdIndexInAdGroup() : C.INDEX_UNSET; - boolean adFinished = wasPlayingAd && playingAdIndexInAdGroup != oldPlayingAdIndexInAdGroup; - if (adFinished) { - // IMA is waiting for the ad playback to finish so invoke the callback now. - // Either CONTENT_RESUME_REQUESTED will be passed next, or playAd will be called again. - @Nullable AdMediaInfo adMediaInfo = imaAdMediaInfo; - if (adMediaInfo == null) { - Log.w(TAG, "onEnded without ad media info"); - } else { - @Nullable AdInfo adInfo = adInfoByAdMediaInfo.get(adMediaInfo); - if (playingAdIndexInAdGroup == C.INDEX_UNSET - || (adInfo != null && adInfo.adIndexInAdGroup < playingAdIndexInAdGroup)) { - for (int i = 0; i < adCallbacks.size(); i++) { - adCallbacks.get(i).onEnded(adMediaInfo); - } - if (configuration.debugModeEnabled) { - Log.d( - TAG, "VideoAdPlayerCallback.onEnded in onTimelineChanged/onPositionDiscontinuity"); - } - } - } - } - if (!sentContentComplete && !wasPlayingAd && playingAd && imaAdState == IMA_AD_STATE_NONE) { - int adGroupIndex = player.getCurrentAdGroupIndex(); - if (adPlaybackState.getAdGroup(adGroupIndex).timeUs == C.TIME_END_OF_SOURCE) { - sendContentComplete(); - } else { - // IMA hasn't called playAd yet, so fake the content position. - fakeContentProgressElapsedRealtimeMs = SystemClock.elapsedRealtime(); - fakeContentProgressOffsetMs = C.usToMs(adPlaybackState.getAdGroup(adGroupIndex).timeUs); - if (fakeContentProgressOffsetMs == C.TIME_END_OF_SOURCE) { - fakeContentProgressOffsetMs = contentDurationMs; - } - } - } - } - - private void loadAdInternal(AdMediaInfo adMediaInfo, AdPodInfo adPodInfo) { - if (adsManager == null) { - // Drop events after release. - if (configuration.debugModeEnabled) { - Log.d( - TAG, - "loadAd after release " + getAdMediaInfoString(adMediaInfo) + ", ad pod " + adPodInfo); - } - return; - } - - int adGroupIndex = getAdGroupIndexForAdPod(adPodInfo); - int adIndexInAdGroup = adPodInfo.getAdPosition() - 1; - AdInfo adInfo = new AdInfo(adGroupIndex, adIndexInAdGroup); - // The ad URI may already be known, so force put to update it if needed. - adInfoByAdMediaInfo.forcePut(adMediaInfo, adInfo); - if (configuration.debugModeEnabled) { - Log.d(TAG, "loadAd " + getAdMediaInfoString(adMediaInfo)); - } - if (adPlaybackState.isAdInErrorState(adGroupIndex, adIndexInAdGroup)) { - // We have already marked this ad as having failed to load, so ignore the request. IMA will - // timeout after its media load timeout. - return; - } - - // The ad count may increase on successive loads of ads in the same ad pod, for example, due to - // separate requests for ad tags with multiple ads within the ad pod completing after an earlier - // ad has loaded. See also https://github.com/google/ExoPlayer/issues/7477. - AdPlaybackState.AdGroup adGroup = adPlaybackState.getAdGroup(adInfo.adGroupIndex); - adPlaybackState = - adPlaybackState.withAdCount( - adInfo.adGroupIndex, max(adPodInfo.getTotalAds(), adGroup.states.length)); - adGroup = adPlaybackState.getAdGroup(adInfo.adGroupIndex); - for (int i = 0; i < adIndexInAdGroup; i++) { - // Any preceding ads that haven't loaded are not going to load. - if (adGroup.states[i] == AdPlaybackState.AD_STATE_UNAVAILABLE) { - adPlaybackState = adPlaybackState.withAdLoadError(adGroupIndex, /* adIndexInAdGroup= */ i); - } - } - - Uri adUri = Uri.parse(adMediaInfo.getUrl()); - adPlaybackState = - adPlaybackState.withAdUri(adInfo.adGroupIndex, adInfo.adIndexInAdGroup, adUri); - updateAdPlaybackState(); - } - - private void playAdInternal(AdMediaInfo adMediaInfo) { - if (configuration.debugModeEnabled) { - Log.d(TAG, "playAd " + getAdMediaInfoString(adMediaInfo)); - } - if (adsManager == null) { - // Drop events after release. - return; - } - - if (imaAdState == IMA_AD_STATE_PLAYING) { - // IMA does not always call stopAd before resuming content. - // See [Internal: b/38354028]. - Log.w(TAG, "Unexpected playAd without stopAd"); - } - - if (imaAdState == IMA_AD_STATE_NONE) { - // IMA is requesting to play the ad, so stop faking the content position. - fakeContentProgressElapsedRealtimeMs = C.TIME_UNSET; - fakeContentProgressOffsetMs = C.TIME_UNSET; - imaAdState = IMA_AD_STATE_PLAYING; - imaAdMediaInfo = adMediaInfo; - imaAdInfo = checkNotNull(adInfoByAdMediaInfo.get(adMediaInfo)); - for (int i = 0; i < adCallbacks.size(); i++) { - adCallbacks.get(i).onPlay(adMediaInfo); - } - if (pendingAdPrepareErrorAdInfo != null && pendingAdPrepareErrorAdInfo.equals(imaAdInfo)) { - pendingAdPrepareErrorAdInfo = null; - for (int i = 0; i < adCallbacks.size(); i++) { - adCallbacks.get(i).onError(adMediaInfo); - } - } - updateAdProgress(); - } else { - imaAdState = IMA_AD_STATE_PLAYING; - checkState(adMediaInfo.equals(imaAdMediaInfo)); - for (int i = 0; i < adCallbacks.size(); i++) { - adCallbacks.get(i).onResume(adMediaInfo); - } - } - if (player == null || !player.getPlayWhenReady()) { - // Either this loader hasn't been activated yet, or the player is paused now. - checkNotNull(adsManager).pause(); - } - } - - private void pauseAdInternal(AdMediaInfo adMediaInfo) { - if (configuration.debugModeEnabled) { - Log.d(TAG, "pauseAd " + getAdMediaInfoString(adMediaInfo)); - } - if (adsManager == null) { - // Drop event after release. - return; - } - if (imaAdState == IMA_AD_STATE_NONE) { - // This method is called if loadAd has been called but the loaded ad won't play due to a seek - // to a different position, so drop the event. See also [Internal: b/159111848]. - return; - } - if (configuration.debugModeEnabled && !adMediaInfo.equals(imaAdMediaInfo)) { - Log.w( - TAG, - "Unexpected pauseAd for " - + getAdMediaInfoString(adMediaInfo) - + ", expected " - + getAdMediaInfoString(imaAdMediaInfo)); - } - imaAdState = IMA_AD_STATE_PAUSED; - for (int i = 0; i < adCallbacks.size(); i++) { - adCallbacks.get(i).onPause(adMediaInfo); - } - } - - private void stopAdInternal(AdMediaInfo adMediaInfo) { - if (configuration.debugModeEnabled) { - Log.d(TAG, "stopAd " + getAdMediaInfoString(adMediaInfo)); - } - if (adsManager == null) { - // Drop event after release. - return; - } - if (imaAdState == IMA_AD_STATE_NONE) { - // This method is called if loadAd has been called but the preloaded ad won't play due to a - // seek to a different position, so drop the event and discard the ad. See also [Internal: - // b/159111848]. - @Nullable AdInfo adInfo = adInfoByAdMediaInfo.get(adMediaInfo); - if (adInfo != null) { - adPlaybackState = - adPlaybackState.withSkippedAd(adInfo.adGroupIndex, adInfo.adIndexInAdGroup); - updateAdPlaybackState(); - } - return; - } - imaAdState = IMA_AD_STATE_NONE; - stopUpdatingAdProgress(); - // TODO: Handle the skipped event so the ad can be marked as skipped rather than played. - checkNotNull(imaAdInfo); - int adGroupIndex = imaAdInfo.adGroupIndex; - int adIndexInAdGroup = imaAdInfo.adIndexInAdGroup; - if (adPlaybackState.isAdInErrorState(adGroupIndex, adIndexInAdGroup)) { - // We have already marked this ad as having failed to load, so ignore the request. - return; - } - adPlaybackState = - adPlaybackState.withPlayedAd(adGroupIndex, adIndexInAdGroup).withAdResumePositionUs(0); - updateAdPlaybackState(); - if (!playingAd) { - imaAdMediaInfo = null; - imaAdInfo = null; - } - } - - private void handleAdGroupLoadError(Exception error) { - int adGroupIndex = getLoadingAdGroupIndex(); - if (adGroupIndex == C.INDEX_UNSET) { - Log.w(TAG, "Unable to determine ad group index for ad group load error", error); - return; - } - markAdGroupInErrorStateAndClearPendingContentPosition(adGroupIndex); - if (pendingAdLoadError == null) { - pendingAdLoadError = AdLoadException.createForAdGroup(error, adGroupIndex); - } - } - - private void markAdGroupInErrorStateAndClearPendingContentPosition(int adGroupIndex) { - // Update the ad playback state so all ads in the ad group are in the error state. - AdPlaybackState.AdGroup adGroup = adPlaybackState.getAdGroup(adGroupIndex); - if (adGroup.count == C.LENGTH_UNSET) { - adPlaybackState = adPlaybackState.withAdCount(adGroupIndex, max(1, adGroup.states.length)); - adGroup = adPlaybackState.getAdGroup(adGroupIndex); - } - for (int i = 0; i < adGroup.count; i++) { - if (adGroup.states[i] == AdPlaybackState.AD_STATE_UNAVAILABLE) { - if (configuration.debugModeEnabled) { - Log.d(TAG, "Removing ad " + i + " in ad group " + adGroupIndex); - } - adPlaybackState = adPlaybackState.withAdLoadError(adGroupIndex, i); - } - } - updateAdPlaybackState(); - // Clear any pending content position that triggered attempting to load the ad group. - pendingContentPositionMs = C.TIME_UNSET; - fakeContentProgressElapsedRealtimeMs = C.TIME_UNSET; - } - - private void handleAdPrepareError(int adGroupIndex, int adIndexInAdGroup, Exception exception) { - if (configuration.debugModeEnabled) { - Log.d( - TAG, "Prepare error for ad " + adIndexInAdGroup + " in group " + adGroupIndex, exception); - } - if (adsManager == null) { - Log.w(TAG, "Ignoring ad prepare error after release"); - return; - } - if (imaAdState == IMA_AD_STATE_NONE) { - // Send IMA a content position at the ad group so that it will try to play it, at which point - // we can notify that it failed to load. - fakeContentProgressElapsedRealtimeMs = SystemClock.elapsedRealtime(); - fakeContentProgressOffsetMs = C.usToMs(adPlaybackState.getAdGroup(adGroupIndex).timeUs); - if (fakeContentProgressOffsetMs == C.TIME_END_OF_SOURCE) { - fakeContentProgressOffsetMs = contentDurationMs; - } - pendingAdPrepareErrorAdInfo = new AdInfo(adGroupIndex, adIndexInAdGroup); - } else { - AdMediaInfo adMediaInfo = checkNotNull(imaAdMediaInfo); - // We're already playing an ad. - if (adIndexInAdGroup > playingAdIndexInAdGroup) { - // Mark the playing ad as ended so we can notify the error on the next ad and remove it, - // which means that the ad after will load (if any). - for (int i = 0; i < adCallbacks.size(); i++) { - adCallbacks.get(i).onEnded(adMediaInfo); - } - } - playingAdIndexInAdGroup = adPlaybackState.getAdGroup(adGroupIndex).getFirstAdIndexToPlay(); - for (int i = 0; i < adCallbacks.size(); i++) { - adCallbacks.get(i).onError(checkNotNull(adMediaInfo)); - } - } - adPlaybackState = adPlaybackState.withAdLoadError(adGroupIndex, adIndexInAdGroup); - updateAdPlaybackState(); - } - - private void ensureSentContentCompleteIfAtEndOfStream() { - if (!sentContentComplete - && contentDurationMs != C.TIME_UNSET - && pendingContentPositionMs == C.TIME_UNSET - && getContentPeriodPositionMs(checkNotNull(player), timeline, period) - + THRESHOLD_END_OF_CONTENT_MS - >= contentDurationMs) { - sendContentComplete(); - } - } - - private void sendContentComplete() { - for (int i = 0; i < adCallbacks.size(); i++) { - adCallbacks.get(i).onContentComplete(); - } - sentContentComplete = true; - if (configuration.debugModeEnabled) { - Log.d(TAG, "adsLoader.contentComplete"); - } - for (int i = 0; i < adPlaybackState.adGroupCount; i++) { - if (adPlaybackState.getAdGroup(i).timeUs != C.TIME_END_OF_SOURCE) { - adPlaybackState = adPlaybackState.withSkippedAdGroup(/* adGroupIndex= */ i); - } - } - updateAdPlaybackState(); - } - - private void updateAdPlaybackState() { - for (int i = 0; i < eventListeners.size(); i++) { - eventListeners.get(i).onAdPlaybackState(adPlaybackState); - } - } - - private void maybeNotifyPendingAdLoadError() { - if (pendingAdLoadError != null) { - for (int i = 0; i < eventListeners.size(); i++) { - eventListeners.get(i).onAdLoadError(pendingAdLoadError, adTagDataSpec); - } - pendingAdLoadError = null; - } - } - - private void maybeNotifyInternalError(String name, Exception cause) { - String message = "Internal error in " + name; - Log.e(TAG, message, cause); - // We can't recover from an unexpected error in general, so skip all remaining ads. - for (int i = 0; i < adPlaybackState.adGroupCount; i++) { - adPlaybackState = adPlaybackState.withSkippedAdGroup(i); - } - updateAdPlaybackState(); - for (int i = 0; i < eventListeners.size(); i++) { - eventListeners - .get(i) - .onAdLoadError( - AdLoadException.createForUnexpected(new RuntimeException(message, cause)), - adTagDataSpec); - } - } - - private int getAdGroupIndexForAdPod(AdPodInfo adPodInfo) { - if (adPodInfo.getPodIndex() == -1) { - // This is a postroll ad. - return adPlaybackState.adGroupCount - 1; - } - - // adPodInfo.podIndex may be 0-based or 1-based, so for now look up the cue point instead. - return getAdGroupIndexForCuePointTimeSeconds(adPodInfo.getTimeOffset()); - } - - /** - * Returns the index of the ad group that will preload next, or {@link C#INDEX_UNSET} if there is - * no such ad group. - */ - private int getLoadingAdGroupIndex() { - if (player == null) { - return C.INDEX_UNSET; - } - long playerPositionUs = C.msToUs(getContentPeriodPositionMs(player, timeline, period)); - int adGroupIndex = - adPlaybackState.getAdGroupIndexForPositionUs(playerPositionUs, C.msToUs(contentDurationMs)); - if (adGroupIndex == C.INDEX_UNSET) { - adGroupIndex = - adPlaybackState.getAdGroupIndexAfterPositionUs( - playerPositionUs, C.msToUs(contentDurationMs)); - } - return adGroupIndex; - } - - private int getAdGroupIndexForCuePointTimeSeconds(double cuePointTimeSeconds) { - // We receive initial cue points from IMA SDK as floats. This code replicates the same - // calculation used to populate adGroupTimesUs (having truncated input back to float, to avoid - // failures if the behavior of the IMA SDK changes to provide greater precision). - float cuePointTimeSecondsFloat = (float) cuePointTimeSeconds; - long adPodTimeUs = Math.round((double) cuePointTimeSecondsFloat * C.MICROS_PER_SECOND); - for (int adGroupIndex = 0; adGroupIndex < adPlaybackState.adGroupCount; adGroupIndex++) { - long adGroupTimeUs = adPlaybackState.getAdGroup(adGroupIndex).timeUs; - if (adGroupTimeUs != C.TIME_END_OF_SOURCE - && Math.abs(adGroupTimeUs - adPodTimeUs) < THRESHOLD_AD_MATCH_US) { - return adGroupIndex; - } - } - throw new IllegalStateException("Failed to find cue point"); - } - - private String getAdMediaInfoString(@Nullable AdMediaInfo adMediaInfo) { - @Nullable AdInfo adInfo = adInfoByAdMediaInfo.get(adMediaInfo); - return "AdMediaInfo[" - + (adMediaInfo == null ? "null" : adMediaInfo.getUrl()) - + ", " - + adInfo - + "]"; - } - - private static long getContentPeriodPositionMs( - Player player, Timeline timeline, Timeline.Period period) { - long contentWindowPositionMs = player.getContentPosition(); - if (timeline.isEmpty()) { - return contentWindowPositionMs; - } else { - return contentWindowPositionMs - - timeline.getPeriod(player.getCurrentPeriodIndex(), period).getPositionInWindowMs(); - } - } - - private static boolean hasMidrollAdGroups(long[] adGroupTimesUs) { - int count = adGroupTimesUs.length; - if (count == 1) { - return adGroupTimesUs[0] != 0 && adGroupTimesUs[0] != C.TIME_END_OF_SOURCE; - } else if (count == 2) { - return adGroupTimesUs[0] != 0 || adGroupTimesUs[1] != C.TIME_END_OF_SOURCE; - } else { - // There's at least one midroll ad group, as adGroupTimesUs is never empty. - return true; - } - } - - private void destroyAdsManager() { - if (adsManager != null) { - adsManager.removeAdErrorListener(componentListener); - if (configuration.applicationAdErrorListener != null) { - adsManager.removeAdErrorListener(configuration.applicationAdErrorListener); - } - adsManager.removeAdEventListener(componentListener); - if (configuration.applicationAdEventListener != null) { - adsManager.removeAdEventListener(configuration.applicationAdEventListener); - } - adsManager.destroy(); - adsManager = null; - } - } - - private final class ComponentListener - implements AdsLoadedListener, - ContentProgressProvider, - AdEventListener, - AdErrorListener, - VideoAdPlayer { - - // AdsLoader.AdsLoadedListener implementation. - - @Override - public void onAdsManagerLoaded(AdsManagerLoadedEvent adsManagerLoadedEvent) { - AdsManager adsManager = adsManagerLoadedEvent.getAdsManager(); - if (!Util.areEqual(pendingAdRequestContext, adsManagerLoadedEvent.getUserRequestContext())) { - adsManager.destroy(); - return; - } - pendingAdRequestContext = null; - AdTagLoader.this.adsManager = adsManager; - adsManager.addAdErrorListener(this); - if (configuration.applicationAdErrorListener != null) { - adsManager.addAdErrorListener(configuration.applicationAdErrorListener); - } - adsManager.addAdEventListener(this); - if (configuration.applicationAdEventListener != null) { - adsManager.addAdEventListener(configuration.applicationAdEventListener); - } - try { - adPlaybackState = - new AdPlaybackState(adsId, getAdGroupTimesUsForCuePoints(adsManager.getAdCuePoints())); - updateAdPlaybackState(); - } catch (RuntimeException e) { - maybeNotifyInternalError("onAdsManagerLoaded", e); - } - } - - // ContentProgressProvider implementation. - - @Override - public VideoProgressUpdate getContentProgress() { - VideoProgressUpdate videoProgressUpdate = getContentVideoProgressUpdate(); - if (configuration.debugModeEnabled) { - Log.d( - TAG, - "Content progress: " + ImaUtil.getStringForVideoProgressUpdate(videoProgressUpdate)); - } - - if (waitingForPreloadElapsedRealtimeMs != C.TIME_UNSET) { - // IMA is polling the player position but we are buffering for an ad to preload, so playback - // may be stuck. Detect this case and signal an error if applicable. - long stuckElapsedRealtimeMs = - SystemClock.elapsedRealtime() - waitingForPreloadElapsedRealtimeMs; - if (stuckElapsedRealtimeMs >= THRESHOLD_AD_PRELOAD_MS) { - waitingForPreloadElapsedRealtimeMs = C.TIME_UNSET; - handleAdGroupLoadError(new IOException("Ad preloading timed out")); - maybeNotifyPendingAdLoadError(); - } - } else if (pendingContentPositionMs != C.TIME_UNSET - && player != null - && player.getPlaybackState() == Player.STATE_BUFFERING - && isWaitingForAdToLoad()) { - // Prepare to timeout the load of an ad for the pending seek operation. - waitingForPreloadElapsedRealtimeMs = SystemClock.elapsedRealtime(); - } - - return videoProgressUpdate; - } - - // AdEvent.AdEventListener implementation. - - @Override - public void onAdEvent(AdEvent adEvent) { - AdEventType adEventType = adEvent.getType(); - if (configuration.debugModeEnabled && adEventType != AdEventType.AD_PROGRESS) { - Log.d(TAG, "onAdEvent: " + adEventType); - } - try { - handleAdEvent(adEvent); - } catch (RuntimeException e) { - maybeNotifyInternalError("onAdEvent", e); - } - } - - // AdErrorEvent.AdErrorListener implementation. - - @Override - public void onAdError(AdErrorEvent adErrorEvent) { - AdError error = adErrorEvent.getError(); - if (configuration.debugModeEnabled) { - Log.d(TAG, "onAdError", error); - } - if (adsManager == null) { - // No ads were loaded, so allow playback to start without any ads. - pendingAdRequestContext = null; - adPlaybackState = new AdPlaybackState(adsId); - updateAdPlaybackState(); - } else if (ImaUtil.isAdGroupLoadError(error)) { - try { - handleAdGroupLoadError(error); - } catch (RuntimeException e) { - maybeNotifyInternalError("onAdError", e); - } - } - if (pendingAdLoadError == null) { - pendingAdLoadError = AdLoadException.createForAllAds(error); - } - maybeNotifyPendingAdLoadError(); - } - - // VideoAdPlayer implementation. - - @Override - public void addCallback(VideoAdPlayerCallback videoAdPlayerCallback) { - adCallbacks.add(videoAdPlayerCallback); - } - - @Override - public void removeCallback(VideoAdPlayerCallback videoAdPlayerCallback) { - adCallbacks.remove(videoAdPlayerCallback); - } - - @Override - public VideoProgressUpdate getAdProgress() { - throw new IllegalStateException("Unexpected call to getAdProgress when using preloading"); - } - - @Override - public int getVolume() { - return getPlayerVolumePercent(); - } - - @Override - public void loadAd(AdMediaInfo adMediaInfo, AdPodInfo adPodInfo) { - try { - loadAdInternal(adMediaInfo, adPodInfo); - } catch (RuntimeException e) { - maybeNotifyInternalError("loadAd", e); - } - } - - @Override - public void playAd(AdMediaInfo adMediaInfo) { - try { - playAdInternal(adMediaInfo); - } catch (RuntimeException e) { - maybeNotifyInternalError("playAd", e); - } - } - - @Override - public void pauseAd(AdMediaInfo adMediaInfo) { - try { - pauseAdInternal(adMediaInfo); - } catch (RuntimeException e) { - maybeNotifyInternalError("pauseAd", e); - } - } - - @Override - public void stopAd(AdMediaInfo adMediaInfo) { - try { - stopAdInternal(adMediaInfo); - } catch (RuntimeException e) { - maybeNotifyInternalError("stopAd", e); - } - } - - @Override - public void release() { - // Do nothing. - } - } - - // TODO: Consider moving this into AdPlaybackState. - private static final class AdInfo { - - public final int adGroupIndex; - public final int adIndexInAdGroup; - - public AdInfo(int adGroupIndex, int adIndexInAdGroup) { - this.adGroupIndex = adGroupIndex; - this.adIndexInAdGroup = adIndexInAdGroup; - } - - @Override - public boolean equals(@Nullable Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - AdInfo adInfo = (AdInfo) o; - if (adGroupIndex != adInfo.adGroupIndex) { - return false; - } - return adIndexInAdGroup == adInfo.adIndexInAdGroup; - } - - @Override - public int hashCode() { - int result = adGroupIndex; - result = 31 * result + adIndexInAdGroup; - return result; - } - - @Override - public String toString() { - return "(" + adGroupIndex + ", " + adIndexInAdGroup + ')'; - } - } -} diff --git a/automatedtests/src/androidTestR2_18_1/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java b/automatedtests/src/androidTestR2_18_1/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java deleted file mode 100644 index 72cae676..00000000 --- a/automatedtests/src/androidTestR2_18_1/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java +++ /dev/null @@ -1,759 +0,0 @@ -/* - * Copyright (C) 2017 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.google.android.exoplayer2.ext.ima; - -import static com.google.android.exoplayer2.ext.ima.ImaUtil.BITRATE_UNSET; -import static com.google.android.exoplayer2.ext.ima.ImaUtil.TIMEOUT_UNSET; -import static com.google.android.exoplayer2.ext.ima.ImaUtil.getImaLooper; -import static com.google.android.exoplayer2.util.Assertions.checkArgument; -import static com.google.android.exoplayer2.util.Assertions.checkNotNull; -import static com.google.android.exoplayer2.util.Assertions.checkState; - -import android.content.Context; -import android.os.Looper; -import android.view.View; -import android.view.ViewGroup; -import androidx.annotation.Nullable; -import androidx.annotation.VisibleForTesting; -import com.google.ads.interactivemedia.v3.api.AdDisplayContainer; -import com.google.ads.interactivemedia.v3.api.AdErrorEvent.AdErrorListener; -import com.google.ads.interactivemedia.v3.api.AdEvent.AdEventListener; -import com.google.ads.interactivemedia.v3.api.AdsManager; -import com.google.ads.interactivemedia.v3.api.AdsRenderingSettings; -import com.google.ads.interactivemedia.v3.api.AdsRequest; -import com.google.ads.interactivemedia.v3.api.CompanionAdSlot; -import com.google.ads.interactivemedia.v3.api.FriendlyObstruction; -import com.google.ads.interactivemedia.v3.api.FriendlyObstructionPurpose; -import com.google.ads.interactivemedia.v3.api.ImaSdkFactory; -import com.google.ads.interactivemedia.v3.api.ImaSdkSettings; -import com.google.ads.interactivemedia.v3.api.UiElement; -import com.google.ads.interactivemedia.v3.api.player.VideoAdPlayer; -import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.ExoPlayerLibraryInfo; -import com.google.android.exoplayer2.Player; -import com.google.android.exoplayer2.Timeline; -import com.google.android.exoplayer2.source.MediaSourceFactory; -import com.google.android.exoplayer2.source.ads.AdsLoader; -import com.google.android.exoplayer2.source.ads.AdsMediaSource; -import com.google.android.exoplayer2.ui.AdViewProvider; -import com.google.android.exoplayer2.upstream.DataSpec; -import com.google.android.exoplayer2.util.MimeTypes; -import com.google.android.exoplayer2.util.Util; -import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableSet; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Set; - -/** - * {@link AdsLoader} using the IMA SDK. All methods must be called on the main thread. - * - *

The player instance that will play the loaded ads must be set before playback using {@link - * #setPlayer(Player)}. If the ads loader is no longer required, it must be released by calling - * {@link #release()}. - * - *

See https://developers.google.com/interactive-media-ads/docs/sdks/android/compatibility for - * information on compatible ad tag formats. Pass the ad tag URI when setting media item playback - * properties (if using the media item API) or as a {@link DataSpec} when constructing the {@link - * AdsMediaSource} (if using media sources directly). For the latter case, please note that this - * implementation delegates loading of the data spec to the IMA SDK, so range and headers - * specifications will be ignored in ad tag URIs. Literal ads responses can be encoded as data - * scheme data specs, for example, by constructing the data spec using a URI generated via {@link - * Util#getDataUriForString(String, String)}. - * - *

The IMA SDK can report obstructions to the ad view for accurate viewability measurement. This - * means that any overlay views that obstruct the ad overlay but are essential for playback need to - * be registered via the {@link AdViewProvider} passed to the {@link AdsMediaSource}. See the IMA - * SDK Open Measurement documentation for more information. - */ -public final class ImaAdsLoader implements Player.Listener, AdsLoader { - - static { - ExoPlayerLibraryInfo.registerModule("goog.exo.ima"); - } - - /** Builder for {@link ImaAdsLoader}. */ - public static final class Builder { - - /** - * The default duration in milliseconds for which the player must buffer while preloading an ad - * group before that ad group is skipped and marked as having failed to load. - * - *

This value should be large enough not to trigger discarding the ad when it actually might - * load soon, but small enough so that user is not waiting for too long. - * - * @see #setAdPreloadTimeoutMs(long) - */ - public static final long DEFAULT_AD_PRELOAD_TIMEOUT_MS = 10 * C.MILLIS_PER_SECOND; - - private final Context context; - - @Nullable private ImaSdkSettings imaSdkSettings; - @Nullable private AdErrorListener adErrorListener; - @Nullable private AdEventListener adEventListener; - @Nullable private VideoAdPlayer.VideoAdPlayerCallback videoAdPlayerCallback; - @Nullable private List adMediaMimeTypes; - @Nullable private Set adUiElements; - @Nullable private Collection companionAdSlots; - @Nullable private Boolean enableContinuousPlayback; - private long adPreloadTimeoutMs; - private int vastLoadTimeoutMs; - private int mediaLoadTimeoutMs; - private int mediaBitrate; - private boolean focusSkipButtonWhenAvailable; - private boolean playAdBeforeStartPosition; - private boolean debugModeEnabled; - private ImaUtil.ImaFactory imaFactory; - - /** - * Creates a new builder for {@link ImaAdsLoader}. - * - * @param context The context; - */ - public Builder(Context context) { - this.context = checkNotNull(context).getApplicationContext(); - adPreloadTimeoutMs = DEFAULT_AD_PRELOAD_TIMEOUT_MS; - vastLoadTimeoutMs = TIMEOUT_UNSET; - mediaLoadTimeoutMs = TIMEOUT_UNSET; - mediaBitrate = BITRATE_UNSET; - focusSkipButtonWhenAvailable = true; - playAdBeforeStartPosition = true; - imaFactory = new DefaultImaFactory(); - } - - /** - * Sets the IMA SDK settings. The provided settings instance's player type and version fields - * may be overwritten. - * - *

If this method is not called the default settings will be used. - * - * @param imaSdkSettings The {@link ImaSdkSettings}. - * @return This builder, for convenience. - */ - public Builder setImaSdkSettings(ImaSdkSettings imaSdkSettings) { - this.imaSdkSettings = checkNotNull(imaSdkSettings); - return this; - } - - /** - * Sets a listener for ad errors that will be passed to {@link - * com.google.ads.interactivemedia.v3.api.AdsLoader#addAdErrorListener(AdErrorListener)} and - * {@link AdsManager#addAdErrorListener(AdErrorListener)}. - * - * @param adErrorListener The ad error listener. - * @return This builder, for convenience. - */ - public Builder setAdErrorListener(AdErrorListener adErrorListener) { - this.adErrorListener = checkNotNull(adErrorListener); - return this; - } - - /** - * Sets a listener for ad events that will be passed to {@link - * AdsManager#addAdEventListener(AdEventListener)}. - * - * @param adEventListener The ad event listener. - * @return This builder, for convenience. - */ - public Builder setAdEventListener(AdEventListener adEventListener) { - this.adEventListener = checkNotNull(adEventListener); - return this; - } - - /** - * Sets a callback to receive video ad player events. Note that these events are handled - * internally by the IMA SDK and this ads loader. For analytics and diagnostics, new - * implementations should generally use events from the top-level {@link Player} listeners - * instead of setting a callback via this method. - * - * @param videoAdPlayerCallback The callback to receive video ad player events. - * @return This builder, for convenience. - * @see com.google.ads.interactivemedia.v3.api.player.VideoAdPlayer.VideoAdPlayerCallback - */ - public Builder setVideoAdPlayerCallback( - VideoAdPlayer.VideoAdPlayerCallback videoAdPlayerCallback) { - this.videoAdPlayerCallback = checkNotNull(videoAdPlayerCallback); - return this; - } - - /** - * Sets the ad UI elements to be rendered by the IMA SDK. - * - * @param adUiElements The ad UI elements to be rendered by the IMA SDK. - * @return This builder, for convenience. - * @see AdsRenderingSettings#setUiElements(Set) - */ - public Builder setAdUiElements(Set adUiElements) { - this.adUiElements = ImmutableSet.copyOf(checkNotNull(adUiElements)); - return this; - } - - /** - * Sets the slots to use for companion ads, if they are present in the loaded ad. - * - * @param companionAdSlots The slots to use for companion ads. - * @return This builder, for convenience. - * @see AdDisplayContainer#setCompanionSlots(Collection) - */ - public Builder setCompanionAdSlots(Collection companionAdSlots) { - this.companionAdSlots = ImmutableList.copyOf(checkNotNull(companionAdSlots)); - return this; - } - - /** - * Sets the MIME types to prioritize for linear ad media. If not specified, MIME types supported - * by the {@link MediaSourceFactory adMediaSourceFactory} used to construct the {@link - * AdsMediaSource} will be used. - * - * @param adMediaMimeTypes The MIME types to prioritize for linear ad media. May contain {@link - * MimeTypes#APPLICATION_MPD}, {@link MimeTypes#APPLICATION_M3U8}, {@link - * MimeTypes#VIDEO_MP4}, {@link MimeTypes#VIDEO_WEBM}, {@link MimeTypes#VIDEO_H263}, {@link - * MimeTypes#AUDIO_MP4} and {@link MimeTypes#AUDIO_MPEG}. - * @return This builder, for convenience. - * @see AdsRenderingSettings#setMimeTypes(List) - */ - public Builder setAdMediaMimeTypes(List adMediaMimeTypes) { - this.adMediaMimeTypes = ImmutableList.copyOf(checkNotNull(adMediaMimeTypes)); - return this; - } - - /** - * Sets whether to enable continuous playback. Pass {@code true} if content videos will be - * played continuously, similar to a TV broadcast. This setting may modify the ads request but - * does not affect ad playback behavior. The requested value is unknown by default. - * - * @param enableContinuousPlayback Whether to enable continuous playback. - * @return This builder, for convenience. - * @see AdsRequest#setContinuousPlayback(boolean) - */ - public Builder setEnableContinuousPlayback(boolean enableContinuousPlayback) { - this.enableContinuousPlayback = enableContinuousPlayback; - return this; - } - - /** - * Sets the duration in milliseconds for which the player must buffer while preloading an ad - * group before that ad group is skipped and marked as having failed to load. Pass {@link - * C#TIME_UNSET} if there should be no such timeout. The default value is {@value - * #DEFAULT_AD_PRELOAD_TIMEOUT_MS} ms. - * - *

The purpose of this timeout is to avoid playback getting stuck in the unexpected case that - * the IMA SDK does not load an ad break based on the player's reported content position. - * - * @param adPreloadTimeoutMs The timeout buffering duration in milliseconds, or {@link - * C#TIME_UNSET} for no timeout. - * @return This builder, for convenience. - */ - public Builder setAdPreloadTimeoutMs(long adPreloadTimeoutMs) { - checkArgument(adPreloadTimeoutMs == C.TIME_UNSET || adPreloadTimeoutMs > 0); - this.adPreloadTimeoutMs = adPreloadTimeoutMs; - return this; - } - - /** - * Sets the VAST load timeout, in milliseconds. - * - * @param vastLoadTimeoutMs The VAST load timeout, in milliseconds. - * @return This builder, for convenience. - * @see AdsRequest#setVastLoadTimeout(float) - */ - public Builder setVastLoadTimeoutMs(int vastLoadTimeoutMs) { - checkArgument(vastLoadTimeoutMs > 0); - this.vastLoadTimeoutMs = vastLoadTimeoutMs; - return this; - } - - /** - * Sets the ad media load timeout, in milliseconds. - * - * @param mediaLoadTimeoutMs The ad media load timeout, in milliseconds. - * @return This builder, for convenience. - * @see AdsRenderingSettings#setLoadVideoTimeout(int) - */ - public Builder setMediaLoadTimeoutMs(int mediaLoadTimeoutMs) { - checkArgument(mediaLoadTimeoutMs > 0); - this.mediaLoadTimeoutMs = mediaLoadTimeoutMs; - return this; - } - - /** - * Sets the media maximum recommended bitrate for ads, in bps. - * - * @param bitrate The media maximum recommended bitrate for ads, in bps. - * @return This builder, for convenience. - * @see AdsRenderingSettings#setBitrateKbps(int) - */ - public Builder setMaxMediaBitrate(int bitrate) { - checkArgument(bitrate > 0); - this.mediaBitrate = bitrate; - return this; - } - - /** - * Sets whether to focus the skip button (when available) on Android TV devices. The default - * setting is {@code true}. - * - * @param focusSkipButtonWhenAvailable Whether to focus the skip button (when available) on - * Android TV devices. - * @return This builder, for convenience. - * @see AdsRenderingSettings#setFocusSkipButtonWhenAvailable(boolean) - */ - public Builder setFocusSkipButtonWhenAvailable(boolean focusSkipButtonWhenAvailable) { - this.focusSkipButtonWhenAvailable = focusSkipButtonWhenAvailable; - return this; - } - - /** - * Sets whether to play an ad before the start position when beginning playback. If {@code - * true}, an ad will be played if there is one at or before the start position. If {@code - * false}, an ad will be played only if there is one exactly at the start position. The default - * setting is {@code true}. - * - * @param playAdBeforeStartPosition Whether to play an ad before the start position when - * beginning playback. - * @return This builder, for convenience. - */ - public Builder setPlayAdBeforeStartPosition(boolean playAdBeforeStartPosition) { - this.playAdBeforeStartPosition = playAdBeforeStartPosition; - return this; - } - - /** - * Sets whether to enable outputting verbose logs for the IMA extension and IMA SDK. The default - * value is {@code false}. This setting is intended for debugging only, and should not be - * enabled in production applications. - * - * @param debugModeEnabled Whether to enable outputting verbose logs for the IMA extension and - * IMA SDK. - * @return This builder, for convenience. - * @see ImaSdkSettings#setDebugMode(boolean) - */ - public Builder setDebugModeEnabled(boolean debugModeEnabled) { - this.debugModeEnabled = debugModeEnabled; - return this; - } - - @VisibleForTesting - /* package */ Builder setImaFactory(ImaUtil.ImaFactory imaFactory) { - this.imaFactory = checkNotNull(imaFactory); - return this; - } - - /** Returns a new {@link ImaAdsLoader}. */ - public ImaAdsLoader build() { - return new ImaAdsLoader( - context, - new ImaUtil.Configuration( - adPreloadTimeoutMs, - vastLoadTimeoutMs, - mediaLoadTimeoutMs, - focusSkipButtonWhenAvailable, - playAdBeforeStartPosition, - mediaBitrate, - enableContinuousPlayback, - adMediaMimeTypes, - adUiElements, - companionAdSlots, - adErrorListener, - adEventListener, - videoAdPlayerCallback, - imaSdkSettings, - debugModeEnabled), - imaFactory); - } - } - - private final ImaUtil.Configuration configuration; - private final Context context; - private final ImaUtil.ImaFactory imaFactory; - private final HashMap adTagLoaderByAdsId; - private final HashMap adTagLoaderByAdsMediaSource; - private final Timeline.Period period; - private final Timeline.Window window; - - private boolean wasSetPlayerCalled; - @Nullable private Player nextPlayer; - private List supportedMimeTypes; - @Nullable private Player player; - @Nullable private AdTagLoader currentAdTagLoader; - - private ImaAdsLoader( - Context context, ImaUtil.Configuration configuration, ImaUtil.ImaFactory imaFactory) { - this.context = context.getApplicationContext(); - this.configuration = configuration; - this.imaFactory = imaFactory; - supportedMimeTypes = ImmutableList.of(); - adTagLoaderByAdsId = new HashMap<>(); - adTagLoaderByAdsMediaSource = new HashMap<>(); - period = new Timeline.Period(); - window = new Timeline.Window(); - } - - /** - * Returns the underlying {@link com.google.ads.interactivemedia.v3.api.AdsLoader} wrapped by this - * instance, or {@code null} if ads have not been requested yet. - */ - @Nullable - public com.google.ads.interactivemedia.v3.api.AdsLoader getAdsLoader() { - return currentAdTagLoader != null ? currentAdTagLoader.getAdsLoader() : null; - } - - /** - * Returns the {@link AdDisplayContainer} used by this loader, or {@code null} if ads have not - * been requested yet. - * - *

Note: any video controls overlays registered via {@link - * AdDisplayContainer#registerFriendlyObstruction(FriendlyObstruction)} will be unregistered - * automatically when the media source detaches from this instance. It is therefore necessary to - * re-register views each time the ads loader is reused. Alternatively, provide overlay views via - * the {@link AdViewProvider} when creating the media source to benefit from automatic - * registration. - */ - @Nullable - public AdDisplayContainer getAdDisplayContainer() { - return currentAdTagLoader != null ? currentAdTagLoader.getAdDisplayContainer() : null; - } - - /** - * Requests ads, if they have not already been requested. Must be called on the main thread. - * - *

Ads will be requested automatically when the player is prepared if this method has not been - * called, so it is only necessary to call this method if you want to request ads before preparing - * the player. - * - * @param adTagDataSpec The data specification of the ad tag to load. See class javadoc for - * information about compatible ad tag formats. - * @param adsId A opaque identifier for the ad playback state across start/stop calls. - * @param adViewGroup A {@link ViewGroup} on top of the player that will show any ad UI, or {@code - * null} if playing audio-only ads. - */ - public void requestAds(DataSpec adTagDataSpec, Object adsId, @Nullable ViewGroup adViewGroup) { - if (!adTagLoaderByAdsId.containsKey(adsId)) { - AdTagLoader adTagLoader = - new AdTagLoader( - context, - configuration, - imaFactory, - supportedMimeTypes, - adTagDataSpec, - adsId, - adViewGroup); - adTagLoaderByAdsId.put(adsId, adTagLoader); - } - } - - /** - * Skips the current ad. - * - *

This method is intended for apps that play audio-only ads and so need to provide their own - * UI for users to skip skippable ads. Apps showing video ads should not call this method, as the - * IMA SDK provides the UI to skip ads in the ad view group passed via {@link AdViewProvider}. - */ - public void skipAd() { - if (currentAdTagLoader != null) { - currentAdTagLoader.skipAd(); - } - } - - /** - * Moves UI focus to the skip button (or other interactive elements), if currently shown. See - * {@link AdsManager#focus()}. - */ - public void focusSkipButton() { - if (currentAdTagLoader != null) { - currentAdTagLoader.focusSkipButton(); - } - } - - // AdsLoader implementation. - - @Override - public void setPlayer(@Nullable Player player) { - checkState(Looper.myLooper() == getImaLooper()); - checkState(player == null || player.getApplicationLooper() == getImaLooper()); - nextPlayer = player; - wasSetPlayerCalled = true; - } - - @Override - public void setSupportedContentTypes(@C.ContentType int... contentTypes) { - List supportedMimeTypes = new ArrayList<>(); - for (@C.ContentType int contentType : contentTypes) { - // IMA does not support Smooth Streaming ad media. - if (contentType == C.TYPE_DASH) { - supportedMimeTypes.add(MimeTypes.APPLICATION_MPD); - } else if (contentType == C.TYPE_HLS) { - supportedMimeTypes.add(MimeTypes.APPLICATION_M3U8); - } else if (contentType == C.TYPE_OTHER) { - supportedMimeTypes.addAll( - Arrays.asList( - MimeTypes.VIDEO_MP4, - MimeTypes.VIDEO_WEBM, - MimeTypes.VIDEO_H263, - MimeTypes.AUDIO_MP4, - MimeTypes.AUDIO_MPEG)); - } - } - this.supportedMimeTypes = Collections.unmodifiableList(supportedMimeTypes); - } - - @Override - public void start( - AdsMediaSource adsMediaSource, - DataSpec adTagDataSpec, - Object adsId, - AdViewProvider adViewProvider, - EventListener eventListener) { - checkState( - wasSetPlayerCalled, "Set player using adsLoader.setPlayer before preparing the player."); - if (adTagLoaderByAdsMediaSource.isEmpty()) { - player = nextPlayer; - @Nullable Player player = this.player; - if (player == null) { - return; - } - player.addListener(this); - } - - @Nullable AdTagLoader adTagLoader = adTagLoaderByAdsId.get(adsId); - if (adTagLoader == null) { - requestAds(adTagDataSpec, adsId, adViewProvider.getAdViewGroup()); - adTagLoader = adTagLoaderByAdsId.get(adsId); - } - adTagLoaderByAdsMediaSource.put(adsMediaSource, checkNotNull(adTagLoader)); - adTagLoader.addListenerWithAdView(eventListener, adViewProvider); - maybeUpdateCurrentAdTagLoader(); - } - - @Override - public void stop(AdsMediaSource adsMediaSource, EventListener eventListener) { - @Nullable AdTagLoader removedAdTagLoader = adTagLoaderByAdsMediaSource.remove(adsMediaSource); - maybeUpdateCurrentAdTagLoader(); - if (removedAdTagLoader != null) { - removedAdTagLoader.removeListener(eventListener); - } - - if (player != null && adTagLoaderByAdsMediaSource.isEmpty()) { - player.removeListener(this); - player = null; - } - } - - @Override - public void release() { - if (player != null) { - player.removeListener(this); - player = null; - maybeUpdateCurrentAdTagLoader(); - } - nextPlayer = null; - - for (AdTagLoader adTagLoader : adTagLoaderByAdsMediaSource.values()) { - adTagLoader.release(); - } - adTagLoaderByAdsMediaSource.clear(); - - for (AdTagLoader adTagLoader : adTagLoaderByAdsId.values()) { - adTagLoader.release(); - } - adTagLoaderByAdsId.clear(); - } - - @Override - public void handlePrepareComplete( - AdsMediaSource adsMediaSource, int adGroupIndex, int adIndexInAdGroup) { - if (player == null) { - return; - } - checkNotNull(adTagLoaderByAdsMediaSource.get(adsMediaSource)) - .handlePrepareComplete(adGroupIndex, adIndexInAdGroup); - } - - @Override - public void handlePrepareError( - AdsMediaSource adsMediaSource, - int adGroupIndex, - int adIndexInAdGroup, - IOException exception) { - if (player == null) { - return; - } - checkNotNull(adTagLoaderByAdsMediaSource.get(adsMediaSource)) - .handlePrepareError(adGroupIndex, adIndexInAdGroup, exception); - } - - // Player.Listener implementation. - - @Override - public void onTimelineChanged(Timeline timeline, @Player.TimelineChangeReason int reason) { - if (timeline.isEmpty()) { - // The player is being reset or contains no media. - return; - } - maybeUpdateCurrentAdTagLoader(); - maybePreloadNextPeriodAds(); - } - - @Override - public void onPositionDiscontinuity( - Player.PositionInfo oldPosition, - Player.PositionInfo newPosition, - @Player.DiscontinuityReason int reason) { - maybeUpdateCurrentAdTagLoader(); - maybePreloadNextPeriodAds(); - } - - @Override - public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) { - maybePreloadNextPeriodAds(); - } - - @Override - public void onRepeatModeChanged(@Player.RepeatMode int repeatMode) { - maybePreloadNextPeriodAds(); - } - - // Internal methods. - - private void maybeUpdateCurrentAdTagLoader() { - @Nullable AdTagLoader oldAdTagLoader = currentAdTagLoader; - @Nullable AdTagLoader newAdTagLoader = getCurrentAdTagLoader(); - if (!Util.areEqual(oldAdTagLoader, newAdTagLoader)) { - if (oldAdTagLoader != null) { - oldAdTagLoader.deactivate(); - } - currentAdTagLoader = newAdTagLoader; - if (newAdTagLoader != null) { - newAdTagLoader.activate(checkNotNull(player)); - } - } - } - - @Nullable - private AdTagLoader getCurrentAdTagLoader() { - @Nullable Player player = this.player; - if (player == null) { - return null; - } - Timeline timeline = player.getCurrentTimeline(); - if (timeline.isEmpty()) { - return null; - } - int periodIndex = player.getCurrentPeriodIndex(); - @Nullable Object adsId = timeline.getPeriod(periodIndex, period).getAdsId(); - if (adsId == null) { - return null; - } - @Nullable AdTagLoader adTagLoader = adTagLoaderByAdsId.get(adsId); - if (adTagLoader == null || !adTagLoaderByAdsMediaSource.containsValue(adTagLoader)) { - return null; - } - return adTagLoader; - } - - private void maybePreloadNextPeriodAds() { - @Nullable Player player = this.player; - if (player == null) { - return; - } - Timeline timeline = player.getCurrentTimeline(); - if (timeline.isEmpty()) { - return; - } - int nextPeriodIndex = - timeline.getNextPeriodIndex( - player.getCurrentPeriodIndex(), - period, - window, - player.getRepeatMode(), - player.getShuffleModeEnabled()); - if (nextPeriodIndex == C.INDEX_UNSET) { - return; - } - timeline.getPeriod(nextPeriodIndex, period); - @Nullable Object nextAdsId = period.getAdsId(); - if (nextAdsId == null) { - return; - } - @Nullable AdTagLoader nextAdTagLoader = adTagLoaderByAdsId.get(nextAdsId); - if (nextAdTagLoader == null || nextAdTagLoader == currentAdTagLoader) { - return; - } - long periodPositionUs = - timeline.getPeriodPosition( - window, period, period.windowIndex, /* windowPositionUs= */ C.TIME_UNSET) - .second; - nextAdTagLoader.maybePreloadAds(C.usToMs(periodPositionUs), C.usToMs(period.durationUs)); - } - - /** - * Default {@link ImaUtil.ImaFactory} for non-test usage, which delegates to {@link - * ImaSdkFactory}. - */ - private static final class DefaultImaFactory implements ImaUtil.ImaFactory { - @Override - public ImaSdkSettings createImaSdkSettings() { - ImaSdkSettings settings = ImaSdkFactory.getInstance().createImaSdkSettings(); - settings.setLanguage(Util.getSystemLanguageCodes()[0]); - return settings; - } - - @Override - public AdsRenderingSettings createAdsRenderingSettings() { - return ImaSdkFactory.getInstance().createAdsRenderingSettings(); - } - - @Override - public AdDisplayContainer createAdDisplayContainer(ViewGroup container, VideoAdPlayer player) { - return ImaSdkFactory.createAdDisplayContainer(container, player); - } - - @Override - public AdDisplayContainer createAudioAdDisplayContainer(Context context, VideoAdPlayer player) { - return ImaSdkFactory.createAudioAdDisplayContainer(context, player); - } - - // The reasonDetail parameter to createFriendlyObstruction is annotated @Nullable but the - // annotation is not kept in the obfuscated dependency. - @SuppressWarnings("nullness:argument.type.incompatible") - @Override - public FriendlyObstruction createFriendlyObstruction( - View view, - FriendlyObstructionPurpose friendlyObstructionPurpose, - @Nullable String reasonDetail) { - return ImaSdkFactory.getInstance() - .createFriendlyObstruction(view, friendlyObstructionPurpose, reasonDetail); - } - - @Override - public AdsRequest createAdsRequest() { - return ImaSdkFactory.getInstance().createAdsRequest(); - } - - @Override - public com.google.ads.interactivemedia.v3.api.AdsLoader createAdsLoader( - Context context, ImaSdkSettings imaSdkSettings, AdDisplayContainer adDisplayContainer) { - return ImaSdkFactory.getInstance() - .createAdsLoader(context, imaSdkSettings, adDisplayContainer); - } - } -} diff --git a/automatedtests/src/androidTestR2_18_1/java/com/google/android/exoplayer2/ext/ima/ImaUtil.java b/automatedtests/src/androidTestR2_18_1/java/com/google/android/exoplayer2/ext/ima/ImaUtil.java deleted file mode 100644 index 7094d24a..00000000 --- a/automatedtests/src/androidTestR2_18_1/java/com/google/android/exoplayer2/ext/ima/ImaUtil.java +++ /dev/null @@ -1,255 +0,0 @@ -/* - * Copyright (C) 2020 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.google.android.exoplayer2.ext.ima; - -import android.content.Context; -import android.os.Looper; -import android.view.View; -import android.view.ViewGroup; -import androidx.annotation.Nullable; -import com.google.ads.interactivemedia.v3.api.AdDisplayContainer; -import com.google.ads.interactivemedia.v3.api.AdError; -import com.google.ads.interactivemedia.v3.api.AdErrorEvent; -import com.google.ads.interactivemedia.v3.api.AdEvent; -import com.google.ads.interactivemedia.v3.api.AdsLoader; -import com.google.ads.interactivemedia.v3.api.AdsManager; -import com.google.ads.interactivemedia.v3.api.AdsRenderingSettings; -import com.google.ads.interactivemedia.v3.api.AdsRequest; -import com.google.ads.interactivemedia.v3.api.CompanionAdSlot; -import com.google.ads.interactivemedia.v3.api.FriendlyObstruction; -import com.google.ads.interactivemedia.v3.api.FriendlyObstructionPurpose; -import com.google.ads.interactivemedia.v3.api.ImaSdkSettings; -import com.google.ads.interactivemedia.v3.api.UiElement; -import com.google.ads.interactivemedia.v3.api.player.VideoAdPlayer; -import com.google.ads.interactivemedia.v3.api.player.VideoProgressUpdate; -import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.ui.AdOverlayInfo; -import com.google.android.exoplayer2.upstream.DataSchemeDataSource; -import com.google.android.exoplayer2.upstream.DataSource; -import com.google.android.exoplayer2.upstream.DataSpec; -import com.google.android.exoplayer2.util.Util; -import java.io.IOException; -import java.util.Arrays; -import java.util.Collection; -import java.util.List; -import java.util.Set; - -/** Utilities for working with IMA SDK and IMA extension data types. */ -/* package */ final class ImaUtil { - - /** Factory for objects provided by the IMA SDK. */ - public interface ImaFactory { - /** Creates {@link ImaSdkSettings} for configuring the IMA SDK. */ - ImaSdkSettings createImaSdkSettings(); - /** - * Creates {@link AdsRenderingSettings} for giving the {@link AdsManager} parameters that - * control rendering of ads. - */ - AdsRenderingSettings createAdsRenderingSettings(); - /** - * Creates an {@link AdDisplayContainer} to hold the player for video ads, a container for - * non-linear ads, and slots for companion ads. - */ - AdDisplayContainer createAdDisplayContainer(ViewGroup container, VideoAdPlayer player); - /** Creates an {@link AdDisplayContainer} to hold the player for audio ads. */ - AdDisplayContainer createAudioAdDisplayContainer(Context context, VideoAdPlayer player); - /** - * Creates a {@link FriendlyObstruction} to describe an obstruction considered "friendly" for - * viewability measurement purposes. - */ - FriendlyObstruction createFriendlyObstruction( - View view, - FriendlyObstructionPurpose friendlyObstructionPurpose, - @Nullable String reasonDetail); - /** Creates an {@link AdsRequest} to contain the data used to request ads. */ - AdsRequest createAdsRequest(); - /** Creates an {@link AdsLoader} for requesting ads using the specified settings. */ - AdsLoader createAdsLoader( - Context context, ImaSdkSettings imaSdkSettings, AdDisplayContainer adDisplayContainer); - } - - /** Stores configuration for ad loading and playback. */ - public static final class Configuration { - - public final long adPreloadTimeoutMs; - public final int vastLoadTimeoutMs; - public final int mediaLoadTimeoutMs; - public final boolean focusSkipButtonWhenAvailable; - public final boolean playAdBeforeStartPosition; - public final int mediaBitrate; - @Nullable public final Boolean enableContinuousPlayback; - @Nullable public final List adMediaMimeTypes; - @Nullable public final Set adUiElements; - @Nullable public final Collection companionAdSlots; - @Nullable public final AdErrorEvent.AdErrorListener applicationAdErrorListener; - @Nullable public final AdEvent.AdEventListener applicationAdEventListener; - @Nullable public final VideoAdPlayer.VideoAdPlayerCallback applicationVideoAdPlayerCallback; - @Nullable public final ImaSdkSettings imaSdkSettings; - public final boolean debugModeEnabled; - - public Configuration( - long adPreloadTimeoutMs, - int vastLoadTimeoutMs, - int mediaLoadTimeoutMs, - boolean focusSkipButtonWhenAvailable, - boolean playAdBeforeStartPosition, - int mediaBitrate, - @Nullable Boolean enableContinuousPlayback, - @Nullable List adMediaMimeTypes, - @Nullable Set adUiElements, - @Nullable Collection companionAdSlots, - @Nullable AdErrorEvent.AdErrorListener applicationAdErrorListener, - @Nullable AdEvent.AdEventListener applicationAdEventListener, - @Nullable VideoAdPlayer.VideoAdPlayerCallback applicationVideoAdPlayerCallback, - @Nullable ImaSdkSettings imaSdkSettings, - boolean debugModeEnabled) { - this.adPreloadTimeoutMs = adPreloadTimeoutMs; - this.vastLoadTimeoutMs = vastLoadTimeoutMs; - this.mediaLoadTimeoutMs = mediaLoadTimeoutMs; - this.focusSkipButtonWhenAvailable = focusSkipButtonWhenAvailable; - this.playAdBeforeStartPosition = playAdBeforeStartPosition; - this.mediaBitrate = mediaBitrate; - this.enableContinuousPlayback = enableContinuousPlayback; - this.adMediaMimeTypes = adMediaMimeTypes; - this.adUiElements = adUiElements; - this.companionAdSlots = companionAdSlots; - this.applicationAdErrorListener = applicationAdErrorListener; - this.applicationAdEventListener = applicationAdEventListener; - this.applicationVideoAdPlayerCallback = applicationVideoAdPlayerCallback; - this.imaSdkSettings = imaSdkSettings; - this.debugModeEnabled = debugModeEnabled; - } - } - - public static final int TIMEOUT_UNSET = -1; - public static final int BITRATE_UNSET = -1; - - /** - * Returns the IMA {@link FriendlyObstructionPurpose} corresponding to the given {@link - * AdOverlayInfo#purpose}. - */ - public static FriendlyObstructionPurpose getFriendlyObstructionPurpose( - @AdOverlayInfo.Purpose int purpose) { - switch (purpose) { - case AdOverlayInfo.PURPOSE_CONTROLS: - return FriendlyObstructionPurpose.VIDEO_CONTROLS; - case AdOverlayInfo.PURPOSE_CLOSE_AD: - return FriendlyObstructionPurpose.CLOSE_AD; - case AdOverlayInfo.PURPOSE_NOT_VISIBLE: - return FriendlyObstructionPurpose.NOT_VISIBLE; - case AdOverlayInfo.PURPOSE_OTHER: - default: - return FriendlyObstructionPurpose.OTHER; - } - } - - /** - * Returns the microsecond ad group timestamps corresponding to the specified cue points. - * - * @param cuePoints The cue points of the ads in seconds, provided by the IMA SDK. - * @return The corresponding microsecond ad group timestamps. - */ - public static long[] getAdGroupTimesUsForCuePoints(List cuePoints) { - if (cuePoints.isEmpty()) { - return new long[] {0L}; - } - - int count = cuePoints.size(); - long[] adGroupTimesUs = new long[count]; - int adGroupIndex = 0; - for (int i = 0; i < count; i++) { - double cuePoint = cuePoints.get(i); - if (cuePoint == -1.0) { - adGroupTimesUs[count - 1] = C.TIME_END_OF_SOURCE; - } else { - adGroupTimesUs[adGroupIndex++] = Math.round(C.MICROS_PER_SECOND * cuePoint); - } - } - // Cue points may be out of order, so sort them. - Arrays.sort(adGroupTimesUs, 0, adGroupIndex); - return adGroupTimesUs; - } - - /** Returns an {@link AdsRequest} based on the specified ad tag {@link DataSpec}. */ - public static AdsRequest getAdsRequestForAdTagDataSpec( - ImaFactory imaFactory, DataSpec adTagDataSpec) throws IOException { - AdsRequest request = imaFactory.createAdsRequest(); - if (DataSchemeDataSource.SCHEME_DATA.equals(adTagDataSpec.uri.getScheme())) { - DataSchemeDataSource dataSchemeDataSource = new DataSchemeDataSource(); - try { - dataSchemeDataSource.open(adTagDataSpec); - request.setAdsResponse(Util.fromUtf8Bytes(readToEnd(dataSchemeDataSource))); - } finally { - dataSchemeDataSource.close(); - } - } else { - request.setAdTagUrl(adTagDataSpec.uri.toString()); - } - return request; - } - - /** - * Reads data from the specified opened {@link DataSource} until it ends, and returns a byte array - * containing the read data. Stolen from last version of ExoPlayer's Util - * - * @param dataSource The source from which to read. - * @return The concatenation of all read data. - * @throws IOException If an error occurs reading from the source. - */ - private static byte[] readToEnd(DataSource dataSource) throws IOException { - byte[] data = new byte[1024]; - int position = 0; - int bytesRead = 0; - while (bytesRead != C.RESULT_END_OF_INPUT) { - if (position == data.length) { - data = Arrays.copyOf(data, data.length * 2); - } - bytesRead = dataSource.read(data, position, data.length - position); - if (bytesRead != C.RESULT_END_OF_INPUT) { - position += bytesRead; - } - } - return Arrays.copyOf(data, position); - } - - /** Returns whether the ad error indicates that an entire ad group failed to load. */ - public static boolean isAdGroupLoadError(AdError adError) { - // TODO: Find out what other errors need to be handled (if any), and whether each one relates to - // a single ad, ad group or the whole timeline. - return adError.getErrorCode() == AdError.AdErrorCode.VAST_LINEAR_ASSET_MISMATCH - || adError.getErrorCode() == AdError.AdErrorCode.UNKNOWN_ERROR; - } - - /** Returns the looper on which all IMA SDK interaction must occur. */ - public static Looper getImaLooper() { - // IMA SDK callbacks occur on the main thread. This method can be used to check that the player - // is using the same looper, to ensure all interaction with this class is on the main thread. - return Looper.getMainLooper(); - } - - /** Returns a human-readable representation of a video progress update. */ - public static String getStringForVideoProgressUpdate(VideoProgressUpdate videoProgressUpdate) { - if (VideoProgressUpdate.VIDEO_TIME_NOT_READY.equals(videoProgressUpdate)) { - return "not ready"; - } else { - return Util.formatInvariant( - "%d ms of %d ms", - videoProgressUpdate.getCurrentTimeMs(), videoProgressUpdate.getDurationMs()); - } - } - - private ImaUtil() {} -} diff --git a/automatedtests/src/androidTestR2_18_1/java/com/mux/stats/sdk/muxstats/automatedtests/ui/SimplePlayerTestActivity.java b/automatedtests/src/androidTestR2_18_1/java/com/mux/stats/sdk/muxstats/automatedtests/ui/SimplePlayerTestActivity.java index ea8ae60d..7ef60e29 100644 --- a/automatedtests/src/androidTestR2_18_1/java/com/mux/stats/sdk/muxstats/automatedtests/ui/SimplePlayerTestActivity.java +++ b/automatedtests/src/androidTestR2_18_1/java/com/mux/stats/sdk/muxstats/automatedtests/ui/SimplePlayerTestActivity.java @@ -34,7 +34,6 @@ import com.google.android.exoplayer2.util.Util; import com.mux.stats.sdk.muxstats.SimplePlayerBaseActivity; import com.mux.stats.sdk.muxstats.automatedtests.R; -import com.mux.stats.sdk.muxstats.SimplePlayerBaseActivity; public class SimplePlayerTestActivity extends SimplePlayerBaseActivity implements diff --git a/automatedtests/src/androidTestR2_13_1/java/com/mux/stats/sdk/muxstats/automatedtests/ui/SimplePlayerTestActivity.java b/automatedtests/src/androidTestR2_19_1/java/com/mux/stats/sdk/muxstats/automatedtests/ui/SimplePlayerTestActivity.java similarity index 89% rename from automatedtests/src/androidTestR2_13_1/java/com/mux/stats/sdk/muxstats/automatedtests/ui/SimplePlayerTestActivity.java rename to automatedtests/src/androidTestR2_19_1/java/com/mux/stats/sdk/muxstats/automatedtests/ui/SimplePlayerTestActivity.java index 9561844d..7ef60e29 100644 --- a/automatedtests/src/androidTestR2_13_1/java/com/mux/stats/sdk/muxstats/automatedtests/ui/SimplePlayerTestActivity.java +++ b/automatedtests/src/androidTestR2_19_1/java/com/mux/stats/sdk/muxstats/automatedtests/ui/SimplePlayerTestActivity.java @@ -8,12 +8,11 @@ import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.DefaultRenderersFactory; +import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.MediaItem; import com.google.android.exoplayer2.MediaItem.AdsConfiguration; -import com.google.android.exoplayer2.PlaybackPreparer; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.RenderersFactory; -import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.ext.ima.ImaAdsLoader; import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector; import com.google.android.exoplayer2.source.DefaultMediaSourceFactory; @@ -33,12 +32,12 @@ import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; import com.google.android.exoplayer2.util.Util; -import com.mux.stats.sdk.muxstats.automatedtests.R; import com.mux.stats.sdk.muxstats.SimplePlayerBaseActivity; +import com.mux.stats.sdk.muxstats.automatedtests.R; -public class SimplePlayerTestActivity extends SimplePlayerBaseActivity - implements Player.EventListener { +public class SimplePlayerTestActivity extends SimplePlayerBaseActivity implements + Player.Listener { MediaSourceFactory mediaSourceFactory; @@ -64,27 +63,32 @@ public void initExoPlayer() { trackSelector.setParameters(trackSelectorParameters); RenderersFactory renderersFactory = new DefaultRenderersFactory(/* context= */ this); player = - new SimpleExoPlayer.Builder(/* context= */ this, renderersFactory) + new ExoPlayer.Builder(/* context= */ this) + .setRenderersFactory(renderersFactory) .setMediaSourceFactory(mediaSourceFactory) .setTrackSelector(trackSelector) .build(); + playerView.setPlayer(player); player.addListener(this); } - // This is for background playback, set appropriate notification and etc public void initAudioSession() { - notificationManager = PlayerNotificationManager.createWithNotificationChannel( - getApplicationContext(), - PLAYBACK_CHANNEL_ID, - R.string.channel_name, - R.string.channel_description, - PLAYBACK_NOTIFICATION_ID, - new MDAdapter(), - new CustomNotificationListener() - ); - notificationManager.setUseNavigationActions(false); + notificationManager = new PlayerNotificationManager.Builder( + getApplicationContext(), + PLAYBACK_NOTIFICATION_ID, + PLAYBACK_CHANNEL_ID + ) + .setChannelNameResourceId(R.string.channel_name) + .setChannelDescriptionResourceId(R.string.channel_description) + .setMediaDescriptionAdapter(new MDAdapter()) + .setNotificationListener(new CustomNotificationListener()) + .build(); + notificationManager.setUseNextAction(true); + notificationManager.setUsePreviousAction(true); notificationManager.setUseStopAction(true); + notificationManager.setUseNextActionInCompactView(true); + notificationManager.setUsePreviousActionInCompactView(true); notificationManager.setPlayer(player); mediaSessionCompat = new MediaSessionCompat(this, "hello_world_media"); @@ -178,7 +182,7 @@ public void startPlayback() { } player.setPlayWhenReady(playWhenReady); - ((SimpleExoPlayer)player).setMediaSource(testMediaSource); + ((ExoPlayer)player).setMediaSource(testMediaSource); player.seekTo(playbackStartPosition); player.prepare(); } @@ -198,7 +202,6 @@ public void onNotificationPosted(int notificationId, Notification notification, Log.e(TAG, "onNotificationPosted"); } } - ////////////////////////////////////////////////////////////////////// ////// Player.EventListener ////////////////////////////////////////// @@ -231,4 +234,5 @@ public void onRepeatModeChanged(int repeatMode) { activityInitialized.signalAll(); activityLock.unlock(); } + } diff --git a/demo/build.gradle b/demo/build.gradle index fc7d6757..0847987d 100644 --- a/demo/build.gradle +++ b/demo/build.gradle @@ -60,7 +60,11 @@ android { } r2_18_1 { dimension 'exoplayer' - matchingFallbacks = ['from2_18toNow'] + matchingFallbacks = ['just2_18'] + } + r2_19_1 { + dimension 'exoplayer' + matchingFallbacks = ['just2_19'] } } variantFilter { // this app doesn't need a release build, so save some resources @@ -129,6 +133,14 @@ dependencies { r2_18_1Api 'org.checkerframework:checker-qual:3.33.0' r2_18_1Api 'com.google.android.exoplayer:exoplayer:2.18.1' + r2_19_1Api 'com.google.android.exoplayer:extension-cronet:2.19.1' + r2_19_1Api 'com.google.android.exoplayer:extension-ima:2.19.1' + r2_19_1Api 'com.google.android.exoplayer:exoplayer-common:2.19.1' + r2_19_1Api 'com.google.android.exoplayer:exoplayer-core:2.19.1' + r2_19_1Api 'com.google.android.exoplayer:exoplayer-ui:2.19.1' + r2_19_1Api 'org.checkerframework:checker-qual:3.33.0' + r2_19_1Api 'com.google.android.exoplayer:exoplayer:2.19.1' + implementation project(':MuxExoPlayer') implementation project(':ExoPlayerAdapter') } diff --git a/demo/src/r2_10_6/AndroidManifest.xml b/demo/src/r2_10_6/AndroidManifest.xml deleted file mode 100644 index 41181cc3..00000000 --- a/demo/src/r2_10_6/AndroidManifest.xml +++ /dev/null @@ -1,113 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/demo/src/r2_10_6/assets/media.exolist.json b/demo/src/r2_10_6/assets/media.exolist.json deleted file mode 100644 index bcb3ef4a..00000000 --- a/demo/src/r2_10_6/assets/media.exolist.json +++ /dev/null @@ -1,582 +0,0 @@ -[ - { - "name": "YouTube DASH", - "samples": [ - { - "name": "Google Glass (MP4,H264)", - "uri": "https://www.youtube.com/api/manifest/dash/id/bf5bb2419360daf1/source/youtube?as=fmp4_audio_clear,fmp4_sd_hd_clear&sparams=ip,ipbits,expire,source,id,as&ip=0.0.0.0&ipbits=0&expire=19000000000&signature=51AF5F39AB0CEC3E5497CD9C900EBFEAECCCB5C7.8506521BFC350652163895D4C26DEE124209AA9E&key=ik0", - "extension": "mpd" - }, - { - "name": "Google Play (MP4,H264)", - "uri": "https://www.youtube.com/api/manifest/dash/id/3aa39fa2cc27967f/source/youtube?as=fmp4_audio_clear,fmp4_sd_hd_clear&sparams=ip,ipbits,expire,source,id,as&ip=0.0.0.0&ipbits=0&expire=19000000000&signature=A2716F75795F5D2AF0E88962FFCD10DB79384F29.84308FF04844498CE6FBCE4731507882B8307798&key=ik0", - "extension": "mpd" - }, - { - "name": "Google Glass (WebM,VP9)", - "uri": "https://www.youtube.com/api/manifest/dash/id/bf5bb2419360daf1/source/youtube?as=fmp4_audio_clear,webm2_sd_hd_clear&sparams=ip,ipbits,expire,source,id,as&ip=0.0.0.0&ipbits=0&expire=19000000000&signature=249B04F79E984D7F86B4D8DB48AE6FAF41C17AB3.7B9F0EC0505E1566E59B8E488E9419F253DDF413&key=ik0", - "extension": "mpd" - }, - { - "name": "Google Play (WebM,VP9)", - "uri": "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)", - "samples": [ - { - "name": "WV: HDCP not specified", - "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" - }, - { - "name": "WV: 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=48fcc369939ac96c&provider=widevine_test" - }, - { - "name": "WV: HDCP 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=e06c39f1151da3df&provider=widevine_test" - }, - { - "name": "WV: Secure video path required (MP4,H264)", - "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" - }, - { - "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", - "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" - }, - { - "name": "WV: 30s license duration (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" - } - ] - }, - { - "name": "Widevine HDCP Capabilities Tests", - "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)", - "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)", - "uri": "https://storage.googleapis.com/wvmedia/clear/h264/tears/tears_uhd.mpd" - }, - { - "name": "WV: Secure SD & HD (cenc,MP4,H264)", - "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)", - "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)", - "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)", - "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)", - "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)", - "uri": "https://storage.googleapis.com/wvmedia/cbcs/h264/tears/tears_aes_cbcs_uhd.mpd", - "drm_scheme": "widevine", - "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test" - } - ] - }, - { - "name": "Widevine DASH: WebM,VP9", - "samples": [ - { - "name": "WV: Clear SD & HD (WebM,VP9)", - "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)", - "uri": "https://storage.googleapis.com/wvmedia/clear/vp9/tears/tears_uhd.mpd" - }, - { - "name": "WV: Secure Fullsample SD & HD (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?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)", - "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)", - "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)", - "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" - } - ] - }, - { - "name": "Widevine DASH: MP4,H265", - "samples": [ - { - "name": "WV: Clear SD & HD (MP4,H265)", - "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)", - "uri": "https://storage.googleapis.com/wvmedia/clear/hevc/tears/tears_uhd.mpd" - }, - { - "name": "WV: Secure SD & HD (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?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)", - "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": "SmoothStreaming", - "samples": [ - { - "name": "Super speed", - "uri": "https://playready.directtaps.net/smoothstreaming/SSWSS720H264/SuperSpeedway_720.ism/Manifest" - }, - { - "name": "Super speed (PlayReady)", - "uri": "https://playready.directtaps.net/smoothstreaming/SSWSS720H264PR/SuperSpeedway_720.ism/Manifest", - "drm_scheme": "playready" - } - ] - }, - { - "name": "HLS", - "samples": [ - { - "name": "Apple 4x3 basic stream", - "uri": "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_4x3/bipbop_4x3_variant.m3u8" - }, - { - "name": "Apple 16x9 basic stream", - "uri": "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_16x9/bipbop_16x9_variant.m3u8" - }, - { - "name": "Apple master playlist advanced (TS)", - "uri": "https://devstreaming-cdn.apple.com/videos/streaming/examples/img_bipbop_adv_example_ts/master.m3u8" - }, - { - "name": "Apple master playlist advanced (fMP4)", - "uri": "https://devstreaming-cdn.apple.com/videos/streaming/examples/img_bipbop_adv_example_fmp4/master.m3u8" - }, - { - "name": "Apple TS media playlist", - "uri": "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_4x3/gear1/prog_index.m3u8" - }, - { - "name": "Apple AAC media playlist", - "uri": "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_4x3/gear0/prog_index.m3u8" - } - ] - }, - { - "name": "Misc", - "samples": [ - { - "name": "Dizzy (MP4)", - "uri": "https://html5demos.com/assets/dizzy.mp4" - }, - { - "name": "Apple AAC 10s", - "uri": "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_4x3/gear0/fileSequence0.aac" - }, - { - "name": "Apple TS 10s", - "uri": "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_4x3/gear1/fileSequence0.ts" - }, - { - "name": "Android screens (Matroska)", - "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv" - }, - { - "name": "Screens 360P (WebM,VP9,No Audio)", - "uri": "https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/video-vp9-360.webm" - }, - { - "name": "Screens 480p (FMP4,H264,No Audio)", - "uri": "https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/video-avc-baseline-480.mp4" - }, - { - "name": "Screens 1080p (FMP4,H264, No Audio)", - "uri": "https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/video-137.mp4" - }, - { - "name": "Screens (FMP4,AAC Audio)", - "uri": "https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/audio-141.mp4" - }, - { - "name": "Google Play (MP3 Audio)", - "uri": "https://storage.googleapis.com/exoplayer-test-media-0/play.mp3" - }, - { - "name": "Google Play (Ogg/Vorbis Audio)", - "uri": "https://storage.googleapis.com/exoplayer-test-media-1/ogg/play.ogg" - }, - { - "name": "Big Buck Bunny (FLV Video)", - "uri": "https://vod.leasewebcdn.com/bbb.flv?ri=1024&rs=150&start=0" - } - ] - }, - { - "name": "Playlists", - "samples": [ - { - "name": "Cats -> Dogs", - "playlist": [ - { - "uri": "https://html5demos.com/assets/dizzy.mp4" - }, - { - "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv" - } - ] - }, - { - "name": "Audio -> Video -> Audio", - "playlist": [ - { - "uri": "https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/audio-141.mp4" - }, - { - "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv" - }, - { - "uri": "https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/audio-141.mp4" - } - ] - }, - { - "name": "Clear -> Enc -> Clear -> Enc -> Enc", - "drm_scheme": "widevine", - "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test", - "playlist": [ - { - "uri": "https://html5demos.com/assets/dizzy.mp4" - }, - { - "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_sd.mpd" - }, - { - "uri": "https://html5demos.com/assets/dizzy.mp4" - }, - { - "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_sd.mpd" - }, - { - "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_sd.mpd" - } - ] - } - ] - }, - { - "name": "IMA sample ad tags", - "samples": [ - { - "name": "Single inline linear", - "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv", - "ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/single_ad_samples&ciu_szs=300x250&impl=s&gdfp_req=1&env=vp&output=vast&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ct%3Dlinear&correlator=" - }, - { - "name": "Single skippable inline", - "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv", - "ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/single_ad_samples&ciu_szs=300x250&impl=s&gdfp_req=1&env=vp&output=vast&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ct%3Dskippablelinear&correlator=" - }, - { - "name": "Single redirect linear", - "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv", - "ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/single_ad_samples&ciu_szs=300x250&impl=s&gdfp_req=1&env=vp&output=vast&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ct%3Dredirectlinear&correlator=" - }, - { - "name": "Single redirect error", - "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv", - "ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/single_ad_samples&ciu_szs=300x250&impl=s&gdfp_req=1&env=vp&output=vast&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ct%3Dredirecterror&nofb=1&correlator=" - }, - { - "name": "Single redirect broken (fallback)", - "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv", - "ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/single_ad_samples&ciu_szs=300x250&impl=s&gdfp_req=1&env=vp&output=vast&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ct%3Dredirecterror&correlator=" - }, - { - "name": "VMAP pre-roll", - "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv", - "ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/ad_rule_samples&ciu_szs=300x250&ad_rule=1&impl=s&gdfp_req=1&env=vp&output=vmap&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ar%3Dpreonly&cmsid=496&vid=short_onecue&correlator=" - }, - { - "name": "VMAP pre-roll + bumper", - "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv", - "ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/ad_rule_samples&ciu_szs=300x250&ad_rule=1&impl=s&gdfp_req=1&env=vp&output=vmap&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ar%3Dpreonlybumper&cmsid=496&vid=short_onecue&correlator=" - }, - { - "name": "VMAP post-roll", - "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv", - "ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/ad_rule_samples&ciu_szs=300x250&ad_rule=1&impl=s&gdfp_req=1&env=vp&output=vmap&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ar%3Dpostonly&cmsid=496&vid=short_onecue&correlator=" - }, - { - "name": "VMAP post-roll + bumper", - "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv", - "ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/ad_rule_samples&ciu_szs=300x250&ad_rule=1&impl=s&gdfp_req=1&env=vp&output=vmap&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ar%3Dpostonlybumper&cmsid=496&vid=short_onecue&correlator=" - }, - { - "name": "VMAP pre-, mid- and post-rolls, single ads", - "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv", - "ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/ad_rule_samples&ciu_szs=300x250&ad_rule=1&impl=s&gdfp_req=1&env=vp&output=vmap&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ar%3Dpremidpost&cmsid=496&vid=short_onecue&correlator=" - }, - { - "name": "VMAP pre-roll single ad, mid-roll standard pod with 3 ads, post-roll single ad", - "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv", - "ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/ad_rule_samples&ciu_szs=300x250&ad_rule=1&impl=s&gdfp_req=1&env=vp&output=vmap&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ar%3Dpremidpostpod&cmsid=496&vid=short_onecue&correlator=" - }, - { - "name": "VMAP pre-roll single ad, mid-roll optimized pod with 3 ads, post-roll single ad", - "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv", - "ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/ad_rule_samples&ciu_szs=300x250&ad_rule=1&impl=s&gdfp_req=1&env=vp&output=vmap&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ar%3Dpremidpostoptimizedpod&cmsid=496&vid=short_onecue&correlator=" - }, - { - "name": "VMAP pre-roll single ad, mid-roll standard pod with 3 ads, post-roll single ad (bumpers around all ad breaks)", - "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv", - "ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/ad_rule_samples&ciu_szs=300x250&ad_rule=1&impl=s&gdfp_req=1&env=vp&output=vmap&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ar%3Dpremidpostpodbumper&cmsid=496&vid=short_onecue&correlator=" - }, - { - "name": "VMAP pre-roll single ad, mid-roll optimized pod with 3 ads, post-roll single ad (bumpers around all ad breaks)", - "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv", - "ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/ad_rule_samples&ciu_szs=300x250&ad_rule=1&impl=s&gdfp_req=1&env=vp&output=vmap&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ar%3Dpremidpostoptimizedpodbumper&cmsid=496&vid=short_onecue&correlator=" - }, - { - "name": "VMAP pre-roll single ad, mid-roll standard pods with 5 ads every 10 seconds for 1:40, post-roll single ad", - "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv", - "ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/ad_rule_samples&ciu_szs=300x250&ad_rule=1&impl=s&gdfp_req=1&env=vp&output=vmap&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ar%3Dpremidpostlongpod&cmsid=496&vid=short_tencue&correlator=" - }, - { - "name": "VMAP empty midroll", - "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv", - "ad_tag_uri": "https://vastsynthesizer.appspot.com/empty-midroll" - }, - { - "name": "VMAP full, empty, full midrolls", - "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv", - "ad_tag_uri": "https://vastsynthesizer.appspot.com/empty-midroll-2" - } - ] - }, - { - "name": "360", - "samples": [ - { - "name": "Congo (360 top-bottom stereo)", - "uri": "https://storage.googleapis.com/exoplayer-test-media-1/360/congo.mp4", - "spherical_stereo_mode": "top_bottom" - }, - { - "name": "Sphericalv2 (180 top-bottom stereo)", - "uri": "https://storage.googleapis.com/exoplayer-test-media-1/360/sphericalv2.mp4", - "spherical_stereo_mode": "top_bottom" - }, - { - "name": "Iceland (360 top-bottom stereo ts)", - "uri": "https://storage.googleapis.com/exoplayer-test-media-1/360/iceland0.ts", - "spherical_stereo_mode": "top_bottom" - } - ] - } -] diff --git a/demo/src/r2_10_6/java/com/google/android/exoplayer2/demo/DemoApplication.java b/demo/src/r2_10_6/java/com/google/android/exoplayer2/demo/DemoApplication.java deleted file mode 100644 index 3e3d4fd3..00000000 --- a/demo/src/r2_10_6/java/com/google/android/exoplayer2/demo/DemoApplication.java +++ /dev/null @@ -1,179 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.google.android.exoplayer2.demo; - -import android.app.Application; -import com.google.android.exoplayer2.DefaultRenderersFactory; -import com.google.android.exoplayer2.RenderersFactory; -import com.google.android.exoplayer2.database.DatabaseProvider; -import com.google.android.exoplayer2.database.ExoDatabaseProvider; -import com.google.android.exoplayer2.offline.ActionFileUpgradeUtil; -import com.google.android.exoplayer2.offline.DefaultDownloadIndex; -import com.google.android.exoplayer2.offline.DefaultDownloaderFactory; -import com.google.android.exoplayer2.offline.DownloadManager; -import com.google.android.exoplayer2.offline.DownloaderConstructorHelper; -import com.google.android.exoplayer2.upstream.DataSource; -import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; -import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory; -import com.google.android.exoplayer2.upstream.FileDataSourceFactory; -import com.google.android.exoplayer2.upstream.HttpDataSource; -import com.google.android.exoplayer2.upstream.cache.Cache; -import com.google.android.exoplayer2.upstream.cache.CacheDataSource; -import com.google.android.exoplayer2.upstream.cache.CacheDataSourceFactory; -import com.google.android.exoplayer2.upstream.cache.NoOpCacheEvictor; -import com.google.android.exoplayer2.upstream.cache.SimpleCache; -import com.google.android.exoplayer2.util.Log; -import com.google.android.exoplayer2.util.Util; -import java.io.File; -import java.io.IOException; - -/** - * Placeholder application to facilitate overriding Application methods for debugging and testing. - */ -public class DemoApplication extends Application { - - private static final String TAG = "DemoApplication"; - private static final String DOWNLOAD_ACTION_FILE = "actions"; - private static final String DOWNLOAD_TRACKER_ACTION_FILE = "tracked_actions"; - private static final String DOWNLOAD_CONTENT_DIRECTORY = "downloads"; - - protected String userAgent; - - private DatabaseProvider databaseProvider; - private File downloadDirectory; - private Cache downloadCache; - private DownloadManager downloadManager; - private DownloadTracker downloadTracker; - - @Override - public void onCreate() { - super.onCreate(); - userAgent = Util.getUserAgent(this, "ExoPlayerDemo"); - } - - /** - * Returns a {@link DataSource.Factory}. - */ - public DataSource.Factory buildDataSourceFactory() { - DefaultDataSourceFactory upstreamFactory = - new DefaultDataSourceFactory(this, buildHttpDataSourceFactory()); - return buildReadOnlyCacheDataSource(upstreamFactory, getDownloadCache()); - } - - /** - * Returns a {@link HttpDataSource.Factory}. - */ - public HttpDataSource.Factory buildHttpDataSourceFactory() { - return new DefaultHttpDataSourceFactory(userAgent); - } - - /** - * Returns whether extension renderers should be used. - */ - public boolean useExtensionRenderers() { - return "withExtensions".equals(BuildConfig.FLAVOR); - } - - public RenderersFactory buildRenderersFactory(boolean preferExtensionRenderer) { - @DefaultRenderersFactory.ExtensionRendererMode - int extensionRendererMode = - useExtensionRenderers() - ? (preferExtensionRenderer - ? DefaultRenderersFactory.EXTENSION_RENDERER_MODE_PREFER - : DefaultRenderersFactory.EXTENSION_RENDERER_MODE_ON) - : DefaultRenderersFactory.EXTENSION_RENDERER_MODE_OFF; - return new DefaultRenderersFactory(/* context= */ this) - .setExtensionRendererMode(extensionRendererMode); - } - - public DownloadManager getDownloadManager() { - initDownloadManager(); - return downloadManager; - } - - public DownloadTracker getDownloadTracker() { - initDownloadManager(); - return downloadTracker; - } - - protected synchronized Cache getDownloadCache() { - if (downloadCache == null) { - File downloadContentDirectory = new File(getDownloadDirectory(), DOWNLOAD_CONTENT_DIRECTORY); - downloadCache = - new SimpleCache(downloadContentDirectory, new NoOpCacheEvictor(), getDatabaseProvider()); - } - return downloadCache; - } - - private synchronized void initDownloadManager() { - if (downloadManager == null) { - DefaultDownloadIndex downloadIndex = new DefaultDownloadIndex(getDatabaseProvider()); - upgradeActionFile( - DOWNLOAD_ACTION_FILE, downloadIndex, /* addNewDownloadsAsCompleted= */ false); - upgradeActionFile( - DOWNLOAD_TRACKER_ACTION_FILE, downloadIndex, /* addNewDownloadsAsCompleted= */ true); - DownloaderConstructorHelper downloaderConstructorHelper = - new DownloaderConstructorHelper(getDownloadCache(), buildHttpDataSourceFactory()); - downloadManager = - new DownloadManager( - this, downloadIndex, new DefaultDownloaderFactory(downloaderConstructorHelper)); - downloadTracker = - new DownloadTracker(/* context= */ this, buildDataSourceFactory(), downloadManager); - } - } - - private void upgradeActionFile( - String fileName, DefaultDownloadIndex downloadIndex, boolean addNewDownloadsAsCompleted) { - try { - ActionFileUpgradeUtil.upgradeAndDelete( - new File(getDownloadDirectory(), fileName), - /* downloadIdProvider= */ null, - downloadIndex, - /* deleteOnFailure= */ true, - addNewDownloadsAsCompleted); - } catch (IOException e) { - Log.e(TAG, "Failed to upgrade action file: " + fileName, e); - } - } - - private DatabaseProvider getDatabaseProvider() { - if (databaseProvider == null) { - databaseProvider = new ExoDatabaseProvider(this); - } - return databaseProvider; - } - - private File getDownloadDirectory() { - if (downloadDirectory == null) { - downloadDirectory = getExternalFilesDir(null); - if (downloadDirectory == null) { - downloadDirectory = getFilesDir(); - } - } - return downloadDirectory; - } - - protected static CacheDataSourceFactory buildReadOnlyCacheDataSource( - DataSource.Factory upstreamFactory, Cache cache) { - return new CacheDataSourceFactory( - cache, - upstreamFactory, - new FileDataSourceFactory(), - /* cacheWriteDataSinkFactory= */ null, - CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR, - /* eventListener= */ null); - } -} diff --git a/demo/src/r2_10_6/java/com/google/android/exoplayer2/demo/DemoDownloadService.java b/demo/src/r2_10_6/java/com/google/android/exoplayer2/demo/DemoDownloadService.java deleted file mode 100644 index 50ff317d..00000000 --- a/demo/src/r2_10_6/java/com/google/android/exoplayer2/demo/DemoDownloadService.java +++ /dev/null @@ -1,93 +0,0 @@ -/* - * Copyright (C) 2017 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.google.android.exoplayer2.demo; - -import android.app.Notification; -import com.google.android.exoplayer2.offline.Download; -import com.google.android.exoplayer2.offline.DownloadManager; -import com.google.android.exoplayer2.offline.DownloadService; -import com.google.android.exoplayer2.scheduler.PlatformScheduler; -import com.google.android.exoplayer2.ui.DownloadNotificationHelper; -import com.google.android.exoplayer2.util.NotificationUtil; -import com.google.android.exoplayer2.util.Util; -import java.util.List; - -/** - * A service for downloading media. - */ -public class DemoDownloadService extends DownloadService { - - private static final String CHANNEL_ID = "download_channel"; - private static final int JOB_ID = 1; - private static final int FOREGROUND_NOTIFICATION_ID = 1; - - private static int nextNotificationId = FOREGROUND_NOTIFICATION_ID + 1; - - private DownloadNotificationHelper notificationHelper; - - public DemoDownloadService() { - super( - FOREGROUND_NOTIFICATION_ID, - DEFAULT_FOREGROUND_NOTIFICATION_UPDATE_INTERVAL, - CHANNEL_ID, - R.string.exo_download_notification_channel_name, - /* channelDescriptionResourceId= */ 0); - nextNotificationId = FOREGROUND_NOTIFICATION_ID + 1; - } - - @Override - public void onCreate() { - super.onCreate(); - notificationHelper = new DownloadNotificationHelper(this, CHANNEL_ID); - } - - @Override - protected DownloadManager getDownloadManager() { - return ((DemoApplication) getApplication()).getDownloadManager(); - } - - @Override - protected PlatformScheduler getScheduler() { - return Util.SDK_INT >= 21 ? new PlatformScheduler(this, JOB_ID) : null; - } - - @Override - protected Notification getForegroundNotification(List downloads) { - return notificationHelper.buildProgressNotification( - R.drawable.ic_download, /* contentIntent= */ null, /* message= */ null, downloads); - } - - @Override - protected void onDownloadChanged(Download download) { - Notification notification; - if (download.state == Download.STATE_COMPLETED) { - notification = - notificationHelper.buildDownloadCompletedNotification( - R.drawable.ic_download_done, - /* contentIntent= */ null, - Util.fromUtf8Bytes(download.request.data)); - } else if (download.state == Download.STATE_FAILED) { - notification = - notificationHelper.buildDownloadFailedNotification( - R.drawable.ic_download_done, - /* contentIntent= */ null, - Util.fromUtf8Bytes(download.request.data)); - } else { - return; - } - NotificationUtil.setNotification(this, nextNotificationId++, notification); - } -} diff --git a/demo/src/r2_10_6/java/com/google/android/exoplayer2/demo/DownloadTracker.java b/demo/src/r2_10_6/java/com/google/android/exoplayer2/demo/DownloadTracker.java deleted file mode 100644 index acb99f7b..00000000 --- a/demo/src/r2_10_6/java/com/google/android/exoplayer2/demo/DownloadTracker.java +++ /dev/null @@ -1,276 +0,0 @@ -/* - * Copyright (C) 2017 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.google.android.exoplayer2.demo; - -import android.content.Context; -import android.content.DialogInterface; -import android.net.Uri; -import android.widget.Toast; -import androidx.annotation.Nullable; -import androidx.fragment.app.FragmentManager; -import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.RenderersFactory; -import com.google.android.exoplayer2.offline.Download; -import com.google.android.exoplayer2.offline.DownloadCursor; -import com.google.android.exoplayer2.offline.DownloadHelper; -import com.google.android.exoplayer2.offline.DownloadIndex; -import com.google.android.exoplayer2.offline.DownloadManager; -import com.google.android.exoplayer2.offline.DownloadRequest; -import com.google.android.exoplayer2.offline.DownloadService; -import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo; -import com.google.android.exoplayer2.upstream.DataSource; -import com.google.android.exoplayer2.util.Log; -import com.google.android.exoplayer2.util.Util; -import java.io.IOException; -import java.util.HashMap; -import java.util.concurrent.CopyOnWriteArraySet; - -/** - * Tracks media that has been downloaded. - */ -public class DownloadTracker { - - /** - * Listens for changes in the tracked downloads. - */ - public interface Listener { - - /** - * Called when the tracked downloads changed. - */ - void onDownloadsChanged(); - } - - private static final String TAG = "DownloadTracker"; - - private final Context context; - private final DataSource.Factory dataSourceFactory; - private final CopyOnWriteArraySet listeners; - private final HashMap downloads; - private final DownloadIndex downloadIndex; - - @Nullable - private StartDownloadDialogHelper startDownloadDialogHelper; - - public DownloadTracker( - Context context, DataSource.Factory dataSourceFactory, DownloadManager downloadManager) { - this.context = context.getApplicationContext(); - this.dataSourceFactory = dataSourceFactory; - listeners = new CopyOnWriteArraySet<>(); - downloads = new HashMap<>(); - downloadIndex = downloadManager.getDownloadIndex(); - downloadManager.addListener(new DownloadManagerListener()); - loadDownloads(); - } - - public void addListener(Listener listener) { - listeners.add(listener); - } - - public void removeListener(Listener listener) { - listeners.remove(listener); - } - - public boolean isDownloaded(Uri uri) { - Download download = downloads.get(uri); - return download != null && download.state != Download.STATE_FAILED; - } - - @SuppressWarnings("unchecked") - public DownloadRequest getDownloadRequest(Uri uri) { - Download download = downloads.get(uri); - return download != null && download.state != Download.STATE_FAILED ? download.request : null; - } - - public void toggleDownload( - FragmentManager fragmentManager, - String name, - Uri uri, - String extension, - RenderersFactory renderersFactory) { - Download download = downloads.get(uri); - if (download != null) { - DownloadService.sendRemoveDownload( - context, DemoDownloadService.class, download.request.id, /* foreground= */ false); - } else { - if (startDownloadDialogHelper != null) { - startDownloadDialogHelper.release(); - } - startDownloadDialogHelper = - new StartDownloadDialogHelper( - fragmentManager, getDownloadHelper(uri, extension, renderersFactory), name); - } - } - - private void loadDownloads() { - try (DownloadCursor loadedDownloads = downloadIndex.getDownloads()) { - while (loadedDownloads.moveToNext()) { - Download download = loadedDownloads.getDownload(); - downloads.put(download.request.uri, download); - } - } catch (IOException e) { - Log.w(TAG, "Failed to query downloads", e); - } - } - - private DownloadHelper getDownloadHelper( - Uri uri, String extension, RenderersFactory renderersFactory) { - int type = Util.inferContentType(uri, extension); - switch (type) { - case C.TYPE_DASH: - return DownloadHelper.forDash(uri, dataSourceFactory, renderersFactory); - case C.TYPE_SS: - return DownloadHelper.forSmoothStreaming(uri, dataSourceFactory, renderersFactory); - case C.TYPE_HLS: - return DownloadHelper.forHls(uri, dataSourceFactory, renderersFactory); - case C.TYPE_OTHER: - return DownloadHelper.forProgressive(uri); - default: - throw new IllegalStateException("Unsupported type: " + type); - } - } - - private class DownloadManagerListener implements DownloadManager.Listener { - - @Override - public void onDownloadChanged(DownloadManager downloadManager, Download download) { - downloads.put(download.request.uri, download); - for (Listener listener : listeners) { - listener.onDownloadsChanged(); - } - } - - @Override - public void onDownloadRemoved(DownloadManager downloadManager, Download download) { - downloads.remove(download.request.uri); - for (Listener listener : listeners) { - listener.onDownloadsChanged(); - } - } - } - - private final class StartDownloadDialogHelper - implements DownloadHelper.Callback, - DialogInterface.OnClickListener, - DialogInterface.OnDismissListener { - - private final FragmentManager fragmentManager; - private final DownloadHelper downloadHelper; - private final String name; - - private TrackSelectionDialog trackSelectionDialog; - private MappedTrackInfo mappedTrackInfo; - - public StartDownloadDialogHelper( - FragmentManager fragmentManager, DownloadHelper downloadHelper, String name) { - this.fragmentManager = fragmentManager; - this.downloadHelper = downloadHelper; - this.name = name; - downloadHelper.prepare(this); - } - - public void release() { - downloadHelper.release(); - if (trackSelectionDialog != null) { - trackSelectionDialog.dismiss(); - } - } - - // DownloadHelper.Callback implementation. - - @Override - public void onPrepared(DownloadHelper helper) { - if (helper.getPeriodCount() == 0) { - Log.d(TAG, "No periods found. Downloading entire stream."); - startDownload(); - downloadHelper.release(); - return; - } - mappedTrackInfo = downloadHelper.getMappedTrackInfo(/* periodIndex= */ 0); - if (!TrackSelectionDialog.willHaveContent(mappedTrackInfo)) { - Log.d(TAG, "No dialog content. Downloading entire stream."); - startDownload(); - downloadHelper.release(); - return; - } - trackSelectionDialog = - TrackSelectionDialog.createForMappedTrackInfoAndParameters( - /* titleId= */ R.string.exo_download_description, - mappedTrackInfo, - /* initialParameters= */ DownloadHelper.DEFAULT_TRACK_SELECTOR_PARAMETERS, - /* allowAdaptiveSelections =*/ false, - /* allowMultipleOverrides= */ true, - /* onClickListener= */ this, - /* onDismissListener= */ this); - trackSelectionDialog.show(fragmentManager, /* tag= */ null); - } - - @Override - public void onPrepareError(DownloadHelper helper, IOException e) { - Toast.makeText( - context.getApplicationContext(), R.string.download_start_error, Toast.LENGTH_LONG) - .show(); - Log.e(TAG, "Failed to start download", e); - } - - // DialogInterface.OnClickListener implementation. - - @Override - public void onClick(DialogInterface dialog, int which) { - for (int periodIndex = 0; periodIndex < downloadHelper.getPeriodCount(); periodIndex++) { - downloadHelper.clearTrackSelections(periodIndex); - for (int i = 0; i < mappedTrackInfo.getRendererCount(); i++) { - if (!trackSelectionDialog.getIsDisabled(/* rendererIndex= */ i)) { - downloadHelper.addTrackSelectionForSingleRenderer( - periodIndex, - /* rendererIndex= */ i, - DownloadHelper.DEFAULT_TRACK_SELECTOR_PARAMETERS, - trackSelectionDialog.getOverrides(/* rendererIndex= */ i)); - } - } - } - DownloadRequest downloadRequest = buildDownloadRequest(); - if (downloadRequest.streamKeys.isEmpty()) { - // All tracks were deselected in the dialog. Don't start the download. - return; - } - startDownload(downloadRequest); - } - - // DialogInterface.OnDismissListener implementation. - - @Override - public void onDismiss(DialogInterface dialogInterface) { - trackSelectionDialog = null; - downloadHelper.release(); - } - - // Internal methods. - - private void startDownload() { - startDownload(buildDownloadRequest()); - } - - private void startDownload(DownloadRequest downloadRequest) { - DownloadService.sendAddDownload( - context, DemoDownloadService.class, downloadRequest, /* foreground= */ false); - } - - private DownloadRequest buildDownloadRequest() { - return downloadHelper.getDownloadRequest(Util.getUtf8Bytes(name)); - } - } -} diff --git a/demo/src/r2_10_6/java/com/google/android/exoplayer2/demo/PlayerActivity.java b/demo/src/r2_10_6/java/com/google/android/exoplayer2/demo/PlayerActivity.java deleted file mode 100644 index 25cd825e..00000000 --- a/demo/src/r2_10_6/java/com/google/android/exoplayer2/demo/PlayerActivity.java +++ /dev/null @@ -1,766 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.google.android.exoplayer2.demo; - -import android.content.Intent; -import android.content.pm.PackageManager; -import android.content.res.Configuration; -import android.graphics.Point; -import android.net.Uri; -import android.os.Build; -import android.os.Bundle; -import android.util.Pair; -import android.view.KeyEvent; -import android.view.View; -import android.view.View.OnClickListener; -import android.widget.Button; -import android.widget.LinearLayout; -import android.widget.TextView; -import android.widget.Toast; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.appcompat.app.AppCompatActivity; -import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.C.ContentType; -import com.google.android.exoplayer2.ExoPlaybackException; -import com.google.android.exoplayer2.ExoPlayerFactory; -import com.google.android.exoplayer2.PlaybackPreparer; -import com.google.android.exoplayer2.Player; -import com.google.android.exoplayer2.RenderersFactory; -import com.google.android.exoplayer2.SimpleExoPlayer; -import com.google.android.exoplayer2.drm.DefaultDrmSessionManager; -import com.google.android.exoplayer2.drm.FrameworkMediaCrypto; -import com.google.android.exoplayer2.drm.FrameworkMediaDrm; -import com.google.android.exoplayer2.drm.HttpMediaDrmCallback; -import com.google.android.exoplayer2.drm.UnsupportedDrmException; -import com.google.android.exoplayer2.ext.ima.ImaAdsLoader; -import com.google.android.exoplayer2.mediacodec.MediaCodecRenderer.DecoderInitializationException; -import com.google.android.exoplayer2.mediacodec.MediaCodecUtil.DecoderQueryException; -import com.google.android.exoplayer2.offline.DownloadHelper; -import com.google.android.exoplayer2.offline.DownloadRequest; -import com.google.android.exoplayer2.source.BehindLiveWindowException; -import com.google.android.exoplayer2.source.ConcatenatingMediaSource; -import com.google.android.exoplayer2.source.MediaSource; -import com.google.android.exoplayer2.source.ProgressiveMediaSource; -import com.google.android.exoplayer2.source.TrackGroupArray; -import com.google.android.exoplayer2.source.ads.AdsLoader; -import com.google.android.exoplayer2.source.ads.AdsMediaSource; -import com.google.android.exoplayer2.source.dash.DashMediaSource; -import com.google.android.exoplayer2.source.hls.HlsMediaSource; -import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource; -import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection; -import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; -import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo; -import com.google.android.exoplayer2.trackselection.RandomTrackSelection; -import com.google.android.exoplayer2.trackselection.TrackSelection; -import com.google.android.exoplayer2.trackselection.TrackSelectionArray; -import com.google.android.exoplayer2.ui.DebugTextViewHelper; -import com.google.android.exoplayer2.ui.PlayerControlView; -import com.google.android.exoplayer2.ui.PlayerView; -import com.google.android.exoplayer2.ui.spherical.SphericalSurfaceView; -import com.google.android.exoplayer2.upstream.DataSource; -import com.google.android.exoplayer2.upstream.HttpDataSource; -import com.google.android.exoplayer2.util.ErrorMessageProvider; -import com.google.android.exoplayer2.util.EventLogger; -import com.google.android.exoplayer2.util.Util; -import com.mux.stats.sdk.core.MuxSDKViewOrientation; -import com.mux.stats.sdk.core.model.CustomData; -import com.mux.stats.sdk.core.model.CustomerData; -import com.mux.stats.sdk.core.model.CustomerPlayerData; -import com.mux.stats.sdk.core.model.CustomerVideoData; -import com.mux.stats.sdk.muxstats.MuxStatsExoPlayer; -import java.lang.reflect.Constructor; -import java.net.CookieHandler; -import java.net.CookieManager; -import java.net.CookiePolicy; -import java.util.UUID; - -/** - * An activity that plays media using {@link SimpleExoPlayer}. - */ -public class PlayerActivity extends AppCompatActivity - implements OnClickListener, PlaybackPreparer, PlayerControlView.VisibilityListener { - - public static final String DRM_SCHEME_EXTRA = "drm_scheme"; - public static final String DRM_LICENSE_URL_EXTRA = "drm_license_url"; - public static final String DRM_KEY_REQUEST_PROPERTIES_EXTRA = "drm_key_request_properties"; - public static final String DRM_MULTI_SESSION_EXTRA = "drm_multi_session"; - public static final String PREFER_EXTENSION_DECODERS_EXTRA = "prefer_extension_decoders"; - - public static final String ACTION_VIEW = "com.google.android.exoplayer.demo.action.VIEW"; - public static final String EXTENSION_EXTRA = "extension"; - - public static final String ACTION_VIEW_LIST = - "com.google.android.exoplayer.demo.action.VIEW_LIST"; - public static final String URI_LIST_EXTRA = "uri_list"; - public static final String EXTENSION_LIST_EXTRA = "extension_list"; - - public static final String AD_TAG_URI_EXTRA = "ad_tag_uri"; - public static final String VIDEO_TITLE_EXTRA = "video_title"; - - public static final String ABR_ALGORITHM_EXTRA = "abr_algorithm"; - public static final String ABR_ALGORITHM_DEFAULT = "default"; - public static final String ABR_ALGORITHM_RANDOM = "random"; - - public static final String SPHERICAL_STEREO_MODE_EXTRA = "spherical_stereo_mode"; - public static final String SPHERICAL_STEREO_MODE_MONO = "mono"; - public static final String SPHERICAL_STEREO_MODE_TOP_BOTTOM = "top_bottom"; - public static final String SPHERICAL_STEREO_MODE_LEFT_RIGHT = "left_right"; - - // For backwards compatibility only. - private static final String DRM_SCHEME_UUID_EXTRA = "drm_scheme_uuid"; - - // Saved instance state keys. - private static final String KEY_TRACK_SELECTOR_PARAMETERS = "track_selector_parameters"; - private static final String KEY_WINDOW = "window"; - private static final String KEY_POSITION = "position"; - private static final String KEY_AUTO_PLAY = "auto_play"; - - private static final CookieManager DEFAULT_COOKIE_MANAGER; - - static { - DEFAULT_COOKIE_MANAGER = new CookieManager(); - DEFAULT_COOKIE_MANAGER.setCookiePolicy(CookiePolicy.ACCEPT_ORIGINAL_SERVER); - } - - private PlayerView playerView; - private LinearLayout debugRootView; - private Button selectTracksButton; - private TextView debugTextView; - private boolean isShowingTrackSelectionDialog; - - private DataSource.Factory dataSourceFactory; - private SimpleExoPlayer player; - private FrameworkMediaDrm mediaDrm; - private MediaSource mediaSource; - private DefaultTrackSelector trackSelector; - private DefaultTrackSelector.Parameters trackSelectorParameters; - private DebugTextViewHelper debugViewHelper; - private TrackGroupArray lastSeenTrackGroupArray; - - private boolean startAutoPlay; - private int startWindow; - private long startPosition; - - // Fields used only for ad playback. The ads loader is loaded via reflection. - - private AdsLoader adsLoader; - private Uri loadedAdTagUri; - - private MuxStatsExoPlayer muxStats; - - // Activity lifecycle - - @Override - public void onCreate(Bundle savedInstanceState) { - String sphericalStereoMode = getIntent().getStringExtra(SPHERICAL_STEREO_MODE_EXTRA); - if (sphericalStereoMode != null) { - setTheme(R.style.PlayerTheme_Spherical); - } - super.onCreate(savedInstanceState); - dataSourceFactory = buildDataSourceFactory(); - if (CookieHandler.getDefault() != DEFAULT_COOKIE_MANAGER) { - CookieHandler.setDefault(DEFAULT_COOKIE_MANAGER); - } - - setContentView(R.layout.player_activity); - debugRootView = findViewById(R.id.controls_root); - debugTextView = findViewById(R.id.debug_text_view); - selectTracksButton = findViewById(R.id.select_tracks_button); - selectTracksButton.setOnClickListener(this); - - playerView = findViewById(R.id.player_view); - playerView.setControllerVisibilityListener(this); - playerView.setErrorMessageProvider(new PlayerErrorMessageProvider()); - playerView.requestFocus(); - if (sphericalStereoMode != null) { - int stereoMode; - if (SPHERICAL_STEREO_MODE_MONO.equals(sphericalStereoMode)) { - stereoMode = C.STEREO_MODE_MONO; - } else if (SPHERICAL_STEREO_MODE_TOP_BOTTOM.equals(sphericalStereoMode)) { - stereoMode = C.STEREO_MODE_TOP_BOTTOM; - } else if (SPHERICAL_STEREO_MODE_LEFT_RIGHT.equals(sphericalStereoMode)) { - stereoMode = C.STEREO_MODE_LEFT_RIGHT; - } else { - showToast(R.string.error_unrecognized_stereo_mode); - finish(); - return; - } - ((SphericalSurfaceView) playerView.getVideoSurfaceView()).setDefaultStereoMode(stereoMode); - } - - if (savedInstanceState != null) { - trackSelectorParameters = savedInstanceState.getParcelable(KEY_TRACK_SELECTOR_PARAMETERS); - startAutoPlay = savedInstanceState.getBoolean(KEY_AUTO_PLAY); - startWindow = savedInstanceState.getInt(KEY_WINDOW); - startPosition = savedInstanceState.getLong(KEY_POSITION); - } else { - trackSelectorParameters = new DefaultTrackSelector.ParametersBuilder().build(); - clearStartPosition(); - } - } - - @Override - public void onConfigurationChanged(Configuration newConfig) { - super.onConfigurationChanged(newConfig); - - if (muxStats == null) { - return; - } - if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) { - muxStats.orientationChange(MuxSDKViewOrientation.LANDSCAPE); - } - if (newConfig.orientation == Configuration.ORIENTATION_PORTRAIT) { - muxStats.orientationChange(MuxSDKViewOrientation.PORTRAIT); - } - } - - @Override - public void onNewIntent(Intent intent) { - super.onNewIntent(intent); - releasePlayer(); - releaseAdsLoader(); - clearStartPosition(); - setIntent(intent); - } - - @Override - public void onStart() { - super.onStart(); - if (Util.SDK_INT > 23) { - initializePlayer(); - if (playerView != null) { - playerView.onResume(); - } - } - } - - @Override - public void onResume() { - super.onResume(); - if (Util.SDK_INT <= 23 || player == null) { - initializePlayer(); - if (playerView != null) { - playerView.onResume(); - } - } - } - - @Override - public void onPause() { - super.onPause(); - if (Util.SDK_INT <= 23) { - if (playerView != null) { - playerView.onPause(); - } - releasePlayer(); - } - } - - @Override - public void onStop() { - super.onStop(); - if (Util.SDK_INT > 23) { - if (playerView != null) { - playerView.onPause(); - } - releasePlayer(); - } - } - - @Override - public void onDestroy() { - super.onDestroy(); - releaseAdsLoader(); - } - - @Override - public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, - @NonNull int[] grantResults) { - if (grantResults.length == 0) { - // Empty results are triggered if a permission is requested while another request was already - // pending and can be safely ignored in this case. - return; - } - if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { - initializePlayer(); - } else { - showToast(R.string.storage_permission_denied); - finish(); - } - } - - @Override - public void onSaveInstanceState(Bundle outState) { - super.onSaveInstanceState(outState); - updateTrackSelectorParameters(); - updateStartPosition(); - outState.putParcelable(KEY_TRACK_SELECTOR_PARAMETERS, trackSelectorParameters); - outState.putBoolean(KEY_AUTO_PLAY, startAutoPlay); - outState.putInt(KEY_WINDOW, startWindow); - outState.putLong(KEY_POSITION, startPosition); - } - - // Activity input - - @Override - public boolean dispatchKeyEvent(KeyEvent event) { - // See whether the player view wants to handle media or DPAD keys events. - return playerView.dispatchKeyEvent(event) || super.dispatchKeyEvent(event); - } - - // OnClickListener methods - - @Override - public void onClick(View view) { - if (view == selectTracksButton - && !isShowingTrackSelectionDialog - && TrackSelectionDialog.willHaveContent(trackSelector)) { - isShowingTrackSelectionDialog = true; - TrackSelectionDialog trackSelectionDialog = - TrackSelectionDialog.createForTrackSelector( - trackSelector, - /* onDismissListener= */ dismissedDialog -> isShowingTrackSelectionDialog = false); - trackSelectionDialog.show(getSupportFragmentManager(), /* tag= */ null); - } - } - - // PlaybackControlView.PlaybackPreparer implementation - - @Override - public void preparePlayback() { - player.retry(); - } - - // PlaybackControlView.VisibilityListener implementation - - @Override - public void onVisibilityChange(int visibility) { - debugRootView.setVisibility(visibility); - } - - // Internal methods - - private void initializePlayer() { - if (player == null) { - Intent intent = getIntent(); - String action = intent.getAction(); - Uri[] uris; - String[] extensions; - if (ACTION_VIEW.equals(action)) { - uris = new Uri[]{intent.getData()}; - extensions = new String[]{intent.getStringExtra(EXTENSION_EXTRA)}; - } else if (ACTION_VIEW_LIST.equals(action)) { - String[] uriStrings = intent.getStringArrayExtra(URI_LIST_EXTRA); - uris = new Uri[uriStrings.length]; - for (int i = 0; i < uriStrings.length; i++) { - uris[i] = Uri.parse(uriStrings[i]); - } - extensions = intent.getStringArrayExtra(EXTENSION_LIST_EXTRA); - if (extensions == null) { - extensions = new String[uriStrings.length]; - } - } else { - showToast(getString(R.string.unexpected_intent_action, action)); - finish(); - return; - } - if (!Util.checkCleartextTrafficPermitted(uris)) { - showToast(R.string.error_cleartext_not_permitted); - return; - } - if (Util.maybeRequestReadExternalStoragePermission(/* activity= */ this, uris)) { - // The player will be reinitialized if the permission is granted. - return; - } - - DefaultDrmSessionManager drmSessionManager = null; - if (intent.hasExtra(DRM_SCHEME_EXTRA) || intent.hasExtra(DRM_SCHEME_UUID_EXTRA)) { - String drmLicenseUrl = intent.getStringExtra(DRM_LICENSE_URL_EXTRA); - String[] keyRequestPropertiesArray = - intent.getStringArrayExtra(DRM_KEY_REQUEST_PROPERTIES_EXTRA); - boolean multiSession = intent.getBooleanExtra(DRM_MULTI_SESSION_EXTRA, false); - int errorStringId = R.string.error_drm_unknown; - if (Util.SDK_INT < 18) { - errorStringId = R.string.error_drm_not_supported; - } else { - try { - String drmSchemeExtra = intent.hasExtra(DRM_SCHEME_EXTRA) ? DRM_SCHEME_EXTRA - : DRM_SCHEME_UUID_EXTRA; - UUID drmSchemeUuid = Util.getDrmUuid(intent.getStringExtra(drmSchemeExtra)); - if (drmSchemeUuid == null) { - errorStringId = R.string.error_drm_unsupported_scheme; - } else { - drmSessionManager = - buildDrmSessionManagerV18( - drmSchemeUuid, drmLicenseUrl, keyRequestPropertiesArray, multiSession); - } - } catch (UnsupportedDrmException e) { - errorStringId = e.reason == UnsupportedDrmException.REASON_UNSUPPORTED_SCHEME - ? R.string.error_drm_unsupported_scheme : R.string.error_drm_unknown; - } - } - if (drmSessionManager == null) { - showToast(errorStringId); - finish(); - return; - } - } - - TrackSelection.Factory trackSelectionFactory; - String abrAlgorithm = intent.getStringExtra(ABR_ALGORITHM_EXTRA); - if (abrAlgorithm == null || ABR_ALGORITHM_DEFAULT.equals(abrAlgorithm)) { - trackSelectionFactory = new AdaptiveTrackSelection.Factory(); - } else if (ABR_ALGORITHM_RANDOM.equals(abrAlgorithm)) { - trackSelectionFactory = new RandomTrackSelection.Factory(); - } else { - showToast(R.string.error_unrecognized_abr_algorithm); - finish(); - return; - } - - boolean preferExtensionDecoders = - intent.getBooleanExtra(PREFER_EXTENSION_DECODERS_EXTRA, false); - RenderersFactory renderersFactory = - ((DemoApplication) getApplication()).buildRenderersFactory(preferExtensionDecoders); - - trackSelector = new DefaultTrackSelector(trackSelectionFactory); - trackSelector.setParameters(trackSelectorParameters); - lastSeenTrackGroupArray = null; - - player = - ExoPlayerFactory.newSimpleInstance( - /* context= */ this, renderersFactory, trackSelector, drmSessionManager); - player.addListener(new PlayerEventListener()); - player.setPlayWhenReady(startAutoPlay); - player.addAnalyticsListener(new EventLogger(trackSelector)); - playerView.setPlayer(player); - playerView.setPlaybackPreparer(this); - debugViewHelper = new DebugTextViewHelper(player, debugTextView); - debugViewHelper.start(); - - CustomerPlayerData customerPlayerData = new CustomerPlayerData(); - customerPlayerData.setEnvironmentKey("YOUR_ENVIRONMENT_KEY_HERE"); - CustomerVideoData customerVideoData = new CustomerVideoData(); - customerVideoData.setVideoTitle(intent.getStringExtra(VIDEO_TITLE_EXTRA)); - CustomData customData = new CustomData(); - customData.setCustomData1("YOUR_CUSTOM_STRING_HERE"); - CustomerData customerData = new CustomerData(customerPlayerData, customerVideoData, null); - customerData.setCustomData(customData); - muxStats = new MuxStatsExoPlayer( - this, player, "demo-player", customerData); - Point size = new Point(); - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) { - getWindowManager().getDefaultDisplay().getSize(size); - } else { - getApplication().getApplicationContext().getDisplay().getSize(size); - } - getWindowManager().getDefaultDisplay().getSize(size); - muxStats.setScreenSize(size.x, size.y); - muxStats.setPlayerView(playerView); - muxStats.enableMuxCoreDebug(true, false); - - MediaSource[] mediaSources = new MediaSource[uris.length]; - for (int i = 0; i < uris.length; i++) { - mediaSources[i] = buildMediaSource(uris[i], extensions[i]); - } - mediaSource = - mediaSources.length == 1 ? mediaSources[0] : new ConcatenatingMediaSource(mediaSources); - String adTagUriString = intent.getStringExtra(AD_TAG_URI_EXTRA); - if (adTagUriString != null) { - Uri adTagUri = Uri.parse(adTagUriString); - if (!adTagUri.equals(loadedAdTagUri)) { - releaseAdsLoader(); - loadedAdTagUri = adTagUri; - } - MediaSource adsMediaSource = createAdsMediaSource(mediaSource, Uri.parse(adTagUriString)); - if (adsMediaSource != null) { - mediaSource = adsMediaSource; - } else { - showToast(R.string.ima_not_loaded); - } - } else { - releaseAdsLoader(); - } - } - boolean haveStartPosition = startWindow != C.INDEX_UNSET; - if (haveStartPosition) { - player.seekTo(startWindow, startPosition); - } - player.prepare(mediaSource, !haveStartPosition, false); - updateButtonVisibility(); - } - - private MediaSource buildMediaSource(Uri uri) { - return buildMediaSource(uri, null); - } - - private MediaSource buildMediaSource(Uri uri, @Nullable String overrideExtension) { - DownloadRequest downloadRequest = - ((DemoApplication) getApplication()).getDownloadTracker().getDownloadRequest(uri); - if (downloadRequest != null) { - return DownloadHelper.createMediaSource(downloadRequest, dataSourceFactory); - } - @ContentType int type = Util.inferContentType(uri, overrideExtension); - switch (type) { - case C.TYPE_DASH: - return new DashMediaSource.Factory(dataSourceFactory).createMediaSource(uri); - case C.TYPE_SS: - return new SsMediaSource.Factory(dataSourceFactory).createMediaSource(uri); - case C.TYPE_HLS: - return new HlsMediaSource.Factory(dataSourceFactory).createMediaSource(uri); - case C.TYPE_OTHER: - return new ProgressiveMediaSource.Factory(dataSourceFactory).createMediaSource(uri); - default: - throw new IllegalStateException("Unsupported type: " + type); - } - } - - private DefaultDrmSessionManager buildDrmSessionManagerV18( - UUID uuid, String licenseUrl, String[] keyRequestPropertiesArray, boolean multiSession) - throws UnsupportedDrmException { - HttpDataSource.Factory licenseDataSourceFactory = - ((DemoApplication) getApplication()).buildHttpDataSourceFactory(); - HttpMediaDrmCallback drmCallback = - new HttpMediaDrmCallback(licenseUrl, licenseDataSourceFactory); - if (keyRequestPropertiesArray != null) { - for (int i = 0; i < keyRequestPropertiesArray.length - 1; i += 2) { - drmCallback.setKeyRequestProperty(keyRequestPropertiesArray[i], - keyRequestPropertiesArray[i + 1]); - } - } - releaseMediaDrm(); - mediaDrm = FrameworkMediaDrm.newInstance(uuid); - return new DefaultDrmSessionManager<>(uuid, mediaDrm, drmCallback, null, multiSession); - } - - private void releasePlayer() { - if (player != null) { - updateTrackSelectorParameters(); - updateStartPosition(); - debugViewHelper.stop(); - debugViewHelper = null; - muxStats.release(); - player.release(); - player = null; - mediaSource = null; - trackSelector = null; - } - if (adsLoader != null) { - adsLoader.setPlayer(null); - } - releaseMediaDrm(); - } - - private void releaseMediaDrm() { - if (mediaDrm != null) { - mediaDrm.release(); - mediaDrm = null; - } - } - - private void releaseAdsLoader() { - if (adsLoader != null) { - adsLoader.release(); - adsLoader = null; - loadedAdTagUri = null; - playerView.getOverlayFrameLayout().removeAllViews(); - } - } - - private void updateTrackSelectorParameters() { - if (trackSelector != null) { - trackSelectorParameters = trackSelector.getParameters(); - } - } - - private void updateStartPosition() { - if (player != null) { - startAutoPlay = player.getPlayWhenReady(); - startWindow = player.getCurrentWindowIndex(); - startPosition = Math.max(0, player.getContentPosition()); - } - } - - private void clearStartPosition() { - startAutoPlay = true; - startWindow = C.INDEX_UNSET; - startPosition = C.TIME_UNSET; - } - - /** - * Returns a new DataSource factory. - */ - private DataSource.Factory buildDataSourceFactory() { - return ((DemoApplication) getApplication()).buildDataSourceFactory(); - } - - /** - * Returns an ads media source, reusing the ads loader if one exists. - */ - private @Nullable - MediaSource createAdsMediaSource(MediaSource mediaSource, Uri adTagUri) { - // Load the extension source using reflection so the demo app doesn't have to depend on it. - // The ads loader is reused for multiple playbacks, so that ad playback can resume. - try { - Class loaderClass = Class.forName("com.google.android.exoplayer2.ext.ima.ImaAdsLoader"); - if (adsLoader == null) { - // Full class names used so the LINT.IfChange rule triggers should any of the classes move. - // LINT.IfChange - Constructor loaderConstructor = - loaderClass - .asSubclass(AdsLoader.class) - .getConstructor(android.content.Context.class, android.net.Uri.class); - // LINT.ThenChange(../../../../../../../../proguard-rules.txt) - adsLoader = loaderConstructor.newInstance(this, adTagUri); - } - adsLoader.setPlayer(player); - AdsMediaSource.MediaSourceFactory adMediaSourceFactory = - new AdsMediaSource.MediaSourceFactory() { - @Override - public MediaSource createMediaSource(Uri uri) { - return PlayerActivity.this.buildMediaSource(uri); - } - - @Override - public int[] getSupportedTypes() { - return new int[]{C.TYPE_DASH, C.TYPE_SS, C.TYPE_HLS, C.TYPE_OTHER}; - } - }; - - // Because of how this is loaded via reflection, we know that this will be - // a ImaAdsLoader, so cast it over so that we can get a reference to the - // real IMA AdsLoader instance. - muxStats.monitorImaAdsLoader(((ImaAdsLoader) adsLoader).getAdsLoader()); - return new AdsMediaSource(mediaSource, adMediaSourceFactory, adsLoader, playerView); - } catch (ClassNotFoundException e) { - // IMA extension not loaded. - return null; - } catch (Exception e) { - throw new RuntimeException(e); - } - } - - // User controls - - private void updateButtonVisibility() { - selectTracksButton.setEnabled( - player != null && TrackSelectionDialog.willHaveContent(trackSelector)); - } - - private void showControls() { - debugRootView.setVisibility(View.VISIBLE); - } - - private void showToast(int messageId) { - showToast(getString(messageId)); - } - - private void showToast(String message) { - Toast.makeText(getApplicationContext(), message, Toast.LENGTH_LONG).show(); - } - - private static boolean isBehindLiveWindow(ExoPlaybackException e) { - if (e.type != ExoPlaybackException.TYPE_SOURCE) { - return false; - } - Throwable cause = e.getSourceException(); - while (cause != null) { - if (cause instanceof BehindLiveWindowException) { - return true; - } - cause = cause.getCause(); - } - return false; - } - - private class PlayerEventListener implements Player.EventListener { - - @Override - public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { - if (playbackState == Player.STATE_ENDED) { - showControls(); - } - updateButtonVisibility(); - } - - @Override - public void onPlayerError(ExoPlaybackException e) { - if (isBehindLiveWindow(e)) { - clearStartPosition(); - initializePlayer(); - } else { - updateButtonVisibility(); - showControls(); - } - } - - @Override - @SuppressWarnings("ReferenceEquality") - public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) { - updateButtonVisibility(); - if (trackGroups != lastSeenTrackGroupArray) { - MappedTrackInfo mappedTrackInfo = trackSelector.getCurrentMappedTrackInfo(); - if (mappedTrackInfo != null) { - if (mappedTrackInfo.getTypeSupport(C.TRACK_TYPE_VIDEO) - == MappedTrackInfo.RENDERER_SUPPORT_UNSUPPORTED_TRACKS) { - showToast(R.string.error_unsupported_video); - } - if (mappedTrackInfo.getTypeSupport(C.TRACK_TYPE_AUDIO) - == MappedTrackInfo.RENDERER_SUPPORT_UNSUPPORTED_TRACKS) { - showToast(R.string.error_unsupported_audio); - } - } - lastSeenTrackGroupArray = trackGroups; - } - } - } - - private class PlayerErrorMessageProvider implements ErrorMessageProvider { - - @Override - public Pair getErrorMessage(ExoPlaybackException e) { - String errorString = getString(R.string.error_generic); - if (e.type == ExoPlaybackException.TYPE_RENDERER) { - Exception cause = e.getRendererException(); - if (cause instanceof DecoderInitializationException) { - // Special case for decoder initialization failures. - DecoderInitializationException decoderInitializationException = - (DecoderInitializationException) cause; - if (decoderInitializationException.decoderName == null) { - if (decoderInitializationException.getCause() instanceof DecoderQueryException) { - errorString = getString(R.string.error_querying_decoders); - } else if (decoderInitializationException.secureDecoderRequired) { - errorString = - getString( - R.string.error_no_secure_decoder, decoderInitializationException.mimeType); - } else { - errorString = - getString(R.string.error_no_decoder, decoderInitializationException.mimeType); - } - } else { - errorString = - getString( - R.string.error_instantiating_decoder, - decoderInitializationException.decoderName); - } - } - } - return Pair.create(0, errorString); - } - } - -} diff --git a/demo/src/r2_10_6/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java b/demo/src/r2_10_6/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java deleted file mode 100644 index c2197101..00000000 --- a/demo/src/r2_10_6/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java +++ /dev/null @@ -1,616 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.google.android.exoplayer2.demo; - -import android.content.Context; -import android.content.Intent; -import android.content.res.AssetManager; -import android.net.Uri; -import android.os.AsyncTask; -import android.os.Bundle; -import android.util.JsonReader; -import android.view.Menu; -import android.view.MenuInflater; -import android.view.MenuItem; -import android.view.View; -import android.view.View.OnClickListener; -import android.view.ViewGroup; -import android.widget.BaseExpandableListAdapter; -import android.widget.ExpandableListView; -import android.widget.ExpandableListView.OnChildClickListener; -import android.widget.ImageButton; -import android.widget.TextView; -import android.widget.Toast; -import androidx.annotation.Nullable; -import androidx.appcompat.app.AppCompatActivity; -import com.google.android.exoplayer2.ParserException; -import com.google.android.exoplayer2.RenderersFactory; -import com.google.android.exoplayer2.offline.DownloadService; -import com.google.android.exoplayer2.upstream.DataSource; -import com.google.android.exoplayer2.upstream.DataSourceInputStream; -import com.google.android.exoplayer2.upstream.DataSpec; -import com.google.android.exoplayer2.upstream.DefaultDataSource; -import com.google.android.exoplayer2.util.Assertions; -import com.google.android.exoplayer2.util.Log; -import com.google.android.exoplayer2.util.Util; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; - -/** - * An activity for selecting from a list of media samples. - */ -public class SampleChooserActivity extends AppCompatActivity - implements DownloadTracker.Listener, OnChildClickListener { - - private static final String TAG = "SampleChooserActivity"; - - private boolean useExtensionRenderers; - private DownloadTracker downloadTracker; - private SampleAdapter sampleAdapter; - private MenuItem preferExtensionDecodersMenuItem; - private MenuItem randomAbrMenuItem; - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.sample_chooser_activity); - sampleAdapter = new SampleAdapter(); - ExpandableListView sampleListView = findViewById(R.id.sample_list); - sampleListView.setAdapter(sampleAdapter); - sampleListView.setOnChildClickListener(this); - - Intent intent = getIntent(); - String dataUri = intent.getDataString(); - String[] uris; - if (dataUri != null) { - uris = new String[]{dataUri}; - } else { - ArrayList uriList = new ArrayList<>(); - AssetManager assetManager = getAssets(); - try { - for (String asset : assetManager.list("")) { - if (asset.endsWith(".exolist.json")) { - uriList.add("asset:///" + asset); - } - } - } catch (IOException e) { - Toast.makeText(getApplicationContext(), R.string.sample_list_load_error, Toast.LENGTH_LONG) - .show(); - } - uris = new String[uriList.size()]; - uriList.toArray(uris); - Arrays.sort(uris); - } - - DemoApplication application = (DemoApplication) getApplication(); - useExtensionRenderers = application.useExtensionRenderers(); - downloadTracker = application.getDownloadTracker(); - SampleListLoader loaderTask = new SampleListLoader(); - loaderTask.execute(uris); - - // Start the download service if it should be running but it's not currently. - // Starting the service in the foreground causes notification flicker if there is no scheduled - // action. Starting it in the background throws an exception if the app is in the background too - // (e.g. if device screen is locked). - try { - DownloadService.start(this, DemoDownloadService.class); - } catch (IllegalStateException e) { - DownloadService.startForeground(this, DemoDownloadService.class); - } - } - - @Override - public boolean onCreateOptionsMenu(Menu menu) { - MenuInflater inflater = getMenuInflater(); - inflater.inflate(R.menu.sample_chooser_menu, menu); - preferExtensionDecodersMenuItem = menu.findItem(R.id.prefer_extension_decoders); - preferExtensionDecodersMenuItem.setVisible(useExtensionRenderers); - randomAbrMenuItem = menu.findItem(R.id.random_abr); - return true; - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - item.setChecked(!item.isChecked()); - return true; - } - - @Override - public void onStart() { - super.onStart(); - downloadTracker.addListener(this); - sampleAdapter.notifyDataSetChanged(); - } - - @Override - public void onStop() { - downloadTracker.removeListener(this); - super.onStop(); - } - - @Override - public void onDownloadsChanged() { - sampleAdapter.notifyDataSetChanged(); - } - - private void onSampleGroups(final List groups, boolean sawError) { - if (sawError) { - Toast.makeText(getApplicationContext(), R.string.sample_list_load_error, Toast.LENGTH_LONG) - .show(); - } - sampleAdapter.setSampleGroups(groups); - } - - @Override - public boolean onChildClick( - ExpandableListView parent, View view, int groupPosition, int childPosition, long id) { - Sample sample = (Sample) view.getTag(); - startActivity( - sample.buildIntent( - /* context= */ this, - isNonNullAndChecked(preferExtensionDecodersMenuItem), - isNonNullAndChecked(randomAbrMenuItem) - ? PlayerActivity.ABR_ALGORITHM_RANDOM - : PlayerActivity.ABR_ALGORITHM_DEFAULT)); - return true; - } - - private void onSampleDownloadButtonClicked(Sample sample) { - int downloadUnsupportedStringId = getDownloadUnsupportedStringId(sample); - if (downloadUnsupportedStringId != 0) { - Toast.makeText(getApplicationContext(), downloadUnsupportedStringId, Toast.LENGTH_LONG) - .show(); - } else { - UriSample uriSample = (UriSample) sample; - RenderersFactory renderersFactory = - ((DemoApplication) getApplication()) - .buildRenderersFactory(isNonNullAndChecked(preferExtensionDecodersMenuItem)); - downloadTracker.toggleDownload( - getSupportFragmentManager(), - sample.name, - uriSample.uri, - uriSample.extension, - renderersFactory); - } - } - - private int getDownloadUnsupportedStringId(Sample sample) { - if (sample instanceof PlaylistSample) { - return R.string.download_playlist_unsupported; - } - UriSample uriSample = (UriSample) sample; - if (uriSample.drmInfo != null) { - return R.string.download_drm_unsupported; - } - if (uriSample.adTagUri != null) { - return R.string.download_ads_unsupported; - } - String scheme = uriSample.uri.getScheme(); - if (!("http".equals(scheme) || "https".equals(scheme))) { - return R.string.download_scheme_unsupported; - } - return 0; - } - - private static boolean isNonNullAndChecked(@Nullable MenuItem menuItem) { - // Temporary workaround for layouts that do not inflate the options menu. - return menuItem != null && menuItem.isChecked(); - } - - private final class SampleListLoader extends AsyncTask> { - - private boolean sawError; - - @Override - protected List doInBackground(String... uris) { - List result = new ArrayList<>(); - Context context = getApplicationContext(); - String userAgent = Util.getUserAgent(context, "ExoPlayerDemo"); - DataSource dataSource = - new DefaultDataSource(context, userAgent, /* allowCrossProtocolRedirects= */ false); - for (String uri : uris) { - DataSpec dataSpec = new DataSpec(Uri.parse(uri)); - InputStream inputStream = new DataSourceInputStream(dataSource, dataSpec); - try { - readSampleGroups(new JsonReader(new InputStreamReader(inputStream, "UTF-8")), result); - } catch (Exception e) { - Log.e(TAG, "Error loading sample list: " + uri, e); - sawError = true; - } finally { - Util.closeQuietly(dataSource); - } - } - return result; - } - - @Override - protected void onPostExecute(List result) { - onSampleGroups(result, sawError); - } - - private void readSampleGroups(JsonReader reader, List groups) throws IOException { - reader.beginArray(); - while (reader.hasNext()) { - readSampleGroup(reader, groups); - } - reader.endArray(); - } - - private void readSampleGroup(JsonReader reader, List groups) throws IOException { - String groupName = ""; - ArrayList samples = new ArrayList<>(); - - reader.beginObject(); - while (reader.hasNext()) { - String name = reader.nextName(); - switch (name) { - case "name": - groupName = reader.nextString(); - break; - case "samples": - reader.beginArray(); - while (reader.hasNext()) { - samples.add(readEntry(reader, false)); - } - reader.endArray(); - break; - case "_comment": - reader.nextString(); // Ignore. - break; - default: - throw new ParserException("Unsupported name: " + name); - } - } - reader.endObject(); - - SampleGroup group = getGroup(groupName, groups); - group.samples.addAll(samples); - } - - private Sample readEntry(JsonReader reader, boolean insidePlaylist) throws IOException { - String sampleName = null; - Uri uri = null; - String extension = null; - String drmScheme = null; - String drmLicenseUrl = null; - String[] drmKeyRequestProperties = null; - boolean drmMultiSession = false; - ArrayList playlistSamples = null; - String adTagUri = null; - String sphericalStereoMode = null; - - reader.beginObject(); - while (reader.hasNext()) { - String name = reader.nextName(); - switch (name) { - case "name": - sampleName = reader.nextString(); - break; - case "uri": - uri = Uri.parse(reader.nextString()); - break; - case "extension": - extension = reader.nextString(); - break; - case "drm_scheme": - Assertions.checkState(!insidePlaylist, "Invalid attribute on nested item: drm_scheme"); - drmScheme = reader.nextString(); - break; - case "drm_license_url": - Assertions.checkState(!insidePlaylist, - "Invalid attribute on nested item: drm_license_url"); - drmLicenseUrl = reader.nextString(); - break; - case "drm_key_request_properties": - Assertions.checkState(!insidePlaylist, - "Invalid attribute on nested item: drm_key_request_properties"); - ArrayList drmKeyRequestPropertiesList = new ArrayList<>(); - reader.beginObject(); - while (reader.hasNext()) { - drmKeyRequestPropertiesList.add(reader.nextName()); - drmKeyRequestPropertiesList.add(reader.nextString()); - } - reader.endObject(); - drmKeyRequestProperties = drmKeyRequestPropertiesList.toArray(new String[0]); - break; - case "drm_multi_session": - drmMultiSession = reader.nextBoolean(); - break; - case "playlist": - Assertions.checkState(!insidePlaylist, "Invalid nesting of playlists"); - playlistSamples = new ArrayList<>(); - reader.beginArray(); - while (reader.hasNext()) { - playlistSamples.add((UriSample) readEntry(reader, true)); - } - reader.endArray(); - break; - case "ad_tag_uri": - adTagUri = reader.nextString(); - break; - case "spherical_stereo_mode": - Assertions.checkState( - !insidePlaylist, "Invalid attribute on nested item: spherical_stereo_mode"); - sphericalStereoMode = reader.nextString(); - break; - default: - throw new ParserException("Unsupported attribute name: " + name); - } - } - reader.endObject(); - DrmInfo drmInfo = - drmScheme == null - ? null - : new DrmInfo(drmScheme, drmLicenseUrl, drmKeyRequestProperties, drmMultiSession); - if (playlistSamples != null) { - UriSample[] playlistSamplesArray = playlistSamples.toArray(new UriSample[0]); - return new PlaylistSample(sampleName, drmInfo, playlistSamplesArray); - } else { - return new UriSample( - sampleName, - drmInfo, - uri, - extension, - adTagUri, - sphericalStereoMode); - } - } - - private SampleGroup getGroup(String groupName, List groups) { - for (int i = 0; i < groups.size(); i++) { - if (Util.areEqual(groupName, groups.get(i).title)) { - return groups.get(i); - } - } - SampleGroup group = new SampleGroup(groupName); - groups.add(group); - return group; - } - - } - - private final class SampleAdapter extends BaseExpandableListAdapter implements OnClickListener { - - private List sampleGroups; - - public SampleAdapter() { - sampleGroups = Collections.emptyList(); - } - - public void setSampleGroups(List sampleGroups) { - this.sampleGroups = sampleGroups; - notifyDataSetChanged(); - } - - @Override - public Sample getChild(int groupPosition, int childPosition) { - return getGroup(groupPosition).samples.get(childPosition); - } - - @Override - public long getChildId(int groupPosition, int childPosition) { - return childPosition; - } - - @Override - public View getChildView(int groupPosition, int childPosition, boolean isLastChild, - View convertView, ViewGroup parent) { - View view = convertView; - if (view == null) { - view = getLayoutInflater().inflate(R.layout.sample_list_item, parent, false); - View downloadButton = view.findViewById(R.id.download_button); - downloadButton.setOnClickListener(this); - downloadButton.setFocusable(false); - } - initializeChildView(view, getChild(groupPosition, childPosition)); - return view; - } - - @Override - public int getChildrenCount(int groupPosition) { - return getGroup(groupPosition).samples.size(); - } - - @Override - public SampleGroup getGroup(int groupPosition) { - return sampleGroups.get(groupPosition); - } - - @Override - public long getGroupId(int groupPosition) { - return groupPosition; - } - - @Override - public View getGroupView(int groupPosition, boolean isExpanded, View convertView, - ViewGroup parent) { - View view = convertView; - if (view == null) { - view = - getLayoutInflater() - .inflate(android.R.layout.simple_expandable_list_item_1, parent, false); - } - ((TextView) view).setText(getGroup(groupPosition).title); - return view; - } - - @Override - public int getGroupCount() { - return sampleGroups.size(); - } - - @Override - public boolean hasStableIds() { - return false; - } - - @Override - public boolean isChildSelectable(int groupPosition, int childPosition) { - return true; - } - - @Override - public void onClick(View view) { - onSampleDownloadButtonClicked((Sample) view.getTag()); - } - - private void initializeChildView(View view, Sample sample) { - view.setTag(sample); - TextView sampleTitle = view.findViewById(R.id.sample_title); - sampleTitle.setText(sample.name); - - boolean canDownload = getDownloadUnsupportedStringId(sample) == 0; - boolean isDownloaded = canDownload && downloadTracker.isDownloaded(((UriSample) sample).uri); - ImageButton downloadButton = view.findViewById(R.id.download_button); - downloadButton.setTag(sample); - downloadButton.setColorFilter( - canDownload ? (isDownloaded ? 0xFF42A5F5 : 0xFFBDBDBD) : 0xFFEEEEEE); - downloadButton.setImageResource( - isDownloaded ? R.drawable.ic_download_done : R.drawable.ic_download); - } - } - - private static final class SampleGroup { - - public final String title; - public final List samples; - - public SampleGroup(String title) { - this.title = title; - this.samples = new ArrayList<>(); - } - - } - - private static final class DrmInfo { - - public final String drmScheme; - public final String drmLicenseUrl; - public final String[] drmKeyRequestProperties; - public final boolean drmMultiSession; - - public DrmInfo( - String drmScheme, - String drmLicenseUrl, - String[] drmKeyRequestProperties, - boolean drmMultiSession) { - this.drmScheme = drmScheme; - this.drmLicenseUrl = drmLicenseUrl; - this.drmKeyRequestProperties = drmKeyRequestProperties; - this.drmMultiSession = drmMultiSession; - } - - public void updateIntent(Intent intent) { - Assertions.checkNotNull(intent); - intent.putExtra(PlayerActivity.DRM_SCHEME_EXTRA, drmScheme); - intent.putExtra(PlayerActivity.DRM_LICENSE_URL_EXTRA, drmLicenseUrl); - intent.putExtra(PlayerActivity.DRM_KEY_REQUEST_PROPERTIES_EXTRA, drmKeyRequestProperties); - intent.putExtra(PlayerActivity.DRM_MULTI_SESSION_EXTRA, drmMultiSession); - } - } - - private abstract static class Sample { - - public final String name; - public final DrmInfo drmInfo; - - public Sample(String name, DrmInfo drmInfo) { - this.name = name; - this.drmInfo = drmInfo; - } - - public Intent buildIntent( - Context context, boolean preferExtensionDecoders, String abrAlgorithm) { - Intent intent = new Intent(context, PlayerActivity.class); - intent.putExtra(PlayerActivity.PREFER_EXTENSION_DECODERS_EXTRA, preferExtensionDecoders); - intent.putExtra(PlayerActivity.ABR_ALGORITHM_EXTRA, abrAlgorithm); - if (drmInfo != null) { - drmInfo.updateIntent(intent); - } - return intent; - } - - } - - private static final class UriSample extends Sample { - - public final Uri uri; - public final String extension; - public final String adTagUri; - public final String sphericalStereoMode; - - public UriSample( - String name, - DrmInfo drmInfo, - Uri uri, - String extension, - String adTagUri, - String sphericalStereoMode) { - super(name, drmInfo); - this.uri = uri; - this.extension = extension; - this.adTagUri = adTagUri; - this.sphericalStereoMode = sphericalStereoMode; - } - - @Override - public Intent buildIntent( - Context context, boolean preferExtensionDecoders, String abrAlgorithm) { - return super.buildIntent(context, preferExtensionDecoders, abrAlgorithm) - .setData(uri) - .putExtra(PlayerActivity.EXTENSION_EXTRA, extension) - .putExtra(PlayerActivity.AD_TAG_URI_EXTRA, adTagUri) - .putExtra(PlayerActivity.SPHERICAL_STEREO_MODE_EXTRA, sphericalStereoMode) - .setAction(PlayerActivity.ACTION_VIEW); - } - - } - - private static final class PlaylistSample extends Sample { - - public final UriSample[] children; - - public PlaylistSample( - String name, - DrmInfo drmInfo, - UriSample... children) { - super(name, drmInfo); - this.children = children; - } - - @Override - public Intent buildIntent( - Context context, boolean preferExtensionDecoders, String abrAlgorithm) { - String[] uris = new String[children.length]; - String[] extensions = new String[children.length]; - for (int i = 0; i < children.length; i++) { - uris[i] = children[i].uri.toString(); - extensions[i] = children[i].extension; - } - return super.buildIntent(context, preferExtensionDecoders, abrAlgorithm) - .putExtra(PlayerActivity.URI_LIST_EXTRA, uris) - .putExtra(PlayerActivity.EXTENSION_LIST_EXTRA, extensions) - .setAction(PlayerActivity.ACTION_VIEW_LIST); - } - - } - -} diff --git a/demo/src/r2_10_6/java/com/google/android/exoplayer2/demo/TrackSelectionDialog.java b/demo/src/r2_10_6/java/com/google/android/exoplayer2/demo/TrackSelectionDialog.java deleted file mode 100644 index d1b49e77..00000000 --- a/demo/src/r2_10_6/java/com/google/android/exoplayer2/demo/TrackSelectionDialog.java +++ /dev/null @@ -1,373 +0,0 @@ -/* - * Copyright (C) 2019 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.google.android.exoplayer2.demo; - -import android.app.Dialog; -import android.content.DialogInterface; -import android.content.res.Resources; -import android.os.Bundle; -import android.util.SparseArray; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.Button; -import androidx.annotation.Nullable; -import androidx.appcompat.app.AppCompatDialog; -import androidx.fragment.app.DialogFragment; -import androidx.fragment.app.Fragment; -import androidx.fragment.app.FragmentManager; -import androidx.fragment.app.FragmentPagerAdapter; -import androidx.viewpager.widget.ViewPager; -import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.source.TrackGroupArray; -import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; -import com.google.android.exoplayer2.trackselection.DefaultTrackSelector.SelectionOverride; -import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo; -import com.google.android.exoplayer2.ui.TrackSelectionView; -import com.google.android.exoplayer2.util.Assertions; -import com.google.android.material.tabs.TabLayout; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - -/** - * Dialog to select tracks. - */ -public final class TrackSelectionDialog extends DialogFragment { - - private final SparseArray tabFragments; - private final ArrayList tabTrackTypes; - - private int titleId; - private DialogInterface.OnClickListener onClickListener; - private DialogInterface.OnDismissListener onDismissListener; - - /** - * Returns whether a track selection dialog will have content to display if initialized with the - * specified {@link DefaultTrackSelector} in its current state. - */ - public static boolean willHaveContent(DefaultTrackSelector trackSelector) { - MappedTrackInfo mappedTrackInfo = trackSelector.getCurrentMappedTrackInfo(); - return mappedTrackInfo != null && willHaveContent(mappedTrackInfo); - } - - /** - * Returns whether a track selection dialog will have content to display if initialized with the - * specified {@link MappedTrackInfo}. - */ - public static boolean willHaveContent(MappedTrackInfo mappedTrackInfo) { - for (int i = 0; i < mappedTrackInfo.getRendererCount(); i++) { - if (showTabForRenderer(mappedTrackInfo, i)) { - return true; - } - } - return false; - } - - /** - * Creates a dialog for a given {@link DefaultTrackSelector}, whose parameters will be - * automatically updated when tracks are selected. - * - * @param trackSelector The {@link DefaultTrackSelector}. - * @param onDismissListener A {@link DialogInterface.OnDismissListener} to call when the dialog is - * dismissed. - */ - public static TrackSelectionDialog createForTrackSelector( - DefaultTrackSelector trackSelector, DialogInterface.OnDismissListener onDismissListener) { - MappedTrackInfo mappedTrackInfo = - Assertions.checkNotNull(trackSelector.getCurrentMappedTrackInfo()); - TrackSelectionDialog trackSelectionDialog = new TrackSelectionDialog(); - DefaultTrackSelector.Parameters parameters = trackSelector.getParameters(); - trackSelectionDialog.init( - /* titleId= */ R.string.track_selection_title, - mappedTrackInfo, - /* initialParameters = */ parameters, - /* allowAdaptiveSelections =*/ true, - /* allowMultipleOverrides= */ false, - /* onClickListener= */ (dialog, which) -> { - DefaultTrackSelector.ParametersBuilder builder = parameters.buildUpon(); - for (int i = 0; i < mappedTrackInfo.getRendererCount(); i++) { - builder - .clearSelectionOverrides(/* rendererIndex= */ i) - .setRendererDisabled( - /* rendererIndex= */ i, - trackSelectionDialog.getIsDisabled(/* rendererIndex= */ i)); - List overrides = - trackSelectionDialog.getOverrides(/* rendererIndex= */ i); - if (!overrides.isEmpty()) { - builder.setSelectionOverride( - /* rendererIndex= */ i, - mappedTrackInfo.getTrackGroups(/* rendererIndex= */ i), - overrides.get(0)); - } - } - trackSelector.setParameters(builder); - }, - onDismissListener); - return trackSelectionDialog; - } - - /** - * Creates a dialog for given {@link MappedTrackInfo} and {@link DefaultTrackSelector.Parameters}. - * - * @param titleId The resource id of the dialog title. - * @param mappedTrackInfo The {@link MappedTrackInfo} to display. - * @param initialParameters The {@link DefaultTrackSelector.Parameters} describing the - * initial track selection. - * @param allowAdaptiveSelections Whether adaptive selections (consisting of more than one track) - * can be made. - * @param allowMultipleOverrides Whether tracks from multiple track groups can be selected. - * @param onClickListener {@link DialogInterface.OnClickListener} called when tracks are - * selected. - * @param onDismissListener {@link DialogInterface.OnDismissListener} called when the dialog - * is dismissed. - */ - public static TrackSelectionDialog createForMappedTrackInfoAndParameters( - int titleId, - MappedTrackInfo mappedTrackInfo, - DefaultTrackSelector.Parameters initialParameters, - boolean allowAdaptiveSelections, - boolean allowMultipleOverrides, - DialogInterface.OnClickListener onClickListener, - DialogInterface.OnDismissListener onDismissListener) { - TrackSelectionDialog trackSelectionDialog = new TrackSelectionDialog(); - trackSelectionDialog.init( - titleId, - mappedTrackInfo, - initialParameters, - allowAdaptiveSelections, - allowMultipleOverrides, - onClickListener, - onDismissListener); - return trackSelectionDialog; - } - - public TrackSelectionDialog() { - tabFragments = new SparseArray<>(); - tabTrackTypes = new ArrayList<>(); - // Retain instance across activity re-creation to prevent losing access to init data. - setRetainInstance(true); - } - - private void init( - int titleId, - MappedTrackInfo mappedTrackInfo, - DefaultTrackSelector.Parameters initialParameters, - boolean allowAdaptiveSelections, - boolean allowMultipleOverrides, - DialogInterface.OnClickListener onClickListener, - DialogInterface.OnDismissListener onDismissListener) { - this.titleId = titleId; - this.onClickListener = onClickListener; - this.onDismissListener = onDismissListener; - for (int i = 0; i < mappedTrackInfo.getRendererCount(); i++) { - if (showTabForRenderer(mappedTrackInfo, i)) { - int trackType = mappedTrackInfo.getRendererType(/* rendererIndex= */ i); - TrackGroupArray trackGroupArray = mappedTrackInfo.getTrackGroups(i); - TrackSelectionViewFragment tabFragment = new TrackSelectionViewFragment(); - tabFragment.init( - mappedTrackInfo, - /* rendererIndex= */ i, - initialParameters.getRendererDisabled(/* rendererIndex= */ i), - initialParameters.getSelectionOverride(/* rendererIndex= */ i, trackGroupArray), - allowAdaptiveSelections, - allowMultipleOverrides); - tabFragments.put(i, tabFragment); - tabTrackTypes.add(trackType); - } - } - } - - /** - * Returns whether a renderer is disabled. - * - * @param rendererIndex Renderer index. - * @return Whether the renderer is disabled. - */ - public boolean getIsDisabled(int rendererIndex) { - TrackSelectionViewFragment rendererView = tabFragments.get(rendererIndex); - return rendererView != null && rendererView.isDisabled; - } - - /** - * Returns the list of selected track selection overrides for the specified renderer. There will - * be at most one override for each track group. - * - * @param rendererIndex Renderer index. - * @return The list of track selection overrides for this renderer. - */ - public List getOverrides(int rendererIndex) { - TrackSelectionViewFragment rendererView = tabFragments.get(rendererIndex); - return rendererView == null ? Collections.emptyList() : rendererView.overrides; - } - - @Override - public Dialog onCreateDialog(Bundle savedInstanceState) { - // We need to own the view to let tab layout work correctly on all API levels. We can't use - // AlertDialog because it owns the view itself, so we use AppCompatDialog instead, themed using - // the AlertDialog theme overlay with force-enabled title. - AppCompatDialog dialog = - new AppCompatDialog(getActivity(), R.style.TrackSelectionDialogThemeOverlay); - dialog.setTitle(titleId); - return dialog; - } - - @Override - public void onDismiss(DialogInterface dialog) { - super.onDismiss(dialog); - onDismissListener.onDismiss(dialog); - } - - @Nullable - @Override - public View onCreateView( - LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { - - View dialogView = inflater.inflate(R.layout.track_selection_dialog, container, false); - TabLayout tabLayout = dialogView.findViewById(R.id.track_selection_dialog_tab_layout); - ViewPager viewPager = dialogView.findViewById(R.id.track_selection_dialog_view_pager); - Button cancelButton = dialogView.findViewById(R.id.track_selection_dialog_cancel_button); - Button okButton = dialogView.findViewById(R.id.track_selection_dialog_ok_button); - viewPager.setAdapter(new FragmentAdapter(getChildFragmentManager())); - tabLayout.setupWithViewPager(viewPager); - tabLayout.setVisibility(tabFragments.size() > 1 ? View.VISIBLE : View.GONE); - cancelButton.setOnClickListener(view -> dismiss()); - okButton.setOnClickListener( - view -> { - onClickListener.onClick(getDialog(), DialogInterface.BUTTON_POSITIVE); - dismiss(); - }); - return dialogView; - } - - private static boolean showTabForRenderer(MappedTrackInfo mappedTrackInfo, int rendererIndex) { - TrackGroupArray trackGroupArray = mappedTrackInfo.getTrackGroups(rendererIndex); - if (trackGroupArray.length == 0) { - return false; - } - int trackType = mappedTrackInfo.getRendererType(rendererIndex); - return isSupportedTrackType(trackType); - } - - private static boolean isSupportedTrackType(int trackType) { - switch (trackType) { - case C.TRACK_TYPE_VIDEO: - case C.TRACK_TYPE_AUDIO: - case C.TRACK_TYPE_TEXT: - return true; - default: - return false; - } - } - - private static String getTrackTypeString(Resources resources, int trackType) { - switch (trackType) { - case C.TRACK_TYPE_VIDEO: - return resources.getString(R.string.exo_track_selection_title_video); - case C.TRACK_TYPE_AUDIO: - return resources.getString(R.string.exo_track_selection_title_audio); - case C.TRACK_TYPE_TEXT: - return resources.getString(R.string.exo_track_selection_title_text); - default: - throw new IllegalArgumentException(); - } - } - - private final class FragmentAdapter extends FragmentPagerAdapter { - - public FragmentAdapter(FragmentManager fragmentManager) { - super(fragmentManager); - } - - @Override - public Fragment getItem(int position) { - return tabFragments.valueAt(position); - } - - @Override - public int getCount() { - return tabFragments.size(); - } - - @Nullable - @Override - public CharSequence getPageTitle(int position) { - return getTrackTypeString(getResources(), tabTrackTypes.get(position)); - } - } - - /** - * Fragment to show a track selection in tab of the track selection dialog. - */ - public static final class TrackSelectionViewFragment extends Fragment - implements TrackSelectionView.TrackSelectionListener { - - private MappedTrackInfo mappedTrackInfo; - private int rendererIndex; - private boolean allowAdaptiveSelections; - private boolean allowMultipleOverrides; - - /* package */ boolean isDisabled; - /* package */ List overrides; - - public TrackSelectionViewFragment() { - // Retain instance across activity re-creation to prevent losing access to init data. - setRetainInstance(true); - } - - public void init( - MappedTrackInfo mappedTrackInfo, - int rendererIndex, - boolean initialIsDisabled, - @Nullable SelectionOverride initialOverride, - boolean allowAdaptiveSelections, - boolean allowMultipleOverrides) { - this.mappedTrackInfo = mappedTrackInfo; - this.rendererIndex = rendererIndex; - this.isDisabled = initialIsDisabled; - this.overrides = - initialOverride == null - ? Collections.emptyList() - : Collections.singletonList(initialOverride); - this.allowAdaptiveSelections = allowAdaptiveSelections; - this.allowMultipleOverrides = allowMultipleOverrides; - } - - @Nullable - @Override - public View onCreateView( - LayoutInflater inflater, - @Nullable ViewGroup container, - @Nullable Bundle savedInstanceState) { - View rootView = - inflater.inflate( - R.layout.exo_track_selection_dialog, container, /* attachToRoot= */ false); - TrackSelectionView trackSelectionView = rootView.findViewById(R.id.exo_track_selection_view); - trackSelectionView.setShowDisableOption(true); - trackSelectionView.setAllowMultipleOverrides(allowMultipleOverrides); - trackSelectionView.setAllowAdaptiveSelections(allowAdaptiveSelections); - trackSelectionView.init( - mappedTrackInfo, rendererIndex, isDisabled, overrides, /* listener= */ this); - return rootView; - } - - @Override - public void onTrackSelectionChanged(boolean isDisabled, List overrides) { - this.isDisabled = isDisabled; - this.overrides = overrides; - } - } -} diff --git a/demo/src/r2_10_6/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java b/demo/src/r2_10_6/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java deleted file mode 100644 index e2b463c8..00000000 --- a/demo/src/r2_10_6/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java +++ /dev/null @@ -1,1509 +0,0 @@ -/* - * Copyright (C) 2017 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.google.android.exoplayer2.ext.ima; - -import android.content.Context; -import android.net.Uri; -import android.os.Looper; -import android.os.SystemClock; -import android.view.View; -import android.view.ViewGroup; -import androidx.annotation.IntDef; -import androidx.annotation.Nullable; -import androidx.annotation.VisibleForTesting; -import com.google.ads.interactivemedia.v3.api.Ad; -import com.google.ads.interactivemedia.v3.api.AdDisplayContainer; -import com.google.ads.interactivemedia.v3.api.AdError; -import com.google.ads.interactivemedia.v3.api.AdError.AdErrorCode; -import com.google.ads.interactivemedia.v3.api.AdErrorEvent; -import com.google.ads.interactivemedia.v3.api.AdErrorEvent.AdErrorListener; -import com.google.ads.interactivemedia.v3.api.AdEvent; -import com.google.ads.interactivemedia.v3.api.AdEvent.AdEventListener; -import com.google.ads.interactivemedia.v3.api.AdEvent.AdEventType; -import com.google.ads.interactivemedia.v3.api.AdPodInfo; -import com.google.ads.interactivemedia.v3.api.AdsLoader.AdsLoadedListener; -import com.google.ads.interactivemedia.v3.api.AdsManager; -import com.google.ads.interactivemedia.v3.api.AdsManagerLoadedEvent; -import com.google.ads.interactivemedia.v3.api.AdsRenderingSettings; -import com.google.ads.interactivemedia.v3.api.AdsRequest; -import com.google.ads.interactivemedia.v3.api.CompanionAdSlot; -import com.google.ads.interactivemedia.v3.api.ImaSdkFactory; -import com.google.ads.interactivemedia.v3.api.ImaSdkSettings; -import com.google.ads.interactivemedia.v3.api.UiElement; -import com.google.ads.interactivemedia.v3.api.player.ContentProgressProvider; -import com.google.ads.interactivemedia.v3.api.player.VideoAdPlayer; -import com.google.ads.interactivemedia.v3.api.player.VideoProgressUpdate; -import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.ExoPlaybackException; -import com.google.android.exoplayer2.ExoPlayerLibraryInfo; -import com.google.android.exoplayer2.Player; -import com.google.android.exoplayer2.Timeline; -import com.google.android.exoplayer2.source.ads.AdPlaybackState; -import com.google.android.exoplayer2.source.ads.AdPlaybackState.AdState; -import com.google.android.exoplayer2.source.ads.AdsLoader; -import com.google.android.exoplayer2.source.ads.AdsMediaSource.AdLoadException; -import com.google.android.exoplayer2.trackselection.TrackSelectionArray; -import com.google.android.exoplayer2.upstream.DataSpec; -import com.google.android.exoplayer2.util.Assertions; -import com.google.android.exoplayer2.util.Log; -import com.google.android.exoplayer2.util.MimeTypes; -import com.google.android.exoplayer2.util.Util; -import java.io.IOException; -import java.lang.annotation.Documented; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; - -/** - * {@link AdsLoader} using the IMA SDK. All methods must be called on the main thread. - * - *

The player instance that will play the loaded ads must be set before playback using {@link - * #setPlayer(Player)}. If the ads loader is no longer required, it must be released by calling - * {@link #release()}. - * - *

The IMA SDK can take into account video control overlay views when calculating ad - * viewability. For more details see {@link AdDisplayContainer#registerVideoControlsOverlay(View)} - * and {@link AdViewProvider#getAdOverlayViews()}. - */ -public final class ImaAdsLoader - implements Player.EventListener, - AdsLoader, - VideoAdPlayer, - ContentProgressProvider, - AdErrorListener, - AdsLoadedListener, - AdEventListener { - - static { - ExoPlayerLibraryInfo.registerModule("goog.exo.ima"); - } - - /** - * Builder for {@link ImaAdsLoader}. - */ - public static final class Builder { - - private final Context context; - - @Nullable - private ImaSdkSettings imaSdkSettings; - @Nullable - private AdEventListener adEventListener; - @Nullable - private Set adUiElements; - private int vastLoadTimeoutMs; - private int mediaLoadTimeoutMs; - private int mediaBitrate; - private boolean focusSkipButtonWhenAvailable; - private ImaFactory imaFactory; - - /** - * Creates a new builder for {@link ImaAdsLoader}. - * - * @param context The context; - */ - public Builder(Context context) { - this.context = Assertions.checkNotNull(context); - vastLoadTimeoutMs = TIMEOUT_UNSET; - mediaLoadTimeoutMs = TIMEOUT_UNSET; - mediaBitrate = BITRATE_UNSET; - focusSkipButtonWhenAvailable = true; - imaFactory = new DefaultImaFactory(); - } - - /** - * Sets the IMA SDK settings. The provided settings instance's player type and version fields - * may be overwritten. - * - *

If this method is not called the default settings will be used. - * - * @param imaSdkSettings The {@link ImaSdkSettings}. - * @return This builder, for convenience. - */ - public Builder setImaSdkSettings(ImaSdkSettings imaSdkSettings) { - this.imaSdkSettings = Assertions.checkNotNull(imaSdkSettings); - return this; - } - - /** - * Sets a listener for ad events that will be passed to {@link AdsManager#addAdEventListener(AdEventListener)}. - * - * @param adEventListener The ad event listener. - * @return This builder, for convenience. - */ - public Builder setAdEventListener(AdEventListener adEventListener) { - this.adEventListener = Assertions.checkNotNull(adEventListener); - return this; - } - - /** - * Sets the ad UI elements to be rendered by the IMA SDK. - * - * @param adUiElements The ad UI elements to be rendered by the IMA SDK. - * @return This builder, for convenience. - * @see AdsRenderingSettings#setUiElements(Set) - */ - public Builder setAdUiElements(Set adUiElements) { - this.adUiElements = new HashSet<>(Assertions.checkNotNull(adUiElements)); - return this; - } - - /** - * Sets the VAST load timeout, in milliseconds. - * - * @param vastLoadTimeoutMs The VAST load timeout, in milliseconds. - * @return This builder, for convenience. - * @see AdsRequest#setVastLoadTimeout(float) - */ - public Builder setVastLoadTimeoutMs(int vastLoadTimeoutMs) { - Assertions.checkArgument(vastLoadTimeoutMs > 0); - this.vastLoadTimeoutMs = vastLoadTimeoutMs; - return this; - } - - /** - * Sets the ad media load timeout, in milliseconds. - * - * @param mediaLoadTimeoutMs The ad media load timeout, in milliseconds. - * @return This builder, for convenience. - * @see AdsRenderingSettings#setLoadVideoTimeout(int) - */ - public Builder setMediaLoadTimeoutMs(int mediaLoadTimeoutMs) { - Assertions.checkArgument(mediaLoadTimeoutMs > 0); - this.mediaLoadTimeoutMs = mediaLoadTimeoutMs; - return this; - } - - /** - * Sets the media maximum recommended bitrate for ads, in bps. - * - * @param bitrate The media maximum recommended bitrate for ads, in bps. - * @return This builder, for convenience. - * @see AdsRenderingSettings#setBitrateKbps(int) - */ - public Builder setMaxMediaBitrate(int bitrate) { - Assertions.checkArgument(bitrate > 0); - this.mediaBitrate = bitrate; - return this; - } - - /** - * Sets whether to focus the skip button (when available) on Android TV devices. The default - * setting is {@code true}. - * - * @param focusSkipButtonWhenAvailable Whether to focus the skip button (when available) on - * Android TV devices. - * @return This builder, for convenience. - * @see AdsRenderingSettings#setFocusSkipButtonWhenAvailable(boolean) - */ - public Builder setFocusSkipButtonWhenAvailable(boolean focusSkipButtonWhenAvailable) { - this.focusSkipButtonWhenAvailable = focusSkipButtonWhenAvailable; - return this; - } - - @VisibleForTesting - /* package */ Builder setImaFactory(ImaFactory imaFactory) { - this.imaFactory = Assertions.checkNotNull(imaFactory); - return this; - } - - /** - * Returns a new {@link ImaAdsLoader} for the specified ad tag. - * - * @param adTagUri The URI of a compatible ad tag to load. See https://developers.google.com/interactive-media-ads/docs/sdks/android/compatibility - * for information on compatible ad tags. - * @return The new {@link ImaAdsLoader}. - */ - public ImaAdsLoader buildForAdTag(Uri adTagUri) { - return new ImaAdsLoader( - context, - adTagUri, - imaSdkSettings, - null, - vastLoadTimeoutMs, - mediaLoadTimeoutMs, - mediaBitrate, - focusSkipButtonWhenAvailable, - adUiElements, - adEventListener, - imaFactory); - } - - /** - * Returns a new {@link ImaAdsLoader} with the specified sideloaded ads response. - * - * @param adsResponse The sideloaded VAST, VMAP, or ad rules response to be used instead of - * making a request via an ad tag URL. - * @return The new {@link ImaAdsLoader}. - */ - public ImaAdsLoader buildForAdsResponse(String adsResponse) { - return new ImaAdsLoader( - context, - null, - imaSdkSettings, - adsResponse, - vastLoadTimeoutMs, - mediaLoadTimeoutMs, - mediaBitrate, - focusSkipButtonWhenAvailable, - adUiElements, - adEventListener, - imaFactory); - } - } - - private static final boolean DEBUG = false; - private static final String TAG = "ImaAdsLoader"; - - /** - * Whether to enable preloading of ads in {@link AdsRenderingSettings}. - */ - private static final boolean ENABLE_PRELOADING = true; - - private static final String IMA_SDK_SETTINGS_PLAYER_TYPE = "google/exo.ext.ima"; - private static final String IMA_SDK_SETTINGS_PLAYER_VERSION = ExoPlayerLibraryInfo.VERSION; - - /** - * The value used in {@link VideoProgressUpdate}s to indicate an unset duration. - */ - private static final long IMA_DURATION_UNSET = -1L; - - /** - * Threshold before the end of content at which IMA is notified that content is complete if the - * player buffers, in milliseconds. - */ - private static final long END_OF_CONTENT_POSITION_THRESHOLD_MS = 5000; - - /** - * The maximum duration before an ad break that IMA may start preloading the next ad. - */ - private static final long MAXIMUM_PRELOAD_DURATION_MS = 8000; - - private static final int TIMEOUT_UNSET = -1; - private static final int BITRATE_UNSET = -1; - - /** - * The state of ad playback. - */ - @Documented - @Retention(RetentionPolicy.SOURCE) - @IntDef({IMA_AD_STATE_NONE, IMA_AD_STATE_PLAYING, IMA_AD_STATE_PAUSED}) - private @interface ImaAdState { - - } - - /** - * The ad playback state when IMA is not playing an ad. - */ - private static final int IMA_AD_STATE_NONE = 0; - /** - * The ad playback state when IMA has called {@link #playAd()} and not {@link #pauseAd()}. - */ - private static final int IMA_AD_STATE_PLAYING = 1; - /** - * The ad playback state when IMA has called {@link #pauseAd()} while playing an ad. - */ - private static final int IMA_AD_STATE_PAUSED = 2; - - private final @Nullable - Uri adTagUri; - private final @Nullable - String adsResponse; - private final int vastLoadTimeoutMs; - private final int mediaLoadTimeoutMs; - private final boolean focusSkipButtonWhenAvailable; - private final int mediaBitrate; - private final @Nullable - Set adUiElements; - private final @Nullable - AdEventListener adEventListener; - private final ImaFactory imaFactory; - private final Timeline.Period period; - private final List adCallbacks; - private final AdDisplayContainer adDisplayContainer; - private final com.google.ads.interactivemedia.v3.api.AdsLoader adsLoader; - - private boolean wasSetPlayerCalled; - @Nullable - private Player nextPlayer; - private Object pendingAdRequestContext; - private List supportedMimeTypes; - @Nullable - private EventListener eventListener; - @Nullable - private Player player; - private VideoProgressUpdate lastContentProgress; - private VideoProgressUpdate lastAdProgress; - private int lastVolumePercentage; - - private AdsManager adsManager; - private boolean initializedAdsManager; - private AdLoadException pendingAdLoadError; - private Timeline timeline; - private long contentDurationMs; - private int podIndexOffset; - private AdPlaybackState adPlaybackState; - - // Fields tracking IMA's state. - - /** - * The expected ad group index that IMA should load next. - */ - private int expectedAdGroupIndex; - /** - * The index of the current ad group that IMA is loading. - */ - private int adGroupIndex; - /** - * Whether IMA has sent an ad event to pause content since the last resume content event. - */ - private boolean imaPausedContent; - /** - * The current ad playback state. - */ - private @ImaAdState - int imaAdState; - /** - * Whether {@link com.google.ads.interactivemedia.v3.api.AdsLoader#contentComplete()} has been - * called since starting ad playback. - */ - private boolean sentContentComplete; - - // Fields tracking the player/loader state. - - /** - * Whether the player is playing an ad. - */ - private boolean playingAd; - /** - * If the player is playing an ad, stores the ad index in its ad group. {@link C#INDEX_UNSET} - * otherwise. - */ - private int playingAdIndexInAdGroup; - /** - * Whether there's a pending ad preparation error which IMA needs to be notified of when it - * transitions from playing content to playing the ad. - */ - private boolean shouldNotifyAdPrepareError; - /** - * If a content period has finished but IMA has not yet called {@link #playAd()}, stores the value - * of {@link SystemClock#elapsedRealtime()} when the content stopped playing. This can be used to - * determine a fake, increasing content position. {@link C#TIME_UNSET} otherwise. - */ - private long fakeContentProgressElapsedRealtimeMs; - /** - * If {@link #fakeContentProgressElapsedRealtimeMs} is set, stores the offset from which the - * content progress should increase. {@link C#TIME_UNSET} otherwise. - */ - private long fakeContentProgressOffsetMs; - /** - * Stores the pending content position when a seek operation was intercepted to play an ad. - */ - private long pendingContentPositionMs; - /** - * Whether {@link #getContentProgress()} has sent {@link #pendingContentPositionMs} to IMA. - */ - private boolean sentPendingContentPositionMs; - - /** - * Creates a new IMA ads loader. - * - *

If you need to customize the ad request, use {@link ImaAdsLoader.Builder} instead. - * - * @param context The context. - * @param adTagUri The {@link Uri} of an ad tag compatible with the Android IMA SDK. See - * https://developers.google.com/interactive-media-ads/docs/sdks/android/compatibility - * for more information. - */ - public ImaAdsLoader(Context context, Uri adTagUri) { - this( - context, - adTagUri, - /* imaSdkSettings= */ null, - /* adsResponse= */ null, - /* vastLoadTimeoutMs= */ TIMEOUT_UNSET, - /* mediaLoadTimeoutMs= */ TIMEOUT_UNSET, - /* mediaBitrate= */ BITRATE_UNSET, - /* focusSkipButtonWhenAvailable= */ true, - /* adUiElements= */ null, - /* adEventListener= */ null, - /* imaFactory= */ new DefaultImaFactory()); - } - - /** - * Creates a new IMA ads loader. - * - * @param context The context. - * @param adTagUri The {@link Uri} of an ad tag compatible with the Android IMA SDK. See - * https://developers.google.com/interactive-media-ads/docs/sdks/android/compatibility - * for more information. - * @param imaSdkSettings {@link ImaSdkSettings} used to configure the IMA SDK, or {@code null} to - * use the default settings. If set, the player type and version fields may - * be overwritten. - * @deprecated Use {@link ImaAdsLoader.Builder}. - */ - @Deprecated - public ImaAdsLoader(Context context, Uri adTagUri, ImaSdkSettings imaSdkSettings) { - this( - context, - adTagUri, - imaSdkSettings, - /* adsResponse= */ null, - /* vastLoadTimeoutMs= */ TIMEOUT_UNSET, - /* mediaLoadTimeoutMs= */ TIMEOUT_UNSET, - /* mediaBitrate= */ BITRATE_UNSET, - /* focusSkipButtonWhenAvailable= */ true, - /* adUiElements= */ null, - /* adEventListener= */ null, - /* imaFactory= */ new DefaultImaFactory()); - } - - private ImaAdsLoader( - Context context, - @Nullable Uri adTagUri, - @Nullable ImaSdkSettings imaSdkSettings, - @Nullable String adsResponse, - int vastLoadTimeoutMs, - int mediaLoadTimeoutMs, - int mediaBitrate, - boolean focusSkipButtonWhenAvailable, - @Nullable Set adUiElements, - @Nullable AdEventListener adEventListener, - ImaFactory imaFactory) { - Assertions.checkArgument(adTagUri != null || adsResponse != null); - this.adTagUri = adTagUri; - this.adsResponse = adsResponse; - this.vastLoadTimeoutMs = vastLoadTimeoutMs; - this.mediaLoadTimeoutMs = mediaLoadTimeoutMs; - this.mediaBitrate = mediaBitrate; - this.focusSkipButtonWhenAvailable = focusSkipButtonWhenAvailable; - this.adUiElements = adUiElements; - this.adEventListener = adEventListener; - this.imaFactory = imaFactory; - if (imaSdkSettings == null) { - imaSdkSettings = imaFactory.createImaSdkSettings(); - if (DEBUG) { - imaSdkSettings.setDebugMode(true); - } - } - imaSdkSettings.setPlayerType(IMA_SDK_SETTINGS_PLAYER_TYPE); - imaSdkSettings.setPlayerVersion(IMA_SDK_SETTINGS_PLAYER_VERSION); - period = new Timeline.Period(); - adCallbacks = new ArrayList<>(/* initialCapacity= */ 1); - adDisplayContainer = imaFactory.createAdDisplayContainer(); - adDisplayContainer.setPlayer(/* videoAdPlayer= */ this); - adsLoader = imaFactory.createAdsLoader(context, imaSdkSettings, adDisplayContainer); - adsLoader.addAdErrorListener(/* adErrorListener= */ this); - adsLoader.addAdsLoadedListener(/* adsLoadedListener= */ this); - fakeContentProgressElapsedRealtimeMs = C.TIME_UNSET; - fakeContentProgressOffsetMs = C.TIME_UNSET; - pendingContentPositionMs = C.TIME_UNSET; - adGroupIndex = C.INDEX_UNSET; - contentDurationMs = C.TIME_UNSET; - timeline = Timeline.EMPTY; - } - - /** - * Returns the underlying {@code com.google.ads.interactivemedia.v3.api.AdsLoader} wrapped by this - * instance. - */ - public com.google.ads.interactivemedia.v3.api.AdsLoader getAdsLoader() { - return adsLoader; - } - - /** - * Returns the {@link AdDisplayContainer} used by this loader. - * - *

Note: any video controls overlays registered via {@link - * AdDisplayContainer#registerVideoControlsOverlay(View)} will be unregistered automatically when - * the media source detaches from this instance. It is therefore necessary to re-register views - * each time the ads loader is reused. Alternatively, provide overlay views via the {@link - * AdsLoader.AdViewProvider} when creating the media source to benefit from automatic - * registration. - */ - public AdDisplayContainer getAdDisplayContainer() { - return adDisplayContainer; - } - - /** - * Sets the slots for displaying companion ads. Individual slots can be created using {@link - * ImaSdkFactory#createCompanionAdSlot()}. - * - * @param companionSlots Slots for displaying companion ads. - * @see AdDisplayContainer#setCompanionSlots(Collection) - * @deprecated Use {@code getAdDisplayContainer().setCompanionSlots(...)}. - */ - @Deprecated - public void setCompanionSlots(Collection companionSlots) { - adDisplayContainer.setCompanionSlots(companionSlots); - } - - /** - * Requests ads, if they have not already been requested. Must be called on the main thread. - * - *

Ads will be requested automatically when the player is prepared if this method has not been - * called, so it is only necessary to call this method if you want to request ads before preparing - * the player. - * - * @param adViewGroup A {@link ViewGroup} on top of the player that will show any ad UI. - */ - public void requestAds(ViewGroup adViewGroup) { - if (adPlaybackState != null || adsManager != null || pendingAdRequestContext != null) { - // Ads have already been requested. - return; - } - adDisplayContainer.setAdContainer(adViewGroup); - pendingAdRequestContext = new Object(); - AdsRequest request = imaFactory.createAdsRequest(); - if (adTagUri != null) { - request.setAdTagUrl(adTagUri.toString()); - } else /* adsResponse != null */ { - request.setAdsResponse(adsResponse); - } - if (vastLoadTimeoutMs != TIMEOUT_UNSET) { - request.setVastLoadTimeout(vastLoadTimeoutMs); - } - request.setContentProgressProvider(this); - request.setUserRequestContext(pendingAdRequestContext); - adsLoader.requestAds(request); - } - - // AdsLoader implementation. - - @Override - public void setPlayer(@Nullable Player player) { - Assertions.checkState(Looper.getMainLooper() == Looper.myLooper()); - Assertions.checkState( - player == null || player.getApplicationLooper() == Looper.getMainLooper()); - nextPlayer = player; - wasSetPlayerCalled = true; - } - - @Override - public void setSupportedContentTypes(@C.ContentType int... contentTypes) { - List supportedMimeTypes = new ArrayList<>(); - for (@C.ContentType int contentType : contentTypes) { - if (contentType == C.TYPE_DASH) { - supportedMimeTypes.add(MimeTypes.APPLICATION_MPD); - } else if (contentType == C.TYPE_HLS) { - supportedMimeTypes.add(MimeTypes.APPLICATION_M3U8); - } else if (contentType == C.TYPE_OTHER) { - supportedMimeTypes.addAll( - Arrays.asList( - MimeTypes.VIDEO_MP4, - MimeTypes.VIDEO_WEBM, - MimeTypes.VIDEO_H263, - MimeTypes.AUDIO_MP4, - MimeTypes.AUDIO_MPEG)); - } else if (contentType == C.TYPE_SS) { - // IMA does not support Smooth Streaming ad media. - } - } - this.supportedMimeTypes = Collections.unmodifiableList(supportedMimeTypes); - } - - @Override - public void start(EventListener eventListener, AdViewProvider adViewProvider) { - Assertions.checkState( - wasSetPlayerCalled, "Set player using adsLoader.setPlayer before preparing the player."); - player = nextPlayer; - if (player == null) { - return; - } - this.eventListener = eventListener; - lastVolumePercentage = 0; - lastAdProgress = null; - lastContentProgress = null; - ViewGroup adViewGroup = adViewProvider.getAdViewGroup(); - adDisplayContainer.setAdContainer(adViewGroup); - View[] adOverlayViews = adViewProvider.getAdOverlayViews(); - for (View view : adOverlayViews) { - adDisplayContainer.registerVideoControlsOverlay(view); - } - player.addListener(this); - maybeNotifyPendingAdLoadError(); - if (adPlaybackState != null) { - // Pass the ad playback state to the player, and resume ads if necessary. - eventListener.onAdPlaybackState(adPlaybackState); - if (imaPausedContent && player.getPlayWhenReady()) { - adsManager.resume(); - } - } else if (adsManager != null) { - adPlaybackState = new AdPlaybackState(getAdGroupTimesUs(adsManager.getAdCuePoints())); - updateAdPlaybackState(); - } else { - // Ads haven't loaded yet, so request them. - requestAds(adViewGroup); - } - } - - @Override - public void stop() { - if (player == null) { - return; - } - if (adsManager != null && imaPausedContent) { - adPlaybackState = - adPlaybackState.withAdResumePositionUs( - playingAd ? C.msToUs(player.getCurrentPosition()) : 0); - adsManager.pause(); - } - lastVolumePercentage = getVolume(); - lastAdProgress = getAdProgress(); - lastContentProgress = getContentProgress(); - adDisplayContainer.unregisterAllVideoControlsOverlays(); - player.removeListener(this); - player = null; - eventListener = null; - } - - @Override - public void release() { - pendingAdRequestContext = null; - if (adsManager != null) { - adsManager.destroy(); - adsManager = null; - } - adsLoader.removeAdsLoadedListener(/* adsLoadedListener= */ this); - adsLoader.removeAdErrorListener(/* adErrorListener= */ this); - imaPausedContent = false; - imaAdState = IMA_AD_STATE_NONE; - pendingAdLoadError = null; - adPlaybackState = AdPlaybackState.NONE; - updateAdPlaybackState(); - } - - @Override - public void handlePrepareError(int adGroupIndex, int adIndexInAdGroup, IOException exception) { - if (player == null) { - return; - } - try { - handleAdPrepareError(adGroupIndex, adIndexInAdGroup, exception); - } catch (Exception e) { - maybeNotifyInternalError("handlePrepareError", e); - } - } - - // com.google.ads.interactivemedia.v3.api.AdsLoader.AdsLoadedListener implementation. - - @Override - public void onAdsManagerLoaded(AdsManagerLoadedEvent adsManagerLoadedEvent) { - AdsManager adsManager = adsManagerLoadedEvent.getAdsManager(); - if (!Util.areEqual(pendingAdRequestContext, adsManagerLoadedEvent.getUserRequestContext())) { - adsManager.destroy(); - return; - } - pendingAdRequestContext = null; - this.adsManager = adsManager; - adsManager.addAdErrorListener(this); - adsManager.addAdEventListener(this); - if (adEventListener != null) { - adsManager.addAdEventListener(adEventListener); - } - if (player != null) { - // If a player is attached already, start playback immediately. - try { - adPlaybackState = new AdPlaybackState(getAdGroupTimesUs(adsManager.getAdCuePoints())); - updateAdPlaybackState(); - } catch (Exception e) { - maybeNotifyInternalError("onAdsManagerLoaded", e); - } - } - } - - // AdEvent.AdEventListener implementation. - - @Override - public void onAdEvent(AdEvent adEvent) { - AdEventType adEventType = adEvent.getType(); - if (DEBUG) { - Log.d(TAG, "onAdEvent: " + adEventType); - } - if (adsManager == null) { - Log.w(TAG, "Ignoring AdEvent after release: " + adEvent); - return; - } - try { - handleAdEvent(adEvent); - } catch (Exception e) { - maybeNotifyInternalError("onAdEvent", e); - } - } - - // AdErrorEvent.AdErrorListener implementation. - - @Override - public void onAdError(AdErrorEvent adErrorEvent) { - AdError error = adErrorEvent.getError(); - if (DEBUG) { - Log.d(TAG, "onAdError", error); - } - if (adsManager == null) { - // No ads were loaded, so allow playback to start without any ads. - pendingAdRequestContext = null; - adPlaybackState = new AdPlaybackState(); - updateAdPlaybackState(); - } else if (isAdGroupLoadError(error)) { - try { - handleAdGroupLoadError(error); - } catch (Exception e) { - maybeNotifyInternalError("onAdError", e); - } - } - if (pendingAdLoadError == null) { - pendingAdLoadError = AdLoadException.createForAllAds(error); - } - maybeNotifyPendingAdLoadError(); - } - - // ContentProgressProvider implementation. - - @Override - public VideoProgressUpdate getContentProgress() { - if (player == null) { - return lastContentProgress; - } - boolean hasContentDuration = contentDurationMs != C.TIME_UNSET; - long contentPositionMs; - if (pendingContentPositionMs != C.TIME_UNSET) { - sentPendingContentPositionMs = true; - contentPositionMs = pendingContentPositionMs; - expectedAdGroupIndex = - adPlaybackState.getAdGroupIndexForPositionUs(C.msToUs(contentPositionMs)); - } else if (fakeContentProgressElapsedRealtimeMs != C.TIME_UNSET) { - long elapsedSinceEndMs = SystemClock.elapsedRealtime() - fakeContentProgressElapsedRealtimeMs; - contentPositionMs = fakeContentProgressOffsetMs + elapsedSinceEndMs; - expectedAdGroupIndex = - adPlaybackState.getAdGroupIndexForPositionUs(C.msToUs(contentPositionMs)); - } else if (imaAdState == IMA_AD_STATE_NONE && !playingAd && hasContentDuration) { - contentPositionMs = player.getCurrentPosition(); - // Update the expected ad group index for the current content position. The update is delayed - // until MAXIMUM_PRELOAD_DURATION_MS before the ad so that an ad group load error delivered - // just after an ad group isn't incorrectly attributed to the next ad group. - int nextAdGroupIndex = - adPlaybackState.getAdGroupIndexAfterPositionUs( - C.msToUs(contentPositionMs), C.msToUs(contentDurationMs)); - if (nextAdGroupIndex != expectedAdGroupIndex && nextAdGroupIndex != C.INDEX_UNSET) { - long nextAdGroupTimeMs = C.usToMs(adPlaybackState.adGroupTimesUs[nextAdGroupIndex]); - if (nextAdGroupTimeMs == C.TIME_END_OF_SOURCE) { - nextAdGroupTimeMs = contentDurationMs; - } - if (nextAdGroupTimeMs - contentPositionMs < MAXIMUM_PRELOAD_DURATION_MS) { - expectedAdGroupIndex = nextAdGroupIndex; - } - } - } else { - return VideoProgressUpdate.VIDEO_TIME_NOT_READY; - } - long contentDurationMs = hasContentDuration ? this.contentDurationMs : IMA_DURATION_UNSET; - return new VideoProgressUpdate(contentPositionMs, contentDurationMs); - } - - // VideoAdPlayer implementation. - - @Override - public VideoProgressUpdate getAdProgress() { - if (player == null) { - return lastAdProgress; - } else if (imaAdState != IMA_AD_STATE_NONE && playingAd) { - long adDuration = player.getDuration(); - return adDuration == C.TIME_UNSET ? VideoProgressUpdate.VIDEO_TIME_NOT_READY - : new VideoProgressUpdate(player.getCurrentPosition(), adDuration); - } else { - return VideoProgressUpdate.VIDEO_TIME_NOT_READY; - } - } - - @Override - public int getVolume() { - if (player == null) { - return lastVolumePercentage; - } - - Player.AudioComponent audioComponent = player.getAudioComponent(); - if (audioComponent != null) { - return (int) (audioComponent.getVolume() * 100); - } - - // Check for a selected track using an audio renderer. - TrackSelectionArray trackSelections = player.getCurrentTrackSelections(); - for (int i = 0; i < player.getRendererCount() && i < trackSelections.length; i++) { - if (player.getRendererType(i) == C.TRACK_TYPE_AUDIO && trackSelections.get(i) != null) { - return 100; - } - } - return 0; - } - - @Override - public void loadAd(String adUriString) { - try { - if (DEBUG) { - Log.d(TAG, "loadAd in ad group " + adGroupIndex); - } - if (adsManager == null) { - Log.w(TAG, "Ignoring loadAd after release"); - return; - } - if (adGroupIndex == C.INDEX_UNSET) { - Log.w( - TAG, - "Unexpected loadAd without LOADED event; assuming ad group index is actually " - + expectedAdGroupIndex); - adGroupIndex = expectedAdGroupIndex; - adsManager.start(); - } - int adIndexInAdGroup = getAdIndexInAdGroupToLoad(adGroupIndex); - if (adIndexInAdGroup == C.INDEX_UNSET) { - Log.w(TAG, "Unexpected loadAd in an ad group with no remaining unavailable ads"); - return; - } - adPlaybackState = - adPlaybackState.withAdUri(adGroupIndex, adIndexInAdGroup, Uri.parse(adUriString)); - updateAdPlaybackState(); - } catch (Exception e) { - maybeNotifyInternalError("loadAd", e); - } - } - - @Override - public void addCallback(VideoAdPlayerCallback videoAdPlayerCallback) { - adCallbacks.add(videoAdPlayerCallback); - } - - @Override - public void removeCallback(VideoAdPlayerCallback videoAdPlayerCallback) { - adCallbacks.remove(videoAdPlayerCallback); - } - - @Override - public void playAd() { - if (DEBUG) { - Log.d(TAG, "playAd"); - } - if (adsManager == null) { - Log.w(TAG, "Ignoring playAd after release"); - return; - } - switch (imaAdState) { - case IMA_AD_STATE_PLAYING: - // IMA does not always call stopAd before resuming content. - // See [Internal: b/38354028, b/63320878]. - Log.w(TAG, "Unexpected playAd without stopAd"); - break; - case IMA_AD_STATE_NONE: - // IMA is requesting to play the ad, so stop faking the content position. - fakeContentProgressElapsedRealtimeMs = C.TIME_UNSET; - fakeContentProgressOffsetMs = C.TIME_UNSET; - imaAdState = IMA_AD_STATE_PLAYING; - for (int i = 0; i < adCallbacks.size(); i++) { - adCallbacks.get(i).onPlay(); - } - if (shouldNotifyAdPrepareError) { - shouldNotifyAdPrepareError = false; - for (int i = 0; i < adCallbacks.size(); i++) { - adCallbacks.get(i).onError(); - } - } - break; - case IMA_AD_STATE_PAUSED: - imaAdState = IMA_AD_STATE_PLAYING; - for (int i = 0; i < adCallbacks.size(); i++) { - adCallbacks.get(i).onResume(); - } - break; - default: - throw new IllegalStateException(); - } - if (player == null) { - // Sometimes messages from IMA arrive after detaching the player. See [Internal: b/63801642]. - Log.w(TAG, "Unexpected playAd while detached"); - } else if (!player.getPlayWhenReady()) { - adsManager.pause(); - } - } - - @Override - public void stopAd() { - if (DEBUG) { - Log.d(TAG, "stopAd"); - } - if (adsManager == null) { - Log.w(TAG, "Ignoring stopAd after release"); - return; - } - if (player == null) { - // Sometimes messages from IMA arrive after detaching the player. See [Internal: b/63801642]. - Log.w(TAG, "Unexpected stopAd while detached"); - } - if (imaAdState == IMA_AD_STATE_NONE) { - Log.w(TAG, "Unexpected stopAd"); - return; - } - try { - stopAdInternal(); - } catch (Exception e) { - maybeNotifyInternalError("stopAd", e); - } - } - - @Override - public void pauseAd() { - if (DEBUG) { - Log.d(TAG, "pauseAd"); - } - if (imaAdState == IMA_AD_STATE_NONE) { - // This method is called after content is resumed. - return; - } - imaAdState = IMA_AD_STATE_PAUSED; - for (int i = 0; i < adCallbacks.size(); i++) { - adCallbacks.get(i).onPause(); - } - } - - @Override - public void resumeAd() { - // This method is never called. See [Internal: b/18931719]. - maybeNotifyInternalError("resumeAd", new IllegalStateException("Unexpected call to resumeAd")); - } - - // Player.EventListener implementation. - - @Override - public void onTimelineChanged( - Timeline timeline, @Nullable Object manifest, @Player.TimelineChangeReason int reason) { - if (timeline.isEmpty()) { - // The player is being reset or contains no media. - return; - } - Assertions.checkArgument(timeline.getPeriodCount() == 1); - this.timeline = timeline; - long contentDurationUs = timeline.getPeriod(0, period).durationUs; - contentDurationMs = C.usToMs(contentDurationUs); - if (contentDurationUs != C.TIME_UNSET) { - adPlaybackState = adPlaybackState.withContentDurationUs(contentDurationUs); - } - if (!initializedAdsManager && adsManager != null) { - initializedAdsManager = true; - initializeAdsManager(); - } - onPositionDiscontinuity(Player.DISCONTINUITY_REASON_INTERNAL); - } - - @Override - public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { - if (adsManager == null) { - return; - } - - if (imaAdState == IMA_AD_STATE_PLAYING && !playWhenReady) { - adsManager.pause(); - return; - } - - if (imaAdState == IMA_AD_STATE_PAUSED && playWhenReady) { - adsManager.resume(); - return; - } - - if (imaAdState == IMA_AD_STATE_NONE && playbackState == Player.STATE_BUFFERING - && playWhenReady) { - checkForContentComplete(); - } else if (imaAdState != IMA_AD_STATE_NONE && playbackState == Player.STATE_ENDED) { - for (int i = 0; i < adCallbacks.size(); i++) { - adCallbacks.get(i).onEnded(); - } - if (DEBUG) { - Log.d(TAG, "VideoAdPlayerCallback.onEnded in onPlayerStateChanged"); - } - } - } - - @Override - public void onPlayerError(ExoPlaybackException error) { - if (imaAdState != IMA_AD_STATE_NONE) { - for (int i = 0; i < adCallbacks.size(); i++) { - adCallbacks.get(i).onError(); - } - } - } - - @Override - public void onPositionDiscontinuity(@Player.DiscontinuityReason int reason) { - if (adsManager == null) { - return; - } - if (!playingAd && !player.isPlayingAd()) { - checkForContentComplete(); - if (sentContentComplete) { - for (int i = 0; i < adPlaybackState.adGroupCount; i++) { - if (adPlaybackState.adGroupTimesUs[i] != C.TIME_END_OF_SOURCE) { - adPlaybackState = adPlaybackState.withSkippedAdGroup(i); - } - } - updateAdPlaybackState(); - } else if (!timeline.isEmpty()) { - long positionMs = player.getCurrentPosition(); - timeline.getPeriod(0, period); - int newAdGroupIndex = period.getAdGroupIndexForPositionUs(C.msToUs(positionMs)); - if (newAdGroupIndex != C.INDEX_UNSET) { - sentPendingContentPositionMs = false; - pendingContentPositionMs = positionMs; - if (newAdGroupIndex != adGroupIndex) { - shouldNotifyAdPrepareError = false; - } - } - } - } - updateImaStateForPlayerState(); - } - - // Internal methods. - - private void initializeAdsManager() { - AdsRenderingSettings adsRenderingSettings = imaFactory.createAdsRenderingSettings(); - adsRenderingSettings.setEnablePreloading(ENABLE_PRELOADING); - adsRenderingSettings.setMimeTypes(supportedMimeTypes); - if (mediaLoadTimeoutMs != TIMEOUT_UNSET) { - adsRenderingSettings.setLoadVideoTimeout(mediaLoadTimeoutMs); - } - if (mediaBitrate != BITRATE_UNSET) { - adsRenderingSettings.setBitrateKbps(mediaBitrate / 1000); - } - adsRenderingSettings.setFocusSkipButtonWhenAvailable(focusSkipButtonWhenAvailable); - if (adUiElements != null) { - adsRenderingSettings.setUiElements(adUiElements); - } - - // Skip ads based on the start position as required. - long[] adGroupTimesUs = getAdGroupTimesUs(adsManager.getAdCuePoints()); - long contentPositionMs = player.getContentPosition(); - int adGroupIndexForPosition = - adPlaybackState.getAdGroupIndexForPositionUs(C.msToUs(contentPositionMs)); - if (adGroupIndexForPosition > 0 && adGroupIndexForPosition != C.INDEX_UNSET) { - // Skip any ad groups before the one at or immediately before the playback position. - for (int i = 0; i < adGroupIndexForPosition; i++) { - adPlaybackState = adPlaybackState.withSkippedAdGroup(i); - } - // Play ads after the midpoint between the ad to play and the one before it, to avoid issues - // with rounding one of the two ad times. - long adGroupForPositionTimeUs = adGroupTimesUs[adGroupIndexForPosition]; - long adGroupBeforeTimeUs = adGroupTimesUs[adGroupIndexForPosition - 1]; - double midpointTimeUs = (adGroupForPositionTimeUs + adGroupBeforeTimeUs) / 2d; - adsRenderingSettings.setPlayAdsAfterTime(midpointTimeUs / C.MICROS_PER_SECOND); - } - - // IMA indexes any remaining midroll ad pods from 1. A preroll (if present) has index 0. - // Store an index offset as we want to index all ads (including skipped ones) from 0. - if (adGroupIndexForPosition == 0 && adGroupTimesUs[0] == 0) { - // We are playing a preroll. - podIndexOffset = 0; - } else if (adGroupIndexForPosition == C.INDEX_UNSET) { - // There's no ad to play which means there's no preroll. - podIndexOffset = -1; - } else { - // We are playing a midroll and any ads before it were skipped. - podIndexOffset = adGroupIndexForPosition - 1; - } - - if (adGroupIndexForPosition != C.INDEX_UNSET && hasMidrollAdGroups(adGroupTimesUs)) { - // Provide the player's initial position to trigger loading and playing the ad. - pendingContentPositionMs = contentPositionMs; - } - - adsManager.init(adsRenderingSettings); - updateAdPlaybackState(); - if (DEBUG) { - Log.d(TAG, "Initialized with ads rendering settings: " + adsRenderingSettings); - } - } - - private void handleAdEvent(AdEvent adEvent) { - Ad ad = adEvent.getAd(); - switch (adEvent.getType()) { - case LOADED: - // The ad position is not always accurate when using preloading. See [Internal: b/62613240]. - AdPodInfo adPodInfo = ad.getAdPodInfo(); - int podIndex = adPodInfo.getPodIndex(); - adGroupIndex = - podIndex == -1 ? (adPlaybackState.adGroupCount - 1) : (podIndex + podIndexOffset); - int adPosition = adPodInfo.getAdPosition(); - int adCount = adPodInfo.getTotalAds(); - adsManager.start(); - if (DEBUG) { - Log.d(TAG, "Loaded ad " + adPosition + " of " + adCount + " in group " + adGroupIndex); - } - int oldAdCount = adPlaybackState.adGroups[adGroupIndex].count; - if (adCount != oldAdCount) { - if (oldAdCount == C.LENGTH_UNSET) { - adPlaybackState = adPlaybackState.withAdCount(adGroupIndex, adCount); - updateAdPlaybackState(); - } else { - // IMA sometimes unexpectedly decreases the ad count in an ad group. - Log.w(TAG, "Unexpected ad count in LOADED, " + adCount + ", expected " + oldAdCount); - } - } - if (adGroupIndex != expectedAdGroupIndex) { - Log.w( - TAG, - "Expected ad group index " - + expectedAdGroupIndex - + ", actual ad group index " - + adGroupIndex); - expectedAdGroupIndex = adGroupIndex; - } - break; - case CONTENT_PAUSE_REQUESTED: - // After CONTENT_PAUSE_REQUESTED, IMA will playAd/pauseAd/stopAd to show one or more ads - // before sending CONTENT_RESUME_REQUESTED. - imaPausedContent = true; - pauseContentInternal(); - break; - case TAPPED: - if (eventListener != null) { - eventListener.onAdTapped(); - } - break; - case CLICKED: - if (eventListener != null) { - eventListener.onAdClicked(); - } - break; - case CONTENT_RESUME_REQUESTED: - imaPausedContent = false; - resumeContentInternal(); - break; - case LOG: - Map adData = adEvent.getAdData(); - String message = "AdEvent: " + adData; - Log.i(TAG, message); - if ("adLoadError".equals(adData.get("type"))) { - handleAdGroupLoadError(new IOException(message)); - } - break; - case STARTED: - case ALL_ADS_COMPLETED: - default: - break; - } - } - - private void updateImaStateForPlayerState() { - boolean wasPlayingAd = playingAd; - int oldPlayingAdIndexInAdGroup = playingAdIndexInAdGroup; - playingAd = player.isPlayingAd(); - playingAdIndexInAdGroup = playingAd ? player.getCurrentAdIndexInAdGroup() : C.INDEX_UNSET; - boolean adFinished = wasPlayingAd && playingAdIndexInAdGroup != oldPlayingAdIndexInAdGroup; - if (adFinished) { - // IMA is waiting for the ad playback to finish so invoke the callback now. - // Either CONTENT_RESUME_REQUESTED will be passed next, or playAd will be called again. - for (int i = 0; i < adCallbacks.size(); i++) { - adCallbacks.get(i).onEnded(); - } - if (DEBUG) { - Log.d(TAG, "VideoAdPlayerCallback.onEnded in onTimelineChanged/onPositionDiscontinuity"); - } - } - if (!sentContentComplete && !wasPlayingAd && playingAd && imaAdState == IMA_AD_STATE_NONE) { - int adGroupIndex = player.getCurrentAdGroupIndex(); - // IMA hasn't called playAd yet, so fake the content position. - fakeContentProgressElapsedRealtimeMs = SystemClock.elapsedRealtime(); - fakeContentProgressOffsetMs = C.usToMs(adPlaybackState.adGroupTimesUs[adGroupIndex]); - if (fakeContentProgressOffsetMs == C.TIME_END_OF_SOURCE) { - fakeContentProgressOffsetMs = contentDurationMs; - } - } - } - - private void resumeContentInternal() { - if (imaAdState != IMA_AD_STATE_NONE) { - imaAdState = IMA_AD_STATE_NONE; - if (DEBUG) { - Log.d(TAG, "Unexpected CONTENT_RESUME_REQUESTED without stopAd"); - } - } - if (adGroupIndex != C.INDEX_UNSET) { - adPlaybackState = adPlaybackState.withSkippedAdGroup(adGroupIndex); - adGroupIndex = C.INDEX_UNSET; - updateAdPlaybackState(); - } - } - - private void pauseContentInternal() { - imaAdState = IMA_AD_STATE_NONE; - if (sentPendingContentPositionMs) { - pendingContentPositionMs = C.TIME_UNSET; - sentPendingContentPositionMs = false; - } - } - - private void stopAdInternal() { - imaAdState = IMA_AD_STATE_NONE; - int adIndexInAdGroup = adPlaybackState.adGroups[adGroupIndex].getFirstAdIndexToPlay(); - // TODO: Handle the skipped event so the ad can be marked as skipped rather than played. - adPlaybackState = - adPlaybackState.withPlayedAd(adGroupIndex, adIndexInAdGroup).withAdResumePositionUs(0); - updateAdPlaybackState(); - if (!playingAd) { - adGroupIndex = C.INDEX_UNSET; - } - } - - private void handleAdGroupLoadError(Exception error) { - int adGroupIndex = - this.adGroupIndex == C.INDEX_UNSET ? expectedAdGroupIndex : this.adGroupIndex; - if (adGroupIndex == C.INDEX_UNSET) { - // Drop the error, as we don't know which ad group it relates to. - return; - } - AdPlaybackState.AdGroup adGroup = adPlaybackState.adGroups[adGroupIndex]; - if (adGroup.count == C.LENGTH_UNSET) { - adPlaybackState = - adPlaybackState.withAdCount(adGroupIndex, Math.max(1, adGroup.states.length)); - adGroup = adPlaybackState.adGroups[adGroupIndex]; - } - for (int i = 0; i < adGroup.count; i++) { - if (adGroup.states[i] == AdPlaybackState.AD_STATE_UNAVAILABLE) { - if (DEBUG) { - Log.d(TAG, "Removing ad " + i + " in ad group " + adGroupIndex); - } - adPlaybackState = adPlaybackState.withAdLoadError(adGroupIndex, i); - } - } - updateAdPlaybackState(); - if (pendingAdLoadError == null) { - pendingAdLoadError = AdLoadException.createForAdGroup(error, adGroupIndex); - } - pendingContentPositionMs = C.TIME_UNSET; - fakeContentProgressElapsedRealtimeMs = C.TIME_UNSET; - } - - private void handleAdPrepareError(int adGroupIndex, int adIndexInAdGroup, Exception exception) { - if (DEBUG) { - Log.d( - TAG, "Prepare error for ad " + adIndexInAdGroup + " in group " + adGroupIndex, exception); - } - if (adsManager == null) { - Log.w(TAG, "Ignoring ad prepare error after release"); - return; - } - if (imaAdState == IMA_AD_STATE_NONE) { - // Send IMA a content position at the ad group so that it will try to play it, at which point - // we can notify that it failed to load. - fakeContentProgressElapsedRealtimeMs = SystemClock.elapsedRealtime(); - fakeContentProgressOffsetMs = C.usToMs(adPlaybackState.adGroupTimesUs[adGroupIndex]); - if (fakeContentProgressOffsetMs == C.TIME_END_OF_SOURCE) { - fakeContentProgressOffsetMs = contentDurationMs; - } - shouldNotifyAdPrepareError = true; - } else { - // We're already playing an ad. - if (adIndexInAdGroup > playingAdIndexInAdGroup) { - // Mark the playing ad as ended so we can notify the error on the next ad and remove it, - // which means that the ad after will load (if any). - for (int i = 0; i < adCallbacks.size(); i++) { - adCallbacks.get(i).onEnded(); - } - } - playingAdIndexInAdGroup = adPlaybackState.adGroups[adGroupIndex].getFirstAdIndexToPlay(); - for (int i = 0; i < adCallbacks.size(); i++) { - adCallbacks.get(i).onError(); - } - } - adPlaybackState = adPlaybackState.withAdLoadError(adGroupIndex, adIndexInAdGroup); - updateAdPlaybackState(); - } - - private void checkForContentComplete() { - if (contentDurationMs != C.TIME_UNSET && pendingContentPositionMs == C.TIME_UNSET - && player.getContentPosition() + END_OF_CONTENT_POSITION_THRESHOLD_MS >= contentDurationMs - && !sentContentComplete) { - adsLoader.contentComplete(); - if (DEBUG) { - Log.d(TAG, "adsLoader.contentComplete"); - } - sentContentComplete = true; - // After sending content complete IMA will not poll the content position, so set the expected - // ad group index. - expectedAdGroupIndex = - adPlaybackState.getAdGroupIndexForPositionUs(C.msToUs(contentDurationMs)); - } - } - - private void updateAdPlaybackState() { - // Ignore updates while detached. When a player is attached it will receive the latest state. - if (eventListener != null) { - eventListener.onAdPlaybackState(adPlaybackState); - } - } - - /** - * Returns the next ad index in the specified ad group to load, or {@link C#INDEX_UNSET} if all - * ads in the ad group have loaded. - */ - private int getAdIndexInAdGroupToLoad(int adGroupIndex) { - @AdState int[] states = adPlaybackState.adGroups[adGroupIndex].states; - int adIndexInAdGroup = 0; - // IMA loads ads in order. - while (adIndexInAdGroup < states.length - && states[adIndexInAdGroup] != AdPlaybackState.AD_STATE_UNAVAILABLE) { - adIndexInAdGroup++; - } - return adIndexInAdGroup == states.length ? C.INDEX_UNSET : adIndexInAdGroup; - } - - private void maybeNotifyPendingAdLoadError() { - if (pendingAdLoadError != null && eventListener != null) { - eventListener.onAdLoadError(pendingAdLoadError, new DataSpec(adTagUri)); - pendingAdLoadError = null; - } - } - - private void maybeNotifyInternalError(String name, Exception cause) { - String message = "Internal error in " + name; - Log.e(TAG, message, cause); - // We can't recover from an unexpected error in general, so skip all remaining ads. - if (adPlaybackState == null) { - adPlaybackState = AdPlaybackState.NONE; - } else { - for (int i = 0; i < adPlaybackState.adGroupCount; i++) { - adPlaybackState = adPlaybackState.withSkippedAdGroup(i); - } - } - updateAdPlaybackState(); - if (eventListener != null) { - eventListener.onAdLoadError( - AdLoadException.createForUnexpected(new RuntimeException(message, cause)), - new DataSpec(adTagUri)); - } - } - - private static long[] getAdGroupTimesUs(List cuePoints) { - if (cuePoints.isEmpty()) { - // If no cue points are specified, there is a preroll ad. - return new long[]{0}; - } - - int count = cuePoints.size(); - long[] adGroupTimesUs = new long[count]; - int adGroupIndex = 0; - for (int i = 0; i < count; i++) { - double cuePoint = cuePoints.get(i); - if (cuePoint == -1.0) { - adGroupTimesUs[count - 1] = C.TIME_END_OF_SOURCE; - } else { - adGroupTimesUs[adGroupIndex++] = (long) (C.MICROS_PER_SECOND * cuePoint); - } - } - // Cue points may be out of order, so sort them. - Arrays.sort(adGroupTimesUs, 0, adGroupIndex); - return adGroupTimesUs; - } - - private static boolean isAdGroupLoadError(AdError adError) { - // TODO: Find out what other errors need to be handled (if any), and whether each one relates to - // a single ad, ad group or the whole timeline. - return adError.getErrorCode() == AdErrorCode.VAST_LINEAR_ASSET_MISMATCH - || adError.getErrorCode() == AdErrorCode.UNKNOWN_ERROR; - } - - private static boolean hasMidrollAdGroups(long[] adGroupTimesUs) { - int count = adGroupTimesUs.length; - if (count == 1) { - return adGroupTimesUs[0] != 0 && adGroupTimesUs[0] != C.TIME_END_OF_SOURCE; - } else if (count == 2) { - return adGroupTimesUs[0] != 0 || adGroupTimesUs[1] != C.TIME_END_OF_SOURCE; - } else { - // There's at least one midroll ad group, as adGroupTimesUs is never empty. - return true; - } - } - - /** - * Factory for objects provided by the IMA SDK. - */ - @VisibleForTesting - /* package */ interface ImaFactory { - - /** - * @see ImaSdkSettings - */ - ImaSdkSettings createImaSdkSettings(); - - /** - * @see com.google.ads.interactivemedia.v3.api.ImaSdkFactory#createAdsRenderingSettings() - */ - AdsRenderingSettings createAdsRenderingSettings(); - - /** - * @see com.google.ads.interactivemedia.v3.api.ImaSdkFactory#createAdDisplayContainer() - */ - AdDisplayContainer createAdDisplayContainer(); - - /** - * @see com.google.ads.interactivemedia.v3.api.ImaSdkFactory#createAdsRequest() - */ - AdsRequest createAdsRequest(); - - /** - * @see ImaSdkFactory#createAdsLoader(Context, ImaSdkSettings, AdDisplayContainer) - */ - com.google.ads.interactivemedia.v3.api.AdsLoader createAdsLoader( - Context context, ImaSdkSettings imaSdkSettings, AdDisplayContainer adDisplayContainer); - } - - /** - * Default {@link ImaFactory} for non-test usage, which delegates to {@link ImaSdkFactory}. - */ - private static final class DefaultImaFactory implements ImaFactory { - - @Override - public ImaSdkSettings createImaSdkSettings() { - return ImaSdkFactory.getInstance().createImaSdkSettings(); - } - - @Override - public AdsRenderingSettings createAdsRenderingSettings() { - return ImaSdkFactory.getInstance().createAdsRenderingSettings(); - } - - @Override - public AdDisplayContainer createAdDisplayContainer() { - return ImaSdkFactory.getInstance().createAdDisplayContainer(); - } - - @Override - public AdsRequest createAdsRequest() { - return ImaSdkFactory.getInstance().createAdsRequest(); - } - - @Override - public com.google.ads.interactivemedia.v3.api.AdsLoader createAdsLoader( - Context context, ImaSdkSettings imaSdkSettings, AdDisplayContainer adDisplayContainer) { - return ImaSdkFactory.getInstance() - .createAdsLoader(context, imaSdkSettings, adDisplayContainer); - } - } -} diff --git a/demo/src/r2_10_6/res/layout/player_activity.xml b/demo/src/r2_10_6/res/layout/player_activity.xml deleted file mode 100644 index 66560397..00000000 --- a/demo/src/r2_10_6/res/layout/player_activity.xml +++ /dev/null @@ -1,60 +0,0 @@ - - - - - - - - - - - -