├── .gitignore ├── DASH-IF-test-vectors.md ├── LICENSE ├── MediaPlayer-DASH ├── .gitignore ├── LICENSE_ISOPARSER ├── LICENSE_OKHTTP ├── LICENSE_OKIO ├── build.gradle ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ └── java │ └── net │ └── protyposis │ └── android │ └── mediaplayer │ └── dash │ ├── AdaptationLogic.java │ ├── AdaptationSet.java │ ├── CachedSegment.java │ ├── ConstantPropertyBasedLogic.java │ ├── DashMediaExtractor.java │ ├── DashParser.java │ ├── DashParserException.java │ ├── DashSource.java │ ├── MPD.java │ ├── Period.java │ ├── Representation.java │ ├── Segment.java │ ├── SegmentDownloader.java │ ├── SegmentLruCache.java │ └── SimpleRateBasedAdaptationLogic.java ├── MediaPlayer ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── main │ ├── AndroidManifest.xml │ └── java │ │ └── net │ │ └── protyposis │ │ └── android │ │ └── mediaplayer │ │ ├── AudioPlayback.java │ │ ├── Cue.java │ │ ├── Decoders.java │ │ ├── FileSource.java │ │ ├── MediaCodecAudioDecoder.java │ │ ├── MediaCodecDecoder.java │ │ ├── MediaCodecVideoDecoder.java │ │ ├── MediaExtractor.java │ │ ├── MediaPlayer.java │ │ ├── MediaSource.java │ │ ├── TimeBase.java │ │ ├── Timeline.java │ │ ├── UriSource.java │ │ └── VideoView.java │ └── test │ └── java │ └── net │ └── protyposis │ └── android │ └── mediaplayer │ └── TimelineTest.java ├── MediaPlayerDemo ├── .gitignore ├── build.gradle ├── ic_launcher.svg ├── proguard-rules.pro ├── signingconfig.gradle ├── signingconfig.properties.example └── src │ ├── debug │ └── res │ │ └── values │ │ └── strings.xml │ └── main │ ├── AndroidManifest.xml │ ├── assets │ ├── licenses.html │ └── shaders │ │ └── fs_colorfilter.s │ ├── ic_launcher-web.png │ ├── java │ └── net │ │ └── protyposis │ │ └── android │ │ └── mediaplayerdemo │ │ ├── MainActivity.java │ │ ├── SideBySideActivity.java │ │ ├── SideBySideSeekTestActivity.java │ │ ├── Utils.java │ │ ├── VideoURIInputDialogFragment.java │ │ └── VideoViewActivity.java │ └── res │ ├── drawable-hdpi │ ├── ic_action_picture.png │ ├── ic_action_save.png │ ├── ic_action_settings.png │ ├── ic_action_switch_camera.png │ └── ic_launcher.png │ ├── drawable-mdpi │ ├── ic_action_picture.png │ ├── ic_action_save.png │ ├── ic_action_settings.png │ ├── ic_action_switch_camera.png │ └── ic_launcher.png │ ├── drawable-xhdpi │ ├── ic_action_picture.png │ ├── ic_action_save.png │ ├── ic_action_settings.png │ ├── ic_action_switch_camera.png │ └── ic_launcher.png │ ├── drawable-xxhdpi │ ├── ic_action_picture.png │ ├── ic_action_save.png │ ├── ic_action_settings.png │ ├── ic_action_switch_camera.png │ └── ic_launcher.png │ ├── layout-land │ ├── activity_side_by_side.xml │ └── activity_side_by_side_seektest.xml │ ├── layout │ ├── activity_main.xml │ ├── activity_side_by_side.xml │ ├── activity_side_by_side_seektest.xml │ ├── activity_videoview.xml │ └── fragment_uriinput_dialog.xml │ ├── menu │ ├── side_by_side.xml │ └── videoview.xml │ ├── values-w820dp │ └── dimens.xml │ └── values │ ├── colors.xml │ ├── dimens.xml │ ├── strings.xml │ └── styles.xml ├── README.md ├── build.gradle ├── gitversioning.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | .idea/ 5 | .DS_Store 6 | /build 7 | /captures 8 | .externalNativeBuild 9 | .cxx 10 | 11 | signingconfig.properties 12 | *.keystore 13 | google-services.json -------------------------------------------------------------------------------- /DASH-IF-test-vectors.md: -------------------------------------------------------------------------------- 1 | DASH-IF Test Vector evaluation 2 | ============================== 3 | 4 | This table is an evaluation of the compatibility of MediaPlayer-Extended with the DASH test vectors 5 | available here: http://dashif.org/test-vectors/ 6 | 7 | Tested on a Nexus 4 with Android 5.1.1. 8 | 9 | Standard Definition MPDs 10 | ------------------------ 11 | ### Single Resolution Multi-Rate 12 | 13 | Test Vector | Result 14 | -------------- | --------------- 15 | Test Vector 1 | nope, single-segment representation 16 | Test Vector 2 | nope, single-segment representation 17 | Test Vector 3 | nope, single-segment representation 18 | Test Vector 4 | nope, single-segment representation 19 | Test Vector 5 | nope, number overflow in media header box, isoparser does not support UInt64 20 | Test Vector 6 | nope, unsupported video codec 21 | Test Vector 7 | CHECK 22 | Test Vector 8 | CHECK 23 | Test Vector 9 | CHECK 24 | Test Vector 10 | nope, timeline with multiple entries 25 | Test Vector 11 | CHECK 26 | Test Vector 12 | CHECK 27 | 28 | ### Multi-Resolution Multi-Rate 29 | 30 | Test Vector | Result 31 | -------------- | --------------- 32 | Test Vector 1 | nope, single-segment representation 33 | Test Vector 2 | nope, single-segment representation 34 | Test Vector 3 | nope, unsupported video codec 35 | Test Vector 4 | CHECK 36 | Test Vector 5 | CHECK 37 | Test Vector 6 | CHECK 38 | Test Vector 7 | CHECK 39 | Test Vector 8 | CHECK 40 | 41 | ### Multiple Audio Representations 42 | 43 | Test Vector | Result 44 | -------------- | --------------- 45 | Test Vector 1 | nope, single-segment representation 46 | Test Vector 2 | nope, single-segment representation 47 | Test Vector 3 | nope, single-segment representation 48 | Test Vector 4 | nope, single-segment representation 49 | Test Vector 5 | nope, single-segment representation 50 | Test Vector 6 | nope, single-segment representation 51 | Test Vector 8 | nope, single-segment representation 52 | Test Vector 9 | nope, single-segment representation 53 | Test Vector 10 | nope, single-segment representation 54 | Test Vector 11 | nope, single-segment representation 55 | Test Vector 12 | nope, single-segment representation 56 | Test Vector 13 | nope, single-segment representation 57 | Test Vector 14 | nope, single-segment representation 58 | 59 | ### Addition of Subtitles 60 | 61 | Subtitle are not supported yet. 62 | 63 | Test Vector | Result 64 | -------------- | --------------- 65 | Test Vector 1 | nope, single-segment representation 66 | Test Vector 2 | nope, single-segment representation 67 | Test Vector 3 | nope, single-segment representation 68 | Test Vector 4 | nope, single-segment representation 69 | 70 | ### Multiple Periods 71 | 72 | Test Vector | Result 73 | -------------- | --------------- 74 | Test Vector 1 | CHECK, only first period 75 | Test Vector 3 | CHECK, only first period 76 | Test Vector 4 | CHECK, only first period 77 | Test Vector 5 | CHECK, only first period 78 | Test Vector 11 | CHECK, only first period 79 | Test Vector 12 | CHECK, only first period 80 | Test Vector 13 | CHECK, only first period 81 | Test Vector 14 | CHECK, only first period 82 | Test Vector 15 | CHECK, only first period 83 | Test Vector 16 | CHECK, only first period 84 | Test Vector 17 | CHECK, only first period 85 | 86 | ### Encryption and Key Rotations 87 | 88 | Encryption is not supported yet. 89 | 90 | ### Dynamic Segment Offering 91 | 92 | Test Vector | Result 93 | -------------- | --------------- 94 | Test Vector 1 | not really, request timing (segment number) problems 95 | Test Vector 2 | not really, request timing (segment number) problems 96 | Test Vector 3 | not really, request timing (segment number) problems 97 | 98 | ### Dynamic Segment Offering with MPD Update 99 | 100 | MPD update is not supported yet. 101 | 102 | Test Vector | Result 103 | -------------- | --------------- 104 | Test Vector 2 | timeout, no answer from server 105 | Test Vector 3 | CHECK, experimental playback 106 | Test Vector 4 | CHECK, experimental playback 107 | Test Vector 5 | CHECK, experimental playback 108 | Test Vector 6 | CHECK, experimental playback 109 | Test Vector 7 | CHECK, experimental playback 110 | Test Vector 8 | CHECK, experimental playback 111 | Test Vector 9 | CHECK, experimental playback 112 | 113 | ### Addition of Trick Mode 114 | 115 | Trick modes are not supported. 116 | 117 | Test Vector | Result 118 | -------------- | --------------- 119 | Test Vector 1 | nope, single-segment representation 120 | Test Vector 2 | nope, single-segment representation 121 | Test Vector 3 | CHECK 122 | Test Vector 4 | CHECK 123 | Test Vector 5 | CHECK 124 | 125 | High Definition MPDs 126 | -------------------- 127 | ### Single Resolution Multi-Rate 128 | 129 | Test Vector | Result 130 | -------------- | --------------- 131 | Test Vector 1 | nope, single-segment representation 132 | Test Vector 2 | nope, single-segment representation 133 | Test Vector 3 | CHECK 134 | Test Vector 4 | CHECK 135 | Test Vector 5 | CHECK 136 | 137 | ### Multi-Resolution Multi-Rate 138 | 139 | Test Vector | Result 140 | -------------- | --------------- 141 | Test Vector 1 | nope, single-segment representation 142 | Test Vector 2 | nope, single-segment representation 143 | Test Vector 3 | nope, single-segment representation 144 | Test Vector 4 | CHECK 145 | Test Vector 5 | CHECK 146 | Test Vector 6 | CHECK 147 | Test Vector 7 | CHECK 148 | 149 | Multichannel Audio Extensions 150 | ----------------------------- 151 | 152 | ### Dolby 153 | 154 | #### 6-Channel ID 155 | 156 | Test Vector | Result 157 | -------------- | --------------- 158 | Test Vector 1 | nope, single-segment representation 159 | 160 | #### 8-Channel ID 161 | 162 | Test Vector | Result 163 | -------------- | --------------- 164 | Test Vector 1 | nope, single-segment representation 165 | 166 | #### Single Stereo Audio Track 167 | 168 | Test Vector | Result 169 | -------------- | --------------- 170 | Test Vector 1 | nope, single-segment representation 171 | 172 | #### Multiple Adaptation Sets 173 | 174 | Test Vector | Result 175 | -------------- | --------------- 176 | Test Vector 1 | nope, single-segment representation 177 | 178 | #### AC-4 Test Vectors 179 | 180 | AC-4 is not supported by Android 5.1.1 181 | 182 | Test Vector | Result 183 | -------------- | --------------- 184 | Test Vector 1 | nope, single-segment representation 185 | Test Vector 2 | nope, single-segment representation 186 | Test Vector 3 | nope, single-segment representation 187 | Test Vector 4 | nope, single-segment representation 188 | Test Vector 5 | nope, single-segment representation 189 | Test Vector 6 | nope, single-segment representation 190 | Test Vector 7 | audio codec not supported, picture playback only 191 | Test Vector 8 | audio codec not supported, picture playback only 192 | Test Vector 9 | audio codec not supported, picture playback only 193 | Test Vector 10 | audio codec not supported, picture playback only 194 | Test Vector 11 | audio codec not supported, picture playback only 195 | Test Vector 12 | audio codec not supported, picture playback only 196 | 197 | ### DTS 198 | 199 | #### Single Multichannel Audio Track 200 | 201 | Segment server is case sensitive and segment URLs are specified with wrong casing in the MPD. 202 | 203 | Test Vector | Result 204 | -------------- | --------------- 205 | Test Vector 1 | segments 404 206 | Test Vector 2 | segments 404 207 | Test Vector 3 | segments 404 208 | Test Vector 4 | segments 404 209 | 210 | #### Single Stereo Audio Track 211 | 212 | Segment server is case sensitive and segment URLs are specified with wrong casing in the MPD. 213 | 214 | Test Vector | Result 215 | -------------- | --------------- 216 | Test Vector 1 | segments 404 217 | Test Vector 2 | segments 404 218 | Test Vector 3 | segments 404 219 | 220 | #### Multiple Adaptation Sets 221 | 222 | Segment server is case sensitive and segment URLs are specified with wrong casing in the MPD. 223 | 224 | Test Vector | Result 225 | -------------- | --------------- 226 | Test Vector 1 | segments 404 227 | Test Vector 2 | segments 404 228 | Test Vector 3 | segments 404 229 | Test Vector 4 | segments 404 230 | 231 | ### HE-AACv2 Multichannel 232 | 233 | #### 6-Channel ID 234 | Test Vector | Result 235 | -------------- | --------------- 236 | Test Vector 1 | nope, single-segment representation 237 | 238 | #### 8-Channel ID 239 | 240 | Test Vector | Result 241 | -------------- | --------------- 242 | Test Vector 1 | nope, single-segment representation 243 | 244 | #### Multiple Audio Representations 245 | 246 | Test Vector | Result 247 | -------------- | --------------- 248 | Test Vector 1 | nope, single-segment representation 249 | Test Vector 2 | nope, single-segment representation 250 | Test Vector 3 | nope, single-segment representation 251 | 252 | ### MPEG Surround 253 | 254 | #### 6-Channel ID 255 | 256 | Test Vector | Result 257 | -------------- | --------------- 258 | Test Vector 1 | nope, single-segment representation 259 | 260 | #### Multiple Audio Representations 261 | 262 | Test Vector | Result 263 | -------------- | --------------- 264 | Test Vector 1 | nope, single-segment representation 265 | Test Vector 2 | nope, single-segment representation 266 | 267 | HEVC Test Vectors 268 | ----------------- 269 | 270 | ### Single Resolution Multi-Rate 271 | 272 | Test Vector | Result 273 | -------------- | --------------- 274 | Test Vector 1 | nope, single-segment representation 275 | Test Vector 2 | nope, single-segment representation 276 | Test Vector 3 | CHECK 277 | Test Vector 4 | CHECK 278 | 279 | ### Multi-Resolution Multi-Rate 280 | 281 | Test Vector | Result 282 | -------------- | --------------- 283 | Test Vector 1 | nope, single-segment representation 284 | Test Vector 2 | nope, single-segment representation 285 | Test Vector 3 | CHECK 286 | Test Vector 4 | CHECK 287 | 288 | Negative Test Vectors 289 | --------------------- 290 | 291 | ### Essential Property 292 | 293 | Test Vector | Result 294 | -------------- | --------------- 295 | Test Vector 1 | nope, single-segment representation 296 | Test Vector 2 | nope, single-segment representation 297 | 298 | ### Content Protection 299 | 300 | Test Vector | Result 301 | -------------- | --------------- 302 | Test Vector 1 | nope, single-segment representation 303 | Test Vector 2 | nope, single-segment representation -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /MediaPlayer-DASH/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /MediaPlayer-DASH/LICENSE_ISOPARSER: -------------------------------------------------------------------------------- 1 | Copyright 2008 CoreMedia AG, Hamburg 2 | Copyright 2009 castLabs GmbH, Berlin 3 | Copyright 2012 Sebastian Annies, Hamburg 4 | Copyright 2011 Stanislav Vitvitskiy 5 | Copyright 2012 The Apache Software Foundation 6 | 7 | Licensed under the Apache License, Version 2.0 (the "License"); 8 | you may not use this file except in compliance with the License. 9 | You may obtain a copy of the License at 10 | 11 | http://www.apache.org/licenses/LICENSE-2.0 12 | 13 | Unless required by applicable law or agreed to in writing, software 14 | distributed under the License is distributed on an "AS IS" BASIS, 15 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | See the License for the specific language governing permissions and 17 | limitations under the License. -------------------------------------------------------------------------------- /MediaPlayer-DASH/LICENSE_OKHTTP: -------------------------------------------------------------------------------- 1 | Copyright 2014 Square, Inc. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. -------------------------------------------------------------------------------- /MediaPlayer-DASH/LICENSE_OKIO: -------------------------------------------------------------------------------- 1 | Copyright 2014 Square, Inc. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. -------------------------------------------------------------------------------- /MediaPlayer-DASH/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | apply plugin: 'maven-publish' 3 | apply plugin: 'signing' 4 | apply from: '../gitversioning.gradle' 5 | 6 | android { 7 | compileSdk 33 8 | 9 | buildFeatures { 10 | buildConfig = true 11 | } 12 | 13 | defaultConfig { 14 | minSdk 16 15 | targetSdk 33 16 | buildConfigField "String", "VERSION_NAME", "\"${gitVersionName}\"" 17 | } 18 | 19 | buildTypes { 20 | release { 21 | minifyEnabled false 22 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 23 | } 24 | } 25 | 26 | namespace 'net.protyposis.android.mediaplayer.dash' 27 | 28 | lint { 29 | // Lint fix for Okio: https://github.com/square/okio/issues/58 30 | warning 'InvalidPackage' 31 | } 32 | 33 | publishing { 34 | singleVariant("release") { 35 | withSourcesJar() 36 | withJavadocJar() 37 | } 38 | } 39 | } 40 | 41 | dependencies { 42 | implementation fileTree(dir: 'libs', include: ['*.jar']) 43 | implementation project(':MediaPlayer') 44 | implementation 'com.squareup.okio:okio:1.8.0' 45 | implementation "com.squareup.okhttp3:okhttp:3.4.2" 46 | implementation "com.googlecode.mp4parser:isoparser:1.0.5.4" 47 | } 48 | 49 | publishing { 50 | publications { 51 | release(MavenPublication) { 52 | artifactId = 'mediaplayer-dash' 53 | version = gitMavenVersionName 54 | pom { 55 | description = 'MediaPlayer-Extended DASH extension module' 56 | } 57 | afterEvaluate { 58 | from components.release 59 | } 60 | } 61 | } 62 | } 63 | 64 | signing { 65 | useGpgCmd() 66 | sign publishing.publications 67 | } 68 | -------------------------------------------------------------------------------- /MediaPlayer-DASH/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in C:/Users/maguggen/AppData/Local/Android/android-studio/sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /MediaPlayer-DASH/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /MediaPlayer-DASH/src/main/java/net/protyposis/android/mediaplayer/dash/AdaptationLogic.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Mario Guggenberger 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 | 17 | package net.protyposis.android.mediaplayer.dash; 18 | 19 | /** 20 | * This class receives performance data on downloaded segments, does some internal magic with it 21 | * and recommends the best fitting representation for a given adaptation set. 22 | * 23 | * Created by maguggen on 26.08.2014. 24 | */ 25 | public interface AdaptationLogic { 26 | 27 | /** 28 | * Returns an initial adaptation set to start, before any segments have been loaded. 29 | */ 30 | Representation initialize(AdaptationSet adaptationSet); 31 | 32 | /** 33 | * Receiver of performance data on downloaded segments in the {@link net.protyposis.android.mediaplayer.dash.DashMediaExtractor}. 34 | */ 35 | void reportSegmentDownload(AdaptationSet adaptationSet, Representation representation, Segment segment, int byteSize, long downloadTimeMs); 36 | 37 | /** 38 | * Returns the recommended representation at the time of calling. 39 | */ 40 | Representation getRecommendedRepresentation(AdaptationSet adaptationSet); 41 | } 42 | -------------------------------------------------------------------------------- /MediaPlayer-DASH/src/main/java/net/protyposis/android/mediaplayer/dash/AdaptationSet.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Mario Guggenberger 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 | 17 | package net.protyposis.android.mediaplayer.dash; 18 | 19 | import java.util.ArrayList; 20 | import java.util.List; 21 | 22 | /** 23 | * Created by maguggen on 28.08.2014. 24 | */ 25 | public class AdaptationSet { 26 | 27 | int group; 28 | String mimeType; 29 | int maxWidth; 30 | int maxHeight; 31 | float par; // picture aspect ratio (also called DAR - display aspect ratio) 32 | List representations; 33 | 34 | AdaptationSet() { 35 | representations = new ArrayList(); 36 | } 37 | 38 | public int getGroup() { 39 | return group; 40 | } 41 | 42 | public String getMimeType() { 43 | return mimeType; 44 | } 45 | 46 | public List getRepresentations() { 47 | return representations; 48 | } 49 | 50 | public boolean hasMaxDimensions() { 51 | return maxWidth > 0 && maxHeight > 0; 52 | } 53 | 54 | public boolean hasPAR() { 55 | return par > 0; 56 | } 57 | 58 | @Override 59 | public String toString() { 60 | return "AdaptationSet{" + 61 | "group=" + group + 62 | ", mimeType='" + mimeType + '\'' + 63 | ", maxWidth='" + maxWidth + 64 | ", maxHeight='" + maxHeight + 65 | ", par='" + par + 66 | //", representations=" + representations + 67 | '}'; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /MediaPlayer-DASH/src/main/java/net/protyposis/android/mediaplayer/dash/CachedSegment.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Mario Guggenberger 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 | 17 | package net.protyposis.android.mediaplayer.dash; 18 | 19 | import java.io.File; 20 | 21 | /** 22 | * Created by Mario on 03.09.2014. 23 | */ 24 | class CachedSegment { 25 | int number; 26 | Segment segment; 27 | Representation representation; 28 | AdaptationSet adaptationSet; 29 | File file; 30 | long ptsOffsetUs; 31 | 32 | CachedSegment(int number, Segment segment, Representation representation, AdaptationSet adaptationSet) { 33 | this.number = number; 34 | this.segment = segment; 35 | this.representation = representation; 36 | this.adaptationSet = adaptationSet; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /MediaPlayer-DASH/src/main/java/net/protyposis/android/mediaplayer/dash/ConstantPropertyBasedLogic.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Mario Guggenberger 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 | 17 | package net.protyposis.android.mediaplayer.dash; 18 | 19 | /** 20 | * Does not do any adaptation and always recommends the same representation as 21 | * specified in the constructor. 22 | * 23 | * Created by Mario on 05.09.2014. 24 | */ 25 | public class ConstantPropertyBasedLogic implements AdaptationLogic { 26 | 27 | public enum Mode { 28 | LOWEST_BITRATE, 29 | HIGHEST_BITRATE 30 | } 31 | 32 | private Mode mMode; 33 | 34 | public ConstantPropertyBasedLogic(Mode mode) { 35 | mMode = mode; 36 | } 37 | 38 | @Override 39 | public Representation initialize(AdaptationSet adaptationSet) { 40 | return calculateRepresentation(adaptationSet); 41 | } 42 | 43 | @Override 44 | public void reportSegmentDownload(AdaptationSet adaptationSet, Representation representation, Segment segment, int byteSize, long downloadTimeMs) { 45 | 46 | } 47 | 48 | @Override 49 | public Representation getRecommendedRepresentation(AdaptationSet adaptationSet) { 50 | return calculateRepresentation(adaptationSet); 51 | } 52 | 53 | private Representation calculateRepresentation(AdaptationSet adaptationSet) { 54 | if(adaptationSet.representations.isEmpty()) { 55 | throw new RuntimeException("invalid state, an adaptation set must not be empty"); 56 | } 57 | 58 | /* Under the assumption that the representations are always ordered by ascending bandwidth 59 | * in an MPD, the representation is solely chosen upon the index. 60 | * TODO order by bitrate to make sure 61 | */ 62 | switch (mMode) { 63 | case LOWEST_BITRATE: 64 | return adaptationSet.representations.get(0); 65 | case HIGHEST_BITRATE: 66 | return adaptationSet.representations.get(adaptationSet.representations.size() - 1); 67 | default: 68 | throw new RuntimeException("invalid state"); 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /MediaPlayer-DASH/src/main/java/net/protyposis/android/mediaplayer/dash/DashParserException.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Mario Guggenberger 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 | 17 | package net.protyposis.android.mediaplayer.dash; 18 | 19 | /** 20 | * Created by Mario on 10.08.2015. 21 | */ 22 | class DashParserException extends Exception { 23 | 24 | public DashParserException() { 25 | super(); 26 | } 27 | 28 | public DashParserException(String message) { 29 | super(message); 30 | } 31 | 32 | public DashParserException(String message, Throwable cause) { 33 | super(message, cause); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /MediaPlayer-DASH/src/main/java/net/protyposis/android/mediaplayer/dash/DashSource.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Mario Guggenberger 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 | 17 | package net.protyposis.android.mediaplayer.dash; 18 | 19 | import android.content.Context; 20 | import android.net.Uri; 21 | 22 | import java.io.IOException; 23 | import java.util.Map; 24 | 25 | import net.protyposis.android.mediaplayer.MediaExtractor; 26 | import net.protyposis.android.mediaplayer.UriSource; 27 | 28 | import okhttp3.OkHttpClient; 29 | 30 | public class DashSource extends UriSource { 31 | 32 | private OkHttpClient mHttpClient; 33 | private SegmentDownloader mSegmentDownloader; 34 | private AdaptationLogic mAdaptationLogic; 35 | private MPD mMPD; 36 | private int mCacheSizeInBytes = 100 * 1024 * 1024; 37 | 38 | public DashSource(Context context, Uri uri, OkHttpClient httpClient, Map headers, AdaptationLogic adaptationLogic) { 39 | super(context, uri, headers); 40 | mHttpClient = httpClient; 41 | mAdaptationLogic = adaptationLogic; 42 | init(); 43 | } 44 | 45 | public DashSource(Context context, Uri uri, Map headers, AdaptationLogic adaptationLogic) { 46 | this(context, uri, null, headers, adaptationLogic); 47 | } 48 | 49 | public DashSource(Context context, Uri uri, OkHttpClient httpClient, AdaptationLogic adaptationLogic) { 50 | super(context, uri); 51 | mHttpClient = httpClient; 52 | mAdaptationLogic = adaptationLogic; 53 | init(); 54 | } 55 | 56 | public DashSource(Context context, Uri uri, AdaptationLogic adaptationLogic) { 57 | this(context, uri, (OkHttpClient)null, adaptationLogic); 58 | } 59 | 60 | public DashSource(Context context, MPD mpd, OkHttpClient httpClient, AdaptationLogic adaptationLogic) { 61 | super(context, null); 62 | mMPD = mpd; 63 | mHttpClient = httpClient; 64 | mAdaptationLogic = adaptationLogic; 65 | } 66 | 67 | public DashSource(Context context, MPD mpd, AdaptationLogic adaptationLogic) { 68 | this(context, mpd, null, adaptationLogic); 69 | } 70 | 71 | private void initHttpClient() { 72 | // Create a http client instance if there is none yet 73 | if(mHttpClient == null) { 74 | mHttpClient = new OkHttpClient(); 75 | } 76 | // Create a segment downloader if there is none yet 77 | if(mSegmentDownloader == null) { 78 | mSegmentDownloader = new SegmentDownloader(mHttpClient, getHeaders()); 79 | } 80 | } 81 | 82 | private void init() { 83 | initHttpClient(); 84 | if(mAdaptationLogic == null) { 85 | throw new RuntimeException("AdaptationLogic missing!"); 86 | } 87 | if(getUri() != null) { 88 | try { 89 | mMPD = new DashParser().parse(this, mHttpClient); 90 | } catch (DashParserException e) { 91 | throw new RuntimeException(e); 92 | } 93 | } 94 | } 95 | 96 | /** 97 | * Gets the size of the segment cache. Default size is 100 megabytes. 98 | * 99 | * @return the size of the segment cache in bytes 100 | */ 101 | public int getCacheSize() { 102 | return mCacheSizeInBytes; 103 | } 104 | 105 | /** 106 | * Sets the size of the segment cache. This only has an effect before the extractors are 107 | * created, i.e. before the DashSource is set as a data source (e.g. in MediaPlayer or VideoView). 108 | * 109 | * If this source has separate video and audio extractors, the used storage size may be twice 110 | * the configured cache size because each extractor has its own cache. 111 | * 112 | * If the size of the cache is smaller than the segments, segments are not cached and caching 113 | * is therefore disabled. 114 | * 115 | * @param sizeInBytes the size of the segment cache in bytes 116 | */ 117 | public void setCacheSize(int sizeInBytes) { 118 | mCacheSizeInBytes = sizeInBytes; 119 | } 120 | 121 | @Override 122 | public MediaExtractor getVideoExtractor() throws IOException { 123 | initHttpClient(); // in case init() has not been called 124 | DashMediaExtractor mediaExtractor = new DashMediaExtractor(); 125 | mediaExtractor.setCacheSize(mCacheSizeInBytes); 126 | mediaExtractor.setDataSource(getContext(), mMPD, mSegmentDownloader, mMPD.getFirstPeriod().getFirstVideoSet(), mAdaptationLogic); 127 | return mediaExtractor; 128 | } 129 | 130 | @Override 131 | public MediaExtractor getAudioExtractor() throws IOException { 132 | initHttpClient(); // in case init() has not been called 133 | AdaptationSet audioSet = mMPD.getFirstPeriod().getFirstAudioSet(); 134 | if(audioSet != null){ 135 | DashMediaExtractor mediaExtractor = new DashMediaExtractor(); 136 | mediaExtractor.setCacheSize(mCacheSizeInBytes); 137 | mediaExtractor.setDataSource(getContext(), mMPD, mSegmentDownloader, audioSet, mAdaptationLogic); 138 | return mediaExtractor; 139 | } else { 140 | return null; 141 | } 142 | } 143 | } -------------------------------------------------------------------------------- /MediaPlayer-DASH/src/main/java/net/protyposis/android/mediaplayer/dash/MPD.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Mario Guggenberger 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 | 17 | package net.protyposis.android.mediaplayer.dash; 18 | 19 | import java.util.ArrayList; 20 | import java.util.Date; 21 | import java.util.List; 22 | 23 | /** 24 | * Created by maguggen on 28.08.2014. 25 | */ 26 | public class MPD { 27 | 28 | boolean isDynamic; 29 | long mediaPresentationDurationUs; 30 | Date availabilityStartTime; 31 | long timeShiftBufferDepthUs; 32 | long suggestedPresentationDelayUs; 33 | long maxSegmentDurationUs; 34 | long minBufferTimeUs; 35 | List periods; 36 | 37 | MPD() { 38 | periods = new ArrayList(); 39 | } 40 | 41 | public long getMediaPresentationDurationUs() { 42 | return mediaPresentationDurationUs; 43 | } 44 | 45 | public long getMinBufferTimeUs() { 46 | return minBufferTimeUs; 47 | } 48 | 49 | public List getPeriods() { 50 | return periods; 51 | } 52 | 53 | public Period getFirstPeriod() { 54 | if(!periods.isEmpty()) { 55 | return periods.get(0); 56 | } 57 | return null; 58 | } 59 | 60 | @Override 61 | public String toString() { 62 | return "MPD{" + 63 | "mediaPresentationDurationUs=" + mediaPresentationDurationUs + 64 | ", minBufferTimeUs=" + minBufferTimeUs + 65 | //", representations=" + representations + 66 | '}'; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /MediaPlayer-DASH/src/main/java/net/protyposis/android/mediaplayer/dash/Period.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Mario Guggenberger 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 | 17 | package net.protyposis.android.mediaplayer.dash; 18 | 19 | import java.util.ArrayList; 20 | import java.util.List; 21 | 22 | /** 23 | * Created by Mario on 13.08.2015. 24 | */ 25 | public class Period { 26 | 27 | String id; 28 | long startUs; 29 | long durationUs; 30 | boolean bitstreamSwitching; 31 | List adaptationSets; 32 | 33 | Period() { 34 | adaptationSets = new ArrayList(); 35 | } 36 | 37 | public List getAdaptationSets() { 38 | return adaptationSets; 39 | } 40 | 41 | public AdaptationSet getFirstSetOfType(String mime) { 42 | for(AdaptationSet as : adaptationSets) { 43 | if(as.mimeType != null && as.mimeType.startsWith(mime)) { 44 | return as; 45 | } else { 46 | for(Representation r : as.representations) { 47 | if(r.mimeType.startsWith(mime)) { 48 | return as; 49 | } 50 | } 51 | } 52 | } 53 | return null; 54 | } 55 | 56 | public AdaptationSet getFirstVideoSet() { 57 | return getFirstSetOfType("video/"); 58 | } 59 | 60 | public AdaptationSet getFirstAudioSet() { 61 | return getFirstSetOfType("audio/"); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /MediaPlayer-DASH/src/main/java/net/protyposis/android/mediaplayer/dash/Representation.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Mario Guggenberger 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 | 17 | package net.protyposis.android.mediaplayer.dash; 18 | 19 | import java.util.ArrayList; 20 | import java.util.List; 21 | 22 | /** 23 | * Created by maguggen on 27.08.2014. 24 | */ 25 | public class Representation { 26 | String id; 27 | String codec; 28 | String mimeType; 29 | int width; // pixels 30 | int height; // pixels 31 | float sar; // storage aspect ratio 32 | int bandwidth; // bits/sec 33 | 34 | long segmentDurationUs; 35 | Segment initSegment; 36 | List segments; 37 | 38 | Representation() { 39 | segments = new ArrayList(); 40 | } 41 | 42 | public String getId() { 43 | return id; 44 | } 45 | 46 | public String getCodec() { 47 | return codec; 48 | } 49 | 50 | public String getMimeType() { 51 | return mimeType; 52 | } 53 | 54 | public int getWidth() { 55 | return width; 56 | } 57 | 58 | public int getHeight() { 59 | return height; 60 | } 61 | 62 | public boolean hasSAR() { 63 | return sar > 0; 64 | } 65 | 66 | public float calculatePAR() { 67 | float sizeRatio = (float) width / height; 68 | return sizeRatio * (hasSAR() ? sar : 1); 69 | } 70 | 71 | public int getBandwidth() { 72 | return bandwidth; 73 | } 74 | 75 | public long getSegmentDurationUs() { 76 | return segmentDurationUs; 77 | } 78 | 79 | public Segment getInitSegment() { 80 | return initSegment; 81 | } 82 | 83 | public List getSegments() { 84 | return segments; 85 | } 86 | 87 | boolean hasSegments() { 88 | return !segments.isEmpty(); 89 | } 90 | 91 | Segment getLastSegment() { 92 | return segments.get(segments.size() - 1); 93 | } 94 | 95 | @Override 96 | public String toString() { 97 | return "Representation{" + 98 | "id=" + id + 99 | ", codec='" + codec + '\'' + 100 | ", mimeType='" + mimeType + '\'' + 101 | ", width=" + width + 102 | ", height=" + height + 103 | ", dar=" + sar + 104 | ", bandwidth=" + bandwidth + 105 | //", initSegment=" + initSegment + 106 | ", segmentDurationUs=" + segmentDurationUs + 107 | //", segments=" + segments + 108 | '}'; 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /MediaPlayer-DASH/src/main/java/net/protyposis/android/mediaplayer/dash/Segment.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Mario Guggenberger 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 | 17 | package net.protyposis.android.mediaplayer.dash; 18 | 19 | /** 20 | * Created by maguggen on 27.08.2014. 21 | */ 22 | public class Segment { 23 | String media; 24 | String range; 25 | 26 | Segment() { 27 | } 28 | 29 | Segment(String media) { 30 | this.media = media; 31 | } 32 | 33 | Segment(String media, String range) { 34 | this(media); 35 | this.range = range; 36 | } 37 | 38 | public String getMedia() { 39 | return media; 40 | } 41 | 42 | public String getRange() { 43 | return range; 44 | } 45 | 46 | public boolean hasRange() { 47 | return range != null; 48 | } 49 | 50 | @Override 51 | public String toString() { 52 | return "Segment{" + 53 | "media='" + media + '\'' + 54 | ", range='" + range + '\'' + 55 | '}'; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /MediaPlayer-DASH/src/main/java/net/protyposis/android/mediaplayer/dash/SegmentDownloader.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Mario Guggenberger 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 | 17 | package net.protyposis.android.mediaplayer.dash; 18 | 19 | import android.os.SystemClock; 20 | import android.util.Log; 21 | 22 | import java.io.IOException; 23 | import java.util.ArrayList; 24 | import java.util.Comparator; 25 | import java.util.HashMap; 26 | import java.util.List; 27 | import java.util.Map; 28 | import java.util.PriorityQueue; 29 | 30 | import okhttp3.Call; 31 | import okhttp3.Callback; 32 | import okhttp3.Headers; 33 | import okhttp3.OkHttpClient; 34 | import okhttp3.Request; 35 | import okhttp3.Response; 36 | 37 | /** 38 | * Created by Mario on 05.11.2016. 39 | */ 40 | 41 | public class SegmentDownloader { 42 | 43 | private static final String TAG = SegmentDownloader.class.getSimpleName(); 44 | 45 | static final int INITSEGMENT = -1; 46 | 47 | private OkHttpClient mHttpClient; 48 | private Headers mHeaders; 49 | private PriorityQueue mDownloadQueue; // segments waiting in line to be requested 50 | private Map mDownloadRequests; // segments currently being requested 51 | private int mMaxConcurrentDownloadRequests = 3; 52 | 53 | public SegmentDownloader(OkHttpClient httpClient, Map headers) { 54 | if (httpClient == null) { 55 | throw new IllegalArgumentException("http client must be set"); 56 | } 57 | 58 | mHttpClient = httpClient; 59 | 60 | Headers.Builder headersBuilder = new Headers.Builder(); 61 | if (headers != null && !headers.isEmpty()) { 62 | for (String name : headers.keySet()) { 63 | headersBuilder.add(name, headers.get(name)); 64 | } 65 | } 66 | mHeaders = headersBuilder.build(); 67 | 68 | mDownloadQueue = new PriorityQueue<>(20, new Comparator() { 69 | @Override 70 | public int compare(DownloadQueueItem lhs, DownloadQueueItem rhs) { 71 | // Sort the downloads by their PTS (sorting by segment number fails when a/v segments are of different length) 72 | // NOTE: do not use lhs.segment.ptsOffsetUs, it is optional and not always filled 73 | return (int)(lhs.segment.number * lhs.segment.representation.segmentDurationUs 74 | - rhs.segment.number * rhs.segment.representation.segmentDurationUs); 75 | } 76 | }); 77 | mDownloadRequests = new HashMap<>(); 78 | } 79 | 80 | SegmentDownloader(OkHttpClient httpClient) { 81 | this(httpClient, null); 82 | } 83 | 84 | Response downloadBlocking(Segment segment, Integer segmentNr) throws IOException { 85 | Request request = buildSegmentRequest(segment); 86 | Response response = mHttpClient.newCall(request).execute(); 87 | 88 | if (!response.isSuccessful()) { 89 | throw new IOException("sync dl error @ segment " + segmentNr + ": " 90 | + response.code() + " " + response.message() 91 | + " " + request.url().toString()); 92 | } 93 | 94 | return response; 95 | } 96 | 97 | synchronized void downloadAsync(CachedSegment segment, SegmentDownloadCallback callback) { 98 | mDownloadQueue.offer(new DownloadQueueItem(segment, callback)); 99 | scheduleDownloads(); 100 | } 101 | 102 | synchronized boolean isDownloading(AdaptationSet adaptationSet, int segmentNr) { 103 | // Check if the segment is in transfer 104 | if(mDownloadRequests.containsKey(getKey(adaptationSet, segmentNr))) { 105 | return true; 106 | } 107 | 108 | // Check if the segment is queued 109 | for(DownloadQueueItem item : mDownloadQueue) { 110 | if(item.segment.number == segmentNr && item.segment.adaptationSet == adaptationSet) { 111 | return true; 112 | } 113 | } 114 | 115 | return false; 116 | } 117 | 118 | synchronized void cancelDownloads(AdaptationSet adaptationSet) { 119 | // Clear waiting queue 120 | List queueItemsToDelete = new ArrayList<>(); 121 | for (DownloadQueueItem item : mDownloadQueue) { 122 | if (item.segment.adaptationSet == adaptationSet) { 123 | queueItemsToDelete.add(item); 124 | } 125 | } 126 | for (DownloadQueueItem item : queueItemsToDelete) { 127 | mDownloadQueue.remove(item); 128 | } 129 | 130 | // Cancel requests 131 | List requestItemsToDelete = new ArrayList<>(); 132 | for (String key : mDownloadRequests.keySet()) { 133 | if (key.startsWith(adaptationSet.group + "-")) { 134 | requestItemsToDelete.add(key); 135 | mDownloadRequests.get(key).cancel(); 136 | } 137 | } 138 | for(String key : requestItemsToDelete) { 139 | mDownloadRequests.remove(key); 140 | } 141 | } 142 | 143 | private synchronized void scheduleDownloads() { 144 | int downloadsToRequest = mMaxConcurrentDownloadRequests - mDownloadRequests.size(); 145 | 146 | for(int i = 0; i < downloadsToRequest && !mDownloadQueue.isEmpty(); i++) { 147 | DownloadQueueItem item = mDownloadQueue.poll(); 148 | 149 | Request request = buildSegmentRequest(item.segment.segment); 150 | 151 | Call call = mHttpClient.newCall(request); 152 | mDownloadRequests.put(getKey(item.segment.adaptationSet, item.segment.number), call); 153 | call.enqueue(new ResponseCallback(item.segment, item.callback)); 154 | } 155 | } 156 | 157 | /** 158 | * Returns a unique key for a segment of an adaptation set. Just using the segment number as 159 | * key does not suffice because multiple adaptation sets (e.g. video and audio) have overlapping 160 | * segment numbers. 161 | */ 162 | private String getKey(AdaptationSet adaptationSet, int segmentNr) { 163 | return adaptationSet.group + "-" + segmentNr; 164 | } 165 | 166 | /** 167 | * Builds a request object for a segment. 168 | */ 169 | private Request buildSegmentRequest(Segment segment) { 170 | // Replace illegal special chars 171 | String url = segment.media 172 | .replace(" ", "%20") // space 173 | .replace("^", "%5E"); // circumflex 174 | 175 | Request.Builder builder = new Request.Builder().url(url).headers(mHeaders); 176 | 177 | if (segment.hasRange()) { 178 | builder.addHeader("Range", "bytes=" + segment.range); 179 | } 180 | 181 | return builder.build(); 182 | } 183 | 184 | class DownloadFinishedArgs { 185 | 186 | CachedSegment cachedSegment; 187 | byte[] data; 188 | long duration; 189 | 190 | DownloadFinishedArgs(CachedSegment cachedSegment, byte[] data, long duration) { 191 | this.cachedSegment = cachedSegment; 192 | this.data = data; 193 | this.duration = duration; 194 | } 195 | } 196 | 197 | interface SegmentDownloadCallback { 198 | void onFailure(CachedSegment cachedSegment, IOException e); 199 | void onSuccess(DownloadFinishedArgs args) throws IOException; 200 | } 201 | 202 | private class ResponseCallback implements Callback { 203 | 204 | private CachedSegment mCachedSegment; 205 | private SegmentDownloadCallback mCallback; 206 | 207 | ResponseCallback(CachedSegment cachedSegment, SegmentDownloadCallback callback) { 208 | mCachedSegment = cachedSegment; 209 | mCallback = callback; 210 | } 211 | 212 | @Override 213 | public void onFailure(Call call, IOException e) { 214 | mDownloadRequests.remove(getKey(mCachedSegment.adaptationSet, mCachedSegment.number)); 215 | 216 | if(!call.isCanceled()) { 217 | // Call back only if a request 'really' failed, i.e. if it hasn't been canceled on purpose 218 | mCallback.onFailure(mCachedSegment, e); 219 | } 220 | 221 | scheduleDownloads(); 222 | } 223 | 224 | @Override 225 | public void onResponse(Call call, Response response) throws IOException { 226 | mDownloadRequests.remove(getKey(mCachedSegment.adaptationSet, mCachedSegment.number)); 227 | 228 | if (call.isCanceled()) { 229 | Log.d(TAG, "skipping processing of canceled download"); 230 | } else if (response.isSuccessful()) { 231 | try { 232 | long startTime = SystemClock.elapsedRealtime(); 233 | byte[] segmentData = response.body().bytes(); 234 | 235 | /* The time it takes to send the request header to the server until the response 236 | * headers arrive. Can be custom implemented through an Interceptor too, in case 237 | * this should ever fail in the future. */ 238 | long headerTime = response.receivedResponseAtMillis() - response.sentRequestAtMillis(); 239 | 240 | /* The time it takes to read the result body, which is the actual segment data. 241 | * The sum of this time together with the header time is the total segment download time. */ 242 | long payloadTime = SystemClock.elapsedRealtime() - startTime; 243 | 244 | mCallback.onSuccess(new DownloadFinishedArgs(mCachedSegment, segmentData, headerTime + payloadTime)); 245 | } catch (IOException e) { 246 | mCallback.onFailure(mCachedSegment, e); 247 | } finally { 248 | response.body().close(); 249 | } 250 | } 251 | 252 | scheduleDownloads(); 253 | } 254 | } 255 | 256 | private class DownloadQueueItem { 257 | 258 | private CachedSegment segment; 259 | private SegmentDownloadCallback callback; 260 | 261 | public DownloadQueueItem(CachedSegment segment, SegmentDownloadCallback callback) { 262 | this.segment = segment; 263 | this.callback = callback; 264 | } 265 | } 266 | } 267 | -------------------------------------------------------------------------------- /MediaPlayer-DASH/src/main/java/net/protyposis/android/mediaplayer/dash/SegmentLruCache.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Mario Guggenberger 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 | 17 | package net.protyposis.android.mediaplayer.dash; 18 | 19 | import android.util.LruCache; 20 | 21 | /** 22 | * Created by maguggen on 28.08.2014. 23 | */ 24 | class SegmentLruCache extends LruCache { 25 | 26 | public SegmentLruCache(int maxBytes) { 27 | super(maxBytes); 28 | } 29 | 30 | @Override 31 | protected void entryRemoved(boolean evicted, Integer key, CachedSegment oldValue, CachedSegment newValue) { 32 | if(newValue != null && newValue == oldValue) { 33 | // When a value replaces itself, do nothing 34 | return; 35 | } 36 | 37 | // Delete the file upon cache removal, no matter if through a put or eviction 38 | oldValue.file.delete(); 39 | } 40 | 41 | @Override 42 | protected int sizeOf(Integer key, CachedSegment value) { 43 | // Return the size of the file 44 | // NOTE an alternative would be to operate on time units and return the length of the segment 45 | return (int)value.file.length(); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /MediaPlayer-DASH/src/main/java/net/protyposis/android/mediaplayer/dash/SimpleRateBasedAdaptationLogic.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Mario Guggenberger 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 | 17 | package net.protyposis.android.mediaplayer.dash; 18 | 19 | import android.util.Log; 20 | 21 | import java.util.Collections; 22 | import java.util.Comparator; 23 | import java.util.HashMap; 24 | import java.util.Map; 25 | 26 | /** 27 | * Created by Mario on 05.09.2014. 28 | */ 29 | public class SimpleRateBasedAdaptationLogic implements AdaptationLogic { 30 | 31 | private static final String TAG = SimpleRateBasedAdaptationLogic.class.getSimpleName(); 32 | 33 | /* The running bandwidth average can be the same for all adaptation sets since they all 34 | * download their segments from the same network. */ 35 | private RunningAverage mRunningAverage; 36 | 37 | private Map mStateMap; 38 | 39 | public SimpleRateBasedAdaptationLogic() { 40 | mRunningAverage = new RunningAverage(10); 41 | mStateMap = new HashMap(); 42 | } 43 | 44 | private AdaptationState getState(AdaptationSet adaptationSet) { 45 | AdaptationState state = mStateMap.get(adaptationSet); 46 | if(state == null) { 47 | state = new AdaptationState(); 48 | mStateMap.put(adaptationSet, state); 49 | } 50 | return state; 51 | } 52 | 53 | @Override 54 | public Representation initialize(AdaptationSet adaptationSet) { 55 | // sort representations by bandwidth ascending 56 | Collections.sort(adaptationSet.representations, new Comparator() { 57 | @Override 58 | public int compare(Representation lhs, Representation rhs) { 59 | return lhs.bandwidth - rhs.bandwidth; 60 | } 61 | }); 62 | 63 | return calculateRepresentation(adaptationSet); 64 | } 65 | 66 | @Override 67 | public void reportSegmentDownload(AdaptationSet adaptationSet, Representation representation, 68 | Segment segment, int byteSize, long downloadTimeMs) { 69 | int bandwidth = (int)(byteSize * 8 / (downloadTimeMs / 1000f)); 70 | int averageBandwidth = mRunningAverage.next(bandwidth); 71 | Log.d(TAG, adaptationSet.getGroup() + " " 72 | + bandwidth + "bps current, " 73 | + averageBandwidth + " bps average"); 74 | } 75 | 76 | @Override 77 | public Representation getRecommendedRepresentation(AdaptationSet adaptationSet) { 78 | return calculateRepresentation(adaptationSet); 79 | } 80 | 81 | private Representation calculateRepresentation(AdaptationSet adaptationSet) { 82 | if(adaptationSet.representations.isEmpty()) { 83 | throw new RuntimeException("invalid state, an adaptation set must not be empty"); 84 | } 85 | 86 | /* Under the assumption that the representations are always ordered by ascending bandwidth 87 | * in an MPD, the representation is solely chosen upon the index. 88 | */ 89 | AdaptationState state = getState(adaptationSet); 90 | int averageBandwidth = mRunningAverage.average(); 91 | Representation newRepresentation = null; 92 | for(Representation representation : adaptationSet.representations) { 93 | if(representation.bandwidth <= averageBandwidth) { 94 | newRepresentation = representation; 95 | } else { 96 | break; 97 | } 98 | } 99 | 100 | if(newRepresentation == null) { 101 | /* When all representations require more bandwidth than currently available, 102 | * the lowest representation is selected. 103 | */ 104 | newRepresentation = adaptationSet.representations.get(0); 105 | } 106 | 107 | if(state.currentRepresentation == null) { 108 | /* At the first calculation, the current representation is null and gets set to the 109 | * determined representation. 110 | */ 111 | state.currentRepresentation = newRepresentation; 112 | } else { 113 | if(newRepresentation.bandwidth < state.currentRepresentation.bandwidth) { 114 | state.vote = -1; 115 | } else if(newRepresentation.bandwidth > state.currentRepresentation.bandwidth) { 116 | state.vote++; 117 | } 118 | 119 | /* If the vote is below zero, for what a singe downvote suffices, the adaptation 120 | * switches down to the fitting lower bandwidth representation. 121 | * If the there have been consecutive upvotes for at least 10 seconds in a row, the 122 | * adaptation switches to the fitting higher bandwidth representation. 123 | * Any other case does not make a change and the current representation is kept. */ 124 | if(state.vote < 0 || state.vote >= Math.max(1, 10000000d / state.currentRepresentation.segmentDurationUs)) { 125 | Log.d(TAG, "vote=" + state.vote + " switch"); 126 | state.currentRepresentation = newRepresentation; 127 | state.vote = 0; 128 | } else { 129 | newRepresentation = state.currentRepresentation; 130 | } 131 | } 132 | 133 | return newRepresentation; 134 | } 135 | 136 | private static class AdaptationState { 137 | private Representation currentRepresentation; 138 | private int vote; 139 | } 140 | 141 | private static class RunningAverage { 142 | 143 | private int[] values; 144 | private int fillLevel; 145 | private int index; 146 | private int averageSum; 147 | 148 | /** 149 | * Creates a running average with the specified count. A count of 5 means an average over 150 | * the last 5 added values. 151 | */ 152 | public RunningAverage(int count) { 153 | values = new int[count]; 154 | reset(); 155 | } 156 | 157 | public void reset() { 158 | index = -1; 159 | fillLevel = 0; 160 | averageSum = 0; 161 | } 162 | 163 | /** 164 | * Adds a new value and returns the new average. 165 | */ 166 | public int next(int value) { 167 | if(fillLevel < values.length) { 168 | fillLevel++; 169 | } 170 | index = (index + 1) % values.length; 171 | int oldestIndex = positiveMod((index - values.length), values.length); 172 | averageSum -= values[oldestIndex]; 173 | values[index] = value; 174 | averageSum += value; 175 | return average(); 176 | } 177 | 178 | /** 179 | * Returns the current average. 180 | */ 181 | public int average() { 182 | if(fillLevel == 0) { 183 | return 0; 184 | } 185 | return averageSum / fillLevel; 186 | } 187 | 188 | /** 189 | * Shifts negative modulo results to the positive to always get a valid array index. 190 | */ 191 | public static int positiveMod(int m, int n) { 192 | int mod = m % n; 193 | if (mod < 0) mod += n; 194 | return mod; 195 | } 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /MediaPlayer/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /MediaPlayer/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | apply plugin: 'maven-publish' 3 | apply plugin: 'signing' 4 | apply from: '../gitversioning.gradle' 5 | 6 | android { 7 | compileSdk 33 8 | 9 | buildFeatures { 10 | buildConfig = true 11 | } 12 | 13 | defaultConfig { 14 | minSdk 16 15 | targetSdk 33 16 | buildConfigField "String", "VERSION_NAME", "\"${gitVersionName}\"" 17 | } 18 | 19 | buildTypes { 20 | release { 21 | minifyEnabled false 22 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 23 | } 24 | } 25 | namespace 'net.protyposis.android.mediaplayer' 26 | publishing { 27 | singleVariant("release") { 28 | withSourcesJar() 29 | withJavadocJar() 30 | } 31 | } 32 | } 33 | 34 | dependencies { 35 | implementation fileTree(dir: 'libs', include: ['*.jar']) 36 | testImplementation 'junit:junit:4.12' 37 | } 38 | 39 | publishing { 40 | publications { 41 | release(MavenPublication) { 42 | artifactId = 'mediaplayer' 43 | version = gitMavenVersionName 44 | pom { 45 | description = 'MediaPlayer-Extended core module' 46 | } 47 | afterEvaluate { 48 | from components.release 49 | } 50 | } 51 | } 52 | } 53 | 54 | signing { 55 | useGpgCmd() 56 | sign publishing.publications 57 | } -------------------------------------------------------------------------------- /MediaPlayer/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in C:/Users/maguggen/AppData/Local/Android/android-studio/sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /MediaPlayer/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /MediaPlayer/src/main/java/net/protyposis/android/mediaplayer/Cue.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 Mario Guggenberger 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 | 17 | package net.protyposis.android.mediaplayer; 18 | 19 | /** 20 | * Created by Mario on 15.03.2018. 21 | */ 22 | 23 | public class Cue { 24 | 25 | private int time; 26 | private Object data; 27 | 28 | Cue(int time, Object data) { 29 | this.time = time; 30 | this.data = data; 31 | } 32 | 33 | /** 34 | * Gets the time at which this cue is cued. This must not necessarily be the exact playback 35 | * time as cue events can be slightly delayed. Use {@link MediaPlayer#getCurrentPosition()} 36 | * to get the current playback time instead. 37 | * @return the time at which this cue is cued 38 | */ 39 | public int getTime() { 40 | return time; 41 | } 42 | 43 | /** 44 | * Gets the custom data object attached to this cue. 45 | * @return the data attached to this cue 46 | */ 47 | public Object getData() { 48 | return data; 49 | } 50 | 51 | /** 52 | * Checks if this cue has data attached. 53 | * @return true if this cue has data attached, else false 54 | */ 55 | public boolean hasData() { 56 | return data != null; 57 | } 58 | 59 | @Override 60 | public String toString() { 61 | return "Cue{" + 62 | "time=" + time + 63 | ", data=" + data + 64 | '}'; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /MediaPlayer/src/main/java/net/protyposis/android/mediaplayer/Decoders.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Mario Guggenberger 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 | 17 | package net.protyposis.android.mediaplayer; 18 | 19 | import android.util.Log; 20 | 21 | import java.io.IOException; 22 | import java.util.ArrayList; 23 | import java.util.List; 24 | 25 | /** 26 | * Created by Mario on 13.09.2015. 27 | */ 28 | class Decoders { 29 | 30 | private static final String TAG = Decoders.class.getSimpleName(); 31 | 32 | private List mDecoders; 33 | private MediaCodecVideoDecoder mVideoDecoder; 34 | private MediaCodecAudioDecoder mAudioDecoder; 35 | 36 | public Decoders() { 37 | mDecoders = new ArrayList<>(); 38 | } 39 | 40 | public void addDecoder(MediaCodecDecoder decoder) { 41 | mDecoders.add(decoder); 42 | 43 | if (decoder instanceof MediaCodecVideoDecoder) { 44 | mVideoDecoder = (MediaCodecVideoDecoder) decoder; 45 | } else if (decoder instanceof MediaCodecAudioDecoder) { 46 | mAudioDecoder = (MediaCodecAudioDecoder) decoder; 47 | } 48 | } 49 | 50 | public List getDecoders() { 51 | return mDecoders; 52 | } 53 | 54 | public MediaCodecVideoDecoder getVideoDecoder() { 55 | return mVideoDecoder; 56 | } 57 | 58 | public MediaCodecAudioDecoder getAudioDecoder() { 59 | return mAudioDecoder; 60 | } 61 | 62 | /** 63 | * Runs the audio/video decoder loop, optionally until a new frame is available. 64 | * The returned frameInfo object keeps metadata of the decoded frame. To render the frame 65 | * to the screen and/or dismiss its data, call {@link MediaCodecVideoDecoder#releaseFrame(MediaCodecDecoder.FrameInfo, boolean)} 66 | * or {@link MediaCodecVideoDecoder#releaseFrame(MediaCodecDecoder.FrameInfo, long)}. 67 | * 68 | * @param force force decoding in a loop until a frame becomes available or the EOS is reached 69 | * @return a VideoFrameInfo object holding metadata of a decoded video frame or NULL if no frame has been decoded 70 | */ 71 | public MediaCodecDecoder.FrameInfo decodeFrame(boolean force) throws IOException { 72 | //Log.d(TAG, "decodeFrame"); 73 | boolean outputEos = false; 74 | 75 | while(!outputEos) { 76 | int outputEosCount = 0; 77 | MediaCodecDecoder.FrameInfo fi; 78 | MediaCodecDecoder.FrameInfo vfi = null; 79 | 80 | for (MediaCodecDecoder decoder : mDecoders) { 81 | while((fi = decoder.dequeueDecodedFrame()) != null) { 82 | 83 | if(decoder == mVideoDecoder) { 84 | vfi = fi; 85 | break; 86 | } else { 87 | decoder.renderFrame(fi, 0); 88 | } 89 | } 90 | 91 | while (decoder.queueSampleToCodec(false)) {} 92 | 93 | if(decoder.isOutputEos()) { 94 | outputEosCount++; 95 | } 96 | } 97 | 98 | if(vfi != null) { 99 | // If a video frame has been decoded, return it 100 | return vfi; 101 | } 102 | 103 | if(!force) { 104 | // If we have not decoded a video frame and we're not forcing decoding until a frame 105 | // becomes available, return null. 106 | return null; 107 | } 108 | 109 | outputEos = (outputEosCount == mDecoders.size()); 110 | } 111 | 112 | Log.d(TAG, "EOS NULL"); 113 | return null; // EOS already reached, no video frame left to return 114 | } 115 | 116 | /** 117 | * Releases all decoders. This must be called to free decoder resources when this object is no longer in use. 118 | */ 119 | public void release() { 120 | for (MediaCodecDecoder decoder : mDecoders) { 121 | // Catch decoder.release() exceptions to avoid breaking the release loop on the first 122 | // exception and leaking unreleased decoders. 123 | try { 124 | decoder.release(); 125 | } catch (Exception e) { 126 | Log.e(TAG, "release failed", e); 127 | } 128 | } 129 | mDecoders.clear(); 130 | } 131 | 132 | public void seekTo(MediaPlayer.SeekMode seekMode, long seekTargetTimeUs) throws IOException { 133 | for (MediaCodecDecoder decoder : mDecoders) { 134 | decoder.seekTo(seekMode, seekTargetTimeUs); 135 | } 136 | } 137 | 138 | public void renderFrames() { 139 | for (MediaCodecDecoder decoder : mDecoders) { 140 | decoder.renderFrame(); 141 | } 142 | } 143 | 144 | public void dismissFrames() { 145 | for (MediaCodecDecoder decoder : mDecoders) { 146 | decoder.dismissFrame(); 147 | } 148 | } 149 | 150 | public long getCurrentDecodingPTS() { 151 | long minPTS = Long.MAX_VALUE; 152 | for (MediaCodecDecoder decoder : mDecoders) { 153 | long pts = decoder.getCurrentDecodingPTS(); 154 | if(pts != MediaCodecDecoder.PTS_NONE && minPTS > pts) { 155 | minPTS = pts; 156 | } 157 | } 158 | return minPTS; 159 | } 160 | 161 | public long getInputSamplePTS() { 162 | long maxPTS = MediaCodecDecoder.PTS_UNKNOWN; 163 | for (MediaCodecDecoder decoder : mDecoders) { 164 | long pts = decoder.getInputSamplePTS(); 165 | if(pts > maxPTS) { 166 | maxPTS = pts; 167 | } 168 | } 169 | return maxPTS; 170 | } 171 | 172 | public boolean isEOS() { 173 | //return getCurrentDecodingPTS() == MediaCodecDecoder.PTS_EOS; 174 | int eosCount = 0; 175 | for (MediaCodecDecoder decoder : mDecoders) { 176 | if(decoder.isOutputEos()) { 177 | eosCount++; 178 | } 179 | } 180 | return eosCount == mDecoders.size(); 181 | } 182 | 183 | public long getCachedDuration() { 184 | // Init with the largest possible value... 185 | long minCachedDuration = Long.MAX_VALUE; 186 | 187 | // ...then decrease to the lowest duration. 188 | // We always return the lowest value, because if only one decoder has to refill its buffer, 189 | // all others have to wait. If one decoder returns -1, this function returns -1 too (which 190 | // makes sense because we cannot calculate a meaningful cache duration in this case). 191 | for (MediaCodecDecoder decoder : mDecoders) { 192 | long cachedDuration = decoder.getCachedDuration(); 193 | minCachedDuration = Math.min(cachedDuration, minCachedDuration); 194 | } 195 | 196 | if(minCachedDuration == Long.MAX_VALUE) { 197 | // There were no decoders that updated this value, which means we don't have information 198 | // on a cached duration, so we return -1 to signal that the information is not available. 199 | return -1; 200 | } 201 | 202 | return minCachedDuration; 203 | } 204 | 205 | /** 206 | * Returns true only if all decoders have reached the end of stream. 207 | */ 208 | public boolean hasCacheReachedEndOfStream() { 209 | for (MediaCodecDecoder decoder : mDecoders) { 210 | if(!decoder.hasCacheReachedEndOfStream()) { 211 | return false; 212 | } 213 | } 214 | return true; 215 | } 216 | } 217 | -------------------------------------------------------------------------------- /MediaPlayer/src/main/java/net/protyposis/android/mediaplayer/FileSource.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Mario Guggenberger 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 | 17 | package net.protyposis.android.mediaplayer; 18 | 19 | import java.io.File; 20 | import java.io.IOException; 21 | 22 | /** 23 | * Created by Mario on 25.01.2015. 24 | */ 25 | public class FileSource implements MediaSource { 26 | 27 | private File mFile; 28 | private File mAudioFile; 29 | 30 | /** 31 | * Creates a media source from a local file. The file can be either video-only, or multiplexed 32 | * audio/video. 33 | * @param file the av source file 34 | */ 35 | public FileSource(File file) { 36 | mFile = file; 37 | } 38 | 39 | /** 40 | * Creates a media source from separate local video and audio files. 41 | * @param videoFile the video source file 42 | * @param audioFile the audio source file 43 | */ 44 | public FileSource(File videoFile, File audioFile) { 45 | mFile = videoFile; 46 | mAudioFile = audioFile; 47 | } 48 | 49 | public File getFile() { 50 | return mFile; 51 | } 52 | 53 | public File getAudioFile() { 54 | return mAudioFile; 55 | } 56 | 57 | @Override 58 | public MediaExtractor getVideoExtractor() throws IOException { 59 | MediaExtractor mediaExtractor = new MediaExtractor(); 60 | mediaExtractor.setDataSource(mFile.getAbsolutePath()); 61 | return mediaExtractor; 62 | } 63 | 64 | @Override 65 | public MediaExtractor getAudioExtractor() throws IOException { 66 | if(mAudioFile != null) { 67 | // In case of a separate audio file, return an audio extractor 68 | MediaExtractor mediaExtractor = new MediaExtractor(); 69 | mediaExtractor.setDataSource(mAudioFile.getAbsolutePath()); 70 | return mediaExtractor; 71 | } 72 | // We do not need a separate audio extractor when only a single (multiplexed) file 73 | // is passed into this class. 74 | return null; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /MediaPlayer/src/main/java/net/protyposis/android/mediaplayer/MediaCodecAudioDecoder.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Mario Guggenberger 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 | 17 | package net.protyposis.android.mediaplayer; 18 | 19 | import android.media.MediaCodec; 20 | import android.media.MediaFormat; 21 | 22 | import java.io.IOException; 23 | 24 | /** 25 | * Created by Mario on 20.04.2016. 26 | */ 27 | class MediaCodecAudioDecoder extends MediaCodecDecoder { 28 | 29 | private AudioPlayback mAudioPlayback; 30 | 31 | public MediaCodecAudioDecoder(MediaExtractor extractor, boolean passive, int trackIndex, 32 | OnDecoderEventListener listener, AudioPlayback audioPlayback) 33 | throws IOException { 34 | super(extractor, passive, trackIndex, listener); 35 | mAudioPlayback = audioPlayback; 36 | reinitCodec(); 37 | } 38 | 39 | @Override 40 | protected void configureCodec(MediaCodec codec, MediaFormat format) { 41 | super.configureCodec(codec, format); 42 | mAudioPlayback.init(format); 43 | } 44 | 45 | @Override 46 | protected boolean shouldDecodeAnotherFrame() { 47 | // If this is an active audio track, decode and buffer only as much as this arbitrarily 48 | // chosen threshold time to avoid filling up the memory with buffered audio data and 49 | // requesting too much data from the network too fast (e.g. DASH segments). 50 | if(!isPassive()) { 51 | return mAudioPlayback.getQueueBufferTimeUs() < 200000; 52 | } 53 | else { 54 | return super.shouldDecodeAnotherFrame(); 55 | } 56 | } 57 | 58 | @Override 59 | public void renderFrame(FrameInfo frameInfo, long offsetUs) { 60 | mAudioPlayback.write(frameInfo.data, frameInfo.presentationTimeUs); 61 | releaseFrame(frameInfo); 62 | } 63 | 64 | @Override 65 | protected void onOutputFormatChanged(MediaFormat format) { 66 | mAudioPlayback.init(format); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /MediaPlayer/src/main/java/net/protyposis/android/mediaplayer/MediaSource.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Mario Guggenberger 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 | 17 | package net.protyposis.android.mediaplayer; 18 | 19 | import java.io.IOException; 20 | 21 | /** 22 | * Created by maguggen on 27.08.2014. 23 | */ 24 | public interface MediaSource { 25 | 26 | /** 27 | * Returns a media extractor for video data and possibly multiplexed audio data. 28 | */ 29 | MediaExtractor getVideoExtractor() throws IOException; 30 | 31 | /** 32 | * Returns a media extractor for audio data from a separate audio stream, or NULL if the source 33 | * does not have a separate audio source or the audio is multiplexed with the video in a single 34 | * stream. 35 | */ 36 | MediaExtractor getAudioExtractor() throws IOException; 37 | } 38 | -------------------------------------------------------------------------------- /MediaPlayer/src/main/java/net/protyposis/android/mediaplayer/TimeBase.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Mario Guggenberger 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 | 17 | package net.protyposis.android.mediaplayer; 18 | 19 | /** 20 | * Created by Mario on 14.06.2014. 21 | * 22 | * A time base in microseconds for media playback. 23 | */ 24 | class TimeBase { 25 | 26 | private long mStartTime; 27 | private double mSpeed = 1.0; 28 | 29 | public TimeBase() { 30 | start(); 31 | } 32 | 33 | public void start() { 34 | startAt(0); 35 | } 36 | 37 | public void startAt(long mediaTime) { 38 | mStartTime = microTime() - mediaTime; 39 | } 40 | 41 | public long getCurrentTime() { 42 | return microTime() - mStartTime; 43 | } 44 | 45 | public long getOffsetFrom(long from) { 46 | return from - getCurrentTime(); 47 | } 48 | 49 | public double getSpeed() { 50 | return mSpeed; 51 | } 52 | 53 | /** 54 | * Sets the playback speed. Can be used for fast forward and slow motion. 55 | * speed 0.5 = half speed / slow motion 56 | * speed 2.0 = double speed / fast forward 57 | * @param speed 58 | */ 59 | public void setSpeed(double speed) { 60 | mSpeed = speed; 61 | } 62 | 63 | private long microTime() { 64 | return (long)(System.nanoTime() / 1000 * mSpeed); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /MediaPlayer/src/main/java/net/protyposis/android/mediaplayer/Timeline.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 Mario Guggenberger 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 | 17 | package net.protyposis.android.mediaplayer; 18 | 19 | import java.util.ArrayList; 20 | import java.util.Collections; 21 | import java.util.Comparator; 22 | import java.util.HashSet; 23 | import java.util.LinkedList; 24 | import java.util.ListIterator; 25 | 26 | /** 27 | * Created by Mario on 15.03.2018. 28 | */ 29 | class Timeline { 30 | 31 | /** 32 | * A linked list that stores the sequence of cues in the timeline ascending by time. 33 | */ 34 | private LinkedList mList; 35 | /** 36 | * The iterator to traverse the timeline sequentially. 37 | */ 38 | private ListIterator mListIterator; 39 | /** 40 | * Tracks the current position in the list so we can easily create a new list iterator 41 | * when necessary. 42 | */ 43 | private int mListPosition; 44 | /** 45 | * A hashtable to keep track of cues in the timeline that can be used to check for existing 46 | * cues in O(1). This is solely used to determine the return value of {@link #removeCue(Cue)} 47 | * and to keep track of the {@link #count()}. 48 | */ 49 | private HashSet mCues; 50 | /** 51 | * A list of cues to be added by the next {@link #setPlaybackPosition(int)} or 52 | * {@link #movePlaybackPosition(int, OnCueListener)}. 53 | * 54 | * We do not insert the cues directly into the timeline for performance reasons: 55 | * - to avoid the need to execute the playback position methods in a synchronized block 56 | * - because batch insertions can be done with a single iteration of the timeline 57 | */ 58 | private ArrayList mCuesToAdd; 59 | /** 60 | * A list of cues to be removed by the next {@link #setPlaybackPosition(int)} or 61 | * {@link #movePlaybackPosition(int, OnCueListener)}. 62 | * 63 | * Same performance reasons as {@link #mCuesToAdd}. 64 | */ 65 | private ArrayList mCuesToRemove; 66 | /** 67 | * Keeps track of the number of additions and removals so we can determine when the cues 68 | * have been added/removed and we need to do a {@link #updateCueList()}. 69 | */ 70 | private int mModCount; 71 | /** 72 | * Keeps track of the number of modifications after the last {@link #updateCueList()}. Is used 73 | * together with {@link #mModCount} to determine if the cue list needs to be updated. 74 | */ 75 | private int mLastUpdateModCount; 76 | 77 | /** 78 | * Sorts cues in ascending time order. 79 | */ 80 | private Comparator mCueTimeSortComparator = new Comparator() { 81 | @Override 82 | public int compare(Cue lhs, Cue rhs) { 83 | if (lhs.getTime() == rhs.getTime()) { 84 | return 0; 85 | } 86 | else if (lhs.getTime() < rhs.getTime()) { 87 | return -1; 88 | } else { 89 | return 1; 90 | } 91 | } 92 | }; 93 | 94 | public Timeline() { 95 | reset(); 96 | } 97 | 98 | public synchronized void addCue(Cue cue) { 99 | mCuesToAdd.add(cue); 100 | mCues.add(cue); 101 | mModCount++; 102 | } 103 | 104 | public synchronized boolean removeCue(Cue cue) { 105 | if (mCues.contains(cue)) { 106 | mCues.remove(cue); 107 | mCuesToRemove.add(cue); 108 | mModCount++; 109 | return true; 110 | } 111 | 112 | return false; 113 | } 114 | 115 | /** 116 | * Sets the playback position to a new position without announcing cues, e.g. when seeking. 117 | * @param position the new playback position 118 | */ 119 | public void setPlaybackPosition(int position) { 120 | if (mModCount != mLastUpdateModCount) { 121 | // Update the timeline list if cues have been added or removed 122 | // We check the mod count here to avoid an unnecessary function call 123 | updateCueList(); 124 | } 125 | 126 | // Create a new iterator, ... 127 | ListIterator iterator = mList.listIterator(); 128 | 129 | // move to the desired position, ... 130 | while(iterator.hasNext()) { 131 | Cue c = iterator.next(); 132 | 133 | if (c.getTime() > position) { 134 | break; 135 | } 136 | } 137 | if(iterator.hasPrevious()) { 138 | iterator.previous(); 139 | } 140 | 141 | // And replace the previous iterator 142 | mListIterator = iterator; 143 | mListPosition = iterator.nextIndex(); 144 | } 145 | 146 | /** 147 | * Moves the playback position from the current to the requested position, announcing all 148 | * passed cues that are positioned in between. 149 | * @param position the new playback position 150 | * @param listener a listener that receives all cues between the previous and new position 151 | */ 152 | public void movePlaybackPosition(int position, OnCueListener listener) { 153 | if (mModCount != mLastUpdateModCount) { 154 | // Update the timeline list if cues have been added or removed 155 | // We check the mod count here to avoid an unnecessary function call 156 | updateCueList(); 157 | } 158 | 159 | // Move through the timeline and announce cues 160 | while (mListIterator.hasNext()) { 161 | Cue cue = mListIterator.next(); 162 | mListPosition++; 163 | 164 | if (cue.getTime() <= position) { 165 | listener.onCue(cue); 166 | } else { 167 | mListIterator.previous(); 168 | mListPosition--; 169 | break; 170 | } 171 | } 172 | } 173 | 174 | /** 175 | * Gets the number of cues. 176 | * @return the number of cues 177 | */ 178 | public int count() { 179 | return mCues.size(); 180 | } 181 | 182 | /** 183 | * Resets the timeline to its initial empty state. 184 | */ 185 | public synchronized void reset() { 186 | mList = new LinkedList<>(); 187 | mListIterator = mList.listIterator(); 188 | mListPosition = 0; 189 | mCues = new HashSet<>(); 190 | mCuesToAdd = new ArrayList<>(); 191 | mCuesToRemove = new ArrayList<>(); 192 | mModCount = 0; 193 | mLastUpdateModCount = 0; 194 | } 195 | 196 | private synchronized void updateCueList() { 197 | if (!mCuesToAdd.isEmpty()) { 198 | Collections.sort(mCuesToAdd, mCueTimeSortComparator); 199 | 200 | int cuesToAddIndex = 0; 201 | ListIterator iterator = mList.listIterator(); 202 | 203 | // Add cues into list 204 | while(cuesToAddIndex < mCuesToAdd.size() && iterator.hasNext()) { 205 | Cue cue = iterator.next(); 206 | if (cue.getTime() < mCuesToAdd.get(cuesToAddIndex).getTime()) { 207 | iterator.add(mCuesToAdd.get(cuesToAddIndex)); 208 | cuesToAddIndex++; 209 | 210 | int cueIndex = iterator.previousIndex(); 211 | if (cueIndex < mListPosition) { 212 | mListPosition++; 213 | } 214 | } 215 | } 216 | 217 | // Append remaining cues to end of list 218 | while(cuesToAddIndex < mCuesToAdd.size()) { 219 | iterator.add(mCuesToAdd.get(cuesToAddIndex)); 220 | cuesToAddIndex++; 221 | } 222 | 223 | mCuesToAdd.clear(); 224 | } 225 | 226 | if (!mCuesToRemove.isEmpty()) { 227 | HashSet cuesToRemove = new HashSet<>(mCuesToRemove); 228 | 229 | int cuesToRemoveIndex = 0; 230 | ListIterator iterator = mList.listIterator(); 231 | 232 | while(cuesToRemoveIndex < mCuesToRemove.size() && iterator.hasNext()) { 233 | Cue cue = iterator.next(); 234 | if (cuesToRemove.contains(cue)) { 235 | iterator.remove(); 236 | cuesToRemoveIndex++; 237 | 238 | int cueIndex = iterator.nextIndex(); 239 | if (cueIndex < mListPosition) { 240 | mListPosition--; 241 | } 242 | } 243 | } 244 | 245 | mCuesToRemove.clear(); 246 | } 247 | 248 | mLastUpdateModCount = mModCount; 249 | 250 | // We possibly modified the cue list so we need to create a new iterator instance 251 | mListIterator = mList.listIterator(mListPosition); 252 | } 253 | 254 | public interface OnCueListener { 255 | void onCue(Cue cue); 256 | } 257 | } 258 | -------------------------------------------------------------------------------- /MediaPlayer/src/main/java/net/protyposis/android/mediaplayer/UriSource.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Mario Guggenberger 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 | 17 | package net.protyposis.android.mediaplayer; 18 | 19 | import android.content.Context; 20 | import android.net.Uri; 21 | 22 | import java.io.IOException; 23 | import java.util.Map; 24 | 25 | /** 26 | * Created by maguggen on 26.08.2014. 27 | */ 28 | public class UriSource implements MediaSource { 29 | 30 | private Context mContext; 31 | private Uri mUri; 32 | private Uri mAudioUri; 33 | private Map mHeaders; 34 | private Map mAudioHeaders; 35 | 36 | /** 37 | * Creates a media source from a URI. The media source must either be a video stream 38 | * or a multiplexed audio/video stream. 39 | * @param context the context to open the URI in 40 | * @param uri the URI pointing to the media source 41 | * @param headers the headers to be passed with the request to the URI 42 | */ 43 | public UriSource(Context context, Uri uri, Map headers) { 44 | this.mContext = context; 45 | this.mUri = uri; 46 | this.mHeaders = headers; 47 | } 48 | 49 | /** 50 | * Creates a media source from a URI. The media source must either be a video stream 51 | * or a multiplexed audio/video stream. 52 | * @param context the context to open the URI in 53 | * @param uri the URI pointing to the media source 54 | */ 55 | public UriSource(Context context, Uri uri) { 56 | this.mContext = context; 57 | this.mUri = uri; 58 | } 59 | 60 | /** 61 | * Creates a media source from separate video and audio URIs. 62 | * @param context the context to open the URIs in 63 | * @param videoUri the URI pointing to the video source 64 | * @param videoHeaders the headers to be passed with the request to the video URI 65 | * @param audioUri the URI pointing to the audio source 66 | * @param audioHeaders the headers to be passed with the request to the audio URI 67 | */ 68 | public UriSource(Context context, Uri videoUri, Map videoHeaders, Uri audioUri, Map audioHeaders) { 69 | this.mContext = context; 70 | this.mUri = videoUri; 71 | this.mHeaders = videoHeaders; 72 | this.mAudioUri = audioUri; 73 | this.mAudioHeaders = audioHeaders; 74 | } 75 | 76 | /** 77 | * Creates a media source from separate video and audio URIs. 78 | * @param context the context to open the URIs in 79 | * @param videoUri the URI pointing to the video source 80 | * @param audioUri the URI pointing to the audio source 81 | */ 82 | public UriSource(Context context, Uri videoUri, Uri audioUri) { 83 | this.mContext = context; 84 | this.mUri = videoUri; 85 | this.mAudioUri = audioUri; 86 | } 87 | 88 | public Context getContext() { 89 | return mContext; 90 | } 91 | 92 | public Uri getUri() { 93 | return mUri; 94 | } 95 | 96 | public Uri getAudioUri() { 97 | return mAudioUri; 98 | } 99 | 100 | public Map getHeaders() { 101 | return mHeaders; 102 | } 103 | 104 | public Map getAudioHeaders() { 105 | return mAudioHeaders; 106 | } 107 | 108 | @Override 109 | public MediaExtractor getVideoExtractor() throws IOException { 110 | MediaExtractor mediaExtractor = new MediaExtractor(); 111 | mediaExtractor.setDataSource(mContext, mUri, mHeaders); 112 | return mediaExtractor; 113 | } 114 | 115 | @Override 116 | public MediaExtractor getAudioExtractor() throws IOException { 117 | if(mAudioUri != null) { 118 | // In case of a separate audio file Uri, return an audio extractor 119 | MediaExtractor mediaExtractor = new MediaExtractor(); 120 | mediaExtractor.setDataSource(mContext, mAudioUri, mAudioHeaders); 121 | return mediaExtractor; 122 | } 123 | // We do not need a separate audio extractor when only a single Uri to a single 124 | // (multiplexed) file is passed into this class. 125 | return null; 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /MediaPlayer/src/test/java/net/protyposis/android/mediaplayer/TimelineTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 Mario Guggenberger 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 | 17 | package net.protyposis.android.mediaplayer; 18 | 19 | import org.junit.Test; 20 | 21 | import java.util.ArrayList; 22 | import java.util.List; 23 | 24 | import static org.junit.Assert.*; 25 | 26 | /** 27 | * Created by Mario on 15.03.2018. 28 | */ 29 | public class TimelineTest { 30 | 31 | private Cue cue0 = new Cue(0, null); 32 | private Cue cue1 = new Cue(1, null); 33 | private Cue cue2 = new Cue(2, null); 34 | 35 | @Test 36 | public void setPlaybackPositionOnEmptyList() { 37 | Timeline t = new Timeline(); 38 | t.setPlaybackPosition(1000); 39 | } 40 | 41 | @Test 42 | public void setPlaybackPositionToStartOnEmptyList() { 43 | Timeline t = new Timeline(); 44 | t.setPlaybackPosition(0); 45 | } 46 | 47 | @Test 48 | public void movePlaybackPositionOnEmptyList() { 49 | Timeline t = new Timeline(); 50 | t.movePlaybackPosition(1000, new OnCueListener()); 51 | } 52 | 53 | @Test 54 | public void addCue() { 55 | Timeline t = new Timeline(); 56 | t.addCue(cue0); 57 | assertEquals(1, t.count()); 58 | } 59 | 60 | @Test 61 | public void removeCue() { 62 | Timeline t = new Timeline(); 63 | t.addCue(cue0); 64 | t.removeCue(cue0); 65 | assertEquals(0, t.count()); 66 | } 67 | 68 | @Test 69 | public void addCuesAndMovePlaybackPositionToEnd() { 70 | Timeline t = new Timeline(); 71 | t.addCue(cue0); 72 | t.addCue(cue1); 73 | t.addCue(cue2); 74 | 75 | OnCueListener onCueListener = new OnCueListener(); 76 | 77 | t.movePlaybackPosition(2, onCueListener); 78 | 79 | assertEquals(3, onCueListener.getCount()); 80 | } 81 | 82 | @Test 83 | public void addCuesAndMovePlaybackPositionOverEnd() { 84 | Timeline t = new Timeline(); 85 | t.addCue(cue0); 86 | t.addCue(cue1); 87 | t.addCue(cue2); 88 | 89 | OnCueListener onCueListener = new OnCueListener(); 90 | 91 | t.movePlaybackPosition(3, onCueListener); 92 | 93 | assertEquals(3, onCueListener.getCount()); 94 | } 95 | 96 | @Test 97 | public void addCuesAndMovePlaybackPositionToMiddle() { 98 | Timeline t = new Timeline(); 99 | t.addCue(cue0); 100 | t.addCue(cue1); 101 | t.addCue(cue2); 102 | 103 | OnCueListener onCueListener = new OnCueListener(); 104 | 105 | t.movePlaybackPosition(1, onCueListener); 106 | 107 | assertEquals(2, onCueListener.getCount()); 108 | } 109 | 110 | @Test 111 | public void removeNextCue() { 112 | Timeline t = new Timeline(); 113 | t.addCue(cue0); 114 | t.addCue(cue1); 115 | t.addCue(cue2); 116 | 117 | OnCueListener onCueListener = new OnCueListener(); 118 | 119 | t.movePlaybackPosition(1, onCueListener); 120 | 121 | assertEquals(2, onCueListener.getCount()); 122 | 123 | t.removeCue(cue2); 124 | 125 | onCueListener = new OnCueListener(); 126 | 127 | t.movePlaybackPosition(3, onCueListener); 128 | 129 | assertEquals(0, onCueListener.getCount()); 130 | } 131 | 132 | @Test 133 | public void removePreviousCue() { 134 | Timeline t = new Timeline(); 135 | t.addCue(cue0); 136 | t.addCue(cue1); 137 | t.addCue(cue2); 138 | 139 | OnCueListener onCueListener = new OnCueListener(); 140 | 141 | t.movePlaybackPosition(1, onCueListener); 142 | 143 | assertEquals(2, onCueListener.getCount()); 144 | 145 | t.removeCue(cue1); 146 | 147 | onCueListener = new OnCueListener(); 148 | 149 | t.movePlaybackPosition(3, onCueListener); 150 | 151 | assertEquals(1, onCueListener.getCount()); 152 | assertEquals(cue2, onCueListener.getCues().get(0)); 153 | } 154 | 155 | class OnCueListener implements Timeline.OnCueListener { 156 | 157 | private List cues = new ArrayList<>(); 158 | 159 | public OnCueListener() { 160 | System.out.println("new OnCueListener"); 161 | } 162 | 163 | @Override 164 | public void onCue(Cue cue) { 165 | System.out.println(cue.toString()); 166 | cues.add(cue); 167 | } 168 | 169 | public int getCount() { 170 | return cues.size(); 171 | } 172 | 173 | public List getCues() { 174 | return cues; 175 | } 176 | } 177 | } -------------------------------------------------------------------------------- /MediaPlayerDemo/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | signingconfig.properties 3 | 4 | -------------------------------------------------------------------------------- /MediaPlayerDemo/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdk 33 5 | 6 | buildFeatures { 7 | buildConfig = true 8 | } 9 | 10 | defaultConfig { 11 | // Keep old ITEC package name as application Id for Play Store compatibility 12 | applicationId 'at.aau.itec.android.mediaplayerdemo' 13 | 14 | minSdk 16 15 | targetSdk 33 16 | versionCode 1 17 | versionName '1.0' 18 | } 19 | 20 | signingConfigs { 21 | debug // configured in signingconfig.gradle 22 | release // configured in signingconfig.gradle 23 | } 24 | 25 | buildTypes { 26 | debug { 27 | applicationIdSuffix ".debug" 28 | versionNameSuffix "-debug" 29 | } 30 | release { 31 | minifyEnabled false 32 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 33 | signingConfig signingConfigs.release 34 | } 35 | } 36 | 37 | namespace 'net.protyposis.android.mediaplayerdemo' 38 | 39 | lint { 40 | // Lint fix for Okio: https://github.com/square/okio/issues/58 41 | warning 'InvalidPackage' 42 | } 43 | 44 | applicationVariants.all { variant -> 45 | variant.outputs.each { output -> 46 | if (variant.name == android.buildTypes.release.name) { 47 | output.outputFileName = output.outputFileName.replace(".apk", "-" + defaultConfig.versionCode + "-" + defaultConfig.versionName + ".apk") 48 | } 49 | } 50 | } 51 | 52 | } 53 | 54 | dependencies { 55 | implementation fileTree(dir: 'libs', include: ['*.jar']) 56 | implementation project(':MediaPlayer') 57 | implementation project(':MediaPlayer-DASH') 58 | implementation platform('com.google.firebase:firebase-bom:32.7.2') 59 | implementation 'com.google.firebase:firebase-crashlytics' 60 | } 61 | 62 | ext.isLibrary = false 63 | apply from: "../gitversioning.gradle" 64 | apply from: "signingconfig.gradle" 65 | 66 | def firebaseConfigFile = file("google-services.json"); 67 | if (!firebaseConfigFile.exists()) { 68 | project.logger.error("Firebase config file for Crashlytics not found at ${firebaseConfigFile.absolutePath}. Please download the file from the Firebase Console and put it there.") 69 | } 70 | 71 | apply plugin: 'com.google.gms.google-services' 72 | apply plugin: 'com.google.firebase.crashlytics' 73 | -------------------------------------------------------------------------------- /MediaPlayerDemo/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in C:/Users/maguggen/AppData/Local/Android/android-studio/sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /MediaPlayerDemo/signingconfig.gradle: -------------------------------------------------------------------------------- 1 | def readPasswordFromConsole(prompt) { 2 | if (project.hasProperty('skippasswordprompts')) { 3 | return "" 4 | } 5 | def c = System.console() 6 | if(c != null) { 7 | return new String(System.console().readPassword("\n\$ " + prompt + " ")) 8 | } 9 | return "" 10 | } 11 | 12 | android { 13 | signingConfigs { 14 | def configPropsFile = file("signingconfig.properties") 15 | if (configPropsFile.exists()) { 16 | def props = new Properties() 17 | configPropsFile.withInputStream { props.load(it) } 18 | 19 | debug { 20 | if(props.debug_store?.trim()) { 21 | storeFile file(props.debug_store) 22 | } 23 | } 24 | 25 | release { 26 | storeFile file(props.release_store) 27 | storePassword(props.release_storePass?.trim() ? props.release_storePass : readPasswordFromConsole("keystore pass:")) 28 | keyAlias props.release_alias 29 | keyPassword(props.release_pass?.trim() ? props.release_pass : readPasswordFromConsole("key pass:")) 30 | } 31 | } else { 32 | println "signingconfig.properties file is missing (required for release builds)!" 33 | } 34 | } 35 | } -------------------------------------------------------------------------------- /MediaPlayerDemo/signingconfig.properties.example: -------------------------------------------------------------------------------- 1 | # This is an example signingconfig.properties file. 2 | debug_store=/path/to/your/debug.keystore // optional, leave empty for default debug keystore 3 | release_store=/path/to/your/release.keystore 4 | release_storePass=your_keystore_password // optional, leave empty for prompt 5 | release_alias=your_alias 6 | release_pass=your_password // optional, leave empty for prompt 7 | -------------------------------------------------------------------------------- /MediaPlayerDemo/src/debug/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | MediaPlayer-Extended Demo Debug 4 | -------------------------------------------------------------------------------- /MediaPlayerDemo/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 11 | 12 | 17 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 53 | 54 | 57 | 58 | 61 | 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /MediaPlayerDemo/src/main/assets/licenses.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Licenses 5 | 6 | 11 | 12 | 13 | 14 |

