├── .gitignore ├── LICENSE ├── README.md ├── build.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── library ├── build.gradle └── src │ └── main │ └── java │ └── com │ └── danikula │ └── videocache │ ├── ByteArrayCache.java │ ├── ByteArraySource.java │ ├── Cache.java │ ├── CacheListener.java │ ├── FileCache.java │ ├── HttpProxyCache.java │ ├── HttpUrlSource.java │ ├── Preconditions.java │ ├── ProxyCache.java │ ├── ProxyCacheException.java │ ├── ProxyCacheUtils.java │ └── Source.java ├── sample ├── build.gradle └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ ├── com │ │ └── exoplayer │ │ │ └── player │ │ │ ├── DebugTrackRenderer.java │ │ │ ├── DemoPlayer.java │ │ │ ├── HlsRendererBuilder.java │ │ │ └── PlayerActivity.java │ │ ├── danikula │ │ └── videocache │ │ │ └── sample │ │ │ └── VideoCacheActivity.java │ │ └── jit │ │ └── video │ │ ├── AdvancedVideoActivity.java │ │ ├── DensityUtil.java │ │ ├── FullScreenSurfaceView.java │ │ ├── FullScreenVideoView.java │ │ ├── LightnessController.java │ │ ├── VolumnController.java │ │ └── VolumnView.java │ └── res │ ├── anim │ ├── option_entry_from_bottom.xml │ ├── option_entry_from_top.xml │ ├── option_leave_from_bottom.xml │ └── option_leave_from_top.xml │ ├── drawable-hdpi │ ├── ic_launcher.png │ ├── line_point.png │ ├── ling.png │ ├── top_btnback.png │ ├── video_btn_down.png │ ├── video_btn_on.png │ ├── video_line01.png │ └── video_line02.png │ ├── drawable-mdpi │ └── ic_launcher.png │ ├── drawable-xhdpi │ ├── ic_launcher.png │ └── ic_vm.png │ ├── drawable-xxhdpi │ └── ic_launcher.png │ ├── drawable │ └── seekbar.xml │ ├── layout │ ├── activity_exo_player.xml │ ├── activity_main.xml │ ├── activity_video.xml │ ├── player_activity.xml │ └── vv.xml │ ├── mipmap-xxhdpi │ └── ic_launcher.png │ └── values │ └── strings.xml ├── settings.gradle └── test ├── build.gradle └── src ├── main ├── AndroidManifest.xml └── assets │ ├── android.jpg │ └── phones.jpg └── test └── java └── com └── danikula └── videocache ├── FileCacheTest.java ├── HttpProxyCacheTest.java ├── HttpUrlSourceTest.java ├── ProxyCacheTest.java └── support ├── AngryHttpUrlSource.java ├── PhlegmaticByteArraySource.java ├── ProxyCacheTestUtils.java └── Response.java /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .DS_Store 3 | .gradle 4 | 5 | /.idea 6 | /build 7 | /local.properties 8 | /gradle.properties 9 | /library/build 10 | /library/build 11 | /sample/build 12 | /test/build 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Video cache support for Android 2 | [![Android Arsenal](https://img.shields.io/badge/Android%20Arsenal-AndroidVideoCache-brightgreen.svg?style=flat)](http://android-arsenal.com/details/1/1751) 3 | 4 | ## Why AndroidVideoCache? 5 | Because android MediaPlayer doesn't cache video while streaming. 6 | 7 | ## How to use? 8 | Just add link to repository and dependency: 9 | ``` 10 | repositories { 11 | maven { url 'https://dl.bintray.com/alexeydanilov/maven' } 12 | } 13 | dependencies { 14 | compile 'com.danikula:videocache:1.0.1' 15 | } 16 | ``` 17 | 18 | and use proxy for caching video: 19 | 20 | ```java 21 | @Override 22 | protected void onCreate(Bundle savedInstanceState) { 23 | super.onCreate(savedInstanceState); 24 | ... 25 | try { 26 | Cache cache = new FileCache(new File(getExternalCacheDir(), VIDEO_CACHE_NAME)); 27 | HttpUrlSource source = new HttpUrlSource(VIDEO_URL); 28 | proxyCache = new HttpProxyCache(source, cache); 29 | videoView.setVideoPath(proxyCache.getUrl()); 30 | videoView.start(); 31 | } catch (ProxyCacheException e) { 32 | Log.e(LOG_TAG, "Error playing video", e); 33 | } 34 | } 35 | 36 | @Override 37 | public void onDestroy() { 38 | super.onDestroy(); 39 | 40 | if (proxyCache != null) { 41 | proxyCache.shutdown(); 42 | } 43 | } 44 | ``` 45 | 46 | See `sample` app for details. 47 | 48 | ## Where published? 49 | [Here](https://bintray.com/alexeydanilov/maven/videocache/view) 50 | 51 | ## License 52 | 53 | Copyright 2014-2015 Alexey Danilov 54 | 55 | Licensed under the Apache License, Version 2.0 (the "License"); 56 | you may not use this file except in compliance with the License. 57 | You may obtain a copy of the License at 58 | 59 | http://www.apache.org/licenses/LICENSE-2.0 60 | 61 | Unless required by applicable law or agreed to in writing, software 62 | distributed under the License is distributed on an "AS IS" BASIS, 63 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 64 | See the License for the specific language governing permissions and 65 | limitations under the License. -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | mavenCentral() 4 | } 5 | dependencies { 6 | classpath 'com.android.tools.build:gradle:1.2.3' 7 | } 8 | } 9 | 10 | allprojects { 11 | repositories { 12 | jcenter() 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/korrio/AdvancedVideoView/b64b3702886cde5bf4ec135c4a64b10a77ee92c8/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Wed Apr 10 15:27:10 PDT 2013 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-2.4-all.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # For Cygwin, ensure paths are in UNIX format before anything is touched. 46 | if $cygwin ; then 47 | [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 48 | fi 49 | 50 | # Attempt to set APP_HOME 51 | # Resolve links: $0 may be a link 52 | PRG="$0" 53 | # Need this for relative symlinks. 54 | while [ -h "$PRG" ] ; do 55 | ls=`ls -ld "$PRG"` 56 | link=`expr "$ls" : '.*-> \(.*\)$'` 57 | if expr "$link" : '/.*' > /dev/null; then 58 | PRG="$link" 59 | else 60 | PRG=`dirname "$PRG"`"/$link" 61 | fi 62 | done 63 | SAVED="`pwd`" 64 | cd "`dirname \"$PRG\"`/" >&- 65 | APP_HOME="`pwd -P`" 66 | cd "$SAVED" >&- 67 | 68 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 69 | 70 | # Determine the Java command to use to start the JVM. 71 | if [ -n "$JAVA_HOME" ] ; then 72 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 73 | # IBM's JDK on AIX uses strange locations for the executables 74 | JAVACMD="$JAVA_HOME/jre/sh/java" 75 | else 76 | JAVACMD="$JAVA_HOME/bin/java" 77 | fi 78 | if [ ! -x "$JAVACMD" ] ; then 79 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 80 | 81 | Please set the JAVA_HOME variable in your environment to match the 82 | location of your Java installation." 83 | fi 84 | else 85 | JAVACMD="java" 86 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 87 | 88 | Please set the JAVA_HOME variable in your environment to match the 89 | location of your Java installation." 90 | fi 91 | 92 | # Increase the maximum file descriptors if we can. 93 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 94 | MAX_FD_LIMIT=`ulimit -H -n` 95 | if [ $? -eq 0 ] ; then 96 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 97 | MAX_FD="$MAX_FD_LIMIT" 98 | fi 99 | ulimit -n $MAX_FD 100 | if [ $? -ne 0 ] ; then 101 | warn "Could not set maximum file descriptor limit: $MAX_FD" 102 | fi 103 | else 104 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 105 | fi 106 | fi 107 | 108 | # For Darwin, add options to specify how the application appears in the dock 109 | if $darwin; then 110 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 111 | fi 112 | 113 | # For Cygwin, switch paths to Windows format before running java 114 | if $cygwin ; then 115 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 116 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 158 | function splitJvmOpts() { 159 | JVM_OPTS=("$@") 160 | } 161 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 162 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 163 | 164 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 165 | -------------------------------------------------------------------------------- /library/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | jcenter() 4 | } 5 | dependencies { 6 | classpath 'com.novoda:bintray-release:0.2.10' 7 | } 8 | } 9 | 10 | apply plugin: 'java' 11 | apply plugin: 'bintray-release' 12 | 13 | dependencies { 14 | compile 'com.google.android:android:1.6_r2' 15 | } 16 | 17 | publish { 18 | userOrg = 'alexeydanilov' 19 | groupId = 'com.danikula' 20 | artifactId = 'videocache' 21 | publishVersion = '1.0.1' 22 | description = 'Cache support for android VideoView' 23 | website = 'https://github.com/danikula/AndroidVideoCache' 24 | } 25 | -------------------------------------------------------------------------------- /library/src/main/java/com/danikula/videocache/ByteArrayCache.java: -------------------------------------------------------------------------------- 1 | package com.danikula.videocache; 2 | 3 | import java.io.ByteArrayInputStream; 4 | import java.util.Arrays; 5 | 6 | import static com.danikula.videocache.Preconditions.checkArgument; 7 | import static com.danikula.videocache.Preconditions.checkNotNull; 8 | 9 | /** 10 | * Simple memory based {@link Cache} implementation. 11 | * 12 | * @author Alexey Danilov (danikula@gmail.com). 13 | */ 14 | public class ByteArrayCache implements Cache { 15 | 16 | private volatile byte[] data; 17 | private volatile boolean completed; 18 | 19 | public ByteArrayCache() { 20 | this(new byte[0]); 21 | } 22 | 23 | public ByteArrayCache(byte[] data) { 24 | this.data = Preconditions.checkNotNull(data); 25 | } 26 | 27 | 28 | @Override 29 | public int read(byte[] buffer, long offset, int length) throws ProxyCacheException { 30 | if (offset >= data.length) { 31 | return -1; 32 | } 33 | if (offset > Integer.MAX_VALUE) { 34 | throw new IllegalArgumentException("Too long offset for memory cache " + offset); 35 | } 36 | return new ByteArrayInputStream(data).read(buffer, (int) offset, length); 37 | } 38 | 39 | @Override 40 | public int available() throws ProxyCacheException { 41 | return data.length; 42 | } 43 | 44 | @Override 45 | public void append(byte[] newData, int length) throws ProxyCacheException { 46 | Preconditions.checkNotNull(data); 47 | Preconditions.checkArgument(length >= 0 && length <= newData.length); 48 | 49 | byte[] appendedData = Arrays.copyOf(data, data.length + length); 50 | System.arraycopy(newData, 0, appendedData, data.length, length); 51 | data = appendedData; 52 | } 53 | 54 | @Override 55 | public void close() throws ProxyCacheException { 56 | } 57 | 58 | @Override 59 | public void complete() { 60 | completed = true; 61 | } 62 | 63 | @Override 64 | public boolean isCompleted() { 65 | return completed; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /library/src/main/java/com/danikula/videocache/ByteArraySource.java: -------------------------------------------------------------------------------- 1 | package com.danikula.videocache; 2 | 3 | import java.io.ByteArrayInputStream; 4 | 5 | /** 6 | * Simple memory based {@link Source} implementation. 7 | * 8 | * @author Alexey Danilov (danikula@gmail.com). 9 | */ 10 | public class ByteArraySource implements Source { 11 | 12 | private final byte[] data; 13 | private ByteArrayInputStream arrayInputStream; 14 | 15 | public ByteArraySource(byte[] data) { 16 | this.data = data; 17 | } 18 | 19 | @Override 20 | public int read(byte[] buffer) throws ProxyCacheException { 21 | return arrayInputStream.read(buffer, 0, buffer.length); 22 | } 23 | 24 | @Override 25 | public int available() throws ProxyCacheException { 26 | return data.length; 27 | } 28 | 29 | @Override 30 | public void open(int offset) throws ProxyCacheException { 31 | arrayInputStream = new ByteArrayInputStream(data); 32 | arrayInputStream.skip(offset); 33 | } 34 | 35 | @Override 36 | public void close() throws ProxyCacheException { 37 | } 38 | } 39 | 40 | -------------------------------------------------------------------------------- /library/src/main/java/com/danikula/videocache/Cache.java: -------------------------------------------------------------------------------- 1 | package com.danikula.videocache; 2 | 3 | /** 4 | * Cache for proxy. 5 | * 6 | * @author Alexey Danilov (danikula@gmail.com). 7 | */ 8 | public interface Cache { 9 | 10 | int available() throws ProxyCacheException; 11 | 12 | int read(byte[] buffer, long offset, int length) throws ProxyCacheException; 13 | 14 | void append(byte[] data, int length) throws ProxyCacheException; 15 | 16 | void close() throws ProxyCacheException; 17 | 18 | void complete() throws ProxyCacheException; 19 | 20 | boolean isCompleted(); 21 | } 22 | -------------------------------------------------------------------------------- /library/src/main/java/com/danikula/videocache/CacheListener.java: -------------------------------------------------------------------------------- 1 | package com.danikula.videocache; 2 | 3 | /** 4 | * @author Egor Makovsky (yahor.makouski@gmail.com). 5 | */ 6 | public interface CacheListener { 7 | void onError(ProxyCacheException e); 8 | 9 | void onCacheDataAvailable(int cachePercentage); 10 | } 11 | -------------------------------------------------------------------------------- /library/src/main/java/com/danikula/videocache/FileCache.java: -------------------------------------------------------------------------------- 1 | package com.danikula.videocache; 2 | 3 | import java.io.File; 4 | import java.io.IOException; 5 | import java.io.RandomAccessFile; 6 | 7 | import static com.danikula.videocache.Preconditions.checkNotNull; 8 | 9 | /** 10 | * {@link Cache} that uses file for storing data. 11 | * 12 | * @author Alexey Danilov (danikula@gmail.com). 13 | */ 14 | public class FileCache implements Cache { 15 | 16 | private static final String TEMP_POSTFIX = ".download"; 17 | 18 | private RandomAccessFile dataFile; 19 | private File file; 20 | 21 | public FileCache(File file) throws ProxyCacheException { 22 | try { 23 | checkNotNull(file); 24 | boolean partialFile = isTempFile(file); 25 | boolean completed = file.exists() && !partialFile; 26 | if (completed) { 27 | this.dataFile = new RandomAccessFile(file, "r"); 28 | this.file = file; 29 | } else { 30 | ProxyCacheUtils.createDirectory(file.getParentFile()); 31 | this.file = partialFile ? file : new File(file.getParentFile(), file.getName() + TEMP_POSTFIX); 32 | this.dataFile = new RandomAccessFile(this.file, "rw"); 33 | } 34 | } catch (IOException e) { 35 | throw new ProxyCacheException("Error using file " + file + " as disc cache", e); 36 | } 37 | } 38 | 39 | @Override 40 | public synchronized int available() throws ProxyCacheException { 41 | try { 42 | return (int) dataFile.length(); 43 | } catch (IOException e) { 44 | throw new ProxyCacheException("Error reading length of file " + dataFile, e); 45 | } 46 | } 47 | 48 | @Override 49 | public synchronized int read(byte[] buffer, long offset, int length) throws ProxyCacheException { 50 | try { 51 | dataFile.seek(offset); 52 | return dataFile.read(buffer, 0, length); 53 | } catch (IOException e) { 54 | String format = "Error reading %d bytes with offset %d from file[%d bytes] to buffer[%d bytes]"; 55 | throw new ProxyCacheException(String.format(format, length, offset, available(), buffer.length), e); 56 | } 57 | } 58 | 59 | @Override 60 | public synchronized void append(byte[] data, int length) throws ProxyCacheException { 61 | try { 62 | if (isCompleted()) { 63 | throw new ProxyCacheException("Error append cache: cache file " + file + " is completed!"); 64 | } 65 | dataFile.seek(available()); 66 | dataFile.write(data, 0, length); 67 | } catch (IOException e) { 68 | String format = "Error writing %d bytes to %s from buffer with size %d"; 69 | throw new ProxyCacheException(String.format(format, length, dataFile, data.length), e); 70 | } 71 | } 72 | 73 | @Override 74 | public synchronized void close() throws ProxyCacheException { 75 | try { 76 | dataFile.close(); 77 | } catch (IOException e) { 78 | throw new ProxyCacheException("Error closing file " + file, e); 79 | } 80 | } 81 | 82 | @Override 83 | public synchronized void complete() throws ProxyCacheException { 84 | if (isCompleted()) { 85 | return; 86 | } 87 | 88 | close(); 89 | String fileName = file.getName().substring(0, file.getName().length() - TEMP_POSTFIX.length()); 90 | File completedFile = new File(file.getParentFile(), fileName); 91 | boolean renamed = file.renameTo(completedFile); 92 | if (!renamed) { 93 | throw new ProxyCacheException("Error renaming file " + file + " to " + completedFile + " for completion!"); 94 | } 95 | file = completedFile; 96 | try { 97 | dataFile = new RandomAccessFile(file, "r"); 98 | } catch (IOException e) { 99 | throw new ProxyCacheException("Error opening " + file + " as disc cache", e); 100 | } 101 | } 102 | 103 | @Override 104 | public synchronized boolean isCompleted() { 105 | return !isTempFile(file); 106 | } 107 | 108 | /** 109 | * Returns file to be used fo caching. It may as original file passed in constructor as some temp file for not completed cache. 110 | * 111 | * @return file for caching. 112 | */ 113 | public File getFile() { 114 | return file; 115 | } 116 | 117 | private boolean isTempFile(File file) { 118 | return file.getName().endsWith(TEMP_POSTFIX); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /library/src/main/java/com/danikula/videocache/HttpProxyCache.java: -------------------------------------------------------------------------------- 1 | package com.danikula.videocache; 2 | 3 | import android.net.Uri; 4 | import android.text.TextUtils; 5 | import android.util.Log; 6 | 7 | import java.io.BufferedOutputStream; 8 | import java.io.BufferedReader; 9 | import java.io.IOException; 10 | import java.io.InputStream; 11 | import java.io.InputStreamReader; 12 | import java.io.OutputStream; 13 | import java.net.InetAddress; 14 | import java.net.ServerSocket; 15 | import java.net.Socket; 16 | import java.util.concurrent.CountDownLatch; 17 | import java.util.concurrent.ExecutorService; 18 | import java.util.concurrent.Executors; 19 | import java.util.regex.Matcher; 20 | import java.util.regex.Pattern; 21 | 22 | /** 23 | * {@link ProxyCache} that uses local server to handle requests and cache data. 24 | * Typical usage: 25 | *

 26 |  * HttpProxyCache proxy;
 27 |  * public onCreate(Bundle state) {
 28 |  *      super.onCreate(state);
 29 |  *      ...
 30 |  *      try{
 31 |  *          HttpUrlSource source = new HttpUrlSource(YOUR_VIDEO_URI);
 32 |  *          Cache cache = new FileCache(new File(context.getCacheDir(), "video.mp4"));
 33 |  *          proxy = new HttpProxyCache(source, cache);
 34 |  *          videoView.setVideoPath(proxy.getUrl());
 35 |  *      } catch(ProxyCacheException e) {
 36 |  *          Log.e(LOG_TAG, "Error playing video", e);
 37 |  *      }
 38 |  * }
 39 |  * public onDestroy(){
 40 |  *     super.onDestroy();
 41 |  *
 42 |  *     if (proxy != null) {
 43 |  *         proxy.shutdown();
 44 |  *     }
 45 |  * }
 46 |  * 
47 | * 48 | * @author Alexey Danilov (danikula@gmail.com). 49 | */ 50 | public class HttpProxyCache extends ProxyCache { 51 | 52 | private static final int CLIENT_COUNT = 3; 53 | private static final Pattern RANGE_HEADER_PATTERN = Pattern.compile("[R,r]ange:[ ]?bytes=(\\d*)-"); 54 | private static final String PROXY_HOST = "127.0.0.1"; 55 | 56 | private final HttpUrlSource httpUrlSource; 57 | private final Cache cache; 58 | private final ServerSocket serverSocket; 59 | private final int port; 60 | private final Thread waitConnectionThread; 61 | private final ExecutorService executorService; 62 | 63 | public HttpProxyCache(HttpUrlSource source, Cache cache, boolean logEnabled) throws ProxyCacheException { 64 | super(source, cache, logEnabled); 65 | 66 | this.httpUrlSource = source; 67 | this.cache = cache; 68 | this.executorService = Executors.newFixedThreadPool(CLIENT_COUNT); 69 | try { 70 | InetAddress inetAddress = InetAddress.getByName(PROXY_HOST); 71 | this.serverSocket = new ServerSocket(0, CLIENT_COUNT, inetAddress); 72 | this.port = serverSocket.getLocalPort(); 73 | CountDownLatch startSignal = new CountDownLatch(1); 74 | this.waitConnectionThread = new Thread(new WaitRequestsRunnable(startSignal)); 75 | this.waitConnectionThread.start(); 76 | startSignal.await(); // freeze thread, wait for server starts 77 | } catch (IOException | InterruptedException e) { 78 | executorService.shutdown(); 79 | throw new ProxyCacheException("Error starting local server", e); 80 | } 81 | } 82 | 83 | public HttpProxyCache(HttpUrlSource source, Cache cache) throws ProxyCacheException { 84 | this(source, cache, false); 85 | } 86 | 87 | public String getUrl() { 88 | return "http://" + PROXY_HOST + ":" + port + Uri.parse(httpUrlSource.url).getPath(); 89 | } 90 | 91 | @Override 92 | public void shutdown() { 93 | super.shutdown(); 94 | 95 | Log.i(ProxyCacheUtils.LOG_TAG, "Shutdown proxy"); 96 | waitConnectionThread.interrupt(); 97 | try { 98 | if (!serverSocket.isClosed()) { 99 | serverSocket.close(); 100 | } 101 | } catch (IOException e) { 102 | onError(new ProxyCacheException("Error shutting down local server", e)); 103 | } 104 | } 105 | 106 | private void waitForRequest() { 107 | try { 108 | while (!Thread.currentThread().isInterrupted()) { 109 | Socket socket = serverSocket.accept(); 110 | Log.d(ProxyCacheUtils.LOG_TAG, "Accept new socket " + socket); 111 | processSocketInBackground(socket); 112 | } 113 | } catch (IOException e) { 114 | onError(new ProxyCacheException("Error during waiting connection", e)); 115 | } 116 | } 117 | 118 | private void processSocketInBackground(final Socket socket) throws IOException { 119 | executorService.submit(new Runnable() { 120 | @Override 121 | public void run() { 122 | try { 123 | processSocket(socket); 124 | } catch (Throwable e) { 125 | onError(e); 126 | } 127 | } 128 | }); 129 | } 130 | 131 | private void processSocket(Socket socket) { 132 | try { 133 | InputStream inputStream = socket.getInputStream(); 134 | String request = readRequest(inputStream); 135 | Log.i(ProxyCacheUtils.LOG_TAG, "Request to cache proxy:\n" + request); 136 | long rangeOffset = getRangeOffset(request); 137 | writeResponse(socket, rangeOffset); 138 | } catch (ProxyCacheException | IOException e) { 139 | onError(new ProxyCacheException("Error processing request", e)); 140 | } finally { 141 | releaseSocket(socket); 142 | } 143 | } 144 | 145 | private void writeResponse(Socket socket, long rangeOffset) throws ProxyCacheException, IOException { 146 | OutputStream out = new BufferedOutputStream(socket.getOutputStream()); 147 | byte[] buffer = new byte[ProxyCacheUtils.DEFAULT_BUFFER_SIZE]; 148 | int readBytes; 149 | long offset = Math.max(rangeOffset, 0); 150 | boolean headersWrote = false; 151 | while ((readBytes = read(buffer, offset, buffer.length)) != -1) { 152 | // tiny optimization: to prevent HEAD request in source for content-length. content-length 'll available after reading source 153 | if (!headersWrote) { 154 | writeResponseHeaders(out, rangeOffset); 155 | headersWrote = true; 156 | } 157 | out.write(buffer, 0, readBytes); 158 | if (isLogEnabled()) { 159 | Log.d(ProxyCacheUtils.LOG_TAG, "Write data[" + readBytes + " bytes] to socket " + socket + " with offset " + offset + ": " + ProxyCacheUtils.preview(buffer, readBytes)); 160 | } 161 | offset += readBytes; 162 | } 163 | out.flush(); 164 | } 165 | 166 | private void writeResponseHeaders(OutputStream out, long rangeOffset) throws IOException, ProxyCacheException { 167 | String responseHeaders = newResponseHeaders(rangeOffset); 168 | out.write(responseHeaders.getBytes("UTF-8")); 169 | Log.i(ProxyCacheUtils.LOG_TAG, "Response headers:\n" + responseHeaders); 170 | } 171 | 172 | private String newResponseHeaders(long offset) throws IOException, ProxyCacheException { 173 | boolean partial = offset >= 0; 174 | String mime = httpUrlSource.getMime(); 175 | boolean mimeKnown = !TextUtils.isEmpty(mime); 176 | int length = cache.isCompleted() ? cache.available() : httpUrlSource.available(); 177 | boolean lengthKnown = length >= 0; 178 | long contentLength = partial ? length - offset : length; 179 | return new StringBuilder() 180 | .append(partial ? "HTTP/1.1 206 PARTIAL CONTENT\n" : "HTTP/1.1 200 OK\n") 181 | .append("Accept-Ranges: bytes\n") 182 | .append(lengthKnown ? String.format("Content-Length: %d\n", contentLength) : "") 183 | .append(lengthKnown && partial ? String.format("Content-Range: bytes %d-%d/%d\n", offset, length, length) : "") 184 | .append(mimeKnown ? String.format("Content-Type: %s\n", mime) : "") 185 | .append("\n") // headers end 186 | .toString(); 187 | } 188 | 189 | private String readRequest(InputStream inputStream) throws IOException { 190 | BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, "UTF-8")); 191 | StringBuilder str = new StringBuilder(); 192 | String line; 193 | while (!TextUtils.isEmpty(line = reader.readLine())) { // until new line (headers ending) 194 | str.append(line).append('\n'); 195 | } 196 | return str.toString(); 197 | } 198 | 199 | private long getRangeOffset(String request) { 200 | Matcher matcher = RANGE_HEADER_PATTERN.matcher(request); 201 | if (matcher.find()) { 202 | String rangeValue = matcher.group(1); 203 | return Long.parseLong(rangeValue); 204 | } 205 | return -1; 206 | } 207 | 208 | private void releaseSocket(Socket socket) { 209 | try { 210 | socket.shutdownInput(); 211 | } catch (IOException e) { 212 | onError(new ProxyCacheException("Error closing socket input stream", e)); 213 | } 214 | try { 215 | socket.shutdownOutput(); 216 | } catch (IOException e) { 217 | onError(new ProxyCacheException("Error closing socket output stream", e)); 218 | } 219 | try { 220 | socket.close(); 221 | } catch (IOException e) { 222 | onError(new ProxyCacheException("Error closing socket", e)); 223 | } 224 | } 225 | 226 | private final class WaitRequestsRunnable implements Runnable { 227 | 228 | private final CountDownLatch startSignal; 229 | 230 | public WaitRequestsRunnable(CountDownLatch startSignal) { 231 | this.startSignal = startSignal; 232 | } 233 | 234 | @Override 235 | public void run() { 236 | startSignal.countDown(); 237 | waitForRequest(); 238 | } 239 | } 240 | } 241 | -------------------------------------------------------------------------------- /library/src/main/java/com/danikula/videocache/HttpUrlSource.java: -------------------------------------------------------------------------------- 1 | package com.danikula.videocache; 2 | 3 | import android.text.TextUtils; 4 | import android.util.Log; 5 | 6 | import java.io.IOException; 7 | import java.io.InputStream; 8 | import java.net.HttpURLConnection; 9 | import java.net.URL; 10 | 11 | import static com.danikula.videocache.Preconditions.checkNotNull; 12 | import static java.net.HttpURLConnection.HTTP_OK; 13 | import static java.net.HttpURLConnection.HTTP_PARTIAL; 14 | 15 | /** 16 | * {@link Source} that uses http resource as source for {@link ProxyCache}. 17 | * 18 | * @author Alexey Danilov (danikula@gmail.com). 19 | */ 20 | public class HttpUrlSource implements Source { 21 | 22 | final String url; 23 | private HttpURLConnection connection; 24 | private InputStream inputStream; 25 | private volatile int available = Integer.MIN_VALUE; 26 | private volatile String mime; 27 | 28 | public HttpUrlSource(String url) { 29 | this(url, ProxyCacheUtils.getSupposablyMime(url)); 30 | } 31 | 32 | public HttpUrlSource(String url, String mime) { 33 | this.url = Preconditions.checkNotNull(url); 34 | this.mime = mime; 35 | } 36 | 37 | @Override 38 | public int available() throws ProxyCacheException { 39 | if (available == Integer.MIN_VALUE) { 40 | fetchContentInfo(); 41 | } 42 | return available; 43 | } 44 | 45 | @Override 46 | public void open(int offset) throws ProxyCacheException { 47 | try { 48 | Log.d(ProxyCacheUtils.LOG_TAG, "Open connection " + (offset > 0 ? " with offset " + offset : "") + " to " + url); 49 | connection = (HttpURLConnection) new URL(url).openConnection(); 50 | if (offset > 0) { 51 | connection.setRequestProperty("Range", "bytes=" + offset + "-"); 52 | } 53 | mime = connection.getContentType(); 54 | inputStream = connection.getInputStream(); 55 | readSourceAvailableBytes(connection, offset); 56 | } catch (IOException e) { 57 | throw new ProxyCacheException("Error opening connection for " + url + " with offset " + offset, e); 58 | } 59 | } 60 | 61 | private void readSourceAvailableBytes(HttpURLConnection connection, int offset) throws IOException { 62 | int contentLength = connection.getContentLength(); 63 | int responseCode = connection.getResponseCode(); 64 | available = responseCode == HTTP_OK ? contentLength : 65 | responseCode == HTTP_PARTIAL ? contentLength + offset : 66 | available; 67 | } 68 | 69 | @Override 70 | public void close() throws ProxyCacheException { 71 | if (connection != null) { 72 | connection.disconnect(); 73 | } 74 | } 75 | 76 | @Override 77 | public int read(byte[] buffer) throws ProxyCacheException { 78 | if (inputStream == null) { 79 | throw new ProxyCacheException("Error reading data from " + url + ": connection is absent!"); 80 | } 81 | try { 82 | return inputStream.read(buffer, 0, buffer.length); 83 | } catch (IOException e) { 84 | throw new ProxyCacheException("Error reading data from " + url, e); 85 | } 86 | } 87 | 88 | private void fetchContentInfo() throws ProxyCacheException { 89 | Log.d(ProxyCacheUtils.LOG_TAG, "Read content info from " + url); 90 | HttpURLConnection urlConnection = null; 91 | try { 92 | urlConnection = (HttpURLConnection) new URL(url).openConnection(); 93 | urlConnection.setRequestMethod("HEAD"); 94 | available = urlConnection.getContentLength(); 95 | mime = urlConnection.getContentType(); 96 | Log.d(ProxyCacheUtils.LOG_TAG, "Content-Length of " + url + " is " + available + " bytes, mime is " + mime); 97 | } catch (IOException e) { 98 | throw new ProxyCacheException("Error fetching Content-Length from " + url); 99 | } finally { 100 | if (urlConnection != null) { 101 | urlConnection.disconnect(); 102 | } 103 | } 104 | } 105 | 106 | public String getMime() throws ProxyCacheException { 107 | if (TextUtils.isEmpty(mime)) { 108 | fetchContentInfo(); 109 | } 110 | return mime; 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /library/src/main/java/com/danikula/videocache/Preconditions.java: -------------------------------------------------------------------------------- 1 | package com.danikula.videocache; 2 | 3 | final class Preconditions { 4 | 5 | static T checkNotNull(T reference) { 6 | if (reference == null) { 7 | throw new NullPointerException(); 8 | } 9 | return reference; 10 | } 11 | 12 | static T checkNotNull(T reference, String errorMessage) { 13 | if (reference == null) { 14 | throw new NullPointerException(errorMessage); 15 | } 16 | return reference; 17 | } 18 | 19 | static void checkArgument(boolean expression) { 20 | if (!expression) { 21 | throw new IllegalArgumentException(); 22 | } 23 | } 24 | 25 | static void checkArgument(boolean expression, String errorMessage) { 26 | if (!expression) { 27 | throw new IllegalArgumentException(errorMessage); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /library/src/main/java/com/danikula/videocache/ProxyCache.java: -------------------------------------------------------------------------------- 1 | package com.danikula.videocache; 2 | 3 | import android.os.Handler; 4 | import android.os.Looper; 5 | import android.util.Log; 6 | 7 | import java.util.concurrent.atomic.AtomicInteger; 8 | 9 | import static com.danikula.videocache.Preconditions.checkNotNull; 10 | import static com.danikula.videocache.ProxyCacheUtils.LOG_TAG; 11 | 12 | /** 13 | * Proxy for {@link Source} with caching support ({@link Cache}). 14 | *

15 | * Can be used only for sources with persistent data (that doesn't change with time). 16 | * Method {@link #read(byte[], long, int)} will be blocked while fetching data from source. 17 | * Useful for streaming something with caching e.g. streaming video/audio etc. 18 | * 19 | * @author Alexey Danilov (danikula@gmail.com). 20 | */ 21 | public class ProxyCache { 22 | 23 | private static final int MAX_READ_SOURCE_ATTEMPTS = 1; 24 | 25 | private final Source source; 26 | private final Cache cache; 27 | private final Object wc; 28 | private final Handler handler; 29 | private volatile Thread sourceReaderThread; 30 | private volatile boolean stopped; 31 | private final AtomicInteger readSourceErrorsCount; 32 | private CacheListener cacheListener; 33 | private final boolean logEnabled; 34 | 35 | public ProxyCache(Source source, Cache cache, boolean logEnabled) { 36 | this.source = checkNotNull(source); 37 | this.cache = checkNotNull(cache); 38 | this.logEnabled = logEnabled; 39 | this.wc = new Object(); 40 | this.handler = new Handler(Looper.getMainLooper()); 41 | this.readSourceErrorsCount = new AtomicInteger(); 42 | } 43 | 44 | public ProxyCache(Source source, Cache cache) { 45 | this(source, cache, false); 46 | } 47 | 48 | public void setCacheListener(CacheListener cacheListener) { 49 | this.cacheListener = cacheListener; 50 | } 51 | 52 | public int read(byte[] buffer, long offset, int length) throws ProxyCacheException { 53 | ProxyCacheUtils.assertBuffer(buffer, offset, length); 54 | 55 | while (!cache.isCompleted() && cache.available() < (offset + length) && !stopped) { 56 | readSourceAsync(); 57 | waitForSourceData(); 58 | checkIsCacheValid(); 59 | checkReadSourceErrorsCount(); 60 | } 61 | int read = cache.read(buffer, offset, length); 62 | if (isLogEnabled()) { 63 | Log.d(LOG_TAG, "Read data[" + read + " bytes] from cache with offset " + offset + ": " + ProxyCacheUtils.preview(buffer, read)); 64 | } 65 | return read; 66 | } 67 | 68 | private void checkIsCacheValid() throws ProxyCacheException { 69 | int sourceAvailable = source.available(); 70 | if (sourceAvailable > 0 && cache.available() > sourceAvailable) { 71 | throw new ProxyCacheException("Unexpected cache: cache [" + cache.available() + " bytes] > source[" + sourceAvailable + " bytes]"); 72 | } 73 | } 74 | 75 | private void checkReadSourceErrorsCount() throws ProxyCacheException { 76 | int errorsCount = readSourceErrorsCount.get(); 77 | if (errorsCount >= MAX_READ_SOURCE_ATTEMPTS) { 78 | readSourceErrorsCount.set(0); 79 | throw new ProxyCacheException("Error reading source " + errorsCount + " times"); 80 | } 81 | } 82 | 83 | public void shutdown() { 84 | try { 85 | stopped = true; 86 | if (sourceReaderThread != null) { 87 | sourceReaderThread.interrupt(); 88 | } 89 | cache.close(); 90 | } catch (ProxyCacheException e) { 91 | onError(e); 92 | } 93 | } 94 | 95 | private void readSourceAsync() throws ProxyCacheException { 96 | 97 | boolean readingInProgress = sourceReaderThread != null && sourceReaderThread.getState() != Thread.State.TERMINATED; 98 | if (!stopped && !cache.isCompleted() && !readingInProgress) { 99 | sourceReaderThread = new Thread(new SourceReaderRunnable(), "Source reader for ProxyCache"); 100 | sourceReaderThread.start(); 101 | } 102 | } 103 | 104 | private void waitForSourceData() throws ProxyCacheException { 105 | synchronized (wc) { 106 | try { 107 | wc.wait(1000); 108 | } catch (InterruptedException e) { 109 | throw new ProxyCacheException("Waiting source data is interrupted!", e); 110 | } 111 | } 112 | } 113 | 114 | private void notifyNewCacheDataAvailable(final int cachePercentage) { 115 | handler.post(new Runnable() { 116 | @Override 117 | public void run() { 118 | if (cacheListener != null) { 119 | cacheListener.onCacheDataAvailable(cachePercentage); 120 | } 121 | } 122 | }); 123 | 124 | synchronized (wc) { 125 | wc.notifyAll(); 126 | } 127 | } 128 | 129 | private void readSource() { 130 | int cachePercentage = 0; 131 | try { 132 | int offset = cache.available(); 133 | source.open(offset); 134 | byte[] buffer = new byte[ProxyCacheUtils.DEFAULT_BUFFER_SIZE]; 135 | int readBytes; 136 | while ((readBytes = source.read(buffer)) != -1 && !Thread.currentThread().isInterrupted() && !stopped) { 137 | if (isLogEnabled()) { 138 | Log.d(LOG_TAG, "Write data[" + readBytes + " bytes] to cache from source with offset " + offset + ": " + ProxyCacheUtils.preview(buffer, readBytes)); 139 | } 140 | cache.append(buffer, readBytes); 141 | offset += readBytes; 142 | cachePercentage = offset * 100 / source.available(); 143 | 144 | notifyNewCacheDataAvailable(cachePercentage); 145 | } 146 | if (cache.available() == source.available()) { 147 | cache.complete(); 148 | } 149 | } catch (Throwable e) { 150 | readSourceErrorsCount.incrementAndGet(); 151 | onError(e); 152 | } finally { 153 | closeSource(); 154 | notifyNewCacheDataAvailable(cachePercentage); 155 | } 156 | } 157 | 158 | private void closeSource() { 159 | try { 160 | source.close(); 161 | } catch (ProxyCacheException e) { 162 | onError(new ProxyCacheException("Error closing source " + source, e)); 163 | } 164 | } 165 | 166 | protected final void onError(final Throwable e) { 167 | Log.e(LOG_TAG, "ProxyCache error", e); 168 | handler.post(new ErrorDeliverer(e)); 169 | } 170 | 171 | protected boolean isLogEnabled() { 172 | return logEnabled; 173 | } 174 | 175 | private class SourceReaderRunnable implements Runnable { 176 | 177 | @Override 178 | public void run() { 179 | readSource(); 180 | } 181 | } 182 | 183 | private class ErrorDeliverer implements Runnable { 184 | 185 | private final Throwable error; 186 | 187 | public ErrorDeliverer(Throwable error) { 188 | this.error = error; 189 | } 190 | 191 | @Override 192 | public void run() { 193 | if (error instanceof ProxyCacheException) { 194 | if (cacheListener != null) { 195 | cacheListener.onError((ProxyCacheException) error); 196 | } 197 | } else { 198 | throw new RuntimeException("Unexpected error!", error); 199 | } 200 | } 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /library/src/main/java/com/danikula/videocache/ProxyCacheException.java: -------------------------------------------------------------------------------- 1 | package com.danikula.videocache; 2 | 3 | /** 4 | * Indicates any error in work of {@link ProxyCache}. 5 | * 6 | * @author Alexey Danilov 7 | */ 8 | public class ProxyCacheException extends Exception { 9 | 10 | public ProxyCacheException(String message) { 11 | super(message); 12 | } 13 | 14 | public ProxyCacheException(String message, Throwable cause) { 15 | super(message, cause); 16 | } 17 | 18 | public ProxyCacheException(Throwable cause) { 19 | super(cause); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /library/src/main/java/com/danikula/videocache/ProxyCacheUtils.java: -------------------------------------------------------------------------------- 1 | package com.danikula.videocache; 2 | 3 | import android.text.TextUtils; 4 | import android.webkit.MimeTypeMap; 5 | 6 | import java.io.File; 7 | import java.io.IOException; 8 | import java.util.Arrays; 9 | 10 | import static com.danikula.videocache.Preconditions.checkArgument; 11 | import static com.danikula.videocache.Preconditions.checkNotNull; 12 | 13 | /** 14 | * Just simple utils. 15 | * 16 | * @author Alexey Danilov (danikula@gmail.com). 17 | */ 18 | class ProxyCacheUtils { 19 | 20 | static final String LOG_TAG = "ProxyCache"; 21 | static final int DEFAULT_BUFFER_SIZE = 8 * 1024; 22 | static final int MAX_ARRAY_PREVIEW = 16; 23 | 24 | static String getSupposablyMime(String url) { 25 | MimeTypeMap mimes = MimeTypeMap.getSingleton(); 26 | String extension = MimeTypeMap.getFileExtensionFromUrl(url); 27 | return TextUtils.isEmpty(extension) ? null : mimes.getMimeTypeFromExtension(extension); 28 | } 29 | 30 | static void assertBuffer(byte[] buffer, long offset, int length) { 31 | checkNotNull(buffer, "Buffer must be not null!"); 32 | checkArgument(offset >= 0, "Data offset must be positive!"); 33 | checkArgument(length >= 0 && length <= buffer.length, "Length must be in range [0..buffer.length]"); 34 | } 35 | 36 | static String preview(byte[] data, int length) { 37 | int previewLength = Math.min(MAX_ARRAY_PREVIEW, Math.max(length, 0)); 38 | byte[] dataRange = Arrays.copyOfRange(data, 0, previewLength); 39 | String preview = Arrays.toString(dataRange); 40 | if (previewLength < length) { 41 | preview = preview.substring(0, preview.length() - 1) + ", ...]"; 42 | } 43 | return preview; 44 | } 45 | 46 | static void createDirectory(File directory) throws IOException { 47 | checkNotNull(directory, "File must be not null!"); 48 | if (directory.exists()) { 49 | checkArgument(directory.isDirectory(), "File is not directory!"); 50 | } else { 51 | boolean isCreated = directory.mkdirs(); 52 | if (!isCreated) { 53 | String error = String.format("Directory %s can't be created", directory.getAbsolutePath()); 54 | throw new IOException(error); 55 | } 56 | } 57 | } 58 | 59 | 60 | } 61 | -------------------------------------------------------------------------------- /library/src/main/java/com/danikula/videocache/Source.java: -------------------------------------------------------------------------------- 1 | package com.danikula.videocache; 2 | 3 | /** 4 | * Source for proxy. 5 | * 6 | * @author Alexey Danilov (danikula@gmail.com). 7 | */ 8 | public interface Source { 9 | 10 | int available() throws ProxyCacheException; 11 | 12 | void open(int offset) throws ProxyCacheException; 13 | 14 | void close() throws ProxyCacheException; 15 | 16 | int read(byte[] buffer) throws ProxyCacheException; 17 | } 18 | -------------------------------------------------------------------------------- /sample/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 22 5 | buildToolsVersion '22.0.1' 6 | 7 | defaultConfig { 8 | applicationId "com.danikula.videocache.sample" 9 | minSdkVersion 15 10 | targetSdkVersion 22 11 | versionCode 1 12 | versionName '1.0' 13 | } 14 | } 15 | 16 | repositories { 17 | maven { url 'https://dl.bintray.com/alexeydanilov/maven' } 18 | 19 | } 20 | 21 | dependencies { 22 | compile 'com.danikula:videocache:1.0.1' 23 | compile 'com.google.android.exoplayer:exoplayer:r1.3.3' 24 | } 25 | -------------------------------------------------------------------------------- /sample/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | 15 | 18 | 19 | 20 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /sample/src/main/java/com/com/exoplayer/player/DebugTrackRenderer.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.com.exoplayer.player; 17 | 18 | import android.widget.TextView; 19 | 20 | import com.google.android.exoplayer.ExoPlaybackException; 21 | import com.google.android.exoplayer.MediaCodecTrackRenderer; 22 | import com.google.android.exoplayer.TrackRenderer; 23 | import com.google.android.exoplayer.chunk.Format; 24 | 25 | /** 26 | * A {@link TrackRenderer} that periodically updates debugging information displayed by a 27 | * {@link TextView}. 28 | */ 29 | /* package */ class DebugTrackRenderer extends TrackRenderer implements Runnable { 30 | 31 | private final TextView textView; 32 | private final DemoPlayer player; 33 | private final MediaCodecTrackRenderer renderer; 34 | 35 | private volatile boolean pendingFailure; 36 | private volatile long currentPositionUs; 37 | 38 | public DebugTrackRenderer(TextView textView, DemoPlayer player, 39 | MediaCodecTrackRenderer renderer) { 40 | this.textView = textView; 41 | this.player = player; 42 | this.renderer = renderer; 43 | } 44 | 45 | public void injectFailure() { 46 | pendingFailure = true; 47 | } 48 | 49 | @Override 50 | protected boolean isEnded() { 51 | return true; 52 | } 53 | 54 | @Override 55 | protected boolean isReady() { 56 | return true; 57 | } 58 | 59 | @Override 60 | protected int doPrepare(long positionUs) throws ExoPlaybackException { 61 | maybeFail(); 62 | return STATE_PREPARED; 63 | } 64 | 65 | @Override 66 | protected void doSomeWork(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException { 67 | maybeFail(); 68 | if (positionUs < currentPositionUs || positionUs > currentPositionUs + 1000000) { 69 | currentPositionUs = positionUs; 70 | textView.post(this); 71 | } 72 | } 73 | 74 | @Override 75 | public void run() { 76 | textView.setText(getRenderString()); 77 | } 78 | 79 | private String getRenderString() { 80 | return getQualityString() + " " + renderer.codecCounters.getDebugString(); 81 | } 82 | 83 | private String getQualityString() { 84 | Format format = player.getVideoFormat(); 85 | return format == null ? "id:? br:? h:?" 86 | : "id:" + format.id + " br:" + format.bitrate + " h:" + format.height; 87 | } 88 | 89 | @Override 90 | protected long getCurrentPositionUs() { 91 | return currentPositionUs; 92 | } 93 | 94 | @Override 95 | protected long getDurationUs() { 96 | return TrackRenderer.MATCH_LONGEST_US; 97 | } 98 | 99 | @Override 100 | protected long getBufferedPositionUs() { 101 | return TrackRenderer.END_OF_TRACK_US; 102 | } 103 | 104 | @Override 105 | protected void seekTo(long timeUs) { 106 | currentPositionUs = timeUs; 107 | } 108 | 109 | private void maybeFail() throws ExoPlaybackException { 110 | if (pendingFailure) { 111 | pendingFailure = false; 112 | throw new ExoPlaybackException("fail() was called on DebugTrackRenderer"); 113 | } 114 | } 115 | 116 | } 117 | -------------------------------------------------------------------------------- /sample/src/main/java/com/com/exoplayer/player/DemoPlayer.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.com.exoplayer.player; 17 | 18 | import android.media.MediaCodec.CryptoException; 19 | import android.os.Handler; 20 | import android.os.Looper; 21 | import android.view.Surface; 22 | 23 | import com.google.android.exoplayer.DummyTrackRenderer; 24 | import com.google.android.exoplayer.ExoPlaybackException; 25 | import com.google.android.exoplayer.ExoPlayer; 26 | import com.google.android.exoplayer.MediaCodecAudioTrackRenderer; 27 | import com.google.android.exoplayer.MediaCodecTrackRenderer.DecoderInitializationException; 28 | import com.google.android.exoplayer.MediaCodecVideoTrackRenderer; 29 | import com.google.android.exoplayer.TrackRenderer; 30 | import com.google.android.exoplayer.audio.AudioTrack; 31 | import com.google.android.exoplayer.chunk.ChunkSampleSource; 32 | import com.google.android.exoplayer.chunk.Format; 33 | import com.google.android.exoplayer.chunk.MultiTrackChunkSource; 34 | import com.google.android.exoplayer.drm.StreamingDrmSessionManager; 35 | import com.google.android.exoplayer.hls.HlsSampleSource; 36 | import com.google.android.exoplayer.metadata.MetadataTrackRenderer; 37 | import com.google.android.exoplayer.text.TextRenderer; 38 | import com.google.android.exoplayer.upstream.DefaultBandwidthMeter; 39 | import com.google.android.exoplayer.util.PlayerControl; 40 | 41 | import java.io.IOException; 42 | import java.util.Map; 43 | import java.util.concurrent.CopyOnWriteArrayList; 44 | 45 | /** 46 | * A wrapper around {@link ExoPlayer} that provides a higher level interface. It can be prepared 47 | * with one of a number of {@link RendererBuilder} classes to suit different use cases (e.g. DASH, 48 | * SmoothStreaming and so on). 49 | */ 50 | public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventListener, 51 | HlsSampleSource.EventListener, DefaultBandwidthMeter.EventListener, 52 | MediaCodecVideoTrackRenderer.EventListener, MediaCodecAudioTrackRenderer.EventListener, 53 | StreamingDrmSessionManager.EventListener, TextRenderer { 54 | 55 | /** 56 | * Builds renderers for the player. 57 | */ 58 | public interface RendererBuilder { 59 | /** 60 | * Constructs the necessary components for playback. 61 | * 62 | * @param player The parent player. 63 | * @param callback The callback to invoke with the constructed components. 64 | */ 65 | void buildRenderers(DemoPlayer player, RendererBuilderCallback callback); 66 | } 67 | 68 | /** 69 | * A callback invoked by a {@link RendererBuilder}. 70 | */ 71 | public interface RendererBuilderCallback { 72 | /** 73 | * Invoked with the results from a {@link RendererBuilder}. 74 | * 75 | * @param trackNames The names of the available tracks, indexed by {@link DemoPlayer} TYPE_* 76 | * constants. May be null if the track names are unknown. An individual element may be null 77 | * if the track names are unknown for the corresponding type. 78 | * @param multiTrackSources Sources capable of switching between multiple available tracks, 79 | * indexed by {@link DemoPlayer} TYPE_* constants. May be null if there are no types with 80 | * multiple tracks. An individual element may be null if it does not have multiple tracks. 81 | * @param renderers Renderers indexed by {@link DemoPlayer} TYPE_* constants. An individual 82 | * element may be null if there do not exist tracks of the corresponding type. 83 | */ 84 | void onRenderers(String[][] trackNames, MultiTrackChunkSource[] multiTrackSources, 85 | TrackRenderer[] renderers); 86 | /** 87 | * Invoked if a {@link RendererBuilder} encounters an error. 88 | * 89 | * @param e Describes the error. 90 | */ 91 | void onRenderersError(Exception e); 92 | } 93 | 94 | /** 95 | * A listener for core events. 96 | */ 97 | public interface Listener { 98 | void onStateChanged(boolean playWhenReady, int playbackState); 99 | void onError(Exception e); 100 | void onVideoSizeChanged(int width, int height, float pixelWidthHeightRatio); 101 | } 102 | 103 | /** 104 | * A listener for internal errors. 105 | *

106 | * These errors are not visible to the user, and hence this listener is provided for 107 | * informational purposes only. Note however that an internal error may cause a fatal 108 | * error if the player fails to recover. If this happens, {@link Listener#onError(Exception)} 109 | * will be invoked. 110 | */ 111 | public interface InternalErrorListener { 112 | void onRendererInitializationError(Exception e); 113 | void onAudioTrackInitializationError(AudioTrack.InitializationException e); 114 | void onAudioTrackWriteError(AudioTrack.WriteException e); 115 | void onDecoderInitializationError(DecoderInitializationException e); 116 | void onCryptoError(CryptoException e); 117 | void onLoadError(int sourceId, IOException e); 118 | void onDrmSessionManagerError(Exception e); 119 | } 120 | 121 | /** 122 | * A listener for debugging information. 123 | */ 124 | public interface InfoListener { 125 | void onVideoFormatEnabled(Format format, int trigger, int mediaTimeMs); 126 | void onAudioFormatEnabled(Format format, int trigger, int mediaTimeMs); 127 | void onDroppedFrames(int count, long elapsed); 128 | void onBandwidthSample(int elapsedMs, long bytes, long bitrateEstimate); 129 | void onLoadStarted(int sourceId, long length, int type, int trigger, Format format, 130 | int mediaStartTimeMs, int mediaEndTimeMs); 131 | void onLoadCompleted(int sourceId, long bytesLoaded, int type, int trigger, Format format, 132 | int mediaStartTimeMs, int mediaEndTimeMs, long elapsedRealtimeMs, long loadDurationMs); 133 | void onDecoderInitialized(String decoderName, long elapsedRealtimeMs, 134 | long initializationDurationMs); 135 | } 136 | 137 | /** 138 | * A listener for receiving notifications of timed text. 139 | */ 140 | public interface TextListener { 141 | void onText(String text); 142 | } 143 | 144 | /** 145 | * A listener for receiving ID3 metadata parsed from the media stream. 146 | */ 147 | public interface Id3MetadataListener { 148 | void onId3Metadata(Map metadata); 149 | } 150 | 151 | // Constants pulled into this class for convenience. 152 | public static final int STATE_IDLE = ExoPlayer.STATE_IDLE; 153 | public static final int STATE_PREPARING = ExoPlayer.STATE_PREPARING; 154 | public static final int STATE_BUFFERING = ExoPlayer.STATE_BUFFERING; 155 | public static final int STATE_READY = ExoPlayer.STATE_READY; 156 | public static final int STATE_ENDED = ExoPlayer.STATE_ENDED; 157 | 158 | public static final int DISABLED_TRACK = -1; 159 | public static final int PRIMARY_TRACK = 0; 160 | 161 | public static final int RENDERER_COUNT = 5; 162 | public static final int TYPE_VIDEO = 0; 163 | public static final int TYPE_AUDIO = 1; 164 | public static final int TYPE_TEXT = 2; 165 | public static final int TYPE_TIMED_METADATA = 3; 166 | public static final int TYPE_DEBUG = 4; 167 | 168 | private static final int RENDERER_BUILDING_STATE_IDLE = 1; 169 | private static final int RENDERER_BUILDING_STATE_BUILDING = 2; 170 | private static final int RENDERER_BUILDING_STATE_BUILT = 3; 171 | 172 | private final RendererBuilder rendererBuilder; 173 | private final ExoPlayer player; 174 | private final PlayerControl playerControl; 175 | private final Handler mainHandler; 176 | private final CopyOnWriteArrayList listeners; 177 | 178 | private int rendererBuildingState; 179 | private int lastReportedPlaybackState; 180 | private boolean lastReportedPlayWhenReady; 181 | 182 | private Surface surface; 183 | private InternalRendererBuilderCallback builderCallback; 184 | private TrackRenderer videoRenderer; 185 | private Format videoFormat; 186 | private int videoTrackToRestore; 187 | 188 | private MultiTrackChunkSource[] multiTrackSources; 189 | private String[][] trackNames; 190 | private int[] selectedTracks; 191 | private boolean backgrounded; 192 | 193 | private TextListener textListener; 194 | private Id3MetadataListener id3MetadataListener; 195 | private InternalErrorListener internalErrorListener; 196 | private InfoListener infoListener; 197 | 198 | public DemoPlayer(RendererBuilder rendererBuilder) { 199 | this.rendererBuilder = rendererBuilder; 200 | player = ExoPlayer.Factory.newInstance(RENDERER_COUNT, 1000, 5000); 201 | player.addListener(this); 202 | playerControl = new PlayerControl(player); 203 | mainHandler = new Handler(); 204 | listeners = new CopyOnWriteArrayList(); 205 | lastReportedPlaybackState = STATE_IDLE; 206 | rendererBuildingState = RENDERER_BUILDING_STATE_IDLE; 207 | selectedTracks = new int[RENDERER_COUNT]; 208 | // Disable text initially. 209 | selectedTracks[TYPE_TEXT] = DISABLED_TRACK; 210 | } 211 | 212 | public PlayerControl getPlayerControl() { 213 | return playerControl; 214 | } 215 | 216 | public void addListener(Listener listener) { 217 | listeners.add(listener); 218 | } 219 | 220 | public void removeListener(Listener listener) { 221 | listeners.remove(listener); 222 | } 223 | 224 | public void setInternalErrorListener(InternalErrorListener listener) { 225 | internalErrorListener = listener; 226 | } 227 | 228 | public void setInfoListener(InfoListener listener) { 229 | infoListener = listener; 230 | } 231 | 232 | public void setTextListener(TextListener listener) { 233 | textListener = listener; 234 | } 235 | 236 | public void setMetadataListener(Id3MetadataListener listener) { 237 | id3MetadataListener = listener; 238 | } 239 | 240 | public void setSurface(Surface surface) { 241 | this.surface = surface; 242 | pushSurface(false); 243 | } 244 | 245 | public Surface getSurface() { 246 | return surface; 247 | } 248 | 249 | public void blockingClearSurface() { 250 | surface = null; 251 | pushSurface(true); 252 | } 253 | 254 | public String[] getTracks(int type) { 255 | return trackNames == null ? null : trackNames[type]; 256 | } 257 | 258 | public int getSelectedTrackIndex(int type) { 259 | return selectedTracks[type]; 260 | } 261 | 262 | public void selectTrack(int type, int index) { 263 | if (selectedTracks[type] == index) { 264 | return; 265 | } 266 | selectedTracks[type] = index; 267 | pushTrackSelection(type, true); 268 | if (type == TYPE_TEXT && index == DISABLED_TRACK && textListener != null) { 269 | textListener.onText(null); 270 | } 271 | } 272 | 273 | public Format getVideoFormat() { 274 | return videoFormat; 275 | } 276 | 277 | public void setBackgrounded(boolean backgrounded) { 278 | if (this.backgrounded == backgrounded) { 279 | return; 280 | } 281 | this.backgrounded = backgrounded; 282 | if (backgrounded) { 283 | videoTrackToRestore = getSelectedTrackIndex(TYPE_VIDEO); 284 | selectTrack(TYPE_VIDEO, DISABLED_TRACK); 285 | blockingClearSurface(); 286 | } else { 287 | selectTrack(TYPE_VIDEO, videoTrackToRestore); 288 | } 289 | } 290 | 291 | public void prepare() { 292 | if (rendererBuildingState == RENDERER_BUILDING_STATE_BUILT) { 293 | player.stop(); 294 | } 295 | if (builderCallback != null) { 296 | builderCallback.cancel(); 297 | } 298 | videoFormat = null; 299 | videoRenderer = null; 300 | multiTrackSources = null; 301 | rendererBuildingState = RENDERER_BUILDING_STATE_BUILDING; 302 | maybeReportPlayerState(); 303 | builderCallback = new InternalRendererBuilderCallback(); 304 | rendererBuilder.buildRenderers(this, builderCallback); 305 | } 306 | 307 | /* package */ void onRenderers(String[][] trackNames, 308 | MultiTrackChunkSource[] multiTrackSources, TrackRenderer[] renderers) { 309 | builderCallback = null; 310 | // Normalize the results. 311 | if (trackNames == null) { 312 | trackNames = new String[RENDERER_COUNT][]; 313 | } 314 | if (multiTrackSources == null) { 315 | multiTrackSources = new MultiTrackChunkSource[RENDERER_COUNT]; 316 | } 317 | for (int i = 0; i < RENDERER_COUNT; i++) { 318 | if (renderers[i] == null) { 319 | // Convert a null renderer to a dummy renderer. 320 | renderers[i] = new DummyTrackRenderer(); 321 | } else if (trackNames[i] == null) { 322 | // We have a renderer so we must have at least one track, but the names are unknown. 323 | // Initialize the correct number of null track names. 324 | int trackCount = multiTrackSources[i] == null ? 1 : multiTrackSources[i].getTrackCount(); 325 | trackNames[i] = new String[trackCount]; 326 | } 327 | } 328 | // Complete preparation. 329 | this.trackNames = trackNames; 330 | this.videoRenderer = renderers[TYPE_VIDEO]; 331 | this.multiTrackSources = multiTrackSources; 332 | pushSurface(false); 333 | pushTrackSelection(TYPE_VIDEO, true); 334 | pushTrackSelection(TYPE_AUDIO, true); 335 | pushTrackSelection(TYPE_TEXT, true); 336 | player.prepare(renderers); 337 | rendererBuildingState = RENDERER_BUILDING_STATE_BUILT; 338 | } 339 | 340 | /* package */ void onRenderersError(Exception e) { 341 | builderCallback = null; 342 | if (internalErrorListener != null) { 343 | internalErrorListener.onRendererInitializationError(e); 344 | } 345 | for (Listener listener : listeners) { 346 | listener.onError(e); 347 | } 348 | rendererBuildingState = RENDERER_BUILDING_STATE_IDLE; 349 | maybeReportPlayerState(); 350 | } 351 | 352 | public void setPlayWhenReady(boolean playWhenReady) { 353 | player.setPlayWhenReady(playWhenReady); 354 | } 355 | 356 | public void seekTo(long positionMs) { 357 | player.seekTo(positionMs); 358 | } 359 | 360 | public void release() { 361 | if (builderCallback != null) { 362 | builderCallback.cancel(); 363 | builderCallback = null; 364 | } 365 | rendererBuildingState = RENDERER_BUILDING_STATE_IDLE; 366 | surface = null; 367 | player.release(); 368 | } 369 | 370 | 371 | public int getPlaybackState() { 372 | if (rendererBuildingState == RENDERER_BUILDING_STATE_BUILDING) { 373 | return ExoPlayer.STATE_PREPARING; 374 | } 375 | int playerState = player.getPlaybackState(); 376 | if (rendererBuildingState == RENDERER_BUILDING_STATE_BUILT 377 | && rendererBuildingState == RENDERER_BUILDING_STATE_IDLE) { 378 | // This is an edge case where the renderers are built, but are still being passed to the 379 | // player's playback thread. 380 | return ExoPlayer.STATE_PREPARING; 381 | } 382 | return playerState; 383 | } 384 | 385 | public long getCurrentPosition() { 386 | return player.getCurrentPosition(); 387 | } 388 | 389 | public long getDuration() { 390 | return player.getDuration(); 391 | } 392 | 393 | public int getBufferedPercentage() { 394 | return player.getBufferedPercentage(); 395 | } 396 | 397 | public boolean getPlayWhenReady() { 398 | return player.getPlayWhenReady(); 399 | } 400 | 401 | /* package */ Looper getPlaybackLooper() { 402 | return player.getPlaybackLooper(); 403 | } 404 | 405 | /* package */ Handler getMainHandler() { 406 | return mainHandler; 407 | } 408 | 409 | @Override 410 | public void onPlayerStateChanged(boolean playWhenReady, int state) { 411 | maybeReportPlayerState(); 412 | } 413 | 414 | @Override 415 | public void onPlayerError(ExoPlaybackException exception) { 416 | rendererBuildingState = RENDERER_BUILDING_STATE_IDLE; 417 | for (Listener listener : listeners) { 418 | listener.onError(exception); 419 | } 420 | } 421 | 422 | @Override 423 | public void onVideoSizeChanged(int width, int height, float pixelWidthHeightRatio) { 424 | for (Listener listener : listeners) { 425 | listener.onVideoSizeChanged(width, height, pixelWidthHeightRatio); 426 | } 427 | } 428 | 429 | @Override 430 | public void onDroppedFrames(int count, long elapsed) { 431 | if (infoListener != null) { 432 | infoListener.onDroppedFrames(count, elapsed); 433 | } 434 | } 435 | 436 | @Override 437 | public void onBandwidthSample(int elapsedMs, long bytes, long bitrateEstimate) { 438 | if (infoListener != null) { 439 | infoListener.onBandwidthSample(elapsedMs, bytes, bitrateEstimate); 440 | } 441 | } 442 | 443 | @Override 444 | public void onDownstreamFormatChanged(int sourceId, Format format, int trigger, int mediaTimeMs) { 445 | if (infoListener == null) { 446 | return; 447 | } 448 | if (sourceId == TYPE_VIDEO) { 449 | videoFormat = format; 450 | infoListener.onVideoFormatEnabled(format, trigger, mediaTimeMs); 451 | } else if (sourceId == TYPE_AUDIO) { 452 | infoListener.onAudioFormatEnabled(format, trigger, mediaTimeMs); 453 | } 454 | } 455 | 456 | @Override 457 | public void onDrmSessionManagerError(Exception e) { 458 | if (internalErrorListener != null) { 459 | internalErrorListener.onDrmSessionManagerError(e); 460 | } 461 | } 462 | 463 | @Override 464 | public void onDecoderInitializationError(DecoderInitializationException e) { 465 | if (internalErrorListener != null) { 466 | internalErrorListener.onDecoderInitializationError(e); 467 | } 468 | } 469 | 470 | @Override 471 | public void onAudioTrackInitializationError(AudioTrack.InitializationException e) { 472 | if (internalErrorListener != null) { 473 | internalErrorListener.onAudioTrackInitializationError(e); 474 | } 475 | } 476 | 477 | @Override 478 | public void onAudioTrackWriteError(AudioTrack.WriteException e) { 479 | if (internalErrorListener != null) { 480 | internalErrorListener.onAudioTrackWriteError(e); 481 | } 482 | } 483 | 484 | @Override 485 | public void onCryptoError(CryptoException e) { 486 | if (internalErrorListener != null) { 487 | internalErrorListener.onCryptoError(e); 488 | } 489 | } 490 | 491 | @Override 492 | public void onDecoderInitialized( 493 | String decoderName, 494 | long elapsedRealtimeMs, 495 | long initializationDurationMs) { 496 | if (infoListener != null) { 497 | infoListener.onDecoderInitialized(decoderName, elapsedRealtimeMs, initializationDurationMs); 498 | } 499 | } 500 | 501 | @Override 502 | public void onLoadError(int sourceId, IOException e) { 503 | if (internalErrorListener != null) { 504 | internalErrorListener.onLoadError(sourceId, e); 505 | } 506 | } 507 | 508 | @Override 509 | public void onText(String text) { 510 | processText(text); 511 | } 512 | 513 | /* package */ MetadataTrackRenderer.MetadataRenderer> 514 | getId3MetadataRenderer() { 515 | return new MetadataTrackRenderer.MetadataRenderer>() { 516 | @Override 517 | public void onMetadata(Map metadata) { 518 | if (id3MetadataListener != null) { 519 | id3MetadataListener.onId3Metadata(metadata); 520 | } 521 | } 522 | }; 523 | } 524 | 525 | @Override 526 | public void onPlayWhenReadyCommitted() { 527 | // Do nothing. 528 | } 529 | 530 | @Override 531 | public void onDrawnToSurface(Surface surface) { 532 | // Do nothing. 533 | } 534 | 535 | @Override 536 | public void onLoadStarted(int sourceId, long length, int type, int trigger, Format format, 537 | int mediaStartTimeMs, int mediaEndTimeMs) { 538 | if (infoListener != null) { 539 | infoListener.onLoadStarted(sourceId, length, type, trigger, format, mediaStartTimeMs, 540 | mediaEndTimeMs); 541 | } 542 | } 543 | 544 | @Override 545 | public void onLoadCompleted(int sourceId, long bytesLoaded, int type, int trigger, Format format, 546 | int mediaStartTimeMs, int mediaEndTimeMs, long elapsedRealtimeMs, long loadDurationMs) { 547 | if (infoListener != null) { 548 | infoListener.onLoadCompleted(sourceId, bytesLoaded, type, trigger, format, mediaStartTimeMs, 549 | mediaEndTimeMs, elapsedRealtimeMs, loadDurationMs); 550 | } 551 | } 552 | 553 | @Override 554 | public void onLoadCanceled(int sourceId, long bytesLoaded) { 555 | // Do nothing. 556 | } 557 | 558 | @Override 559 | public void onUpstreamDiscarded(int sourceId, int mediaStartTimeMs, int mediaEndTimeMs) { 560 | // Do nothing. 561 | } 562 | 563 | private void maybeReportPlayerState() { 564 | boolean playWhenReady = player.getPlayWhenReady(); 565 | int playbackState = getPlaybackState(); 566 | if (lastReportedPlayWhenReady != playWhenReady || lastReportedPlaybackState != playbackState) { 567 | for (Listener listener : listeners) { 568 | listener.onStateChanged(playWhenReady, playbackState); 569 | } 570 | lastReportedPlayWhenReady = playWhenReady; 571 | lastReportedPlaybackState = playbackState; 572 | } 573 | } 574 | 575 | private void pushSurface(boolean blockForSurfacePush) { 576 | if (videoRenderer == null) { 577 | return; 578 | } 579 | 580 | if (blockForSurfacePush) { 581 | player.blockingSendMessage( 582 | videoRenderer, MediaCodecVideoTrackRenderer.MSG_SET_SURFACE, surface); 583 | } else { 584 | player.sendMessage( 585 | videoRenderer, MediaCodecVideoTrackRenderer.MSG_SET_SURFACE, surface); 586 | } 587 | } 588 | 589 | private void pushTrackSelection(int type, boolean allowRendererEnable) { 590 | if (multiTrackSources == null) { 591 | return; 592 | } 593 | 594 | int trackIndex = selectedTracks[type]; 595 | if (trackIndex == DISABLED_TRACK) { 596 | player.setRendererEnabled(type, false); 597 | } else if (multiTrackSources[type] == null) { 598 | player.setRendererEnabled(type, allowRendererEnable); 599 | } else { 600 | boolean playWhenReady = player.getPlayWhenReady(); 601 | player.setPlayWhenReady(false); 602 | player.setRendererEnabled(type, false); 603 | player.sendMessage(multiTrackSources[type], MultiTrackChunkSource.MSG_SELECT_TRACK, 604 | trackIndex); 605 | player.setRendererEnabled(type, allowRendererEnable); 606 | player.setPlayWhenReady(playWhenReady); 607 | } 608 | } 609 | 610 | /* package */ void processText(String text) { 611 | if (textListener == null || selectedTracks[TYPE_TEXT] == DISABLED_TRACK) { 612 | return; 613 | } 614 | textListener.onText(text); 615 | } 616 | 617 | private class InternalRendererBuilderCallback implements RendererBuilderCallback { 618 | 619 | private boolean canceled; 620 | 621 | public void cancel() { 622 | canceled = true; 623 | } 624 | 625 | @Override 626 | public void onRenderers(String[][] trackNames, MultiTrackChunkSource[] multiTrackSources, 627 | TrackRenderer[] renderers) { 628 | if (!canceled) { 629 | DemoPlayer.this.onRenderers(trackNames, multiTrackSources, renderers); 630 | } 631 | } 632 | 633 | @Override 634 | public void onRenderersError(Exception e) { 635 | if (!canceled) { 636 | DemoPlayer.this.onRenderersError(e); 637 | } 638 | } 639 | 640 | } 641 | 642 | } 643 | -------------------------------------------------------------------------------- /sample/src/main/java/com/com/exoplayer/player/HlsRendererBuilder.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.com.exoplayer.player; 17 | 18 | import android.content.Context; 19 | import android.media.MediaCodec; 20 | import android.os.Handler; 21 | import android.widget.TextView; 22 | 23 | import com.com.exoplayer.player.DemoPlayer.RendererBuilderCallback; 24 | import com.google.android.exoplayer.MediaCodecAudioTrackRenderer; 25 | import com.google.android.exoplayer.MediaCodecUtil.DecoderQueryException; 26 | import com.google.android.exoplayer.MediaCodecVideoTrackRenderer; 27 | import com.google.android.exoplayer.TrackRenderer; 28 | import com.google.android.exoplayer.audio.AudioCapabilities; 29 | import com.google.android.exoplayer.chunk.VideoFormatSelectorUtil; 30 | import com.google.android.exoplayer.hls.HlsChunkSource; 31 | import com.google.android.exoplayer.hls.HlsMasterPlaylist; 32 | import com.google.android.exoplayer.hls.HlsPlaylist; 33 | import com.google.android.exoplayer.hls.HlsPlaylistParser; 34 | import com.google.android.exoplayer.hls.HlsSampleSource; 35 | import com.google.android.exoplayer.metadata.Id3Parser; 36 | import com.google.android.exoplayer.metadata.MetadataTrackRenderer; 37 | import com.google.android.exoplayer.text.eia608.Eia608TrackRenderer; 38 | import com.google.android.exoplayer.upstream.DataSource; 39 | import com.google.android.exoplayer.upstream.DefaultBandwidthMeter; 40 | import com.google.android.exoplayer.upstream.DefaultUriDataSource; 41 | import com.google.android.exoplayer.util.ManifestFetcher; 42 | import com.google.android.exoplayer.util.ManifestFetcher.ManifestCallback; 43 | 44 | import java.io.IOException; 45 | import java.util.Map; 46 | 47 | 48 | public class HlsRendererBuilder implements DemoPlayer.RendererBuilder, ManifestCallback { 49 | 50 | private static final int REQUESTED_BUFFER_SIZE = 18 * 1024 * 1024; 51 | private static final long REQUESTED_BUFFER_DURATION_MS = 40000; 52 | 53 | private final Context context; 54 | private final String userAgent; 55 | private final String url; 56 | private final TextView debugTextView; 57 | private final AudioCapabilities audioCapabilities; 58 | 59 | private DemoPlayer player; 60 | private RendererBuilderCallback callback; 61 | 62 | public HlsRendererBuilder(Context context, String userAgent, String url, TextView debugTextView, 63 | AudioCapabilities audioCapabilities) { 64 | this.context = context; 65 | this.userAgent = userAgent; 66 | this.url = url; 67 | this.debugTextView = debugTextView; 68 | this.audioCapabilities = audioCapabilities; 69 | } 70 | 71 | @Override 72 | public void buildRenderers(DemoPlayer player, RendererBuilderCallback callback) { 73 | this.player = player; 74 | this.callback = callback; 75 | HlsPlaylistParser parser = new HlsPlaylistParser(); 76 | ManifestFetcher playlistFetcher = new ManifestFetcher(url, 77 | new DefaultUriDataSource(context, userAgent), parser); 78 | playlistFetcher.singleLoad(player.getMainHandler().getLooper(), this); 79 | } 80 | 81 | @Override 82 | public void onSingleManifestError(IOException e) { 83 | callback.onRenderersError(e); 84 | } 85 | 86 | @Override 87 | public void onSingleManifest(HlsPlaylist manifest) { 88 | Handler mainHandler = player.getMainHandler(); 89 | DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter(); 90 | 91 | int[] variantIndices = null; 92 | if (manifest instanceof HlsMasterPlaylist) { 93 | HlsMasterPlaylist masterPlaylist = (HlsMasterPlaylist) manifest; 94 | try { 95 | variantIndices = VideoFormatSelectorUtil.selectVideoFormatsForDefaultDisplay( 96 | context, masterPlaylist.variants, null, false); 97 | } catch (DecoderQueryException e) { 98 | callback.onRenderersError(e); 99 | return; 100 | } 101 | } 102 | 103 | DataSource dataSource = new DefaultUriDataSource(context, bandwidthMeter, userAgent); 104 | HlsChunkSource chunkSource = new HlsChunkSource(dataSource, url, manifest, bandwidthMeter, 105 | variantIndices, HlsChunkSource.ADAPTIVE_MODE_SPLICE, audioCapabilities); 106 | HlsSampleSource sampleSource = new HlsSampleSource(chunkSource, true, 3, REQUESTED_BUFFER_SIZE, 107 | REQUESTED_BUFFER_DURATION_MS, mainHandler, player, DemoPlayer.TYPE_VIDEO); 108 | MediaCodecVideoTrackRenderer videoRenderer = new MediaCodecVideoTrackRenderer(sampleSource, 109 | MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT, 5000, mainHandler, player, 50); 110 | MediaCodecAudioTrackRenderer audioRenderer = new MediaCodecAudioTrackRenderer(sampleSource); 111 | 112 | MetadataTrackRenderer> id3Renderer = 113 | new MetadataTrackRenderer>(sampleSource, new Id3Parser(), 114 | player.getId3MetadataRenderer(), mainHandler.getLooper()); 115 | 116 | Eia608TrackRenderer closedCaptionRenderer = new Eia608TrackRenderer(sampleSource, player, 117 | mainHandler.getLooper()); 118 | 119 | // Build the debug renderer. 120 | TrackRenderer debugRenderer = debugTextView != null 121 | ? new DebugTrackRenderer(debugTextView, player, videoRenderer) : null; 122 | 123 | TrackRenderer[] renderers = new TrackRenderer[DemoPlayer.RENDERER_COUNT]; 124 | renderers[DemoPlayer.TYPE_VIDEO] = videoRenderer; 125 | renderers[DemoPlayer.TYPE_AUDIO] = audioRenderer; 126 | renderers[DemoPlayer.TYPE_TIMED_METADATA] = id3Renderer; 127 | renderers[DemoPlayer.TYPE_TEXT] = closedCaptionRenderer; 128 | renderers[DemoPlayer.TYPE_DEBUG] = debugRenderer; 129 | callback.onRenderers(null, null, renderers); 130 | } 131 | 132 | 133 | } 134 | -------------------------------------------------------------------------------- /sample/src/main/java/com/com/exoplayer/player/PlayerActivity.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.com.exoplayer.player; 17 | 18 | import android.app.Activity; 19 | import android.os.Bundle; 20 | import android.view.KeyEvent; 21 | import android.view.MotionEvent; 22 | import android.view.SurfaceHolder; 23 | import android.view.View; 24 | import android.view.View.OnClickListener; 25 | import android.view.View.OnKeyListener; 26 | import android.view.View.OnTouchListener; 27 | import android.widget.MediaController; 28 | 29 | import com.danikula.videocache.sample.R; 30 | import com.google.android.exoplayer.ExoPlayer; 31 | import com.google.android.exoplayer.VideoSurfaceView; 32 | import com.google.android.exoplayer.audio.AudioCapabilities; 33 | import com.google.android.exoplayer.audio.AudioCapabilitiesReceiver; 34 | import com.google.android.exoplayer.util.Util; 35 | 36 | /** 37 | * An activity that plays media using {@link DemoPlayer}. 38 | */ 39 | public class PlayerActivity extends Activity implements SurfaceHolder.Callback, OnClickListener, 40 | DemoPlayer.Listener, 41 | AudioCapabilitiesReceiver.Listener { 42 | 43 | private MediaController mediaController; 44 | private VideoSurfaceView surfaceView; 45 | 46 | private DemoPlayer player; 47 | private boolean playerNeedsPrepare; 48 | private long playerPosition; 49 | 50 | private String VIDEO_URL; 51 | 52 | private AudioCapabilitiesReceiver audioCapabilitiesReceiver; 53 | private AudioCapabilities audioCapabilities; 54 | 55 | // Activity lifecycle 56 | 57 | @Override 58 | public void onCreate(Bundle savedInstanceState) { 59 | super.onCreate(savedInstanceState); 60 | 61 | VIDEO_URL = "http://stream-1.vdomax.com:1935/vod/__definst__/mp4:youlove/youlove_xxx_7043.mp4/playlist.m3u8"; 62 | 63 | setContentView(R.layout.player_activity); 64 | View root = findViewById(R.id.root); 65 | root.setOnTouchListener(new OnTouchListener() { 66 | @Override 67 | public boolean onTouch(View view, MotionEvent motionEvent) { 68 | if (motionEvent.getAction() == MotionEvent.ACTION_DOWN) { 69 | toggleControlsVisibility(); 70 | } else if (motionEvent.getAction() == MotionEvent.ACTION_UP) { 71 | view.performClick(); 72 | } 73 | return true; 74 | } 75 | }); 76 | root.setOnKeyListener(new OnKeyListener() { 77 | @Override 78 | public boolean onKey(View v, int keyCode, KeyEvent event) { 79 | if (keyCode == KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE) { 80 | return mediaController.dispatchKeyEvent(event); 81 | } 82 | return false; 83 | } 84 | }); 85 | audioCapabilitiesReceiver = new AudioCapabilitiesReceiver(getApplicationContext(), this); 86 | 87 | 88 | surfaceView = (VideoSurfaceView) findViewById(R.id.surface_view); 89 | surfaceView.getHolder().addCallback(this); 90 | 91 | mediaController = new MediaController(this); 92 | mediaController.setAnchorView(root); 93 | 94 | preparePlayer(); 95 | 96 | //DemoUtil.setDefaultCookieManager(); 97 | } 98 | 99 | @Override 100 | public void onResume() { 101 | super.onResume(); 102 | //configureSubtitleView(); 103 | 104 | // The player will be prepared on receiving audio capabilities. 105 | audioCapabilitiesReceiver.register(); 106 | } 107 | 108 | @Override 109 | public void onPause() { 110 | super.onPause(); 111 | if (!true) { 112 | releasePlayer(); 113 | } else { 114 | player.setBackgrounded(true); 115 | } 116 | audioCapabilitiesReceiver.unregister(); 117 | 118 | } 119 | 120 | @Override 121 | public void onDestroy() { 122 | super.onDestroy(); 123 | releasePlayer(); 124 | } 125 | 126 | // OnClickListener methods 127 | 128 | @Override 129 | public void onClick(View view) { 130 | //if (view == retryButton) { 131 | //preparePlayer(); 132 | //} 133 | } 134 | 135 | // AudioCapabilitiesReceiver.Listener methods 136 | 137 | @Override 138 | public void onAudioCapabilitiesChanged(AudioCapabilities audioCapabilities) { 139 | boolean audioCapabilitiesChanged = !audioCapabilities.equals(this.audioCapabilities); 140 | if (player == null || audioCapabilitiesChanged) { 141 | this.audioCapabilities = audioCapabilities; 142 | releasePlayer(); 143 | preparePlayer(); 144 | } else if (player != null) { 145 | player.setBackgrounded(false); 146 | } 147 | } 148 | 149 | // Internal methods 150 | 151 | 152 | 153 | private void preparePlayer() { 154 | 155 | if (player == null) { 156 | String userAgent = Util.getUserAgent(this, "ExoPlayerDemo"); 157 | player = new DemoPlayer(new HlsRendererBuilder(this, userAgent, VIDEO_URL.toString(), null, 158 | audioCapabilities)); 159 | player.addListener(this); 160 | player.seekTo(playerPosition); 161 | playerNeedsPrepare = true; 162 | mediaController.setMediaPlayer(player.getPlayerControl()); 163 | mediaController.setEnabled(true); 164 | 165 | } 166 | if (playerNeedsPrepare) { 167 | player.prepare(); 168 | playerNeedsPrepare = false; 169 | updateButtonVisibilities(); 170 | } 171 | player.setSurface(surfaceView.getHolder().getSurface()); 172 | player.setPlayWhenReady(true); 173 | } 174 | 175 | private void releasePlayer() { 176 | if (player != null) { 177 | playerPosition = player.getCurrentPosition(); 178 | player.release(); 179 | player = null; 180 | //eventLogger.endSession(); 181 | //eventLogger = null; 182 | } 183 | } 184 | 185 | // DemoPlayer.Listener implementation 186 | 187 | @Override 188 | public void onStateChanged(boolean playWhenReady, int playbackState) { 189 | if (playbackState == ExoPlayer.STATE_ENDED) { 190 | showControls(); 191 | } 192 | String text = "playWhenReady=" + playWhenReady + ", playbackState="; 193 | switch(playbackState) { 194 | case ExoPlayer.STATE_BUFFERING: 195 | text += "buffering"; 196 | break; 197 | case ExoPlayer.STATE_ENDED: 198 | text += "ended"; 199 | break; 200 | case ExoPlayer.STATE_IDLE: 201 | text += "idle"; 202 | break; 203 | case ExoPlayer.STATE_PREPARING: 204 | text += "preparing"; 205 | break; 206 | case ExoPlayer.STATE_READY: 207 | text += "ready"; 208 | break; 209 | default: 210 | text += "unknown"; 211 | break; 212 | } 213 | //playerStateTextView.setText(text); 214 | updateButtonVisibilities(); 215 | } 216 | 217 | @Override 218 | public void onError(Exception e) { 219 | 220 | playerNeedsPrepare = true; 221 | updateButtonVisibilities(); 222 | showControls(); 223 | } 224 | 225 | @Override 226 | public void onVideoSizeChanged(int width, int height, float pixelWidthAspectRatio) { 227 | //shutterView.setVisibility(View.GONE); 228 | surfaceView.setVideoWidthHeightRatio( 229 | height == 0 ? 1 : (width * pixelWidthAspectRatio) / height); 230 | } 231 | 232 | // User controls 233 | 234 | private void updateButtonVisibilities() { 235 | //retryButton.setVisibility(playerNeedsPrepare ? View.VISIBLE : View.GONE); 236 | //videoButton.setVisibility(haveTracks(DemoPlayer.TYPE_VIDEO) ? View.VISIBLE : View.GONE); 237 | //audioButton.setVisibility(haveTracks(DemoPlayer.TYPE_AUDIO) ? View.VISIBLE : View.GONE); 238 | //textButton.setVisibility(haveTracks(DemoPlayer.TYPE_TEXT) ? View.VISIBLE : View.GONE); 239 | } 240 | 241 | 242 | private void toggleControlsVisibility() { 243 | if (mediaController.isShowing()) { 244 | mediaController.hide(); 245 | //debugRootView.setVisibility(View.GONE); 246 | } else { 247 | showControls(); 248 | } 249 | } 250 | 251 | private void showControls() { 252 | mediaController.show(0); 253 | //debugRootView.setVisibility(View.VISIBLE); 254 | } 255 | 256 | // DemoPlayer.TextListener implementation 257 | 258 | 259 | // SurfaceHolder.Callback implementation 260 | 261 | @Override 262 | public void surfaceCreated(SurfaceHolder holder) { 263 | if (player != null) { 264 | player.setSurface(holder.getSurface()); 265 | } 266 | } 267 | 268 | @Override 269 | public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { 270 | // Do nothing. 271 | } 272 | 273 | @Override 274 | public void surfaceDestroyed(SurfaceHolder holder) { 275 | if (player != null) { 276 | player.blockingClearSurface(); 277 | } 278 | } 279 | 280 | 281 | 282 | } 283 | -------------------------------------------------------------------------------- /sample/src/main/java/com/danikula/videocache/sample/VideoCacheActivity.java: -------------------------------------------------------------------------------- 1 | package com.danikula.videocache.sample; 2 | 3 | import android.annotation.SuppressLint; 4 | import android.app.Activity; 5 | import android.content.Context; 6 | import android.content.res.Configuration; 7 | import android.media.AudioManager; 8 | import android.media.MediaPlayer; 9 | import android.net.Uri; 10 | import android.os.Bundle; 11 | import android.os.Handler; 12 | import android.os.Message; 13 | import android.util.Log; 14 | import android.view.MotionEvent; 15 | import android.view.View; 16 | import android.view.animation.Animation; 17 | import android.view.animation.AnimationUtils; 18 | import android.widget.ImageView; 19 | import android.widget.ProgressBar; 20 | import android.widget.SeekBar; 21 | import android.widget.TextView; 22 | 23 | import com.danikula.videocache.Cache; 24 | import com.danikula.videocache.CacheListener; 25 | import com.danikula.videocache.FileCache; 26 | import com.danikula.videocache.HttpProxyCache; 27 | import com.danikula.videocache.HttpUrlSource; 28 | import com.danikula.videocache.ProxyCacheException; 29 | import com.jit.video.DensityUtil; 30 | import com.jit.video.FullScreenVideoView; 31 | import com.jit.video.LightnessController; 32 | import com.jit.video.VolumnController; 33 | 34 | import java.io.File; 35 | import java.text.DateFormat; 36 | import java.text.SimpleDateFormat; 37 | import java.util.Date; 38 | import java.util.Timer; 39 | import java.util.TimerTask; 40 | 41 | public class VideoCacheActivity extends Activity implements CacheListener,View.OnClickListener { 42 | 43 | private static final String LOG_TAG = "VideoActivity"; 44 | private static final String VIDEO_CACHE_NAME = "X4gPz_110559_60765d771b815d6faadf2f978fb8fcfe_ori.mp4"; 45 | private static final String VIDEO_URL = "http://stream-1.vdomax.com:1935/vod/__definst__/mp4:youlove/youlove_xxx_7043.mp4/playlist.m3u8"; 46 | 47 | //private static final String VIDEO_URL = "http://stream-1.vdomax.com:1935/vod/__definst__/mp4:110559/110559_720p.mp4/playlist.m3u8"; 48 | 49 | 50 | //private static final String VIDEO_URL ="https://www.vdomax.com/clips/2015/05/X4gPz_110559_60765d771b815d6faadf2f978fb8fcfe_ori.mp4"; 51 | 52 | //private VideoView videoView; 53 | 54 | //private String videoUrl = "https://www.vdomax.com/clips/2015/05/X4gPz_110559_60765d771b815d6faadf2f978fb8fcfe_ori.mp4"; 55 | 56 | 57 | 58 | private ProgressBar progressBar; 59 | private HttpProxyCache proxyCache; 60 | 61 | private FullScreenVideoView mVideo; 62 | private View mTopView; 63 | private View mBottomView; 64 | private SeekBar mSeekBar; 65 | private ImageView mPlay; 66 | private TextView mPlayTime; 67 | private TextView mDurationTime; 68 | 69 | private AudioManager mAudioManager; 70 | 71 | private float width; 72 | private float height; 73 | 74 | private int playTime; 75 | 76 | 77 | private static final int HIDE_TIME = 5000; 78 | 79 | private VolumnController volumnController; 80 | 81 | private int orginalLight; 82 | 83 | @Override 84 | protected void onCreate(Bundle savedInstanceState) { 85 | super.onCreate(savedInstanceState); 86 | 87 | setUpUi(); 88 | playVideo(); 89 | } 90 | 91 | private void setUpUi() { 92 | setContentView(R.layout.activity_video); 93 | //videoView = (VideoView) findViewById(R.id.videoView); 94 | progressBar = (ProgressBar) findViewById(R.id.progressBar); 95 | progressBar.setMax(100); 96 | 97 | volumnController = new VolumnController(this); 98 | mVideo = (FullScreenVideoView) findViewById(R.id.videoview); 99 | mPlayTime = (TextView) findViewById(R.id.play_time); 100 | mDurationTime = (TextView) findViewById(R.id.total_time); 101 | mPlay = (ImageView) findViewById(R.id.play_btn); 102 | mSeekBar = (SeekBar) findViewById(R.id.seekbar); 103 | mTopView = findViewById(R.id.top_layout); 104 | mBottomView = findViewById(R.id.bottom_layout); 105 | 106 | mAudioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE); 107 | 108 | width = DensityUtil.getWidthInPx(this); 109 | height = DensityUtil.getHeightInPx(this); 110 | threshold = DensityUtil.dip2px(this, 18); 111 | 112 | orginalLight = LightnessController.getLightness(this); 113 | 114 | mPlay.setOnClickListener(this); 115 | mSeekBar.setOnSeekBarChangeListener(mSeekBarChangeListener); 116 | 117 | //playVideo(); 118 | 119 | } 120 | 121 | private void playVideo() { 122 | 123 | runOnUiThread(new Runnable() { 124 | @Override 125 | public void run() { 126 | mVideo.setVideoURI(Uri.parse(VIDEO_URL)); 127 | } 128 | }); 129 | 130 | //mVideo.setVideoPath(VIDEO_URL); 131 | mVideo.requestFocus(); 132 | mVideo.setOnPreparedListener(new MediaPlayer.OnPreparedListener() { 133 | @Override 134 | public void onPrepared(MediaPlayer mp) { 135 | mVideo.setVideoWidth(mp.getVideoWidth()); 136 | mVideo.setVideoHeight(mp.getVideoHeight()); 137 | 138 | mVideo.start(); 139 | if (playTime != 0) { 140 | mVideo.seekTo(playTime); 141 | } 142 | 143 | mHandler.removeCallbacks(hideRunnable); 144 | mHandler.postDelayed(hideRunnable, HIDE_TIME); 145 | mDurationTime.setText(formatTime(mVideo.getDuration())); 146 | Timer timer = new Timer(); 147 | timer.schedule(new TimerTask() { 148 | 149 | @Override 150 | public void run() { 151 | mHandler.sendEmptyMessage(1); 152 | } 153 | }, 0, 1000); 154 | } 155 | }); 156 | mVideo.setOnCompletionListener(new MediaPlayer.OnCompletionListener() { 157 | @Override 158 | public void onCompletion(MediaPlayer mp) { 159 | mPlay.setImageResource(R.drawable.video_btn_down); 160 | mPlayTime.setText("00:00"); 161 | mSeekBar.setProgress(0); 162 | } 163 | }); 164 | mVideo.setOnTouchListener(mTouchListener); 165 | } 166 | 167 | 168 | 169 | private void playWithCache() { 170 | try { 171 | Cache cache = new FileCache(new File(getExternalCacheDir(), VIDEO_CACHE_NAME)); 172 | HttpUrlSource source = new HttpUrlSource(VIDEO_URL); 173 | proxyCache = new HttpProxyCache(source, cache); 174 | proxyCache.setCacheListener(this); 175 | mVideo.setVideoPath(proxyCache.getUrl()); 176 | mVideo.start(); 177 | } catch (ProxyCacheException e) { 178 | // do nothing. onError() handles all errors 179 | } 180 | } 181 | 182 | @Override 183 | public void onConfigurationChanged(Configuration newConfig) { 184 | if (this.getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) { 185 | height = DensityUtil.getWidthInPx(this); 186 | width = DensityUtil.getHeightInPx(this); 187 | } else if (this.getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT) { 188 | width = DensityUtil.getWidthInPx(this); 189 | height = DensityUtil.getHeightInPx(this); 190 | } 191 | super.onConfigurationChanged(newConfig); 192 | } 193 | 194 | @Override 195 | public void onDestroy() { 196 | super.onDestroy(); 197 | 198 | mHandler.removeMessages(0); 199 | mHandler.removeCallbacksAndMessages(null); 200 | 201 | if (proxyCache != null) { 202 | proxyCache.shutdown(); 203 | } 204 | } 205 | 206 | @Override 207 | public void onError(ProxyCacheException e) { 208 | Log.e(LOG_TAG, "Error playing video", e); 209 | } 210 | 211 | @Override 212 | public void onCacheDataAvailable(int cachePercentage) { 213 | progressBar.setProgress(cachePercentage); 214 | } 215 | 216 | private SeekBar.OnSeekBarChangeListener mSeekBarChangeListener = new SeekBar.OnSeekBarChangeListener() { 217 | 218 | @Override 219 | public void onStopTrackingTouch(SeekBar seekBar) { 220 | mHandler.postDelayed(hideRunnable, HIDE_TIME); 221 | } 222 | 223 | @Override 224 | public void onStartTrackingTouch(SeekBar seekBar) { 225 | mHandler.removeCallbacks(hideRunnable); 226 | } 227 | 228 | @Override 229 | public void onProgressChanged(SeekBar seekBar, int progress, 230 | boolean fromUser) { 231 | if (fromUser) { 232 | int time = progress * mVideo.getDuration() / 100; 233 | mVideo.seekTo(time); 234 | } 235 | } 236 | }; 237 | 238 | private void backward(float delataX) { 239 | int current = mVideo.getCurrentPosition(); 240 | int backwardTime = (int) (delataX / width * mVideo.getDuration()); 241 | int currentTime = current - backwardTime; 242 | mVideo.seekTo(currentTime); 243 | mSeekBar.setProgress(currentTime * 100 / mVideo.getDuration()); 244 | mPlayTime.setText(formatTime(currentTime)); 245 | } 246 | 247 | private void forward(float delataX) { 248 | int current = mVideo.getCurrentPosition(); 249 | int forwardTime = (int) (delataX / width * mVideo.getDuration()); 250 | int currentTime = current + forwardTime; 251 | mVideo.seekTo(currentTime); 252 | mSeekBar.setProgress(currentTime * 100 / mVideo.getDuration()); 253 | mPlayTime.setText(formatTime(currentTime)); 254 | } 255 | 256 | private void volumeDown(float delatY) { 257 | int max = mAudioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC); 258 | int current = mAudioManager.getStreamVolume(AudioManager.STREAM_MUSIC); 259 | int down = (int) (delatY / height * max * 3); 260 | int volume = Math.max(current - down, 0); 261 | mAudioManager.setStreamVolume(AudioManager.STREAM_MUSIC, volume, 0); 262 | int transformatVolume = volume * 100 / max; 263 | volumnController.show(transformatVolume); 264 | } 265 | 266 | private void volumeUp(float delatY) { 267 | int max = mAudioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC); 268 | int current = mAudioManager.getStreamVolume(AudioManager.STREAM_MUSIC); 269 | int up = (int) ((delatY / height) * max * 3); 270 | int volume = Math.min(current + up, max); 271 | mAudioManager.setStreamVolume(AudioManager.STREAM_MUSIC, volume, 0); 272 | int transformatVolume = volume * 100 / max; 273 | volumnController.show(transformatVolume); 274 | } 275 | 276 | private void lightDown(float delatY) { 277 | int down = (int) (delatY / height * 255 * 3); 278 | int transformatLight = LightnessController.getLightness(this) - down; 279 | LightnessController.setLightness(this, transformatLight); 280 | } 281 | 282 | private void lightUp(float delatY) { 283 | int up = (int) (delatY / height * 255 * 3); 284 | int transformatLight = LightnessController.getLightness(this) + up; 285 | LightnessController.setLightness(this, transformatLight); 286 | } 287 | 288 | 289 | @SuppressLint("HandlerLeak") 290 | private Handler mHandler = new Handler() { 291 | 292 | @Override 293 | public void handleMessage(Message msg) { 294 | super.handleMessage(msg); 295 | switch (msg.what) { 296 | case 1: 297 | if (mVideo.getCurrentPosition() > 0) { 298 | mPlayTime.setText(formatTime(mVideo.getCurrentPosition())); 299 | int progress = mVideo.getCurrentPosition() * 100 / mVideo.getDuration(); 300 | mSeekBar.setProgress(progress); 301 | if (mVideo.getCurrentPosition() > mVideo.getDuration() - 100) { 302 | mPlayTime.setText("00:00"); 303 | mSeekBar.setProgress(0); 304 | } 305 | mSeekBar.setSecondaryProgress(mVideo.getBufferPercentage()); 306 | } else { 307 | mPlayTime.setText("00:00"); 308 | mSeekBar.setProgress(0); 309 | } 310 | 311 | break; 312 | case 2: 313 | showOrHide(); 314 | break; 315 | 316 | default: 317 | break; 318 | } 319 | } 320 | }; 321 | 322 | 323 | 324 | 325 | private Runnable hideRunnable = new Runnable() { 326 | 327 | @Override 328 | public void run() { 329 | showOrHide(); 330 | } 331 | }; 332 | 333 | @SuppressLint("SimpleDateFormat") 334 | private String formatTime(long time) { 335 | DateFormat formatter = new SimpleDateFormat("mm:ss"); 336 | return formatter.format(new Date(time)); 337 | } 338 | 339 | private float mLastMotionX; 340 | private float mLastMotionY; 341 | private int startX; 342 | private int startY; 343 | private int threshold; 344 | private boolean isClick = true; 345 | 346 | private View.OnTouchListener mTouchListener = new View.OnTouchListener() { 347 | 348 | @Override 349 | public boolean onTouch(View v, MotionEvent event) { 350 | final float x = event.getX(); 351 | final float y = event.getY(); 352 | 353 | switch (event.getAction()) { 354 | case MotionEvent.ACTION_DOWN: 355 | mLastMotionX = x; 356 | mLastMotionY = y; 357 | startX = (int) x; 358 | startY = (int) y; 359 | break; 360 | case MotionEvent.ACTION_MOVE: 361 | float deltaX = x - mLastMotionX; 362 | float deltaY = y - mLastMotionY; 363 | float absDeltaX = Math.abs(deltaX); 364 | float absDeltaY = Math.abs(deltaY); 365 | // 声音调节标识 366 | boolean isAdjustAudio = false; 367 | if (absDeltaX > threshold && absDeltaY > threshold) { 368 | if (absDeltaX < absDeltaY) { 369 | isAdjustAudio = true; 370 | } else { 371 | isAdjustAudio = false; 372 | } 373 | } else if (absDeltaX < threshold && absDeltaY > threshold) { 374 | isAdjustAudio = true; 375 | } else if (absDeltaX > threshold && absDeltaY < threshold) { 376 | isAdjustAudio = false; 377 | } else { 378 | return true; 379 | } 380 | if (isAdjustAudio) { 381 | if (x < width / 2) { 382 | if (deltaY > 0) { 383 | lightDown(absDeltaY); 384 | } else if (deltaY < 0) { 385 | lightUp(absDeltaY); 386 | } 387 | } else { 388 | if (deltaY > 0) { 389 | volumeDown(absDeltaY); 390 | } else if (deltaY < 0) { 391 | volumeUp(absDeltaY); 392 | } 393 | } 394 | 395 | } else { 396 | if (deltaX > 0) { 397 | forward(absDeltaX); 398 | } else if (deltaX < 0) { 399 | backward(absDeltaX); 400 | } 401 | } 402 | mLastMotionX = x; 403 | mLastMotionY = y; 404 | break; 405 | case MotionEvent.ACTION_UP: 406 | if (Math.abs(x - startX) > threshold 407 | || Math.abs(y - startY) > threshold) { 408 | isClick = false; 409 | } 410 | mLastMotionX = 0; 411 | mLastMotionY = 0; 412 | startX = (int) 0; 413 | if (isClick) { 414 | showOrHide(); 415 | } 416 | isClick = true; 417 | break; 418 | 419 | default: 420 | break; 421 | } 422 | return true; 423 | } 424 | 425 | }; 426 | 427 | @Override 428 | public void onClick(View v) { 429 | switch (v.getId()) { 430 | case R.id.play_btn: 431 | if (mVideo.isPlaying()) { 432 | mVideo.pause(); 433 | mPlay.setImageResource(R.drawable.video_btn_down); 434 | } else { 435 | mVideo.start(); 436 | mPlay.setImageResource(R.drawable.video_btn_on); 437 | } 438 | break; 439 | default: 440 | break; 441 | } 442 | } 443 | 444 | private void showOrHide() { 445 | if (mTopView.getVisibility() == View.VISIBLE) { 446 | mTopView.clearAnimation(); 447 | Animation animation = AnimationUtils.loadAnimation(this, 448 | R.anim.option_leave_from_top); 449 | animation.setAnimationListener(new AnimationImp() { 450 | @Override 451 | public void onAnimationEnd(Animation animation) { 452 | super.onAnimationEnd(animation); 453 | mTopView.setVisibility(View.GONE); 454 | } 455 | }); 456 | mTopView.startAnimation(animation); 457 | 458 | mBottomView.clearAnimation(); 459 | Animation animation1 = AnimationUtils.loadAnimation(this, 460 | R.anim.option_leave_from_bottom); 461 | animation1.setAnimationListener(new AnimationImp() { 462 | @Override 463 | public void onAnimationEnd(Animation animation) { 464 | super.onAnimationEnd(animation); 465 | mBottomView.setVisibility(View.GONE); 466 | } 467 | }); 468 | mBottomView.startAnimation(animation1); 469 | } else { 470 | mTopView.setVisibility(View.VISIBLE); 471 | mTopView.clearAnimation(); 472 | Animation animation = AnimationUtils.loadAnimation(this, 473 | R.anim.option_entry_from_top); 474 | mTopView.startAnimation(animation); 475 | 476 | mBottomView.setVisibility(View.VISIBLE); 477 | mBottomView.clearAnimation(); 478 | Animation animation1 = AnimationUtils.loadAnimation(this, 479 | R.anim.option_entry_from_bottom); 480 | mBottomView.startAnimation(animation1); 481 | mHandler.removeCallbacks(hideRunnable); 482 | mHandler.postDelayed(hideRunnable, HIDE_TIME); 483 | } 484 | } 485 | 486 | private class AnimationImp implements Animation.AnimationListener { 487 | 488 | @Override 489 | public void onAnimationEnd(Animation animation) { 490 | 491 | } 492 | 493 | @Override 494 | public void onAnimationRepeat(Animation animation) { 495 | } 496 | 497 | @Override 498 | public void onAnimationStart(Animation animation) { 499 | } 500 | 501 | } 502 | 503 | } 504 | -------------------------------------------------------------------------------- /sample/src/main/java/com/jit/video/AdvancedVideoActivity.java: -------------------------------------------------------------------------------- 1 | package com.jit.video; 2 | 3 | import android.annotation.SuppressLint; 4 | import android.app.Activity; 5 | import android.content.Context; 6 | import android.content.res.Configuration; 7 | import android.media.AudioManager; 8 | import android.media.MediaPlayer; 9 | import android.media.MediaPlayer.OnCompletionListener; 10 | import android.media.MediaPlayer.OnPreparedListener; 11 | import android.os.Bundle; 12 | import android.os.Handler; 13 | import android.os.Message; 14 | import android.view.MotionEvent; 15 | import android.view.View; 16 | import android.view.View.OnClickListener; 17 | import android.view.View.OnTouchListener; 18 | import android.view.animation.Animation; 19 | import android.view.animation.Animation.AnimationListener; 20 | import android.view.animation.AnimationUtils; 21 | import android.widget.ImageView; 22 | import android.widget.SeekBar; 23 | import android.widget.SeekBar.OnSeekBarChangeListener; 24 | import android.widget.TextView; 25 | 26 | import com.danikula.videocache.sample.R; 27 | 28 | import java.text.DateFormat; 29 | import java.text.SimpleDateFormat; 30 | import java.util.Date; 31 | import java.util.Timer; 32 | import java.util.TimerTask; 33 | 34 | public class AdvancedVideoActivity extends Activity implements OnClickListener { 35 | 36 | // 自定义VideoView 37 | private FullScreenVideoView mVideo; 38 | 39 | // 头部View 40 | private View mTopView; 41 | 42 | // 底部View 43 | private View mBottomView; 44 | // 视频播放拖动条 45 | private SeekBar mSeekBar; 46 | private ImageView mPlay; 47 | private TextView mPlayTime; 48 | private TextView mDurationTime; 49 | 50 | // 音频管理器 51 | private AudioManager mAudioManager; 52 | 53 | // 屏幕宽高 54 | private float width; 55 | private float height; 56 | 57 | // 视频播放时间 58 | private int playTime; 59 | 60 | private String videoUrl = "https://www.vdomax.com/clips/2015/05/X4gPz_110559_60765d771b815d6faadf2f978fb8fcfe_ori.mp4"; 61 | // 自动隐藏顶部和底部View的时间 62 | private static final int HIDE_TIME = 5000; 63 | 64 | // 声音调节Toast 65 | private VolumnController volumnController; 66 | 67 | // 原始屏幕亮度 68 | private int orginalLight; 69 | 70 | @Override 71 | protected void onCreate(Bundle savedInstanceState) { 72 | super.onCreate(savedInstanceState); 73 | setContentView(R.layout.activity_main); 74 | volumnController = new VolumnController(this); 75 | mVideo = (FullScreenVideoView) findViewById(R.id.videoview); 76 | mPlayTime = (TextView) findViewById(R.id.play_time); 77 | mDurationTime = (TextView) findViewById(R.id.total_time); 78 | mPlay = (ImageView) findViewById(R.id.play_btn); 79 | mSeekBar = (SeekBar) findViewById(R.id.seekbar); 80 | mTopView = findViewById(R.id.top_layout); 81 | mBottomView = findViewById(R.id.bottom_layout); 82 | 83 | mAudioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE); 84 | 85 | width = DensityUtil.getWidthInPx(this); 86 | height = DensityUtil.getHeightInPx(this); 87 | threshold = DensityUtil.dip2px(this, 18); 88 | 89 | orginalLight = LightnessController.getLightness(this); 90 | 91 | mPlay.setOnClickListener(this); 92 | mSeekBar.setOnSeekBarChangeListener(mSeekBarChangeListener); 93 | 94 | playVideo(); 95 | } 96 | 97 | @Override 98 | public void onConfigurationChanged(Configuration newConfig) { 99 | if (this.getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) { 100 | height = DensityUtil.getWidthInPx(this); 101 | width = DensityUtil.getHeightInPx(this); 102 | } else if (this.getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT) { 103 | width = DensityUtil.getWidthInPx(this); 104 | height = DensityUtil.getHeightInPx(this); 105 | } 106 | super.onConfigurationChanged(newConfig); 107 | } 108 | 109 | @Override 110 | protected void onPause() { 111 | super.onPause(); 112 | LightnessController.setLightness(this, orginalLight); 113 | } 114 | 115 | private OnSeekBarChangeListener mSeekBarChangeListener = new OnSeekBarChangeListener() { 116 | 117 | @Override 118 | public void onStopTrackingTouch(SeekBar seekBar) { 119 | mHandler.postDelayed(hideRunnable, HIDE_TIME); 120 | } 121 | 122 | @Override 123 | public void onStartTrackingTouch(SeekBar seekBar) { 124 | mHandler.removeCallbacks(hideRunnable); 125 | } 126 | 127 | @Override 128 | public void onProgressChanged(SeekBar seekBar, int progress, 129 | boolean fromUser) { 130 | if (fromUser) { 131 | int time = progress * mVideo.getDuration() / 100; 132 | mVideo.seekTo(time); 133 | } 134 | } 135 | }; 136 | 137 | private void backward(float delataX) { 138 | int current = mVideo.getCurrentPosition(); 139 | int backwardTime = (int) (delataX / width * mVideo.getDuration()); 140 | int currentTime = current - backwardTime; 141 | mVideo.seekTo(currentTime); 142 | mSeekBar.setProgress(currentTime * 100 / mVideo.getDuration()); 143 | mPlayTime.setText(formatTime(currentTime)); 144 | } 145 | 146 | private void forward(float delataX) { 147 | int current = mVideo.getCurrentPosition(); 148 | int forwardTime = (int) (delataX / width * mVideo.getDuration()); 149 | int currentTime = current + forwardTime; 150 | mVideo.seekTo(currentTime); 151 | mSeekBar.setProgress(currentTime * 100 / mVideo.getDuration()); 152 | mPlayTime.setText(formatTime(currentTime)); 153 | } 154 | 155 | private void volumeDown(float delatY) { 156 | int max = mAudioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC); 157 | int current = mAudioManager.getStreamVolume(AudioManager.STREAM_MUSIC); 158 | int down = (int) (delatY / height * max * 3); 159 | int volume = Math.max(current - down, 0); 160 | mAudioManager.setStreamVolume(AudioManager.STREAM_MUSIC, volume, 0); 161 | int transformatVolume = volume * 100 / max; 162 | volumnController.show(transformatVolume); 163 | } 164 | 165 | private void volumeUp(float delatY) { 166 | int max = mAudioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC); 167 | int current = mAudioManager.getStreamVolume(AudioManager.STREAM_MUSIC); 168 | int up = (int) ((delatY / height) * max * 3); 169 | int volume = Math.min(current + up, max); 170 | mAudioManager.setStreamVolume(AudioManager.STREAM_MUSIC, volume, 0); 171 | int transformatVolume = volume * 100 / max; 172 | volumnController.show(transformatVolume); 173 | } 174 | 175 | private void lightDown(float delatY) { 176 | int down = (int) (delatY / height * 255 * 3); 177 | int transformatLight = LightnessController.getLightness(this) - down; 178 | LightnessController.setLightness(this, transformatLight); 179 | } 180 | 181 | private void lightUp(float delatY) { 182 | int up = (int) (delatY / height * 255 * 3); 183 | int transformatLight = LightnessController.getLightness(this) + up; 184 | LightnessController.setLightness(this, transformatLight); 185 | } 186 | 187 | @Override 188 | protected void onDestroy() { 189 | super.onDestroy(); 190 | mHandler.removeMessages(0); 191 | mHandler.removeCallbacksAndMessages(null); 192 | } 193 | 194 | @SuppressLint("HandlerLeak") 195 | private Handler mHandler = new Handler() { 196 | 197 | @Override 198 | public void handleMessage(Message msg) { 199 | super.handleMessage(msg); 200 | switch (msg.what) { 201 | case 1: 202 | if (mVideo.getCurrentPosition() > 0) { 203 | mPlayTime.setText(formatTime(mVideo.getCurrentPosition())); 204 | int progress = mVideo.getCurrentPosition() * 100 / mVideo.getDuration(); 205 | mSeekBar.setProgress(progress); 206 | if (mVideo.getCurrentPosition() > mVideo.getDuration() - 100) { 207 | mPlayTime.setText("00:00"); 208 | mSeekBar.setProgress(0); 209 | } 210 | mSeekBar.setSecondaryProgress(mVideo.getBufferPercentage()); 211 | } else { 212 | mPlayTime.setText("00:00"); 213 | mSeekBar.setProgress(0); 214 | } 215 | 216 | break; 217 | case 2: 218 | showOrHide(); 219 | break; 220 | 221 | default: 222 | break; 223 | } 224 | } 225 | }; 226 | 227 | private void playVideo() { 228 | mVideo.setVideoPath(videoUrl); 229 | mVideo.requestFocus(); 230 | mVideo.setOnPreparedListener(new OnPreparedListener() { 231 | @Override 232 | public void onPrepared(MediaPlayer mp) { 233 | mVideo.setVideoWidth(mp.getVideoWidth()); 234 | mVideo.setVideoHeight(mp.getVideoHeight()); 235 | 236 | mVideo.start(); 237 | if (playTime != 0) { 238 | mVideo.seekTo(playTime); 239 | } 240 | 241 | mHandler.removeCallbacks(hideRunnable); 242 | mHandler.postDelayed(hideRunnable, HIDE_TIME); 243 | mDurationTime.setText(formatTime(mVideo.getDuration())); 244 | Timer timer = new Timer(); 245 | timer.schedule(new TimerTask() { 246 | 247 | @Override 248 | public void run() { 249 | mHandler.sendEmptyMessage(1); 250 | } 251 | }, 0, 1000); 252 | } 253 | }); 254 | mVideo.setOnCompletionListener(new OnCompletionListener() { 255 | @Override 256 | public void onCompletion(MediaPlayer mp) { 257 | mPlay.setImageResource(R.drawable.video_btn_down); 258 | mPlayTime.setText("00:00"); 259 | mSeekBar.setProgress(0); 260 | } 261 | }); 262 | mVideo.setOnTouchListener(mTouchListener); 263 | } 264 | 265 | private Runnable hideRunnable = new Runnable() { 266 | 267 | @Override 268 | public void run() { 269 | showOrHide(); 270 | } 271 | }; 272 | 273 | @SuppressLint("SimpleDateFormat") 274 | private String formatTime(long time) { 275 | DateFormat formatter = new SimpleDateFormat("mm:ss"); 276 | return formatter.format(new Date(time)); 277 | } 278 | 279 | private float mLastMotionX; 280 | private float mLastMotionY; 281 | private int startX; 282 | private int startY; 283 | private int threshold; 284 | private boolean isClick = true; 285 | 286 | private OnTouchListener mTouchListener = new OnTouchListener() { 287 | 288 | @Override 289 | public boolean onTouch(View v, MotionEvent event) { 290 | final float x = event.getX(); 291 | final float y = event.getY(); 292 | 293 | switch (event.getAction()) { 294 | case MotionEvent.ACTION_DOWN: 295 | mLastMotionX = x; 296 | mLastMotionY = y; 297 | startX = (int) x; 298 | startY = (int) y; 299 | break; 300 | case MotionEvent.ACTION_MOVE: 301 | float deltaX = x - mLastMotionX; 302 | float deltaY = y - mLastMotionY; 303 | float absDeltaX = Math.abs(deltaX); 304 | float absDeltaY = Math.abs(deltaY); 305 | // 声音调节标识 306 | boolean isAdjustAudio = false; 307 | if (absDeltaX > threshold && absDeltaY > threshold) { 308 | if (absDeltaX < absDeltaY) { 309 | isAdjustAudio = true; 310 | } else { 311 | isAdjustAudio = false; 312 | } 313 | } else if (absDeltaX < threshold && absDeltaY > threshold) { 314 | isAdjustAudio = true; 315 | } else if (absDeltaX > threshold && absDeltaY < threshold) { 316 | isAdjustAudio = false; 317 | } else { 318 | return true; 319 | } 320 | if (isAdjustAudio) { 321 | if (x < width / 2) { 322 | if (deltaY > 0) { 323 | lightDown(absDeltaY); 324 | } else if (deltaY < 0) { 325 | lightUp(absDeltaY); 326 | } 327 | } else { 328 | if (deltaY > 0) { 329 | volumeDown(absDeltaY); 330 | } else if (deltaY < 0) { 331 | volumeUp(absDeltaY); 332 | } 333 | } 334 | 335 | } else { 336 | if (deltaX > 0) { 337 | forward(absDeltaX); 338 | } else if (deltaX < 0) { 339 | backward(absDeltaX); 340 | } 341 | } 342 | mLastMotionX = x; 343 | mLastMotionY = y; 344 | break; 345 | case MotionEvent.ACTION_UP: 346 | if (Math.abs(x - startX) > threshold 347 | || Math.abs(y - startY) > threshold) { 348 | isClick = false; 349 | } 350 | mLastMotionX = 0; 351 | mLastMotionY = 0; 352 | startX = (int) 0; 353 | if (isClick) { 354 | showOrHide(); 355 | } 356 | isClick = true; 357 | break; 358 | 359 | default: 360 | break; 361 | } 362 | return true; 363 | } 364 | 365 | }; 366 | 367 | @Override 368 | public void onClick(View v) { 369 | switch (v.getId()) { 370 | case R.id.play_btn: 371 | if (mVideo.isPlaying()) { 372 | mVideo.pause(); 373 | mPlay.setImageResource(R.drawable.video_btn_down); 374 | } else { 375 | mVideo.start(); 376 | mPlay.setImageResource(R.drawable.video_btn_on); 377 | } 378 | break; 379 | default: 380 | break; 381 | } 382 | } 383 | 384 | private void showOrHide() { 385 | if (mTopView.getVisibility() == View.VISIBLE) { 386 | mTopView.clearAnimation(); 387 | Animation animation = AnimationUtils.loadAnimation(this, 388 | R.anim.option_leave_from_top); 389 | animation.setAnimationListener(new AnimationImp() { 390 | @Override 391 | public void onAnimationEnd(Animation animation) { 392 | super.onAnimationEnd(animation); 393 | mTopView.setVisibility(View.GONE); 394 | } 395 | }); 396 | mTopView.startAnimation(animation); 397 | 398 | mBottomView.clearAnimation(); 399 | Animation animation1 = AnimationUtils.loadAnimation(this, 400 | R.anim.option_leave_from_bottom); 401 | animation1.setAnimationListener(new AnimationImp() { 402 | @Override 403 | public void onAnimationEnd(Animation animation) { 404 | super.onAnimationEnd(animation); 405 | mBottomView.setVisibility(View.GONE); 406 | } 407 | }); 408 | mBottomView.startAnimation(animation1); 409 | } else { 410 | mTopView.setVisibility(View.VISIBLE); 411 | mTopView.clearAnimation(); 412 | Animation animation = AnimationUtils.loadAnimation(this, 413 | R.anim.option_entry_from_top); 414 | mTopView.startAnimation(animation); 415 | 416 | mBottomView.setVisibility(View.VISIBLE); 417 | mBottomView.clearAnimation(); 418 | Animation animation1 = AnimationUtils.loadAnimation(this, 419 | R.anim.option_entry_from_bottom); 420 | mBottomView.startAnimation(animation1); 421 | mHandler.removeCallbacks(hideRunnable); 422 | mHandler.postDelayed(hideRunnable, HIDE_TIME); 423 | } 424 | } 425 | 426 | private class AnimationImp implements AnimationListener { 427 | 428 | @Override 429 | public void onAnimationEnd(Animation animation) { 430 | 431 | } 432 | 433 | @Override 434 | public void onAnimationRepeat(Animation animation) { 435 | } 436 | 437 | @Override 438 | public void onAnimationStart(Animation animation) { 439 | } 440 | 441 | } 442 | 443 | } 444 | -------------------------------------------------------------------------------- /sample/src/main/java/com/jit/video/DensityUtil.java: -------------------------------------------------------------------------------- 1 | package com.jit.video; 2 | 3 | import android.content.Context; 4 | 5 | public class DensityUtil { 6 | 7 | public static final float getHeightInPx(Context context) { 8 | final float height = context.getResources().getDisplayMetrics().heightPixels; 9 | return height; 10 | } 11 | 12 | public static final float getWidthInPx(Context context) { 13 | final float width = context.getResources().getDisplayMetrics().widthPixels; 14 | return width; 15 | } 16 | 17 | public static final int getHeightInDp(Context context) { 18 | final float height = context.getResources().getDisplayMetrics().heightPixels; 19 | int heightInDp = px2dip(context, height); 20 | return heightInDp; 21 | } 22 | 23 | public static final int getWidthInDp(Context context) { 24 | final float height = context.getResources().getDisplayMetrics().heightPixels; 25 | int widthInDp = px2dip(context, height); 26 | return widthInDp; 27 | } 28 | 29 | public static int dip2px(Context context, float dpValue) { 30 | final float scale = context.getResources().getDisplayMetrics().density; 31 | return (int) (dpValue * scale + 0.5f); 32 | } 33 | 34 | public static int px2dip(Context context, float pxValue) { 35 | final float scale = context.getResources().getDisplayMetrics().density; 36 | return (int) (pxValue / scale + 0.5f); 37 | } 38 | 39 | public static int px2sp(Context context, float pxValue) { 40 | final float scale = context.getResources().getDisplayMetrics().density; 41 | return (int) (pxValue / scale + 0.5f); 42 | } 43 | 44 | public static int sp2px(Context context, float spValue) { 45 | final float scale = context.getResources().getDisplayMetrics().density; 46 | return (int) (spValue * scale + 0.5f); 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /sample/src/main/java/com/jit/video/FullScreenSurfaceView.java: -------------------------------------------------------------------------------- 1 | package com.jit.video; 2 | 3 | import android.content.Context; 4 | import android.util.AttributeSet; 5 | import android.view.SurfaceView; 6 | 7 | /** 8 | * 自动全屏的VideoView 9 | */ 10 | public class FullScreenSurfaceView extends SurfaceView { 11 | 12 | private int videoWidth; 13 | private int videoHeight; 14 | 15 | public FullScreenSurfaceView(Context context) { 16 | super(context); 17 | } 18 | 19 | public FullScreenSurfaceView(Context context, AttributeSet attrs) { 20 | super(context, attrs); 21 | } 22 | 23 | public FullScreenSurfaceView(Context context, AttributeSet attrs, int defStyle) { 24 | super(context, attrs, defStyle); 25 | } 26 | 27 | @Override 28 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 29 | int width = getDefaultSize(videoWidth, widthMeasureSpec); 30 | int height = getDefaultSize(videoHeight, heightMeasureSpec); 31 | if (videoWidth > 0 && videoHeight > 0) { 32 | if (videoWidth * height > width * videoHeight) { 33 | height = width * videoHeight / videoWidth; 34 | } else if (videoWidth * height < width * videoHeight) { 35 | width = height * videoWidth / videoHeight; 36 | } 37 | } 38 | setMeasuredDimension(width, height); 39 | } 40 | 41 | public int getVideoWidth() { 42 | return videoWidth; 43 | } 44 | 45 | public void setVideoWidth(int videoWidth) { 46 | this.videoWidth = videoWidth; 47 | } 48 | 49 | public int getVideoHeight() { 50 | return videoHeight; 51 | } 52 | 53 | public void setVideoHeight(int videoHeight) { 54 | this.videoHeight = videoHeight; 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /sample/src/main/java/com/jit/video/FullScreenVideoView.java: -------------------------------------------------------------------------------- 1 | package com.jit.video; 2 | 3 | import android.content.Context; 4 | import android.util.AttributeSet; 5 | import android.widget.VideoView; 6 | 7 | /** 8 | * 自动全屏的VideoView 9 | */ 10 | public class FullScreenVideoView extends VideoView { 11 | 12 | private int videoWidth; 13 | private int videoHeight; 14 | 15 | public FullScreenVideoView(Context context) { 16 | super(context); 17 | } 18 | 19 | public FullScreenVideoView(Context context, AttributeSet attrs) { 20 | super(context, attrs); 21 | } 22 | 23 | public FullScreenVideoView(Context context, AttributeSet attrs, int defStyle) { 24 | super(context, attrs, defStyle); 25 | } 26 | 27 | @Override 28 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 29 | int width = getDefaultSize(videoWidth, widthMeasureSpec); 30 | int height = getDefaultSize(videoHeight, heightMeasureSpec); 31 | if (videoWidth > 0 && videoHeight > 0) { 32 | if (videoWidth * height > width * videoHeight) { 33 | height = width * videoHeight / videoWidth; 34 | } else if (videoWidth * height < width * videoHeight) { 35 | width = height * videoWidth / videoHeight; 36 | } 37 | } 38 | setMeasuredDimension(width, height); 39 | } 40 | 41 | public int getVideoWidth() { 42 | return videoWidth; 43 | } 44 | 45 | public void setVideoWidth(int videoWidth) { 46 | this.videoWidth = videoWidth; 47 | } 48 | 49 | public int getVideoHeight() { 50 | return videoHeight; 51 | } 52 | 53 | public void setVideoHeight(int videoHeight) { 54 | this.videoHeight = videoHeight; 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /sample/src/main/java/com/jit/video/LightnessController.java: -------------------------------------------------------------------------------- 1 | package com.jit.video; 2 | 3 | import android.app.Activity; 4 | import android.content.ContentResolver; 5 | import android.provider.Settings; 6 | import android.provider.Settings.System; 7 | import android.view.WindowManager; 8 | import android.widget.Toast; 9 | 10 | public class LightnessController { 11 | 12 | // 判断是否开启了自动亮度调节 13 | public static boolean isAutoBrightness(Activity act) { 14 | boolean automicBrightness = false; 15 | ContentResolver aContentResolver = act.getContentResolver(); 16 | try { 17 | automicBrightness = Settings.System.getInt(aContentResolver, 18 | Settings.System.SCREEN_BRIGHTNESS_MODE) == Settings.System.SCREEN_BRIGHTNESS_MODE_AUTOMATIC; 19 | } catch (Exception e) { 20 | Toast.makeText(act, "Error auto lightness", Toast.LENGTH_SHORT).show(); 21 | } 22 | return automicBrightness; 23 | } 24 | 25 | // 改变亮度 26 | public static void setLightness(Activity act, int value) { 27 | try { 28 | System.putInt(act.getContentResolver(), System.SCREEN_BRIGHTNESS, value); 29 | WindowManager.LayoutParams lp = act.getWindow().getAttributes(); 30 | lp.screenBrightness = (value <= 0 ? 1 : value) / 255f; 31 | act.getWindow().setAttributes(lp); 32 | } catch (Exception e) { 33 | Toast.makeText(act, "Error set lightness", Toast.LENGTH_SHORT).show(); 34 | } 35 | } 36 | 37 | // 获取亮度 38 | public static int getLightness(Activity act) { 39 | return System.getInt(act.getContentResolver(), System.SCREEN_BRIGHTNESS, -1); 40 | } 41 | 42 | // 停止自动亮度调节 43 | public static void stopAutoBrightness(Activity activity) { 44 | Settings.System.putInt(activity.getContentResolver(), 45 | Settings.System.SCREEN_BRIGHTNESS_MODE, 46 | Settings.System.SCREEN_BRIGHTNESS_MODE_MANUAL); 47 | } 48 | 49 | // 开启亮度自动调节 50 | public static void startAutoBrightness(Activity activity) { 51 | Settings.System.putInt(activity.getContentResolver(), 52 | Settings.System.SCREEN_BRIGHTNESS_MODE, 53 | Settings.System.SCREEN_BRIGHTNESS_MODE_AUTOMATIC); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /sample/src/main/java/com/jit/video/VolumnController.java: -------------------------------------------------------------------------------- 1 | package com.jit.video; 2 | 3 | import android.content.Context; 4 | import android.view.Gravity; 5 | import android.view.LayoutInflater; 6 | import android.view.View; 7 | import android.widget.Toast; 8 | 9 | import com.danikula.videocache.sample.R; 10 | 11 | public class VolumnController { 12 | private Toast t; 13 | private VolumnView tv; 14 | 15 | private Context context; 16 | 17 | public VolumnController(Context context) { 18 | this.context = context; 19 | } 20 | 21 | public void show(float progress) { 22 | if (t == null) { 23 | t = new Toast(context); 24 | View layout = LayoutInflater.from(context).inflate(R.layout.vv, null); 25 | tv = (VolumnView) layout.findViewById(R.id.volumnView); 26 | t.setView(layout); 27 | t.setGravity(Gravity.BOTTOM, 0, 100); 28 | t.setDuration(Toast.LENGTH_SHORT); 29 | } 30 | tv.setProgress(progress); 31 | t.show(); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /sample/src/main/java/com/jit/video/VolumnView.java: -------------------------------------------------------------------------------- 1 | package com.jit.video; 2 | 3 | import android.content.Context; 4 | import android.graphics.Bitmap; 5 | import android.graphics.BitmapFactory; 6 | import android.graphics.Canvas; 7 | import android.graphics.Color; 8 | import android.graphics.Paint; 9 | import android.graphics.Paint.Style; 10 | import android.graphics.RectF; 11 | import android.util.AttributeSet; 12 | import android.view.View; 13 | 14 | import com.danikula.videocache.sample.R; 15 | 16 | /** 17 | * 仿小米声音调节环形进度条 18 | */ 19 | public class VolumnView extends View { 20 | 21 | // 半径 22 | float r1 = 0; 23 | float r2 = 0; 24 | float r3 = 0; 25 | // 外圆宽度 26 | float w1 = 15; 27 | // 内圆宽度 28 | float w2 = 30; 29 | Paint paint; 30 | 31 | // 进度 32 | float progress = 0; 33 | 34 | Bitmap bitmap; 35 | 36 | RectF oval; 37 | 38 | public VolumnView(Context context, AttributeSet attrs, int defStyle) { 39 | super(context, attrs, defStyle); 40 | init(context); 41 | } 42 | 43 | public VolumnView(Context context, AttributeSet attrs) { 44 | super(context, attrs); 45 | init(context); 46 | } 47 | 48 | public VolumnView(Context context) { 49 | super(context); 50 | init(context); 51 | } 52 | 53 | void init(Context context) { 54 | paint = new Paint(Paint.ANTI_ALIAS_FLAG); 55 | paint.setStyle(Style.STROKE); 56 | bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.ic_vm); 57 | } 58 | 59 | @Override 60 | protected void onDraw(Canvas canvas) { 61 | float cx = getMeasuredWidth() / 2; 62 | float cy = getMeasuredHeight() / 2; 63 | r1 = cx - w1 / 2; 64 | r2 = cx - w1 / 2 - w2 / 2; 65 | r3 = cx - w1 / 2 - w2; 66 | 67 | // 绘制外圆 68 | paint.setStrokeWidth(w1); 69 | paint.setColor(Color.parseColor("#454547")); 70 | canvas.drawCircle(cx, cy, r1, paint); 71 | 72 | // 绘制中间圆环 73 | paint.setColor(Color.parseColor("#747476")); 74 | paint.setStrokeWidth(w2); 75 | canvas.drawCircle(cx, cy, r2, paint); 76 | 77 | // 绘制内圆 78 | paint.setColor(Color.parseColor("#464648")); 79 | paint.setStyle(Style.FILL); 80 | canvas.drawCircle(cx, cy, r3, paint); 81 | 82 | // 绘制中间的图片 83 | canvas.drawBitmap(bitmap, cx - bitmap.getWidth() / 2, 84 | cx - bitmap.getHeight() / 2, paint); 85 | 86 | // 绘制文本 87 | paint.setColor(Color.WHITE); 88 | paint.setStrokeWidth(0); 89 | paint.setTextSize(24); 90 | float textWidth = paint.measureText("Vol"); // 测量字体宽度,我们需要根据字体的宽度设置在圆环中间 91 | 92 | canvas.drawText("Vol", cx - textWidth / 2, cx + bitmap.getHeight() / 2 93 | + 24, paint); 94 | 95 | // 绘制进度 96 | paint.setStyle(Style.STROKE); 97 | paint.setStrokeWidth(w2); 98 | paint.setColor(Color.WHITE); 99 | if (oval == null) { 100 | oval = new RectF(cx - r2, cy - r2, cx + r2, cy + r2); // 用于定义的圆弧的形状和大小的界限 101 | } 102 | canvas.drawArc(oval, 270, 360 * progress / 100, false, paint); 103 | 104 | super.onDraw(canvas); 105 | } 106 | 107 | /** 108 | * 设置进度 109 | * 110 | * @param progress 111 | * 范围(0-100) 112 | */ 113 | public void setProgress(float progress) { 114 | this.progress = progress; 115 | if (this.progress >= 100) 116 | this.progress = 100; 117 | if (this.progress <= 0) 118 | this.progress = 0; 119 | postInvalidate(); 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /sample/src/main/res/anim/option_entry_from_bottom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 15 | -------------------------------------------------------------------------------- /sample/src/main/res/anim/option_entry_from_top.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 15 | -------------------------------------------------------------------------------- /sample/src/main/res/anim/option_leave_from_bottom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 15 | -------------------------------------------------------------------------------- /sample/src/main/res/anim/option_leave_from_top.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 15 | -------------------------------------------------------------------------------- /sample/src/main/res/drawable-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/korrio/AdvancedVideoView/b64b3702886cde5bf4ec135c4a64b10a77ee92c8/sample/src/main/res/drawable-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-hdpi/line_point.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/korrio/AdvancedVideoView/b64b3702886cde5bf4ec135c4a64b10a77ee92c8/sample/src/main/res/drawable-hdpi/line_point.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-hdpi/ling.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/korrio/AdvancedVideoView/b64b3702886cde5bf4ec135c4a64b10a77ee92c8/sample/src/main/res/drawable-hdpi/ling.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-hdpi/top_btnback.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/korrio/AdvancedVideoView/b64b3702886cde5bf4ec135c4a64b10a77ee92c8/sample/src/main/res/drawable-hdpi/top_btnback.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-hdpi/video_btn_down.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/korrio/AdvancedVideoView/b64b3702886cde5bf4ec135c4a64b10a77ee92c8/sample/src/main/res/drawable-hdpi/video_btn_down.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-hdpi/video_btn_on.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/korrio/AdvancedVideoView/b64b3702886cde5bf4ec135c4a64b10a77ee92c8/sample/src/main/res/drawable-hdpi/video_btn_on.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-hdpi/video_line01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/korrio/AdvancedVideoView/b64b3702886cde5bf4ec135c4a64b10a77ee92c8/sample/src/main/res/drawable-hdpi/video_line01.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-hdpi/video_line02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/korrio/AdvancedVideoView/b64b3702886cde5bf4ec135c4a64b10a77ee92c8/sample/src/main/res/drawable-hdpi/video_line02.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/korrio/AdvancedVideoView/b64b3702886cde5bf4ec135c4a64b10a77ee92c8/sample/src/main/res/drawable-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/korrio/AdvancedVideoView/b64b3702886cde5bf4ec135c4a64b10a77ee92c8/sample/src/main/res/drawable-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-xhdpi/ic_vm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/korrio/AdvancedVideoView/b64b3702886cde5bf4ec135c4a64b10a77ee92c8/sample/src/main/res/drawable-xhdpi/ic_vm.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/korrio/AdvancedVideoView/b64b3702886cde5bf4ec135c4a64b10a77ee92c8/sample/src/main/res/drawable-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable/seekbar.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /sample/src/main/res/layout/activity_exo_player.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 14 | 15 | -------------------------------------------------------------------------------- /sample/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 12 | 13 | 17 | 18 | 23 | 24 | 33 | 34 | 35 | 36 | 42 | 43 | 50 | 51 | 62 | 63 | 74 | 75 | 89 | 91 | 92 | 93 | -------------------------------------------------------------------------------- /sample/src/main/res/layout/activity_video.xml: -------------------------------------------------------------------------------- 1 | 8 | 9 | 13 | 14 | 18 | 19 | 24 | 25 | 34 | 35 | 42 | 43 | 44 | 45 | 51 | 52 | 59 | 60 | 71 | 72 | 83 | 84 | 98 | 100 | 101 | 102 | 103 | -------------------------------------------------------------------------------- /sample/src/main/res/layout/player_activity.xml: -------------------------------------------------------------------------------- 1 | 2 | 16 | 23 | 24 | 28 | 29 | 33 | 34 | 39 | 40 | 49 | 50 | 57 | 58 | 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /sample/src/main/res/layout/vv.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 10 | 11 | -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/korrio/AdvancedVideoView/b64b3702886cde5bf4ec135c4a64b10a77ee92c8/sample/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | VideoCacheSample 3 | 4 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':sample', ':library', ':test' 2 | -------------------------------------------------------------------------------- /test/build.gradle: -------------------------------------------------------------------------------- 1 | repositories { 2 | maven { url 'https://dl.bintray.com/alexeydanilov/maven' } 3 | } 4 | 5 | apply plugin: 'com.android.application' 6 | 7 | android { 8 | compileSdkVersion 22 9 | buildToolsVersion '22.0.1' 10 | 11 | defaultConfig { 12 | applicationId 'com.danikula.proxycache.test' 13 | minSdkVersion 16 14 | targetSdkVersion 22 15 | versionCode 1 16 | versionName '0.1' 17 | } 18 | buildTypes { 19 | debug { 20 | buildConfigField "int", "MIN_SDK_VERSION", Integer.toString(android.defaultConfig.minSdkVersion.apiLevel) 21 | } 22 | release { 23 | buildConfigField "int", "MIN_SDK_VERSION", Integer.toString(android.defaultConfig.minSdkVersion.apiLevel) 24 | } 25 | } 26 | compileOptions { 27 | sourceCompatibility JavaVersion.VERSION_1_7 28 | targetCompatibility JavaVersion.VERSION_1_7 29 | } 30 | } 31 | 32 | dependencies { 33 | compile project (':library') 34 | 35 | testCompile 'junit:junit:4.12' 36 | testCompile('org.robolectric:robolectric:3.0-rc2') { 37 | exclude group: 'commons-logging', module: 'commons-logging' 38 | exclude group: 'org.apache.httpcomponents', module: 'httpclient' 39 | } 40 | testCompile 'com.squareup:fest-android:1.0.0' 41 | testCompile 'com.google.guava:guava-jdk5:17.0' 42 | testCompile('com.danikula:android-garden:2.1.4') { 43 | exclude group: 'com.google.android' 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /test/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /test/src/main/assets/android.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/korrio/AdvancedVideoView/b64b3702886cde5bf4ec135c4a64b10a77ee92c8/test/src/main/assets/android.jpg -------------------------------------------------------------------------------- /test/src/main/assets/phones.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/korrio/AdvancedVideoView/b64b3702886cde5bf4ec135c4a64b10a77ee92c8/test/src/main/assets/phones.jpg -------------------------------------------------------------------------------- /test/src/test/java/com/danikula/videocache/FileCacheTest.java: -------------------------------------------------------------------------------- 1 | package com.danikula.videocache; 2 | 3 | import com.danikula.android.garden.io.Files; 4 | import com.danikula.android.garden.io.IoUtils; 5 | import com.danikula.videocache.test.BuildConfig; 6 | 7 | import org.junit.Assert; 8 | import org.junit.Ignore; 9 | import org.junit.Test; 10 | import org.junit.runner.RunWith; 11 | import org.robolectric.RobolectricGradleTestRunner; 12 | import org.robolectric.annotation.Config; 13 | 14 | import java.io.File; 15 | import java.util.Arrays; 16 | 17 | import static com.danikula.videocache.support.ProxyCacheTestUtils.ASSETS_DATA_NAME; 18 | import static com.danikula.videocache.support.ProxyCacheTestUtils.generate; 19 | import static com.danikula.videocache.support.ProxyCacheTestUtils.getFileContent; 20 | import static com.danikula.videocache.support.ProxyCacheTestUtils.getTempFile; 21 | import static com.danikula.videocache.support.ProxyCacheTestUtils.loadAssetFile; 22 | import static com.danikula.videocache.support.ProxyCacheTestUtils.newCacheFile; 23 | import static org.fest.assertions.api.Assertions.assertThat; 24 | 25 | /** 26 | * @author Alexey Danilov (danikula@gmail.com). 27 | */ 28 | @RunWith(RobolectricGradleTestRunner.class) 29 | @Config(constants = BuildConfig.class, emulateSdk = BuildConfig.MIN_SDK_VERSION) 30 | public class FileCacheTest { 31 | 32 | @Test 33 | public void testWriteReadDiscCache() throws Exception { 34 | int firstPortionLength = 10000; 35 | byte[] firstDataPortion = generate(firstPortionLength); 36 | File file = newCacheFile(); 37 | Cache fileCache = new FileCache(file); 38 | 39 | fileCache.append(firstDataPortion, firstDataPortion.length); 40 | byte[] readData = new byte[firstPortionLength]; 41 | fileCache.read(readData, 0, firstPortionLength); 42 | assertThat(readData).isEqualTo(firstDataPortion); 43 | byte[] fileContent = getFileContent(getTempFile(file)); 44 | assertThat(readData).isEqualTo(fileContent); 45 | } 46 | 47 | @Test 48 | public void testFileCacheCompletion() throws Exception { 49 | File file = newCacheFile(); 50 | File tempFile = getTempFile(file); 51 | Cache fileCache = new FileCache(file); 52 | assertThat(file.exists()).isFalse(); 53 | assertThat(tempFile.exists()).isTrue(); 54 | 55 | int dataSize = 345; 56 | fileCache.append(generate(dataSize), dataSize); 57 | fileCache.complete(); 58 | 59 | assertThat(file.exists()).isTrue(); 60 | assertThat(tempFile.exists()).isFalse(); 61 | assertThat(file.length()).isEqualTo(dataSize); 62 | } 63 | 64 | @Test(expected = ProxyCacheException.class) 65 | public void testErrorAppendFileCacheAfterCompletion() throws Exception { 66 | Cache fileCache = new FileCache(newCacheFile()); 67 | fileCache.append(generate(20), 10); 68 | fileCache.complete(); 69 | fileCache.append(generate(20), 10); 70 | Assert.fail(); 71 | } 72 | 73 | @Test 74 | public void testAppendDiscCache() throws Exception { 75 | File file = newCacheFile(); 76 | Cache fileCache = new FileCache(file); 77 | 78 | int firstPortionLength = 10000; 79 | byte[] firstDataPortion = generate(firstPortionLength); 80 | fileCache.append(firstDataPortion, firstDataPortion.length); 81 | 82 | int secondPortionLength = 30000; 83 | byte[] secondDataPortion = generate(secondPortionLength * 2); 84 | fileCache.append(secondDataPortion, secondPortionLength); 85 | 86 | byte[] wroteSecondPortion = Arrays.copyOfRange(secondDataPortion, 0, secondPortionLength); 87 | byte[] readData = new byte[secondPortionLength]; 88 | fileCache.read(readData, firstPortionLength, secondPortionLength); 89 | assertThat(readData).isEqualTo(wroteSecondPortion); 90 | 91 | readData = new byte[fileCache.available()]; 92 | fileCache.read(readData, 0, readData.length); 93 | byte[] fileContent = getFileContent(getTempFile(file)); 94 | assertThat(readData).isEqualTo(fileContent); 95 | } 96 | 97 | @Test 98 | public void testIsFileCacheCompleted() throws Exception { 99 | File file = newCacheFile(); 100 | File partialFile = new File(file.getParentFile(), file.getName() + ".download"); 101 | IoUtils.saveToFile(loadAssetFile(ASSETS_DATA_NAME), partialFile); 102 | Cache fileCache = new FileCache(partialFile); 103 | 104 | assertThat(file.exists()).isFalse(); 105 | assertThat(partialFile.exists()).isTrue(); 106 | assertThat(fileCache.isCompleted()).isFalse(); 107 | 108 | fileCache.complete(); 109 | 110 | assertThat(file.exists()).isTrue(); 111 | assertThat(partialFile.exists()).isFalse(); 112 | assertThat(fileCache.isCompleted()).isTrue(); 113 | assertThat(partialFile.exists()).isFalse(); 114 | assertThat(new FileCache(file).isCompleted()).isTrue(); 115 | } 116 | 117 | @Test(expected = ProxyCacheException.class) 118 | public void testErrorWritingCompletedCache() throws Exception { 119 | File file = newCacheFile(); 120 | IoUtils.saveToFile(loadAssetFile(ASSETS_DATA_NAME), file); 121 | FileCache fileCache = new FileCache(file); 122 | fileCache.append(generate(100), 20); 123 | Assert.fail(); 124 | } 125 | 126 | @Test(expected = ProxyCacheException.class) 127 | public void testErrorWritingAfterCompletion() throws Exception { 128 | File file = newCacheFile(); 129 | File partialFile = new File(file.getParentFile(), file.getName() + ".download"); 130 | IoUtils.saveToFile(loadAssetFile(ASSETS_DATA_NAME), partialFile); 131 | FileCache fileCache = new FileCache(partialFile); 132 | fileCache.complete(); 133 | fileCache.append(generate(100), 20); 134 | Assert.fail(); 135 | } 136 | 137 | @Ignore("How to emulate file error?") 138 | @Test(expected = ProxyCacheException.class) 139 | public void testFileErrorForDiscCache() throws Exception { 140 | File file = new File("/system/data.bin"); 141 | FileCache fileCache = new FileCache(file); 142 | Files.delete(file); 143 | fileCache.available(); 144 | Assert.fail(); 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /test/src/test/java/com/danikula/videocache/HttpProxyCacheTest.java: -------------------------------------------------------------------------------- 1 | package com.danikula.videocache; 2 | 3 | import com.danikula.android.garden.io.IoUtils; 4 | import com.danikula.videocache.support.AngryHttpUrlSource; 5 | import com.danikula.videocache.support.Response; 6 | import com.danikula.videocache.test.BuildConfig; 7 | 8 | import org.junit.Test; 9 | import org.junit.runner.RunWith; 10 | import org.robolectric.RobolectricGradleTestRunner; 11 | import org.robolectric.annotation.Config; 12 | 13 | import java.io.File; 14 | import java.util.Arrays; 15 | 16 | import static com.danikula.videocache.support.ProxyCacheTestUtils.ASSETS_DATA_BIG_NAME; 17 | import static com.danikula.videocache.support.ProxyCacheTestUtils.ASSETS_DATA_NAME; 18 | import static com.danikula.videocache.support.ProxyCacheTestUtils.HTTP_DATA_BIG_SIZE; 19 | import static com.danikula.videocache.support.ProxyCacheTestUtils.HTTP_DATA_BIG_URL; 20 | import static com.danikula.videocache.support.ProxyCacheTestUtils.HTTP_DATA_URL; 21 | import static com.danikula.videocache.support.ProxyCacheTestUtils.generate; 22 | import static com.danikula.videocache.support.ProxyCacheTestUtils.getFileContent; 23 | import static com.danikula.videocache.support.ProxyCacheTestUtils.loadAssetFile; 24 | import static com.danikula.videocache.support.ProxyCacheTestUtils.newCacheFile; 25 | import static com.danikula.videocache.support.ProxyCacheTestUtils.readProxyResponse; 26 | import static org.fest.assertions.api.Assertions.assertThat; 27 | 28 | /** 29 | * @author Alexey Danilov (danikula@gmail.com). 30 | */ 31 | @RunWith(RobolectricGradleTestRunner.class) 32 | @Config(constants = BuildConfig.class, emulateSdk = BuildConfig.MIN_SDK_VERSION) 33 | public class HttpProxyCacheTest { 34 | 35 | @Test 36 | public void testHttpProxyCache() throws Exception { 37 | HttpUrlSource source = new HttpUrlSource(HTTP_DATA_URL); 38 | File file = newCacheFile(); 39 | HttpProxyCache proxy = new HttpProxyCache(source, new FileCache(file)); 40 | Response response = readProxyResponse(proxy); 41 | assertThat(response.code).isEqualTo(200); 42 | assertThat(response.data).isEqualTo(getFileContent(file)); 43 | assertThat(response.data).isEqualTo(loadAssetFile(ASSETS_DATA_NAME)); 44 | proxy.shutdown(); 45 | } 46 | 47 | @Test 48 | public void testProxyContentWithPartialCache() throws Exception { 49 | HttpUrlSource source = new HttpUrlSource(HTTP_DATA_URL); 50 | int cacheSize = 1000; 51 | HttpProxyCache proxy = new HttpProxyCache(source, new ByteArrayCache(new byte[cacheSize])); 52 | 53 | Response proxyResponse = readProxyResponse(proxy); 54 | byte[] expected = loadAssetFile(ASSETS_DATA_NAME); 55 | Arrays.fill(expected, 0, cacheSize, (byte) 0); 56 | assertThat(proxyResponse.data).isEqualTo(expected); 57 | proxy.shutdown(); 58 | } 59 | 60 | @Test 61 | public void testMimeFromResponse() throws Exception { 62 | HttpUrlSource source = new HttpUrlSource("https://dl.dropboxusercontent.com/u/15506779/persistent/proxycache/android"); 63 | HttpProxyCache proxy = new HttpProxyCache(source, new ByteArrayCache(new byte[0])); 64 | proxy.read(new byte[1], 0, 1); 65 | assertThat(source.getMime()).isEqualTo("application/octet-stream"); 66 | proxy.shutdown(); 67 | } 68 | 69 | @Test 70 | public void testProxyFullResponse() throws Exception { 71 | File file = newCacheFile(); 72 | HttpProxyCache proxy = new HttpProxyCache(new HttpUrlSource(HTTP_DATA_BIG_URL), new FileCache(file)); 73 | Response response = readProxyResponse(proxy); 74 | 75 | assertThat(response.code).isEqualTo(200); 76 | assertThat(response.contentLength).isEqualTo(HTTP_DATA_BIG_SIZE); 77 | assertThat(response.contentType).isEqualTo("image/jpeg"); 78 | assertThat(response.headers.containsKey("Accept-Ranges")).isTrue(); 79 | assertThat(response.headers.get("Accept-Ranges").get(0)).isEqualTo("bytes"); 80 | assertThat(response.headers.containsKey("Content-Range")).isFalse(); 81 | assertThat(response.data).isEqualTo(getFileContent(file)); 82 | assertThat(response.data).isEqualTo(loadAssetFile(ASSETS_DATA_BIG_NAME)); 83 | proxy.shutdown(); 84 | } 85 | 86 | @Test 87 | public void testProxyPartialResponse() throws Exception { 88 | int offset = 42000; 89 | File file = newCacheFile(); 90 | HttpProxyCache proxy = new HttpProxyCache(new HttpUrlSource(HTTP_DATA_BIG_URL), new FileCache(file)); 91 | Response response = readProxyResponse(proxy, offset); 92 | 93 | assertThat(response.code).isEqualTo(206); 94 | assertThat(response.contentLength).isEqualTo(HTTP_DATA_BIG_SIZE - offset); 95 | assertThat(response.contentType).isEqualTo("image/jpeg"); 96 | assertThat(response.headers.containsKey("Accept-Ranges")).isTrue(); 97 | assertThat(response.headers.get("Accept-Ranges").get(0)).isEqualTo("bytes"); 98 | assertThat(response.headers.containsKey("Content-Range")).isTrue(); 99 | String rangeHeader = String.format("bytes %d-%d/%d", offset, HTTP_DATA_BIG_SIZE, HTTP_DATA_BIG_SIZE); 100 | assertThat(response.headers.get("Content-Range").get(0)).isEqualTo(rangeHeader); 101 | byte[] expectedData = Arrays.copyOfRange(loadAssetFile(ASSETS_DATA_BIG_NAME), offset, HTTP_DATA_BIG_SIZE); 102 | assertThat(response.data).isEqualTo(expectedData); 103 | assertThat(getFileContent(file)).isEqualTo(loadAssetFile(ASSETS_DATA_BIG_NAME)); 104 | proxy.shutdown(); 105 | } 106 | 107 | @Test 108 | public void testAppendCache() throws Exception { 109 | byte[] cachedPortion = generate(1200); 110 | File file = newCacheFile(); 111 | File partialFile = new File(file.getParentFile(), file.getName() + ".download"); 112 | IoUtils.saveToFile(cachedPortion, partialFile); 113 | Cache cache = new FileCache(partialFile); 114 | assertThat(cache.isCompleted()).isFalse(); 115 | 116 | HttpProxyCache proxy = new HttpProxyCache(new HttpUrlSource(HTTP_DATA_BIG_URL), cache); 117 | readProxyResponse(proxy); 118 | proxy.shutdown(); 119 | 120 | assertThat(cache.isCompleted()).isTrue(); 121 | 122 | byte[] expectedData = loadAssetFile(ASSETS_DATA_BIG_NAME); 123 | System.arraycopy(cachedPortion, 0, expectedData, 0, cachedPortion.length); 124 | assertThat(file.length()).isEqualTo(HTTP_DATA_BIG_SIZE); 125 | assertThat(expectedData).isEqualTo(getFileContent(file)); 126 | } 127 | 128 | @Test 129 | public void testNoTouchSource() throws Exception { 130 | File file = newCacheFile(); 131 | IoUtils.saveToFile(loadAssetFile(ASSETS_DATA_BIG_NAME), file); 132 | FileCache cache = new FileCache(file); 133 | HttpProxyCache proxy = new HttpProxyCache(new HttpUrlSource(HTTP_DATA_BIG_URL), cache); 134 | Response response = readProxyResponse(proxy); 135 | proxy.shutdown(); 136 | assertThat(response.code).isEqualTo(200); 137 | 138 | proxy = new HttpProxyCache(new AngryHttpUrlSource(HTTP_DATA_BIG_URL, "image/jpeg"), new FileCache(file)); 139 | readProxyResponse(proxy); 140 | assertThat(response.code).isEqualTo(200); 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /test/src/test/java/com/danikula/videocache/HttpUrlSourceTest.java: -------------------------------------------------------------------------------- 1 | package com.danikula.videocache; 2 | 3 | import com.danikula.videocache.test.BuildConfig; 4 | 5 | import org.junit.Ignore; 6 | import org.junit.Test; 7 | import org.junit.runner.RunWith; 8 | import org.robolectric.RobolectricGradleTestRunner; 9 | import org.robolectric.annotation.Config; 10 | 11 | import java.io.ByteArrayOutputStream; 12 | import java.util.Arrays; 13 | 14 | import static com.danikula.videocache.support.ProxyCacheTestUtils.ASSETS_DATA_BIG_NAME; 15 | import static com.danikula.videocache.support.ProxyCacheTestUtils.ASSETS_DATA_NAME; 16 | import static com.danikula.videocache.support.ProxyCacheTestUtils.HTTP_DATA_BIG_SIZE; 17 | import static com.danikula.videocache.support.ProxyCacheTestUtils.HTTP_DATA_BIG_URL; 18 | import static com.danikula.videocache.support.ProxyCacheTestUtils.HTTP_DATA_URL; 19 | import static com.danikula.videocache.support.ProxyCacheTestUtils.loadAssetFile; 20 | import static org.fest.assertions.api.Assertions.assertThat; 21 | 22 | /** 23 | * @author Alexey Danilov (danikula@gmail.com). 24 | */ 25 | @RunWith(RobolectricGradleTestRunner.class) 26 | @Config(constants = BuildConfig.class, emulateSdk = BuildConfig.MIN_SDK_VERSION) 27 | public class HttpUrlSourceTest { 28 | 29 | @Test 30 | public void testHttpUrlSourceRange() throws Exception { 31 | int offset = 1000; 32 | int length = 10; 33 | Source source = new HttpUrlSource(HTTP_DATA_URL); 34 | source.open(offset); 35 | byte[] readData = new byte[length]; 36 | source.read(readData); 37 | source.close(); 38 | byte[] expectedData = Arrays.copyOfRange(loadAssetFile(ASSETS_DATA_NAME), offset, offset + length); 39 | assertThat(readData).isEqualTo(expectedData); 40 | } 41 | 42 | @Test 43 | public void testHttpUrlSourceWithOffset() throws Exception { 44 | int offset = 30000; 45 | Source source = new HttpUrlSource(HTTP_DATA_BIG_URL); 46 | source.open(offset); 47 | ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); 48 | int read; 49 | byte[] buffer = new byte[3000]; 50 | while ((read = (source.read(buffer))) != -1) { 51 | outputStream.write(buffer, 0, read); 52 | } 53 | source.close(); 54 | byte[] expectedData = Arrays.copyOfRange(loadAssetFile(ASSETS_DATA_BIG_NAME), offset, HTTP_DATA_BIG_SIZE); 55 | assertThat(outputStream.toByteArray()).isEqualTo(expectedData); 56 | } 57 | 58 | @Test 59 | public void testFetchContentLength() throws Exception { 60 | Source source = new HttpUrlSource(HTTP_DATA_URL); 61 | assertThat(source.available()).isEqualTo(loadAssetFile(ASSETS_DATA_NAME).length); 62 | } 63 | 64 | @Ignore("Seems Robolectric bug: MimeTypeMap.getFileExtensionFromUrl always returns null") 65 | @Test 66 | public void testMimeByUrl() throws Exception { 67 | assertThat(new HttpUrlSource("http://mysite.by/video.mp4").getMime()).isEqualTo("video/mp4"); 68 | assertThat(new HttpUrlSource(HTTP_DATA_URL).getMime()).isEqualTo("image/jpeg"); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /test/src/test/java/com/danikula/videocache/ProxyCacheTest.java: -------------------------------------------------------------------------------- 1 | package com.danikula.videocache; 2 | 3 | import com.danikula.videocache.support.PhlegmaticByteArraySource; 4 | import com.danikula.videocache.test.BuildConfig; 5 | 6 | import org.junit.Assert; 7 | import org.junit.Test; 8 | import org.junit.runner.RunWith; 9 | import org.robolectric.RobolectricGradleTestRunner; 10 | import org.robolectric.annotation.Config; 11 | 12 | import java.io.File; 13 | import java.util.Arrays; 14 | import java.util.Random; 15 | import java.util.concurrent.TimeUnit; 16 | 17 | import static com.danikula.videocache.support.ProxyCacheTestUtils.ASSETS_DATA_NAME; 18 | import static com.danikula.videocache.support.ProxyCacheTestUtils.HTTP_DATA_SIZE; 19 | import static com.danikula.videocache.support.ProxyCacheTestUtils.HTTP_DATA_URL; 20 | import static com.danikula.videocache.support.ProxyCacheTestUtils.generate; 21 | import static com.danikula.videocache.support.ProxyCacheTestUtils.getFileContent; 22 | import static com.danikula.videocache.support.ProxyCacheTestUtils.loadAssetFile; 23 | import static com.danikula.videocache.support.ProxyCacheTestUtils.newCacheFile; 24 | import static org.fest.assertions.api.Assertions.assertThat; 25 | 26 | /** 27 | * @author Alexey Danilov (danikula@gmail.com). 28 | */ 29 | @RunWith(RobolectricGradleTestRunner.class) 30 | @Config(constants = BuildConfig.class, emulateSdk = BuildConfig.MIN_SDK_VERSION) 31 | public class ProxyCacheTest { 32 | 33 | @Test 34 | public void testNoCache() throws Exception { 35 | byte[] sourceData = generate(345); 36 | ProxyCache proxyCache = new ProxyCache(new ByteArraySource(sourceData), new ByteArrayCache()); 37 | 38 | byte[] buffer = new byte[sourceData.length]; 39 | int read = proxyCache.read(buffer, 0, sourceData.length); 40 | 41 | assertThat(read).isEqualTo(sourceData.length); 42 | assertThat(buffer).isEqualTo(sourceData); 43 | } 44 | 45 | @Test 46 | public void testAllFromCacheNoSource() throws Exception { 47 | byte[] cacheData = generate(34564); 48 | ProxyCache proxyCache = new ProxyCache(new ByteArraySource(new byte[0]), new ByteArrayCache(cacheData)); 49 | 50 | byte[] buffer = new byte[cacheData.length + 100]; 51 | int read = proxyCache.read(buffer, 0, cacheData.length); 52 | byte[] readData = Arrays.copyOfRange(buffer, 0, cacheData.length); 53 | 54 | assertThat(read).isEqualTo(cacheData.length); 55 | assertThat(readData).isEqualTo(cacheData); 56 | } 57 | 58 | @Test 59 | public void testMergeSourceAndCache() throws Exception { 60 | byte[] sourceData = generate(2345); 61 | byte[] cacheData = generate(1048); 62 | ProxyCache proxyCache = new ProxyCache(new ByteArraySource(sourceData), new ByteArrayCache(cacheData)); 63 | 64 | byte[] buffer = new byte[sourceData.length]; 65 | int read = proxyCache.read(buffer, 0, sourceData.length); 66 | 67 | byte[] expected = new byte[sourceData.length]; 68 | System.arraycopy(cacheData, 0, expected, 0, cacheData.length); 69 | System.arraycopy(sourceData, cacheData.length, expected, cacheData.length, sourceData.length - cacheData.length); 70 | assertThat(read).isEqualTo(sourceData.length); 71 | assertThat(buffer).isEqualTo(expected); 72 | } 73 | 74 | @Test 75 | public void testReuseCache() throws Exception { 76 | int size = 20000; 77 | byte[] sourceData = generate(size); 78 | Cache cache = new ByteArrayCache(); 79 | ProxyCache proxyCache = new ProxyCache(new ByteArraySource(sourceData), cache); 80 | byte[] fetchedData = new byte[size]; 81 | proxyCache.read(fetchedData, 0, size); 82 | assertThat(fetchedData).isEqualTo(sourceData); 83 | 84 | byte[] sourceCopy = Arrays.copyOf(sourceData, size); 85 | byte[] newSource = generate(size); 86 | proxyCache = new ProxyCache(new ByteArraySource(newSource), cache); 87 | Arrays.fill(fetchedData, (byte) 0); 88 | proxyCache.read(fetchedData, 0, size); 89 | assertThat(fetchedData).isEqualTo(sourceCopy); 90 | } 91 | 92 | @Test(expected = ProxyCacheException.class) 93 | public void testNoMoreSource() throws Exception { 94 | int sourceSize = 942; 95 | int cacheSize = 6157; 96 | ByteArraySource source = new ByteArraySource(generate(sourceSize)); 97 | ByteArrayCache cache = new ByteArrayCache(generate(cacheSize)); 98 | ProxyCache proxyCache = new ProxyCache(source, cache); 99 | proxyCache.read(new byte[sourceSize + cacheSize], sourceSize + cacheSize + 1, 10); 100 | Assert.fail(); 101 | } 102 | 103 | @Test 104 | public void testProxyWithPhlegmaticSource() throws Exception { 105 | int dataSize = 100000; 106 | byte[] sourceData = generate(dataSize); 107 | Source source = new PhlegmaticByteArraySource(sourceData, 200); 108 | ProxyCache proxyCache = new ProxyCache(source, new FileCache(newCacheFile())); 109 | byte[] readData = new byte[dataSize]; 110 | proxyCache.read(readData, 0, dataSize); 111 | assertThat(readData).isEqualTo(sourceData); 112 | } 113 | 114 | 115 | @Test 116 | public void testReadEnd() throws Exception { 117 | int capacity = 5323; 118 | Source source = new PhlegmaticByteArraySource(generate(capacity), 200); 119 | Cache cache = new FileCache(newCacheFile()); 120 | ProxyCache proxyCache = new ProxyCache(source, cache); 121 | proxyCache.read(new byte[1], capacity - 1, 1); 122 | TimeUnit.MILLISECONDS.sleep(200); // wait for completion 123 | assertThat(cache.isCompleted()).isTrue(); 124 | } 125 | 126 | @Test 127 | public void testReadRandomParts() throws Exception { 128 | int dataSize = 123456; 129 | byte[] sourceData = generate(dataSize); 130 | Source source = new PhlegmaticByteArraySource(sourceData, 300); 131 | File file = newCacheFile(); 132 | Cache cache = new FileCache(file); 133 | ProxyCache proxyCache = new ProxyCache(source, cache); 134 | Random random = new Random(System.currentTimeMillis()); 135 | for (int i = 0; i < 100; i++) { 136 | int offset = random.nextInt(dataSize); 137 | int bufferSize = random.nextInt(dataSize / 4); 138 | bufferSize = Math.min(bufferSize, dataSize - offset); 139 | byte[] buffer = new byte[bufferSize]; 140 | proxyCache.read(buffer, offset, bufferSize); 141 | byte[] dataPortion = Arrays.copyOfRange(sourceData, offset, offset + bufferSize); 142 | assertThat(buffer).isEqualTo(dataPortion); 143 | } 144 | proxyCache.read(new byte[1], dataSize - 1, 1); 145 | TimeUnit.MILLISECONDS.sleep(200); // wait for completion 146 | assertThat(cache.isCompleted()).isTrue(); 147 | assertThat(sourceData).isEqualTo(getFileContent(file)); 148 | } 149 | 150 | @Test 151 | public void testLoadingHttpData() throws Exception { 152 | Source source = new HttpUrlSource(HTTP_DATA_URL); 153 | ProxyCache proxyCache = new ProxyCache(source, new FileCache(newCacheFile())); 154 | byte[] remoteData = new byte[HTTP_DATA_SIZE]; 155 | proxyCache.read(remoteData, 0, HTTP_DATA_SIZE); 156 | proxyCache.shutdown(); 157 | 158 | assertThat(remoteData).isEqualTo(loadAssetFile(ASSETS_DATA_NAME)); 159 | } 160 | 161 | @Test 162 | public void testReadMoreThanAvailable() throws Exception { 163 | byte[] data = generate(20000); 164 | Cache fileCache = new FileCache(newCacheFile()); 165 | ProxyCache proxyCache = new ProxyCache(new ByteArraySource(data), fileCache); 166 | 167 | byte[] buffer = new byte[15000]; 168 | proxyCache.read(buffer, 18000, buffer.length); 169 | byte[] expectedData = new byte[15000]; 170 | System.arraycopy(data, 18000, expectedData, 0, 2000); 171 | assertThat(buffer).isEqualTo(expectedData); 172 | } 173 | 174 | @Test 175 | public void testCompletion() throws Exception { 176 | Cache cache = new FileCache(newCacheFile()); 177 | ProxyCache proxyCache = new ProxyCache(new ByteArraySource(generate(20000)), cache); 178 | proxyCache.read(new byte[5], 19999, 5); 179 | assertThat(cache.isCompleted()).isTrue(); 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /test/src/test/java/com/danikula/videocache/support/AngryHttpUrlSource.java: -------------------------------------------------------------------------------- 1 | package com.danikula.videocache.support; 2 | 3 | import android.text.TextUtils; 4 | 5 | import com.danikula.videocache.HttpUrlSource; 6 | import com.danikula.videocache.ProxyCacheException; 7 | 8 | /** 9 | * {@link HttpUrlSource} that throws exception in all methods. 10 | * 11 | * @author Alexey Danilov (danikula@gmail.com). 12 | */ 13 | public class AngryHttpUrlSource extends HttpUrlSource { 14 | 15 | public AngryHttpUrlSource(String url, String mime) { 16 | super(url, mime); 17 | } 18 | 19 | @Override 20 | public int available() throws ProxyCacheException { 21 | throw new IllegalStateException(); 22 | } 23 | 24 | @Override 25 | public void open(int offset) throws ProxyCacheException { 26 | throw new IllegalStateException(); 27 | } 28 | 29 | @Override 30 | public void close() throws ProxyCacheException { 31 | throw new IllegalStateException(); 32 | } 33 | 34 | @Override 35 | public int read(byte[] buffer) throws ProxyCacheException { 36 | throw new IllegalStateException(); 37 | } 38 | 39 | public String getMime() throws ProxyCacheException { 40 | String mime = super.getMime(); 41 | if (!TextUtils.isEmpty(mime)) { 42 | return mime; 43 | } 44 | throw new IllegalStateException(); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /test/src/test/java/com/danikula/videocache/support/PhlegmaticByteArraySource.java: -------------------------------------------------------------------------------- 1 | package com.danikula.videocache.support; 2 | 3 | import com.danikula.videocache.ByteArraySource; 4 | import com.danikula.videocache.ProxyCacheException; 5 | 6 | import java.util.Random; 7 | 8 | /** 9 | * @author Alexey Danilov (danikula@gmail.com). 10 | */ 11 | public class PhlegmaticByteArraySource extends ByteArraySource { 12 | 13 | private final Random delayGenerator; 14 | private final int maxDelayMs; 15 | 16 | public PhlegmaticByteArraySource(byte[] data, int maxDelayMs) { 17 | super(data); 18 | this.maxDelayMs = maxDelayMs; 19 | this.delayGenerator = new Random(System.currentTimeMillis()); 20 | } 21 | 22 | @Override 23 | public int read(byte[] buffer) throws ProxyCacheException { 24 | try { 25 | Thread.sleep(delayGenerator.nextInt(maxDelayMs)); 26 | } catch (InterruptedException e) { 27 | throw new ProxyCacheException("Error sleeping", e); 28 | } 29 | return super.read(buffer); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /test/src/test/java/com/danikula/videocache/support/ProxyCacheTestUtils.java: -------------------------------------------------------------------------------- 1 | package com.danikula.videocache.support; 2 | 3 | import com.danikula.android.garden.io.IoUtils; 4 | import com.danikula.videocache.HttpProxyCache; 5 | import com.google.common.io.Files; 6 | 7 | import org.robolectric.RuntimeEnvironment; 8 | 9 | import java.io.ByteArrayOutputStream; 10 | import java.io.File; 11 | import java.io.IOException; 12 | import java.io.InputStream; 13 | import java.net.HttpURLConnection; 14 | import java.net.URL; 15 | import java.util.Random; 16 | import java.util.UUID; 17 | 18 | /** 19 | * @author Alexey Danilov (danikula@gmail.com). 20 | */ 21 | public class ProxyCacheTestUtils { 22 | 23 | public static final String HTTP_DATA_URL = "https://dl.dropboxusercontent.com/u/15506779/persistent/proxycache/android.jpg"; 24 | public static final String HTTP_DATA_BIG_URL = "https://dl.dropboxusercontent.com/u/15506779/persistent/proxycache/phones.jpg"; 25 | public static final String ASSETS_DATA_NAME = "android.jpg"; 26 | public static final String ASSETS_DATA_BIG_NAME = "phones.jpg"; 27 | public static final int HTTP_DATA_SIZE = 4768; 28 | public static final int HTTP_DATA_BIG_SIZE = 94363; 29 | 30 | public static byte[] getFileContent(File file) throws IOException { 31 | return Files.asByteSource(file).read(); 32 | } 33 | 34 | public static Response readProxyResponse(HttpProxyCache proxy) throws IOException { 35 | return readProxyResponse(proxy, -1); 36 | } 37 | 38 | public static Response readProxyResponse(HttpProxyCache proxy, int offset) throws IOException { 39 | URL url = new URL(proxy.getUrl()); 40 | HttpURLConnection connection = (HttpURLConnection) url.openConnection(); 41 | try { 42 | if (offset >= 0) { 43 | connection.setRequestProperty("Range", "bytes=" + offset + "-"); 44 | } 45 | return new Response(connection); 46 | } finally { 47 | connection.disconnect(); 48 | } 49 | } 50 | 51 | public static byte[] loadAssetFile(String name) throws IOException { 52 | InputStream in = RuntimeEnvironment.application.getResources().getAssets().open(name); 53 | ByteArrayOutputStream out = new ByteArrayOutputStream(); 54 | IoUtils.copy(in, out); 55 | IoUtils.closeSilently(in); 56 | IoUtils.closeSilently(out); 57 | return out.toByteArray(); 58 | } 59 | 60 | public static File getTempFile(File file) { 61 | return new File(file.getParentFile(), file.getName() + ".download"); 62 | } 63 | 64 | public static File newCacheFile() { 65 | return new File(RuntimeEnvironment.application.getCacheDir(), UUID.randomUUID().toString()); 66 | } 67 | 68 | public static byte[] generate(int capacity) { 69 | Random random = new Random(System.currentTimeMillis()); 70 | byte[] result = new byte[capacity]; 71 | random.nextBytes(result); 72 | return result; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /test/src/test/java/com/danikula/videocache/support/Response.java: -------------------------------------------------------------------------------- 1 | package com.danikula.videocache.support; 2 | 3 | import com.google.common.io.ByteStreams; 4 | 5 | import java.io.IOException; 6 | import java.net.HttpURLConnection; 7 | import java.util.List; 8 | import java.util.Map; 9 | 10 | public class Response { 11 | 12 | public final int code; 13 | public final byte[] data; 14 | public final int contentLength; 15 | public final String contentType; 16 | public final Map> headers; 17 | 18 | public Response(HttpURLConnection connection) throws IOException { 19 | this.code = connection.getResponseCode(); 20 | this.contentLength = connection.getContentLength(); 21 | this.contentType = connection.getContentType(); 22 | this.headers = connection.getHeaderFields(); 23 | this.data = ByteStreams.toByteArray(connection.getInputStream()); 24 | } 25 | } --------------------------------------------------------------------------------