├── .gitignore ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── example │ │ └── andrioidexoplayertrackselection │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── assets │ │ └── media.exolist.json │ ├── java │ │ └── com │ │ │ └── example │ │ │ └── andrioidexoplayertrackselection │ │ │ ├── MainActivity.kt │ │ │ ├── exoplayer2 │ │ │ ├── DemoDownloadService.java │ │ │ ├── DownloadTracker.java │ │ │ ├── PlayerActivity.java │ │ │ └── VideoDownloadManager.java │ │ │ └── track_selection_dialog │ │ │ ├── FragmentAdapter.java │ │ │ ├── TrackSelectionDialog.java │ │ │ └── TrackSelectionViewFragment.java │ └── res │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ ├── ic_download.xml │ │ ├── ic_download_done.xml │ │ └── ic_launcher_background.xml │ │ ├── layout │ │ ├── activity_main.xml │ │ ├── player_activity.xml │ │ └── track_selection_dialog.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ └── values │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── com │ └── example │ └── andrioidexoplayertrackselection │ └── ExampleUnitTest.kt ├── build.gradle ├── captures ├── screen1.png └── selection.png ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | #built application files 2 | *.apk 3 | *.ap_ 4 | 5 | # files for the dex VM 6 | *.dex 7 | 8 | # Java class files 9 | *.class 10 | 11 | # generated files 12 | bin/ 13 | gen/ 14 | 15 | # Local configuration file (sdk path, etc) 16 | local.properties 17 | 18 | # Windows thumbnail db 19 | Thumbs.db 20 | 21 | # OSX files 22 | .DS_Store 23 | 24 | # Eclipse project files 25 | .classpath 26 | .project 27 | 28 | # Android Studio 29 | *.iml 30 | .idea 31 | #.idea/workspace.xml - remove # and delete .idea if it better suit your needs. 32 | .gradle 33 | build/ 34 | 35 | #NDK 36 | obj/ 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Android ExoPlayer 2 track selection example 2 | 3 | https://www.codexpedia.com/android/android-exoplayer-2-track-selection-example/ 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | apply plugin: 'kotlin-android' 4 | 5 | apply plugin: 'kotlin-android-extensions' 6 | 7 | android { 8 | compileSdkVersion 28 9 | defaultConfig { 10 | applicationId "com.example.andrioidexoplayertrackselection" 11 | minSdkVersion 16 12 | targetSdkVersion 28 13 | versionCode 1 14 | versionName "1.0" 15 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 16 | } 17 | buildTypes { 18 | release { 19 | minifyEnabled false 20 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 21 | } 22 | } 23 | 24 | compileOptions { 25 | sourceCompatibility JavaVersion.VERSION_1_8 26 | targetCompatibility JavaVersion.VERSION_1_8 27 | } 28 | } 29 | 30 | dependencies { 31 | implementation fileTree(dir: 'libs', include: ['*.jar']) 32 | implementation"org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 33 | implementation 'androidx.appcompat:appcompat:1.1.0' 34 | implementation 'androidx.core:core-ktx:1.1.0' 35 | implementation 'androidx.constraintlayout:constraintlayout:1.1.3' 36 | testImplementation 'junit:junit:4.12' 37 | androidTestImplementation 'androidx.test.ext:junit:1.1.1' 38 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' 39 | 40 | implementation 'com.google.android.material:material:1.0.0' 41 | implementation 'com.google.android.exoplayer:exoplayer:2.10.7' 42 | implementation 'com.google.code.gson:gson:2.8.6' 43 | } 44 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/example/andrioidexoplayertrackselection/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.example.andrioidexoplayertrackselection 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 22 | assertEquals("com.example.andrioidexoplayertrackselection", appContext.packageName) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /app/src/main/assets/media.exolist.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "HLS", 4 | "samples": [ 5 | { 6 | "name": "Apple master playlist advanced (fMP4)", 7 | "uri": "https://devstreaming-cdn.apple.com/videos/streaming/examples/img_bipbop_adv_example_fmp4/master.m3u8" 8 | }, 9 | { 10 | "name": "Apple 4x3 basic stream", 11 | "uri": "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_4x3/bipbop_4x3_variant.m3u8" 12 | }, 13 | { 14 | "name": "Apple 16x9 basic stream", 15 | "uri": "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_16x9/bipbop_16x9_variant.m3u8" 16 | }, 17 | { 18 | "name": "Apple master playlist advanced (TS)", 19 | "uri": "https://devstreaming-cdn.apple.com/videos/streaming/examples/img_bipbop_adv_example_ts/master.m3u8" 20 | }, 21 | { 22 | "name": "Apple TS media playlist", 23 | "uri": "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_4x3/gear1/prog_index.m3u8" 24 | }, 25 | { 26 | "name": "Apple AAC media playlist", 27 | "uri": "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_4x3/gear0/prog_index.m3u8" 28 | } 29 | ] 30 | }, 31 | { 32 | "name": "YouTube DASH", 33 | "samples": [ 34 | { 35 | "name": "Google Glass (MP4,H264)", 36 | "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", 37 | "extension": "mpd" 38 | }, 39 | { 40 | "name": "Google Play (MP4,H264)", 41 | "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", 42 | "extension": "mpd" 43 | }, 44 | { 45 | "name": "Google Glass (WebM,VP9)", 46 | "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", 47 | "extension": "mpd" 48 | }, 49 | { 50 | "name": "Google Play (WebM,VP9)", 51 | "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", 52 | "extension": "mpd" 53 | } 54 | ] 55 | }, 56 | { 57 | "name": "Widevine DASH Policy Tests (GTS)", 58 | "samples": [ 59 | { 60 | "name": "WV: HDCP not specified", 61 | "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd", 62 | "drm_scheme": "widevine", 63 | "drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=d286538032258a1c&provider=widevine_test" 64 | }, 65 | { 66 | "name": "WV: HDCP not required", 67 | "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd", 68 | "drm_scheme": "widevine", 69 | "drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=48fcc369939ac96c&provider=widevine_test" 70 | }, 71 | { 72 | "name": "WV: HDCP required", 73 | "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd", 74 | "drm_scheme": "widevine", 75 | "drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=e06c39f1151da3df&provider=widevine_test" 76 | }, 77 | { 78 | "name": "WV: Secure video path required (MP4,H264)", 79 | "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd", 80 | "drm_scheme": "widevine", 81 | "drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=0894c7c8719b28a0&provider=widevine_test" 82 | }, 83 | { 84 | "name": "WV: Secure video path required (WebM,VP9)", 85 | "uri": "https://storage.googleapis.com/wvmedia/cenc/vp9/tears/tears.mpd", 86 | "drm_scheme": "widevine", 87 | "drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=0894c7c8719b28a0&provider=widevine_test" 88 | }, 89 | { 90 | "name": "WV: Secure video path required (MP4,H265)", 91 | "uri": "https://storage.googleapis.com/wvmedia/cenc/hevc/tears/tears.mpd", 92 | "drm_scheme": "widevine", 93 | "drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=0894c7c8719b28a0&provider=widevine_test" 94 | }, 95 | { 96 | "name": "WV: HDCP + secure video path required", 97 | "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd", 98 | "drm_scheme": "widevine", 99 | "drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=efd045b1eb61888a&provider=widevine_test" 100 | }, 101 | { 102 | "name": "WV: 30s license duration (fails at ~30s)", 103 | "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd", 104 | "drm_scheme": "widevine", 105 | "drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=f9a34cab7b05881a&provider=widevine_test" 106 | } 107 | ] 108 | }, 109 | { 110 | "name": "Widevine HDCP Capabilities Tests", 111 | "samples": [ 112 | { 113 | "name": "WV: HDCP: None (not required)", 114 | "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd", 115 | "drm_scheme": "widevine", 116 | "drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=HDCP_None&provider=widevine_test" 117 | }, 118 | { 119 | "name": "WV: HDCP: 1.0 required", 120 | "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd", 121 | "drm_scheme": "widevine", 122 | "drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=HDCP_V1&provider=widevine_test" 123 | }, 124 | { 125 | "name": "WV: HDCP: 2.0 required", 126 | "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd", 127 | "drm_scheme": "widevine", 128 | "drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=HDCP_V2&provider=widevine_test" 129 | }, 130 | { 131 | "name": "WV: HDCP: 2.1 required", 132 | "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd", 133 | "drm_scheme": "widevine", 134 | "drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=HDCP_V2_1&provider=widevine_test" 135 | }, 136 | { 137 | "name": "WV: HDCP: 2.2 required", 138 | "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd", 139 | "drm_scheme": "widevine", 140 | "drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=HDCP_V2_2&provider=widevine_test" 141 | }, 142 | { 143 | "name": "WV: HDCP: No digital output", 144 | "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd", 145 | "drm_scheme": "widevine", 146 | "drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=HDCP_NO_DIGTAL_OUTPUT&provider=widevine_test" 147 | } 148 | ] 149 | }, 150 | { 151 | "name": "Widevine DASH: MP4,H264", 152 | "samples": [ 153 | { 154 | "name": "WV: Clear SD & HD (MP4,H264)", 155 | "uri": "https://storage.googleapis.com/wvmedia/clear/h264/tears/tears.mpd" 156 | }, 157 | { 158 | "name": "WV: Clear SD (MP4,H264)", 159 | "uri": "https://storage.googleapis.com/wvmedia/clear/h264/tears/tears_sd.mpd" 160 | }, 161 | { 162 | "name": "WV: Clear HD (MP4,H264)", 163 | "uri": "https://storage.googleapis.com/wvmedia/clear/h264/tears/tears_hd.mpd" 164 | }, 165 | { 166 | "name": "WV: Clear UHD (MP4,H264)", 167 | "uri": "https://storage.googleapis.com/wvmedia/clear/h264/tears/tears_uhd.mpd" 168 | }, 169 | { 170 | "name": "WV: Secure SD & HD (cenc,MP4,H264)", 171 | "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd", 172 | "drm_scheme": "widevine", 173 | "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test" 174 | }, 175 | { 176 | "name": "WV: Secure SD (cenc,MP4,H264)", 177 | "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_sd.mpd", 178 | "drm_scheme": "widevine", 179 | "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test" 180 | }, 181 | { 182 | "name": "WV: Secure HD (cenc,MP4,H264)", 183 | "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_hd.mpd", 184 | "drm_scheme": "widevine", 185 | "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test" 186 | }, 187 | { 188 | "name": "WV: Secure UHD (cenc,MP4,H264)", 189 | "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_uhd.mpd", 190 | "drm_scheme": "widevine", 191 | "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test" 192 | }, 193 | { 194 | "name": "WV: Secure SD & HD (cbc1,MP4,H264)", 195 | "uri": "https://storage.googleapis.com/wvmedia/cbc1/h264/tears/tears_aes_cbc1.mpd", 196 | "drm_scheme": "widevine", 197 | "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test" 198 | }, 199 | { 200 | "name": "WV: Secure SD (cbc1,MP4,H264)", 201 | "uri": "https://storage.googleapis.com/wvmedia/cbc1/h264/tears/tears_aes_cbc1_sd.mpd", 202 | "drm_scheme": "widevine", 203 | "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test" 204 | }, 205 | { 206 | "name": "WV: Secure HD (cbc1,MP4,H264)", 207 | "uri": "https://storage.googleapis.com/wvmedia/cbc1/h264/tears/tears_aes_cbc1_hd.mpd", 208 | "drm_scheme": "widevine", 209 | "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test" 210 | }, 211 | { 212 | "name": "WV: Secure UHD (cbc1,MP4,H264)", 213 | "uri": "https://storage.googleapis.com/wvmedia/cbc1/h264/tears/tears_aes_cbc1_uhd.mpd", 214 | "drm_scheme": "widevine", 215 | "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test" 216 | }, 217 | { 218 | "name": "WV: Secure SD & HD (cbcs,MP4,H264)", 219 | "uri": "https://storage.googleapis.com/wvmedia/cbcs/h264/tears/tears_aes_cbcs.mpd", 220 | "drm_scheme": "widevine", 221 | "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test" 222 | }, 223 | { 224 | "name": "WV: Secure SD (cbcs,MP4,H264)", 225 | "uri": "https://storage.googleapis.com/wvmedia/cbcs/h264/tears/tears_aes_cbcs_sd.mpd", 226 | "drm_scheme": "widevine", 227 | "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test" 228 | }, 229 | { 230 | "name": "WV: Secure HD (cbcs,MP4,H264)", 231 | "uri": "https://storage.googleapis.com/wvmedia/cbcs/h264/tears/tears_aes_cbcs_hd.mpd", 232 | "drm_scheme": "widevine", 233 | "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test" 234 | }, 235 | { 236 | "name": "WV: Secure UHD (cbcs,MP4,H264)", 237 | "uri": "https://storage.googleapis.com/wvmedia/cbcs/h264/tears/tears_aes_cbcs_uhd.mpd", 238 | "drm_scheme": "widevine", 239 | "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test" 240 | } 241 | ] 242 | }, 243 | { 244 | "name": "Widevine DASH: WebM,VP9", 245 | "samples": [ 246 | { 247 | "name": "WV: Clear SD & HD (WebM,VP9)", 248 | "uri": "https://storage.googleapis.com/wvmedia/clear/vp9/tears/tears.mpd" 249 | }, 250 | { 251 | "name": "WV: Clear SD (WebM,VP9)", 252 | "uri": "https://storage.googleapis.com/wvmedia/clear/vp9/tears/tears_sd.mpd" 253 | }, 254 | { 255 | "name": "WV: Clear HD (WebM,VP9)", 256 | "uri": "https://storage.googleapis.com/wvmedia/clear/vp9/tears/tears_hd.mpd" 257 | }, 258 | { 259 | "name": "WV: Clear UHD (WebM,VP9)", 260 | "uri": "https://storage.googleapis.com/wvmedia/clear/vp9/tears/tears_uhd.mpd" 261 | }, 262 | { 263 | "name": "WV: Secure Fullsample SD & HD (WebM,VP9)", 264 | "uri": "https://storage.googleapis.com/wvmedia/cenc/vp9/tears/tears.mpd", 265 | "drm_scheme": "widevine", 266 | "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test" 267 | }, 268 | { 269 | "name": "WV: Secure Fullsample SD (WebM,VP9)", 270 | "uri": "https://storage.googleapis.com/wvmedia/cenc/vp9/tears/tears_sd.mpd", 271 | "drm_scheme": "widevine", 272 | "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test" 273 | }, 274 | { 275 | "name": "WV: Secure Fullsample HD (WebM,VP9)", 276 | "uri": "https://storage.googleapis.com/wvmedia/cenc/vp9/tears/tears_hd.mpd", 277 | "drm_scheme": "widevine", 278 | "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test" 279 | }, 280 | { 281 | "name": "WV: Secure Fullsample UHD (WebM,VP9)", 282 | "uri": "https://storage.googleapis.com/wvmedia/cenc/vp9/tears/tears_uhd.mpd", 283 | "drm_scheme": "widevine", 284 | "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test" 285 | }, 286 | { 287 | "name": "WV: Secure Subsample SD & HD (WebM,VP9)", 288 | "uri": "https://storage.googleapis.com/wvmedia/cenc/vp9/subsample/24fps/tears/tears.mpd", 289 | "drm_scheme": "widevine", 290 | "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test" 291 | }, 292 | { 293 | "name": "WV: Secure Subsample SD (WebM,VP9)", 294 | "uri": "https://storage.googleapis.com/wvmedia/cenc/vp9/subsample/24fps/tears/tears_sd.mpd", 295 | "drm_scheme": "widevine", 296 | "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test" 297 | }, 298 | { 299 | "name": "WV: Secure Subsample HD (WebM,VP9)", 300 | "uri": "https://storage.googleapis.com/wvmedia/cenc/vp9/subsample/24fps/tears/tears_hd.mpd", 301 | "drm_scheme": "widevine", 302 | "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test" 303 | }, 304 | { 305 | "name": "WV: Secure Subsample UHD (WebM,VP9)", 306 | "uri": "https://storage.googleapis.com/wvmedia/cenc/vp9/subsample/24fps/tears/tears_uhd.mpd", 307 | "drm_scheme": "widevine", 308 | "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test" 309 | } 310 | ] 311 | }, 312 | { 313 | "name": "Widevine DASH: MP4,H265", 314 | "samples": [ 315 | { 316 | "name": "WV: Clear SD & HD (MP4,H265)", 317 | "uri": "https://storage.googleapis.com/wvmedia/clear/hevc/tears/tears.mpd" 318 | }, 319 | { 320 | "name": "WV: Clear SD (MP4,H265)", 321 | "uri": "https://storage.googleapis.com/wvmedia/clear/hevc/tears/tears_sd.mpd" 322 | }, 323 | { 324 | "name": "WV: Clear HD (MP4,H265)", 325 | "uri": "https://storage.googleapis.com/wvmedia/clear/hevc/tears/tears_hd.mpd" 326 | }, 327 | { 328 | "name": "WV: Clear UHD (MP4,H265)", 329 | "uri": "https://storage.googleapis.com/wvmedia/clear/hevc/tears/tears_uhd.mpd" 330 | }, 331 | { 332 | "name": "WV: Secure SD & HD (MP4,H265)", 333 | "uri": "https://storage.googleapis.com/wvmedia/cenc/hevc/tears/tears.mpd", 334 | "drm_scheme": "widevine", 335 | "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test" 336 | }, 337 | { 338 | "name": "WV: Secure SD (MP4,H265)", 339 | "uri": "https://storage.googleapis.com/wvmedia/cenc/hevc/tears/tears_sd.mpd", 340 | "drm_scheme": "widevine", 341 | "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test" 342 | }, 343 | { 344 | "name": "WV: Secure HD (MP4,H265)", 345 | "uri": "https://storage.googleapis.com/wvmedia/cenc/hevc/tears/tears_hd.mpd", 346 | "drm_scheme": "widevine", 347 | "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test" 348 | }, 349 | { 350 | "name": "WV: Secure UHD (MP4,H265)", 351 | "uri": "https://storage.googleapis.com/wvmedia/cenc/hevc/tears/tears_uhd.mpd", 352 | "drm_scheme": "widevine", 353 | "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test" 354 | } 355 | ] 356 | }, 357 | { 358 | "name": "SmoothStreaming", 359 | "samples": [ 360 | { 361 | "name": "Super speed", 362 | "uri": "https://playready.directtaps.net/smoothstreaming/SSWSS720H264/SuperSpeedway_720.ism/Manifest" 363 | }, 364 | { 365 | "name": "Super speed (PlayReady)", 366 | "uri": "https://playready.directtaps.net/smoothstreaming/SSWSS720H264PR/SuperSpeedway_720.ism/Manifest", 367 | "drm_scheme": "playready" 368 | } 369 | ] 370 | }, 371 | { 372 | "name": "Misc", 373 | "samples": [ 374 | { 375 | "name": "Dizzy (MP4)", 376 | "uri": "https://html5demos.com/assets/dizzy.mp4" 377 | }, 378 | { 379 | "name": "Apple AAC 10s", 380 | "uri": "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_4x3/gear0/fileSequence0.aac" 381 | }, 382 | { 383 | "name": "Apple TS 10s", 384 | "uri": "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_4x3/gear1/fileSequence0.ts" 385 | }, 386 | { 387 | "name": "Android screens (Matroska)", 388 | "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv" 389 | }, 390 | { 391 | "name": "Screens 360P (WebM,VP9,No Audio)", 392 | "uri": "https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/video-vp9-360.webm" 393 | }, 394 | { 395 | "name": "Screens 480p (FMP4,H264,No Audio)", 396 | "uri": "https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/video-avc-baseline-480.mp4" 397 | }, 398 | { 399 | "name": "Screens 1080p (FMP4,H264, No Audio)", 400 | "uri": "https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/video-137.mp4" 401 | }, 402 | { 403 | "name": "Screens (FMP4,AAC Audio)", 404 | "uri": "https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/audio-141.mp4" 405 | }, 406 | { 407 | "name": "Google Play (MP3 Audio)", 408 | "uri": "https://storage.googleapis.com/exoplayer-test-media-0/play.mp3" 409 | }, 410 | { 411 | "name": "Google Play (Ogg/Vorbis Audio)", 412 | "uri": "https://storage.googleapis.com/exoplayer-test-media-1/ogg/play.ogg" 413 | }, 414 | { 415 | "name": "Big Buck Bunny (FLV Video)", 416 | "uri": "https://vod.leasewebcdn.com/bbb.flv?ri=1024&rs=150&start=0" 417 | } 418 | ] 419 | }, 420 | { 421 | "name": "Playlists", 422 | "samples": [ 423 | { 424 | "name": "Cats -> Dogs", 425 | "playlist": [ 426 | { 427 | "uri": "https://html5demos.com/assets/dizzy.mp4" 428 | }, 429 | { 430 | "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv" 431 | } 432 | ] 433 | }, 434 | { 435 | "name": "Audio -> Video -> Audio", 436 | "playlist": [ 437 | { 438 | "uri": "https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/audio-141.mp4" 439 | }, 440 | { 441 | "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv" 442 | }, 443 | { 444 | "uri": "https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/audio-141.mp4" 445 | } 446 | ] 447 | }, 448 | { 449 | "name": "Clear -> Enc -> Clear -> Enc -> Enc", 450 | "drm_scheme": "widevine", 451 | "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test", 452 | "playlist": [ 453 | { 454 | "uri": "https://html5demos.com/assets/dizzy.mp4" 455 | }, 456 | { 457 | "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_sd.mpd" 458 | }, 459 | { 460 | "uri": "https://html5demos.com/assets/dizzy.mp4" 461 | }, 462 | { 463 | "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_sd.mpd" 464 | }, 465 | { 466 | "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_sd.mpd" 467 | } 468 | ] 469 | } 470 | ] 471 | }, 472 | { 473 | "name": "IMA sample ad tags", 474 | "samples": [ 475 | { 476 | "name": "Single inline linear", 477 | "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv", 478 | "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=" 479 | }, 480 | { 481 | "name": "Single skippable inline", 482 | "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv", 483 | "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=" 484 | }, 485 | { 486 | "name": "Single redirect linear", 487 | "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv", 488 | "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=" 489 | }, 490 | { 491 | "name": "Single redirect error", 492 | "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv", 493 | "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=" 494 | }, 495 | { 496 | "name": "Single redirect broken (fallback)", 497 | "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv", 498 | "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=" 499 | }, 500 | { 501 | "name": "VMAP pre-roll", 502 | "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv", 503 | "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=" 504 | }, 505 | { 506 | "name": "VMAP pre-roll + bumper", 507 | "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv", 508 | "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=" 509 | }, 510 | { 511 | "name": "VMAP post-roll", 512 | "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv", 513 | "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=" 514 | }, 515 | { 516 | "name": "VMAP post-roll + bumper", 517 | "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv", 518 | "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=" 519 | }, 520 | { 521 | "name": "VMAP pre-, mid- and post-rolls, single ads", 522 | "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv", 523 | "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=" 524 | }, 525 | { 526 | "name": "VMAP pre-roll single ad, mid-roll standard pod with 3 ads, post-roll single ad", 527 | "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv", 528 | "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=" 529 | }, 530 | { 531 | "name": "VMAP pre-roll single ad, mid-roll optimized pod with 3 ads, post-roll single ad", 532 | "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv", 533 | "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=" 534 | }, 535 | { 536 | "name": "VMAP pre-roll single ad, mid-roll standard pod with 3 ads, post-roll single ad (bumpers around all ad breaks)", 537 | "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv", 538 | "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=" 539 | }, 540 | { 541 | "name": "VMAP pre-roll single ad, mid-roll optimized pod with 3 ads, post-roll single ad (bumpers around all ad breaks)", 542 | "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv", 543 | "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=" 544 | }, 545 | { 546 | "name": "VMAP pre-roll single ad, mid-roll standard pods with 5 ads every 10 seconds for 1:40, post-roll single ad", 547 | "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv", 548 | "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=" 549 | }, 550 | { 551 | "name": "VMAP empty midroll", 552 | "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv", 553 | "ad_tag_uri": "https://vastsynthesizer.appspot.com/empty-midroll" 554 | }, 555 | { 556 | "name": "VMAP full, empty, full midrolls", 557 | "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv", 558 | "ad_tag_uri": "https://vastsynthesizer.appspot.com/empty-midroll-2" 559 | } 560 | ] 561 | }, 562 | { 563 | "name": "360", 564 | "samples": [ 565 | { 566 | "name": "Congo (360 top-bottom stereo)", 567 | "uri": "https://storage.googleapis.com/exoplayer-test-media-1/360/congo.mp4", 568 | "spherical_stereo_mode": "top_bottom" 569 | }, 570 | { 571 | "name": "Sphericalv2 (180 top-bottom stereo)", 572 | "uri": "https://storage.googleapis.com/exoplayer-test-media-1/360/sphericalv2.mp4", 573 | "spherical_stereo_mode": "top_bottom" 574 | }, 575 | { 576 | "name": "Iceland (360 top-bottom stereo ts)", 577 | "uri": "https://storage.googleapis.com/exoplayer-test-media-1/360/iceland0.ts", 578 | "spherical_stereo_mode": "top_bottom" 579 | } 580 | ] 581 | } 582 | ] 583 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/andrioidexoplayertrackselection/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.example.andrioidexoplayertrackselection 2 | 3 | import android.content.Intent 4 | import android.net.Uri 5 | import androidx.appcompat.app.AppCompatActivity 6 | import android.os.Bundle 7 | import com.example.andrioidexoplayertrackselection.exoplayer2.PlayerActivity 8 | import com.example.andrioidexoplayertrackselection.exoplayer2.PlayerActivity.EXTENSION_EXTRA 9 | import kotlinx.android.synthetic.main.activity_main.* 10 | 11 | class MainActivity : AppCompatActivity() { 12 | 13 | // Sample videos, more can be found in the media.exolist.json in the assets folder 14 | private val hls = "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_16x9/bipbop_16x9_variant.m3u8" 15 | private val dash = "http://www.youtube.com/api/manifest/dash/id/bf5bb2419360daf1/source/youtube?as=fmp4_audio_clear,fmp4_sd_hd_clear&sparams=ip,ipbits,expire,source,id,as&ip=0.0.0.0&ipbits=0&expire=19000000000&signature=51AF5F39AB0CEC3E5497CD9C900EBFEAECCCB5C7.8506521BFC350652163895D4C26DEE124209AA9E&key=ik0" 16 | private val mp4 = "https://html5demos.com/assets/dizzy.mp4" 17 | 18 | override fun onCreate(savedInstanceState: Bundle?) { 19 | super.onCreate(savedInstanceState) 20 | setContentView(R.layout.activity_main) 21 | 22 | btn1.setOnClickListener { et_url.setText(hls) } 23 | btn2.setOnClickListener { et_url.setText(dash) } 24 | btn3.setOnClickListener { et_url.setText(mp4) } 25 | 26 | btn_play.setOnClickListener { 27 | val preferExtensionDecoders = false 28 | val abrAlgorithm = PlayerActivity.ABR_ALGORITHM_DEFAULT 29 | val intent = Intent(this, PlayerActivity::class.java) 30 | 31 | val videoUrl = et_url.text.toString() 32 | intent.putExtra(PlayerActivity.PREFER_EXTENSION_DECODERS_EXTRA, preferExtensionDecoders) 33 | intent.putExtra(PlayerActivity.ABR_ALGORITHM_EXTRA, abrAlgorithm) 34 | intent.data = Uri.parse(videoUrl) 35 | intent.action = PlayerActivity.ACTION_VIEW 36 | 37 | // this video url doesn't end with a file extension, therefore it needs a extension override 38 | // for buildMediaSource(Uri uri, @Nullable String overrideExtension) in the PlayerActivity 39 | if (videoUrl == dash) { 40 | intent.putExtra(EXTENSION_EXTRA, ".mpd") 41 | } 42 | 43 | startActivity(intent) 44 | } 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/andrioidexoplayertrackselection/exoplayer2/DemoDownloadService.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2017 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.example.andrioidexoplayertrackselection.exoplayer2; 17 | 18 | import android.app.Notification; 19 | 20 | import com.example.andrioidexoplayertrackselection.R; 21 | import com.google.android.exoplayer2.offline.Download; 22 | import com.google.android.exoplayer2.offline.DownloadManager; 23 | import com.google.android.exoplayer2.offline.DownloadService; 24 | import com.google.android.exoplayer2.scheduler.PlatformScheduler; 25 | import com.google.android.exoplayer2.ui.DownloadNotificationHelper; 26 | import com.google.android.exoplayer2.util.NotificationUtil; 27 | import com.google.android.exoplayer2.util.Util; 28 | 29 | import java.util.List; 30 | 31 | /** A service for downloading media. */ 32 | public class DemoDownloadService extends DownloadService { 33 | 34 | private static final String CHANNEL_ID = "download_channel"; 35 | private static final int JOB_ID = 1; 36 | private static final int FOREGROUND_NOTIFICATION_ID = 1; 37 | 38 | private static int nextNotificationId = FOREGROUND_NOTIFICATION_ID + 1; 39 | 40 | private DownloadNotificationHelper notificationHelper; 41 | 42 | public DemoDownloadService() { 43 | super( 44 | FOREGROUND_NOTIFICATION_ID, 45 | DEFAULT_FOREGROUND_NOTIFICATION_UPDATE_INTERVAL, 46 | CHANNEL_ID, 47 | R.string.exo_download_notification_channel_name, 48 | /* channelDescriptionResourceId= */ 0); 49 | nextNotificationId = FOREGROUND_NOTIFICATION_ID + 1; 50 | } 51 | 52 | @Override 53 | public void onCreate() { 54 | super.onCreate(); 55 | notificationHelper = new DownloadNotificationHelper(this, CHANNEL_ID); 56 | } 57 | 58 | @Override 59 | protected DownloadManager getDownloadManager() { 60 | return VideoDownloadManager.getInstance(getApplicationContext()).getDownloadManager(); 61 | } 62 | 63 | @Override 64 | protected PlatformScheduler getScheduler() { 65 | return Util.SDK_INT >= 21 ? new PlatformScheduler(this, JOB_ID) : null; 66 | } 67 | 68 | @Override 69 | protected Notification getForegroundNotification(List downloads) { 70 | return notificationHelper.buildProgressNotification( 71 | R.drawable.ic_download, /* contentIntent= */ null, /* message= */ null, downloads); 72 | } 73 | 74 | @Override 75 | protected void onDownloadChanged(Download download) { 76 | Notification notification; 77 | if (download.state == Download.STATE_COMPLETED) { 78 | notification = 79 | notificationHelper.buildDownloadCompletedNotification( 80 | R.drawable.ic_download_done, 81 | /* contentIntent= */ null, 82 | Util.fromUtf8Bytes(download.request.data)); 83 | } else if (download.state == Download.STATE_FAILED) { 84 | notification = 85 | notificationHelper.buildDownloadFailedNotification( 86 | R.drawable.ic_download_done, 87 | /* contentIntent= */ null, 88 | Util.fromUtf8Bytes(download.request.data)); 89 | } else { 90 | return; 91 | } 92 | NotificationUtil.setNotification(this, nextNotificationId++, notification); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/andrioidexoplayertrackselection/exoplayer2/DownloadTracker.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2017 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.example.andrioidexoplayertrackselection.exoplayer2; 17 | 18 | import android.content.Context; 19 | import android.content.DialogInterface; 20 | import android.net.Uri; 21 | import android.widget.Toast; 22 | 23 | import androidx.annotation.Nullable; 24 | import androidx.fragment.app.FragmentManager; 25 | 26 | import com.example.andrioidexoplayertrackselection.R; 27 | import com.example.andrioidexoplayertrackselection.track_selection_dialog.TrackSelectionDialog; 28 | import com.google.android.exoplayer2.C; 29 | import com.google.android.exoplayer2.RenderersFactory; 30 | import com.google.android.exoplayer2.offline.Download; 31 | import com.google.android.exoplayer2.offline.DownloadCursor; 32 | import com.google.android.exoplayer2.offline.DownloadHelper; 33 | import com.google.android.exoplayer2.offline.DownloadIndex; 34 | import com.google.android.exoplayer2.offline.DownloadManager; 35 | import com.google.android.exoplayer2.offline.DownloadRequest; 36 | import com.google.android.exoplayer2.offline.DownloadService; 37 | import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo; 38 | import com.google.android.exoplayer2.upstream.DataSource; 39 | import com.google.android.exoplayer2.util.Log; 40 | import com.google.android.exoplayer2.util.Util; 41 | 42 | import java.io.IOException; 43 | import java.util.HashMap; 44 | import java.util.concurrent.CopyOnWriteArraySet; 45 | 46 | /** Tracks media that has been downloaded. */ 47 | public class DownloadTracker { 48 | 49 | /** Listens for changes in the tracked downloads. */ 50 | public interface Listener { 51 | 52 | /** Called when the tracked downloads changed. */ 53 | void onDownloadsChanged(); 54 | } 55 | 56 | private static final String TAG = "DownloadTracker"; 57 | 58 | private final Context context; 59 | private final DataSource.Factory dataSourceFactory; 60 | private final CopyOnWriteArraySet listeners; 61 | private final HashMap downloads; 62 | private final DownloadIndex downloadIndex; 63 | 64 | @Nullable private StartDownloadDialogHelper startDownloadDialogHelper; 65 | 66 | public DownloadTracker( 67 | Context context, DataSource.Factory dataSourceFactory, DownloadManager downloadManager) { 68 | this.context = context.getApplicationContext(); 69 | this.dataSourceFactory = dataSourceFactory; 70 | listeners = new CopyOnWriteArraySet<>(); 71 | downloads = new HashMap<>(); 72 | downloadIndex = downloadManager.getDownloadIndex(); 73 | downloadManager.addListener(new DownloadManagerListener()); 74 | loadDownloads(); 75 | } 76 | 77 | public void addListener(Listener listener) { 78 | listeners.add(listener); 79 | } 80 | 81 | public void removeListener(Listener listener) { 82 | listeners.remove(listener); 83 | } 84 | 85 | public boolean isDownloaded(Uri uri) { 86 | Download download = downloads.get(uri); 87 | return download != null && download.state != Download.STATE_FAILED; 88 | } 89 | 90 | @SuppressWarnings("unchecked") 91 | public DownloadRequest getDownloadRequest(Uri uri) { 92 | Download download = downloads.get(uri); 93 | return download != null && download.state != Download.STATE_FAILED ? download.request : null; 94 | } 95 | 96 | public void toggleDownload( 97 | FragmentManager fragmentManager, 98 | String name, 99 | Uri uri, 100 | String extension, 101 | RenderersFactory renderersFactory) { 102 | Download download = downloads.get(uri); 103 | if (download != null) { 104 | DownloadService.sendRemoveDownload( 105 | context, DemoDownloadService.class, download.request.id, /* foreground= */ false); 106 | } else { 107 | if (startDownloadDialogHelper != null) { 108 | startDownloadDialogHelper.release(); 109 | } 110 | startDownloadDialogHelper = 111 | new StartDownloadDialogHelper( 112 | fragmentManager, getDownloadHelper(uri, extension, renderersFactory), name); 113 | } 114 | } 115 | 116 | private void loadDownloads() { 117 | try (DownloadCursor loadedDownloads = downloadIndex.getDownloads()) { 118 | while (loadedDownloads.moveToNext()) { 119 | Download download = loadedDownloads.getDownload(); 120 | downloads.put(download.request.uri, download); 121 | } 122 | } catch (IOException e) { 123 | Log.w(TAG, "Failed to query downloads", e); 124 | } 125 | } 126 | 127 | private DownloadHelper getDownloadHelper( 128 | Uri uri, String extension, RenderersFactory renderersFactory) { 129 | int type = Util.inferContentType(uri, extension); 130 | switch (type) { 131 | case C.TYPE_DASH: 132 | return DownloadHelper.forDash(uri, dataSourceFactory, renderersFactory); 133 | case C.TYPE_SS: 134 | return DownloadHelper.forSmoothStreaming(uri, dataSourceFactory, renderersFactory); 135 | case C.TYPE_HLS: 136 | return DownloadHelper.forHls(uri, dataSourceFactory, renderersFactory); 137 | case C.TYPE_OTHER: 138 | return DownloadHelper.forProgressive(uri); 139 | default: 140 | throw new IllegalStateException("Unsupported type: " + type); 141 | } 142 | } 143 | 144 | private class DownloadManagerListener implements DownloadManager.Listener { 145 | 146 | @Override 147 | public void onDownloadChanged(DownloadManager downloadManager, Download download) { 148 | downloads.put(download.request.uri, download); 149 | for (Listener listener : listeners) { 150 | listener.onDownloadsChanged(); 151 | } 152 | } 153 | 154 | @Override 155 | public void onDownloadRemoved(DownloadManager downloadManager, Download download) { 156 | downloads.remove(download.request.uri); 157 | for (Listener listener : listeners) { 158 | listener.onDownloadsChanged(); 159 | } 160 | } 161 | } 162 | 163 | private final class StartDownloadDialogHelper 164 | implements DownloadHelper.Callback, 165 | DialogInterface.OnClickListener, 166 | DialogInterface.OnDismissListener { 167 | 168 | private final FragmentManager fragmentManager; 169 | private final DownloadHelper downloadHelper; 170 | private final String name; 171 | 172 | private TrackSelectionDialog trackSelectionDialog; 173 | private MappedTrackInfo mappedTrackInfo; 174 | 175 | public StartDownloadDialogHelper( 176 | FragmentManager fragmentManager, DownloadHelper downloadHelper, String name) { 177 | this.fragmentManager = fragmentManager; 178 | this.downloadHelper = downloadHelper; 179 | this.name = name; 180 | downloadHelper.prepare(this); 181 | } 182 | 183 | public void release() { 184 | downloadHelper.release(); 185 | if (trackSelectionDialog != null) { 186 | trackSelectionDialog.dismiss(); 187 | } 188 | } 189 | 190 | // DownloadHelper.Callback implementation. 191 | 192 | @Override 193 | public void onPrepared(DownloadHelper helper) { 194 | if (helper.getPeriodCount() == 0) { 195 | Log.d(TAG, "No periods found. Downloading entire stream."); 196 | startDownload(); 197 | downloadHelper.release(); 198 | return; 199 | } 200 | mappedTrackInfo = downloadHelper.getMappedTrackInfo(/* periodIndex= */ 0); 201 | if (!TrackSelectionDialog.willHaveContent(mappedTrackInfo)) { 202 | Log.d(TAG, "No dialog content. Downloading entire stream."); 203 | startDownload(); 204 | downloadHelper.release(); 205 | return; 206 | } 207 | trackSelectionDialog = 208 | TrackSelectionDialog.createForMappedTrackInfoAndParameters( 209 | /* titleId= */ R.string.exo_download_description, 210 | mappedTrackInfo, 211 | /* initialParameters= */ DownloadHelper.DEFAULT_TRACK_SELECTOR_PARAMETERS, 212 | /* allowAdaptiveSelections =*/ false, 213 | /* allowMultipleOverrides= */ true, 214 | /* onClickListener= */ this, 215 | /* onDismissListener= */ this); 216 | trackSelectionDialog.show(fragmentManager, /* tag= */ null); 217 | } 218 | 219 | @Override 220 | public void onPrepareError(DownloadHelper helper, IOException e) { 221 | Toast.makeText( 222 | context.getApplicationContext(), R.string.download_start_error, Toast.LENGTH_LONG) 223 | .show(); 224 | Log.e(TAG, "Failed to start download", e); 225 | } 226 | 227 | // DialogInterface.OnClickListener implementation. 228 | 229 | @Override 230 | public void onClick(DialogInterface dialog, int which) { 231 | for (int periodIndex = 0; periodIndex < downloadHelper.getPeriodCount(); periodIndex++) { 232 | downloadHelper.clearTrackSelections(periodIndex); 233 | for (int i = 0; i < mappedTrackInfo.getRendererCount(); i++) { 234 | if (!trackSelectionDialog.getIsDisabled(/* rendererIndex= */ i)) { 235 | downloadHelper.addTrackSelectionForSingleRenderer( 236 | periodIndex, 237 | /* rendererIndex= */ i, 238 | DownloadHelper.DEFAULT_TRACK_SELECTOR_PARAMETERS, 239 | trackSelectionDialog.getOverrides(/* rendererIndex= */ i)); 240 | } 241 | } 242 | } 243 | DownloadRequest downloadRequest = buildDownloadRequest(); 244 | if (downloadRequest.streamKeys.isEmpty()) { 245 | // All tracks were deselected in the dialog. Don't start the download. 246 | return; 247 | } 248 | startDownload(downloadRequest); 249 | } 250 | 251 | // DialogInterface.OnDismissListener implementation. 252 | 253 | @Override 254 | public void onDismiss(DialogInterface dialogInterface) { 255 | trackSelectionDialog = null; 256 | downloadHelper.release(); 257 | } 258 | 259 | // Internal methods. 260 | 261 | private void startDownload() { 262 | startDownload(buildDownloadRequest()); 263 | } 264 | 265 | private void startDownload(DownloadRequest downloadRequest) { 266 | DownloadService.sendAddDownload( 267 | context, DemoDownloadService.class, downloadRequest, /* foreground= */ false); 268 | } 269 | 270 | private DownloadRequest buildDownloadRequest() { 271 | return downloadHelper.getDownloadRequest(Util.getUtf8Bytes(name)); 272 | } 273 | } 274 | } 275 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/andrioidexoplayertrackselection/exoplayer2/PlayerActivity.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.example.andrioidexoplayertrackselection.exoplayer2; 17 | 18 | import android.content.Intent; 19 | import android.content.pm.PackageManager; 20 | import android.net.Uri; 21 | import android.os.Bundle; 22 | import android.util.Log; 23 | import android.util.Pair; 24 | import android.view.KeyEvent; 25 | import android.view.View; 26 | import android.view.View.OnClickListener; 27 | import android.widget.Button; 28 | import android.widget.LinearLayout; 29 | import android.widget.TextView; 30 | import android.widget.Toast; 31 | 32 | import androidx.annotation.NonNull; 33 | import androidx.annotation.Nullable; 34 | import androidx.appcompat.app.AppCompatActivity; 35 | 36 | import com.example.andrioidexoplayertrackselection.R; 37 | import com.example.andrioidexoplayertrackselection.track_selection_dialog.TrackSelectionDialog; 38 | import com.google.android.exoplayer2.C; 39 | import com.google.android.exoplayer2.C.ContentType; 40 | import com.google.android.exoplayer2.ExoPlaybackException; 41 | import com.google.android.exoplayer2.ExoPlayerFactory; 42 | import com.google.android.exoplayer2.PlaybackPreparer; 43 | import com.google.android.exoplayer2.Player; 44 | import com.google.android.exoplayer2.RendererCapabilities; 45 | import com.google.android.exoplayer2.RenderersFactory; 46 | import com.google.android.exoplayer2.SimpleExoPlayer; 47 | import com.google.android.exoplayer2.drm.DefaultDrmSessionManager; 48 | import com.google.android.exoplayer2.drm.FrameworkMediaCrypto; 49 | import com.google.android.exoplayer2.drm.FrameworkMediaDrm; 50 | import com.google.android.exoplayer2.drm.HttpMediaDrmCallback; 51 | import com.google.android.exoplayer2.drm.UnsupportedDrmException; 52 | import com.google.android.exoplayer2.mediacodec.MediaCodecRenderer.DecoderInitializationException; 53 | import com.google.android.exoplayer2.mediacodec.MediaCodecUtil.DecoderQueryException; 54 | import com.google.android.exoplayer2.offline.DownloadHelper; 55 | import com.google.android.exoplayer2.offline.DownloadRequest; 56 | import com.google.android.exoplayer2.source.BehindLiveWindowException; 57 | import com.google.android.exoplayer2.source.ConcatenatingMediaSource; 58 | import com.google.android.exoplayer2.source.MediaSource; 59 | import com.google.android.exoplayer2.source.ProgressiveMediaSource; 60 | import com.google.android.exoplayer2.source.TrackGroupArray; 61 | import com.google.android.exoplayer2.source.ads.AdsLoader; 62 | import com.google.android.exoplayer2.source.ads.AdsMediaSource; 63 | import com.google.android.exoplayer2.source.dash.DashMediaSource; 64 | import com.google.android.exoplayer2.source.hls.HlsMediaSource; 65 | import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource; 66 | import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection; 67 | import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; 68 | import com.google.android.exoplayer2.trackselection.MappingTrackSelector; 69 | import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo; 70 | import com.google.android.exoplayer2.trackselection.RandomTrackSelection; 71 | import com.google.android.exoplayer2.trackselection.TrackSelection; 72 | import com.google.android.exoplayer2.trackselection.TrackSelectionArray; 73 | import com.google.android.exoplayer2.ui.DebugTextViewHelper; 74 | import com.google.android.exoplayer2.ui.DefaultTrackNameProvider; 75 | import com.google.android.exoplayer2.ui.PlayerControlView; 76 | import com.google.android.exoplayer2.ui.PlayerView; 77 | import com.google.android.exoplayer2.ui.spherical.SphericalSurfaceView; 78 | import com.google.android.exoplayer2.upstream.DataSource; 79 | import com.google.android.exoplayer2.upstream.HttpDataSource; 80 | import com.google.android.exoplayer2.util.Assertions; 81 | import com.google.android.exoplayer2.util.ErrorMessageProvider; 82 | import com.google.android.exoplayer2.util.EventLogger; 83 | import com.google.android.exoplayer2.util.Util; 84 | import com.google.gson.Gson; 85 | 86 | import java.lang.reflect.Constructor; 87 | import java.net.CookieHandler; 88 | import java.net.CookieManager; 89 | import java.net.CookiePolicy; 90 | import java.util.UUID; 91 | 92 | /** An activity that plays media using {@link SimpleExoPlayer}. */ 93 | public class PlayerActivity extends AppCompatActivity 94 | implements OnClickListener, PlaybackPreparer, PlayerControlView.VisibilityListener { 95 | 96 | public static final String TAG = "PlayerActivity"; 97 | public static final String DRM_SCHEME_EXTRA = "drm_scheme"; 98 | public static final String DRM_LICENSE_URL_EXTRA = "drm_license_url"; 99 | public static final String DRM_KEY_REQUEST_PROPERTIES_EXTRA = "drm_key_request_properties"; 100 | public static final String DRM_MULTI_SESSION_EXTRA = "drm_multi_session"; 101 | public static final String PREFER_EXTENSION_DECODERS_EXTRA = "prefer_extension_decoders"; 102 | 103 | public static final String ACTION_VIEW = "com.google.android.exoplayer.demo.action.VIEW"; 104 | public static final String EXTENSION_EXTRA = "extension"; 105 | 106 | public static final String ACTION_VIEW_LIST = 107 | "com.google.android.exoplayer.demo.action.VIEW_LIST"; 108 | public static final String URI_LIST_EXTRA = "uri_list"; 109 | public static final String EXTENSION_LIST_EXTRA = "extension_list"; 110 | 111 | public static final String AD_TAG_URI_EXTRA = "ad_tag_uri"; 112 | 113 | public static final String ABR_ALGORITHM_EXTRA = "abr_algorithm"; 114 | public static final String ABR_ALGORITHM_DEFAULT = "default"; 115 | public static final String ABR_ALGORITHM_RANDOM = "random"; 116 | 117 | public static final String SPHERICAL_STEREO_MODE_EXTRA = "spherical_stereo_mode"; 118 | public static final String SPHERICAL_STEREO_MODE_MONO = "mono"; 119 | public static final String SPHERICAL_STEREO_MODE_TOP_BOTTOM = "top_bottom"; 120 | public static final String SPHERICAL_STEREO_MODE_LEFT_RIGHT = "left_right"; 121 | 122 | // For backwards compatibility only. 123 | private static final String DRM_SCHEME_UUID_EXTRA = "drm_scheme_uuid"; 124 | 125 | // Saved instance state keys. 126 | private static final String KEY_TRACK_SELECTOR_PARAMETERS = "track_selector_parameters"; 127 | private static final String KEY_WINDOW = "window"; 128 | private static final String KEY_POSITION = "position"; 129 | private static final String KEY_AUTO_PLAY = "auto_play"; 130 | 131 | private static final CookieManager DEFAULT_COOKIE_MANAGER; 132 | static { 133 | DEFAULT_COOKIE_MANAGER = new CookieManager(); 134 | DEFAULT_COOKIE_MANAGER.setCookiePolicy(CookiePolicy.ACCEPT_ORIGINAL_SERVER); 135 | } 136 | 137 | private PlayerView playerView; 138 | private LinearLayout debugRootView; 139 | private Button selectTracksButton; 140 | private Button logTracksButton; 141 | private Button showTextCaptionsButton; 142 | private Button removeTextCaptionsButton; 143 | private TextView debugTextView; 144 | private boolean isShowingTrackSelectionDialog; 145 | 146 | private DataSource.Factory dataSourceFactory; 147 | private SimpleExoPlayer player; 148 | private FrameworkMediaDrm mediaDrm; 149 | private MediaSource mediaSource; 150 | private DefaultTrackSelector trackSelector; 151 | private DefaultTrackSelector.Parameters trackSelectorParameters; 152 | private DebugTextViewHelper debugViewHelper; 153 | private TrackGroupArray lastSeenTrackGroupArray; 154 | 155 | private boolean startAutoPlay; 156 | private int startWindow; 157 | private long startPosition; 158 | 159 | // Fields used only for ad playback. The ads loader is loaded via reflection. 160 | 161 | private AdsLoader adsLoader; 162 | private Uri loadedAdTagUri; 163 | 164 | // Activity lifecycle 165 | 166 | 167 | private void initDebugViews() { 168 | logTracksButton.setOnClickListener( view -> { 169 | Log.d(TAG, "log tracks clicked"); 170 | MappedTrackInfo mappedTrackInfo = Assertions.checkNotNull(trackSelector.getCurrentMappedTrackInfo()); 171 | DefaultTrackSelector.Parameters parameters = trackSelector.getParameters(); 172 | 173 | for (int rendererIndex = 0; rendererIndex < mappedTrackInfo.getRendererCount(); rendererIndex++) { 174 | if (TrackSelectionDialog.showTabForRenderer(mappedTrackInfo, rendererIndex)) { 175 | int trackType = mappedTrackInfo.getRendererType(rendererIndex); 176 | TrackGroupArray trackGroupArray = mappedTrackInfo.getTrackGroups(rendererIndex); 177 | Boolean isRendererDisabled = parameters.getRendererDisabled(rendererIndex); 178 | DefaultTrackSelector.SelectionOverride selectionOverride = parameters.getSelectionOverride(rendererIndex, trackGroupArray); 179 | 180 | Log.d(TAG, "------------------------------------------------------Track item " + rendererIndex + "------------------------------------------------------"); 181 | Log.d(TAG, "track type: " + trackTypeToName(trackType)); 182 | Log.d(TAG, "track group array: " + new Gson().toJson(trackGroupArray)); 183 | for (int groupIndex = 0; groupIndex < trackGroupArray.length; groupIndex++) { 184 | for (int trackIndex = 0; trackIndex < trackGroupArray.get(groupIndex).length; trackIndex++) { 185 | String trackName = new DefaultTrackNameProvider(getResources()).getTrackName(trackGroupArray.get(groupIndex).getFormat(trackIndex)); 186 | Boolean isTrackSupported = mappedTrackInfo.getTrackSupport(rendererIndex, groupIndex, trackIndex) == RendererCapabilities.FORMAT_HANDLED; 187 | Log.d(TAG, "track item " + groupIndex +": trackName: " + trackName + ", isTrackSupported: " + isTrackSupported); 188 | } 189 | } 190 | Log.d(TAG, "isRendererDisabled: " + isRendererDisabled); 191 | Log.d(TAG, "selectionOverride: " + new Gson().toJson(selectionOverride)); 192 | } 193 | } 194 | 195 | }); 196 | 197 | showTextCaptionsButton.setOnClickListener(view -> { 198 | Log.d(TAG, "showTextCaptionsButton clicked"); 199 | 200 | MappedTrackInfo mappedTrackInfo = Assertions.checkNotNull(trackSelector.getCurrentMappedTrackInfo()); 201 | DefaultTrackSelector.Parameters parameters = trackSelector.getParameters(); 202 | DefaultTrackSelector.ParametersBuilder builder = parameters.buildUpon(); 203 | for (int rendererIndex = 0; rendererIndex < mappedTrackInfo.getRendererCount(); rendererIndex++) { 204 | int trackType = mappedTrackInfo.getRendererType(rendererIndex); 205 | if (trackType == C.TRACK_TYPE_TEXT) { 206 | builder.clearSelectionOverrides(rendererIndex).setRendererDisabled(rendererIndex, false); 207 | //{"data":0,"groupIndex":1,"length":1,"reason":2,"tracks":[0]} 208 | int groupIndex = 0; 209 | int [] tracks = {0}; 210 | int reason = 2; 211 | int data = 0; 212 | DefaultTrackSelector.SelectionOverride override = new DefaultTrackSelector.SelectionOverride(groupIndex, tracks, reason, data); 213 | 214 | builder.setSelectionOverride(rendererIndex, mappedTrackInfo.getTrackGroups(rendererIndex), override); 215 | } 216 | } 217 | 218 | trackSelector.setParameters(builder); 219 | 220 | }); 221 | 222 | removeTextCaptionsButton.setOnClickListener(view -> { 223 | Log.d(TAG, "removeTextCaptionsButton clicked"); 224 | MappedTrackInfo mappedTrackInfo = Assertions.checkNotNull(trackSelector.getCurrentMappedTrackInfo()); 225 | DefaultTrackSelector.Parameters parameters = trackSelector.getParameters(); 226 | DefaultTrackSelector.ParametersBuilder builder = parameters.buildUpon(); 227 | for (int rendererIndex = 0; rendererIndex < mappedTrackInfo.getRendererCount(); rendererIndex++) { 228 | int trackType = mappedTrackInfo.getRendererType(rendererIndex); 229 | if (trackType == C.TRACK_TYPE_TEXT) { 230 | builder.clearSelectionOverrides(rendererIndex).setRendererDisabled(rendererIndex, true); 231 | } 232 | } 233 | trackSelector.setParameters(builder); 234 | }); 235 | 236 | } 237 | 238 | @Override 239 | public void onCreate(Bundle savedInstanceState) { 240 | String sphericalStereoMode = getIntent().getStringExtra(SPHERICAL_STEREO_MODE_EXTRA); 241 | if (sphericalStereoMode != null) { 242 | setTheme(R.style.PlayerTheme_Spherical); 243 | } 244 | super.onCreate(savedInstanceState); 245 | dataSourceFactory = buildDataSourceFactory(); 246 | if (CookieHandler.getDefault() != DEFAULT_COOKIE_MANAGER) { 247 | CookieHandler.setDefault(DEFAULT_COOKIE_MANAGER); 248 | } 249 | 250 | setContentView(R.layout.player_activity); 251 | debugRootView = findViewById(R.id.controls_root); 252 | debugTextView = findViewById(R.id.debug_text_view); 253 | selectTracksButton = findViewById(R.id.select_tracks_button); 254 | selectTracksButton.setOnClickListener(this); 255 | 256 | logTracksButton = findViewById(R.id.log_tracks); 257 | showTextCaptionsButton = findViewById(R.id.show_text_caption); 258 | removeTextCaptionsButton = findViewById(R.id.remove_text_caption); 259 | initDebugViews(); 260 | 261 | playerView = findViewById(R.id.player_view); 262 | playerView.setControllerVisibilityListener(this); 263 | playerView.setErrorMessageProvider(new PlayerErrorMessageProvider()); 264 | playerView.requestFocus(); 265 | if (sphericalStereoMode != null) { 266 | int stereoMode; 267 | if (SPHERICAL_STEREO_MODE_MONO.equals(sphericalStereoMode)) { 268 | stereoMode = C.STEREO_MODE_MONO; 269 | } else if (SPHERICAL_STEREO_MODE_TOP_BOTTOM.equals(sphericalStereoMode)) { 270 | stereoMode = C.STEREO_MODE_TOP_BOTTOM; 271 | } else if (SPHERICAL_STEREO_MODE_LEFT_RIGHT.equals(sphericalStereoMode)) { 272 | stereoMode = C.STEREO_MODE_LEFT_RIGHT; 273 | } else { 274 | showToast(R.string.error_unrecognized_stereo_mode); 275 | finish(); 276 | return; 277 | } 278 | ((SphericalSurfaceView) playerView.getVideoSurfaceView()).setDefaultStereoMode(stereoMode); 279 | } 280 | 281 | if (savedInstanceState != null) { 282 | trackSelectorParameters = savedInstanceState.getParcelable(KEY_TRACK_SELECTOR_PARAMETERS); 283 | startAutoPlay = savedInstanceState.getBoolean(KEY_AUTO_PLAY); 284 | startWindow = savedInstanceState.getInt(KEY_WINDOW); 285 | startPosition = savedInstanceState.getLong(KEY_POSITION); 286 | } else { 287 | trackSelectorParameters = new DefaultTrackSelector.ParametersBuilder().build(); 288 | clearStartPosition(); 289 | } 290 | } 291 | 292 | private String trackTypeToName(int trackType) { 293 | switch(trackType) { 294 | case C.TRACK_TYPE_VIDEO: 295 | return "TRACK_TYPE_VIDEO"; 296 | case C.TRACK_TYPE_AUDIO: 297 | return "TRACK_TYPE_AUDIO"; 298 | case C.TRACK_TYPE_TEXT: 299 | return "TRACK_TYPE_TEXT"; 300 | default: 301 | return "Invalid track type"; 302 | } 303 | } 304 | 305 | 306 | @Override 307 | public void onNewIntent(Intent intent) { 308 | super.onNewIntent(intent); 309 | releasePlayer(); 310 | releaseAdsLoader(); 311 | clearStartPosition(); 312 | setIntent(intent); 313 | } 314 | 315 | @Override 316 | public void onStart() { 317 | super.onStart(); 318 | if (Util.SDK_INT > 23) { 319 | initializePlayer(); 320 | if (playerView != null) { 321 | playerView.onResume(); 322 | } 323 | } 324 | } 325 | 326 | @Override 327 | public void onResume() { 328 | super.onResume(); 329 | if (Util.SDK_INT <= 23 || player == null) { 330 | initializePlayer(); 331 | if (playerView != null) { 332 | playerView.onResume(); 333 | } 334 | } 335 | } 336 | 337 | @Override 338 | public void onPause() { 339 | super.onPause(); 340 | if (Util.SDK_INT <= 23) { 341 | if (playerView != null) { 342 | playerView.onPause(); 343 | } 344 | releasePlayer(); 345 | } 346 | } 347 | 348 | @Override 349 | public void onStop() { 350 | super.onStop(); 351 | if (Util.SDK_INT > 23) { 352 | if (playerView != null) { 353 | playerView.onPause(); 354 | } 355 | releasePlayer(); 356 | } 357 | } 358 | 359 | @Override 360 | public void onDestroy() { 361 | super.onDestroy(); 362 | releaseAdsLoader(); 363 | } 364 | 365 | @Override 366 | public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, 367 | @NonNull int[] grantResults) { 368 | if (grantResults.length == 0) { 369 | // Empty results are triggered if a permission is requested while another request was already 370 | // pending and can be safely ignored in this case. 371 | return; 372 | } 373 | if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { 374 | initializePlayer(); 375 | } else { 376 | showToast(R.string.storage_permission_denied); 377 | finish(); 378 | } 379 | } 380 | 381 | @Override 382 | public void onSaveInstanceState(Bundle outState) { 383 | super.onSaveInstanceState(outState); 384 | updateTrackSelectorParameters(); 385 | updateStartPosition(); 386 | outState.putParcelable(KEY_TRACK_SELECTOR_PARAMETERS, trackSelectorParameters); 387 | outState.putBoolean(KEY_AUTO_PLAY, startAutoPlay); 388 | outState.putInt(KEY_WINDOW, startWindow); 389 | outState.putLong(KEY_POSITION, startPosition); 390 | } 391 | 392 | // Activity input 393 | 394 | @Override 395 | public boolean dispatchKeyEvent(KeyEvent event) { 396 | // See whether the player view wants to handle media or DPAD keys events. 397 | return playerView.dispatchKeyEvent(event) || super.dispatchKeyEvent(event); 398 | } 399 | 400 | // OnClickListener methods 401 | 402 | @Override 403 | public void onClick(View view) { 404 | if (view == selectTracksButton && !isShowingTrackSelectionDialog && TrackSelectionDialog.willHaveContent(trackSelector)) { 405 | isShowingTrackSelectionDialog = true; 406 | TrackSelectionDialog trackSelectionDialog = TrackSelectionDialog.createForTrackSelector(trackSelector, dismissedDialog -> 407 | isShowingTrackSelectionDialog = false 408 | ); 409 | trackSelectionDialog.show(getSupportFragmentManager(),null); 410 | } 411 | } 412 | 413 | // PlaybackControlView.PlaybackPreparer implementation 414 | 415 | @Override 416 | public void preparePlayback() { 417 | player.retry(); 418 | } 419 | 420 | // PlaybackControlView.VisibilityListener implementation 421 | 422 | @Override 423 | public void onVisibilityChange(int visibility) { 424 | debugRootView.setVisibility(visibility); 425 | } 426 | 427 | // Internal methods 428 | 429 | private void initializePlayer() { 430 | if (player == null) { 431 | Intent intent = getIntent(); 432 | String action = intent.getAction(); 433 | Uri[] uris; 434 | String[] extensions; 435 | if (ACTION_VIEW.equals(action)) { 436 | uris = new Uri[] {intent.getData()}; 437 | extensions = new String[] {intent.getStringExtra(EXTENSION_EXTRA)}; 438 | } else if (ACTION_VIEW_LIST.equals(action)) { 439 | String[] uriStrings = intent.getStringArrayExtra(URI_LIST_EXTRA); 440 | uris = new Uri[uriStrings.length]; 441 | for (int i = 0; i < uriStrings.length; i++) { 442 | uris[i] = Uri.parse(uriStrings[i]); 443 | } 444 | extensions = intent.getStringArrayExtra(EXTENSION_LIST_EXTRA); 445 | if (extensions == null) { 446 | extensions = new String[uriStrings.length]; 447 | } 448 | } else { 449 | showToast(getString(R.string.unexpected_intent_action, action)); 450 | finish(); 451 | return; 452 | } 453 | if (!Util.checkCleartextTrafficPermitted(uris)) { 454 | showToast(R.string.error_cleartext_not_permitted); 455 | return; 456 | } 457 | if (Util.maybeRequestReadExternalStoragePermission(/* activity= */ this, uris)) { 458 | // The player will be reinitialized if the permission is granted. 459 | return; 460 | } 461 | 462 | DefaultDrmSessionManager drmSessionManager = null; 463 | if (intent.hasExtra(DRM_SCHEME_EXTRA) || intent.hasExtra(DRM_SCHEME_UUID_EXTRA)) { 464 | String drmLicenseUrl = intent.getStringExtra(DRM_LICENSE_URL_EXTRA); 465 | String[] keyRequestPropertiesArray = 466 | intent.getStringArrayExtra(DRM_KEY_REQUEST_PROPERTIES_EXTRA); 467 | boolean multiSession = intent.getBooleanExtra(DRM_MULTI_SESSION_EXTRA, false); 468 | int errorStringId = R.string.error_drm_unknown; 469 | if (Util.SDK_INT < 18) { 470 | errorStringId = R.string.error_drm_not_supported; 471 | } else { 472 | try { 473 | String drmSchemeExtra = intent.hasExtra(DRM_SCHEME_EXTRA) ? DRM_SCHEME_EXTRA 474 | : DRM_SCHEME_UUID_EXTRA; 475 | UUID drmSchemeUuid = Util.getDrmUuid(intent.getStringExtra(drmSchemeExtra)); 476 | if (drmSchemeUuid == null) { 477 | errorStringId = R.string.error_drm_unsupported_scheme; 478 | } else { 479 | drmSessionManager = 480 | buildDrmSessionManagerV18( 481 | drmSchemeUuid, drmLicenseUrl, keyRequestPropertiesArray, multiSession); 482 | } 483 | } catch (UnsupportedDrmException e) { 484 | errorStringId = e.reason == UnsupportedDrmException.REASON_UNSUPPORTED_SCHEME 485 | ? R.string.error_drm_unsupported_scheme : R.string.error_drm_unknown; 486 | } 487 | } 488 | if (drmSessionManager == null) { 489 | showToast(errorStringId); 490 | finish(); 491 | return; 492 | } 493 | } 494 | 495 | TrackSelection.Factory trackSelectionFactory; 496 | String abrAlgorithm = intent.getStringExtra(ABR_ALGORITHM_EXTRA); 497 | if (abrAlgorithm == null || ABR_ALGORITHM_DEFAULT.equals(abrAlgorithm)) { 498 | trackSelectionFactory = new AdaptiveTrackSelection.Factory(); 499 | } else if (ABR_ALGORITHM_RANDOM.equals(abrAlgorithm)) { 500 | trackSelectionFactory = new RandomTrackSelection.Factory(); 501 | } else { 502 | showToast(R.string.error_unrecognized_abr_algorithm); 503 | finish(); 504 | return; 505 | } 506 | 507 | boolean preferExtensionDecoders = 508 | intent.getBooleanExtra(PREFER_EXTENSION_DECODERS_EXTRA, false); 509 | RenderersFactory renderersFactory = VideoDownloadManager.getInstance(getApplicationContext()).buildRenderersFactory(preferExtensionDecoders); 510 | 511 | trackSelector = new DefaultTrackSelector(trackSelectionFactory); 512 | trackSelector.setParameters(trackSelectorParameters); 513 | lastSeenTrackGroupArray = null; 514 | 515 | player = ExoPlayerFactory.newSimpleInstance(this, renderersFactory, trackSelector, drmSessionManager); 516 | player.addListener(new PlayerEventListener()); 517 | player.setPlayWhenReady(startAutoPlay); 518 | player.addAnalyticsListener(new EventLogger(trackSelector)); 519 | playerView.setPlayer(player); 520 | playerView.setPlaybackPreparer(this); 521 | debugViewHelper = new DebugTextViewHelper(player, debugTextView); 522 | debugViewHelper.start(); 523 | 524 | MediaSource[] mediaSources = new MediaSource[uris.length]; 525 | for (int i = 0; i < uris.length; i++) { 526 | mediaSources[i] = buildMediaSource(uris[i], extensions[i]); 527 | } 528 | mediaSource = 529 | mediaSources.length == 1 ? mediaSources[0] : new ConcatenatingMediaSource(mediaSources); 530 | String adTagUriString = intent.getStringExtra(AD_TAG_URI_EXTRA); 531 | if (adTagUriString != null) { 532 | Uri adTagUri = Uri.parse(adTagUriString); 533 | if (!adTagUri.equals(loadedAdTagUri)) { 534 | releaseAdsLoader(); 535 | loadedAdTagUri = adTagUri; 536 | } 537 | MediaSource adsMediaSource = createAdsMediaSource(mediaSource, Uri.parse(adTagUriString)); 538 | if (adsMediaSource != null) { 539 | mediaSource = adsMediaSource; 540 | } else { 541 | showToast(R.string.ima_not_loaded); 542 | } 543 | } else { 544 | releaseAdsLoader(); 545 | } 546 | } 547 | boolean haveStartPosition = startWindow != C.INDEX_UNSET; 548 | if (haveStartPosition) { 549 | player.seekTo(startWindow, startPosition); 550 | } 551 | player.prepare(mediaSource, !haveStartPosition, false); 552 | updateButtonVisibility(); 553 | } 554 | 555 | private MediaSource buildMediaSource(Uri uri) { 556 | return buildMediaSource(uri, null); 557 | } 558 | 559 | private MediaSource buildMediaSource(Uri uri, @Nullable String overrideExtension) { 560 | Log.d(TAG, "buildMediaSource uri: " + uri.toString() + " overrideExtension: " + overrideExtension); 561 | DownloadRequest downloadRequest = VideoDownloadManager.getInstance(getApplicationContext()).getDownloadTracker().getDownloadRequest(uri); 562 | if (downloadRequest != null) { 563 | return DownloadHelper.createMediaSource(downloadRequest, dataSourceFactory); 564 | } 565 | @ContentType int type = Util.inferContentType(uri, overrideExtension); 566 | switch (type) { 567 | case C.TYPE_DASH: 568 | Log.d(TAG, "buildMediaSource TYPE_DASH: TYPE_DASH"); 569 | return new DashMediaSource.Factory(dataSourceFactory).createMediaSource(uri); 570 | case C.TYPE_SS: 571 | Log.d(TAG, "buildMediaSource TYPE_DASH: TYPE_SS"); 572 | return new SsMediaSource.Factory(dataSourceFactory).createMediaSource(uri); 573 | case C.TYPE_HLS: 574 | Log.d(TAG, "buildMediaSource TYPE_DASH: TYPE_HLS"); 575 | return new HlsMediaSource.Factory(dataSourceFactory).createMediaSource(uri); 576 | case C.TYPE_OTHER: 577 | Log.d(TAG, "buildMediaSource TYPE_DASH: TYPE_OTHER"); 578 | return new ProgressiveMediaSource.Factory(dataSourceFactory).createMediaSource(uri); 579 | default: 580 | throw new IllegalStateException("Unsupported type: " + type); 581 | } 582 | } 583 | 584 | private DefaultDrmSessionManager buildDrmSessionManagerV18(UUID uuid, String licenseUrl, String[] keyRequestPropertiesArray, boolean multiSession) throws UnsupportedDrmException { 585 | HttpDataSource.Factory licenseDataSourceFactory = VideoDownloadManager.getInstance(getApplicationContext()).buildHttpDataSourceFactory(); 586 | HttpMediaDrmCallback drmCallback = new HttpMediaDrmCallback(licenseUrl, licenseDataSourceFactory); 587 | if (keyRequestPropertiesArray != null) { 588 | for (int i = 0; i < keyRequestPropertiesArray.length - 1; i += 2) { 589 | drmCallback.setKeyRequestProperty(keyRequestPropertiesArray[i], 590 | keyRequestPropertiesArray[i + 1]); 591 | } 592 | } 593 | releaseMediaDrm(); 594 | mediaDrm = FrameworkMediaDrm.newInstance(uuid); 595 | return new DefaultDrmSessionManager<>(uuid, mediaDrm, drmCallback, null, multiSession); 596 | } 597 | 598 | private void releasePlayer() { 599 | if (player != null) { 600 | updateTrackSelectorParameters(); 601 | updateStartPosition(); 602 | debugViewHelper.stop(); 603 | debugViewHelper = null; 604 | player.release(); 605 | player = null; 606 | mediaSource = null; 607 | trackSelector = null; 608 | } 609 | if (adsLoader != null) { 610 | adsLoader.setPlayer(null); 611 | } 612 | releaseMediaDrm(); 613 | } 614 | 615 | private void releaseMediaDrm() { 616 | if (mediaDrm != null) { 617 | mediaDrm.release(); 618 | mediaDrm = null; 619 | } 620 | } 621 | 622 | private void releaseAdsLoader() { 623 | if (adsLoader != null) { 624 | adsLoader.release(); 625 | adsLoader = null; 626 | loadedAdTagUri = null; 627 | playerView.getOverlayFrameLayout().removeAllViews(); 628 | } 629 | } 630 | 631 | private void updateTrackSelectorParameters() { 632 | if (trackSelector != null) { 633 | trackSelectorParameters = trackSelector.getParameters(); 634 | } 635 | } 636 | 637 | private void updateStartPosition() { 638 | if (player != null) { 639 | startAutoPlay = player.getPlayWhenReady(); 640 | startWindow = player.getCurrentWindowIndex(); 641 | startPosition = Math.max(0, player.getContentPosition()); 642 | } 643 | } 644 | 645 | private void clearStartPosition() { 646 | startAutoPlay = true; 647 | startWindow = C.INDEX_UNSET; 648 | startPosition = C.TIME_UNSET; 649 | } 650 | 651 | /** Returns a new DataSource factory. */ 652 | private DataSource.Factory buildDataSourceFactory() { 653 | return VideoDownloadManager.getInstance(getApplicationContext()).buildDataSourceFactory(); 654 | } 655 | 656 | /** Returns an ads media source, reusing the ads loader if one exists. */ 657 | private @Nullable 658 | MediaSource createAdsMediaSource(MediaSource mediaSource, Uri adTagUri) { 659 | // Load the extension source using reflection so the demo app doesn't have to depend on it. 660 | // The ads loader is reused for multiple playbacks, so that ad playback can resume. 661 | try { 662 | Class loaderClass = Class.forName("com.google.android.exoplayer2.ext.ima.ImaAdsLoader"); 663 | if (adsLoader == null) { 664 | // Full class names used so the LINT.IfChange rule triggers should any of the classes move. 665 | // LINT.IfChange 666 | Constructor loaderConstructor = 667 | loaderClass 668 | .asSubclass(AdsLoader.class) 669 | .getConstructor(android.content.Context.class, Uri.class); 670 | // LINT.ThenChange(../../../../../../../../proguard-rules.txt) 671 | adsLoader = loaderConstructor.newInstance(this, adTagUri); 672 | } 673 | adsLoader.setPlayer(player); 674 | AdsMediaSource.MediaSourceFactory adMediaSourceFactory = 675 | new AdsMediaSource.MediaSourceFactory() { 676 | @Override 677 | public MediaSource createMediaSource(Uri uri) { 678 | return PlayerActivity.this.buildMediaSource(uri); 679 | } 680 | 681 | @Override 682 | public int[] getSupportedTypes() { 683 | return new int[] {C.TYPE_DASH, C.TYPE_SS, C.TYPE_HLS, C.TYPE_OTHER}; 684 | } 685 | }; 686 | return new AdsMediaSource(mediaSource, adMediaSourceFactory, adsLoader, playerView); 687 | } catch (ClassNotFoundException e) { 688 | // IMA extension not loaded. 689 | return null; 690 | } catch (Exception e) { 691 | throw new RuntimeException(e); 692 | } 693 | } 694 | 695 | // User controls 696 | 697 | private void updateButtonVisibility() { 698 | if (player != null && TrackSelectionDialog.willHaveContent(trackSelector)) { 699 | selectTracksButton.setEnabled(true); 700 | logTracksButton.setEnabled(true); 701 | showTextCaptionsButton.setEnabled(true); 702 | removeTextCaptionsButton.setEnabled(true); 703 | } else { 704 | selectTracksButton.setEnabled(false); 705 | logTracksButton.setEnabled(false); 706 | showTextCaptionsButton.setEnabled(false); 707 | removeTextCaptionsButton.setEnabled(false); 708 | } 709 | } 710 | 711 | private void showControls() { 712 | debugRootView.setVisibility(View.VISIBLE); 713 | } 714 | 715 | private void showToast(int messageId) { 716 | showToast(getString(messageId)); 717 | } 718 | 719 | private void showToast(String message) { 720 | Toast.makeText(getApplicationContext(), message, Toast.LENGTH_LONG).show(); 721 | } 722 | 723 | private static boolean isBehindLiveWindow(ExoPlaybackException e) { 724 | if (e.type != ExoPlaybackException.TYPE_SOURCE) { 725 | return false; 726 | } 727 | Throwable cause = e.getSourceException(); 728 | while (cause != null) { 729 | if (cause instanceof BehindLiveWindowException) { 730 | return true; 731 | } 732 | cause = cause.getCause(); 733 | } 734 | return false; 735 | } 736 | 737 | private class PlayerEventListener implements Player.EventListener { 738 | 739 | @Override 740 | public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { 741 | if (playbackState == Player.STATE_ENDED) { 742 | showControls(); 743 | } 744 | updateButtonVisibility(); 745 | } 746 | 747 | @Override 748 | public void onPlayerError(ExoPlaybackException e) { 749 | if (isBehindLiveWindow(e)) { 750 | clearStartPosition(); 751 | initializePlayer(); 752 | } else { 753 | updateButtonVisibility(); 754 | showControls(); 755 | } 756 | } 757 | 758 | @Override 759 | @SuppressWarnings("ReferenceEquality") 760 | public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) { 761 | updateButtonVisibility(); 762 | if (trackGroups != lastSeenTrackGroupArray) { 763 | MappedTrackInfo mappedTrackInfo = trackSelector.getCurrentMappedTrackInfo(); 764 | if (mappedTrackInfo != null) { 765 | if (mappedTrackInfo.getTypeSupport(C.TRACK_TYPE_VIDEO) 766 | == MappedTrackInfo.RENDERER_SUPPORT_UNSUPPORTED_TRACKS) { 767 | showToast(R.string.error_unsupported_video); 768 | } 769 | if (mappedTrackInfo.getTypeSupport(C.TRACK_TYPE_AUDIO) 770 | == MappedTrackInfo.RENDERER_SUPPORT_UNSUPPORTED_TRACKS) { 771 | showToast(R.string.error_unsupported_audio); 772 | } 773 | } 774 | lastSeenTrackGroupArray = trackGroups; 775 | } 776 | } 777 | } 778 | 779 | private class PlayerErrorMessageProvider implements ErrorMessageProvider { 780 | 781 | @Override 782 | public Pair getErrorMessage(ExoPlaybackException e) { 783 | String errorString = getString(R.string.error_generic); 784 | if (e.type == ExoPlaybackException.TYPE_RENDERER) { 785 | Exception cause = e.getRendererException(); 786 | if (cause instanceof DecoderInitializationException) { 787 | // Special case for decoder initialization failures. 788 | DecoderInitializationException decoderInitializationException = 789 | (DecoderInitializationException) cause; 790 | if (decoderInitializationException.decoderName == null) { 791 | if (decoderInitializationException.getCause() instanceof DecoderQueryException) { 792 | errorString = getString(R.string.error_querying_decoders); 793 | } else if (decoderInitializationException.secureDecoderRequired) { 794 | errorString = 795 | getString( 796 | R.string.error_no_secure_decoder, decoderInitializationException.mimeType); 797 | } else { 798 | errorString = 799 | getString(R.string.error_no_decoder, decoderInitializationException.mimeType); 800 | } 801 | } else { 802 | errorString = 803 | getString( 804 | R.string.error_instantiating_decoder, 805 | decoderInitializationException.decoderName); 806 | } 807 | } 808 | } 809 | return Pair.create(0, errorString); 810 | } 811 | } 812 | 813 | } 814 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/andrioidexoplayertrackselection/exoplayer2/VideoDownloadManager.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.example.andrioidexoplayertrackselection.exoplayer2; 17 | 18 | import android.content.Context; 19 | 20 | import com.example.andrioidexoplayertrackselection.BuildConfig; 21 | import com.google.android.exoplayer2.DefaultRenderersFactory; 22 | import com.google.android.exoplayer2.RenderersFactory; 23 | import com.google.android.exoplayer2.database.DatabaseProvider; 24 | import com.google.android.exoplayer2.database.ExoDatabaseProvider; 25 | import com.google.android.exoplayer2.offline.ActionFileUpgradeUtil; 26 | import com.google.android.exoplayer2.offline.DefaultDownloadIndex; 27 | import com.google.android.exoplayer2.offline.DefaultDownloaderFactory; 28 | import com.google.android.exoplayer2.offline.DownloadManager; 29 | import com.google.android.exoplayer2.offline.DownloaderConstructorHelper; 30 | import com.google.android.exoplayer2.upstream.DataSource; 31 | import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; 32 | import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory; 33 | import com.google.android.exoplayer2.upstream.FileDataSourceFactory; 34 | import com.google.android.exoplayer2.upstream.HttpDataSource; 35 | import com.google.android.exoplayer2.upstream.cache.Cache; 36 | import com.google.android.exoplayer2.upstream.cache.CacheDataSource; 37 | import com.google.android.exoplayer2.upstream.cache.CacheDataSourceFactory; 38 | import com.google.android.exoplayer2.upstream.cache.NoOpCacheEvictor; 39 | import com.google.android.exoplayer2.upstream.cache.SimpleCache; 40 | import com.google.android.exoplayer2.util.Log; 41 | import com.google.android.exoplayer2.util.Util; 42 | 43 | import java.io.File; 44 | import java.io.IOException; 45 | 46 | /** 47 | * Placeholder application to facilitate overriding Application methods for debugging and testing. 48 | */ 49 | public class VideoDownloadManager { 50 | 51 | private static final String TAG = "DemoApplication"; 52 | private static final String DOWNLOAD_ACTION_FILE = "actions"; 53 | private static final String DOWNLOAD_TRACKER_ACTION_FILE = "tracked_actions"; 54 | private static final String DOWNLOAD_CONTENT_DIRECTORY = "downloads"; 55 | 56 | protected String userAgent; 57 | 58 | private DatabaseProvider databaseProvider; 59 | private File downloadDirectory; 60 | private Cache downloadCache; 61 | private DownloadManager downloadManager; 62 | private DownloadTracker downloadTracker; 63 | 64 | 65 | private static VideoDownloadManager INSTANCE = null; 66 | private Context context = null; 67 | 68 | private VideoDownloadManager(){} 69 | 70 | private VideoDownloadManager(Context context){ 71 | this.context = context; 72 | userAgent = Util.getUserAgent(context, "ExoPlayer2DownloadManager"); 73 | } 74 | 75 | public static VideoDownloadManager getInstance(Context context) { 76 | if (INSTANCE == null) { 77 | synchronized(VideoDownloadManager.class) { 78 | if (INSTANCE == null) { 79 | INSTANCE = new VideoDownloadManager(context); 80 | } 81 | } 82 | } 83 | return INSTANCE; 84 | } 85 | /** Returns a {@link DataSource.Factory}. */ 86 | public DataSource.Factory buildDataSourceFactory() { 87 | DefaultDataSourceFactory upstreamFactory = 88 | new DefaultDataSourceFactory(context, buildHttpDataSourceFactory()); 89 | return buildReadOnlyCacheDataSource(upstreamFactory, getDownloadCache()); 90 | } 91 | 92 | /** Returns a {@link HttpDataSource.Factory}. */ 93 | public HttpDataSource.Factory buildHttpDataSourceFactory() { 94 | return new DefaultHttpDataSourceFactory(userAgent); 95 | } 96 | 97 | /** Returns whether extension renderers should be used. */ 98 | public boolean useExtensionRenderers() { 99 | return "withExtensions".equals(BuildConfig.FLAVOR); 100 | } 101 | 102 | public RenderersFactory buildRenderersFactory(boolean preferExtensionRenderer) { 103 | @DefaultRenderersFactory.ExtensionRendererMode 104 | int extensionRendererMode = 105 | useExtensionRenderers() 106 | ? (preferExtensionRenderer 107 | ? DefaultRenderersFactory.EXTENSION_RENDERER_MODE_PREFER 108 | : DefaultRenderersFactory.EXTENSION_RENDERER_MODE_ON) 109 | : DefaultRenderersFactory.EXTENSION_RENDERER_MODE_OFF; 110 | return new DefaultRenderersFactory(/* context= */ context) 111 | .setExtensionRendererMode(extensionRendererMode); 112 | } 113 | 114 | public DownloadManager getDownloadManager() { 115 | initDownloadManager(); 116 | return downloadManager; 117 | } 118 | 119 | public DownloadTracker getDownloadTracker() { 120 | initDownloadManager(); 121 | return downloadTracker; 122 | } 123 | 124 | protected synchronized Cache getDownloadCache() { 125 | if (downloadCache == null) { 126 | File downloadContentDirectory = new File(getDownloadDirectory(), DOWNLOAD_CONTENT_DIRECTORY); 127 | downloadCache = 128 | new SimpleCache(downloadContentDirectory, new NoOpCacheEvictor(), getDatabaseProvider()); 129 | } 130 | return downloadCache; 131 | } 132 | 133 | private synchronized void initDownloadManager() { 134 | if (downloadManager == null) { 135 | DefaultDownloadIndex downloadIndex = new DefaultDownloadIndex(getDatabaseProvider()); 136 | upgradeActionFile( 137 | DOWNLOAD_ACTION_FILE, downloadIndex, /* addNewDownloadsAsCompleted= */ false); 138 | upgradeActionFile( 139 | DOWNLOAD_TRACKER_ACTION_FILE, downloadIndex, /* addNewDownloadsAsCompleted= */ true); 140 | DownloaderConstructorHelper downloaderConstructorHelper = 141 | new DownloaderConstructorHelper(getDownloadCache(), buildHttpDataSourceFactory()); 142 | downloadManager = 143 | new DownloadManager( 144 | context, downloadIndex, new DefaultDownloaderFactory(downloaderConstructorHelper)); 145 | downloadTracker = 146 | new DownloadTracker(/* context= */ context, buildDataSourceFactory(), downloadManager); 147 | } 148 | } 149 | 150 | private void upgradeActionFile( 151 | String fileName, DefaultDownloadIndex downloadIndex, boolean addNewDownloadsAsCompleted) { 152 | try { 153 | ActionFileUpgradeUtil.upgradeAndDelete( 154 | new File(getDownloadDirectory(), fileName), 155 | /* downloadIdProvider= */ null, 156 | downloadIndex, 157 | /* deleteOnFailure= */ true, 158 | addNewDownloadsAsCompleted); 159 | } catch (IOException e) { 160 | Log.e(TAG, "Failed to upgrade action file: " + fileName, e); 161 | } 162 | } 163 | 164 | private DatabaseProvider getDatabaseProvider() { 165 | if (databaseProvider == null) { 166 | databaseProvider = new ExoDatabaseProvider(context); 167 | } 168 | return databaseProvider; 169 | } 170 | 171 | private File getDownloadDirectory() { 172 | if (downloadDirectory == null) { 173 | downloadDirectory = context.getExternalFilesDir(null); 174 | if (downloadDirectory == null) { 175 | downloadDirectory = context.getFilesDir(); 176 | } 177 | } 178 | return downloadDirectory; 179 | } 180 | 181 | protected static CacheDataSourceFactory buildReadOnlyCacheDataSource( 182 | DataSource.Factory upstreamFactory, Cache cache) { 183 | return new CacheDataSourceFactory( 184 | cache, 185 | upstreamFactory, 186 | new FileDataSourceFactory(), 187 | /* cacheWriteDataSinkFactory= */ null, 188 | CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR, 189 | /* eventListener= */ null); 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/andrioidexoplayertrackselection/track_selection_dialog/FragmentAdapter.java: -------------------------------------------------------------------------------- 1 | package com.example.andrioidexoplayertrackselection.track_selection_dialog; 2 | 3 | import android.content.Context; 4 | import android.util.SparseArray; 5 | 6 | import androidx.annotation.Nullable; 7 | import androidx.fragment.app.Fragment; 8 | import androidx.fragment.app.FragmentManager; 9 | import androidx.fragment.app.FragmentPagerAdapter; 10 | 11 | import com.example.andrioidexoplayertrackselection.R; 12 | import com.google.android.exoplayer2.C; 13 | 14 | import java.util.ArrayList; 15 | 16 | public class FragmentAdapter extends FragmentPagerAdapter { 17 | 18 | private final SparseArray tabFragments; 19 | private final ArrayList tabTrackTypes; 20 | private Context context; 21 | 22 | public FragmentAdapter(FragmentManager fragmentManager, Context context, SparseArray tabFragments, ArrayList tabTrackTypes) { 23 | super(fragmentManager); 24 | this.context = context.getApplicationContext(); 25 | this.tabFragments = tabFragments; 26 | this.tabTrackTypes = tabTrackTypes; 27 | } 28 | 29 | @Override 30 | public Fragment getItem(int position) { 31 | return tabFragments.valueAt(position); 32 | } 33 | 34 | @Override 35 | public int getCount() { 36 | return tabFragments.size(); 37 | } 38 | 39 | @Nullable 40 | @Override 41 | public CharSequence getPageTitle(int position) { 42 | Integer trackType = tabTrackTypes.get(position); 43 | switch (trackType) { 44 | case C.TRACK_TYPE_VIDEO: 45 | return context.getResources().getString(R.string.exo_track_selection_title_video); 46 | case C.TRACK_TYPE_AUDIO: 47 | return context.getResources().getString(R.string.exo_track_selection_title_audio); 48 | case C.TRACK_TYPE_TEXT: 49 | return context.getResources().getString(R.string.exo_track_selection_title_text); 50 | default: 51 | throw new IllegalArgumentException(); 52 | } 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/andrioidexoplayertrackselection/track_selection_dialog/TrackSelectionDialog.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2019 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.example.andrioidexoplayertrackselection.track_selection_dialog; 17 | 18 | import android.app.Dialog; 19 | import android.content.DialogInterface; 20 | import android.os.Bundle; 21 | import android.util.Log; 22 | import android.util.SparseArray; 23 | import android.view.LayoutInflater; 24 | import android.view.View; 25 | import android.view.ViewGroup; 26 | import android.widget.Button; 27 | 28 | import androidx.annotation.Nullable; 29 | import androidx.appcompat.app.AppCompatDialog; 30 | import androidx.fragment.app.DialogFragment; 31 | import androidx.viewpager.widget.ViewPager; 32 | 33 | import com.example.andrioidexoplayertrackselection.R; 34 | import com.google.android.exoplayer2.C; 35 | import com.google.android.exoplayer2.source.TrackGroupArray; 36 | import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; 37 | import com.google.android.exoplayer2.trackselection.DefaultTrackSelector.SelectionOverride; 38 | import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo; 39 | import com.google.android.exoplayer2.util.Assertions; 40 | import com.google.android.material.tabs.TabLayout; 41 | import com.google.gson.Gson; 42 | 43 | import java.util.ArrayList; 44 | import java.util.Collections; 45 | import java.util.List; 46 | 47 | /** Dialog to select tracks. */ 48 | public final class TrackSelectionDialog extends DialogFragment { 49 | private static String TAG = "TrackSelectionDialog"; 50 | private final SparseArray tabFragments; 51 | private final ArrayList tabTrackTypes; 52 | 53 | private int titleId; 54 | private DialogInterface.OnClickListener onClickListener; 55 | private DialogInterface.OnDismissListener onDismissListener; 56 | 57 | /** 58 | * Returns whether a track selection dialog will have content to display if initialized with the 59 | * specified {@link DefaultTrackSelector} in its current state. 60 | */ 61 | public static boolean willHaveContent(DefaultTrackSelector trackSelector) { 62 | MappedTrackInfo mappedTrackInfo = trackSelector.getCurrentMappedTrackInfo(); 63 | return mappedTrackInfo != null && willHaveContent(mappedTrackInfo); 64 | } 65 | 66 | /** 67 | * Returns whether a track selection dialog will have content to display if initialized with the 68 | * specified {@link MappedTrackInfo}. 69 | */ 70 | public static boolean willHaveContent(MappedTrackInfo mappedTrackInfo) { 71 | for (int i = 0; i < mappedTrackInfo.getRendererCount(); i++) { 72 | if (showTabForRenderer(mappedTrackInfo, i)) { 73 | return true; 74 | } 75 | } 76 | return false; 77 | } 78 | 79 | /** 80 | * Creates a dialog for a given {@link DefaultTrackSelector}, whose parameters will be 81 | * automatically updated when tracks are selected. 82 | * 83 | * @param trackSelector The {@link DefaultTrackSelector}. 84 | * @param onDismissListener A {@link DialogInterface.OnDismissListener} to call when the dialog is 85 | * dismissed. 86 | */ 87 | public static TrackSelectionDialog createForTrackSelector(DefaultTrackSelector trackSelector, DialogInterface.OnDismissListener onDismissListener) { 88 | MappedTrackInfo mappedTrackInfo = Assertions.checkNotNull(trackSelector.getCurrentMappedTrackInfo()); 89 | TrackSelectionDialog trackSelectionDialog = new TrackSelectionDialog(); 90 | DefaultTrackSelector.Parameters parameters = trackSelector.getParameters(); 91 | trackSelectionDialog.init( 92 | /* titleId= */ R.string.track_selection_title, 93 | mappedTrackInfo, 94 | /* initialParameters = */ parameters, 95 | /* allowAdaptiveSelections =*/ true, 96 | /* allowMultipleOverrides= */ false, 97 | /* onClickListener= */ (dialog, which) -> { 98 | DefaultTrackSelector.ParametersBuilder builder = parameters.buildUpon(); 99 | for (int rendererIndex = 0; rendererIndex < mappedTrackInfo.getRendererCount(); rendererIndex++) { 100 | builder.clearSelectionOverrides(rendererIndex).setRendererDisabled(rendererIndex, trackSelectionDialog.getIsDisabled(rendererIndex)); 101 | List overrides = trackSelectionDialog.getOverrides(rendererIndex); 102 | if (!overrides.isEmpty()) { 103 | Log.d(TAG, "override: " + new Gson().toJson(overrides.get(0))); 104 | builder.setSelectionOverride( 105 | rendererIndex, 106 | mappedTrackInfo.getTrackGroups(rendererIndex), 107 | overrides.get(0)); 108 | } 109 | } 110 | trackSelector.setParameters(builder); 111 | }, 112 | onDismissListener); 113 | return trackSelectionDialog; 114 | } 115 | 116 | /** 117 | * Creates a dialog for given {@link MappedTrackInfo} and {@link DefaultTrackSelector.Parameters}. 118 | * 119 | * @param titleId The resource id of the dialog title. 120 | * @param mappedTrackInfo The {@link MappedTrackInfo} to display. 121 | * @param initialParameters The {@link DefaultTrackSelector.Parameters} describing the initial 122 | * track selection. 123 | * @param allowAdaptiveSelections Whether adaptive selections (consisting of more than one track) 124 | * can be made. 125 | * @param allowMultipleOverrides Whether tracks from multiple track groups can be selected. 126 | * @param onClickListener {@link DialogInterface.OnClickListener} called when tracks are selected. 127 | * @param onDismissListener {@link DialogInterface.OnDismissListener} called when the dialog is 128 | * dismissed. 129 | */ 130 | public static TrackSelectionDialog createForMappedTrackInfoAndParameters( 131 | int titleId, 132 | MappedTrackInfo mappedTrackInfo, 133 | DefaultTrackSelector.Parameters initialParameters, 134 | boolean allowAdaptiveSelections, 135 | boolean allowMultipleOverrides, 136 | DialogInterface.OnClickListener onClickListener, 137 | DialogInterface.OnDismissListener onDismissListener) { 138 | TrackSelectionDialog trackSelectionDialog = new TrackSelectionDialog(); 139 | trackSelectionDialog.init( 140 | titleId, 141 | mappedTrackInfo, 142 | initialParameters, 143 | allowAdaptiveSelections, 144 | allowMultipleOverrides, 145 | onClickListener, 146 | onDismissListener); 147 | return trackSelectionDialog; 148 | } 149 | 150 | public TrackSelectionDialog() { 151 | tabFragments = new SparseArray<>(); 152 | tabTrackTypes = new ArrayList<>(); 153 | // Retain instance across activity re-creation to prevent losing access to init data. 154 | setRetainInstance(true); 155 | } 156 | 157 | private void init( 158 | int titleId, 159 | MappedTrackInfo mappedTrackInfo, 160 | DefaultTrackSelector.Parameters initialParameters, 161 | boolean allowAdaptiveSelections, 162 | boolean allowMultipleOverrides, 163 | DialogInterface.OnClickListener onClickListener, 164 | DialogInterface.OnDismissListener onDismissListener) { 165 | this.titleId = titleId; 166 | this.onClickListener = onClickListener; 167 | this.onDismissListener = onDismissListener; 168 | for (int rendererIndex = 0; rendererIndex < mappedTrackInfo.getRendererCount(); rendererIndex++) { 169 | if (showTabForRenderer(mappedTrackInfo, rendererIndex)) { 170 | int trackType = mappedTrackInfo.getRendererType(rendererIndex); 171 | TrackGroupArray trackGroupArray = mappedTrackInfo.getTrackGroups(rendererIndex); 172 | TrackSelectionViewFragment tabFragment = new TrackSelectionViewFragment(); 173 | tabFragment.init( 174 | mappedTrackInfo, 175 | rendererIndex, 176 | initialParameters.getRendererDisabled(rendererIndex), 177 | initialParameters.getSelectionOverride(rendererIndex, trackGroupArray), 178 | allowAdaptiveSelections, 179 | allowMultipleOverrides); 180 | tabFragments.put(rendererIndex, tabFragment); 181 | tabTrackTypes.add(trackType); 182 | } 183 | } 184 | } 185 | 186 | /** 187 | * Returns whether a renderer is disabled. 188 | * 189 | * @param rendererIndex Renderer index. 190 | * @return Whether the renderer is disabled. 191 | */ 192 | public boolean getIsDisabled(int rendererIndex) { 193 | TrackSelectionViewFragment rendererView = tabFragments.get(rendererIndex); 194 | return rendererView != null && rendererView.isDisabled; 195 | } 196 | 197 | /** 198 | * Returns the list of selected track selection overrides for the specified renderer. There will 199 | * be at most one override for each track group. 200 | * 201 | * @param rendererIndex Renderer index. 202 | * @return The list of track selection overrides for this renderer. 203 | */ 204 | public List getOverrides(int rendererIndex) { 205 | TrackSelectionViewFragment rendererView = tabFragments.get(rendererIndex); 206 | return rendererView == null ? Collections.emptyList() : rendererView.overrides; 207 | } 208 | 209 | @Override 210 | public Dialog onCreateDialog(Bundle savedInstanceState) { 211 | // We need to own the view to let tab layout work correctly on all API levels. We can't use 212 | // AlertDialog because it owns the view itself, so we use AppCompatDialog instead, themed using 213 | // the AlertDialog theme overlay with force-enabled title. 214 | AppCompatDialog dialog = 215 | new AppCompatDialog(getActivity(), R.style.TrackSelectionDialogThemeOverlay); 216 | dialog.setTitle(titleId); 217 | return dialog; 218 | } 219 | 220 | @Override 221 | public void onDismiss(DialogInterface dialog) { 222 | super.onDismiss(dialog); 223 | onDismissListener.onDismiss(dialog); 224 | } 225 | 226 | @Nullable 227 | @Override 228 | public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { 229 | 230 | View dialogView = inflater.inflate(R.layout.track_selection_dialog, container, false); 231 | TabLayout tabLayout = dialogView.findViewById(R.id.track_selection_dialog_tab_layout); 232 | ViewPager viewPager = dialogView.findViewById(R.id.track_selection_dialog_view_pager); 233 | Button cancelButton = dialogView.findViewById(R.id.track_selection_dialog_cancel_button); 234 | Button okButton = dialogView.findViewById(R.id.track_selection_dialog_ok_button); 235 | viewPager.setAdapter(new FragmentAdapter(getChildFragmentManager(), requireContext(), tabFragments, tabTrackTypes)); 236 | tabLayout.setupWithViewPager(viewPager); 237 | tabLayout.setVisibility(tabFragments.size() > 1 ? View.VISIBLE : View.GONE); 238 | cancelButton.setOnClickListener(view -> dismiss()); 239 | okButton.setOnClickListener( 240 | view -> { 241 | onClickListener.onClick(getDialog(), DialogInterface.BUTTON_POSITIVE); 242 | dismiss(); 243 | }); 244 | return dialogView; 245 | } 246 | 247 | public static boolean showTabForRenderer(MappedTrackInfo mappedTrackInfo, int rendererIndex) { 248 | TrackGroupArray trackGroupArray = mappedTrackInfo.getTrackGroups(rendererIndex); 249 | if (trackGroupArray.length == 0) { 250 | return false; 251 | } 252 | int trackType = mappedTrackInfo.getRendererType(rendererIndex); 253 | return isSupportedTrackType(trackType); 254 | } 255 | 256 | private static boolean isSupportedTrackType(int trackType) { 257 | switch (trackType) { 258 | case C.TRACK_TYPE_VIDEO: 259 | case C.TRACK_TYPE_AUDIO: 260 | case C.TRACK_TYPE_TEXT: 261 | return true; 262 | default: 263 | return false; 264 | } 265 | } 266 | 267 | } 268 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/andrioidexoplayertrackselection/track_selection_dialog/TrackSelectionViewFragment.java: -------------------------------------------------------------------------------- 1 | package com.example.andrioidexoplayertrackselection.track_selection_dialog; 2 | 3 | import android.os.Bundle; 4 | import android.view.LayoutInflater; 5 | import android.view.View; 6 | import android.view.ViewGroup; 7 | 8 | import androidx.annotation.Nullable; 9 | import androidx.fragment.app.Fragment; 10 | 11 | import com.example.andrioidexoplayertrackselection.R; 12 | import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; 13 | import com.google.android.exoplayer2.trackselection.MappingTrackSelector; 14 | import com.google.android.exoplayer2.ui.TrackSelectionView; 15 | 16 | import java.util.Collections; 17 | import java.util.List; 18 | 19 | /** Fragment to show a track selection in tab of the track selection dialog. */ 20 | public final class TrackSelectionViewFragment extends Fragment implements TrackSelectionView.TrackSelectionListener { 21 | 22 | private MappingTrackSelector.MappedTrackInfo mappedTrackInfo; 23 | private int rendererIndex; 24 | private boolean allowAdaptiveSelections; 25 | private boolean allowMultipleOverrides; 26 | 27 | /* package */ boolean isDisabled; 28 | /* package */ List overrides; 29 | 30 | public TrackSelectionViewFragment() { 31 | // Retain instance across activity re-creation to prevent losing access to init data. 32 | setRetainInstance(true); 33 | } 34 | 35 | public void init( 36 | MappingTrackSelector.MappedTrackInfo mappedTrackInfo, 37 | int rendererIndex, 38 | boolean initialIsDisabled, 39 | @Nullable DefaultTrackSelector.SelectionOverride initialOverride, 40 | boolean allowAdaptiveSelections, 41 | boolean allowMultipleOverrides) { 42 | this.mappedTrackInfo = mappedTrackInfo; 43 | this.rendererIndex = rendererIndex; 44 | this.isDisabled = initialIsDisabled; 45 | this.overrides = initialOverride == null ? Collections.emptyList() : Collections.singletonList(initialOverride); 46 | this.allowAdaptiveSelections = allowAdaptiveSelections; 47 | this.allowMultipleOverrides = allowMultipleOverrides; 48 | } 49 | 50 | @Nullable 51 | @Override 52 | public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { 53 | View rootView = inflater.inflate(R.layout.exo_track_selection_dialog, container, /* attachToRoot= */ false); 54 | TrackSelectionView trackSelectionView = rootView.findViewById(R.id.exo_track_selection_view); 55 | trackSelectionView.setShowDisableOption(true); 56 | trackSelectionView.setAllowMultipleOverrides(allowMultipleOverrides); 57 | trackSelectionView.setAllowAdaptiveSelections(allowAdaptiveSelections); 58 | trackSelectionView.init(mappedTrackInfo, rendererIndex, isDisabled, overrides, /* listener= */ this); 59 | return rootView; 60 | } 61 | 62 | @Override 63 | public void onTrackSelectionChanged(boolean isDisabled, List overrides) { 64 | this.isDisabled = isDisabled; 65 | this.overrides = overrides; 66 | } 67 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 12 | 13 | 19 | 22 | 25 | 26 | 27 | 28 | 34 | 35 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_download.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_download_done.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 14 | 20 | 21 | 22 | 26 | 27 |