├── .gitignore ├── .gitmodules ├── .travis.yml ├── LICENSE ├── README.md ├── build.gradle ├── example ├── build.gradle └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── ffmpegtest │ │ ├── AppConstants.java │ │ ├── MainActivity.java │ │ ├── UserPreferences.java │ │ ├── VideoActivity.java │ │ └── adapter │ │ ├── ItemsAdapter.java │ │ └── VideoItem.java │ └── res │ ├── layout │ ├── main_activity.xml │ ├── main_list_item.xml │ └── video_surfaceview.xml │ ├── menu │ └── main_activity.xml │ ├── mipmap-hdpi │ └── ic_launcher.png │ ├── mipmap-mdpi │ └── ic_launcher.png │ ├── mipmap-xhdpi │ └── ic_launcher.png │ ├── mipmap-xxhdpi │ └── ic_launcher.png │ ├── mipmap-xxxhdpi │ └── ic_launcher.png │ └── values │ ├── constans.xml │ └── strings.xml ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── library-jni └── jni │ ├── Android-tropicssl.mk │ ├── Android.mk │ ├── Application.mk │ ├── aes-protocol.c │ ├── aes-protocol.h │ ├── android-ndk-profiler-3.1 │ ├── android-ndk-profiler.mk │ ├── armeabi-v7a │ │ └── libandprof.a │ ├── armeabi │ │ └── libandprof.a │ └── prof.h │ ├── blend.c │ ├── blend.h │ ├── build_android.sh │ ├── convert.cpp │ ├── convert.h │ ├── fetch.sh │ ├── ffmpeg-jni.c │ ├── helpers.c │ ├── helpers.h │ ├── jni-protocol.c │ ├── jni-protocol.h │ ├── nativetester-jni.c │ ├── nativetester.c │ ├── nativetester.h │ ├── player.c │ ├── player.h │ ├── queue.c │ ├── queue.h │ └── sync.h ├── library ├── build.gradle ├── lint.xml └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── appunite │ │ └── ffmpeg │ │ ├── FFmpegDisplay.java │ │ ├── FFmpegError.java │ │ ├── FFmpegListener.java │ │ ├── FFmpegPlayer.java │ │ ├── FFmpegStreamInfo.java │ │ ├── FFmpegSurfaceView.java │ │ ├── FpsCounter.java │ │ ├── JniReader.java │ │ ├── NativeTester.java │ │ ├── NotPlayingException.java │ │ ├── SeekerView.java │ │ └── ViewCompat.java │ ├── jniLibs │ └── res │ └── values │ ├── attrs.xml │ └── styles.xml └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | #Specific 2 | library-jni/libs/ 3 | library-jni/obj/ 4 | library-jni/jni/ffmpeg-build/ 5 | 6 | #IntelliJ IDEA 7 | .idea 8 | *.iml 9 | *.ipr 10 | *.iws 11 | classes 12 | gen-external-apklibs 13 | 14 | #Gradle 15 | .gradle 16 | build 17 | local.properties 18 | 19 | #Other 20 | .DS_Store 21 | tmp 22 | *.swp 23 | *.tmp 24 | *.bak 25 | *.swp 26 | *.launch 27 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "library-jni/jni/ffmpeg"] 2 | path = library-jni/jni/ffmpeg 3 | url = git://source.ffmpeg.org/ffmpeg.git 4 | [submodule "library-jni/jni/vo-aacenc"] 5 | path = library-jni/jni/vo-aacenc 6 | url = git://git.code.sf.net/p/opencore-amr/vo-aacenc 7 | [submodule "library-jni/jni/vo-amrwbenc"] 8 | path = library-jni/jni/vo-amrwbenc 9 | url = git://git.code.sf.net/p/opencore-amr/vo-amrwbenc 10 | [submodule "library-jni/jni/x264"] 11 | path = library-jni/jni/x264 12 | url = git://git.videolan.org/x264.git 13 | [submodule "library-jni/jni/tropicssl"] 14 | path = library-jni/jni/tropicssl 15 | url = https://github.com/appunite/tropicssl.git 16 | [submodule "library-jni/jni/libass"] 17 | path = library-jni/jni/libass 18 | url = https://github.com/libass/libass.git 19 | [submodule "library-jni/jni/fribidi"] 20 | path = library-jni/jni/fribidi 21 | url = https://github.com/appunite/fribidi.git 22 | [submodule "library-jni/jni/freetype2"] 23 | path = library-jni/jni/freetype2 24 | url = git://git.sv.gnu.org/freetype/freetype2.git 25 | [submodule "library-jni/jni/libyuv"] 26 | path = library-jni/jni/libyuv 27 | url = https://chromium.googlesource.com/external/libyuv 28 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: android 2 | jdk: oraclejdk7 3 | sudo: false 4 | 5 | env: 6 | global: 7 | - NDK_VERSION=r10e 8 | 9 | addons: 10 | apt: 11 | packages: 12 | - libgd2-xpm 13 | - ia32-libs 14 | - ia32-libs-multiarch 15 | - yasm 16 | - pkg-config 17 | - make 18 | - autoconf 19 | - libtool 20 | - make 21 | - autoconf-archive 22 | - automake 23 | 24 | cache: 25 | directories: 26 | - $HOME/.gradle/caches 27 | 28 | android: 29 | components: 30 | - build-tools-22.0.1 31 | - android-15 32 | - extra-android-m2repository 33 | licenses: 34 | - 'android-sdk-preview-license-52d11cd2' 35 | - 'android-sdk-license-.+' 36 | - 'google-gdk-license-.+' 37 | 38 | before_install: 39 | # Android NDK 40 | - if [ `uname -m` = x86_64 ]; then wget http://dl.google.com/android/ndk/android-ndk-$NDK_VERSION-linux-x86_64.bin -O ndk.bin; else wget http://dl.google.com/android/ndk/android-ndk-$NDK_VERSION-linux-x86.bin -O ndk.bin; fi 41 | - chmod a+x ndk.bin 42 | - ./ndk.bin | egrep -v ^Extracting 43 | - rm ndk.bin 44 | - export ANDROID_NDK_HOME=`pwd`/android-ndk-$NDK_VERSION 45 | - export PATH=${PATH}:${ANDROID_HOME}/tools:${ANDROID_HOME}/platform-tools:${ANDROID_NDK_HOME} 46 | install: 47 | - sh -c 'cd library-jni/jni/ && ./fetch.sh' 48 | - sh -c 'cd library-jni/jni && ./build_android.sh' 49 | - sh -c 'cd library-jni/jni && ndk-build' 50 | - rm -rf $ANDROID_NDK_HOME 51 | - rm -rf library-jni/jni 52 | - TERM=dumb ./gradlew build 53 | 54 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AndroidFFmpegLibrary 2 | This project aims to create **working** library providing playing video files in android via ffmpeg libraries. With some effort and NDK knowledge you can use this ffmpeg libraries build to convert video files. 3 | We rather want to use ffmpeg library without modifications to facilitate updating of ffmpeg core. 4 | 5 | ![Application screenshot](http://s12.postimage.org/o528w8jst/Screenshot1.png) 6 | 7 | This project aim to simplify compilation of FFmpeg for android different architectures to one big apk file. 8 | 9 | I'm afraid this project is not prepared for android beginners - build it and using it requires some NDK skills. 10 | 11 | [![Build Status](https://travis-ci.org/appunite/AndroidFFmpeg.svg?branch=master)](https://travis-ci.org/appunite/AndroidFFmpeg) 12 | 13 | ## License 14 | Copyright (C) 2012 Appunite.com 15 | Licensed under the Apache License, Verision 2.0 16 | 17 | FFmpeg, libvo-aacenc, vo-amrwbenc, libyuv and others libraries projects are distributed on theirs own license. 18 | 19 | ## Patent disclaimer 20 | We do not grant of patent rights. 21 | Some codecs use patented techniques and before use those parts of library you have to buy thrid-party patents. 22 | 23 | ## Pre-requirments 24 | on mac: you have to install xcode and command tools from xcode preferences 25 | you have to install (on mac you can use brew command from homebrew): 26 | you have to install: 27 | - autoconf 28 | - libtool 29 | - make 30 | - autoconf-archive 31 | - automake 32 | - pkg-config 33 | - git 34 | 35 | on Debian/Ubuntu - you can use apt-get 36 | 37 | on Mac - you can use tool brew from homebrew project. You have additionally install xcode. 38 | 39 | ## Bug reporting and questions 40 | 41 | **Please read instruciton very carefully**. A lot of people had trouble because they did not read this manual with attention. **If you have some problems or questions do not send me emails**. First: look on past issues on github. Than: try figure out problem with google. If you did not find solution then you can ask on github issue tracker. 42 | 43 | ## Installation 44 | 45 | ### Go to the work 46 | downloading source code 47 | 48 | git clone --recurse-submodules https://github.com/appunite/AndroidFFmpeg.git AndroidFFmpeg 49 | cd AndroidFFmpeg 50 | git submodule init 51 | git submodule sync #if you are updating source code 52 | git submodule update 53 | cd library-jni 54 | cd jni 55 | 56 | download libyuv and configure libs 57 | 58 | ./fetch.sh 59 | 60 | build external libraries 61 | Download r8e ndk: https://dl.google.com/android/ndk/android-ndk-r8e-darwin-x86_64.tar.bz2 or 62 | ttps://dl.google.com/android/ndk/android-ndk-r8e-linux-x86_64.tar.bz2 63 | Now it should also support r10e 64 | 65 | export ANDROID_NDK_HOME=/your/path/to/android-ndk 66 | ./build_android.sh 67 | 68 | make sure that files library-jni/jni/ffmpeg-build/{armeabi,armeabi-v7a,x86}/libffmpeg.so was created, otherwise you are in truble 69 | 70 | 71 | build ndk jni library (in `library-jni` directory) 72 | 73 | export PATH="${PATH}:${ANDROID_NDK_HOME}" 74 | ndk-build 75 | 76 | make sure that files library-jni/libs/{armeabi,armeabi-v7a,x86}/libffmpeg.so was created, otherwise you are in truble 77 | 78 | build your project 79 | 80 | ./gradlew build 81 | 82 | ## More codecs 83 | If you need more codecs: 84 | - edit build_android.sh 85 | - add more codecs in ffmpeg configuration section 86 | - remove old ffmpeg-build directory by 87 | 88 | rm -r ffmpeg-build 89 | 90 | - build ffmpeg end supporting libraries 91 | 92 | ./build_android.sh 93 | 94 | During this process make sure that ffmpeg configuration goes without error. 95 | 96 | After build make sure that files FFmpegLibrary/jni/ffmpeg-build/{armeabi,armeabi-v7a,x86}/libffmpeg.so was created, otherwise you are in truble 97 | 98 | - build your ndk library 99 | 100 | ndk-build 101 | 102 | - refresh your FFmpegLibrary project in eclipse!!!! 103 | - build your FFmpegExample project 104 | 105 | 106 | ## Credits 107 | Library made by Jacek Marchwicki from Appunite.com 108 | 109 | - Thanks to Martin Böhme for writing tutorial: http://www.inb.uni-luebeck.de/~boehme/libavcodec_update.html 110 | - Thanks to Stephen Dranger for writing tutorial: http://dranger.com/ffmpeg/ 111 | - Thanks to Liu Feipeng for writing blog: http://www.roman10.net/how-to-port-ffmpeg-the-program-to-androidideas-and-thoughts/ 112 | - Thanks to ffmpeg team for writing cool stuff http://ffmpeg.org 113 | - Thanks to Alvaro for writing blog: http://odroid.foros-phpbb.com/t338-ffmpeg-compiled-with-android-ndk 114 | - Thanks to android-fplayer for sample code: http://code.google.com/p/android-fplayer/ 115 | - Thanks to best-video-player for sample code: http://code.google.com/p/best-video-player/ 116 | - Thanks to Robin Watts for his work in yuv2rgb converter http://wss.co.uk/pinknoise/yuv2rgb/ 117 | - Thanks to Mohamed Naufal (https://github.com/hexene) and Martin Storsjö (https://github.com/mstorsjo) for theirs work on sample code for stagefright/openmax integration layer. 118 | - Thanks www.fourcc.org for theirs http://www.fourcc.org/yuv.php page 119 | - Thanks to Cedric Fungfor his blog bost: http://vec.io/posts/use-android-hardware-decoder-with-omxcodec-in-ndk 120 | - Thanks Google/Google chrome/Chromium teams for libyuv library https://code.google.com/p/libyuv/ 121 | - Thanks to Picker Wengs for this slides about android multimedia stack http://www.slideshare.net/pickerweng/android-multimedia-framework 122 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | buildscript { 3 | repositories { 4 | jcenter() 5 | } 6 | dependencies { 7 | classpath 'com.android.tools.build:gradle:1.2.3' 8 | } 9 | } 10 | 11 | allprojects { 12 | repositories { 13 | jcenter() 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /example/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | dependencies { 4 | compile 'com.android.support:support-v4:22.1.1' 5 | compile "javax.annotation:javax.annotation-api:1.2" 6 | compile "com.google.code.findbugs:jsr305:2.0.1" 7 | 8 | compile(project(":library")) 9 | } 10 | 11 | android { 12 | compileSdkVersion 15 13 | buildToolsVersion "22.0.1" 14 | 15 | defaultConfig { 16 | minSdkVersion 9 17 | targetSdkVersion 17 18 | } 19 | 20 | buildTypes { 21 | release { 22 | minifyEnabled false 23 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt' 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /example/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 10 | 11 | 12 | 13 | 14 | 15 | 20 | 21 | 22 | 27 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | -------------------------------------------------------------------------------- /example/src/main/java/com/ffmpegtest/AppConstants.java: -------------------------------------------------------------------------------- 1 | package com.ffmpegtest; 2 | 3 | public class AppConstants { 4 | public static final String VIDEO_PLAY_ACTION = "com.ffmpegtest.VIDEO_PLAY_ACTION"; 5 | public static final String VIDEO_PLAY_ACTION_EXTRA_URL = "url"; 6 | public static final String VIDEO_PLAY_ACTION_EXTRA_ENCRYPTION_KEY = "encryption_key"; 7 | } 8 | -------------------------------------------------------------------------------- /example/src/main/java/com/ffmpegtest/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.ffmpegtest; 2 | 3 | import java.io.File; 4 | import java.util.ArrayList; 5 | import java.util.List; 6 | 7 | import android.app.Activity; 8 | import android.content.Intent; 9 | import android.os.Bundle; 10 | import android.os.Environment; 11 | import android.support.annotation.NonNull; 12 | import android.view.LayoutInflater; 13 | import android.view.Menu; 14 | import android.view.View; 15 | import android.widget.AdapterView; 16 | import android.widget.AdapterView.OnItemClickListener; 17 | import android.widget.EditText; 18 | import android.widget.ListView; 19 | 20 | import com.ffmpegtest.adapter.ItemsAdapter; 21 | import com.ffmpegtest.adapter.VideoItem; 22 | 23 | public class MainActivity extends Activity implements OnItemClickListener { 24 | 25 | private ItemsAdapter adapter; 26 | 27 | @Override 28 | protected void onCreate(Bundle savedInstanceState) { 29 | super.onCreate(savedInstanceState); 30 | setContentView(R.layout.main_activity); 31 | 32 | final ListView listView = (ListView) findViewById(R.id.main_activity_list); 33 | final EditText editText = (EditText) findViewById(R.id.main_activity_video_url); 34 | final View button = findViewById(R.id.main_activity_play_button); 35 | 36 | final UserPreferences userPreferences = new UserPreferences(this); 37 | if (savedInstanceState == null) { 38 | editText.setText(userPreferences.getUrl()); 39 | } 40 | adapter = new ItemsAdapter(LayoutInflater.from(this)); 41 | adapter.swapItems(getVideoItems()); 42 | 43 | listView.setAdapter(adapter); 44 | listView.setOnItemClickListener(this); 45 | 46 | button.setOnClickListener(new View.OnClickListener() { 47 | @Override 48 | public void onClick(View v) { 49 | final String url = String.valueOf(editText.getText()); 50 | playVideo(url); 51 | userPreferences.setUrl(url); 52 | } 53 | }); 54 | } 55 | 56 | private void playVideo(String url) { 57 | final Intent intent = new Intent(AppConstants.VIDEO_PLAY_ACTION) 58 | .putExtra(AppConstants.VIDEO_PLAY_ACTION_EXTRA_URL, url); 59 | startActivity(intent); 60 | } 61 | 62 | @NonNull 63 | private List getVideoItems() { 64 | final List items = new ArrayList(); 65 | items.add(new VideoItem( 66 | items.size(), 67 | "\"localfile.mp4\" on sdcard", 68 | getSDCardFile("localfile.mp4"), 69 | null)); 70 | items.add(new VideoItem( 71 | items.size(), 72 | "Apple sample", 73 | "http://devimages.apple.com.edgekey.net/resources/http-streaming/examples/bipbop_4x3/bipbop_4x3_variant.m3u8", 74 | null)); 75 | items.add(new VideoItem( 76 | items.size(), 77 | "Apple advenced sample", 78 | "https://devimages.apple.com.edgekey.net/resources/http-streaming/examples/bipbop_16x9/bipbop_16x9_variant.m3u8", 79 | null)); 80 | items.add(new VideoItem( 81 | items.size(), 82 | "IP camera", 83 | "rtsp://ip.appunite-local.net:554", 84 | null)); 85 | return items; 86 | } 87 | 88 | private static String getSDCardFile(String file) { 89 | File videoFile = new File(Environment.getExternalStorageDirectory(), 90 | file); 91 | return "file://" + videoFile.getAbsolutePath(); 92 | } 93 | 94 | @Override 95 | public boolean onCreateOptionsMenu(Menu menu) { 96 | getMenuInflater().inflate(R.menu.main_activity, menu); 97 | return true; 98 | } 99 | 100 | @Override 101 | public void onItemClick(AdapterView listView, View view, int position, long id) { 102 | final VideoItem videoItem = adapter.getItem(position); 103 | final Intent intent = new Intent(AppConstants.VIDEO_PLAY_ACTION) 104 | .putExtra(AppConstants.VIDEO_PLAY_ACTION_EXTRA_URL, videoItem.video()) 105 | .putExtra(AppConstants.VIDEO_PLAY_ACTION_EXTRA_ENCRYPTION_KEY, videoItem.video()); 106 | startActivity(intent); 107 | } 108 | 109 | } 110 | -------------------------------------------------------------------------------- /example/src/main/java/com/ffmpegtest/UserPreferences.java: -------------------------------------------------------------------------------- 1 | package com.ffmpegtest; 2 | 3 | import android.content.Context; 4 | import android.content.SharedPreferences; 5 | import android.support.annotation.Nullable; 6 | 7 | import javax.annotation.Nonnull; 8 | 9 | public class UserPreferences { 10 | 11 | public static final String USER_PREFERENCES = "USER_PREFERENCES"; 12 | private static final String KEY_URL = "url"; 13 | private final SharedPreferences preferences; 14 | 15 | public UserPreferences(@Nonnull Context context) { 16 | preferences = context.getSharedPreferences(USER_PREFERENCES, 0); 17 | } 18 | 19 | public void setUrl(@Nullable String url) { 20 | preferences.edit().putString(KEY_URL, url).apply(); 21 | } 22 | 23 | @Nullable 24 | public String getUrl() { 25 | return preferences.getString(KEY_URL, "rtsp://ip.appunite-local.net:554"); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /example/src/main/java/com/ffmpegtest/VideoActivity.java: -------------------------------------------------------------------------------- 1 | /* 2 | * MainActivity.java 3 | * Copyright (c) 2012 Jacek Marchwicki 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | * 17 | */ 18 | 19 | package com.ffmpegtest; 20 | 21 | import java.io.File; 22 | import java.util.HashMap; 23 | import java.util.Locale; 24 | 25 | import android.annotation.TargetApi; 26 | import android.app.Activity; 27 | import android.app.AlertDialog; 28 | import android.app.AlertDialog.Builder; 29 | import android.content.DialogInterface; 30 | import android.content.Intent; 31 | import android.content.pm.ActivityInfo; 32 | import android.database.Cursor; 33 | import android.database.MatrixCursor; 34 | import android.graphics.PixelFormat; 35 | import android.net.Uri; 36 | import android.os.Build; 37 | import android.os.Bundle; 38 | import android.os.Environment; 39 | import android.provider.BaseColumns; 40 | import android.support.v4.widget.SimpleCursorAdapter; 41 | import android.view.View; 42 | import android.view.View.OnClickListener; 43 | import android.view.Window; 44 | import android.view.WindowManager; 45 | import android.widget.AdapterView; 46 | import android.widget.AdapterView.OnItemSelectedListener; 47 | import android.widget.Button; 48 | import android.widget.SeekBar; 49 | import android.widget.SeekBar.OnSeekBarChangeListener; 50 | import android.widget.Spinner; 51 | 52 | import com.appunite.ffmpeg.FFmpegDisplay; 53 | import com.appunite.ffmpeg.FFmpegError; 54 | import com.appunite.ffmpeg.FFmpegListener; 55 | import com.appunite.ffmpeg.FFmpegPlayer; 56 | import com.appunite.ffmpeg.FFmpegStreamInfo; 57 | import com.appunite.ffmpeg.FFmpegStreamInfo.CodecType; 58 | import com.appunite.ffmpeg.NotPlayingException; 59 | 60 | public class VideoActivity extends Activity implements OnClickListener, 61 | FFmpegListener, OnSeekBarChangeListener, OnItemSelectedListener { 62 | 63 | private static final String[] PROJECTION = new String[] {"title", BaseColumns._ID}; 64 | private static final int PROJECTION_ID = 1; 65 | 66 | private FFmpegPlayer mMpegPlayer; 67 | protected boolean mPlay = false; 68 | private View mControlsView; 69 | private View mLoadingView; 70 | private SeekBar mSeekBar; 71 | private View mVideoView; 72 | private Button mPlayPauseButton; 73 | private boolean mTracking = false; 74 | private View mStreamsView; 75 | private Spinner mLanguageSpinner; 76 | private int mLanguageSpinnerSelectedPosition = 0; 77 | private Spinner mSubtitleSpinner; 78 | private int mSubtitleSpinnerSelectedPosition = 0; 79 | private SimpleCursorAdapter mLanguageAdapter; 80 | private SimpleCursorAdapter mSubtitleAdapter; 81 | 82 | private int mAudioStreamNo = FFmpegPlayer.UNKNOWN_STREAM; 83 | private int mSubtitleStreamNo = FFmpegPlayer.NO_STREAM; 84 | private View mScaleButton; 85 | private long mCurrentTimeUs; 86 | 87 | @Override 88 | public void onCreate(Bundle savedInstanceState) { 89 | this.getWindow().requestFeature(Window.FEATURE_NO_TITLE); 90 | getWindow().setFormat(PixelFormat.RGBA_8888); 91 | getWindow().clearFlags(WindowManager.LayoutParams.FLAG_DITHER); 92 | 93 | super.onCreate(savedInstanceState); 94 | 95 | this.getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); 96 | this.getWindow().clearFlags( 97 | WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN); 98 | this.getWindow().setBackgroundDrawable(null); 99 | 100 | this.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE); 101 | 102 | this.setContentView(R.layout.video_surfaceview); 103 | 104 | mSeekBar = (SeekBar) this.findViewById(R.id.seek_bar); 105 | mSeekBar.setOnSeekBarChangeListener(this); 106 | 107 | mPlayPauseButton = (Button) this.findViewById(R.id.play_pause); 108 | mPlayPauseButton.setOnClickListener(this); 109 | 110 | mScaleButton = this.findViewById(R.id.scale_type); 111 | mScaleButton.setOnClickListener(this); 112 | 113 | mControlsView = this.findViewById(R.id.controls); 114 | mStreamsView = this.findViewById(R.id.streams); 115 | mLoadingView = this.findViewById(R.id.loading_view); 116 | mLanguageSpinner = (Spinner) this.findViewById(R.id.language_spinner); 117 | mSubtitleSpinner = (Spinner) this.findViewById(R.id.subtitle_spinner); 118 | 119 | mLanguageAdapter = new SimpleCursorAdapter(this, 120 | android.R.layout.simple_spinner_item, null, PROJECTION, 121 | new int[] { android.R.id.text1 }, 0); 122 | mLanguageAdapter 123 | .setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); 124 | 125 | mLanguageSpinner.setAdapter(mLanguageAdapter); 126 | mLanguageSpinner.setOnItemSelectedListener(this); 127 | 128 | mSubtitleAdapter = new SimpleCursorAdapter(this, 129 | android.R.layout.simple_spinner_item, null, PROJECTION, 130 | new int[] { android.R.id.text1 }, 0); 131 | mSubtitleAdapter 132 | .setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); 133 | 134 | mSubtitleSpinner.setAdapter(mSubtitleAdapter); 135 | mSubtitleSpinner.setOnItemSelectedListener(this); 136 | 137 | mVideoView = this.findViewById(R.id.video_view); 138 | mMpegPlayer = new FFmpegPlayer((FFmpegDisplay) mVideoView, this); 139 | mMpegPlayer.setMpegListener(this); 140 | setDataSource(); 141 | } 142 | 143 | @Override 144 | protected void onPause() { 145 | super.onPause(); 146 | }; 147 | 148 | @Override 149 | protected void onResume() { 150 | super.onResume(); 151 | } 152 | 153 | @Override 154 | protected void onDestroy() { 155 | super.onDestroy(); 156 | this.mMpegPlayer.setMpegListener(null); 157 | this.mMpegPlayer.stop(); 158 | stop(); 159 | } 160 | 161 | private void setDataSource() { 162 | HashMap params = new HashMap(); 163 | 164 | // set font for ass 165 | File assFont = new File(Environment.getExternalStorageDirectory(), 166 | "DroidSansFallback.ttf"); 167 | params.put("ass_default_font_path", assFont.getAbsolutePath()); 168 | 169 | Intent intent = getIntent(); 170 | Uri uri = intent.getData(); 171 | String url; 172 | if (uri != null) { 173 | url = uri.toString(); 174 | } else { 175 | url = intent 176 | .getStringExtra(AppConstants.VIDEO_PLAY_ACTION_EXTRA_URL); 177 | if (url == null) { 178 | throw new IllegalArgumentException(String.format( 179 | "\"%s\" did not provided", 180 | AppConstants.VIDEO_PLAY_ACTION_EXTRA_URL)); 181 | } 182 | if (intent 183 | .hasExtra(AppConstants.VIDEO_PLAY_ACTION_EXTRA_ENCRYPTION_KEY)) { 184 | params.put( 185 | "aeskey", 186 | intent.getStringExtra(AppConstants.VIDEO_PLAY_ACTION_EXTRA_ENCRYPTION_KEY)); 187 | } 188 | } 189 | 190 | this.mPlayPauseButton 191 | .setBackgroundResource(android.R.drawable.ic_media_play); 192 | this.mPlayPauseButton.setEnabled(true); 193 | mPlay = false; 194 | 195 | mMpegPlayer.setDataSource(url, params, FFmpegPlayer.UNKNOWN_STREAM, mAudioStreamNo, 196 | mSubtitleStreamNo); 197 | 198 | } 199 | 200 | @Override 201 | public void onClick(View v) { 202 | int viewId = v.getId(); 203 | switch (viewId) { 204 | case R.id.play_pause: 205 | resumePause(); 206 | return; 207 | case R.id.scale_type: 208 | return; 209 | default: 210 | throw new RuntimeException(); 211 | } 212 | } 213 | 214 | @Override 215 | public void onFFUpdateTime(long currentTimeUs, long videoDurationUs, boolean isFinished) { 216 | mCurrentTimeUs = currentTimeUs; 217 | if (!mTracking) { 218 | int currentTimeS = (int)(currentTimeUs / 1000 / 1000); 219 | int videoDurationS = (int)(videoDurationUs / 1000 / 1000); 220 | mSeekBar.setMax(videoDurationS); 221 | mSeekBar.setProgress(currentTimeS); 222 | } 223 | 224 | if (isFinished) { 225 | new AlertDialog.Builder(this) 226 | .setTitle(R.string.dialog_end_of_video_title) 227 | .setMessage(R.string.dialog_end_of_video_message) 228 | .setCancelable(true).show(); 229 | } 230 | } 231 | 232 | @Override 233 | public void onFFDataSourceLoaded(FFmpegError err, FFmpegStreamInfo[] streams) { 234 | if (err != null) { 235 | String format = getResources().getString( 236 | R.string.main_could_not_open_stream); 237 | String message = String.format(format, err.getMessage()); 238 | 239 | Builder builder = new AlertDialog.Builder(VideoActivity.this); 240 | builder.setTitle(R.string.app_name) 241 | .setMessage(message) 242 | .setOnCancelListener( 243 | new DialogInterface.OnCancelListener() { 244 | 245 | @Override 246 | public void onCancel(DialogInterface dialog) { 247 | VideoActivity.this.finish(); 248 | } 249 | }).show(); 250 | return; 251 | } 252 | mPlayPauseButton.setBackgroundResource(android.R.drawable.ic_media_play); 253 | mPlayPauseButton.setEnabled(true); 254 | this.mControlsView.setVisibility(View.VISIBLE); 255 | this.mStreamsView.setVisibility(View.VISIBLE); 256 | this.mLoadingView.setVisibility(View.GONE); 257 | MatrixCursor audio = new MatrixCursor(PROJECTION); 258 | MatrixCursor subtitles = new MatrixCursor(PROJECTION); 259 | subtitles.addRow(new Object[] {"None", FFmpegPlayer.NO_STREAM}); 260 | for (FFmpegStreamInfo streamInfo : streams) { 261 | CodecType mediaType = streamInfo.getMediaType(); 262 | Locale locale = streamInfo.getLanguage(); 263 | String languageName = locale == null ? getString( 264 | R.string.unknown) : locale.getDisplayLanguage(); 265 | if (FFmpegStreamInfo.CodecType.AUDIO.equals(mediaType)) { 266 | audio.addRow(new Object[] {languageName, streamInfo.getStreamNumber()}); 267 | } else if (FFmpegStreamInfo.CodecType.SUBTITLE.equals(mediaType)) { 268 | subtitles.addRow(new Object[] {languageName, streamInfo.getStreamNumber()}); 269 | } 270 | } 271 | mLanguageAdapter.swapCursor(audio); 272 | mSubtitleAdapter.swapCursor(subtitles); 273 | } 274 | 275 | private void displaySystemMenu(boolean visible) { 276 | if (Build.VERSION.SDK_INT >= 14) { 277 | displaySystemMenu14(visible); 278 | } else if (Build.VERSION.SDK_INT >= 11) { 279 | displaySystemMenu11(visible); 280 | } 281 | } 282 | 283 | @SuppressWarnings("deprecation") 284 | @TargetApi(11) 285 | private void displaySystemMenu11(boolean visible) { 286 | if (visible) { 287 | this.mVideoView.setSystemUiVisibility(View.STATUS_BAR_VISIBLE); 288 | } else { 289 | this.mVideoView.setSystemUiVisibility(View.STATUS_BAR_HIDDEN); 290 | } 291 | } 292 | 293 | @TargetApi(14) 294 | private void displaySystemMenu14(boolean visible) { 295 | if (visible) { 296 | this.mVideoView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_VISIBLE); 297 | } else { 298 | this.mVideoView 299 | .setSystemUiVisibility(View.SYSTEM_UI_FLAG_LOW_PROFILE); 300 | } 301 | } 302 | 303 | public void resumePause() { 304 | this.mPlayPauseButton.setEnabled(false); 305 | if (mPlay) { 306 | mMpegPlayer.pause(); 307 | } else { 308 | mMpegPlayer.resume(); 309 | displaySystemMenu(true); 310 | } 311 | mPlay = !mPlay; 312 | } 313 | 314 | @Override 315 | public void onFFResume(NotPlayingException result) { 316 | this.mPlayPauseButton 317 | .setBackgroundResource(android.R.drawable.ic_media_pause); 318 | this.mPlayPauseButton.setEnabled(true); 319 | 320 | displaySystemMenu(false); 321 | mPlay = true; 322 | } 323 | 324 | @Override 325 | public void onFFPause(NotPlayingException err) { 326 | this.mPlayPauseButton 327 | .setBackgroundResource(android.R.drawable.ic_media_play); 328 | this.mPlayPauseButton.setEnabled(true); 329 | mPlay = false; 330 | } 331 | 332 | private void stop() { 333 | this.mControlsView.setVisibility(View.GONE); 334 | this.mStreamsView.setVisibility(View.GONE); 335 | this.mLoadingView.setVisibility(View.VISIBLE); 336 | } 337 | 338 | @Override 339 | public void onFFStop() { 340 | } 341 | 342 | @Override 343 | public void onFFSeeked(NotPlayingException result) { 344 | // if (result != null) 345 | // throw new RuntimeException(result); 346 | } 347 | 348 | @Override 349 | public void onProgressChanged(SeekBar seekBar, int progress, 350 | boolean fromUser) { 351 | if (fromUser) { 352 | long timeUs = progress * 1000 * 1000; 353 | mMpegPlayer.seek(timeUs); 354 | } 355 | } 356 | 357 | @Override 358 | public void onStartTrackingTouch(SeekBar seekBar) { 359 | mTracking = true; 360 | } 361 | 362 | @Override 363 | public void onStopTrackingTouch(SeekBar seekBar) { 364 | mTracking = false; 365 | } 366 | 367 | private void setDataSourceAndResumeState() { 368 | setDataSource(); 369 | mMpegPlayer.seek(mCurrentTimeUs); 370 | mMpegPlayer.resume(); 371 | } 372 | 373 | @Override 374 | public void onItemSelected(AdapterView parentView, 375 | View selectedItemView, int position, long id) { 376 | Cursor c = (Cursor) parentView 377 | .getItemAtPosition(position); 378 | if (parentView == mLanguageSpinner) { 379 | if (mLanguageSpinnerSelectedPosition != position) { 380 | mLanguageSpinnerSelectedPosition = position; 381 | mAudioStreamNo = c.getInt(PROJECTION_ID); 382 | setDataSourceAndResumeState(); 383 | } 384 | } else if (parentView == mSubtitleSpinner) { 385 | if (mSubtitleSpinnerSelectedPosition != position) { 386 | mSubtitleSpinnerSelectedPosition = position; 387 | mSubtitleStreamNo = c.getInt(PROJECTION_ID); 388 | setDataSourceAndResumeState(); 389 | } 390 | } else { 391 | throw new RuntimeException(); 392 | } 393 | } 394 | 395 | @Override 396 | public void onNothingSelected(AdapterView parentView) { 397 | // if (parentView == languageSpinner) { 398 | // audioStream = null; 399 | // } else if (parentView == subtitleSpinner) { 400 | // subtitleStream = null; 401 | // } else { 402 | // throw new RuntimeException(); 403 | // } 404 | // play(); 405 | } 406 | 407 | } 408 | -------------------------------------------------------------------------------- /example/src/main/java/com/ffmpegtest/adapter/ItemsAdapter.java: -------------------------------------------------------------------------------- 1 | package com.ffmpegtest.adapter; 2 | 3 | import android.view.LayoutInflater; 4 | import android.view.View; 5 | import android.view.ViewGroup; 6 | import android.widget.BaseAdapter; 7 | import android.widget.TextView; 8 | 9 | import com.ffmpegtest.R; 10 | import com.ffmpegtest.adapter.VideoItem; 11 | 12 | import java.util.ArrayList; 13 | import java.util.List; 14 | 15 | import javax.annotation.Nonnull; 16 | 17 | public class ItemsAdapter extends BaseAdapter { 18 | @Nonnull 19 | private final LayoutInflater inflater; 20 | private List videoItems = new ArrayList(); 21 | 22 | public static class ViewHolder { 23 | 24 | @Nonnull 25 | private final View view; 26 | @Nonnull 27 | private final TextView textView; 28 | 29 | public static ViewHolder fromConvertView(@Nonnull View convertView) { 30 | return (ViewHolder) convertView.getTag(); 31 | } 32 | 33 | public ViewHolder(@Nonnull LayoutInflater inflater, @Nonnull ViewGroup parent) { 34 | view = inflater.inflate(R.layout.main_list_item, parent, false); 35 | textView = (TextView) view.findViewById(R.id.main_list_item_text); 36 | view.setTag(this); 37 | } 38 | 39 | @Nonnull 40 | public View getView() { 41 | return view; 42 | } 43 | 44 | public void bind(@Nonnull VideoItem videoItem) { 45 | textView.setText(videoItem.text()); 46 | } 47 | } 48 | 49 | public ItemsAdapter(@Nonnull LayoutInflater inflater) { 50 | this.inflater = inflater; 51 | } 52 | 53 | @Override 54 | public int getCount() { 55 | return videoItems.size(); 56 | } 57 | 58 | @Override 59 | public VideoItem getItem(int position) { 60 | return videoItems.get(position); 61 | } 62 | 63 | @Override 64 | public long getItemId(int position) { 65 | return videoItems.get(position).id(); 66 | } 67 | 68 | @Override 69 | public View getView(int position, View convertView, ViewGroup parent) { 70 | final ViewHolder holder; 71 | if (convertView == null) { 72 | holder = new ViewHolder(inflater, parent); 73 | convertView = holder.getView(); 74 | } else { 75 | holder = ViewHolder.fromConvertView(convertView); 76 | } 77 | holder.bind(videoItems.get(position)); 78 | return convertView; 79 | } 80 | 81 | public void swapItems(@Nonnull List items) { 82 | videoItems = items; 83 | notifyDataSetChanged(); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /example/src/main/java/com/ffmpegtest/adapter/VideoItem.java: -------------------------------------------------------------------------------- 1 | package com.ffmpegtest.adapter; 2 | 3 | import android.support.annotation.Nullable; 4 | 5 | import javax.annotation.Nonnull; 6 | 7 | public class VideoItem { 8 | private final long id; 9 | @Nullable 10 | private final String text; 11 | @Nonnull 12 | private String video; 13 | @Nullable 14 | private String key; 15 | 16 | public VideoItem(long id, 17 | @Nullable String text, 18 | @Nonnull String video, 19 | @Nullable String key) { 20 | this.id = id; 21 | this.text = text; 22 | this.video = video; 23 | this.key = key; 24 | } 25 | 26 | public long id() { 27 | return id; 28 | } 29 | 30 | @Nullable 31 | public String text() { 32 | return text; 33 | } 34 | 35 | @Nonnull 36 | public String video() { 37 | return video; 38 | } 39 | 40 | @Nullable 41 | public String key() { 42 | return key; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /example/src/main/res/layout/main_activity.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 15 | 16 | 20 | 28 |