This app uses the following components:

15 | 21 | 22 | 23 | 24 |

MediaPlayer-Extended

25 |
 26 |     Copyright 2014 Mario Guggenberger <mg@protyposis.net>
 27 | 
 28 |     Licensed under the Apache License, Version 2.0 (the "License");
 29 |     you may not use this file except in compliance with the License.
 30 |     You may obtain a copy of the License at
 31 | 
 32 |         http://www.apache.org/licenses/LICENSE-2.0
 33 | 
 34 |     Unless required by applicable law or agreed to in writing, software
 35 |     distributed under the License is distributed on an "AS IS" BASIS,
 36 |     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 37 |     See the License for the specific language governing permissions and
 38 |     limitations under the License.
 39 | 
40 | 41 | 42 | 43 |

ISO Parser

44 |
 45 |     Copyright 2008 CoreMedia AG, Hamburg
 46 |     Copyright 2009 castLabs GmbH, Berlin
 47 |     Copyright 2012 Sebastian Annies, Hamburg
 48 |     Copyright 2011 Stanislav Vitvitskiy
 49 |     Copyright 2012 The Apache Software Foundation
 50 | 
 51 |     Licensed under the Apache License, Version 2.0 (the "License");
 52 |     you may not use this file except in compliance with the License.
 53 |     You may obtain a copy of the License at
 54 | 
 55 |     http://www.apache.org/licenses/LICENSE-2.0
 56 | 
 57 |     Unless required by applicable law or agreed to in writing, software
 58 |     distributed under the License is distributed on an "AS IS" BASIS,
 59 |     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 60 |     See the License for the specific language governing permissions and
 61 |     limitations under the License.
 62 | 
