├── .gitignore ├── README.md ├── app ├── .gitignore ├── CMakeLists.txt ├── build.gradle ├── libs │ └── FFmpegAndroid-0.3.2.aar ├── proguard-rules.pro └── src │ ├── Android视频滤镜添加硬解码方案.md │ ├── Camera滤镜的添加.md │ └── main │ ├── AndroidManifest.xml │ ├── assets │ └── test.mp4 │ ├── cpp │ └── Test.cpp │ ├── java │ └── com │ │ └── pinssible │ │ └── camerarecorder │ │ └── camerarecorderdemo │ │ ├── CameraCaptureActivity.java │ │ ├── FfmpegTestActivity.java │ │ ├── FunctionViewBuilder.java │ │ ├── MainActivity.java │ │ ├── PlayerActivity.java │ │ └── TestRemuxerActivity.java │ └── res │ ├── drawable │ └── ic_launcher_background.xml │ ├── layout │ ├── activity_camera.xml │ ├── activity_ffmpeg_test.xml │ ├── activity_main.xml │ ├── activity_player.xml │ ├── activity_remuxer.xml │ ├── layout_control.xml │ ├── layout_filter_item.xml │ ├── layout_function_item.xml │ └── layout_test_texture_view_output.xml │ ├── mipmap-anydpi-v26 │ ├── ic_launcher.xml │ └── ic_launcher_round.xml │ ├── mipmap-hdpi │ ├── ic_launcher.png │ ├── ic_launcher_foreground.png │ └── ic_launcher_round.png │ ├── mipmap-mdpi │ ├── ic_launcher.png │ ├── ic_launcher_foreground.png │ └── ic_launcher_round.png │ ├── mipmap-xhdpi │ ├── ic_launcher.png │ ├── ic_launcher_foreground.png │ └── ic_launcher_round.png │ ├── mipmap-xxhdpi │ ├── ic_launcher.png │ ├── ic_launcher_foreground.png │ ├── ic_launcher_round.png │ └── ic_video.png │ ├── mipmap-xxxhdpi │ ├── ic_launcher.png │ ├── ic_launcher_foreground.png │ └── ic_launcher_round.png │ └── values │ ├── colors.xml │ ├── strings.xml │ └── styles.xml ├── build.gradle ├── ezgif.com-optimize.gif ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── libRecorderEditor ├── .gitignore ├── README.md ├── build.gradle ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── pinssible │ │ └── librecorder │ │ ├── base │ │ ├── Drawable2d.java │ │ ├── EglCore.java │ │ ├── EglSurfaceBase.java │ │ ├── FullFrameRect.java │ │ ├── GlUtil.java │ │ ├── Viewport.java │ │ └── WindowSurface.java │ │ ├── camera │ │ └── CameraInstance.java │ │ ├── filter │ │ ├── Filters.java │ │ └── program │ │ │ ├── GpuImageProgram.java │ │ │ ├── LerpBlurGpuProgram.java │ │ │ ├── Program.java │ │ │ └── Texture2dProgram.java │ │ ├── gles │ │ ├── EGLLogWrapper.java │ │ ├── EglContextWrapper.java │ │ ├── EglHelper.java │ │ ├── EglHelperAPI17.java │ │ ├── EglHelperFactory.java │ │ ├── GLThread.java │ │ ├── IEglHelper.java │ │ └── RenderListener.java │ │ ├── listener │ │ ├── OnMuxFinishListener.java │ │ └── PreSettingListener.java │ │ ├── player │ │ ├── PinMediaPlayer.java │ │ ├── PlayerRender.java │ │ └── SimplePinPlayerView.java │ │ ├── recorder │ │ ├── AVRecorder.java │ │ ├── AndroidEncoder.java │ │ ├── AndroidMuxer.java │ │ ├── AudioEncoderCore.java │ │ ├── CameraSurfaceRender.java │ │ ├── MicrophoneEncoder.java │ │ ├── Muxer.java │ │ ├── PreviewConfig.java │ │ ├── PreviewState.java │ │ ├── RecorderConfig.java │ │ ├── VideoEncoder.java │ │ └── VideoEncoderCore2.java │ │ ├── remux │ │ ├── InputSurface.java │ │ ├── OutputSurface.java │ │ ├── PinRemuxer.java │ │ └── RemuxerFactory.java │ │ └── view │ │ └── GLTextureView.java │ └── res │ └── values │ └── strings.xml └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | # Built application files 2 | *.apk 3 | *.ap_ 4 | 5 | # Files for the ART/Dalvik VM 6 | *.dex 7 | 8 | # Java class files 9 | *.class 10 | 11 | # Generated files 12 | bin/ 13 | gen/ 14 | out/ 15 | 16 | # Gradle files 17 | .gradle/ 18 | build/ 19 | 20 | # Local configuration file (sdk path, etc) 21 | local.properties 22 | 23 | # Proguard folder generated by Eclipse 24 | proguard/ 25 | 26 | # Log Files 27 | *.log 28 | 29 | # Android Studio Navigation editor temp files 30 | .navigation/ 31 | 32 | # Android Studio captures folder 33 | captures/ 34 | 35 | # Intellij 36 | *.iml 37 | .idea 38 | 39 | 40 | # Keystore files 41 | *.jks 42 | 43 | # External native build folder generated in Android Studio 2.2 and later 44 | .externalNativeBuild 45 | 46 | # Google Services (e.g. APIs or Firebase) 47 | google-services.json 48 | 49 | # Freeline 50 | freeline.py 51 | freeline/ 52 | freeline_project_description.json 53 | 54 | 55 | #lib 56 | libRecorderEditor/.git 57 | libRecorderEditor/build 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PinFilter 2 | ![CocoaPods](https://img.shields.io/badge/Android%20-4.3%2B-brightgreen.svg) ![CircleCI](https://img.shields.io/circleci/project/github/RedSparr0w/node-csgo-parser.svg) 3 | @[Filter, Camera, Player, Recorder] 4 | 5 | Introduction 6 | ====== 7 | - PinFilter is a libary for android which can be used to add filter to mp4. 8 | - PinFilter is based on Android MediaCodec and MediaMuxer,All video decode or encode by hardware. 9 | - So you have to use this in the device on Android 4.3+. 10 | 11 | ## Function 12 | 1. Camera Preview With Filter 13 | 2. Capture Camera Preview With Filter ,And Generate MP4 14 | 3. Preview Video with Filter 15 | 4. Decode MP4 and Regenerate MP4 with Filter 16 | 17 | Preview 18 | ====== 19 | ![](ezgif.com-optimize.gif) 20 | 21 | Getting started 22 | ====== 23 | - In your ` build.gradle`: 24 | ``` 25 | dependencies { 26 | compile 'com.pinssible:pin-filter:0.0.2' 27 | } 28 | ``` 29 | - Or you can clone the project get the Lib `libRecorderEditor`,Import it to your project: 30 | ``` 31 | dependencies { 32 | compile project(path: ':libRecorderEditor') 33 | } 34 | ``` 35 | 36 | Usage 37 | ====== 38 | - reference demo project 39 | - ` CameraCaptureActivity` is for Camera preview and recoder with filter 40 | - ` PlayerActivity` is for video play with filter 41 | - ` TestRemuxerActivity` is for MP4 remux with filter 42 | 43 | - Camera Preview and recorder 44 | 45 | ``` 46 | private AVRecorder recorder; 47 | private RecorderConfig recorderConfig; 48 | private PreviewConfig previewConfig; 49 | private GLTextureView/GlSurfaceView preview; 50 | 51 | 52 | //recoder config 53 | private RecorderConfig createRecorderConfig() { 54 | String path = Environment.getExternalStorageDirectory().getAbsolutePath() + "/Neon"; 55 | String outputPath = path + "/NeonRecordTest_" + System.currentTimeMillis() + ".mp4"; 56 | FileUtils.createOrExistsDir(path); 57 | //file 58 | File mOutputFile = new File(outputPath); 59 | //setting 60 | RecorderConfig.VideoEncoderConfig videoConfig = new RecorderConfig.VideoEncoderConfig(480, 640, 61 | 5 * 1000 * 1000, EGL14.eglGetCurrentContext()); 62 | RecorderConfig.AudioEncoderConfig audioConfig = new RecorderConfig.AudioEncoderConfig(1, 96 * 1000, 44100); 63 | //finish callback 64 | OnMuxFinishListener listener = new OnMuxFinishListener() { 65 | @Override 66 | public void onMuxFinish() { 67 | //todo 68 | } 69 | 70 | @Override 71 | public void onMuxFail(Exception e) { 72 | //todo 73 | } 74 | }; 75 | return new RecorderConfig(videoConfig, audioConfig, 76 | mOutputFile, RecorderConfig.SCREEN_ROTATION.VERTICAL, listener); 77 | } 78 | 79 | 80 | //preview config 81 | private PreviewConfig createPreviewConfig() { 82 | return new PreviewConfig(480, 640); 83 | } 84 | 85 | 86 | //init preview 87 | recorder = new AVRecorder(previewConfig, recorderConfig, preview); 88 | 89 | 90 | //start recoder 91 | recorder.startRecording(); 92 | 93 | //stop recoder 94 | recorder.stopRecording(); 95 | 96 | //reset recoder for new recording 97 | recorder.reset(recorderConfig); 98 | 99 | //take shot 100 | recorder.takeShot(CallBack callback) 101 | 102 | //change filter 103 | recorder.setFilter(int filterType); 104 | ``` 105 | 106 | - Mp4 play with filter (Usage as exoPlayer) 107 | 108 | ``` 109 | private SimplePinPlayerView previewSurface; //a simple play view base on exoplayer SimpleExoPlayerView 110 | private PinMediaPlayer player; 111 | 112 | //init 113 | String outputPath = "file:///android_asset/test.mp4"; 114 | Uri source = Uri.parse(outputPath); 115 | player = new PinMediaPlayer(this, source, isloop); 116 | 117 | //change filter 118 | player.setFilter(int filterType); 119 | ``` 120 | 121 | - Remux mp4 with filter 122 | 123 | ``` 124 | private PinRemuxer remuxer; 125 | 126 | //start remuxer 127 | remuxer = new PinRemuxer(srcPath, dstPath, new OnRemuxListener remuxListener{ 128 | void onRemuxStart(long totalPts); 129 | 130 | void onRemuxProcess(long pts); 131 | 132 | void onRemuxFinish(); 133 | 134 | void onRemuxFail(Exception e); 135 | }); 136 | remuxer.start(int filterType); 137 | ``` 138 | 139 | Achieve 140 | ====== 141 | - The filter achieved by Opengl 142 | - The Player with filter is based on [ExoPlayer](https://github.com/google/ExoPlayer) 143 | - The Video Remux is based on Android test [ExtractDecodeEditEncodeMuxTest.java](https://android.googlesource.com/platform/cts/+/jb-mr2-release/tests/tests/media/src/android/media/cts/ExtractDecodeEditEncodeMuxTest.java.) -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # For more information about using CMake with Android Studio, read the 2 | # documentation: https://d.android.com/studio/projects/add-native-code.html 3 | 4 | # Sets the minimum version of CMake required to build the native library. 5 | 6 | cmake_minimum_required(VERSION 3.4.1) 7 | 8 | # Creates and names a library, sets it as either STATIC 9 | # or SHARED, and provides the relative paths to its source code. 10 | # You can define multiple libraries, and CMake builds them for you. 11 | # Gradle automatically packages shared libraries with your APK. 12 | 13 | add_library( # Sets the name of the library. 14 | ffmpeg 15 | 16 | # Sets the library as a shared library. 17 | SHARED 18 | 19 | # Provides a relative path to your source file(s). 20 | src/main/cpp/Test.cpp ) 21 | 22 | # Searches for a specified prebuilt library and stores the path as a 23 | # variable. Because CMake includes system libraries in the search path by 24 | # default, you only need to specify the name of the public NDK library 25 | # you want to add. CMake verifies that the library exists before 26 | # completing its build. 27 | 28 | find_library( # Sets the name of the path variable. 29 | log-lib 30 | 31 | # Specifies the name of the NDK library that 32 | # you want CMake to locate. 33 | log ) 34 | 35 | # Specifies libraries CMake should link to your target library. You 36 | # can link multiple libraries, such as libraries you define in this 37 | # build script, prebuilt third-party libraries, or system libraries. 38 | 39 | target_link_libraries( # Specifies the target library. 40 | ffmpeg 41 | 42 | # Links the target library to the log library 43 | # included in the NDK. 44 | ${log-lib} ) -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 26 5 | buildToolsVersion "26.0.2" 6 | defaultConfig { 7 | applicationId "com.pinssible.camerarecorder.camerarecorderdemo" 8 | minSdkVersion 18 9 | targetSdkVersion 23 10 | versionCode 1 11 | versionName "1.0" 12 | 13 | externalNativeBuild { 14 | cmake { 15 | cppFlags "" 16 | } 17 | } 18 | 19 | } 20 | 21 | externalNativeBuild { 22 | cmake { 23 | path "CMakeLists.txt" 24 | } 25 | } 26 | 27 | buildTypes { 28 | release { 29 | minifyEnabled false 30 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 31 | } 32 | } 33 | } 34 | 35 | 36 | repositories { 37 | flatDir { 38 | dirs 'libs' 39 | } 40 | } 41 | 42 | 43 | 44 | dependencies { 45 | implementation fileTree(dir: 'libs', include: ['*.jar']) 46 | implementation 'com.android.support:appcompat-v7:26.1.0' 47 | implementation 'com.android.support.constraint:constraint-layout:1.0.2' 48 | compile project(path: ':libRecorderEditor') 49 | 50 | //rxjava rxAndroid 51 | implementation "io.reactivex.rxjava2:rxjava:2.1.2" 52 | implementation "io.reactivex.rxjava2:rxandroid:2.0.1" 53 | //util 54 | implementation 'com.blankj:utilcode:1.9.3' 55 | //ffmpeg lib 56 | compile(name: 'FFmpegAndroid-0.3.2', ext: 'aar') 57 | } 58 | -------------------------------------------------------------------------------- /app/libs/FFmpegAndroid-0.3.2.aar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DiskyZhs/PinFilter/068e1e29f59062888eee33dcfe62fb1c0329c55e/app/libs/FFmpegAndroid-0.3.2.aar -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in D:\AndroidSdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | 19 | # Uncomment this to preserve the line number information for 20 | # debugging stack traces. 21 | #-keepattributes SourceFile,LineNumberTable 22 | 23 | # If you keep the line number information, uncomment this to 24 | # hide the original source file name. 25 | #-renamesourcefileattribute SourceFile 26 | -------------------------------------------------------------------------------- /app/src/Android视频滤镜添加硬解码方案.md: -------------------------------------------------------------------------------- 1 | title: Android 滤镜添加硬解码 2 | date: 2017-11-21 15:03:33 3 | tags: Android 音视频 滤镜 硬解码 4 | comments: true 5 | --- 6 | 7 | 由于工作的需求,研究过了一段时间的Android 的音视频播放渲染以及编辑方面的知识,这里就自己一些浅薄的了解对所了解做一个简单的介绍和记录,如有不对的地方请指正!同时也会记录下硬件解码的情况下完成滤镜的添加。 8 | 9 | 这里以MP4格式的视频作为介绍,因为在实际的移动开发中,传输播放渲染的基本都是MP4文件。 10 | 11 | 这里先介绍一些基础概念,当理解了这些,再去查看以及学习关于音视频的一些著名开源项目,如Grafika,ExoPlayer,IJKPlayer,GPUImage等开源项目学习会更加方便! 12 | 13 | 安利一下自己的库,基于MediaCodec的硬件解码的添加滤镜的库 14 | [PinFilter](https://github.com/DiskyZhs/PinFilter) 15 | 实现了滤镜的添加,预览,播放,录制功能。 16 | 17 | 18 | 19 | ## 基础概念## 20 | ### 音视轨,编解码,软硬解码### 21 | VideoTrack(视轨),AudioTrack(音轨)分别对应视频文件的画面和声音的数据存储,一般来说常见的MP4一般都是一条VideoTrack,一条AudioTrack。当然有一些左右声道,背景音或存在多条AudioTrack的情况存在。 22 |
23 | 无论是声音信息还是图像信息储存在MP4中都是经过**编码(encode)**的,可以理解为按照一定规则压缩过的。所以你要进行MP4的播放,就需要将图像信息和音频信息取出来,然后经过**解码(decode)**还原成原始的可供渲染和播放的音视频单元。 24 |
25 | 以上就是编解码的基础概念,事实上音视频数据在MP4中是以Box的形式存储的,感兴趣的可以自己搜索学习。而MP4无论播放还是处理音视轨都是单独分开的,所以这就需要你将音轨和视轨从MP4文件中分离出来,而这个分离的过程就称之为**解复用**,所以同理,将音视轨重新合在一起的过程就称之为**复用**。 26 |
27 | 所以可以看出来,我要进行视频播放需要做的就是解复用取得音视轨,然后分别进行解码还原成原始可显示的数据,接着去渲染和播放,解复用 -》解码 -》播放。 28 | 我们如果要进行视频编辑就需要,解复用 -》解码 -》处理数据-》编码-》复用生成MP4。 29 |
30 | 而使用代码运行,将Video/AudioTrack解码的方式称为**软解码**,软解码最出名的就是开源项目FFMPEG,基本上所有的视频领域都有它的影子。 31 | 同样,由于软解码/编码是运行在CPU中的,所以无论是在cpu,内存的使用率还是解码效率上面都可能存在一些限制和瓶颈。因此,ffmpeg的实际运用中的优化也是很重要的。 32 |
33 | 事实上,除了软件码以外,手机,PC等视频播放设备上都存在**硬件解码器**(一种寄存器)来专门做视频的播放工作,我们把调用硬件解码器进行解码的方式称为**硬解码**。硬解码无疑在效率上更加高效,但是由于各大厂商的缘故,对于硬件解码器都有自己的调整,所以在硬解码的时候,各种机器的适配就显得尤为重要。 34 |
35 | 实际情况下,真正成熟的视频SDK都是软硬结合同时在适配方面做了一些处理的。 36 | 这里只介绍Android 平台下的硬解码情况下,视频播放,编辑以及滤镜的添加。 37 | 着重说明VideoTrack的处理。 38 |
39 | 40 | ### Extractor,Codec,Muxer### 41 | 42 | Android 事实上提供了现成的API来进行视频编解码和复用的工作。这里只是对这些API做个简单介绍。 43 | 44 | - MediaExtractor (Android 4.1+),用来进行MP4的解复用工作。 45 | 46 | ` getTrackCount()` 与` getTrackFormat(index)` 来获取音视轨的数量和信息 47 | ` selectTrack()` 函数选择当前音视轨 48 | 49 | - MediaCodec (Android 4.1+),用来进行MP4的解码/编码。 50 | 51 | 这是视频播放以及重新编辑的核心类,用来调用底层的硬件解码器来编解码。 52 | MediaCodec的设计实际上是生产者消费者模式。如下 53 | 54 | ![这里写图片描述](http://img.blog.csdn.net/20171117112313637?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvemhzNDQzMDE2OQ==/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast) 55 | 56 | 57 | 通过` dequeueInputBuffer()` 函数来取得可被使用的寄存器Buffer,然后通过 58 | ` Extractor.readSampleData()` 将数据送入解码器,最后通过` dequeueOutputBufferr()` 来取出解码/编码后的数据。由于是生产消费者模式, 故你需要不断的查询Buffer的状态直至整个的编解码结束。 59 | 60 | 视频的编解码都是有MediaCodec这个类完成的,你可以创建不同的decoder和encoder。 61 | 62 | Codec作为Decoder(解码器)的时候,输出可以为Buffer或者Surface(后面会讲到,Surface其实也就是一个Raw data buffer),Surface作为输出的情况下,解码后的数据会直接送到Surface中,一般直接进行显示,这种情况下,你无法取得Buffer中的数据 63 | 64 | 同上Codec作为Encoder(编码器)的时候,输入数据源也可以为Buffer或者Surface。 65 | 66 | - MediaMuxer (Android 4.3+),用来进行重新生成MP4。 67 | 68 | ` addTrack()` 添加音视轨, ` writeSampleData()` 函数向音视轨中写入编码好的数据。 69 | 70 | - 所以硬解码的情况下: 71 | 72 | 视频播放:MediaExtractor(解复用) -》MediaCodec(Decoder解码) -》 73 | 数据处理(如果添加滤镜)-》渲染显示 74 | 75 | 视频编辑:MediaExtractor(解复用) -》MediaCodec(Decoder解码) -》 76 | 数据处理(如果添加滤镜)-》MediaCodec(Encoder编码) -》 MediaMuxer(复用,生成MP4) 77 | 78 | - 只是视频播放的,Android 4.1以上的设备就可以采取硬解码方案, 79 | 如果视频编辑,Android 4.3 以上的设备才可行。 80 | 目前Android 4.3以上设备覆盖率已达到90%+,硬解码方案是比较可行的。 81 | 同样是编解码,在高分辨率,高码率的情况下,硬解码有明显的速度优势。 82 | 83 | 84 | ### Frame,关键帧,H.264/AVC### 85 | 86 | **H.264** 是视频编解码器标准,也就是说视频数据是以h264标准进行压缩的, 87 | 无论是本地视频还是网络数据流传输都是H264标准。 88 | 89 | **帧(Frame)**就是video显示传输的数据单元,一帧可以理解为显示的完整的一副图像的数据。 90 | 91 | **FPS(Frame Pre Second)**每秒显示帧数,一般人眼只能识别24帧左右,也就说只要FPS大于24一般画面看起来就是流畅的。 92 | 93 | Extractor,Codec都是以一帧数据作为单位进行处理。当然帧与帧也有区别,根据h264标准,分别分为I帧,P帧,B帧。 94 | 95 | I帧是一种自带全部信息的独立帧,无需参考其他图像便可独立进行解码,可以简单理解为一张静态画面。视频序列中的第一个帧始终都是I帧,因为它是关键帧。 96 | 97 | P帧又称帧间预测编码帧,需要参考前面的I帧才能进行编码。表示的是当前帧画面与前一帧(前一帧可能是I帧也可能是P帧)的差别。解码时需要用之前缓存的画面叠加上本帧定义的差别,生成最终画面。 98 | 99 | B帧又称双向预测编码帧,也就是B帧记录的是本帧与前后帧的差别。也就是说要解码B帧,不仅要取得之前的缓存画面,还要解码之后的画面,通过前后画面的与本帧数据的叠加取得最终的画面。 100 | 101 | **关键帧** 及IDR帧,就是I帧,IDR帧的作用是立刻刷新,使错误不致传播,从IDR帧开始,重新算一个新的序列开始编码。所以在IDR帧之后的所有帧都不能引用任何IDR帧之前的帧的内容。 102 | 103 | 104 | ### Surface,SurfaceView,SurfaceTexure,TextureView### 105 | 106 | - Surface 107 | surface可以简单的理解为装有当前显示frame buffer,内存中的一段绘图缓冲区。每个Activity每个Layer都有一个Surface。 108 | 109 | - SurfaceView 110 | 111 | SurfaceView内部自己管理生成一个Surface作为渲染,所有SurfaceView视频播放的View载体。 112 | 这样的好处是对这个Surface的渲染可以放到单独线程去做,渲染时可以有自己的GL context。这对于一些游戏、视频等性能相关的应用非常有益,因为它不会影响主线程对事件的响应。但它也有缺点,因为这个Surface不在View hierachy中,它的显示也不受View的属性控制,所以不能进行平移,缩放等变换,也不能放在其它ViewGroup中,一些View中的特性也无法使用。 113 | 114 | ![这里写图片描述](http://img.blog.csdn.net/20171117164305836?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvemhzNDQzMDE2OQ==/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast) 115 | 116 | - GlSurfaceView 117 | 118 | GlSurfaceView作为SurfaceView的补充。它可以看作是SurfaceView的一种典型使用模式。在SurfaceView的基础上,它加入了EGL的管理,并自带了渲染线程。另外它定义了用户需要实现的Render接口,可以使用户调用OpenGl自定义渲染的过程。 119 | 120 | - SurfaceTexture 121 | 122 | SurfaceTexture对图像流的处理并不直接显示,而是转为GL外部纹理,因此可用于图像流数据的二次处理(如Camera滤镜,桌面特效等)。 123 | SurfaceTexture从图像流(来自Camera预览,视频解码,GL绘制场景等)中获得帧数据,当调用updateTexImage()时,根据内容流中最近的图像更新SurfaceTexture对应的GL纹理对象,接下来,就可以像操作普通GL纹理一样操作它了。 124 | SurfaceTexture.OnFrameAvailableListener用于让SurfaceTexture的使用者知道有新数据到来。 125 | SurfaceTexture中的attachToGLContext()和detachToGLContext()可以让多个GL context共享同一个内容源。 126 | SurfaceTexture对象可以在任何线程上创建。 updateTexImage()只能在包含纹理对象的OpenGL ES上下文的线程上调用。 在任意线程上调用frame-available回调函数,不与updateTexImage()在同一线程上出现。 127 | 128 | - TextureView 129 | 130 | SurfaceView由于使用的是独立的绘图层,并且使用独立的线程去进行绘制。所以SurfaceView不能进行Transition,Rotation,Scale等变换,这就导致一个问题SurfaceView在滑动的时候,SurfaceView的刷新由于不受主线程控制导致SurfaceView在滑动的时候会出现黑边的情况。 131 | 为了应对这种情况,所以Android 4.0 google推出了TextureView进行视频播放或者游戏渲染。 132 | 和SurfaceView不同,它不会在WMS中单独创建窗口,而是作为View hierachy中的一个普通View,因此可以和其它普通View一样进行移动,旋转,缩放,动画等变化。值得注意的是TextureView必须在硬件加速的窗口中。 133 | 134 | 135 | ### Yuv,OpenGl,GLSL### 136 | 137 | - Yuv,RGB 138 | 139 | Yuv是一种颜色格式,VideoTrack 经过 MediaCodec,decode解码出来的图像的格式就是Yuv420格式,事实上Yuv420格式也分为YV12,NV21等4种,具体可以参考 140 | [详解YUV数据格式](http://blog.csdn.net/beyond_cn/article/details/12998247) 141 | 142 | Codec解码出来的yuv格式其实4种都存在,具体信息可以在输出的MediaFormat中查到,造成这种差异也和Android厂商有一定的关系。 143 | 144 | RGB(ARGB8888或者RGB565)是被设备渲染所需要的颜色格式,所以当你Codec解码出来的数据需要转化为RGB然后进行显示.(当然你如果直接将Surface设置为Codec的输出的话,产生的yuv的数据会被系统直接转化为RGB格式(一般直接在GPU中直接处理了,更加高效)) 145 |
146 | 147 | - OpenGl,Egl,GLSL 148 | 149 | 这些都会涉及到计算机图形学的一些概念,个人知道的也很肤浅,只能简单的说说自己的理解。 150 | 151 | OpenGL是个与硬件无关的软件接口,定义了一系列图形渲染的操作,而嵌入式设备(Android 设备)都支持Opengl进行图形操作,同时Android sdk也为我们封装并实现了了OpenGl接口,这也就意味着我们可以通过调用Android原生API来自己修改预定义图像的渲染。(这些操作与修改是发生在GPU显存当中的,并不会大量的消耗cpu与内存,大大的优化了视频播放的体验),OpenGL ES是opengl 在Android平台上使用的版本 152 | 153 | Embedded Graphics Library (EGL)是连接OpenGL ES和本地窗口系统的接口,由于OpenGL ES是跨平台的,引入EGL就是为了屏蔽不同平台上的区别。本地窗口相关的API提供了访问本地窗口系统的接口,EGL提供了创建渲染表面,接下来OpenGL ES就可以在这个渲染表面上绘制,同时提供了图形上下文,用来进行状态管理。 154 | 155 | ![这里写图片描述](http://img.blog.csdn.net/20171117151212193?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvemhzNDQzMDE2OQ==/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast) 156 | 157 | GLSL(GLslang)是官方的opengl着色语言的简称(OpenGL Shading Language)。 GLSL是类似于C/C++的高级语言,适用于一部分显卡。使用GLSL,你能够编写一些短小的程序,称为着色器(shader),这些着色器在GPU上运行。 158 | 简单的理解就是GLSL就是加载在GPU中操作图形显示的操作语言。 159 | 160 | 简单理解就是EGL是关联硬件设备,给opengl的运行创建环境的同时对opengl的生命周期进行管理。GLSL是被opengl调用运行在gpu中的图像渲染处理的程序语言,用来处理图像旋转,添加滤镜等一系列渲染特效。 161 | 162 | Opengl是一门复杂的知识体系,深入研究使用需要大量的时间和专业的人才,当然我们如果只是进行一些滤镜的开发等肤浅的应用并不需要了解的那么多,后续会向大家介绍这部分开发。 163 | 164 | 最后大概知识路径如下 165 | 166 | ![这里写图片描述](http://img.blog.csdn.net/20171117191322676?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvemhzNDQzMDE2OQ==/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast) 167 | 168 | -------------------------------------------------------------------------------- /app/src/Camera滤镜的添加.md: -------------------------------------------------------------------------------- 1 | title: Camera Filter 美颜相机的实现 2 | date: 2017-11-22 22:36:23 3 | tags: android camera filter recoeder 4 | comments: true 5 | --- 6 | 7 | # 前言 8 | 9 | 当你需要实现Camera 添加滤镜的预览以及录制的时候,那么你可以看过来了!
10 | 在这里会给你讲解基于opengl的Camera滤镜的实现。 11 | 12 | 之前已经介绍过了视频编解码以及渲染的相关概念了,详见 13 | [Android视频滤镜添加硬解码方案](http://blog.csdn.net/zhs4430169/article/details/76502217) 14 | 15 | 当然如果你只是想要简单的进行Camera预览以及拍照的话,推荐google的CameraView,简单兼容性好。 16 | [轻松玩转Camera,使用CameraView来拍照,修改CameraView 实现自定义拍照分辨率](http://blog.csdn.net/zhs4430169/article/details/76502217) 17 | 18 | 如果需要Camera的预览以及录制,并不需要添加滤镜特效的话,那么原生API,Camera + MediaRecorder就能搞定。
19 | 推荐开源项目[opencamera](https://opencamera.sourceforge.io/) 20 | 21 | 对于Camera Filter的添加以及MP4视屏Filter的添加,自己这里写了一个开源项目 22 | [PinFilter](https://github.com/DiskyZhs/PinFilter) 23 | 你可以直接使用。 24 | 25 | 26 | 27 | # 原理 28 | 29 | Camera的setPreview设置Preview输出的时候可以是SurfaceHolder也可以是SurfaceTexture,必须使用SurfaceTexture, 30 | 只有通过SurfaceTexture获取到Camera输出数据然后一方面通过OpenGl对输出的图像进行加工然后渲染,这一部分的实现 31 | 可以参考Grafika的MoviePlayer的实现。 32 | 当你需要录制添加滤镜的视频的时候,你必须在同一个Opengl环境下也就是EGl对象下面将SurfaceTexture中的texture通过 33 | opengl渲染然后送给MediaCodec(Encoder)进行编码然后送入MediaMuxer重新生成MP4视频文件。 34 | 35 | 36 | 为什么不用SurfaceHolder作为输出? 37 | SurfaceHolder是关联SurfaceView,Camera的数据会直接送给SurfaceView的Surface 38 | 进行渲染,无法取得图像数据进行再处理。 39 | 40 | 41 | 为什么不在Camera的onPreviewFrame(byte[] data, Camera camera)取得图像数据,然后进行处理图像添加滤镜再送去渲染? 42 | 原因是效率问题,onPreviewFrame回调中取得的是Yuv格式的图像,而送入渲染的无论是SurfaceView还是Canvas绘制等 43 | 方法需要的是RGB的颜色格式,这也就意味着要实现这些必须先将Yuv颜色格式转化为RGB,然后将RGB进行添加滤镜处理 44 | 然后渲染,无论这部分转化处理操作是Java实现还是利用Jni,C来实现(事实上快一点)都会比较慢,同时这些处理是 45 | 跑在Cpu上面的,内存里面的,也就意味着更大的资源消耗。 46 | 而利用Opengl对SurfaceTexture中的图像数据进行处理采用GLSL语言,这一切都是在gpu中完成的,所以这无疑是效率 47 | 最高的方式。 48 | 49 | 为什么使用MediaCodec以及MediaMuxer? 50 | 因为MediaCodec是属于硬件编解码,效率最高!但是需要Android 4.3+支持,当然你也可以使用ffmpeg等软编码方案来 51 | 替代后半部分的录制功能! 52 | 53 | SurfaceView,GlSurfaceView还是TextureView作为Camera Preview的渲染显示? 54 | 首先由于需要支持Opengl所以GlSurfaceView无疑是最优先的选择,但是TextureView拥有View的平移旋转等特性,更关 55 | 键的是在ScrollView,ViewPager等控件中只有TextureView才不会出现滑动黑边等问题,所以TextureView才是视频渲染 56 | 最好的载体。 57 | 为了使TextureView支持OpenGl,就必须仿照GlSurfaceView自己进行创建EGL,创建GlThread进行渲染,具体的实现可以 58 | 参考 59 | [GlTextureView](https://github.com/DiskyZhs/PinFilter/blob/master/libRecorderEditor/src/main/java/com/pinssible/librecorder/view/GLTextureView.java) 60 | [gles](https://github.com/DiskyZhs/PinFilter/tree/master/libRecorderEditor/src/main/java/com/pinssible/librecorder/gles) 61 | 或者开源项目[android-openGL-canvas](https://github.com/ChillingVan/android-openGL-canvas) 62 | 63 | # 实现 64 | 65 | 无论是GlSurfaceView还是GlTextureView的核心都是实现Render接口,即 66 | ```java 67 | void onSurfaceCreated(); 68 | void onSurfaceChanged(); 69 | void onDrawFrame(); 70 | ``` 71 | 72 | 也就是在**onSurfaceCreated**与**onSurfaceChanged**进行初始化操作,在**onDrawFrame** 73 | 中完成对与图像的绘制,滤镜添加以及视频录制功能的实现。 74 | 75 | 话不多说,直接上Render的代码核心代码,要想了解全部的代码可以去 76 | [PinFilter](https://github.com/DiskyZhs/PinFilter) 找。 77 | 78 | 79 | # 最后 80 | 感觉非常抱歉,本来是将相关的核心的代码贴出来,逐一进行分析的,实际写的时候发现,要贴出的代码过多,而以我苍白的语言 81 | 怎么也解释不完全,雾里看花水里看月!所以推荐大家还是分析代码吧!关键部分都有注释! 82 | 83 | Camera这一部分的实现实际上是对开源项目 84 | [kickflip-android-sdk](https://github.com/Kickflip/kickflip-android-sdk)以及 85 | [android-gpuimage](https://github.com/CyberAgent/android-gpuimage)的借鉴和再封装, 86 | 特别感谢! -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 12 | 13 | 14 | 15 | 16 | 19 | 20 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /app/src/main/assets/test.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DiskyZhs/PinFilter/068e1e29f59062888eee33dcfe62fb1c0329c55e/app/src/main/assets/test.mp4 -------------------------------------------------------------------------------- /app/src/main/cpp/Test.cpp: -------------------------------------------------------------------------------- 1 | // 2 | // Created by 54340 on 2017/11/7. 3 | // 4 | #include 5 | #include 6 | 7 | extern "C" 8 | JNIEXPORT jstring JNICALL Java_com_pinssible_camerarecorder_camerarecorderdemo_FfmpegTestActivity_helloTest 9 | (JNIEnv * env, jobject){ 10 | std::string hello = "Hello from C++"; 11 | return env->NewStringUTF(hello.c_str()); 12 | } 13 | -------------------------------------------------------------------------------- /app/src/main/java/com/pinssible/camerarecorder/camerarecorderdemo/FfmpegTestActivity.java: -------------------------------------------------------------------------------- 1 | package com.pinssible.camerarecorder.camerarecorderdemo; 2 | 3 | import android.os.Environment; 4 | import android.support.v7.app.AppCompatActivity; 5 | import android.os.Bundle; 6 | import android.util.Log; 7 | import android.widget.TextView; 8 | 9 | import com.github.hiteshsondhi88.libffmpeg.ExecuteBinaryResponseHandler; 10 | import com.github.hiteshsondhi88.libffmpeg.FFmpeg; 11 | import com.github.hiteshsondhi88.libffmpeg.LoadBinaryResponseHandler; 12 | import com.github.hiteshsondhi88.libffmpeg.exceptions.FFmpegCommandAlreadyRunningException; 13 | import com.github.hiteshsondhi88.libffmpeg.exceptions.FFmpegNotSupportedException; 14 | 15 | public class FfmpegTestActivity extends AppCompatActivity { 16 | private final String TAG = "FfmpegTestActivity"; 17 | 18 | static { 19 | System.loadLibrary("ffmpeg"); 20 | } 21 | 22 | private TextView testTextView; 23 | 24 | @Override 25 | protected void onCreate(Bundle savedInstanceState) { 26 | super.onCreate(savedInstanceState); 27 | setContentView(R.layout.activity_ffmpeg_test); 28 | testTextView = findViewById(R.id.tv_test); 29 | testTextView.setText(helloTest()); 30 | 31 | //Load ffmpeg 32 | loadFfmpeg(); 33 | } 34 | 35 | 36 | public native String helloTest(); 37 | 38 | private void loadFfmpeg() { 39 | FFmpeg ffmpeg = FFmpeg.getInstance(this); 40 | try { 41 | ffmpeg.loadBinary(new LoadBinaryResponseHandler() { 42 | 43 | @Override 44 | public void onStart() { 45 | Log.e(TAG, "load ffmpeg onStart"); 46 | } 47 | 48 | @Override 49 | public void onFailure() { 50 | Log.e(TAG, "load ffmpeg onFailure"); 51 | } 52 | 53 | @Override 54 | public void onSuccess() { 55 | Log.e(TAG, "load ffmpeg onSuccess"); 56 | } 57 | 58 | @Override 59 | public void onFinish() { 60 | Log.e(TAG, "load ffmpeg onFinish"); 61 | queryVideoInfo(); 62 | } 63 | }); 64 | } catch (FFmpegNotSupportedException e) { 65 | Log.e(TAG, "load ffmpeg exception = " + e.toString()); 66 | } 67 | } 68 | 69 | 70 | private void queryVideoInfo() { 71 | String outputPath = Environment.getExternalStorageDirectory().getAbsolutePath() + "/test.mp4"; 72 | String[] cmd = {"-version"}; 73 | FFmpeg ffmpeg = FFmpeg.getInstance(this); 74 | try { 75 | // to execute "ffmpeg -version" command you just need to pass "-version" 76 | ffmpeg.execute(cmd, new ExecuteBinaryResponseHandler() { 77 | 78 | @Override 79 | public void onStart() { 80 | Log.e(TAG, "queryVideoInfo onStart"); 81 | } 82 | 83 | @Override 84 | public void onProgress(String message) { 85 | Log.e(TAG, "queryVideoInfo onProgress msg = " + message); 86 | } 87 | 88 | @Override 89 | public void onFailure(String message) { 90 | Log.e(TAG, "queryVideoInfo onFailure msg = " + message); 91 | } 92 | 93 | @Override 94 | public void onSuccess(String message) { 95 | Log.e(TAG, "queryVideoInfo onSuccess msg = " + message); 96 | } 97 | 98 | @Override 99 | public void onFinish() { 100 | Log.e(TAG, "queryVideoInfo onFinish"); 101 | } 102 | }); 103 | } catch (FFmpegCommandAlreadyRunningException e) { 104 | // Handle if FFmpeg is already running 105 | Log.e(TAG, "queryVideoInfo exception = " + e.toString()); 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /app/src/main/java/com/pinssible/camerarecorder/camerarecorderdemo/FunctionViewBuilder.java: -------------------------------------------------------------------------------- 1 | package com.pinssible.camerarecorder.camerarecorderdemo; 2 | 3 | import android.app.Activity; 4 | import android.content.Context; 5 | import android.view.View; 6 | import android.widget.TextView; 7 | 8 | /** 9 | * Created by ZhangHaoSong on 2017/11/16. 10 | */ 11 | 12 | public class FunctionViewBuilder { 13 | public static View getFunctionView(Activity context, String name, String description, View.OnClickListener listener) { 14 | View functionView = context.getLayoutInflater().inflate(R.layout.layout_function_item, null); 15 | ((TextView) functionView.findViewById(R.id.tv_name)).setText(name); 16 | ((TextView) functionView.findViewById(R.id.tv_description)).setText(description); 17 | functionView.setOnClickListener(listener); 18 | return functionView; 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /app/src/main/java/com/pinssible/camerarecorder/camerarecorderdemo/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.pinssible.camerarecorder.camerarecorderdemo; 2 | 3 | import android.Manifest; 4 | import android.content.Intent; 5 | import android.content.pm.PackageManager; 6 | import android.support.v4.app.ActivityCompat; 7 | import android.support.v7.app.AppCompatActivity; 8 | import android.os.Bundle; 9 | import android.view.View; 10 | import android.view.ViewGroup; 11 | import android.widget.LinearLayout; 12 | 13 | import com.blankj.utilcode.util.SizeUtils; 14 | import com.blankj.utilcode.util.ToastUtils; 15 | import com.blankj.utilcode.util.Utils; 16 | 17 | public class MainActivity extends AppCompatActivity { 18 | private LinearLayout container; 19 | 20 | private static final int REQUEST_EXTERNAL_STORAGE = 1; 21 | private static final int REQUEST_CAMERA = 2; 22 | private static final int REQUEST_AUDIO = 3; 23 | private static String[] PERMISSIONS_STORAGE = { 24 | Manifest.permission.READ_EXTERNAL_STORAGE, 25 | Manifest.permission.WRITE_EXTERNAL_STORAGE}; 26 | 27 | private static String[] PERMISSIONS_CAMERA = { 28 | Manifest.permission.CAMERA}; 29 | 30 | private static String[] PERMISSIONS_MIRCO = { 31 | Manifest.permission.RECORD_AUDIO}; 32 | 33 | @Override 34 | protected void onCreate(Bundle savedInstanceState) { 35 | Utils.init(getApplication()); 36 | super.onCreate(savedInstanceState); 37 | setContentView(R.layout.activity_main); 38 | 39 | container = findViewById(R.id.main_container); 40 | 41 | // Check if we have write permission 42 | checkoPermission(); 43 | 44 | LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT); 45 | lp.height = SizeUtils.dp2px(75); 46 | 47 | container.addView( 48 | FunctionViewBuilder.getFunctionView(this 49 | , "CameraCaptureActivity" 50 | , "Camera Preview and Recorder with Filter" 51 | , new View.OnClickListener() { 52 | @Override 53 | public void onClick(View v) { 54 | if (isPermissionContain()) { 55 | startActivity(new Intent(MainActivity.this, CameraCaptureActivity.class)); 56 | } 57 | } 58 | }), lp 59 | ); 60 | 61 | container.addView( 62 | FunctionViewBuilder.getFunctionView(this 63 | , "PlayerActivity" 64 | , "Play Video with filter" 65 | , new View.OnClickListener() { 66 | @Override 67 | public void onClick(View v) { 68 | if (isPermissionContain()) { 69 | startActivity(new Intent(MainActivity.this, PlayerActivity.class)); 70 | } 71 | } 72 | }), lp 73 | ); 74 | 75 | container.addView( 76 | FunctionViewBuilder.getFunctionView(this 77 | , "RemuxerActivity" 78 | , "Generate mp4 with filter according to other mp4" 79 | , new View.OnClickListener() { 80 | @Override 81 | public void onClick(View v) { 82 | if (isPermissionContain()) { 83 | startActivity(new Intent(MainActivity.this, TestRemuxerActivity.class)); 84 | } 85 | } 86 | }), lp 87 | ); 88 | } 89 | 90 | 91 | private void checkoPermission() { 92 | if (ActivityCompat.checkSelfPermission(this, 93 | Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { 94 | ActivityCompat.requestPermissions(this, PERMISSIONS_STORAGE, 95 | REQUEST_EXTERNAL_STORAGE); 96 | } 97 | if (ActivityCompat.checkSelfPermission(this, 98 | Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) { 99 | ActivityCompat.requestPermissions(this, PERMISSIONS_CAMERA, 100 | REQUEST_CAMERA); 101 | } 102 | if (ActivityCompat.checkSelfPermission(this, 103 | Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { 104 | ActivityCompat.requestPermissions(this, PERMISSIONS_MIRCO, 105 | REQUEST_AUDIO); 106 | } 107 | } 108 | 109 | 110 | private boolean isPermissionContain() { 111 | if (ActivityCompat.checkSelfPermission(MainActivity.this, 112 | Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED && 113 | ActivityCompat.checkSelfPermission(MainActivity.this, 114 | Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED && 115 | ActivityCompat.checkSelfPermission(MainActivity.this, 116 | Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED) { 117 | return true; 118 | } else { 119 | ToastUtils.showShort("Please open write storage,camera and record audio permission!"); 120 | return false; 121 | } 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /app/src/main/java/com/pinssible/camerarecorder/camerarecorderdemo/PlayerActivity.java: -------------------------------------------------------------------------------- 1 | package com.pinssible.camerarecorder.camerarecorderdemo; 2 | 3 | import android.Manifest; 4 | import android.app.Activity; 5 | import android.graphics.Rect; 6 | import android.net.Uri; 7 | import android.opengl.GLSurfaceView; 8 | import android.os.Bundle; 9 | import android.os.Environment; 10 | import android.support.v7.app.AppCompatActivity; 11 | import android.util.DisplayMetrics; 12 | import android.util.Log; 13 | import android.view.View; 14 | import android.view.ViewTreeObserver; 15 | import android.view.Window; 16 | import android.view.WindowManager; 17 | import android.widget.Button; 18 | import android.widget.RelativeLayout; 19 | 20 | import com.blankj.utilcode.util.BarUtils; 21 | import com.blankj.utilcode.util.PermissionUtils; 22 | import com.blankj.utilcode.util.ToastUtils; 23 | import com.pinssible.librecorder.player.PinMediaPlayer; 24 | import com.pinssible.librecorder.player.SimplePinPlayerView; 25 | 26 | import java.security.Permission; 27 | 28 | public class PlayerActivity extends Activity { 29 | //view 30 | private SimplePinPlayerView previewSurface; 31 | private Button filterBtn; 32 | 33 | //player 34 | private PinMediaPlayer player; 35 | 36 | private int filterType = 1; 37 | 38 | @Override 39 | protected void onCreate(Bundle savedInstanceState) { 40 | super.onCreate(savedInstanceState); 41 | this.requestWindowFeature(Window.FEATURE_NO_TITLE); 42 | this.getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN); 43 | setContentView(R.layout.activity_player); 44 | 45 | //view 46 | previewSurface = findViewById(R.id.player_view); 47 | filterBtn = findViewById(R.id.btn_filter); 48 | 49 | //player 50 | String outputPath = "file:///android_asset/test.mp4"; 51 | Uri source = Uri.parse(outputPath); 52 | try { 53 | player = new PinMediaPlayer(this, source, true); 54 | previewSurface.setPlayer(player); 55 | } catch (Exception e) { 56 | e.printStackTrace(); 57 | ToastUtils.showShort("create player fail cause = " + e.toString()); 58 | } 59 | 60 | 61 | //filter 62 | filterBtn.setOnClickListener(new View.OnClickListener() { 63 | @Override 64 | public void onClick(View view) { 65 | int type = (filterType++) % 17; 66 | Log.e("Test", "filterType = " + type); 67 | player.setFilter(type); 68 | } 69 | }); 70 | 71 | } 72 | 73 | } 74 | -------------------------------------------------------------------------------- /app/src/main/java/com/pinssible/camerarecorder/camerarecorderdemo/TestRemuxerActivity.java: -------------------------------------------------------------------------------- 1 | package com.pinssible.camerarecorder.camerarecorderdemo; 2 | 3 | import android.Manifest; 4 | import android.app.Activity; 5 | import android.content.pm.PackageManager; 6 | import android.content.res.AssetFileDescriptor; 7 | import android.graphics.Color; 8 | import android.os.Bundle; 9 | import android.os.Environment; 10 | import android.support.v4.app.ActivityCompat; 11 | import android.support.v4.graphics.ColorUtils; 12 | import android.text.method.ScrollingMovementMethod; 13 | import android.util.Log; 14 | import android.view.View; 15 | import android.view.Window; 16 | import android.view.WindowManager; 17 | import android.widget.ArrayAdapter; 18 | import android.widget.Button; 19 | import android.widget.MultiAutoCompleteTextView; 20 | import android.widget.ProgressBar; 21 | import android.widget.Spinner; 22 | import android.widget.TextView; 23 | 24 | import com.blankj.utilcode.util.ToastUtils; 25 | import com.blankj.utilcode.util.Utils; 26 | import com.pinssible.librecorder.filter.Filters; 27 | import com.pinssible.librecorder.remux.PinRemuxer; 28 | import com.pinssible.librecorder.remux.RemuxerFactory; 29 | 30 | import java.io.File; 31 | import java.io.IOException; 32 | import java.io.InputStream; 33 | import java.util.ArrayList; 34 | import java.util.Collections; 35 | import java.util.Comparator; 36 | import java.util.HashMap; 37 | import java.util.List; 38 | 39 | public class TestRemuxerActivity extends Activity implements RemuxerFactory.OnRemuxListener { 40 | private final String TAG = "TestRemuxerActivity"; 41 | 42 | //View 43 | private TextView srcText, dstText; 44 | private Button startBtn; 45 | private Spinner filterSpanner; 46 | private ProgressBar progressBar; 47 | private TextView processTv; 48 | 49 | //path 50 | String dstPath = Environment.getExternalStorageDirectory().getAbsolutePath() + "/20171109.mp4"; 51 | 52 | //other 53 | private HashMap filtersMap; 54 | 55 | private boolean isMuxing = false; 56 | private PinRemuxer remuxer; 57 | 58 | @Override 59 | protected void onCreate(Bundle savedInstanceState) { 60 | super.onCreate(savedInstanceState); 61 | this.requestWindowFeature(Window.FEATURE_NO_TITLE); 62 | this.getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN); 63 | setContentView(R.layout.layout_test_texture_view_output); 64 | 65 | //view 66 | initView(); 67 | } 68 | 69 | 70 | private void initView() { 71 | srcText = findViewById(R.id.tv_src_path); 72 | dstText = findViewById(R.id.tv_dst_path); 73 | startBtn = findViewById(R.id.btn_start); 74 | filterSpanner = findViewById(R.id.spinner_filter); 75 | progressBar = findViewById(R.id.progress_remux); 76 | processTv = findViewById(R.id.mulit_tv_process); 77 | processTv.setMovementMethod(ScrollingMovementMethod.getInstance()); 78 | 79 | initFilters(); 80 | //spinner 81 | List filters = new ArrayList<>(filtersMap.values()); 82 | Collections.sort(filters, new Comparator() { 83 | @Override 84 | public int compare(String o1, String o2) { 85 | return getFilter(o1) - getFilter(o2); 86 | } 87 | }); 88 | ArrayAdapter filterAdapter = new ArrayAdapter(this, R.layout.layout_filter_item, R.id.tv_filter, filters); 89 | filterSpanner.setAdapter(filterAdapter); 90 | 91 | //text 92 | srcText.setText("Asset://test.mp4"); 93 | dstText.setText(dstPath); 94 | 95 | //btn 96 | startBtn.setOnClickListener(new View.OnClickListener() { 97 | @Override 98 | public void onClick(View v) { 99 | if (isMuxing) { 100 | remuxer.stop(); 101 | startBtn.setText("Start"); 102 | isMuxing = false; 103 | } else { 104 | startRemux(); 105 | startBtn.setText("Stop"); 106 | isMuxing = true; 107 | } 108 | } 109 | }); 110 | 111 | } 112 | 113 | private void initFilters() { 114 | filtersMap = new HashMap<>(); 115 | filtersMap.put(Filters.FILTER_NONE, "FILTER_NONE".toLowerCase()); 116 | filtersMap.put(Filters.FILTER_BLACK_WHITE, "FILTER_BLACK_WHITE".toLowerCase()); 117 | filtersMap.put(Filters.FILTER_NIGHT, "FILTER_NIGHT".toLowerCase()); 118 | filtersMap.put(Filters.FILTER_CHROMA_KEY, "FILTER_CHROMA_KEY".toLowerCase()); 119 | filtersMap.put(Filters.FILTER_BLUR, "FILTER_BLUR".toLowerCase()); 120 | filtersMap.put(Filters.FILTER_SHARPEN, "FILTER_SHARPEN".toLowerCase()); 121 | filtersMap.put(Filters.FILTER_EDGE_DETECT, "FILTER_EDGE_DETECT".toLowerCase()); 122 | filtersMap.put(Filters.FILTER_EMBOSS, "FILTER_EMBOSS".toLowerCase()); 123 | filtersMap.put(Filters.FILTER_SQUEEZE, "FILTER_SQUEEZE".toLowerCase()); 124 | filtersMap.put(Filters.FILTER_TWIRL, "FILTER_TWIRL".toLowerCase()); 125 | filtersMap.put(Filters.FILTER_TUNNEL, "FILTER_TUNNEL".toLowerCase()); 126 | filtersMap.put(Filters.FILTER_BULGE, "FILTER_BULGE".toLowerCase()); 127 | filtersMap.put(Filters.FILTER_DENT, "FILTER_DENT".toLowerCase()); 128 | filtersMap.put(Filters.FILTER_FISHEYE, "FILTER_FISHEYE".toLowerCase()); 129 | filtersMap.put(Filters.FILTER_STRETCH, "FILTER_STRETCH".toLowerCase()); 130 | filtersMap.put(Filters.FILTER_MIRROR, "FILTER_MIRROR".toLowerCase()); 131 | } 132 | 133 | private int getFilter(String filterName) { 134 | if (filtersMap.containsValue(filterName)) { 135 | for (int filter : filtersMap.keySet()) { 136 | if (filtersMap.get(filter).equals(filterName)) { 137 | return filter; 138 | } 139 | } 140 | } 141 | return Filters.FILTER_NONE; 142 | } 143 | 144 | private void startRemux() { 145 | File dstFile = new File(dstPath); 146 | if (!dstFile.exists()) { 147 | try { 148 | dstFile.createNewFile(); 149 | } catch (IOException e) { 150 | e.printStackTrace(); 151 | } 152 | } 153 | 154 | //src 155 | try { 156 | AssetFileDescriptor asd = getAssets().openFd("test.mp4"); 157 | //remuxer 158 | remuxer = new PinRemuxer(asd, dstPath, this); 159 | remuxer.start(getFilter(((String) filterSpanner.getSelectedItem()).toLowerCase())); 160 | } catch (IOException e) { 161 | e.printStackTrace(); 162 | } 163 | 164 | 165 | 166 | } 167 | 168 | @Override 169 | public void onRemuxStart(final long totalPts) { 170 | Log.e(TAG, "onRemuxStart totalPts = " + totalPts); 171 | runOnUiThread(new Runnable() { 172 | @Override 173 | public void run() { 174 | processTv.setText("\n Remux Start"); 175 | processTv.append("\n Video duration = " + totalPts); 176 | progressBar.setMax((int) (totalPts / 1000)); 177 | } 178 | }); 179 | } 180 | 181 | @Override 182 | public void onRemuxProcess(final long pts) { 183 | Log.e(TAG, "onRemuxProcess pts = " + pts); 184 | runOnUiThread(new Runnable() { 185 | @Override 186 | public void run() { 187 | progressBar.setProgress((int) (pts / 1000)); 188 | processTv.append("\n Remuxing current pts = " + pts); 189 | if (processTv.getLineCount() * processTv.getLineHeight() - processTv.getHeight() > 0) 190 | processTv.scrollTo(0, processTv.getLineCount() * processTv.getLineHeight() - processTv.getHeight()); 191 | } 192 | }); 193 | } 194 | 195 | @Override 196 | public void onRemuxFinish() { 197 | Log.e(TAG, "onRemuxFinish"); 198 | runOnUiThread(new Runnable() { 199 | @Override 200 | public void run() { 201 | isMuxing = false; 202 | startBtn.setText("Start"); 203 | ToastUtils.showShort("Remux Success!"); 204 | processTv.append("\n Remux finish"); 205 | processTv.scrollTo(0, processTv.getLineCount() * processTv.getLineHeight() - processTv.getHeight()); 206 | progressBar.setProgress(0); 207 | } 208 | }); 209 | } 210 | 211 | @Override 212 | public void onRemuxFail(final Exception e) { 213 | Log.e(TAG, "onRemuxFail e = " + e.toString()); 214 | runOnUiThread(new Runnable() { 215 | @Override 216 | public void run() { 217 | isMuxing = false; 218 | startBtn.setText("Start"); 219 | ToastUtils.showShort("Remux fail ! cause of e = " + e.toString()); 220 | processTv.append("\n Remux fail cause e = " + e.toString()); 221 | processTv.scrollTo(0, processTv.getLineCount() * processTv.getLineHeight() - processTv.getHeight()); 222 | } 223 | }); 224 | } 225 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 12 | 17 | 22 | 27 | 32 | 37 | 42 | 47 | 52 | 57 | 62 | 67 | 72 | 77 | 82 | 87 | 92 | 97 | 102 | 107 | 112 | 113 | 114 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_camera.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | 16 | 17 | 18 | 22 | 23 | 26 | 27 |