63 | 64 | 65 | 66 |

OkHttp

67 |
 68 |     Copyright 2014 Square, Inc.
 69 | 
 70 |     Licensed under the Apache License, Version 2.0 (the "License");
 71 |     you may not use this file except in compliance with the License.
 72 |     You may obtain a copy of the License at
 73 | 
 74 |     http://www.apache.org/licenses/LICENSE-2.0
 75 | 
 76 |     Unless required by applicable law or agreed to in writing, software
 77 |     distributed under the License is distributed on an "AS IS" BASIS,
 78 |     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 79 |     See the License for the specific language governing permissions and
 80 |     limitations under the License.
 81 | 
82 | 83 | 84 | 85 |

Okio

86 |
 87 |     Copyright 2014 Square, Inc.
 88 | 
 89 |     Licensed under the Apache License, Version 2.0 (the "License");
 90 |     you may not use this file except in compliance with the License.
 91 |     You may obtain a copy of the License at
 92 | 
 93 |     http://www.apache.org/licenses/LICENSE-2.0
 94 | 
 95 |     Unless required by applicable law or agreed to in writing, software
 96 |     distributed under the License is distributed on an "AS IS" BASIS,
 97 |     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 98 |     See the License for the specific language governing permissions and
 99 |     limitations under the License.
100 | 
101 | 102 | -------------------------------------------------------------------------------- /MediaPlayerDemo/src/main/assets/shaders/fs_colorfilter.s: -------------------------------------------------------------------------------- 1 | precision mediump float; 2 | 3 | uniform sampler2D s_Texture; 4 | uniform vec2 u_TextureSize; 5 | uniform vec4 color; 6 | varying vec2 v_TextureCoord; 7 | 8 | void main() { 9 | gl_FragColor = texture2D(s_Texture, v_TextureCoord) * color; 10 | } -------------------------------------------------------------------------------- /MediaPlayerDemo/src/main/ic_launcher-web.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/protyposis/MediaPlayer-Extended/460dcccf70894aa45d2c59ff8f8f3ec7c794faee/MediaPlayerDemo/src/main/ic_launcher-web.png -------------------------------------------------------------------------------- /MediaPlayerDemo/src/main/java/net/protyposis/android/mediaplayerdemo/SideBySideActivity.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Mario Guggenberger 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 | 17 | package net.protyposis.android.mediaplayerdemo; 18 | 19 | import android.annotation.TargetApi; 20 | import android.app.Activity; 21 | import android.media.MediaPlayer; 22 | import android.net.Uri; 23 | import android.os.Build; 24 | import android.os.Bundle; 25 | import android.os.Handler; 26 | import android.view.Menu; 27 | import android.view.MenuItem; 28 | import android.view.MotionEvent; 29 | import android.widget.MediaController; 30 | 31 | import java.util.ArrayList; 32 | import java.util.List; 33 | 34 | import net.protyposis.android.mediaplayer.VideoView; 35 | 36 | 37 | public class SideBySideActivity extends Activity { 38 | 39 | private static final String TAG = SideBySideActivity.class.getSimpleName(); 40 | 41 | private Uri mVideoUri; 42 | private android.widget.VideoView mAndroidVideoView; 43 | private VideoView mMpxVideoView; 44 | 45 | private MediaPlayerMultiControl mMediaPlayerControl; 46 | private MediaController mMediaController; 47 | 48 | @Override 49 | protected void onCreate(Bundle savedInstanceState) { 50 | super.onCreate(savedInstanceState); 51 | setContentView(R.layout.activity_side_by_side); 52 | Utils.setActionBarSubtitleEllipsizeMiddle(this); 53 | 54 | mAndroidVideoView = (android.widget.VideoView) findViewById(R.id.androidvv); 55 | mMpxVideoView = (VideoView) findViewById(R.id.mpxvv); 56 | 57 | mMediaPlayerControl = new MediaPlayerMultiControl(mAndroidVideoView, mMpxVideoView); 58 | mMediaController = new MediaController(this); 59 | mMediaController.setAnchorView(findViewById(R.id.container)); 60 | mMediaController.setMediaPlayer(mMediaPlayerControl); 61 | mMediaController.setEnabled(false); 62 | 63 | mVideoUri = getIntent().getData(); 64 | getActionBar().setSubtitle(mVideoUri+""); 65 | 66 | // HACK: this needs to be deferred, else it fails when setting video on both players (it works when doing it just on one) 67 | new Handler().postDelayed(new Runnable() { 68 | @Override 69 | public void run() { 70 | final Runnable enableMediaController = new Runnable() { 71 | int preparedCount = 0; 72 | @Override 73 | public void run() { 74 | if(++preparedCount == mMediaPlayerControl.getControlsCount()) { 75 | // Enable controller when all players are initialized 76 | mMediaController.setEnabled(true); 77 | } 78 | } 79 | }; 80 | 81 | mAndroidVideoView.setOnPreparedListener(new MediaPlayer.OnPreparedListener() { 82 | @Override 83 | public void onPrepared(MediaPlayer mp) { 84 | mAndroidVideoView.seekTo(0); // display first frame 85 | enableMediaController.run(); 86 | 87 | } 88 | }); 89 | mMpxVideoView.setOnPreparedListener(new net.protyposis.android.mediaplayer.MediaPlayer.OnPreparedListener() { 90 | @Override 91 | public void onPrepared(net.protyposis.android.mediaplayer.MediaPlayer mp) { 92 | mMpxVideoView.seekTo(0); // display first frame 93 | enableMediaController.run(); 94 | } 95 | }); 96 | 97 | mAndroidVideoView.setVideoURI(mVideoUri); 98 | mMpxVideoView.setVideoURI(mVideoUri); 99 | } 100 | }, 1000); 101 | } 102 | 103 | 104 | @Override 105 | public boolean onCreateOptionsMenu(Menu menu) { 106 | // Inflate the menu; this adds items to the action bar if it is present. 107 | getMenuInflater().inflate(R.menu.side_by_side, menu); 108 | return true; 109 | } 110 | 111 | @Override 112 | public boolean onOptionsItemSelected(MenuItem item) { 113 | // Handle action bar item clicks here. The action bar will 114 | // automatically handle clicks on the Home/Up button, so long 115 | // as you specify a parent activity in AndroidManifest.xml. 116 | int id = item.getItemId(); 117 | if (id == R.id.action_settings) { 118 | return true; 119 | } 120 | return super.onOptionsItemSelected(item); 121 | } 122 | 123 | @Override 124 | public boolean onTouchEvent(MotionEvent event) { 125 | mMediaController.show(); 126 | return super.onTouchEvent(event); 127 | } 128 | 129 | @Override 130 | protected void onStop() { 131 | mMediaController.hide(); 132 | super.onStop(); 133 | } 134 | 135 | private class MediaPlayerMultiControl implements MediaController.MediaPlayerControl { 136 | 137 | private List mControls; 138 | 139 | public MediaPlayerMultiControl(MediaController.MediaPlayerControl... controls) { 140 | mControls = new ArrayList<>(); 141 | for(MediaController.MediaPlayerControl mpc : controls) { 142 | mControls.add(mpc); 143 | } 144 | } 145 | 146 | public int getControlsCount() { 147 | return mControls.size(); 148 | } 149 | 150 | @Override 151 | public void start() { 152 | for(MediaController.MediaPlayerControl mpc : mControls) { 153 | mpc.start(); 154 | } 155 | } 156 | 157 | @Override 158 | public void pause() { 159 | for(MediaController.MediaPlayerControl mpc : mControls) { 160 | mpc.pause(); 161 | } 162 | } 163 | 164 | @Override 165 | public int getDuration() { 166 | if(!mControls.isEmpty()) { 167 | return mControls.get(0).getDuration(); 168 | } 169 | return 0; 170 | } 171 | 172 | @Override 173 | public int getCurrentPosition() { 174 | if(!mControls.isEmpty()) { 175 | return mControls.get(0).getCurrentPosition(); 176 | } 177 | return 0; 178 | } 179 | 180 | @Override 181 | public void seekTo(int pos) { 182 | for(MediaController.MediaPlayerControl mpc : mControls) { 183 | mpc.seekTo(pos); 184 | } 185 | } 186 | 187 | @Override 188 | public boolean isPlaying() { 189 | if(!mControls.isEmpty()) { 190 | return mControls.get(0).isPlaying(); 191 | } 192 | return false; 193 | } 194 | 195 | @Override 196 | public int getBufferPercentage() { 197 | if(!mControls.isEmpty()) { 198 | return mControls.get(0).getBufferPercentage(); 199 | } 200 | return 0; 201 | } 202 | 203 | @Override 204 | public boolean canPause() { 205 | if(!mControls.isEmpty()) { 206 | return mControls.get(0).canPause(); 207 | } 208 | return false; 209 | } 210 | 211 | @Override 212 | public boolean canSeekBackward() { 213 | if(!mControls.isEmpty()) { 214 | return mControls.get(0).canSeekBackward(); 215 | } 216 | return false; 217 | } 218 | 219 | @Override 220 | public boolean canSeekForward() { 221 | if(!mControls.isEmpty()) { 222 | return mControls.get(0).canSeekForward(); 223 | } 224 | return false; 225 | } 226 | 227 | @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2) 228 | @Override 229 | public int getAudioSessionId() { 230 | if(!mControls.isEmpty()) { 231 | return mControls.get(0).getAudioSessionId(); 232 | } 233 | return 0; 234 | } 235 | } 236 | } 237 | -------------------------------------------------------------------------------- /MediaPlayerDemo/src/main/java/net/protyposis/android/mediaplayerdemo/SideBySideSeekTestActivity.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Mario Guggenberger 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 | 17 | package net.protyposis.android.mediaplayerdemo; 18 | 19 | import android.annotation.TargetApi; 20 | import android.app.Activity; 21 | import android.net.Uri; 22 | import android.os.Build; 23 | import android.os.Bundle; 24 | import android.util.Log; 25 | import android.view.Menu; 26 | import android.view.MenuItem; 27 | import android.view.MotionEvent; 28 | import android.view.View; 29 | import android.view.Window; 30 | import android.widget.AdapterView; 31 | import android.widget.ArrayAdapter; 32 | import android.widget.MediaController; 33 | import android.widget.Spinner; 34 | 35 | import java.util.ArrayList; 36 | import java.util.LinkedHashMap; 37 | import java.util.List; 38 | import java.util.Map; 39 | 40 | import net.protyposis.android.mediaplayer.MediaPlayer; 41 | import net.protyposis.android.mediaplayer.MediaSource; 42 | import net.protyposis.android.mediaplayer.VideoView; 43 | 44 | 45 | public class SideBySideSeekTestActivity extends Activity { 46 | 47 | private static final String TAG = SideBySideSeekTestActivity.class.getSimpleName(); 48 | 49 | private VideoView mVideoView1; 50 | private VideoView mVideoView2; 51 | 52 | private Spinner mSpinner1; 53 | private Spinner mSpinner2; 54 | 55 | private MediaController.MediaPlayerControl mMediaPlayerControl; 56 | private MediaController mMediaController; 57 | private int mSeekTarget; 58 | private int mSeekingViewsCount; 59 | 60 | @Override 61 | protected void onCreate(Bundle savedInstanceState) { 62 | super.onCreate(savedInstanceState); 63 | requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS); 64 | setContentView(R.layout.activity_side_by_side_seektest); 65 | Utils.setActionBarSubtitleEllipsizeMiddle(this); 66 | 67 | mVideoView1 = (VideoView) findViewById(R.id.vv1); 68 | mVideoView2 = (VideoView) findViewById(R.id.vv2); 69 | mSpinner1 = (Spinner) findViewById(R.id.vv1_mode); 70 | mSpinner2 = (Spinner) findViewById(R.id.vv2_mode); 71 | 72 | mMediaPlayerControl = new MediaPlayerMultiControl(mVideoView1, mVideoView2); 73 | mMediaController = new MediaController(this); 74 | mMediaController.setAnchorView(findViewById(R.id.container)); 75 | mMediaController.setMediaPlayer(mMediaPlayerControl); 76 | 77 | final Uri uri = getIntent().getData(); 78 | getActionBar().setSubtitle(uri+""); 79 | 80 | final Map videoViews = new LinkedHashMap(2); 81 | videoViews.put(mVideoView1, mSpinner1); 82 | videoViews.put(mVideoView2, mSpinner2); 83 | 84 | final int[] count = {0}; 85 | for(final VideoView videoView : videoViews.keySet()) { 86 | videoView.setOnPreparedListener(new MediaPlayer.OnPreparedListener() { 87 | @Override 88 | public void onPrepared(MediaPlayer mp) { 89 | Spinner spinner = videoViews.get(videoView); 90 | ArrayAdapter dataAdapter = new ArrayAdapter( 91 | SideBySideSeekTestActivity.this, 92 | android.R.layout.simple_spinner_item, 93 | MediaPlayer.SeekMode.values() 94 | ); 95 | dataAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); 96 | spinner.setAdapter(dataAdapter); 97 | spinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { 98 | @Override 99 | public void onItemSelected(AdapterView parent, View view, int position, long id) { 100 | // set seek mode 101 | videoView.getMediaPlayer().setSeekMode((MediaPlayer.SeekMode) parent.getItemAtPosition(position)); 102 | // update picture 103 | mSeekingViewsCount++; 104 | setProgressBarIndeterminateVisibility(true); 105 | videoView.seekTo(mSeekTarget); 106 | } 107 | 108 | @Override 109 | public void onNothingSelected(AdapterView parent) { 110 | 111 | } 112 | }); 113 | if(++count[0] == videoViews.size()) { 114 | mSpinner1.setSelection(0); 115 | mSpinner2.setSelection(5); 116 | } 117 | } 118 | }); 119 | videoView.setOnSeekCompleteListener(new MediaPlayer.OnSeekCompleteListener() { 120 | @Override 121 | public void onSeekComplete(MediaPlayer mp) { 122 | if(--mSeekingViewsCount == 0) { 123 | setProgressBarIndeterminateVisibility(false); 124 | } 125 | } 126 | }); 127 | 128 | Utils.uriToMediaSourceAsync(this, uri, new Utils.MediaSourceAsyncCallbackHandler() { 129 | @Override 130 | public void onMediaSourceLoaded(MediaSource mediaSource) { 131 | videoView.setVideoSource(mediaSource); 132 | } 133 | 134 | @Override 135 | public void onException(Exception e) { 136 | Log.e(TAG, "error loading video", e); 137 | } 138 | }); 139 | } 140 | } 141 | 142 | @Override 143 | public boolean onCreateOptionsMenu(Menu menu) { 144 | // Inflate the menu; this adds items to the action bar if it is present. 145 | getMenuInflater().inflate(R.menu.side_by_side, menu); 146 | return true; 147 | } 148 | 149 | @Override 150 | public boolean onOptionsItemSelected(MenuItem item) { 151 | // Handle action bar item clicks here. The action bar will 152 | // automatically handle clicks on the Home/Up button, so long 153 | // as you specify a parent activity in AndroidManifest.xml. 154 | int id = item.getItemId(); 155 | if (id == R.id.action_settings) { 156 | return true; 157 | } 158 | return super.onOptionsItemSelected(item); 159 | } 160 | 161 | @Override 162 | public boolean onTouchEvent(MotionEvent event) { 163 | mMediaController.show(); 164 | return super.onTouchEvent(event); 165 | } 166 | 167 | @Override 168 | protected void onStop() { 169 | mMediaController.hide(); 170 | super.onStop(); 171 | } 172 | 173 | private class MediaPlayerMultiControl implements MediaController.MediaPlayerControl { 174 | 175 | private List mControls; 176 | 177 | public MediaPlayerMultiControl(MediaController.MediaPlayerControl... controls) { 178 | mControls = new ArrayList(); 179 | for(MediaController.MediaPlayerControl mpc : controls) { 180 | mControls.add(mpc); 181 | } 182 | } 183 | 184 | @Override 185 | public void start() { 186 | for(MediaController.MediaPlayerControl mpc : mControls) { 187 | mpc.start(); 188 | } 189 | } 190 | 191 | @Override 192 | public void pause() { 193 | for(MediaController.MediaPlayerControl mpc : mControls) { 194 | mpc.pause(); 195 | } 196 | } 197 | 198 | @Override 199 | public int getDuration() { 200 | if(!mControls.isEmpty()) { 201 | return mControls.get(0).getDuration(); 202 | } 203 | return 0; 204 | } 205 | 206 | @Override 207 | public int getCurrentPosition() { 208 | if(!mControls.isEmpty()) { 209 | return mControls.get(0).getCurrentPosition(); 210 | } 211 | return 0; 212 | } 213 | 214 | @Override 215 | public void seekTo(int pos) { 216 | mSeekTarget = pos; 217 | mSeekingViewsCount = mControls.size(); 218 | setProgressBarIndeterminateVisibility(true); 219 | for(MediaController.MediaPlayerControl mpc : mControls) { 220 | mpc.seekTo(pos); 221 | } 222 | } 223 | 224 | @Override 225 | public boolean isPlaying() { 226 | if(!mControls.isEmpty()) { 227 | return mControls.get(0).isPlaying(); 228 | } 229 | return false; 230 | } 231 | 232 | @Override 233 | public int getBufferPercentage() { 234 | if(!mControls.isEmpty()) { 235 | return mControls.get(0).getBufferPercentage(); 236 | } 237 | return 0; 238 | } 239 | 240 | @Override 241 | public boolean canPause() { 242 | if(!mControls.isEmpty()) { 243 | return mControls.get(0).canPause(); 244 | } 245 | return false; 246 | } 247 | 248 | @Override 249 | public boolean canSeekBackward() { 250 | if(!mControls.isEmpty()) { 251 | return mControls.get(0).canSeekBackward(); 252 | } 253 | return false; 254 | } 255 | 256 | @Override 257 | public boolean canSeekForward() { 258 | if(!mControls.isEmpty()) { 259 | return mControls.get(0).canSeekForward(); 260 | } 261 | return false; 262 | } 263 | 264 | @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2) 265 | @Override 266 | public int getAudioSessionId() { 267 | if(!mControls.isEmpty()) { 268 | return mControls.get(0).getAudioSessionId(); 269 | } 270 | return 0; 271 | } 272 | } 273 | } 274 | -------------------------------------------------------------------------------- /MediaPlayerDemo/src/main/java/net/protyposis/android/mediaplayerdemo/Utils.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Mario Guggenberger 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 | 17 | package net.protyposis.android.mediaplayerdemo; 18 | 19 | import android.app.Activity; 20 | import android.content.Context; 21 | import android.graphics.Bitmap; 22 | import android.net.Uri; 23 | import android.os.AsyncTask; 24 | import android.os.Environment; 25 | import android.text.TextUtils; 26 | import android.util.Log; 27 | import android.widget.TextView; 28 | import android.widget.Toast; 29 | 30 | import java.io.BufferedOutputStream; 31 | import java.io.File; 32 | import java.io.FileOutputStream; 33 | import java.io.IOException; 34 | 35 | import net.protyposis.android.mediaplayer.MediaSource; 36 | import net.protyposis.android.mediaplayer.UriSource; 37 | import net.protyposis.android.mediaplayer.dash.AdaptationLogic; 38 | import net.protyposis.android.mediaplayer.dash.DashSource; 39 | import net.protyposis.android.mediaplayer.dash.SimpleRateBasedAdaptationLogic; 40 | 41 | /** 42 | * Created by maguggen on 28.08.2014. 43 | */ 44 | public class Utils { 45 | 46 | private static final String TAG = Utils.class.getSimpleName(); 47 | 48 | public static MediaSource uriToMediaSource(Context context, Uri uri) { 49 | MediaSource source = null; 50 | 51 | // A DASH source is either detected if the given URL has an .mpd extension or if the DASH 52 | // pseudo protocol has been prepended. 53 | if(uri.toString().endsWith(".mpd") || uri.toString().startsWith("dash://")) { 54 | AdaptationLogic adaptationLogic; 55 | 56 | // Strip dash:// pseudo protocol 57 | if(uri.toString().startsWith("dash://")) { 58 | uri = Uri.parse(uri.toString().substring(7)); 59 | } 60 | 61 | //adaptationLogic = new ConstantPropertyBasedLogic(ConstantPropertyBasedLogic.Mode.HIGHEST_BITRATE); 62 | adaptationLogic = new SimpleRateBasedAdaptationLogic(); 63 | 64 | source = new DashSource(context, uri, adaptationLogic); 65 | } else { 66 | source = new UriSource(context, uri); 67 | } 68 | return source; 69 | } 70 | 71 | public static void uriToMediaSourceAsync(final Context context, Uri uri, MediaSourceAsyncCallbackHandler callback) { 72 | LoadMediaSourceAsyncTask loadingTask = new LoadMediaSourceAsyncTask(context, callback); 73 | 74 | try { 75 | loadingTask.execute(uri).get(); 76 | } catch (Exception e) { 77 | Log.e(TAG, e.getMessage(), e); 78 | } 79 | } 80 | 81 | public static void setActionBarSubtitleEllipsizeMiddle(Activity activity) { 82 | // http://blog.wu-man.com/2011/12/actionbar-api-provided-by-google-on.html 83 | int subtitleId = activity.getResources().getIdentifier("action_bar_subtitle", "id", "android"); 84 | TextView subtitleView = (TextView) activity.findViewById(subtitleId); 85 | subtitleView.setEllipsize(TextUtils.TruncateAt.MIDDLE); 86 | } 87 | 88 | public static boolean saveBitmapToFile(Bitmap bmp, File file) { 89 | try { 90 | BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(file)); 91 | bmp.compress(Bitmap.CompressFormat.PNG, 90, bos); 92 | bos.close(); 93 | return true; 94 | } catch (IOException e) { 95 | Log.e(TAG, "failed to save frame", e); 96 | } 97 | return false; 98 | } 99 | 100 | private static class LoadMediaSourceAsyncTask extends AsyncTask { 101 | 102 | private Context mContext; 103 | private MediaSourceAsyncCallbackHandler mCallbackHandler; 104 | private MediaSource mMediaSource; 105 | private Exception mException; 106 | 107 | public LoadMediaSourceAsyncTask(Context context, MediaSourceAsyncCallbackHandler callbackHandler) { 108 | mContext = context; 109 | mCallbackHandler = callbackHandler; 110 | } 111 | 112 | @Override 113 | protected MediaSource doInBackground(Uri... params) { 114 | try { 115 | mMediaSource = Utils.uriToMediaSource(mContext, params[0]); 116 | return mMediaSource; 117 | } catch (Exception e) { 118 | mException = e; 119 | return null; 120 | } 121 | } 122 | 123 | @Override 124 | protected void onPostExecute(MediaSource mediaSource) { 125 | if(mException != null) { 126 | mCallbackHandler.onException(mException); 127 | } else { 128 | mCallbackHandler.onMediaSourceLoaded(mMediaSource); 129 | } 130 | } 131 | } 132 | 133 | public static interface MediaSourceAsyncCallbackHandler { 134 | void onMediaSourceLoaded(MediaSource mediaSource); 135 | void onException(Exception e); 136 | } 137 | 138 | /** 139 | * Iterates a hierarchy of exceptions and combines their messages. Useful for compact 140 | * error representation to the user during debugging/development. 141 | */ 142 | public static String getExceptionMessageHistory(Throwable e) { 143 | String messages = ""; 144 | 145 | String message = e.getMessage(); 146 | if(message != null) { 147 | messages += message; 148 | } 149 | while((e = e.getCause()) != null) { 150 | message = e.getMessage(); 151 | if(message != null) { 152 | messages += " <- " + message; 153 | } 154 | } 155 | 156 | return messages; 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /MediaPlayerDemo/src/main/java/net/protyposis/android/mediaplayerdemo/VideoURIInputDialogFragment.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Mario Guggenberger 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 | 17 | package net.protyposis.android.mediaplayerdemo; 18 | 19 | import android.app.Activity; 20 | import android.app.AlertDialog; 21 | import android.app.Dialog; 22 | import android.app.DialogFragment; 23 | import android.content.DialogInterface; 24 | import android.net.Uri; 25 | import android.os.Bundle; 26 | import android.view.View; 27 | import android.widget.EditText; 28 | 29 | 30 | public class VideoURIInputDialogFragment extends DialogFragment { 31 | 32 | private OnVideoURISelectedListener mListener; 33 | 34 | public VideoURIInputDialogFragment() { 35 | // Required empty public constructor 36 | } 37 | 38 | @Override 39 | public Dialog onCreateDialog(Bundle savedInstanceState) { 40 | AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); 41 | View dialogView = getActivity().getLayoutInflater() 42 | .inflate(R.layout.fragment_uriinput_dialog, null); 43 | 44 | final EditText urlText = (EditText) dialogView.findViewById(R.id.url); 45 | 46 | builder.setTitle(getString(R.string.openvideo_type)) 47 | .setView(dialogView) 48 | .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { 49 | @Override 50 | public void onClick(DialogInterface dialog, int which) { 51 | mListener.onVideoURISelected(Uri.parse(urlText.getText().toString())); 52 | } 53 | }) 54 | .setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() { 55 | @Override 56 | public void onClick(DialogInterface dialog, int which) { 57 | // nothing to do here 58 | } 59 | }); 60 | return builder.create(); 61 | } 62 | 63 | @Override 64 | public void onAttach(Activity activity) { 65 | super.onAttach(activity); 66 | try { 67 | mListener = (OnVideoURISelectedListener) activity; 68 | } catch (ClassCastException e) { 69 | throw new ClassCastException(activity.toString() 70 | + " must implement OnVideoURISelectedListener"); 71 | } 72 | } 73 | 74 | @Override 75 | public void onDetach() { 76 | super.onDetach(); 77 | mListener = null; 78 | } 79 | 80 | public interface OnVideoURISelectedListener { 81 | public void onVideoURISelected(Uri uri); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /MediaPlayerDemo/src/main/java/net/protyposis/android/mediaplayerdemo/VideoViewActivity.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Mario Guggenberger 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 | 17 | package net.protyposis.android.mediaplayerdemo; 18 | 19 | import android.app.Activity; 20 | import android.net.Uri; 21 | import android.os.Bundle; 22 | import android.util.Log; 23 | import android.view.Menu; 24 | import android.view.MenuItem; 25 | import android.view.MotionEvent; 26 | import android.view.View; 27 | import android.widget.MediaController; 28 | import android.widget.ProgressBar; 29 | import android.widget.Toast; 30 | 31 | import net.protyposis.android.mediaplayer.Cue; 32 | import net.protyposis.android.mediaplayer.MediaPlayer; 33 | import net.protyposis.android.mediaplayer.MediaSource; 34 | import net.protyposis.android.mediaplayer.VideoView; 35 | 36 | public class VideoViewActivity extends Activity { 37 | 38 | private static final String TAG = VideoViewActivity.class.getSimpleName(); 39 | 40 | private VideoView mVideoView; 41 | private ProgressBar mProgress; 42 | 43 | private MediaController.MediaPlayerControl mMediaPlayerControl; 44 | private MediaController mMediaController; 45 | 46 | private Uri mVideoUri; 47 | private int mVideoPosition; 48 | private float mVideoPlaybackSpeed; 49 | private boolean mVideoPlaying; 50 | private MediaSource mMediaSource; 51 | 52 | @Override 53 | protected void onCreate(Bundle savedInstanceState) { 54 | super.onCreate(savedInstanceState); 55 | setContentView(R.layout.activity_videoview); 56 | Utils.setActionBarSubtitleEllipsizeMiddle(this); 57 | 58 | mVideoView = (VideoView) findViewById(R.id.vv); 59 | mProgress = (ProgressBar) findViewById(R.id.progress); 60 | 61 | mMediaPlayerControl = mVideoView; //new MediaPlayerDummyControl(); 62 | mMediaController = new MediaController(this); 63 | mMediaController.setAnchorView(findViewById(R.id.container)); 64 | mMediaController.setMediaPlayer(mMediaPlayerControl); 65 | mMediaController.setEnabled(false); 66 | 67 | mProgress.setVisibility(View.VISIBLE); 68 | 69 | // Init video playback state (will eventually be overwritten by saved instance state) 70 | mVideoUri = getIntent().getData(); 71 | mVideoPosition = 0; 72 | mVideoPlaybackSpeed = 1; 73 | mVideoPlaying = false; 74 | } 75 | 76 | @Override 77 | protected void onRestoreInstanceState(Bundle savedInstanceState) { 78 | super.onRestoreInstanceState(savedInstanceState); 79 | mVideoUri = savedInstanceState.getParcelable("uri"); 80 | mVideoPosition = savedInstanceState.getInt("position"); 81 | mVideoPlaybackSpeed = savedInstanceState.getFloat("playbackSpeed"); 82 | mVideoPlaying = savedInstanceState.getBoolean("playing"); 83 | } 84 | 85 | @Override 86 | protected void onResume() { 87 | super.onResume(); 88 | if(!mVideoView.isPlaying()) { 89 | initPlayer(); 90 | } 91 | } 92 | 93 | private void initPlayer() { 94 | getActionBar().setSubtitle(mVideoUri+""); 95 | 96 | mVideoView.setOnPreparedListener(new MediaPlayer.OnPreparedListener() { 97 | @Override 98 | public void onPrepared(MediaPlayer vp) { 99 | mProgress.setVisibility(View.GONE); 100 | mMediaController.setEnabled(true); 101 | 102 | vp.addCue(1000, "test cue @ 1000"); 103 | vp.addCue(2000, "test cue @ 2000"); 104 | vp.addCue(3000, "test cue @ 3000"); 105 | vp.addCue(10000, "test cue @ 10000"); 106 | vp.setOnCueListener(new MediaPlayer.OnCueListener() { 107 | @Override 108 | public void onCue(MediaPlayer mp, Cue cue) { 109 | Log.d(TAG, "onCue: " + cue.toString() + " (" + mp.getCurrentPosition() + ")"); 110 | } 111 | }); 112 | } 113 | }); 114 | mVideoView.setOnErrorListener(new MediaPlayer.OnErrorListener() { 115 | @Override 116 | public boolean onError(MediaPlayer mp, int what, int extra) { 117 | Toast.makeText(VideoViewActivity.this, 118 | "Cannot play the video, see logcat for the detailed exception", 119 | Toast.LENGTH_LONG).show(); 120 | mProgress.setVisibility(View.GONE); 121 | mMediaController.setEnabled(false); 122 | return true; 123 | } 124 | }); 125 | mVideoView.setOnInfoListener(new MediaPlayer.OnInfoListener() { 126 | @Override 127 | public boolean onInfo(MediaPlayer mp, int what, int extra) { 128 | String whatName = ""; 129 | switch (what) { 130 | case MediaPlayer.MEDIA_INFO_BUFFERING_END: 131 | whatName = "MEDIA_INFO_BUFFERING_END"; 132 | break; 133 | case MediaPlayer.MEDIA_INFO_BUFFERING_START: 134 | whatName = "MEDIA_INFO_BUFFERING_START"; 135 | break; 136 | case MediaPlayer.MEDIA_INFO_VIDEO_RENDERING_START: 137 | whatName = "MEDIA_INFO_VIDEO_RENDERING_START"; 138 | break; 139 | case MediaPlayer.MEDIA_INFO_VIDEO_TRACK_LAGGING: 140 | whatName = "MEDIA_INFO_VIDEO_TRACK_LAGGING"; 141 | break; 142 | } 143 | Log.d(TAG, "onInfo " + whatName); 144 | return false; 145 | } 146 | }); 147 | mVideoView.setOnSeekListener(new MediaPlayer.OnSeekListener() { 148 | @Override 149 | public void onSeek(MediaPlayer mp) { 150 | Log.d(TAG, "onSeek"); 151 | mProgress.setVisibility(View.VISIBLE); 152 | } 153 | }); 154 | mVideoView.setOnSeekCompleteListener(new MediaPlayer.OnSeekCompleteListener() { 155 | @Override 156 | public void onSeekComplete(MediaPlayer mp) { 157 | Log.d(TAG, "onSeekComplete"); 158 | mProgress.setVisibility(View.GONE); 159 | } 160 | }); 161 | mVideoView.setOnBufferingUpdateListener(new MediaPlayer.OnBufferingUpdateListener() { 162 | @Override 163 | public void onBufferingUpdate(MediaPlayer mp, int percent) { 164 | Log.d(TAG, "onBufferingUpdate " + percent + "%"); 165 | } 166 | }); 167 | 168 | Utils.MediaSourceAsyncCallbackHandler mMediaSourceAsyncCallbackHandler = 169 | new Utils.MediaSourceAsyncCallbackHandler() { 170 | @Override 171 | public void onMediaSourceLoaded(MediaSource mediaSource) { 172 | mMediaSource = mediaSource; 173 | mVideoView.setVideoSource(mediaSource); 174 | mVideoView.seekTo(mVideoPosition); 175 | mVideoView.setPlaybackSpeed(mVideoPlaybackSpeed); 176 | if (mVideoPlaying) { 177 | mVideoView.start(); 178 | } 179 | } 180 | 181 | @Override 182 | public void onException(Exception e) { 183 | Log.e(TAG, "error loading video", e); 184 | } 185 | }; 186 | if(mMediaSource == null) { 187 | // Convert uri to media source asynchronously to avoid UI blocking 188 | // It could take a while, e.g. if it's a DASH source and needs to be preprocessed 189 | Utils.uriToMediaSourceAsync(this, mVideoUri, mMediaSourceAsyncCallbackHandler); 190 | } else { 191 | // Media source is already here, just use it 192 | mMediaSourceAsyncCallbackHandler.onMediaSourceLoaded(mMediaSource); 193 | } 194 | } 195 | 196 | @Override 197 | public boolean onCreateOptionsMenu(Menu menu) { 198 | // Inflate the menu; this adds items to the action bar if it is present. 199 | getMenuInflater().inflate(R.menu.videoview, menu); 200 | return true; 201 | } 202 | 203 | @Override 204 | public boolean onOptionsItemSelected(MenuItem item) { 205 | // Handle action bar item clicks here. The action bar will 206 | // automatically handle clicks on the Home/Up button, so long 207 | // as you specify a parent activity in AndroidManifest.xml. 208 | int id = item.getItemId(); 209 | if (id == R.id.action_slowspeed) { 210 | mVideoView.setPlaybackSpeed(0.2f); 211 | return true; 212 | } else if(id == R.id.action_halfspeed) { 213 | mVideoView.setPlaybackSpeed(0.5f); 214 | return true; 215 | } else if(id == R.id.action_doublespeed) { 216 | mVideoView.setPlaybackSpeed(2.0f); 217 | return true; 218 | } else if(id == R.id.action_quadspeed) { 219 | mVideoView.setPlaybackSpeed(4.0f); 220 | return true; 221 | } else if(id == R.id.action_normalspeed) { 222 | mVideoView.setPlaybackSpeed(1.0f); 223 | return true; 224 | } else if(id == R.id.action_seekcurrentposition) { 225 | mVideoView.pause(); 226 | mVideoView.seekTo(mVideoView.getCurrentPosition()); 227 | return true; 228 | } else if(id == R.id.action_seekcurrentpositionplus1ms) { 229 | mVideoView.pause(); 230 | mVideoView.seekTo(mVideoView.getCurrentPosition()+1); 231 | return true; 232 | } else if(id == R.id.action_seektoend) { 233 | mVideoView.pause(); 234 | mVideoView.seekTo(mVideoView.getDuration()); 235 | return true; 236 | } else if(id == R.id.action_getcurrentposition) { 237 | Toast.makeText(this, "current position: " + mVideoView.getCurrentPosition(), Toast.LENGTH_SHORT).show(); 238 | return true; 239 | } else if(id == R.id.action_reload_source) { 240 | initPlayer(); 241 | } 242 | return super.onOptionsItemSelected(item); 243 | } 244 | 245 | @Override 246 | public boolean onTouchEvent(MotionEvent event) { 247 | if (event.getAction() == MotionEvent.ACTION_UP) { 248 | if (mMediaController.isShowing()) { 249 | mMediaController.hide(); 250 | } else { 251 | mMediaController.show(); 252 | } 253 | } 254 | return super.onTouchEvent(event); 255 | } 256 | 257 | @Override 258 | protected void onStop() { 259 | mMediaController.hide(); 260 | super.onStop(); 261 | } 262 | 263 | @Override 264 | protected void onSaveInstanceState(Bundle outState) { 265 | super.onSaveInstanceState(outState); 266 | if (mVideoView != null) { 267 | mVideoPosition = mVideoView.getCurrentPosition(); 268 | mVideoPlaybackSpeed = mVideoView.getPlaybackSpeed(); 269 | mVideoPlaying = mVideoView.isPlaying(); 270 | // the uri is stored in the base activity 271 | outState.putParcelable("uri", mVideoUri); 272 | outState.putInt("position", mVideoPosition); 273 | outState.putFloat("playbackSpeed", mVideoView.getPlaybackSpeed()); 274 | outState.putBoolean("playing", mVideoPlaying); 275 | } 276 | } 277 | } 278 | -------------------------------------------------------------------------------- /MediaPlayerDemo/src/main/res/drawable-hdpi/ic_action_picture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/protyposis/MediaPlayer-Extended/460dcccf70894aa45d2c59ff8f8f3ec7c794faee/MediaPlayerDemo/src/main/res/drawable-hdpi/ic_action_picture.png -------------------------------------------------------------------------------- /MediaPlayerDemo/src/main/res/drawable-hdpi/ic_action_save.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/protyposis/MediaPlayer-Extended/460dcccf70894aa45d2c59ff8f8f3ec7c794faee/MediaPlayerDemo/src/main/res/drawable-hdpi/ic_action_save.png -------------------------------------------------------------------------------- /MediaPlayerDemo/src/main/res/drawable-hdpi/ic_action_settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/protyposis/MediaPlayer-Extended/460dcccf70894aa45d2c59ff8f8f3ec7c794faee/MediaPlayerDemo/src/main/res/drawable-hdpi/ic_action_settings.png -------------------------------------------------------------------------------- /MediaPlayerDemo/src/main/res/drawable-hdpi/ic_action_switch_camera.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/protyposis/MediaPlayer-Extended/460dcccf70894aa45d2c59ff8f8f3ec7c794faee/MediaPlayerDemo/src/main/res/drawable-hdpi/ic_action_switch_camera.png -------------------------------------------------------------------------------- /MediaPlayerDemo/src/main/res/drawable-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/protyposis/MediaPlayer-Extended/460dcccf70894aa45d2c59ff8f8f3ec7c794faee/MediaPlayerDemo/src/main/res/drawable-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /MediaPlayerDemo/src/main/res/drawable-mdpi/ic_action_picture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/protyposis/MediaPlayer-Extended/460dcccf70894aa45d2c59ff8f8f3ec7c794faee/MediaPlayerDemo/src/main/res/drawable-mdpi/ic_action_picture.png -------------------------------------------------------------------------------- /MediaPlayerDemo/src/main/res/drawable-mdpi/ic_action_save.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/protyposis/MediaPlayer-Extended/460dcccf70894aa45d2c59ff8f8f3ec7c794faee/MediaPlayerDemo/src/main/res/drawable-mdpi/ic_action_save.png -------------------------------------------------------------------------------- /MediaPlayerDemo/src/main/res/drawable-mdpi/ic_action_settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/protyposis/MediaPlayer-Extended/460dcccf70894aa45d2c59ff8f8f3ec7c794faee/MediaPlayerDemo/src/main/res/drawable-mdpi/ic_action_settings.png -------------------------------------------------------------------------------- /MediaPlayerDemo/src/main/res/drawable-mdpi/ic_action_switch_camera.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/protyposis/MediaPlayer-Extended/460dcccf70894aa45d2c59ff8f8f3ec7c794faee/MediaPlayerDemo/src/main/res/drawable-mdpi/ic_action_switch_camera.png -------------------------------------------------------------------------------- /MediaPlayerDemo/src/main/res/drawable-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/protyposis/MediaPlayer-Extended/460dcccf70894aa45d2c59ff8f8f3ec7c794faee/MediaPlayerDemo/src/main/res/drawable-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /MediaPlayerDemo/src/main/res/drawable-xhdpi/ic_action_picture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/protyposis/MediaPlayer-Extended/460dcccf70894aa45d2c59ff8f8f3ec7c794faee/MediaPlayerDemo/src/main/res/drawable-xhdpi/ic_action_picture.png -------------------------------------------------------------------------------- /MediaPlayerDemo/src/main/res/drawable-xhdpi/ic_action_save.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/protyposis/MediaPlayer-Extended/460dcccf70894aa45d2c59ff8f8f3ec7c794faee/MediaPlayerDemo/src/main/res/drawable-xhdpi/ic_action_save.png -------------------------------------------------------------------------------- /MediaPlayerDemo/src/main/res/drawable-xhdpi/ic_action_settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/protyposis/MediaPlayer-Extended/460dcccf70894aa45d2c59ff8f8f3ec7c794faee/MediaPlayerDemo/src/main/res/drawable-xhdpi/ic_action_settings.png -------------------------------------------------------------------------------- /MediaPlayerDemo/src/main/res/drawable-xhdpi/ic_action_switch_camera.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/protyposis/MediaPlayer-Extended/460dcccf70894aa45d2c59ff8f8f3ec7c794faee/MediaPlayerDemo/src/main/res/drawable-xhdpi/ic_action_switch_camera.png -------------------------------------------------------------------------------- /MediaPlayerDemo/src/main/res/drawable-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/protyposis/MediaPlayer-Extended/460dcccf70894aa45d2c59ff8f8f3ec7c794faee/MediaPlayerDemo/src/main/res/drawable-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /MediaPlayerDemo/src/main/res/drawable-xxhdpi/ic_action_picture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/protyposis/MediaPlayer-Extended/460dcccf70894aa45d2c59ff8f8f3ec7c794faee/MediaPlayerDemo/src/main/res/drawable-xxhdpi/ic_action_picture.png -------------------------------------------------------------------------------- /MediaPlayerDemo/src/main/res/drawable-xxhdpi/ic_action_save.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/protyposis/MediaPlayer-Extended/460dcccf70894aa45d2c59ff8f8f3ec7c794faee/MediaPlayerDemo/src/main/res/drawable-xxhdpi/ic_action_save.png -------------------------------------------------------------------------------- /MediaPlayerDemo/src/main/res/drawable-xxhdpi/ic_action_settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/protyposis/MediaPlayer-Extended/460dcccf70894aa45d2c59ff8f8f3ec7c794faee/MediaPlayerDemo/src/main/res/drawable-xxhdpi/ic_action_settings.png -------------------------------------------------------------------------------- /MediaPlayerDemo/src/main/res/drawable-xxhdpi/ic_action_switch_camera.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/protyposis/MediaPlayer-Extended/460dcccf70894aa45d2c59ff8f8f3ec7c794faee/MediaPlayerDemo/src/main/res/drawable-xxhdpi/ic_action_switch_camera.png -------------------------------------------------------------------------------- /MediaPlayerDemo/src/main/res/drawable-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/protyposis/MediaPlayer-Extended/460dcccf70894aa45d2c59ff8f8f3ec7c794faee/MediaPlayerDemo/src/main/res/drawable-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /MediaPlayerDemo/src/main/res/layout-land/activity_side_by_side.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 27 | 28 | 35 | 36 | 46 | 47 | 54 | 55 | 63 | 64 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /MediaPlayerDemo/src/main/res/layout-land/activity_side_by_side_seektest.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 27 | 28 | 35 | 36 | 45 | 46 | 47 | 54 | 55 | 64 | 65 | 66 | 73 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /MediaPlayerDemo/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 18 | 19 | 25 | 26 | 34 | 35 | 42 | 43 | 50 | 51 | 58 | 59 | 60 | 61 | 64 | 65 |